Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4172f5d
fix(weather): correct Open-Meteo units, feels-like, hourly icons and …
jtn0123 Aug 18, 2026
414dcbc
fix(display): drive epd3in7-class panels instead of failing on them
jtn0123 Aug 18, 2026
f446cde
fix(plugins): resolve background colours against the target image mode
jtn0123 Aug 18, 2026
44b4a6a
fix(refresh): gate the systemd watchdog on refresh-loop progress
jtn0123 Aug 18, 2026
e2004f6
feat(install): verify updates are serving, and roll back unattended
jtn0123 Aug 18, 2026
63a1432
feat(refresh): record crash breadcrumbs and quarantine the culprit pl…
jtn0123 Aug 18, 2026
ba304b9
feat(plugins): skip-a-turn hook, image-less plugins, screenshot optio…
jtn0123 Aug 18, 2026
5e7e657
feat(images): add Auto fit mode with a central padImage -> fitMode mi…
jtn0123 Aug 18, 2026
796a27c
fix(stats): stop counting every successful refresh as an error
jtn0123 Aug 18, 2026
3aaee02
fix(ui): unclip the sidebar nav, and correct three misleading labels
jtn0123 Aug 18, 2026
a6ee47e
test: add a simulation tier and fix the silently-skipping systemd gate
jtn0123 Aug 18, 2026
98cff06
test: annotate the new test files to satisfy the mypy tests ratchet
jtn0123 Aug 18, 2026
b6d5ce4
fix: address CodeRabbit review findings on PR #632
jtn0123 Aug 18, 2026
392c89a
ci: run the systemd container gates once instead of across the matrix
jtn0123 Aug 18, 2026
d9580ca
ci: add a manual workflow to refresh visual baselines on a CI runner
jtn0123 Aug 18, 2026
d21871c
docs: surround the skip_display_condition example with blank lines (M…
jtn0123 Aug 18, 2026
57dd71d
fix: restore Open-Meteo wind and count rollback in start-limit events
jtn0123 Aug 18, 2026
c367554
fix: never skip a user-requested refresh, and harden breadcrumb inputs
jtn0123 Aug 18, 2026
cc58c2b
docs: record the Open-Meteo timezone finding as a tracked follow-up
jtn0123 Aug 18, 2026
ac77331
test: refresh the dashboard-desktop layout baseline from a CI render
jtn0123 Aug 18, 2026
2699029
fix(deps): clear all 45 known vulnerabilities in the lockfiles
jtn0123 Aug 21, 2026
5212659
docs: track the SonarCloud S2083 finding as a reviewed false positive
jtn0123 Aug 21, 2026
2689ec3
refactor: address the valid SonarCloud findings on new code
jtn0123 Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,11 @@ jobs:
# because REQUIRE_BROWSER_SMOKE is not set (they need Playwright
# Chromium which isn't installed in this job). The browser-smoke
# job runs them with a real browser.
pytest --cov --cov-report=xml -q
# Container tests are excluded here and run once in the dedicated
# `Container tests` job below: they boot systemd as PID 1, which does
# not vary by Python version, so running them across the matrix only
# triples the cost. Including them pushed 3.13 past the 20-minute cap.
pytest --cov --cov-report=xml -q -m "not container"
- name: Upload coverage
if: always()
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -745,15 +749,21 @@ jobs:
if-no-files-found: ignore

install-crash-loop-gate:
name: Install crash-loop regression gate
name: Systemd container gates
# JTN-614: runs the JTN-609 Docker-based regression gate that verifies
# JTN-600 (systemctl disable during install) and JTN-607 (install-in-progress
# lockfile) both prevent a mid-install crash from spawning a restart loop
# that would OOM a Pi Zero 2 W. The test auto-skips without Docker, so we
# set REQUIRE_INSTALL_CRASH_LOOP_TEST=1 to force it on CI.
#
# Now runs every `container`-marked test, which also covers
# test_boot_health_under_systemd.py (OnFailure= -> boot-health.sh ->
# rollback). Those are excluded from the pytest matrix: systemd behaviour
# does not vary by Python version, so running them three times only
# triples the cost — it pushed 3.13 past its 20-minute cap.
needs: tests
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -768,12 +778,12 @@ jobs:
run: |
python -m pip install --upgrade pip wheel
pip install -r install/requirements.txt -r install/requirements-dev.txt
- name: Run install crash-loop regression gate
- name: Run systemd container gates
env:
REQUIRE_INSTALL_CRASH_LOOP_TEST: '1'
PYTHONPATH: src
run: |
pytest tests/integration/test_install_crash_loop.py -v
pytest -m container -v

ci-gate:
name: CI gate (all checks pass)
Expand Down
89 changes: 89 additions & 0 deletions .github/workflows/refresh-visual-baselines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Refresh visual baselines

# Regenerate layout/plugin snapshot baselines in the *same* environment the
# `Browser smoke` job renders them in, and upload the PNGs as an artifact to
# download and commit.
#
# Why this exists: the baselines are pixel comparisons and are documented as
# reproducible only on Linux x86_64 with ubuntu-24.04's font set
# (tests/snapshots/README.md). That leaves a contributor on Apple Silicon with
# no way to refresh them after an intentional CSS change — the documented
# `--platform linux/amd64` docker one-liner installs fine but Chromium SIGABRTs
# under emulation. Rather than commit baselines that cannot be verified, render
# them on the same runner CI compares against.
#
# Usage:
# gh workflow run refresh-visual-baselines.yml --ref <branch>
# gh run download <run-id> -n refreshed-visual-baselines
# # copy over tests/snapshots/, inspect the diff, commit

on:
workflow_dispatch:
inputs:
target:
description: 'Which baselines to regenerate'
required: true
default: 'layout'
type: choice
options:
- layout
- plugins
- both

jobs:
refresh:
name: Refresh baselines
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: |
install/requirements.txt
install/requirements-dev.txt
- name: Install OS dependencies
# Matches the browser-smoke job — the font set is what makes these
# baselines reproducible.
run: |
sudo apt-get update
sudo apt-get install -y \
libopenjp2-7 \
libopenblas-dev \
libfreetype6-dev \
fonts-noto-color-emoji
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip wheel
pip install -r install/requirements.txt -r install/requirements-dev.txt
- name: Install Playwright Chromium
run: python -m playwright install --with-deps chromium
- name: Build CSS
run: python scripts/build_css.py
- name: Regenerate baselines
env:
REQUIRE_BROWSER_SMOKE: '1'
INKYPI_ENV: dev
INKYPI_NO_REFRESH: '1'
PYTHONPATH: src
run: |
case "${{ inputs.target }}" in
layout) targets="tests/integration/test_visual_regression.py" ;;
plugins) targets="tests/snapshots/" ;;
both) targets="tests/integration/test_visual_regression.py tests/snapshots/" ;;
esac
# --update-snapshots makes every assert_image_snapshot() write instead
# of compare, so the run is expected to pass trivially; the artifact is
# the point.
pytest $targets -q --update-snapshots
- name: Show what changed
run: git --no-pager diff --stat -- tests/snapshots/ || true
- name: Upload refreshed baselines
uses: actions/upload-artifact@v4
with:
name: refreshed-visual-baselines
path: tests/snapshots/**/*.png
if-no-files-found: error
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,6 @@ tests/snapshots/layout/actual/
/src/static/styles/main.css
# Ignore bundled/minified assets (regenerated by scripts/build_assets.py)
/src/static/dist/

# Dogfood QA output — generated screenshots/videos/report, not source.
dogfood-output/
19 changes: 19 additions & 0 deletions docs/building_plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ This guide walks you through the process of creating a new plugin for InkyPi.
# update value for next refresh
settings["index"] = settings["index"] + 1
```
- (Optional) If your plugin sometimes has nothing worth showing, implement `skip_display_condition` to yield its playlist turn instead of rendering an empty frame. On an e-ink panel the cheapest refresh is the one that never happens.
- Return `None` to render normally, or a short reason string to skip this cycle. The reason is recorded against the plugin, so a skip reads as deliberate rather than as a silent failure.
- Intended for plugins with legitimate quiet periods — a scoreboard out of season, a calendar with no events today, a feed with nothing new.
- Only playlist refreshes are skipped. A manual **Update Now** always renders, because declining an explicit request looks like a broken button.
- If the hook fetches data to decide and then returns `None`, cache what it fetched in a plugin-private `settings` key so `generate_image` does not immediately repeat the request.

```python
def skip_display_condition(self, settings, device_config, current_dt):
games = fetch_games(settings, current_dt)
if not games:
return "No games to display"

# Reuse this in generate_image instead of fetching twice.
settings["_scoreboard_games_cache"] = games
return None
```
Comment thread
jtn0123 marked this conversation as resolved.

- (Optional) `generate_image` may return `None` when your plugin has no image to show at all — it exists for its side effect, such as driving a servo or calling a webhook. The refresh completes and the display is left untouched.
- This is different from `skip_display_condition`: returning `None` means *"I was never about showing anything"*, while a skip means *"I normally show something, just not this cycle"*.

### 3. Create a Settings Template (Optional)

Expand Down
79 changes: 79 additions & 0 deletions docs/security/sonar-s2083-crash-breadcrumb-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Tracking: SonarCloud S2083 on `utils/crash_breadcrumb.py`

Created: 2026-08-20

## Finding

- Rule: `pythonsecurity:S2083` — "Change this code to not construct the path from user-controlled data."
- Severity: Blocker (drives `new_security_rating` to **E**, failing the PR quality gate)
- Location: `src/utils/crash_breadcrumb.py`, the `write_text` call inside `_write_json`
- First reported: PR [#632](https://github.com/jtn0123/InkyPi/pull/632)

## Why It Fires

Sonar's taint analysis treats `os.getenv()` as an attacker-controlled source and
`Path.write_text()` as a file-write sink. The breadcrumb's directories come from
`INKYPI_RUNTIME_DIR` / `INKYPI_LOCKFILE_DIR` / `INKYPI_STATE_DIR`, so there is a
source-to-sink path and the rule reports it.

## Assessment: false positive, but the code was hardened anyway

**Not a privilege boundary.** These variables are set by the systemd unit that
launches the service. Anyone able to change them can already execute code as the
service user, so redirecting a breadcrumb write gains an attacker nothing they
did not already have. This is configuration, not untrusted input.

The environment override exists so tests and dev runs can redirect state to a
temp directory — the same contract `install/update.sh` and
`blueprints/settings/_update_status.py` already honour. Those modules read the
same variables and are not flagged, because they have no write sink.

Hardening applied in #632 regardless, because one part of the finding pointed at
a real (if minor) bug:

- The directory must now be **absolute**, and is resolved. A relative value used
to scatter breadcrumbs relative to the service's working directory instead of
where the next boot reads them — a genuine correctness bug, not just a
security one.
- `_in_dir()` refuses a filename that resolves outside its directory, so these
helpers cannot become an arbitrary-write primitive if a future caller passes
something that is not a module constant.
- Values read back out of the breadcrumb are sanitised before they reach logs or
`disabled_reason` (this closed the three companion `S5145` findings).

Sonar's engine does not model any of that as a sanitizer. It recognises
allow-list comparison against literals, which is not usable here: the tests that
exercise crash recovery need arbitrary `tmp_path` directories.

## Deliberately Not Done

- **No `# NOSONAR`.** Suppressing the marker in code hides the finding from
future readers and from any genuinely unsafe path added later.
- **No laundering the value** through string/`Path` round-trips to break taint
propagation. That would clear the gate only by confusing the analyser, and
would silence the rule for real issues in this file afterwards.

## Resolution Required

Mark the issue **Safe** (or *Won't Fix*) in the SonarCloud UI, referencing this
document. This needs a maintainer with project permissions; it is a review
decision rather than a code change, which is why it is not automated.

Until then `SonarCloud Scan`, `SonarCloud Code Analysis`, and the aggregate
`CI gate` stay red on any PR touching this file. Note `main`'s Sonar gate is
independently red on `new_reliability_rating`.

## Closure Criteria

Close this tracking item when either:

- The issue is marked Safe in SonarCloud and `new_security_rating` returns to A; or
- The environment override is removed from the breadcrumb write path entirely
(for example, resolved once at startup in `config.py` and injected), which
would remove the source-to-sink flow rather than mask it.

## GitHub Issue Attempt

Preferred tracking was a GitHub issue, but the `jtn0123/InkyPi` repository has
issues disabled — same constraint recorded in
[the pip advisory tracking doc](./pip-ghsa-58qw-9mgm-455v-tracking.md).
Loading
Loading