From 83e2b56a8c01b0c44d273b96e7d47d364c0a6094 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 25 Jul 2026 09:49:23 -0500 Subject: docs(todo): file 2 roam items, archive completed tasks (wrap-up) File the two archsetup-owned roam captures (auto-dim forgotten on layout change; maint queue copy button), and archive 2 completed level-2 tasks to Resolved via the wrap-up cleanup. --- todo.org | 4756 +++++++++++++++++++++++++++++++------------------------------- 1 file changed, 2372 insertions(+), 2384 deletions(-) diff --git a/todo.org b/todo.org index d65dab7..21528ba 100644 --- a/todo.org +++ b/todo.org @@ -45,2875 +45,2863 @@ below): input-side-spec.org (DRAFT, four decisions open). * Archsetup Open Work -** DONE [#A] Hyprlock lockout: AMD-iGPU DPMS invalidates the lock, session wedges :bug:hyprland:installer:solo: -CLOSED: [2026-07-24 Fri] +** TODO [#D] net-scenarios harness times out under back-to-back suite runs :test:tooling: :PROPERTIES: :LAST_REVIEWED: 2026-07-24 :END: -Fixed as archsetup =a9391c9= + dotfiles =3046c9c=, both pushed; applied live to ratio and velox. Reboot ratio to activate the root fix (=amdgpu.runpm=0=); the watchdog covers until then. - -WHAT HAPPENED. Ratio's screen idle-locked, then wedged: hyprlock gone, the compositor still holding the ext-session-lock, no password prompt, recoverable only from a console. Recovered live with =hyprctl dispatch exec hyprlock= (=allow_session_lock_restore=true= was already set, so a replacement client adopted the dead lock). - -ROOT CAUSE (evidence, not the first guess). My first read was "hyprlock crashed on its screenshot buffer" — WRONG. Coredumps are captured here (two telega SIGSEGVs the same afternoon) and there is NO hyprlock coredump, so it did not segfault; memory was fine, so not OOM. The hyprland log shows the real chain: =Modesetting DP-4= / =Restoring crtc 86= (a display modeset) → =color management protocol is enabled and outputs changed= → =SessionLock.cpp:50 SessionLockSurface object remains but surface is being destroyed=. A display power cycle tore down the lock surface. Online research confirms it's a documented AMD-integrated-Radeon issue (hyprlock#953, Hyprland#5822): the GPU resources the lock client holds become invalid when the display powers down and back up. Ratio is a Strix Halo Radeon 8060S — exactly that hardware, and its cmdline already carried =amdgpu.dcdebugmask=0x10= + =no_vpe_idle_pg=1= display workarounds, a history of the same fragility. - -THE FIX, four layers, research-validated: -1. Root cause: =amdgpu.runpm=0= on the kernel cmdline (AMD only, added in =update_grub_cmdline= behind =detect_gpu_vendors=). Keeps GPU runtime PM from invalidating the resources on a display cycle. Live in ratio's grub.cfg; effective next boot. -2. Separate crash cause: =configure_hyprlock_pam= writes a complete =/etc/pam.d/hyprlock= (auth/account/session). The package default is =auth include login= only, so pam_end() crashes on uninitialised handles. Applied live to both machines. -3. Recovery net: the =screen-lock= watchdog (dotfiles) relaunches hyprlock on a non-zero exit; hypridle's =lock_cmd= routes through it. Independently the same shape as the community's watchdog layer. -4. NOT done, deliberately: the =dpms off= listener stays in the committed hypridle — =runpm=0= makes it safe on AMD, and it's wanted on Intel/velox for idle display-off. Ratio's test rail already removed it as a local choice. - -REVERTED a wrong turn: I'd first built a screenshot-to-file change (grim the desktop, point hyprlock at the file) on the theory the live screencopy buffer crashed. The research showed the cause is GPU runtime PM, not the background source, so I dropped it and reverted hyprlock.conf to =path = screenshot=. - -PROCESS NOTE — I hit the pathspec-commit trap AGAIN (the one the =Two agent sessions sharing one repo= VERIFY documents). After surgically staging only the =lock_cmd= line via =git update-index=, I ran =git commit -m ...=, which commits the WORKING TREE of that path, not the index — so it committed ratio's test rail (dpms-off removed, timeout 450) with a message claiming dpms-off stays. Caught it before push, =git reset --soft=, re-verified. The rule: after =update-index=, commit with =git commit= (no pathspec), never =git commit =. +=tests/net-scenarios/test_run_net_scenarios.py= errored on all 5 tests twice during round 12, each time =subprocess.TimeoutExpired= after its 20s budget on =scripts/testing/run-net-scenarios.sh --target root@fake-vm=. Both occurrences were in =make test-unit= runs launched immediately after a previous full run. It then passed 6 runs in a row (3 on a pristine tree, 3 with the round-12 change), and standalone it finishes in 0.09s, so this is not a regression from any code change. -Tests: archsetup 372 (test_grub_cmdline AMD-runpm cases + test_hyprlock_pam, both call sites in CALL_SITES); dotfiles 3687 incl. tests/screen-lock. Each guard proven by deletion. +The harness stubs =ssh=, =rsync= and =jq= onto =PATH=, so nothing should touch the network at all — which is what makes a 20s timeout suspicious rather than merely slow. Worth reproducing under load before deciding whether the fix is a larger timeout or a real hang in the script. Evidence logs from the round: =/tmp/tu.log= and =/tmp/tu2.log= (tmpfs, gone after reboot). -Grading: Critical severity (full session lockout, console-only recovery) x rare edge case (needs an idle lock plus a display modeset on the AMD iGPU) = P2 = [#B by the matrix]. Raised to [#A] here because it stranded a live machine and the root fix needs a reboot to arm — worth Craig seeing at the top until he reboots ratio. +Not graded on the bug matrix: it is test infrastructure, not the shipped codebase. -** DONE [#B] Adversarial review of the sentry run — six fixes reworked :bug:test:tooling:solo: -CLOSED: [2026-07-24 Fri] +** VERIFY Should coredump entries group as one journal-digest row per binary? :maint: :PROPERTIES: :LAST_REVIEWED: 2026-07-24 :END: -Craig asked for a skeptical review of every sentry change. Eight agents covered all 23 code commits, each told to disbelieve by default and to answer three questions per commit: does the problem exist and is it reachable, is the fix correct or is there a better one, would each test fail with the fix reverted. Every finding below was re-verified by hand before acting on it. - -SIX COMMITS NEEDED WORK, now fixed: archsetup =1207ca5= (wipedisk), =96e12b5= (firmware trim), =560e1dd= (autologin), =3c2155d= (initramfs tabs); dotfiles =ec7229b= (tunnel import), =a81aa0e= (thumbnail sweep), =56807e5= (three residual guards), =c90ee34= (event-log isolation). Both suites green: archsetup 341, dotfiles 3687 on both gates. - -THE ONE THAT MATTERED MOST. =wipedisk= ran =blkdiscard -f= BEFORE the busy check. =-f= disables the exclusive open util-linux has used since 2.36, so on the exact case the round-11 commit reasoned about — the user picked the wrong disk — it discarded a live filesystem and only then let sgdisk fail, printing "could not clear the partition table ... run this again". Data gone, user told nothing happened. The ordering predates the sentry commit, but round 11 wrote reasoning about the busy-disk case into the comment and error text while leaving the discard first, which made the misreport worse in the one direction that costs something. Dropping =-f= makes the kernel's own O_EXCL the gate. +Noticed in the 2026-07-24 sentry bug-hunt, round 8, while verifying the =journal_errors= probe against the live journal. Not filed as a bug — it needs your call on what "the same error" means here. -THREE PATTERNS WORTH MORE THAN THE INDIVIDUAL FIXES: +The probe's counting is correct, and I checked it rather than assumed: it reports 1674 real + 16 noise = 1690, and =journalctl -p 3 -b -o json | wc -l= returns exactly 1690 entries this boot. (A =wc -l= on =-o cat= says 23037, but that splits multi-line messages like coredump stack traces across lines — the probe counts entries, which is right.) -1. CALL SITES WENT UNTESTED IN FIVE SUITES. Every helper had thorough tests; not one proved it was called. Deleting the call left everything green — including the guard on a =pacman -Rdd= of twelve firmware packages, whose removal would have run the trim on ratio. Closed with =CALL_SITES= in =test_orchestrators= (nine pairs, static) and a wiring assertion in the settings suite. Static on purpose: the behavioural harness runs un-stubbed bodies for real, which is fine for an orchestrator and not for a leaf that removes packages. +What's off is the *grouping* for multi-line messages. Thirteen =systemd-coredump= entries on ratio right now — nine usbredirect, three telega-server, one python3 — land as four separate digest rows (counts 4, 3, 3, 3) instead of one row per binary. =_signature()= blanks hex addresses and long integer runs, which handles the pid, but two dumps of the same binary still differ in frame count and thread layout, so their signatures diverge. -2. A NEW OUTCOME VALUE NEEDS EVERY CONSUMER WALKED, EVERY TIME. Done for the portal enum in round 3, skipped for the tunnel-import one in round 4 — where =import_configs= folded a disarm failure into "none imported (N failed)", the opposite of what happened, in the multi-select flow the GUI actually uses. +Consequence is modest: the digest's top-N rows get eaten by near-duplicates, so genuinely distinct errors fall off the evidence list sooner. It doesn't affect the metric's value or severity. -3. MY FIXTURES TWICE CLAIMED A FIDELITY THEY DID NOT HAVE. The wipedisk fixture used this machine's real disk names, so five of six tests passed with the seam removed. The mkplaylist fake does a full =cat > /dev/null= drain while its docstring says it "drains stdin exactly when the real one would" — which is what let the wrong failure mode survive. +The question is what you'd want: group coredumps by the binary named in the first line (a special case for =systemd-coredump=), signature only the *first line* of any multi-line message (a general rule, and arguably the right one — the first line is the error, the rest is context), or leave it alone. The middle option is the smallest general change and I'd lean that way, but it changes grouping for every multi-line error, so it's yours to call. +** VERIFY [#B] Two agent sessions sharing one git repo :chore:tooling: +SCHEDULED: <2026-07-27 Mon> +Twice on 2026-07-22/23 a session swept the other's uncommitted work into its commit while both worked the dotfiles tree. The dotfiles session pathspec'd around mine once; my 2bd7d56 then carried their settings-panel window-rule revert (100%-854 → 100%-584) under a message describing only hypridle. Nothing needed undoing — the change was wanted — but the commit misdescribes itself, and the next collision may not be benign. -AND ONE FINDING WAS DISPROVED OUTRIGHT: round 1's =a57c443= claimed ffmpeg drains the read loop so only the first track is processed. Measured under strace and driven end to end with real ffmpeg (three runs of three, four 120s mp3s), the loop never truncates. The hazard is real and =-nostdin= is right; the symptom was reasoned from shellcheck SC2095 and never run. Corrected in =a30741a=, along with the OpenVPN autoconnect claim and the "four consumers" undercount. +Root of it: =git commit -- = commits the *working tree* state of that path, so a pathspec guards against other files but not against another session's edits to the same file. The shared =.git/index= is the deeper half — one session's =git add= disturbs the other's staging even when their files don't overlap. -ALL THREE NOW CLOSED, in dotfiles =c7cb40d= (pushed). =_restore_dot='s =noop= split into =already-on= and =not-managed=, so the step stops claiming a restore that never happened. =_disable_dot= checks its restart as well as its move, since moving the drop-in aside does nothing until resolved reloads. +Worktrees are the standard answer but fight this repo: stow symlinks point at =~/.dotfiles=, so only that clone is live and a worktree edit isn't real until it lands back there. A file-ownership split needs renegotiating whenever work crosses a boundary, which is most interesting work. -The thumbnail one could not be built as described, and that is worth recording. The cache name is a SHA-1 of realpath plus mtime, so no filename says which source it came from; per-source sweeping would mean changing the key format and invalidating every cached thumbnail. Bounding the growth gets the same result for less: a deferred sweep now trims to a 500-file ceiling, oldest first, because eviction is safe exactly where sweeping is not (an evicted thumbnail is rebuilt on the next warm pass, costing one decode and never a file). WHEN A FIX CANNOT BE BUILT AS SPECIFIED, SAY SO AND SOLVE THE ACTUAL HAZARD — the hazard here was unbounded growth, not imprecise attribution. +The unintuitive part, worth stating plainly because both sessions built habits on the wrong model: a pathspec commit reads the *working tree*, not the index. The dotfiles session's pathspec habit carried the same exposure the whole time and only held because I never edited a file on one of their lists. Neither side had a mechanism, just different luck. -** DONE [#D] Repair tiers call an unverifiable service restart a failed one :bug:network:bluetooth:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as dotfiles =041d6b9= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). 7 new tests across =tests/bt/test_bt.py= and =tests/net/test_net.py=; dotfiles suite 3665 -> 3672, =make test= exit 0 on both gates. Each of the three guards proven a real gate by deleting it and watching the suite go red. +*Options, after their skeptical review (their read is better than my first two):* -Found in the 2026-07-24 sentry bug-hunt, round 14, on the cross-package =repair.py= diff that rounds 5-13 had left unspent. +1. =GIT_INDEX_FILE= per session — real but heavier than it looks. It only helps if both sessions also stop pathspec-committing (otherwise the working tree still wins, so it isn't coordination-free), it needs seeding from =.git/index= or git sees every tracked file as new, and it confuses hooks and anything reading default-index state. +2. "Read the staged hunks, not the stat" — *already on the books*. The publish flow requires =/review-code --staged= before a commit, and that reviews the diff. So this is an existing rule that didn't run, not a missing rule. I skipped it on both speedrun commits (2bd7d56, f9b6404) after Craig waived approvals — waiving the approval gate is not the same as waiving the review, and I collapsed the two. The question worth answering is why it was skipped, not what new habit to add. -=cmd.service_active= is tri-state in both the net and bt packages, and its docstring says so outright: True, False, or None when systemctl itself can't answer (absent binary, or a timeout). Six callers. Three rule on it correctly — =bt/doctor._service_step= branches on None with "systemctl unavailable — can't check the service", and =net/diag= compares =is False= at both its call sites. Three tested it with plain truthiness: + *Both sessions agree this is the finding that matters more than the lock shape*, and that the durable version should state it in one line: *an approval waiver never waives the review, because the review is the gate that reads hunks.* Two gates that both stand between an edit and a commit collapse into one the moment either is waived. +3. *Their proposal, and the best of the three:* a =dotfiles-commit= lock via the existing =.ai/scripts/agent-lock= (already used for sentry's runner and roam-write), held across stage → review → commit. It serializes the only window where sweeping happens, needs no change to how either session commits, and self-clears on a crashed holder. It doesn't stop a concurrent edit mid-window, but that window is seconds rather than minutes. -- =bt/repair.py= =repair_service_restart= -- =net/repair.py= =_service_restart= (the nm-restart and resolved-restart tiers) -- =net/repair.py= =repair_unmask_nm= +Craig's call: whether this becomes a durable rule in the shared rules layer (it's cross-project — any repo two sessions touch), which shape, and which session implements it. Both sessions have offered to own it. -So an unanswerable systemctl was reported as "bluetooth.service is still not active" / "NetworkManager still isn't running after a restart" — a statement about the service made on no evidence at all. Each then pointed the user at =journalctl -u =, which is the same systemd client stack that had just failed to answer. That last part is round 10's read again: an error message advertising a remedy it cannot honour. +** TODO [#B] Weather tooltip caching :feature:waybar:weather:solo: +From the roam inbox (Craig, claimed 2026-07-22): retrieve the weather tooltip data once per hour and cache it. If the network is unavailable, display the cached tooltip with explanatory text saying so. Dotfiles-side work (archsetup owns the lifecycle); touches common/.local/bin/weather. +** TODO [#B] Settings gear becomes four device toggles :feature:waybar:dotfiles:solo: +From the roam inbox (Craig, claimed 2026-07-23): the waybar gear should become four icons — touchpad, mouse, webcam, and a notification bubble. Clicking each toggles that setting directly. The first three turn red when disabled; the bubble turns red when DND is enabled. -All three now report =warn= on None, with evidence naming the verification rather than the service, and a next action of checking systemd is reachable and re-running the doctor. Control flow is unchanged: =warn= was already a status both packages emit, both CLIs already exit non-zero on anything but =pass=, and =net/doctor= only inspects a repair step's status for the =dns-test= tier — every consumer was checked before the change, not after. (An adversarial re-review counted twelve, not four; all twelve handle =warn= correctly, so the conclusion held while the claim understated the work.) +Today =custom/settings= (=hyprland/.config/waybar/config=) is one gear glyph (󰒓) whose only job is =on-click: settings-panel=. The toggles themselves already exist and are tested — the settings package owns touchpad, mouse, and webcam (=webcam.py= is the USB-authorized kill switch from 2026-07-22), so this is a bar-side surface over existing backends rather than new capability. -THE SEAM FOR THE TESTS, worth reusing: both suites already carry an exec-failure harness that plants a non-executable file on an emptied PATH, which is exactly what makes =cmd.run= return None. So the None case is reachable through the real code path with no mocking at all. Each test class asserts that premise first (=service_active= really is None in the sandbox) rather than assuming it. +Note the state-polarity split when wiring the colors: three read "red = off" and DND reads "red = on". That asymmetry is deliberate (red means "something is disabled that normally isn't, or suppressed that normally isn't"), so encode it per-icon rather than deriving one rule. -Grading: Minor severity (the claim is wrong but errs pessimistic — it says a repair failed when it may have worked, rather than falsely reassuring; nothing is damaged) x rare edge case = P4 = [#D]. Fixed rather than filed because the change is three branches and it completes a class — leaving two of three sites collapsed is the failure mode the round-6 =c2eb3e1= commit exists to remember. +Decided 2026-07-23 (Craig): the gear STAYS alongside the four toggles as the panel launcher. So the bar's right side grows from 12 modules to 16 — the four toggles are net-new, the gear keeps its =on-click: settings-panel=. Open sub-question for build time, not blocking: whether the four toggles are four separate waybar modules or one custom module rendering four glyphs (fewer layout entries, one exec). Pick at build; the four-module shape is simplest and matches how mic/net already sit as individual modules. -NOT PART OF THIS CLASS, checked and left alone: =settings/toggles.dim_state= is the only other genuine True/False/None helper in the tree, and both its callers pass the value through to the viewmodel rather than collapsing it. Every other "or None" in the packages is two-state (a value or nothing), where falsy handling is correct. +** TODO [#B] World face: orientation + hour-format pickers in the settings panel :feature:dotfiles: +The world face (=settings/faces/world.html=, shipped 6afbe09) already supports both orientations and 12/24-hour via =?layout= and =?hour12= query params, defaulting vertical/12h. What's missing is letting Craig CHOOSE them from the desktop-settings panel. Build: (1) two new fields in the wallpaper state (world_layout, world_hour12) with the vertical/12h defaults; (2) =project.py build_uri("world")= appends =&layout=&hour12== read from state; (3) panel controls in the world channel's config section (=gui.py _conf_projected=, currently just a preview) — an orientation toggle and a 12/24 toggle; (4) TDD the URI-building and state round-trip. Solo — buildable, agent-verifiable (URI + state tests, headless render), no open design question (the two faces already exist and are approved). Requested by Craig 2026-07-23; deferred so the vertical face could ship first. +** TODO [#C] Wallpaper channel: timed transitions as an alternative to sunrise/sunset :feature:dotfiles: +From the roam inbox (Craig, claimed 2026-07-23): the wallpaper channel switches on sunrise/sunset today (the sun-pair mode, =settings/src/settings/wallpaper.py=, location read live via whereami with a state.json cache). Add a timed-schedule mode as an alternative: fixed clock times drive the transitions rather than the solar calc. -** DONE [#B] Firmware trim gated on a DMI field that never carries the vendor :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] +Not :solo: — the capture itself flags the missing inputs ("we'll need to know the transition times, and how many of them there are"). The count and the times are a design decision Craig owes: is it a two-image day/night flip at fixed hours, an N-way ring across the day, per-image dwell vs shared interval? The =set= channel already does fixed-interval cycling through a set, so the new part is specifically clock-anchored transition points, not just "a timer". Ask for the schedule shape at pickup, then build against the existing wallpaper.apply presenter vocabulary. +** TODO [#C] Wallpaper panel selection and scroll state :feature:dotfiles:solo: :PROPERTIES: -:LAST_REVIEWED: 2026-07-24 +:LAST_REVIEWED: 2026-07-25 :END: -Fixed as archsetup =2e228f7= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/installer-steps/test_framework_firmware_trim.py=, 12 tests carrying the real DMI strings off both daily drivers. Each of the three conditions proven load-bearing by deleting it and watching the suite go red, and the old gate proven wrong by restoring it (4 failures). +From the roam inbox (Craig, 2026-07-25). Screenshot: =~/pictures/screenshots/2026-07-25_013041.png=. Three related behaviors in the settings wallpaper panel (=settings/src/settings/wallpaper.py=): +1. Open at the wallpaper currently displayed, not the top of the list. +2. Highlight that wallpaper as selected in the scrollable pane while it shows in the preview. +3. Keep the scroll position when a picture is selected. Today selecting a picture snaps the scroll back to the top, which is the bug half of this. +Grade: minor scroll-reset defect x every panel selection = P3 = [#C]; the open-at-current and select-current behaviors are enhancements at the same level. One type tag, so filed =:feature:= with the scroll-reset called out as the bug. Solo: buildable in the settings GTK panel, agent-verifiable via headless capture plus the wallpaper.py tests, no design call — swww query gives the current wallpaper, and scroll-position preservation and row selection are standard GTK. +** TODO [#D] Worldclock tooltip blanks on one bad timezone row :bug:dotfiles:waybar:quick:solo: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-25 +:END: +Found by sentry (2026-07-25), verified by exercising. =hyprland/.local/bin/waybar-worldclock= builds each zone with =ZoneInfo(tz)= inside the loop (line ~99) with no guard, so a single malformed timezone row in =worldclock.conf= raises =ZoneInfoNotFoundError= and crashes the whole python pass. The tooltip then renders empty and *every* zone is lost, not just the bad row; the traceback only reaches stderr, where waybar never surfaces it. +Repro: a conf with =America/Chicago|Home=, =Not/AZone|Bad=, =Europe/London|London= renders =tooltip: ""= (Home and London gone too). +Grade: minor severity (one module's tooltip blanks, no data loss) x rare edge case (a malformed conf row) = P4 = [#D]. +Fix: wrap the per-row =ZoneInfo=/=datetime= in a try/except and =continue=, so a typo drops only that row and the valid zones still render. Solo + quick: the script already has an env-override test harness (=WAYBAR_TIME_EPOCH=, =WAYBAR_WORLDCLOCK_CONF=), so a red-first test is cheap. +** TODO [#C] Auto-dim status forgotten on layout change :bug:dotfiles: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-25 +:END: +From the roam inbox (Craig, 2026-07-25). If auto-dim is toggled off and the layout then changes, auto-dim silently comes back on. A layout switch should not touch the auto-dim state. Likely related to the 2026-07-25 =layout-cycle= rebuild (floating ring) or a hook it fires — check whether the layout-change path resets the dim toggle, and where auto-dim state lives. Grade: minor severity (dim re-enables unexpectedly, no data loss) x every layout change made while dim is off = P3 = [#C]. +** TODO [#C] Maint queue button status wall needs a copy button :feature:dotfiles:maint: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-25 +:END: +From the roam inbox (Craig, 2026-07-25). The maintenance queue button shows the status wall but has no copy button. Add one, following the global COPY key pattern already on the maint doctor wall (dotfiles =8bc79ba=). Grade: cosmetic/feature = [#C]. +** TODO [#B] Panel family: unify the look across net/bt/maint/audio and desktop-settings :feature:design:dotfiles: +From the roam inbox (Craig, claimed 2026-07-24): the network, bt, maint, and audio waybar panels look alike, but the desktop-settings panel looks quite different. He wants them to read as one family. Deliverable: enumerate every difference (chrome, header layout, typography, spacing, control styling, color roles, close-button placement, section dividers) between the two groups and a plan to converge them on one look. Not :solo: — it needs a design pass and Craig's taste calls on which direction each group moves. When picked up, catalogue the deltas from live captures of all five, propose the shared design language (likely the Dupre instrument-console the settings panel uses, since that's the newest and most deliberate), then bring Craig the change list before touching code. +** TODO [#C] Panel text cut off — needs a few px more space :bug:dotfiles:solo: +From the roam inbox (Craig, claimed 2026-07-24): panel labels look cut off; a few more pixels of space fixes it. He named the audio and bt panels, but his "before" capture is the networking panel (=~/pictures/screenshots/2026-07-23_202419.png=; "after" resizing =~/pictures/screenshots/2026-07-23_202458.png=), so the whole panel family likely shares the tight spacing. Confirm which panels clip at pickup, then add the padding/width. Grade: cosmetic × every glance at the affected panels = P3 = [#C]. Solo — buildable (CSS/size tweak) and screenshot-verifiable, no design call once the clipping panels are identified. +** TODO [#C] Net tooltip IPs and line order :feature:waybar:network:solo: +From the roam inbox (Craig, claimed 2026-07-23): in the wifi hover, add the internal IP, external IP, and gateway IP just below the Interface line; move the Signal line to just above the keyboard-shortcuts line. Design constraint: the bar's hot path does no network I/O (status.py deliberately skips _address_facts on the 2s beat) — internal IP + gateway can ride cheap local reads, but the external IP must come from a cache the connectivity probe refreshes, never a live lookup in waybar-net. +** TODO [#C] Night-watch live telemetry :feature:maint: +Craig, 2026-07-21 ("mind. blown."): drive the Dupre Night Watch screensaver (docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html, the idle-pipeline eye-candy stage in the desktop-settings spec) with real system data instead of synthetic signals — a passive status wall while the machine idles. Candidate mappings: scope = CPU load trace, drift chart = memory pressure history, spectrum = per-core utilization, VU pair = net throughput up/down, blinkenlights = disk I/O, tape counter = uptime, systems lamps / annunciator = maint status verdicts, engine-order telegraph = current power profile, VFD wire = maint status one-liner (temps, battery, pending updates). Browser prototype can poll a small local JSON endpoint; the production shape belongs to the idle-stage build. Depends on the spec's idle-pipeline implementation landing first. +** TODO [#B] Dupre Kit merge — casting additions :feature:tooling:solo: +Fold docs/prototypes/dupre-kit-additions.js back into the kit proper: detentFader (NEW — multi-detent slide attenuator with speedbump drag physics: magnet + escape hysteresis, parked tick glow) and the drumRoller redefinition (UPGRADE — 1..N channels and min/max range; stock hardcodes two drums and throws on one, defaults reproduce stock exactly) and the guardedToggle redefinition (UPGRADE — lever throws with rotateX so it flips toward the viewer instead of the stock 180° planar spin that sweeps sideways mid-transition; contract unchanged). Merge means: builders into widgets.js, the additions CSS into DUPRE_CSS, additions-scoped gradients into the shared defs plate, gallery cards for both in panel-widget-gallery.html, and POLICY entries. Origin: the desktop-settings casting sitting 2026-07-21 — Craig's direction is that components get finished by being needed ("the ones needed most will have had the most attention"), so more additions may accrue here before the merge; batch them. +** TODO [#C] Wlogout screen review :bug:hyprland:dotfiles: +Craig, 2026-07-21: the wlogout window (Super+Shift+Q — lock/reboot/shutdown/logout/suspend/hibernate) "isn't great and has bugs." Review it end to end: catalogue the specific bugs, then assess the design against the Dupre instrument-console family (it predates the panel aesthetic). Config lives in dotfiles; the bind is hyprland.conf:428 (=pgrep -x wlogout || wlogout-menu=). Context: the desktop-settings panel spec withdrew lock/suspend in favor of this screen (2026-07-21 amendment), so it's now the sole owner of session-exit actions — worth being good. Grade each bug found via the severity×frequency matrix; this parent stays a [#C] review until specifics emerge. +** TODO [#B] Audit cgit-published repos for secrets and privacy :bug:security: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-21 +:END: +Grading: security carve-out — cgit at git.cjennings.net serves every repo under scan-path=/var/git over unauthenticated https (any repo is anonymously cloneable). archsetup being public is by design (curl-install), but this means ANY secret in ANY /var/git repo is world-readable, and any repo meant to be private is not. Severity depends on what else lives there = P2 = [#B], raise if a private repo with secrets is found. +Not :solo: — needs Craig's decisions. Steps: list repos under /var/git; for each, decide intended public vs private; scan each for secrets (keys, tokens, credentials) the way this WireGuard leak was found; for any meant-to-be-private repo, actually restrict access (cgit repo.hide only hides from the index — a known repo name is still cloneable; use http auth or move it off the public scan-path); for public repos, confirm no secrets and add a pre-receive/CI secret scan. Check dotfiles (git.cjennings.net/dotfiles) specifically — it is also under /var/git. archsetup's own move is decided and tracked separately below. +** TODO [#B] Move archsetup off cgit to cjennings@cjennings.net :chore:security: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-21 +:END: +Decided (Craig, 2026-07-20): move the archsetup repo off the public cgit host (git@cjennings.net, scan-path /var/git) to Craig's private account remote cjennings@cjennings.net, so it is no longer world-cloneable. This is the archsetup-specific fix for the cgit-exposure finding above. +Plan: create a bare repo under cjennings's control off the cgit scan-path (e.g. =~cjennings/git/archsetup.git=); push current main + tags there; migrate the post-receive hook that publishes the installer to =/var/www/cjennings/archsetup= so curl-install keeps working (the single published file stays public by design; only the repo goes private); update the origin remote on ratio and velox to =cjennings@cjennings.net:git/archsetup.git=; remove =/var/git/archsetup.git= so cgit no longer serves it. Verify: anonymous =git clone https://git.cjennings.net/archsetup.git= fails, the new private clone works from both machines, and the curl-install URL still returns the installer. Keep the two daily drivers' remotes in sync (daily-drivers rule). +** TODO [#B] Velox boot-failure retrospective — upgrade guard gaps :bug:zfs:maint: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-21 +:END: +Post-mortem for the 2026-07-15 velox no-kernel boot failure, from the archsetup/maint code review: +- maint's UPDATE remedy runs a plain =yay -Syu --noconfirm= (remedies.py:297). The live-update guard (guard.py) only matches mesa/hyprland (the 2026-06-07 live-swap class) — it never checks /boot, kernel, initramfs, or mkinitcpio exit. No post-upgrade /boot assertion exists. An interrupted kernel transaction slips straight through. +- Add a post-upgrade /boot assertion: after a transaction touching linux/linux-*, confirm vmlinuz-* + initramfs-*.img present and mkinitcpio exit 0; refuse to end the run (or page Craig) otherwise. Would have caught this. +- Sanoid-vs-actual dataset drift: configure_zfs_snapshots configures zroot/var/log + zroot/var/lib/pacman as separate datasets; velox's actual layout has neither separate (/var/log sits inside zroot/var). Reconcile. +- CONFIRMED (2026-07-21): the pre-pacman snapshot hook fired on velox — the 2026-07-15 no-kernel boot was recovered via the pre-pacman ZFS snapshot rollback, and velox is back on the tailnet running linux-lts 6.18.38 with initramfs present (2026-07-19 session). Root-cause hook-ordering fix shipped separately. Still open: the post-upgrade /boot assertion in guard.py and the sanoid-vs-actual dataset drift reconcile (the two bullets above). -Found in the 2026-07-24 sentry bug-hunt, round 13, reading archsetup's remaining state-mutating steps. =trim_firmware= gated on =grep -qi "framework" /sys/class/dmi/id/product_name= and no Framework machine has "framework" in =product_name= — it lives in =sys_vendor=. Read live: velox is =Framework= / ="Laptop (13th Gen Intel Core)"=, ratio is =Framework= / ="Desktop (AMD Ryzen AI Max 300 Series)"=. The gate returns false on both, so the step has been a silent no-op on the exact hardware it was written for. velox IS trimmed today (=linux-firmware-{atheros,intel,realtek,whence}= and nothing else) but not by this code path. +** TODO [#B] Assess a Hyprland left-drag window gesture :feature:hyprland: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-21 +:END: +Evaluate whether a global left-click drag can move ordinary windows without +breaking application selection, text interaction, or Wayland security +expectations. Document the safe modifier/gesture alternatives before changing +any binding. -THE REPAIR IS WHERE THE DANGER IS, which is why this is worth reading twice. Swapping =product_name= for =sys_vendor= is the obvious one-word fix and it is wrong: ratio is a Framework Desktop, and =trim_firmware= runs =pacman -Rdd linux-firmware-amdgpu=, which takes the firmware its Ryzen AI Max iGPU needs to bring up a display. Today only the =grep -qi intel /proc/cpuinfo= second gate stands between ratio and that. So =is_framework_intel_laptop= wants three DMI facts — vendor Framework, and a model naming both Laptop and Intel — and the cpuinfo read stays as an independent second gate rather than the only one. +** TODO [#B] Reconcile panel keybindings around Super+N :feature:hyprland: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-21 +:END: +Swap the notification and networking bindings so primary panels are one +Super-plus-letter chord away, audit the other exceptions, and bring the +proposed family to Craig for a final mapping decision. -Verified live after the change: velox TRIM=yes, ratio TRIM=no, where the old gate said no to both. +** TODO [#B] Add storage-capacity signals to the maintenance module :feature:maint: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-21 +:END: +Investigate capacity and growth diagnostics for full disks, identify the +appropriate remedies, and incorporate a clear storage signal into the +maintenance console. -Grading: Minor severity (the trim never happens; nothing breaks, the machine just carries ~550MB it was meant to shed) x every user, every time (every Framework Intel install, which is the whole population the step targets) = P2 = [#B]. The AMD-firmware removal is not graded separately because it never shipped — it is the hazard the fix is shaped to avoid. +** TODO [#B] Add per-channel controls to the audio panel :feature:audio: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-21 +:END: +Expose channel-level input and output volume controls without losing the +existing device-level workflow. -** DONE [#B] Fresh install leaves the dotfiles repo permanently dirty :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] +** DOING [#B] Widget gallery upgrades :feature:design: :PROPERTIES: -:LAST_REVIEWED: 2026-07-24 +:LAST_REVIEWED: 2026-07-13 :END: -Fixed as archsetup =c3b3617= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/installer-steps/test_mark_volatile_configs.py=, 8 tests against a fixture git repo with =sudo= stubbed on PATH. Every guard proven a real gate by deletion. A note went to =~/.dotfiles/inbox/= because =skip-volatile= now has an outside caller. +Usability + documentation pass over the [[file:docs/prototypes/panel-widget-gallery.html][panel widget gallery]], orthogonal to the component-generation spec work, so it runs on the =gallery-upgrades= branch (squash merge to main after Craig's UI confirmation + tweaks). Items 1-4 run as a no-approvals speedrun (Craig authorized 2026-07-12); item 5 is a joint brainstorm after the merge. +*** TODO Extraction-readiness bar for every gallery component :refactor:design: +Craig's standing directive (2026-07-18, set while finishing the split-flap): every =DUPRE.*= builder should meet the bar the split-flap now sets, since these become regular components. The bar: a contract comment documenting every opt and the full handle surface; no page globals touched (page owns cadence via handles/callbacks, e.g. =onSettle=); all component CSS in one named =DUPRE_CSS= block; refactored until no opportunity worth doing remains (small named helpers, no duplication); construction axes declared via =STYLES= where the component has them. Sweep the existing builders against that list, fix the gaps, and make the bar a stated convention in the widgets.js header or README so new builders inherit it. Overlaps the component-generation spec's extraction phase — reconcile there rather than doing the work twice. -Found in the 2026-07-24 sentry bug-hunt, round 13, diffing archsetup's =stow_dotfiles= against the dotfiles Makefile's =stow= target — two implementations of one operation, which is round 5's read applied across repos rather than across packages. +*** 2026-07-18 Sat @ 04:32:20 -0500 Made the N20 split-flap an honest Solari mechanism +=GW.splitFlap= rebuilt from the drop-fade fake: charset-as-drum stepping (=opts.chars= is the flap order, one flip at a time through intermediates, staggered arrival), re-aim-not-queue retargeting, and the real two-half-panel fold (WAAPI, backfaces hidden), with =animate:false= collapsing to instant jump for reduced motion. Handle grew =setText=/=chars=/=reading()=; default width 3 → 4 cells. Nine probe checks written red-first (arrival, intermediates, one-flap stepping, re-aim discriminator, instant path); technique studied from HotFX and re-derived — no license on their repo, nothing copied (reference filed in =working/retro-stereo-widgets/references/=). Review: sound; its two test-strength notes addressed in the same change. -The Makefile's =stow= target ends with =$(MAKE) skip-volatile=, setting git's skip-worktree bit on the four configs their apps rewrite in place (=btop=, =qalculate=, =calibre=, =waypaper=; the list is =volatile-configs=). archsetup stows inline with raw =stow= calls and never ran that step. So a machine archsetup installed goes dirty the first time one of those apps writes its config, and every later =git pull --ff-only= trips over paths the user never edited. Confirmed by grep: archsetup contains no =skip-volatile=, no =volatile=, and no =make stow= — yet both daily drivers carry the bits, so they came from a hand-run =make stow=, not the installer. ratio in fact carries seven, three more than =volatile-configs= lists, which is evidence the churn is real and ongoing. +*** 2026-07-12 Sun @ 12:59:48 -0500 Added the card size toggle (1x/2x/3x, default 3x) +Masthead size chips apply CSS zoom per grid; 2x/3x drop the 1320px wrap cap so wide monitors get the room. CDP-verified: 84 cards, no exceptions, fader drag and toggle click both track at 3x (drag helpers are rect-ratio based, so zoom is transparent to them). +*** 2026-07-12 Sun @ 13:42:29 -0500 Retuned the scale split per Craig +Craig: cards a fifth smaller, fonts and palette back to normal but a little larger. The toggle (now S/M/L, "widget size") zooms only the stage — L is 2.4x, M 1.7x — while card text sits at its own raised base size (wname .95rem, note .85, spec-sheet .82) and the palette left the zoom rules entirely (tiles bumped 148→176px, fonts up a notch). Grid columns widen per size step instead of zooming. Probes re-run green. +*** 2026-07-12 Sun @ 13:52:11 -0500 Raised all reading text to 12-13pt per Craig +One move: html font-size 130%, scaling every rem-based size (notes/spec sheets/palette/masthead/readouts). Verified computed: notes 13.3pt, spec sheets 12.8pt, palette 12.5pt — was ~10pt. Widget-internal px sizes untouched (they are drawing, scaled by the stage zoom). +*** 2026-07-12 Sun @ 14:07:00 -0500 Added R32 mechanical timer dial from Craig's reference +Wind-up interval timer (Controls, after R29; gallery at 85 cards): knurled coin-edge dial rotating under a fixed red index, OFF/20/40/60 at 4.5°/min, hub screw, winding arrow, PUSH TO STOP / TURN TO START engraving, red fluted stop knob. Drag winds (turn to start), the dial runs itself back down at a demo minute-per-second (reduced-motion gated), red knob click zeroes it (push to stop). Spec sheet included. Reference filed (2026-07-12-mechanical-timer-dial.png). CDP-verified: wind → T-35 MIN, tick 35→34, stop → OFF, zero exceptions. +*** 2026-07-12 Sun @ 14:21:27 -0500 Added R33 four-way rocker from Craig's reference +Navigation pad (Controls, after R32; gallery at 86 cards): recessed plate, rubber pad, four outward silver arrows, center pivot nub. Quadrant-sized click zones step a tracked cursor (readout shows direction + x/y), pressed arrow flashes gold. Click-only — noted in the spec sheet as the most Emacs-portable control in the kit (it is literally arrow keys). Reference filed (2026-07-12-four-way-rocker.png). CDP-verified: all four quadrants step and accumulate correctly, zero exceptions. +*** 2026-07-12 Sun @ 14:26:20 -0500 Timing-marker dial judged covered (Craig concurred: probable dupe) +Scope timing-marker reference (VARIABLE TIMING / MARKER SEC): a knob rotates a printed scale disc under a fixed top index — that mechanism is R02 (vernier disc under hairline), and the value-in-a-window read is R20 (drum roller). BANKED enrichment, new to the kit: the backlit active-value window — the full scale stays visible but only the selected value glows through an amber window (R02 shows all + highlights none; R20 highlights one + hides the rest). Also noted: the 1-2-5 stepped decade scale (us/ms/s) as detent content for any stepped dial. Reference filed (2026-07-12-timing-marker-dial.png); no new card. +*** 2026-07-12 Sun @ 14:29:22 -0500 Added R34 four-way toggle selector from Craig's reference +NOT a dupe of R33: the rocker is momentary (press = step), this is stateful (the lever lives in A/B/C/D). Test-panel styling: printed X/Y axes + circle + diagonal legend square, A-D label rings, corner lamps wired by printed lines, chrome ball lever that throws to the clicked quadrant while its lamp takes the light (exactly one lit, jewel-a). Click-only. Gallery at 87 cards. Reference filed (2026-07-12-four-way-toggle.png). CDP-verified: default POS C per the photo, all quadrants select, single-lamp invariant holds, lever transform tracks, zero exceptions. +*** 2026-07-12 Sun @ 14:37:46 -0500 Added R35 day-date disc calendar from Craig's reference +Watch day-date complication (Indicators, after R31; gallery at 88 cards). Judged NOT covered: N04 is two nested input knobs, R02 one disc under a hairline — a compound READOUT from independent coaxial discs is a new indicator form. Cream disc, 31 radial date numerals (upside down at bottom like the movement), weekday names twice around inside, fixed yellow hand as the read index, movement-plate screws. Live: initializes to today; click rolls midnight one day (both discs step). Spec sheet notes the honest limitation — the 31-slot disc ignores short months, exactly like the hardware. Reference filed (2026-07-12-daydate-discs.png). CDP-verified: today correct (SUN 12), click → MON 13, both disc transforms track, zero exceptions. +*** 2026-07-12 Sun @ 14:40:47 -0500 Added R36 LED dot matrix from Craig's reference (answered: what display is this) +Answered Craig's question: an 8x8 red LED dot-matrix module (64 discrete LEDs behind a tinted window, multiplexed; Kingbright/Lite-On family part) on Kilpatrick Audio's K4816 Pattern Generator, drawing its K logo. Judged new form: seven-seg/starburst are fixed segments, VFD marquee is character cells, R10 is a text LCD — a free bitmap display is none of those. Built as R36 (Indicators; gallery at 89 cards): 8x8 paintable matrix starting on the reference K (17 dots), click toggles any dot, readout counts lit. Reference filed (2026-07-12-led-dot-matrix.png). CDP-verified: K = 17/64, paint on → 18, paint off → 17, zero exceptions. +*** 2026-07-12 Sun @ 18:11:02 -0500 Built the takuzu-survey Tier 1+2: R37-R47 + N23 alarm lifecycle (gallery at 100 cards) +Craig picked option 2 from my assessment of takuzu's historical-panel-components research (60+ candidates; ~a fifth of its "new" verdicts were stale against the current kit, and the magic-eye entry re-litigated a removal takuzu itself requested 2026-07-11). Built + CDP-verified in 4 commits: 9b625d6 R37 pin routing matrix (VCS3 many-to-many, pins seat/pull, route count) + R38 dead-man button (first held-state control: pointer-capture, dwell clock, release-to-safe) + R39 rotary telephone dial (wheel winds to the stop and returns, digit pacing authentic); 109b3dd R40 breaker panel (first system-thrown control: TRIP pops a breaker to the amber mid-state, two-step reset) + R41 DSKY (verb/noun grammar, V35 lamp test, OPR ERR on bad grammar — one bug caught by probe: stale verb register re-executed on bare ENTR, fixed) + R42 cam-timer drum (procedure-as-position, self-advances after a click starts it); eb926ea R43 attitude indicator (first two-axis instrument, 2D drag) + R44 heading bug + servo needle (commanded-vs-actual, shortest-path chase); cbcad5b R45 flip-disc array (bistable pixel, scaleX flip) + R46 dekatron (pulse-stepped glow, carry blink on wrap) + R47 landing gear (spatial three-greens + transit pulse) + N23 upgraded with the alarm lifecycle (flash → ACK steadies → new alarm re-arms → RESET / TEST). All spec sheets written. BANKED as idioms, not cards: mimic diagram, oscilloscope cluster, throttle quadrant, voice-loop strip, blinkenlights, lockout-tag disabled-with-reason, knife-switch skin, decade-switch bank, normalled-jack overlay, neon-vs-jewel, VHF detent, valve handwheel, circular chart, two-hand anti-tie-down. Judged covered/stale in the doc: warning flag (R11), split-flap (N20), strip-chart (N16), odometer (N25), interval timer (R32), telegraph (R30), foot switch (R24), light gun/trackball (the mouse), guarded-toggle two-step (R29 already does it). Full regression green at cbcad5b. +*** 2026-07-12 Sun @ 18:29:52 -0500 Control-grammars reference note + the six promoted banked cards (gallery at 106) +Craig picked option 3 (note first, then build) and asked for the reading list in home's inbox. Shipped: docs/design/2026-07-12-control-grammars-reference.org (b0cebcf — the seven named grammars each with a kit exemplar, the literature from Moran CLG through MIL-STD-1472/NASA HIDH, and seven proposed taxonomy axes; the gap-finding move is crossing task x cardinality for empty cells). Reading list delivered to home/inbox (2026-07-12-1822). Then promoted six banked items to cards, CDP-verified in two commits: 5ef616f R48 knife switch (side-view blade on a real hinge; the air gap is the proof) + R49 decade box (four skirted decade knobs compose 3,500 Ω style values) + R50 two-hand safety (arm window 0.5s, same-hand and timeout both fault); 8b4978b R51 voice-loop keyset (independent monitors, exclusive talk, per-loop activity flicker) + R52 blinkenlights (live ADDR/DATA lamps folding in a clickable switch register, octal readout) + R53 circular chart recorder (day-per-revolution pen trace, fresh paper on click). Full regression green at 8b4978b, 106 cards. Still banked (genuinely idioms): mimic diagram, scope cluster, throttle quadrant, lockout tag, knife-skin, normalled jacks, neon-vs-jewel, VHF detent, valve handwheel. +*** 2026-07-12 Sun @ 18:52:41 -0500 Added R54 vertical tape + R55 twin-needle gauge from the Ki-57 panel (gallery at 108) +Craig fed a Mitsubishi Ki-57 right-panel photo (instruments identified card by card, labels read from the Japanese: 速度計 airspeed, 遠方回転計 remote tach, 昇降計 VSI, 人工水平儀 artificial horizon, 高度計 altimeter, 気筒温度計 CHT, etc.). Two genuine gaps built: R54 vertical tape instrument (scrolling scale behind a fixed amber index — the Ki-57 remote tach's form, universal in glass cockpits; drag spins the tape) and R55 twin-needle gauge (mirrored FUEL/OIL half-scales, two needles one hub, per-half drag surfaces — the banked "twin mirrored gauge" from the French jet panel, distinct from N13 whose needles are read at their crossing). Cleanup pass earlier (c4fcee6): rotary-dial digits were painted under the finger wheel — now on top; legible demo defaults for pin matrix / voice loop / DSKY PROG; caption color unified; cam labels bumped. Reference filed (2026-07-12-ki57-right-panel.jpg). CDP-verified: tape drags (2400→1329), needles independent (fuel up leaves oil, oil down leaves fuel), zero exceptions. +*** 2026-07-12 Sun @ 18:59:08 -0500 Added R56 comfort-zone crossed needles (gallery at 109) +Craig fed a brass weather-station comfort meter and correctly picked it as different from both dual meters: N13 derives a NUMBER from the crossing (iso-curves), R55 reads two needles separately; this one lands the crossing in a printed categorical VERDICT (TOO WARM / TOO DRY / JUST RIGHT — active zone goes red, a digital advantage the paper face lacks). Brass knurled bezel, cream face, temp needle from bottom-left, humidity from bottom-right, per-half drag surfaces, needles clipped to the face (probe caught tails poking past the bezel at low values — fixed). Reference filed (2026-07-12-comfort-meter.png). CDP-verified: default 72F/45%/JUST RIGHT, warm and dry verdicts flip with the active label, zero exceptions. +*** 2026-07-12 Sun @ 19:13:41 -0500 Squash-merged to main (bc93388) + cleanup round 2 +Squash merge of the 25-commit branch landed on main; branch deleted local+remote. Then a programmatic defect sweep (per-card overflow/empty/dead-readout/sparse bounding-box audit) caught R51's monitor bars streaking across the card — Chrome's CSS zoom miscomputes absolute left/right insets, fr tracks, AND stretched widths inside zoomed stages, so the keyset now uses fixed 52px grid tracks and fixed 40px flow bars (fixed px scale correctly under zoom; this is the third zoom-layout trap after the two in the same widget — noted for future stage-internal CSS: prefer fixed px inside .stagew). Seven intrinsically tiny widgets (toggle, chip, arm-to-fire, mini signal, ladder, thermometer, status lamp) got a .boost stage zoom (1.5x, compounds with the size toggle) so they stop floating in empty stages. Sweep false positives understood: clipped content (R43 horizon, R54 tape, N22 marquee, N25 counter) reports as overflow because getBoundingClientRect ignores clip; N13/N17/N28/R20/10 verified clean visually. Full regression green at 109 cards. +*** 2026-07-12 Sun @ 19:25:04 -0500 Page iteration: palette rebuilt as named instrument colors, moved to bottom; default size M +Per Craig: the palette left the top (now below Indicators) and stopped being a token dump — it is now a curated card of 36 NAMED colors grouped by role (Materials / Faces & inks / Lamps, LEDs & jewels / Screens & phosphors / Needles & controls), each with a name (Brass, Chart paper, Neon orange, Graticule green...) and where it appears on the instruments. No hex codes visible anywhere on the page (verified by innerText scan); page chrome (background, body text) deliberately excluded. Default widget size dropped 3x → M (1.7x). Data lives in a PALETTE array in gallery JS — the curation IS the data, since most instrument colors are local literals, not tokens. +*** 2026-07-12 Sun @ 13:01:27 -0500 Added the palette section +New "Palette — design tokens" section above Controls: 33 tiles read live from the generated =:root= rule via document.styleSheets (zero drift possible — what renders is what the widgets use). Hex tokens and glow rgb triples get swatches; mono/pulse-rate render as text tiles. Scales with the size toggle. +*** 2026-07-12 Sun @ 13:08:42 -0500 Added screen-color families + chips (first round) +Six period families defined (green = P1 phosphor built on =phos=, amber = P3 on the gold family, red = the nixie neon pulled out, blue = P4 white-blue, vfd = the marquee's =--vfd=, white = mono LCD), applied via scoped CSS vars with the shipped color as fallback — defaults are pixel-identical until a chip is clicked. Chips on the five clearly-screen widgets: R10 data matrix (amber), R17 CRT scope (green face), R19 waveform LCD (white), R31 radar (amber — the "in amber" one; green chip gives the classic PPI), N11 oscilloscope (green). Answers to the green question: the kit deliberately has four greens — phos #7fe0a0 (CRT trace), vfd #63e6c8 (marquee, blue-leaning), sevgrn #57d357 (LED), jewel-g #6fce33 (jewel lens); the phosphor-screen green is phos, not the others. Nixie left chipless: neon is only ever orange. Families live in gallery JS, not tokens.json — tokenize the winners once picked, the way amber earned its tokens. CDP-verified: recolors land on all five, interactions intact, zero exceptions. +*** 2026-07-12 Sun @ 13:15:19 -0500 Added spec sheets to all 84 cards +Every card now carries a collapsible "spec sheet" (a details element under the note) with up to 8 fields: input (always present, names the model — click ports everywhere including Emacs click-regions, drag needs a click/key idiom there, display widgets take no real input), solves, use (common vs specialty + where it shines), limits, origin, difficulty, prefer-when, and period (present on 41 of 84 — only where the component is clearly not timeless, e.g. nixie 1955-75 sits with keypads/telegraphs and clashes with VFD and LED seven-seg). Content lives in one INFO object keyed by card number; card() renders it. CDP-verified: 84/84 sheets present (missing-key check), fields render, zero exceptions. +*** 2026-07-14 Tue @ 02:00:49 -0500 Judged the two 2026-07-13 cross-needle references covered +The weather-station comfort meter is R56's form exactly (crossing lands in a printed categorical verdict; five zones vs R56's three is content, not mechanism). The SWR cross-needle meter is N13's defining mechanism (the crossing derives a number off printed iso-curves — forward/reflected watts → SWR). No new cards; both references stay filed in working/retro-stereo-widgets/references/. +*** DOING Widget validation pass +Craig walks all 110 cards; the lamps are his (click cycles off → amber → green, per-card localStorage key =gv-=). Gate change with the option-1 approval (2026-07-12): the lamps no longer gate the widgets.js extraction (lossless, done) — they gate per-widget Emacs ports and the final catalogue blessing. -The fix calls the dotfiles target rather than copying its logic, so the volatile list stays single-source. Two details that are load-bearing: it runs *after* =git restore .= so the bit lands on a pristine tree, and it runs as the user, because root writing =.git/index= leaves it root-owned and the user's next git command then cannot update the index at all. A checkout with no Makefile is a quiet no-op — nothing to delegate to is not an error. +Runs as a *joint loop* (Craig, 2026-07-16): he walks a batch of 10-15 and reports card numbers + what's wrong; Claude fixes them in one pass, gates on the three probes, he reloads and re-walks. The lamps track progress only — they never recorded *what* was wrong with a card, so the defects live in this task body as they surface. -DELIBERATELY NOT DONE: replacing the whole inline stow with =make -C "$dotfiles_dir" stow "$desktop_env"=. The Makefile stows =--target=$(HOME)=, which during an install is root's home, and it carries interactive conflict handling; archsetup stows =--target=/home/$username --adopt= as root on purpose. =skip-volatile= is the one target with no such coupling — it works on the repo through =git -C= and never reads HOME. +Statuses bake into the gallery source (=VSTATUS=, top of the lamp section) *periodically*, not once at completion as originally planned — localStorage is per-profile and dies with a cache clear, so at 8 cards baking is free and at 60 it's the difference between a record and a bad afternoon. Craig clicks "copy for source" under the index tally and pastes the block into the chat; the agent pastes it into =VSTATUS=. Precedence is localStorage → VSTATUS → off, so the live walk wins on the machine doing it and the baked record fills in on a fresh profile, after a clear, or on velox. Covered by =tests/gallery-probes/probe-vstatus.mjs=. -Grading: Minor severity (a repo that reads dirty forever and pulls that need a stash; the workaround is one command) x every user, every time (every fresh install that stows dotfiles) = P2 = [#B]. +*The page can't copy for you; you press Ctrl+C* (settled 2026-07-16). Clicking "copy for source" drops the export into a selected textarea at the bottom-left of the page. Ctrl+C it and paste it into the chat. From a =file://= origin Chrome refuses both programmatic copy paths — =navigator.clipboard.writeText= rejects NotAllowedError, and =execCommand('copy')= returns *true while writing nothing*, which is worse because the page then claims a success it never had. A hand-typed Ctrl+C works fine, so nothing about the clipboard is broken: a Wayland-set clipboard crosses to X11 and Emacs reads it correctly (sentinel-verified against =gui-get-selection=). -** DONE [#C] Unattended install blocks on an interactive prompt :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =cbcb53f= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/installer-steps/test_configure_autologin.py= (11) and =tests/installer-steps/test_select_locale.py= (11). Every guard proven a real gate by breaking it and watching the suite go red: dropping the autologin unattended branch fails 1 (on the leftover-stdin assertion, which is the real gate — the drop-in still gets written because the read swallows the sentinel and treats it as "yes"); dropping the locale unattended branch fails 1; breaking either precedence rule fails 2. +*And the agent doesn't fetch it.* Never read the export with =wl-paste --primary=: PRIMARY holds whatever was last selected anywhere, so a blind read returns unrelated content (it surfaced a private SMS during this session). Craig pastes; the agent doesn't reach for it. -Found in the 2026-07-24 sentry bug-hunt, round 12, continuing through archsetup's own installer. Two members of one class, which is the point: round 10 fixed the third member and left these. +Progress: 8 green + 2 amber (R07, R52) of 109, baked 2026-07-16. +*** 2026-07-16 Thu @ 08:11:18 -0500 Ran the classification brainstorm — two empty cells and a missing axis +Brainstormed with Craig and wrote the result into [[file:docs/design/2026-07-12-control-grammars-reference.org][the control-grammars reference]] ("The brainstorm, run"). Crossed Foley's six elemental tasks against cardinality over the 109 cards. -THE CLASS: an advisory prompt — one that carries its own default — still reading stdin under =--config-file=, the documented unattended mode. Round 10 ruled on it for =nvidia_preflight='s rc-10 prompt. Two sites never got the ruling. +Added *axis 8, set stability* (fixed at manufacture vs discovered at run time). The doc's original seven axes cannot find the kit's largest gap: axis 4 lumps all one-of-N together, so a chicken-head selector and a WiFi list share a cell that then reads as densely covered. The axis is invisible from inside hardware — every one-of-N in the kit has its positions engraved at manufacture — so a catalogue derived from period hardware inherits a blind spot exactly at dynamic sets, which is what its software consumers are made of. -1. =configure_autologin=. When =enable_autologin= is unset (=AUTOLOGIN= is optional, and =archsetup.conf.example= line 31 ships it commented out) and the root is encrypted, it prompted =Enable automatic console login for $username? [Y/n]= on a bare =read=. It runs from =configure_encrypted_autologin=, inside =boot_ux=, the last entry in =STEPS= — so an unattended install of an encrypted machine works for 40-60 minutes and then sits at a prompt nobody is watching. Under =curl | bash= it is worse: stdin is the script itself, so the read eats a line of source. +Two empty cells, both wanted by live panels today: *select x one-of-N dynamic* (the bt device list, the net network list) and *text x alphanumeric* (the WiFi password; the kit enters digits three ways and text zero ways). Foley names six irreducible tasks; the kit serves five and a half. -2. =select_locale= (extracted from =preflight_checks= by this commit). The =Choice [1]:= menu fired whenever =/etc/locale.conf= carried no =LANG== and =LOCALE= was unset — also commented out in the example config. archsetup does not require an archangel install, and =configure_build_environment='s own "no LANG=" branch is proof it expects that state. +Period models proposed rather than invented, per Craig's aesthetic gate: the answer to a changing set was never a control but a *re-labelable slot* — jukebox title-strip rack, Rolodex, scribble strip, switchboard strips. The slot is manufactured, the label is not. -Both now take the prompt's own default under =--config-file= and print an =[OK] ... (unattended, --config-file)= line saying so. An explicit =AUTOLOGIN=yes/no= or =LOCALE== still wins; the default only answers a question nobody can. +Also found: the gallery cannot build its own chrome from its own kit (swatch chips, validation lamps, size toggle are all bespoke HTML; the size toggle duplicates card 06). That is a free completeness test worth re-running as the chrome grows. -WHAT MADE THEM TESTABLE, which is round 10's read (d) applied again: =configure_autologin= hardcoded =/etc/systemd/system/getty@tty1.service.d= and =select_locale= hardcoded =/etc/locale.conf=, so neither could run against a fixture — while their siblings =replace_sudoers_pacnew= and =ensure_nvme_early_module= both take a defaulted path argument for exactly that reason. Both now do. Zero shellcheck delta against HEAD; =make test-unit= 276 -> 298, exit 0. +Still open, recorded in the doc: the full seven-axis classification, the card-by-card audit behind the first-pass cell assignments, and the display side. -Grading: Major severity (unattended installation, a documented feature, does not complete; recoverable by pressing a key, no data loss) x some users, sometimes (needs unattended mode plus an omitted key) = P3 = [#C]. +*** DOING Build the four widgets the taxonomy found :feature:design: +From the [[file:docs/design/2026-07-12-control-grammars-reference.org][taxonomy brainstorm]] (2026-07-16). Craig's gate: stick to the aesthetic — model a period control, don't invent a software-native one. Reference photo first per the usual pipeline, then judge, then card. -THE PROMPTS DELIBERATELY LEFT ALONE, because the class is "prompts with a default", not "all prompts": username (line 636) and password (648/650) have no default to take — there is no sane fallback for either, and =archsetup.conf.example= documents both as "If not set, you will be prompted". They also fire in =preflight_checks=, in the first second of the run, where a blocked prompt is visible rather than silent. The "Enter locale" sub-prompt is reachable only from menu choice 9, which unattended never picks. +1. *Dynamic list* — model the jukebox wallbox title-strip rack (page-flip browse, rich slotted strips, select one). Fills the most damning cell; the bt and net panels both want it. Alternative model: Rolodex card spinner, better for long sets and weaker at showing several rows at once. +2. *Alphanumeric entry* — model the *industrial ABC-order keypad*. Craig's four references (=working/retro-stereo-widgets/references/2026-07-16-abc-keypad-*.png=, =-letter-drum-bank.png=) plus a survey superseded the teletype: the references are all ABC-order, not QWERTY, on stainless or membrane, with colour-coded function keys (yellow CANCEL, red CLEAR/NO, green ENTER/YES) that land on the kit's palette without translation. A faceplate, not furniture. +3. *Index typewriter* — stylus over a printed index plate, print lever commits (Hall 1881, AEG Mignon 1924). The survey's strongest find: select and commit on two separate controls is a grammar the kit has no exemplar of, and it is small and beautiful. Second card after the keypad. -** TODO [#D] net-scenarios harness times out under back-to-back suite runs :test:tooling: -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -=tests/net-scenarios/test_run_net_scenarios.py= errored on all 5 tests twice during round 12, each time =subprocess.TimeoutExpired= after its 20s budget on =scripts/testing/run-net-scenarios.sh --target root@fake-vm=. Both occurrences were in =make test-unit= runs launched immediately after a previous full run. It then passed 6 runs in a row (3 on a pristine tree, 3 with the round-12 change), and standalone it finishes in 0.09s, so this is not a regression from any code change. + Craig's brief (2026-07-16, after reading the AEG Mignon Model 4 reference): *reproduce the device, keep its extended character set, treat the layout as ours.* What he admires is that the plate considered high-ASCII at all — accents, section mark, fractions, the full punctuation ring — and it carries both cases with no shift key, which is how a keyless machine reaches a whole character set. What he doesn't admire is the Mignon's key order: the real plate runs =P U G Q / V I N A B / L D E T M= (a frequency layout), which you cannot read your way around. "We'll revisit the keys and the layout. The best ideas will be preserved." -The harness stubs =ssh=, =rsync= and =jq= onto =PATH=, so nothing should touch the network at all — which is what makes a 20s timeout suspicious rather than merely slow. Worth reproducing under load before deciding whether the fix is a larger timeout or a real hang in the script. Evidence logs from the round: =/tmp/tu.log= and =/tmp/tu2.log= (tmpfs, gone after reboot). + So the layout ships as *data, not drawing* — a table the builder renders — because a revisable layout that requires touching the mechanism won't get revised. First pass: alphabetical, capitals block beside lowercase block at the same column offset (find the letter, then pick the case), the Mignon's two-block structure with a legible order. Expect it to change. -Not graded on the bug matrix: it is test infrastructure, not the shipped codebase. + References: =working/retro-stereo-widgets/references/2026-07-16-mignon-aeg-detail.jpg= (Model 4, casing off, plate legible; CC BY-SA, Uwe Aranas, attribution required if reproduced) and =-mignon-4-index-typewriter.jpg= (museum context; public domain). Credits in =2026-07-16-mignon-CREDITS.txt=. Reference only, drawn from scratch. -** DONE [#C] wipedisk says "Disk erased." when it erased nothing :bug:tooling:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =HEAD= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). New =tests/wipedisk/test_wipedisk.py=, 6 tests running the real script against a fixture device directory with fake blkdiscard/sgdisk on PATH. All four guards proven real by deleting each and watching the suite go red. +**** 2026-07-16 Thu @ 16:39:05 -0500 Built R58 index typewriter (gallery at 111) +Item 3 done. =GW.indexPlate=: click a cell to move the stylus, pull the lever to print, and only the lever prints. The first card where selecting and committing are separate acts. Keyboard follows the same rule — typing SELECTS, Enter is the lever — because a keypress that printed would make this R57 with a nicer plate. -Found in the 2026-07-24 sentry bug-hunt, round 11, reading =scripts/= — 30 lines, no tests, and the most destructive script in the repo. Not installed by the installer; it is run by hand from the checkout, which is why the frequency axis stays low. +No case folding, unlike R57: the plate holds both cases as distinct cells, so Shift does the work a shift key would on a machine that has none. =KEYS= and =ACTIONS= are both *derived* from =LAYOUT= at load, so rewriting the table relays the plate, the keymap and the allowlist together — a binding aimed at a character the plate no longer carries isn't expressible. -Three defects, all of which make the script's final word untrue: +Craig's brief honoured: kept the extended set (Ä Ö Ü ä ö ü ß § ½ ¼ and the punctuation row) and the no-shift both-cases plate; dropped the Mignon's frequency order for alphabetical, capitals beside lowercase at the same column offset. -1. =sgdisk --zap-all= had its result discarded, and "Disk erased." printed unconditionally. sgdisk refuses a busy device — a mounted filesystem or a live md/LVM/ZFS holder — which is exactly what a user hits after picking the wrong disk. So the tool announced an erase it had not performed and exited 0. +Review (subagent) cleared the grammar — no path where selecting prints, none where the lever prints anything but the resting selection — and found five defects, all fixed: +- *Clicks bypassed press().* The contract says click and key must both route through it; R57 obeys, R58 had three direct handler bindings. No divergence yet, only because press was a pure dispatcher — add a guard to it and the mouse, the primary input on this card, would silently skip it with every probe green. Exactly the drift the rule exists to prevent, in the second card written against the rule. +- *Check 14c could not fail.* =/ss$/= on the readout also matches 'sss', so it passed even if selecting printed — blind to the one bug the card is about. Now exact, against the buffer. +- *cells{} collided on duplicate layout characters* and resolved =select('constructor')= through Object.prototype. Now =Object.create(null)=, plus a check that the table has no duplicates (14e couldn't see it: a duplicate inflates the drawn and declared counts equally). +- *Geometry broke on SHRINK.* The lever and legend anchor to the plate's top, CLR anchored to the viewBox, so a five-row table rode CLR up over the PRINT legend. Growth was always safe; shrink was the trap. VH now takes a floor, and I verified it by building the plate at 4, 5 and 9 rows rather than trusting the arithmetic: 190/192/280, no collisions. +- *No allowlist coverage on press().* R57 had it, R58 didn't. -2. "Disk erased." overstates what the tool does even on success. =sgdisk --zap-all= destroys partition tables, not data, and =blkdiscard -f ... || true= deliberately tolerates a device that cannot discard. On a disk without discard support the script cleared the partition table and left every byte readable, while telling the user the disk was erased. That is the one path where the wrong belief has a privacy consequence — someone trusting the message before disposing of a drive. +Known gap, Craig's to weigh when he revisits the keys: no space cell, so Space isn't in KEYS and still scrolls the page (correct per the contract). You can type Mignon-4 but not "Mignon 4". The real machine has a separate space key. +4. [@4] *Chorded keyset* — six keys, no key per letter; the chord IS the character (Microwriter 1978, Perkins Brailler 1951). The most compact honest password field. The kit names the chorded grammar already (R50 is its safety form) but holds no chorded text control. Third, if a compact field is wanted. +5. [@5] *Swatch picker* — model the signal-lamp lens turret or theatrical gel wheel; a rotary whose detents are coloured lenses. Undecided whether it earns a card or stays page furniture. +6. [@6] *Legend switch* — the lit pushbutton whose cap legend and colour ARE the state, press to step (MIL-STD-1472 catalogs it; the doc already cites the standard). The validation lamps are one. -3. The prompt says "Select the disk id to use" and then listed every entry in =/dev/disk/by-id=. On this machine that is 18 entries of which 12 are =-partN= partitions (verified by listing it). The menu promised disks and offered partitions. +Items 1 and 2 fill the two empty cells and are wanted by live panels today; 3 and 4 are the survey's genuinely-new grammars; 5 and 6 come from the gallery-can't-build-itself finding and are lower stakes. -Fix: whole disks only (globbed rather than =ls | grep=, so a name with whitespace cannot split into two menu entries); the zap's result is checked and a failure exits 1 naming the busy-device cause; the closing message reports what actually happened, and when discard was unsupported it says the data is still recoverable and points at =nvme format= / =hdparm= for a disposal-grade wipe. +**** 2026-07-16 Thu @ 13:53:34 -0500 Built R57 ABC entry keypad — the text x alphanumeric cell is filled (gallery at 110) +Item 2 done. Modelled on the membrane reference (=working/retro-stereo-widgets/references/2026-07-16-abc-keypad-membrane-color.png=): a 0-9 block beside A-Z in alphabetical order, CLR / ENT / CANCEL, typed text in an amber window, 16-char cap with the window showing the tail. The kit's first free-text control, and R16's alphanumeric sibling. -Grading: Major severity (the tool reports an outcome it did not achieve; in the disposal case that is a data-exposure consequence) × rare edge case (a hand-run helper the installer does not install, and defect 1 additionally needs sgdisk to fail) = P3 = [#C]. +Two deliberate departures from the photo, both recorded in the builder comment: its letters are *blue*, and the kit has no blue control colour (blue is only ever a screen phosphor or a jewel lens), so letters take the standard pale keycap and digits a darker one — which keeps the photo's two-tone digit/letter grouping without importing a foreign hue. CANCEL is amber, borrowed from the sibling reference whose CANCEL is yellow; CLEAR/NO and ENTER/YES keep the reference's red and green, which are already =--fail= and =--pass=. -Worth recording about the tests rather than the code: two of the six passed against the unmodified script for the wrong reason. Without the =WIPEDISK_BY_ID= override the script read the real =/dev/disk/by-id=, so the harness was driving a menu of this machine's actual disks (harmless — the fake blkdiscard/sgdisk shadowed the real ones on PATH — but it was not testing the fixture). And =test_empty_by_id_directory= was not a gate at first: with the guard deleted the empty select menu still falls through to the confirm prompt, reads EOF and declines, so exit code and call log alone pass either way. It now asserts the message. -** DONE [#B] zfs-replicate reports success when every backup failed :bug:backup:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =HEAD= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). Diagnostics moved to stderr; the loop counts failures and exits 1 when any dataset failed. New =tests/zfs-replicate/test_zfs_replicate.py=, 9 tests driving the real script with a fake syncoid and a fake ping on PATH (the =tests/zfs-pre-snapshot/fake-zfs= pattern). Both fixes proven real gates by reverting them: dropping the counter fails 3, putting =error()= back on stdout fails 1. +TDD: four probe checks written first and confirmed red (A-Z in alphabetical order — a QWERTY drift would silently lose the idiom; a full 0-9 block; typed characters accumulate in order; ENT commits while CLR empties). All green, plus the card count bumped 109 → 110. Verified visually in both the empty and typed states rather than trusting the green run. -Found in the 2026-07-24 sentry bug-hunt, round 11, reading =scripts/= — 73 lines with no test file, installed by =configure_zfs_snapshots= as =/usr/local/bin/zfs-replicate= and run by =zfs-replicate.service=, a =Type=oneshot= on a nightly timer. Its exit code and its journal output are the only signals anyone ever sees. +Craig then caught a layout flaw the membrane reference itself carries: with the digit block on the left, A-L sits in columns 3-5 while M-X starts at column 0, so the alphabet stops column-aligning with itself halfway down and the eye has to jump. Swapped to letters-left / digits-right, which is the *stainless* reference's arrangement — so the card is now a synthesis of the two photos (membrane colour-coding, stainless layout) and reads A-Z down one unbroken block. Card note and spec-sheet origin updated to credit both. -Two defects, both verified by running the script rather than argued: +That swap also exposed a hole in my own checks: the A-Z check reads DOM order, which the builder controls by push order, so it would pass with every key rendered in the wrong place. Added a geometric check (letters left of digits, function block on the digit side, and A/D/G/J/M/S/Y sharing one x) that pins the layout Craig asked for. -1. The full-replication loop caught each =syncoid= failure, warned, carried on, then printed "Replication complete." and exited 0 regardless. Driven with a fake syncoid failing all four datasets: four =[WARN] Failed= lines, then "Replication complete.", exit code 0. systemd records =Result=success=. A backup that has not run for months is indistinguishable from a working one — and the whole point of the tool is to have a copy when the primary is gone. +Craig then caught the missing backspace: with only CLR, one mistyped character costs the whole entry. Added DEL, and on his call it sits in the digit block wearing amber while CLR is exiled to the far corner in red. Both swaps pull the same way — DEL is the key you reach for constantly and costs one character, CLR is the one you reach for almost never and costs the entry, so the safe key gets the good spot and the colour grades the cost before you read the legend. The three function keys now read as a ladder: amber takes one back, red throws it all away, green commits. -2. =determine_host= runs inside a command substitution (=TRUENAS_HOST=$(determine_host)=) and its =error()= wrote to stdout. On an unreachable TrueNAS the message was captured into =TRUENAS_HOST= and discarded, and =set -e= then killed the script. Driven with both hosts unreachable: exit 1 and completely empty output. A nightly service failing with nothing in the journal to say why. +And he caught that CLR and CANCEL did the same thing — both just emptied the buffer, differing only in the readout string. Dropped CANCEL. The reference plate carries it because on that device the plate IS the whole terminal and has a transaction to abandon; here the keypad is one control inside a panel that owns its own dismiss, so CANCEL meant nothing the panel didn't already mean. SPACE widened to fill, DEL took the rightmost slot, and the now-unused amber gradient and tone came out with it. The only colour left is CLR and ENT, the two irreversible keys. -Same class as three bugs already fixed this session — =_restore_dot= claiming "DNS-over-TLS restored" without checking, =portal_restore_watch= discarding its outcome, =import_config= returning ok on an unchecked modify. A mutating operation that reports a success it did not get. +Adding the DEL check also caught order-dependence in my own suite: it emptied the buffer that the ENT/CLR check had been inheriting from the check above it — the same defect the reviewer found in check 9 this morning. That check now types its own buffer. -Grading: Critical severity (a backup system that reports success while backing nothing up; the failure surfaces only when the backup is needed — graded on the harm once in the failure state, not on how rarely it is entered) × rare edge case (needs a ZFS root, a reachable TrueNAS, and the user enabling the timer by hand — archsetup deliberately does not enable it, and =findmnt -n -o FSTYPE /= on this machine says btrfs, so it is latent here) = P2 = [#B]. +Review (subagent, since I wrote it all) cleared the widget itself — buffer logic, layout arithmetic, and the gradient id space all traced clean — and found three real defects, all fixed before the commit: +- *A typed space was invisible past 13 characters.* SVG collapses trailing whitespace, and past the display's truncation boundary there are no pad dots left for a space to displace, so SPACE became a keypress with no feedback: press, see nothing, press again, and carry two spaces you can't see in a passphrase you can't read back. The window now draws spaces as =␣= while the buffer keeps the real character. Verified by eye that the glyph isn't a tofu box. +- *The ENT check could not fail.* It typed NET5, pressed ENT, and asserted =/NET5/= — which typing the 5 had already made true. Deleting the whole ENT branch still passed, because the key then fell through to =buf += 'ENT'= and NET5 still matched. It now asserts the commit signal itself. +- *The DEL check asserted half its name*, computing the past-empty state and never looking at it. -Left alone: =BACKUP_PATH="backups" # TODO: Configure actual path= is still an unresolved TODO in the destination, and single-dataset mode relies on =set -e= to propagate a syncoid failure rather than reporting it. Neither is a defect in the sense above; the TODO is Craig's call. -** DONE [#D] Wireless regdom is silently unset for a three-letter-language locale :bug:installer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =249bb93=. =locale_country= matches the =_CC= group instead of counting characters, and =set_wireless_regdom= verifies the substitution landed rather than trusting sed's exit code. 16 tests. -=configure_networking= derives the wireless regulatory domain by fixed offset: =wireless_region="${current_lang:3:2}"=, with a comment reading "extract country code (positions 3-4)". That is correct only for a two-letter language code. +Third vacuous check in one day, and the shape is consistent: checks written in the builder's own vocabulary inherit its assumptions and confirm what the code does rather than what it should do. The one that mattered was found by someone reading the render arithmetic cold. -=validate_config= accepts =^[a-z]{2,3}(_[A-Z]{2})?...=, so a three-letter language is a legal =LOCALE=, and glibc ships 75 of them (=agr_PE=, =ast_ES=, =ber_DZ=, =ayc_PE=, ...). Verified by running the expansion: =ber_DZ.UTF-8= yields =_D=, =ayc_PE.UTF-8= yields =_P=, =C= yields the empty string, =POSIX= yields =IX=. +**** 2026-07-16 Thu @ 14:42 -0500 Keyboard contract recorded, R57 built against it +Craig asked whether keyboard input is the widget's job or its container's, and picked "record the contract first, then build against it" — it's a decision about all 110 cards, not one. -The sed that follows only uncomments an existing =#WIRELESS_REGDOM="XX"= line in =/etc/conf.d/wireless-regdom= (176 of them, owned by wireless-regdb). A garbage region matches nothing, sed exits 0, and the =|| error_warn= never fires — so the regdom is never set and nothing says so. The task line does print the garbage region ("configuring wireless regulatory domain (_D)"), so it is visible in the log rather than fully silent. +The kit already held the answer in two halves. =GW.slideRule= takes arrow keys scoped to its *own focusable element* (the browser arbitrates focus, which is why it never fights the gallery's global Escape handler at line 1157) — that's the web-correct pattern. But Emacs can't do that at all: the SVG region is an image and never sees a keypress, so the mode's keymap must own delivery and call =press=. Same split as the tick contract, which the README already states: the page owns the ambient resource (the clock there, focus here), the builder exposes a handle. -Confirmed the mechanism itself works for the normal case: line 168 of this machine's =/etc/conf.d/wireless-regdom= reads =WIRELESS_REGDOM="US"= uncommented, which is archsetup's own edit. +Contract written into [[file:docs/prototypes/README.org][the widget-library README]] beside the tick contract: *the target owns focus and delivery, the builder declares what it accepts* via a =KEYS= table. Deliberately a table and not a function over a DOM event — the Emacs port installs it into a keymap and never sees a keydown, so a function would force it to re-derive the widget's intent and the two bindings would drift. Five rules, each one a bug otherwise shipped: never listen on =document=; spend =preventDefault= only where there's a default worth killing (Space scrolls, Backspace navigates back); let Tab and Escape bubble (Tab is how the page stays navigable, Escape belongs to the audit stepper); =press= filters rather than trusts (it appends whatever it's handed, so a stray F1 would land as text); click and key both route through =press= so they can't drift. A widget with no =KEYS= table takes no keys, which is most of them — the kit is click-first and that's what makes it port. -Grading: Minor severity (WiFi falls back to the conservative "00" regdomain — fewer channels and lower tx power, but WiFi works) × rare edge case (one of 75 three-letter-language locales, or a =LOCALE= with no country) = P4 = [#D]. +R57 built against it: =tabindex=, focus on click, element-scoped keydown, gold focus ring (an unlit focus state means typing vanishes into a card you thought was live). Five checks written first and confirmed red, including the contract's own rules — unfocused keys ignored (the no-document-listener rule, tested behaviourally), Space and Backspace claimed, Tab/Escape/F1 let through, unmapped keys dropped. Audited the kit: no document-level key listener anywhere in widgets.js, so the contract holds retroactively. -Fix when it comes up: derive the country from the =_CC= group by pattern rather than by offset, and warn when it cannot be derived or when the sed changed nothing. Worth doing together with the sibling gap — nothing in the installer verifies that a =sed -i= uncomment actually matched, so a distro reshuffling one of these config files would fail the same silent way. All 22 =sed -i= sites share that stance, so it is a uniform design choice rather than an odd one out. -** DONE [#B] Initramfs hook swap can leave a LUKS machine unbootable :bug:installer:solo: -CLOSED: [2026-07-24 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-24 -:END: -Fixed as archsetup =HEAD= (committed locally, deliberately NOT pushed — held for Craig's morning review of the sentry run). The swap moved into =switch_udev_hook_to_systemd=, which declines when =hooks_need_busybox_init= sees a standalone =encrypt= token, and the caller now rebuilds the initramfs only when the conf actually changed. New =tests/installer-steps/test_switch_udev_hook.py=, 10 tests; both guards proven real by breaking them (removing the refusal: 4 failures; loosening the token match to a bare =encrypt= substring: 1 failure). +Spec sheet reworded. "Click-only, so it ports to Emacs unchanged" stopped being true, and the honest version is the opposite: keys are the *more* native idiom in Emacs, so the card ports better, not worse. -Found in the 2026-07-24 sentry bug-hunt, round 10. =configure_initramfs_hook= ran =sed -i '/^HOOKS=/ s/\budev\b/systemd/'= on any non-ZFS root, then =mkinitcpio -P=. Its only guard was =is_zfs_root=. +Review (subagent) found two High defects in the keyboard work, both fixed: +- *The contract's own rule was unmet by the commit that recorded it.* The README says "press filters, it does not trust", and the filter went into the keydown handler instead — leaving press wide open for the one caller the table exists to serve, since the Emacs port installs KEYS into a keymap and calls press directly with no handler in the stack. Now gated on =GW.abcKeypad.ACTIONS= (the plate's whole vocabulary), which is a superset of the KEYS values because CLR is a real key no keystroke reaches. +- *The focus ring never appeared on the path anyone uses.* =:focus-visible= doesn't match a mouse-driven focus on a non-text element in Chrome, and clicking the plate IS how it takes focus, so the ring showed only when tabbed to. Now =:focus=. +Also: two more weak checks. One claimed click and key can't drift while only reading a string (now enters the same sequence both ways and compares buffer + readout); one asserted Tab/Escape/F1 aren't swallowed, which couldn't fail because those keys return before the preventDefault (now also checks 'A' and Enter, which are mapped and reach the same code path). -Why that breaks a LUKS machine, verified against the installed mkinitcpio rather than argued: -- =/usr/lib/initcpio/install/systemd= line 70 is =add_symlink /init usr/lib/systemd/systemd=, so the systemd hook replaces the busybox init outright. -- =/usr/lib/initcpio/hooks/encrypt= is an =#!/usr/bin/ash= script whose entire body is a =run_hook()= function — the busybox init's mechanism. Under systemd init nothing calls it. -- =mkinitcpio= carries no conflict check for the pairing (grepped; nothing), so the rebuild succeeds and archsetup reports success. -- This machine's own =/etc/mkinitcpio.conf= documents the two valid pairings as separate examples: =udev= + =encrypt= (line 45) and =systemd= + =sd-encrypt= (line 51). The sed converted half of the first pairing and produced neither. +*** 2026-07-16 Thu @ 15:20 -0500 Screen families on the keypad window, and a probe that was lying +Craig: offer the display in every colour, including the vfd marquee cyan. The window now takes the =--scr-*= vars with the shipped colours as fallbacks, so the default is pixel-identical until a chip is clicked (=--gold-hi= IS the amber family's =--scr-hi=, which is what makes that exact rather than close). Both the glass and the ink recolour — a screen that changes its text and keeps its backlight isn't a screen. R57 offers all six families where its siblings each carry five: a passphrase window has no reason to prefer one phosphor. -Effect: on a LUKS root using the standard busybox =encrypt= hook, archsetup rewrites HOOKS to =systemd= while leaving =encrypt= behind, rebuilds the initramfs, and exits cleanly. At the next boot the root is never unlocked. The machine needs live media and manual mkinitcpio surgery to recover. +Found a real defect while adding the checks, and a nasty one: *probe-fams.mjs had no try/finally*, so a failing check orphaned its headless browser. The next run then connected to the survivor on the same port and reported results from a STALE page — old widgets.js, chips already clicked. It cost a confusing debug loop: R10's default ink read as green and R57's chips read as absent, neither of which was true of the actual page. A probe that answers from the previous run is worse than one that crashes. Wrapped the checks in try/finally like its two siblings already had, and swept nine orphaned chromes (the pattern is bracketed on the debug port, so it cannot touch Craig's daily browser, which has none). -The sibling asymmetry: =is_encrypted_root()= already exists in this script and =configure_autologin= uses it to branch on exactly this condition. The initramfs step consulted neither it nor HOOKS. =merge_grub_cmdline='s own comment names =cryptdevice== as a boot-critical parameter to preserve — and =cryptdevice== is read only by the =encrypt= hook, so archsetup explicitly anticipates the configuration that another of its steps then breaks. +Craig called the ABC entry keypad done at 110 cards. -Grading: Critical severity (the machine will not boot and recovery needs external media — graded on the harm once in the failure state, not on how rarely it is entered) × some users, sometimes (LUKS-encrypted non-ZFS root using the busybox =encrypt= hook; deterministic for those machines, absent everywhere else) = P2 = [#B]. +Banked from the survey, not cards yet: CDU/MCDU scratchpad + line-select keys (a staged-commit *flow*, not a new selector), joystick scroll-and-fire alphabet, multi-tap/T9 letter cycling (the most practical small-panel password entry), trackball gesture (Atari Quantum), keypunch program drum (IBM 029), Enigma lampboard (a *feedback* idiom — steal the 26-lamp grid as a readout, not as entry), Teletype Model 15 tape perforator. -Deliberately not attempted: migrating =encrypt= to =sd-encrypt=. That means rewriting the kernel cmdline from =cryptdevice== to =rd.luks.name== against the volume's UUID, which is a real migration and not a mechanical edit. Refusing the cosmetic swap keeps a working machine working, which is the right trade against quieter fsck output. -** DONE [#D] keymap and consolefont hooks are inert under the systemd initramfs :bug:installer:solo: -CLOSED: [2026-07-24 Fri] +Two flags carried from the survey: no true A-Z *thumbwheel switch* could be verified (Digitran/Grayhill mil-spec wheels are 0-9 or hex only), so the letter-drum reference is a *combination lock* and a card must say so rather than imply a switch that may not exist; and the trackball-gesture details are single-sourced. + +*** TODO Taxonomy audit — verify the first-pass cell assignments :design: +The cross's cell assignments were read off card names and spec sheets, not audited card by card, so "confidence of completeness" isn't earned yet. Walk all 109 against elemental task x cardinality x set stability, cell the display side (left uncelled deliberately), and argue the four untouched axes (grammar, state authority, persistence, era). Would either confirm the two empty cells or find more. +** DOING [#B] Retro widget catalogue :feature:design: :PROPERTIES: -:LAST_REVIEWED: 2026-07-24 +:SPEC_ID: 3ac0d42c-db1a-4d21-bce4-e63785fef0ba +:LAST_REVIEWED: 2026-07-13 :END: -Fixed as archsetup =249bb93=. The swap rewrites both to =sd-vconsole=, collapsing them into one entry and never duplicating an existing one. The open question is answered: this machine is KEYMAP=us with no encrypt hook, but the function runs on LUKS machines where a non-US layout at the passphrase prompt is exactly what sd-vconsole restores. 7 tests. -Same class as the =encrypt= bug above, but cosmetic rather than boot-critical, so it was filed rather than bundled into that fix. +The panel widget gallery ([[file:docs/prototypes/panel-widget-gallery.html][docs/prototypes/panel-widget-gallery.html]]) grows into a retro-instrument component catalogue: reference photos of period hardware → gallery cards (the visual + behavioral spec) → reusable components for three targets (emacs svg.el, web/React, waybar). Tokens single-sourced in [[file:docs/prototypes/tokens.json][tokens.json]] (gen_tokens.py emits web/waybar/elisp); svg.el proof widget shipped (gallery-widget.el, needle gauge). Reference photos live in [[file:working/retro-stereo-widgets/][working/retro-stereo-widgets/]]. Collection converged at R56, then reopened at R57 as the taxonomy found empty cells (110 cards, all behaviorally verified; probes in [[file:tests/gallery-probes/][tests/gallery-probes/]]). -Enumerating the busybox-only hooks on this machine (every hook under =/usr/lib/initcpio/hooks/= defining =run_hook=/=run_earlyhook=/=run_latehook=) gives: btrfs, consolefont, encrypt, grub-btrfs-overlayfs, keymap, memdisk, resume, sleep, udev, usr. All go inert once =/init= is systemd. Of those, =encrypt= is the only boot-critical one — =resume= is handled natively by systemd's hibernate-resume generator, and =btrfs= by udev rules (this machine runs =btrfs= alongside =systemd= and boots fine). +Build runs per the [[file:docs/specs/2026-07-12-component-generation-spec.org][component-generation spec]] (DOING; reviewed + decomposed 2026-07-12): web extraction first (ungated, lossless), then demand-gated Emacs/waybar ports. Banked variant/composition ledger lives in the 2026-07-11/12 session archive. +*** TODO [#B] Rebuild the magic-eye tube component :feature:design: +Reinstate the magic-eye tuning/level indicator (EM34/EM84/6E5 family), but +replace the earlier weak UI rather than reviving it unchanged. The component +must read as a real glass tube: P1-green phosphor bloom, a smooth shadow-angle +response, and a clearly different tuning-minimum / record-level interaction. +Build it to the current extraction-readiness bar with probes and a visual +comparison before proposing ports. Reference: [[https://en.wikipedia.org/wiki/File:Em11-ani.gif][EM11 animation]]. +*** TODO [#B] Add a wind-direction rose component :feature:weather:design: +Bank the weather companion from the Home handoff: a 16-point meteorological +wind rose that reads source direction and optional speed, distinct from the +aviation heading selector. Revisit when the weather surface needs more than +the compact vane/speed slot; no implementation before that demand exists. +*** 2026-07-19 Sun @ 15:40:01 -0500 Filed component and hardware references +Inbox zero filed the clock/chronograph concepts in +[[file:working/clock-display-references/][working/clock-display-references/]] +and the knob, split-flap, and display references in the existing +[[file:working/retro-stereo-widgets/references/][widget references]]. The Home +component handoff was reconciled: Craig reinstated the magic eye subject to a +better tube UI; the wind rose is banked for a future richer weather surface. +*** DOING [#B] Weather kit integration :feature:weather:dotfiles: +[[file:working/weather-kit-integration/][Working package]] received from Home on 2026-07-18. Land the shared, dependency-free =weather= CLI in dotfiles first, with cache/location/rendering tests; then wire a compact Dupre Waybar surface and port the supplied chip renderer to svg.el when its placement and interaction contract are settled. The chip itself remains current-conditions-only. Its hover exposes the next six hours; clicking it toggles a separate five-day forecast panel with each day's high/low and sunny/rainy condition. No coordinates ship in the repository: machines configure =$WEATHER_LAT=/$WEATHER_LON= or =~/.config/weather/config.json= (with =--geo= for travellers). -=keymap= and =consolefont= are the live leftovers. Run =grep '^HOOKS=' /etc/mkinitcpio.conf= on this machine: the line carries =systemd= plus =keymap consolefont= and no =udev=, so archsetup's swap has already run here and both hooks are installed into the image and never executed. The systemd equivalent is the single =sd-vconsole= hook, which is what the distro's own systemd example on line 51 of =/etc/mkinitcpio.conf= uses. +**** 2026-07-19 Sun @ 05:21:15 -0500 Waybar forecast shipped +Dotfiles =f49764f= (pushed) extends the shared cached =weather= command with a normalized six-hour hourly outlook and five-day daily outlook. The Waybar chip now exposes the hourly forecast in its hover and toggles a Dupre GTK forecast panel on click. The full dotfiles unit suite passed. Live panel verification awaits a stowed desktop with a private weather location configured; the svg.el port remains later work. +**** 2026-07-19 Sun @ 05:26:36 -0500 Travel refresh linked to timezone update +Dotfiles =513c9d5= (pushed) makes the existing right-click =timezone-set= action reuse =whereami --json= — the same WiFi geolocation path and encrypted Google key that Emacs wttrin uses. A successful WiFi lookup atomically writes the machine-private weather config, clears the previous weather cache, and signals Waybar; the less-accurate IP timezone fallback intentionally does not change the weather location. Full dotfiles tests passed. +**** 2026-07-19 Sun @ 10:49:52 -0500 Waybar glyphs added +Dotfiles =50cd1d1= (pushed) adds compact Nerd Font weather-condition glyphs and either an eight-way wind-direction vane or gust glyph to the Waybar chip. The live module was signalled to redraw; full dotfiles tests passed. +**** 2026-07-19 Sun @ 10:52:17 -0500 Waybar glyph slots enlarged +Dotfiles =6f08634= (pushed) wraps the condition and wind glyph slots in Pango =x-large= spans while preserving compact numerical text. The active Waybar config was regenerated and the module signalled to redraw; full dotfiles tests passed. +**** 2026-07-19 Sun @ 10:58:50 -0500 Glyph treatment polished +Dotfiles =ad7cae7= (pushed) raises the chip's glyph and number baselines together, enlarges only the glyph slots to =xx-large=, and colors them with the reference renderer's bright gold. The live module was signalled to redraw; full dotfiles tests passed. +**** 2026-07-19 Sun @ 11:18:18 -0500 Condition glyph optically centered +Dotfiles =db77bc4= (pushed) corrects the Nerd Font condition glyph's internal-metrics offset independently from the already-centered vane and numeric readout. A live Waybar crop verified the glyph and temperature share a visual center; full dotfiles tests passed. +**** 2026-07-19 Sun @ 11:24:36 -0500 Weather chip grouped +Dotfiles =103cccb= (pushed) tightens the temperature/wind spacing into one reading and adds a subtle date-facing divider. A live crop verified the grouping; full dotfiles tests passed. Proposed but unapproved: retain gold condition/wind glyphs and use temperature text alone for a cool/neutral/hot color band. +**** 2026-07-19 Sun @ 11:27:19 -0500 Weather glyph bottoms aligned +Dotfiles =d0f48ec= (pushed) follows an identical-crop screenshot comparison to adjust the condition glyph's vertical offset. The live crop confirms it now shares the visual bottom edge with the vane, temperature, and neighbouring Waybar text; full dotfiles tests passed. +**** 2026-07-19 Sun @ 11:50:00 -0500 Weather numerals baseline-verified +Craig identified that the speed digit still sat lower than the date text. An exact live-crop measurement found the speed =8= bottom at y=45 and the =S= in =Sun= at y=41: a four-pixel mismatch. Dotfiles =f339ce9= (pushed) raises only the weather numeric Pango span by those four pixels. A fresh live capture measures both bottoms at y=35 (zero-pixel difference); focused and full dotfiles tests passed. +**** 2026-07-19 Sun @ 12:05:43 -0500 Comfort color and humidity signals added +Dotfiles =c3ef604= (pushed) colors the compact temperature by apparent temperature, preserving high contrast against the dark panel: cool blue below 55°F, near-white through 78°F, orange through 87°F, coral through 94°F, and vivid red at 95°F or hotter. The weather glyphs remain gold. Open-Meteo’s current relative humidity now appears in the hover and five-day panel without adding chip clutter. A live 92°F / feels-like 100°F refresh visibly rendered the red band; focused and full dotfiles tests passed. -Effect: the early-boot console keeps the default font and keymap until =systemd-vconsole-setup= runs in the real root. =add_nvme_early_module= sets =FONT=ter-132n= in =/etc/vconsole.conf= expecting it to apply at that stage, so the configured font is briefly not what archsetup asked for. +*** 2026-07-12 Sun @ 10:14:10 -0500 Wrote the component-generation spec (DRAFT) +[[file:docs/specs/2026-07-12-component-generation-spec.org][2026-07-12-component-generation-spec.org]] via spec-create: full spine (summary, problem, goals, two-altitude design, alternatives, 8 decisions with 1 open, 5 phases, acceptance criteria, readiness dimensions, risks). Gallery cited as the prototype evidence per the ui-prototyping rule (filed references + R01-R31 iteration history + final at 52a43ec). vNext items logged as the "Widget catalogue vNext" task. +*** 2026-07-12 Sun @ 20:24:37 -0500 Web library packaging approved — classic-script widgets.js + GW namespace +Craig approved with the componentization go-ahead (option 1): =widgets.js= as a classic script exposing one =GW= namespace, relative =