aboutsummaryrefslogtreecommitdiff
path: root/todo.org
diff options
context:
space:
mode:
Diffstat (limited to 'todo.org')
-rw-r--r--todo.org321
1 files changed, 240 insertions, 81 deletions
diff --git a/todo.org b/todo.org
index ed37e3c..f69127d 100644
--- a/todo.org
+++ b/todo.org
@@ -45,6 +45,45 @@ below):
input-side-spec.org (DRAFT, four decisions open).
* Archsetup Open Work
+** TODO [#B] gcalcli in the installer, token carried from the other daily driver :feature:velox:tooling:solo:
+:PROPERTIES:
+:CREATED: [2026-08-25 Tue]
+:LAST_REVIEWED: 2026-08-25
+:END:
+From home's 2026-08-25 handoff: velox's 08-13 reinstall left it without
+gcalcli, and on 08-21 both calendar write paths on velox were down at once
+(the google-calendar MCP with expired tokens, and no gcalcli), so a booked lab
+appointment sat uncalendared for three days. The daily-drivers one-time-setup
+drift, exactly.
+
+Part 1 is done (2026-08-25, this session): =pipx install gcalcli==4.5.1= on
+velox to match ratio, then ratio's =~/.local/share/gcalcli/{oauth,cache}=
+copied over tailscale (=oauth= is a 1 KB pickled google-auth credential,
+=chmod 600=). =gcalcli list= on velox returned all six calendars with no
+re-consent, so the token is portable between the daily drivers and the OAuth
+click-through is not needed when the other machine is reachable.
+
+Part 2, this task: make the installer do it.
+- =pip_install gcalcli= in the tool set beside =pip_install yt-dlp= (archsetup
+ ~line 3109; =pip_install= wraps =pipx install= as =$username=). Pin or not:
+ ratio and velox are both 4.5.1; unpinned matches how yt-dlp is installed.
+- The credential can't be installed: add a named post-install manual step
+ "copy =~/.local/share/gcalcli/oauth= from the other daily driver
+ (=scp <other>:.local/share/gcalcli/oauth ~/.local/share/gcalcli/=,
+ =chmod 600=), or run =gcalcli init= per
+ =assets/2026-02-01-gcalcli-setup.org= when neither machine has it."
+- A =post-rebuild-check= item: gcalcli on PATH and the oauth file present, so
+ the drift is caught by the checker rather than by a missed appointment.
+- Tests: an installer-steps pytest asserting the tool set carries
+ =pip_install gcalcli=; a post-rebuild-check test for the new item, both
+ states.
+- When it lands, confirm back to home (=inbox-send home=) so it can retire
+ its "gcalcli is not installed on velox" notes.
+
+Grading: feature, no hard date, real improvement to the install = [#B].
+:solo: — build path (installer + checker + tests) and verify path (pytest;
+the live proof already exists on velox) with no open decision.
+
** TODO [#A] Topgrade guarded-upgrade spec — decisions, review, decomposition :feature:maint:dotfiles:
SCHEDULED: <2026-08-25 Tue>
:PROPERTIES:
@@ -73,26 +112,6 @@ Then: flip the decisions DONE, run spec-review (DRAFT → READY), run
spec-response to decompose the four phases into build tasks here, file the
vNext =[#D]= kernel-reboot item, and commit the spec.
-** TODO [#A] Ratio: pull .emacs.d before upgrading Emacs to 31.1 :chore:ratio:emacs:
-SCHEDULED: <2026-08-25 Tue>
-:PROPERTIES:
-:CREATED: [2026-08-25 Tue]
-:LAST_REVIEWED: 2026-08-25
-:END:
-Emacs 31.1's warnings.el defers daemon-startup warnings into a closure holding
-the =*Warnings*= buffer; the config's dashboard-only sweep killed that buffer,
-so the first client frame of every fresh 31.1 daemon failed on Wayland and
-emacsclient silently fell back to =$DISPLAY= (XWayland, pgtk warning dialog).
-Fixed in =.emacs.d= commit =63831060= (2026-08-25, velox verified live:
-=GdkWaylandDisplay=). Ratio is still on 30.2, which lacks the deferring code,
-so it is fine until it upgrades — then it hits the same trap once per daemon
-start unless the fix is pulled first.
-
-Order on ratio: =git -C ~/.emacs.d pull= (the push from velox is the telega
-session's; confirm =63831060= is on origin first), then the =pacman -Syu= that
-brings =emacs-wayland 31.1=, then restart the daemon. Check afterwards:
-=emacsclient -e '(pgtk-backend-display-class)'= → =GdkWaylandDisplay=.
-
** TODO [#A] Lock-screen clock stale after a real sleep :bug:hyprland:dotfiles:velox:
SCHEDULED: <2026-08-25 Tue>
:PROPERTIES:
@@ -1645,10 +1664,10 @@ Restyle the audio panel's GTK CSS onto =tokens-waybar.css= + the banked composit
After ~5 hand ports, weigh widget-level codegen with evidence (mechanical duplication vs judgment per port). Recorded as a dated decision in the spec; go spawns its own spec.
*** TODO Flip the spec to IMPLEMENTED
When the phases above close: status heading keyword → =IMPLEMENTED=, dated history line with the reason, Metadata =Status= mirror. Three lines, one file.
-** TODO [#B] Net doctor expansion v1 — VM live verification :feature:dotfiles:network:
+** TODO [#B] Net doctor expansion v1 — VM live verification :feature:dotfiles:network:solo:
:PROPERTIES:
:SPEC_ID: ce29b103-ed9d-4f56-bf8c-9ed8fe680ff3
-:LAST_REVIEWED: 2026-07-13
+:LAST_REVIEWED: 2026-08-25
:END:
Build the [[file:docs/specs/2026-07-11-net-doctor-expansion-spec.org][net doctor expansion]] (IMPLEMENTED). Adds the control-plane cluster (rival-manager / nm-masked / keyfile-perms) and a sharper auth verdict to the shipped net doctor (=~/.dotfiles/net/=). Archsetup owns the dotfiles work end to end — edit, test, commit, and push in =~/.dotfiles=, then drop an inbox note. All build phases shipped and fake-verified; the one open piece is the VM live verification below.
*** 2026-07-11 Sat @ 02:47:47 -0500 Built the read-only control-plane probe
@@ -1670,13 +1689,13 @@ Spec status heading now IMPLEMENTED (dated history line + Status mirror); all fo
** DOING [#B] Run-time privilege model, standard across every panel doctor :feature:dotfiles:
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-13
+:LAST_REVIEWED: 2026-08-25
:END:
The audio input/output doctor is gaining a run-time privilege model (see [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]], decision "The doctor may use sudo, resolved by context at run time"). Craig's call, 2026-07-10: make it a standard, "revise the other panels to be consistent with these changes."
The model: a doctor resolves its privilege at run time from three signals — passwordless sudo available (=sudo -n true=, which never hangs), a tty to prompt at, and whether it is the GUI panel. Four remedy classes: Auto (user-scope, reversible), Privileged (needs sudo — runs where passwordless, prompts on a CLI tty, degrades to Guide in a GUI with neither), Reboot-tail (run the applicable part, then instruct the reboot), and Guide (physical/BIOS/wait-for-upstream, nothing to run). Safety floor: every Privileged and Reboot-tail remedy defaults to Confirm or Arm tier, never silent Auto, because passwordless sudo is not consequence-free.
-The shared helper is built (see the dated entry below). What remains is per-panel adoption: wire each doctor's remedies through =panelkit.privmodel.resolve()= and audit them against the Confirm/Arm floor, and reconcile maint's =priv.py= build/fire table with the model rather than leaving its implicit always-passwordless assumption. That wiring lives in the per-panel fix phases (net Phase 1, bt Phase 2, audio input/output), each needing a real privileged host to verify =--fix= end to end, so none is agent-solo.
+The shared helper is built (see the dated entries below), maint is reconciled onto it, and net is wired: =classify.py= carries the =remedy_class= on its privileged verdicts and =doctor.py= resolves each through =panelkit.privmodel.resolve()= (net Phase 1, shipped 07-11). Adoption is the gate only — every panel's repair actions already exist; what adoption changes is whether and how an existing privileged action is allowed to run (RUN where passwordless, PROMPT on a CLI tty, GUIDE in a GUI), under the Confirm/Arm floor. What remains, checked against the tree 2026-08-25: bluetooth is part-wired (=bt/doctor.py= makes one =resolve(PRIVILEGED, ...)= call, no per-remedy classes yet — audit its individual fixes against the floor), and audio has nothing on the doctor side (pending the input-side spec). Each needs a real privileged host to verify =--fix= end to end, so not agent-solo.
Craig's decision, 2026-07-12: maint's harmless-reclaim privileged remedies (the silent CLEAN UP set — paccache keep3, journal vacuum, coredump clean) STAY silent-auto. The reconciliation gives that class a sanctioned, documented exception to the confirm floor rather than forcing Confirm/Arm; the value of the floor holds for everything else. Where sudo is not passwordless, maint should degrade per the model (prompt on a tty, guide in a GUI) instead of hard-failing.
@@ -1701,7 +1720,7 @@ The support machinery was deliberately kept for this task: =layout-navigate= and
** TODO [#B] Audit dotfiles/common directory :chore:dotfiles:
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
+:LAST_REVIEWED: 2026-08-25
:END:
Refiled from the archsetup task audit (2026-06-28), landed via ~/.dotfiles/inbox; the dotfiles content split into its own repo 2026-06-16 but the task tracking stays here per Craig (2026-07-02). Three parts:
- Review all 50+ scripts in =~/.local/bin= and remove unused ones.
@@ -1780,7 +1799,7 @@ Acceptance: fresh VM install of the ratio profile reaches an endpoint on =:8081=
** TODO [#B] Test + CI infrastructure :test:
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-13
+:LAST_REVIEWED: 2026-08-25
:END:
Umbrella for the test-harness and CI-automation buildout. Consolidated from the 2026-06-28 task audit: these were scattered top-level tasks circling one effort, re-homed as children so the work reads as a unit. Each child ships independently and keeps the priority it carried before. No CI runner exists yet, so the CI/CD-pipeline child gates several of the others.
@@ -1852,7 +1871,7 @@ Keep test runs performant as installs and post-install tests grow (target < 2 ho
:LAST_REVIEWED: 2026-05-21
:END:
Proactive monitoring integrated with testing
-*** TODO [#B] Fix VM cloning machine-ID conflicts for parallel testing
+*** TODO [#C] Fix VM cloning machine-ID conflicts for parallel testing :no-sync:
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
@@ -1960,7 +1979,7 @@ machine state.
** TODO [#B] Security hardening + audit :security:
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
+:LAST_REVIEWED: 2026-08-25
:END:
Umbrella for the security-hardening and audit effort. Consolidated from the 2026-06-28 task audit, re-homing the scattered security tasks as children so the work reads as a unit. Each child ships independently and keeps its prior priority.
@@ -1976,12 +1995,12 @@ Umbrella for the security-hardening and audit effort. Consolidated from the 2026
**** TODO [#B] Implement port scanning check
**** TODO [#B] Create security posture verification script
**** TODO [#B] Set up intrusion detection monitoring
-*** TODO [#B] Document threat model and mitigations within 6 months
+*** TODO [#B] Document threat model and mitigations
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Identify attack vectors, what's mitigated, what remains
-*** TODO [#B] Complete security education within 3 months
+*** TODO [#B] Security education
:PROPERTIES:
:LAST_REVIEWED: 2026-06-24
:END:
@@ -2052,6 +2071,122 @@ filed here at Craig's direction so the sleep task carries it. Nothing
diagnosed yet — first step is =journalctl -b -1 -u systemd-suspend-then-hibernate=
around those timestamps to see which device reported busy.
+*** 2026-08-26 Wed @ 16:16:08 -0600 Diagnosed the hibernate battery drain: three separate faults, one task each
+Craig hibernated twice in ten days and found the battery dead both times. Read
+all 39 boots since the 08-13 reinstall, upower's charge history
+(=/var/lib/upower/history-charge-Framewo-55-03F5.dat=, root-only, starts
+08-19), sysfs, and the scripts inside =/efi/EFI/ZBM/zfsbootmenu.efi=.
+
+Hibernate is configured right and has worked: five hibernate+resume cycles
+since reinstall (08-13, 08-17 14:31, and three suspend-then-hibernate cycles on
+08-20/21). The two fatal events are the two overnight explicit
+=systemctl hibernate= runs, 08-17 22:20 and 08-21 19:58. Both journals end at
+"PM: hibernation: hibernation entry"; the next power-ons (08-18 10:13, 08-22
+17:22) were fresh boots whose resume hook found no image, no later swapon
+reported a leftover suspend signature, and on 08-22 the battery read 2% at
+power-on. The 08-23/24 night was on AC and not a battery death (three suspends
+failed to enter, machine awake all night at the charge limit; the 09:57 end was
+three power-key presses and a hard cut at 63%). The 08-19 death was the
+caffeine/hypridle one already diagnosed.
+
+Three faults, tracked as the children below:
+- Hibernate hard-freezes on entry (documented on Framework 13 AMD incl. Ryzen
+ AI 300: black screen, never powers off, intermittent, amdgpu-side). Fits
+ everything: the freeze precedes the swap signature, so the next boot is
+ fresh, and a frozen laptop at ~5 W empties 44.7 Wh in ~8 h. Unprovable from
+ logs by nature; the alternative (completed hibernate, unattended power-on to
+ the ZBM passphrase prompt) predicts a surviving image, which neither boot
+ had — see the VERIFY.
+- ZFS ARC starves the hibernate image: "Image allocation is 8118265 pages
+ short" today 14:09, "390678 pages short" 08-20 09:14. ARC 58 GB of 93,
+ =zfs_arc_max=0= so =c_max= = RAM − 1 GiB; the kernel must free RAM −
+ =image_size= (37.4 GB) ≈ 56 GB. systemd falls back to s2idle and retries
+ every 90 min, so suspend-then-hibernate never actually hibernates.
+- The SD card reader (090c:3350, =sda=, no media) can block suspend entirely:
+ "Freezing remaining freezable tasks failed after 20s (wq_busy=1)", pending
+ =disk_events_workfn= on =events_freezable_pwr_efficient=, three times on
+ 08-23/24. On battery that is a dead laptop by morning.
+
+Mistake worth remembering: =journalctl --since … -k= silently limits itself to
+the current boot (=-k= implies =-b=); cross-boot kernel facts need
+=_TRANSPORT=kernel= or an explicit =-b=.
+
+*** TODO Hibernate entry freeze — confirm under observation, then mitigate :bug:velox:hibernate:
+Interim rule until this closes: do not hibernate unattended on battery. Shut
+down, or suspend on AC.
+
+What is known: the two dead-battery hibernates match the Framework 13 AMD
+"hard freeze on hibernate entry" reports (community threads 69516 and 53860,
+Arch bbs 293242): screen black, power LED on, never powers off; intermittent
+(one report: every 6–7 cycles); TTM/amdgpu warnings; improved by newer
+=linux-firmware=; no confirmed fix. Board A9, BIOS 03.05, linux-lts 6.18.46,
+=amdgpu.dcdebugmask=0x610= already on the cmdline.
+
+Confirm first: the "Hibernate entry freeze: five observed cycles on AC" test
+under Manual testing and validation. A failed cycle there is the proof the
+journal cannot give.
+
+Mitigations to try in order once confirmed, one at a time, re-running the
+cycles after each: (1) =linux-firmware= at current, then =linux-firmware-git=
+if the freeze persists; (2) =/sys/power/disk= = =shutdown= instead of
+=platform= (a systemd =HibernateMode=shutdown= drop-in), which skips the ACPI
+S4 path some Framework users found hanging; (3) a newer kernel (=linux= vs
+=linux-lts=) for the amdgpu delta; (4) unload =mt7925e= in a pre-sleep hook
+if the freeze survives the first three. Not =:solo:=: each cycle needs a
+person watching the power LED.
+
+*** TODO ZFS ARC starves the hibernate image — cap it or shrink it pre-hibernate :bug:zfs:velox:solo:
+The arithmetic: the kernel preallocates RAM − =image_size= pages before
+snapshotting; with 93 GB RAM and the default =image_size= (2/5 of RAM,
+37.4 GB) that is ~56 GB, and only free memory plus what shrinkers give back
+counts. ARC was 58 GB today and the ZFS shrinker released little inside the
+preallocation window, so it came up 31 GiB short. Nothing in
+=/etc/modprobe.d/= sets =zfs_arc_max=.
+
+Two fixes, either or both:
+- Cap the ARC: =options zfs zfs_arc_max=<bytes>= in =/etc/modprobe.d/zfs.conf=
+ (16 GiB leaves ~70 GB reclaimable) plus =echo <bytes> >
+ /sys/module/zfs/parameters/zfs_arc_max= for the running system.
+- Or a =/usr/lib/systemd/system-sleep/= pre hook for the hibernate class that
+ lowers =zfs_arc_max=, waits for =size= in
+ =/proc/spl/kstat/zfs/arcstats= to fall, and restores it post-sleep. Keeps
+ the big ARC while awake.
+- Raising =image_size= toward the kernel's ceiling (about half of RAM) also
+ shrinks the demand; combine with the cap.
+Install it through archsetup so the next rebuild carries it (velox-only: ratio
+has no swap partition).
+
+Verify: after the change =arcstats size= drops below the cap within seconds;
+then one live suspend-then-hibernate cycle on AC with the delay temporarily
+short shows "hibernation exit" and no "Image allocation … short" line in the
+journal. That live cycle rides the entry-freeze test above; the ARC half is
+checkable without it.
+
+*** TODO SD card reader media polling can block suspend :bug:velox:solo:
+The reader (USB 090c:3350 Silicon Motion, =sda=, "Media removed, stopped
+polling" at boot yet =events_poll_msecs= = −1 → default 2000 ms) left a
+=disk_events_workfn= item pending on the freezable workqueue three times on
+08-23/24, and the freezer gives up after 20 s: "Failed to put system to
+sleep … Device or resource busy". Same symptom as the flaky expansion slot in
+the ribbon task; a stalled poll never completes.
+
+Fix: a udev rule for that vendor/product setting
+=ATTR{events_poll_msecs}="0"= (or =block.events_dfl_poll_msecs=0= on the
+cmdline if every removable disk should stop polling), shipped by archsetup.
+Verify with =rtcwake -m mem -s 20= on AC: journal shows "PM: suspend entry"
+and "PM: suspend exit" with no "Freezing remaining freezable tasks failed",
+and =/sys/block/sda/events_poll_msecs= reads 0 after a replug. Pulling the
+card before sleeping is the manual workaround meanwhile.
+
+*** VERIFY After the 08-17 and 08-21 dead batteries, did the first power-on hang, or boot straight to a fresh login?
+Decides between the two mechanisms. An entry freeze leaves no image, so the
+next power-on boots straight through. A completed hibernate followed by an
+unattended power-on (phantom power button, ZBM passphrase prompt until dead)
+leaves the image in place, so the next power-on would try to resume — and the
+only way that ends in the fresh boots the journal shows is a hung resume that
+got force-cut. If both power-ons went straight to a fresh login, the freeze
+is the answer.
+
** TODO [#A] Port Hyprland config to Lua before 0.57 drops .conf support :hyprland:dotfiles:
SCHEDULED: <2026-08-25 Tue>
:PROPERTIES:
@@ -2892,6 +3027,32 @@ NOTE (2026-07-04 audit): the "four-tab panel" framing predates the instrument-co
- Expected: ↓/↑ Mbps + ping + server shown inline.
- Byte-rate→Mbps unit: VERIFIED 2026-06-30 (velox). Raw =speedtest-go --json= dl_speed read ~3.66M, unambiguously bytes/s (29 down / 80 up Mbps); =net speedtest= reported 33.62 / 77.99 through the wired path. =BYTES_PER_SEC = True= + =* 8 / 1e6= are correct, no flip needed. Remaining here is only that the panel renders the inline result.
+*** Hibernate entry freeze: five observed hibernate cycles on AC
+What we're verifying: whether velox hard-freezes on hibernate entry (black
+screen, power LED on, never powers off), the documented Framework 13 AMD
+failure that fits both dead-battery events. The journal cannot show it; a
+person watching the LED can.
+- Plug in AC, lid open, nothing important unsaved.
+- Note the cycle number, then hibernate from a terminal:
+#+begin_src sh :results output
+date; systemctl hibernate
+#+end_src
+- Watch: the screen goes black; within about two minutes the power LED goes
+ off and the fans stop.
+- Press power, enter the ZBM passphrase, and confirm the same session comes
+ back (windows still open).
+- Check that the cycle was a real hibernate and not a fallback:
+#+begin_src sh :results output
+journalctl -b -o short-iso | grep -E "systemd-sleep|hibernation (entry|exit)|Image allocation|Failed to put" | tail -6
+#+end_src
+- Repeat until five cycles are logged.
+Expected: all five cycles power off within two minutes and resume into the
+same session, with "hibernation exit" and no "Image allocation … short" line.
+A cycle where the screen stays black with the power LED on for more than five
+minutes is the entry freeze: hold power for 10 s, and write down the cycle
+number and whether the keyboard backlight was lit. A cycle that instead comes
+straight back with "Cannot allocate memory" is the ARC task, not a freeze.
+
** DOING [#B] Prepare for GitHub open-source release
:PROPERTIES:
:LAST_REVIEWED: 2026-08-17
@@ -3022,9 +3183,9 @@ Rewrote the bare =if $var= boolean conditionals (=show_status_only=, =fresh_inst
*** 2026-05-26 Tue @ 15:27:09 -0500 eval task moot — the line-434 eval is gone, the survivor is deliberate
Verified: the only =eval= left in =archsetup= is line 578 in =retry_install=, and it's intentional and documented — it captures =$?= directly from =eval "$cmd"= to dodge the if-compound-swallows-exit-code trap. Replacing it with an array would reintroduce that bug. The line-434 eval this task pointed at no longer exists. Nothing to change.
-** TODO [#B] The audio doctor never checks the microphone :bug:audio:
+** TODO [#C] The audio doctor never checks the microphone :bug:audio:
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-13
+:LAST_REVIEWED: 2026-08-25
:END:
The classifier is output-only. =diag.probe_semantic= already collects =default_source= and =default_source_present=, and =classify.py= reads neither: the word "source" appears once in the whole module, in the graph row that counts them. So a muted mic, a default source naming an unplugged device, or a mic at zero volume all classify as =healthy=, and the verdict prints "the default output is present and audible" while the input side goes unexamined. Found 2026-07-10 while asking whether the doctor would have caught Chrome losing the mic. It would not have.
@@ -3034,10 +3195,12 @@ Work: mirror the sink rules onto the source. =probe_semantic= gains =default_sou
Two things not to get wrong. An absent microphone is legitimate on a desktop, so "no input devices" must never be a fault the way =no-output-devices= is. And a monitor source is a legitimate default source (recording desktop audio), which is why =probe_semantic= passes =include_monitors=True= — inheriting the panel's display filter here would call a working setup broken.
-Specced 2026-07-10 after discussion with Craig, and the design grew past the original gap: [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]] (DRAFT, four decisions open). A doctor key per direction, a kernel-level capture probe below PipeWire, PTT-aware muting, and a direction-aware guard. The precedence question the build would have faced is gone: a doctor per direction means the user's press says which side they came to fix.
+Specced 2026-07-10 after discussion with Craig, and the design grew past the original gap: [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]] (DRAFT, three decisions open as of 2026-08-25). A doctor key per direction, a kernel-level capture probe below PipeWire, PTT-aware muting, and a direction-aware guard. The precedence question the build would have faced is gone: a doctor per direction means the user's press says which side they came to fix.
Parent spec: [[file:docs/specs/2026-07-09-audio-doctor-spec.org][docs/specs/2026-07-09-audio-doctor-spec.org]] (IMPLEMENTED). This is a v1 gap found after the fact, not a phase of it.
+Grading (2026-08-25 review): Major severity — the doctor's verdict is silently wrong for a whole direction, workaround is checking the mic by hand — × "some users, sometimes" (mic faults are occasional) = P3 = [#C]. Was held at [#B] ungraded; regraded by the matrix.
+
** TODO [#C] Weather chip color signals unclear + unenforced :bug:dotfiles:waybar:weather:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-21
@@ -3089,7 +3252,7 @@ manual-testing entry; everything else verifies with fakes.
** TODO [#C] Net panel speedtest history :feature:dotfiles:network:
:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
+:LAST_REVIEWED: 2026-08-25
:END:
From the roam inbox (routed 2026-07-13): the networking panel should track speedtests over time with appropriate info. Shape: persist each SPEED TEST result (timestamp, down/up, latency, server) to a small local store and surface history in the net panel. Design questions for work time: retention window, which fields matter, and presentation within the panel's ~400px width (recent-results list vs trend readout). Point-in-time results exist today; the gap is comparison across days and venues.
@@ -3360,53 +3523,6 @@ carry a date and Craig's return date isn't known yet — date it and raise it to
Workaround in the meantime: Bluetooth mouse, already in use.
-** DONE [#B] Weather tooltip caching :feature:waybar:weather:solo:
-CLOSED: [2026-07-25 Sat 10:53]
-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.
-Verified complete in the 2026-07-25 batch: the weather CLI already had the hourly default TTL, fresh-cache no-fetch path, stale fallback, and explicit offline footer. Its 33-test suite and the full dotfiles suite pass.
-** DONE [#B] Settings gear becomes four device toggles :feature:waybar:dotfiles:solo:
-CLOSED: [2026-07-25 Sat 10:53]
-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.
-
-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.
-
-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.
-
-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.
-Shipped in the 2026-07-25 batch as four independent JSON modules over the existing verified settings backends. Touchpad, mouse, and webcam turn terracotta when disabled; DND uses the deliberate inverse polarity; unavailable hardware dims. The gear remains the panel launcher. The live and Dupre theme CSS copies stay byte-identical.
-** DONE [#C] Wallpaper panel selection and scroll state :feature:dotfiles:solo:
-CLOSED: [2026-07-25 Sat 10:53]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-25
-:END:
-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.
-Shipped in the 2026-07-25 batch. The panel queries =awww query= off the UI thread, prefers the actually displayed image over stale stored state, highlights it, scrolls it into view on first open, and remembers the horizontal adjustment across selection-triggered rebuilds.
-** DONE [#C] Net tooltip IPs and line order :feature:waybar:network:solo:
-CLOSED: [2026-07-25 Sat 10:53]
-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.
-Shipped in the 2026-07-25 batch. The slow connectivity probe caches local addressing and a validated external IP with the network identity; the Waybar hot path only reads that valid cache. Tooltip order is Interface, internal/external/gateway IPs, connectivity detail, throughput, Signal, shortcut.
-** DONE [#B] Dupre Kit merge — casting additions :feature:tooling:solo:
-CLOSED: [2026-07-25 Sat 10:53]
-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.
-Shipped in the 2026-07-25 batch. =widgets.js= now owns all three builders, shared gradients/CSS, contracts, and policy records; additions no longer redefines them when older casting pages load it. The gallery has a three-detent fader card and a three-channel 0–12 drum demonstration (112 cards total). Static ownership tests, JS syntax checks, and the complete headless interaction probe pass.
-** DONE [#C] Maint live-refresh hairline replacement :feature:maint:solo:
-CLOSED: [2026-07-25 Sat 10:53]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-07-14
-:END:
-From the roam inbox (routed 2026-07-13): the memory-killer section seemed to update too often, and "it's a bit unclear what the line is doing; consider something else." Diagnosis (2026-07-14): the data cadence is already the requested 3s (gui live tier, _LIVE_SECONDS); the perceived churn is the live-refresh hairline — the 2px bar under the live sections that drains full-to-empty over each 3s window, redrawn at 150ms (gui._hair_tick, viewmodel.refresh_fraction). It exists to tell a stale board from a frozen one (2026-07-09), but it reads as constant unexplained motion. Design call for Craig: replace the draining line with something whose meaning is legible — candidates: a dot that blinks once per refresh, a "3s" age caption that only appears when refresh is overdue, slowing the drain redraw, or dropping the indicator on live tiers and keeping it only when data goes stale. Keep the stale-vs-frozen distinguishability that motivated the hairline.
-*** 2026-07-21 Tue @ 08:35:00 -0500 Decided (Craig): silent-until-stale age caption
-Replace the draining 2px hairline with an age caption that shows ONLY when refresh is overdue (e.g. "3s", "8s" once past the expected window) and shows nothing while the board is healthy. This keeps the stale-vs-frozen signal — a frozen board surfaces a growing age number, a live one stays clean — while removing the constant motion the hairline created. Implementation (dotfiles, archsetup-owned): drop =gui._hair_tick= / the hairline draw, add an overdue-age caption driven off =viewmodel.refresh_fraction= (or the last-refresh timestamp) rendered only past the live window. Now unblocked; needs a live visual check on the panel after.
-Shipped in the 2026-07-25 batch. The animated draw area and 150ms timer are gone; the memory section header stays silent through the healthy three-second window, then shows a once-per-second growing age caption. Pure boundary tests and the full maintenance suite pass.
-** DONE [#D] Test-framework + prototype refactor cluster :refactor:solo:
-CLOSED: [2026-07-25 Sat 10:53]
-Grading: no behavior change; parking lot. Refactors from the S5-S7 audit, distinct from the installer refactor rollup above.
-scripts/testing/run-test.sh + run-test-baremetal.sh duplicate the run/poll/report skeleton and have drifted (VM uses setsid + copy helpers, baremetal uses nohup + hand-rolled sshpass scp) — extract the shared core so baremetal inherits the sturdier paths; run-maint-nspawn.sh:66 + run-maint-scenarios.sh:78 duplicate the transport-independent _scenario_var/_validate_scenario/run_scenario (a sourced lib/maint-scenario.sh); run-test.sh:251,265 uses two different mechanisms (pgrep vs ps|grep) for the same liveness check; docs/prototypes/gen_tokens.py:78 repeats the section-iteration skeleton across four emitters; gallery-widget.el:95,136 hardcodes SVG arc/hub path strings that duplicate the cx/cy/radius geometry (dial desyncs silently on a constant change); gallery-widget.el:72,84 leans on the private svg--append. See findings doc (S5, S6, S7).
-Completed test-first in the 2026-07-25 batch. QEMU and bare-metal runners share liveness/report helpers; maintenance transports share scenario validation/execution; token emitters share ordered section traversal; and the Emacs SVG gauge shares semicircle geometry and uses the public DOM append API. Every fast Python/ERT suite passes.
** DONE [#B] Two agent sessions sharing one git repo :chore:tooling:
CLOSED: [2026-07-26 Sun]
Craig approved the shared-rules-layer solution on 2026-07-26.
@@ -4314,3 +4430,46 @@ installed.
*** 2026-08-21 Fri @ 14:18:03 -0700 Promoted the Phase 5 residual out to its own task
Rescoped 2026-07-04 (audit): the tunnels track already shipped most of the original Phase 5. Panel tunnel bring-up/down and detection landed (dotfiles 2d9d060 probes tailscale/NM-wireguard/Proton; 21db05a brings overlays up/down from the panel's Tunnels sub-view; 31ba056 diagnose/doctor understand tunnel routes; archsetup 2e40781 wireguard config import; the net-panel-other-interfaces spec is IMPLEMENTED). What remains for Phase 5 is only the =net vpn ...= CLI subcommand — cli.py still has no vpn/tunnel parser. Fold the panel's existing tunnel operations into a CLI surface; spec separately when picked up.
+** DONE [#A] Ratio: pull .emacs.d before upgrading Emacs to 31.1 :chore:ratio:emacs:
+CLOSED: [2026-08-25 Tue]
+:PROPERTIES:
+:CREATED: [2026-08-25 Tue]
+:LAST_REVIEWED: 2026-08-25
+:END:
+Emacs 31.1's warnings.el defers daemon-startup warnings into a closure holding
+the =*Warnings*= buffer; the config's dashboard-only sweep killed that buffer,
+so the first client frame of every fresh 31.1 daemon failed on Wayland and
+emacsclient silently fell back to =$DISPLAY= (XWayland, pgtk warning dialog).
+Fixed in =.emacs.d= commit =63831060= (2026-08-25, velox verified live:
+=GdkWaylandDisplay=). Ratio is still on 30.2, which lacks the deferring code,
+so it is fine until it upgrades — then it hits the same trap once per daemon
+start unless the fix is pulled first.
+
+Order on ratio: =git -C ~/.emacs.d pull= (the push from velox is the telega
+session's; confirm =63831060= is on origin first), then the =pacman -Syu= that
+brings =emacs-wayland 31.1=, then restart the daemon. Check afterwards:
+=emacsclient -e '(pgtk-backend-display-class)'= → =GdkWaylandDisplay=.
+
+*** 2026-08-25 18:10 — pull already landed; the upgrade half remains
+Checked ratio over tailscale: =~/.emacs.d= is clean at =91fbac72= (= =origin/main=),
+and =63831060= is an ancestor of HEAD — =modules/undead-buffers.el= carries the
+=*Warnings*= entry. Ratio is on =emacs-wayland 30.2-3= with =31.1-1= pending among
+720 updates (last full upgrade 2026-08-01; kernel 7.1.5 → 7.1.9 also pending,
+btrfs root, uptime 3.5 weeks). The daemon is a plain =emacs --daemon= (not a
+user unit) holding 2 live frames, so the restart step will drop those frames.
+What remains: the =pacman -Syu= on ratio, the daemon restart, and the
+=(pgtk-backend-display-class)= check.
+
+*** 2026-08-25 Tue @ 18:35:00 -0600 Upgraded ratio to Emacs 31.1 and verified the Wayland backend
+Ran the upgrade over tailscale as a transient unit (=ratio-upgrade.service=,
+log at =/var/log/ratio-upgrade.log=): 714 packages, =--ignore= on the six
+packages the live-update guard would have blocked (aquamarine, hyprland,
+hyprutils, mesa, vulkan-radeon, wayland — still pending, apply from a TTY
+before the reboot). One orphan cleared first: =qemu-block-gluster= had been
+dropped from the repo and pinned =qemu-common=; the new =qemu-full= no
+longer needs it. Killed the plain =emacs --daemon= (no modified buffers, no
+graphical frames), started =emacs.service= instead so the daemon carries the
+systemd user environment, and probed from a throwaway frame:
+=(pgtk-backend-display-class)= → =GdkWaylandDisplay=, =*Warnings*= alive.
+Ratio still wants a reboot for =linux 7.1.9=. Pacnews to review there:
+=/etc/ssh/sshd_config.pacnew= and two =/etc/tpm2-tss/fapi-profiles/*.json=.