Skip to content

Port the post-v0.227.0 development train - #69

Merged
kapoorankush merged 2 commits into
masterfrom
port/v0.228.0
Sep 19, 2026
Merged

kapoorankush merged 2 commits into
masterfrom
port/v0.228.0

Conversation

@kapoorankush

@kapoorankush kapoorankush commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

Ports the development train that landed after v0.227.0 — 12 code, test and corpus commits. Mostly display and shutdown correctness, plus the second half of the automatic-update safety story and a corpus reclassification.

The port boundary was established by content, not by the previous port commit's stated range: this repo's tree matches the development repo at the last v0.227.0 port-review commit, and 39 files diverge there permanently by policy (this repo's qualified issue references, its own CI workflows, its user-facing CHANGELOG and README).

Changes

What a clock owner gets:

  • Rebooting while the startup screen is still on the panel paints the restart farewell. Previously the panel could stay frozen on the startup screen through the power-off, or show the shutdown farewell for a restart.
  • litclock.service and the IP-change dispatcher are ordered after the boot splash, so a timer-fired render no longer loses the display to it mid-paint.
  • A blank or invalid nightly display-cleaning hour falls back to the default instead of wedging the painter every minute.
  • The setup page's hidden-network box stays out of the way until "My network isn't listed" is picked, and a stale retry echo is cleared.
  • Preparing an SD card for cloning aborts a failed settings wipe before anything irreversible, and the shell-history clear is locked against write-back from the shell the script was run from.
  • A release that fails the update smoke gate is recorded, so a failing release is no longer re-applied and re-reverted every week.
  • A clock with a hand-typed location stops reporting a stale location it never refreshes.
  • Two quotes are reclassified as mature; quote images v11.

Internals with no owner-visible effect (correctly absent from the CHANGELOG): one source for the env.sh defaults block, a shared guarded exit helper in the new src/hard_exit.py, a bounded image-build validator step, and a prose sweep correcting the reversed 010_pi-nopasswd plan.

Images: .images-version goes v9 → v11, paired with the published litclock-images-v11 release. v10 is skipped deliberately — this repo's v9 and the development repo's v10 were the same image set, so the numbering was one behind; publishing this corpus as v11 in both places re-aligns them. The set was re-derived rather than copied: this repo's generator, run against the ported CSV, regenerated 0 of 4808 images.

Not ported: the development repo's traffic snapshots and its own issue-reference hygiene test (this repo has tests/test_issue_ref_namespace.py). No CI or dependency changes in this train. CHANGELOG text is summarised, never copied.

Related Issues

Ports litclock-dev#832, litclock-dev#833, litclock-dev#834, litclock-dev#836, litclock-dev#837, litclock-dev#838, litclock-dev#839, litclock-dev#840, litclock-dev#845, litclock-dev#847, litclock-dev#848, litclock-dev#856, litclock-dev#861, litclock-dev#862, litclock-dev#865 and the litclock-dev#635 corpus rows.

Test Plan

  • Tested locally
  • Tested on Raspberry Pi hardware
  • No testing needed (documentation/config only)

Local, on this branch: ruff check . clean; the exact CI shellcheck invocation clean; bare pytest (what CI runs) 4636 passed / 65 skipped; npm run test:js 209 passed. All four corpus-integrity.yml checks were run against the published release and pass.

Hardware: the shutdown/boot splash ordering was verified end to end on the bench, control-fails-then-fixed-passes, and the update smoke gate's revert, blocked-release and recovery ticks were each verified on a device.

Port fidelity was verified mechanically rather than by eye: every ported file is byte-identical to the development repo's with the reference requalification applied, except the files where this repo already diverged — no public-only content was clobbered. The three hand-merge decisions (a conflict resolution, a raised span bound, three line reflows) were each mutation-tested and go red.

Checklist

Ports litclock-dev 0f5e08d3..d1c335d9 — 12 code/test/corpus commits. The
boundary was established by CONTENT, not by the previous port commit's stated
range: public's tree matches dev at 0f5e08d3 (the last v0.227.0 port-review
commit), and 39 files diverge there permanently by policy.

What owners get (CHANGELOG has the user-facing summary):
- the shutdown splash paints correctly when a reboot lands during the boot
  splash: the units are ordered and the boot splash's stop is bounded at 10s
  (litclock-dev#856), and the action is resolved from PID 1 as root so a
  restart no longer paints the power-off farewell (litclock-dev#862/#861)
- litclock.service and the IP-change dispatcher are ordered after the boot
  splash, so a timer-fired render no longer loses GPIO17 to it
  (litclock-dev#832); a plain reset-setup.sh run leaves
  litclock-shutdown.service armed (litclock-dev#833)
- an empty or malformed DISPLAY_CLEAR_HOUR falls back to the default instead
  of wedging the painter every minute (litclock-dev#838)
- the splash renderers' hard exit survives a broken stderr, via one shared
  guarded helper in the new src/hard_exit.py (litclock-dev#837/#840)
- the 7-day IP-geo staleness anomaly is gated on the location mode, so a
  Specific-mode clock stops reporting a stale location it never refreshes
  (litclock-dev#836)
- the setup page's hidden-network box is usable only once "My network isn't
  listed" is picked, and a stale retry echo is cleared (litclock-dev#848)
- prepare-for-cloning.sh aborts a failed env.sh wipe BEFORE anything
  irreversible, and the bash-history clear is locked against write-back from
  the shell the script was run from (litclock-dev#839, litclock-dev#834)
- a smoke-reverted release is recorded in /var/lib/litclock/blocked-sha, so a
  failing release is no longer re-applied and re-reverted every week
  (litclock-dev#865)
- update.sh revert arms re-install the units they restart, an offline tick is
  no longer stamped failed_unrecovered, sudoers/020 grants the timer starts
  those arms need, and the runtime-render validation leaves a negative-result
  memo (litclock-dev#845, litclock-dev#847)
- the env.sh defaults block has one source, env_sh_defaults() in
  scripts/lib/state.sh, instead of three hand-copied bodies (litclock-dev#840)
- two quotes with breast references are flagged NSFW; quote images v11
  (litclock-dev#635)

Deliberately NOT ported:
- stats/ — the development repo's own traffic snapshots; the directory does
  not exist here
- tests/test_issue_ref_hygiene.py — it enforces the development repo's
  reference conventions; this repo has tests/test_issue_ref_namespace.py
- CHANGELOG text — summarised, not copied
- no .github/ or dependency changes in this train

Hand-merged rather than applied:
- tests/test_reset_setup_sh.py: took the litclock-dev#840 rewrite of
  test_defaults_include_weather_location_mode_and_ip_country (the body now
  comes from env_sh_defaults(), so grepping the script for the literal no
  longer works) over this repo's line-reflow of the old assertion
- tests/test_first_boot_flow.py: the Setup-Incomplete span tripwire is raised
  to 4100 from 4000. The block carries four issue references and this repo
  spells each `litclock-dev#NNN`, 12 characters longer apiece; the bound is a
  runaway-span tripwire, not a size budget
- three assertions reflowed for E501: the qualified references push them past
  120 columns here but not in the development repo

Verified: ruff check . clean, the CI shellcheck invocation clean, 4570 pytest
passed / 65 skipped, 209 vitest passed. The image set was re-derived rather
than trusted: this repo's generator, run against the ported CSV, regenerates
0 of 4808 images and produces a manifest whose corpus_hash matches the
development repo's byte-for-byte.
Pairs with the corpus change in the previous commit (two quotes reclassified
as mature). Separate commit because cutting the release is a side effect on
GitHub and bumping the pin is a source-of-truth change — docs/quote-releases.md
keeps them apart so review can catch a wrong number before the release is
visible to fresh installs.

v11, not v10: this repo's v9 and the development repo's v10 are the same image
set, so the numbering was one behind. Publishing this corpus as v11 in both
places re-aligns them, and a future port no longer has to translate the number.
v10 is deliberately skipped here and will never exist on this repo.

litclock-images-v11 is published with the tarball, its sha256 and manifest.json.
The set was re-derived rather than copied: this repo's generator, run against
the ported CSV, regenerated 0 of 4808 images, and the manifest's corpus_hash
(aaa302fa) matches sha1 of image-gen/litclock_annotated.csv — the comparison
.github/workflows/corpus-integrity.yml makes. All four of that workflow's
checks were run locally against the published release and pass.
@kapoorankush
kapoorankush merged commit 36bcb57 into master Sep 19, 2026
7 checks passed
@kapoorankush
kapoorankush deleted the port/v0.228.0 branch September 19, 2026 04:19
kapoorankush added a commit that referenced this pull request Sep 21, 2026
* feat: port the post-v0.228.0 development train

Ports litclock-dev afa2d6b4..fde50fc6 — the seven PRs merged 2026-09-19/20
(litclock-dev#872-#878). The boundary was established by CONTENT, not by the
previous port commit's stated range: public carries litclock-dev#865's
blocked-sha and none of litclock-dev#868/#870/#871, and the file sizes agree
(update.sh 2542 vs 2739, state.sh 317 vs 416, quote_corpus.py 249 vs 368).

Gated on the bench QA pass of 2026-09-20 (`dev-20260920-512c291`, a fresh
flash on a Pi Zero 2 W): 13 checks, no blocking findings, every member of this
train exercised on hardware.

What owners get (one line in the CHANGELOG, because that is all that is
visible from outside):
- gift mode and the app's Factory reset now wipe AND LOCK the default shell
  history files, as clone prep already did (litclock-dev#868). The lock is a
  root-owned empty directory at the path: `history -c` reaches only the
  script's own shell, and the console or SSH shell the operator ran it from
  writes its history back on exit, during the power-off. first-boot.sh removes
  both directories on the not-yet-set-up path, so the recipient's first login
  gets an ordinary history file.

Shipping silently, by design:
- the quote corpus is resolved per active language from the registry
  (litclock-dev#870). Latent while English is the sole active language: the
  pre-rendered PNGs stay English, so a second language needs on-device text
  rendering before it can be activated.
- the updater's runtime-render self-test, shipped INERT (litclock-dev#871
  Stage A). On a marker-bearing device an applied release now asks whether the
  device can actually paint a quote from text and records the verdict in
  /var/lib/litclock/runtime-render-selftest.json; it changes no setting, and a
  failure is not an update failure. First field measurement: 3.8s on a Pi
  Zero 2 W during an update, 3.4-3.7s idle — whole-process wall time, not
  render time.

Also: the LKG recorder no longer claims bootcheck/revert is unshipped
(litclock-dev#877), the WiFi retry tests stub the IP-geo resolver that was
making a live network call (litclock-dev#876), and CLAUDE.md gains the
2026-09-20 bench results plus the rewritten litclock-dev#867 raw-journal check
(the old one passed vacuously).

Port mechanics:
- 48 bare `#NNN` refs and 14 `dev#NNN` shorthands requalified to
  `litclock-dev#NNN` on ported lines only, validated by this repo's own
  tests/test_issue_ref_namespace.py rather than by a hand regex.
- Hand-merged where public has diverged: README (its own structure and
  headings), docs/script-reference.md (public's `#resetting` anchor),
  scripts/lib/state.sh (public's already-qualified env.sh writer-lock header),
  and the two comment conflicts in update.sh and status.py.
- CONTRIBUTING.md edited in place to preserve its CRLF line endings.
- Scrubbed on the way over: the `~/archives/litclock-qa` harness path, which
  does not exist for a reader of this repo.

ruff clean, shellcheck clean, pytest 4638 passed / 66 skipped, vitest 209
passed.

* fix(test,update): port the litclock-dev#879/#880 review fixes

Ports litclock-dev 7c2063ea..9c8ddc81. Both defects were carried over by the
train in the parent commit, not introduced by it; both were found by the
gstack /review of this branch.

- tests/test_wifi_retry_flow.py: the IP-geo stub litclock-dev#876 added covered
  ONE class of three. Measured with a getaddrinfo probe on this checkout: 24
  DNS lookups of ip-api.com across 8 tests. The resolver's success path calls
  set_system_timezone(), which shells out to sudo, and ENV_FILE is
  monkeypatched by these tests while that call is not sandboxed by anything —
  so on a machine where the installer has run, `pytest tests/` could change the
  system timezone. The fixture is module-scoped and autouse now, so a class
  inherits it rather than appearing on a list of the classes somebody
  remembered. Probe on this branch after the port: 0 attempts, 70 passed.

- scripts/update.sh: ${EPOCHREALTIME/[.,]/}, not a literal dot. The separator
  follows LC_NUMERIC, so under a comma locale the strip matched nothing and the
  comma parsed as bash arithmetic's comma operator. Three measured outcomes,
  all wrong; the dangerous one is silent (rc 0, nothing on stderr) and IS
  recorded, beside "result":"passed", in the file litclock-dev#871 Stage B is
  meant to gate on. Production always got C from systemd; the exposure is a
  maintainer running the script by hand from a non-English desktop, which
  README's manual-update section tells owners they may do.

The seven tests lift the shipped t0=/dur_ms=/duration= assignments out of
_runtime_render_selftest and execute them verbatim with EPOCHREALTIME injected,
rather than a copy — a copy proves nothing about the script, and neither did
counting occurrences near it (an indirect-expansion mutant satisfied the count
and reintroduced the bug). Five mutants verified red against THIS checkout's
update.sh, including the full revert.

No CHANGELOG entry: nothing here is visible to an owner.

ruff clean, shellcheck clean, pytest 4645 passed / 66 skipped, vitest 209.

* fix(test,update): port the litclock-dev#882 review round, and two findings of this repo's own

Ports litclock-dev 9c8ddc81..7438f611, plus two fixes that exist only here.

From dev:
- `${EPOCHREALTIME//[^0-9]/}` — strip every non-digit, not just `[.,]`. glibc
  gives fa_IR and ps_AF U+066B (٫), and measured, `[.,]` leaves it in place for
  rc 0, duration=0.0, `invalid arithmetic operator` on stderr, and a wrong
  value jq accepts and records. Same hazard class, rarer locale family.
- The test that holds it stopped asserting source text. Four review rounds each
  broke the previous round's rule with a shape it did not model (a decoy in a
  comment, an indirect expansion, a `local`-prefixed assignment, and finally
  `printf -v duration '0.0'`, which is not an assignment at all). So a second
  class EXECUTES the shipped `_runtime_render_selftest` with its helpers
  stubbed and a painter that sleeps a known time, and asserts on the value
  `_runtime_selftest_record_write` is handed, with a second painter as the
  control. Verified against THIS checkout: printf -v and eval red, the full
  revert red, and narrowing back to `[.,]` red.
- The hazard test now uses a fraction whose wrong output `jq tonumber`
  ACCEPTS, so it exercises the silent-and-recorded shape the fix exists for;
  the earlier default produced `0.-3`, which jq refuses, i.e. the loud band.

Found in this repo, by the review of this branch at its final HEAD:
- `tests/test_wifi_retry_flow.py:161` carried `litclock-dev litclock-dev#876` —
  the requalifier prefixed a `#876` on a line that already said `litclock-dev`
  with a space. The only occurrence in the tree; this is the dev#767 doubled-
  qualifier class, so it is worth catching rather than leaving.
- `PUBLIC_NUMBER_CEILING` was still 67, two ports behind: #68 and #69 landed
  the v0.227.0 and v0.228.0 trains and this lands as #70. Audited 68..70 across
  all tracked files — ZERO bare `#N` in that range, so nothing in the tree
  changes meaning — and raised to 70 with that audit recorded, which is the
  deliberate act the module docstring asks for.

Also removed a stray empty `0755/` directory in the working tree, a `mkdir -m`
typo from 2026-09-13.

ruff clean, shellcheck clean, pytest 4648 passed / 66 skipped, vitest 209.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant