#+TITLE: ArchSetup Tasks #+AUTHOR: Craig Jennings #+DATE: 2026-02-14 * Archsetup Priority Scheme Four levels, matching the Emacs config (=org-highest-priority ?A=, =org-lowest-priority ?D=, =org-default-priority ?D= in =modules/org-config.el=). Priority answers "how much does this matter"; a date answers "when". They are independent — assign both deliberately. Org priority alone never schedules anything, which is why undated [#A]/[#B] tasks feel ungrounded. - [#A] Must happen. Broken install, data loss, security, or a blocker for other work. An [#A] REQUIRES a SCHEDULED or DEADLINE date — if it can't be dated, it isn't really an A; drop it to B. (The main agenda always shows open A's.) - [#B] Should happen, this cycle. Real improvement or fix with no hard date. Surfaces in the agenda's priority-B block only while undated; add a SCHEDULED date when you commit to a week and it moves into the schedule. - [#C] Nice to have / someday. Kept for the record, low urgency. Date it only when it graduates to B. - [#D] Default / unsorted. A bare TODO with no cookie is D. Stays out of the agenda — the inbox of priorities. Triage D's up to A/B/C or let them sit. Rule of thumb: A = dated-and-must; B = the active backlog; C = parking lot; D = untriaged. Fixing the undated A/B tasks means either dating them or demoting to C. ** Tags The vocabulary is open — topic tags are coined as needed — so these are conventions, not a closed set. A task carries at most one type tag, optionally the effort/autonomy tags, and any number of topic tags. Because the set is open, the task audit leaves topic tags alone (it doesn't strip "unknown" tags). - *Type* (one per task where the kind is clear): =:feature:= new capability, =:bug:= fix for broken behavior, =:test:= test coverage or test infra, =:refactor:= restructure with no behavior change, =:chore:= tooling / meta / housekeeping. - *Effort / autonomy*: =:quick:= a spare-moment fix (minutes, not a sitting); =:solo:= Claude can carry it end to end — there's a build path, a test path, and no upfront decision needed (a leftover manual spot-check doesn't disqualify it). - *Topic / area* (open): the subsystem a task touches — e.g. =:hyprland:= =:waybar:= =:mpd:= =:music:= =:network:= =:tooling:= =:llm:= =:eask:= =:pocketbook:= =:cmail:=. Coin a new one when it aids filtering. * Next Session Focus On 2026-07-19, Craig selected the following autonomous work. Status after the 2026-07-19 evening session: 5 shipped (dotfiles, pushed), 3 held for a design decision. Each shipped item has its own DONE task below. Shipped: notification loudness -40% (808ca23); clock panel right-click dismiss (fc9a2b7, live-verified); date-format scrolling as a date-only ring (9dfe082); show the active wired interface (22867f9); connect the best saved WiFi profile on enable (9105361). Held for a design decision (not solo — each needs Craig's call, kept as TODO below): - Order network connections by availability — the task wants one tiered list (available saved -> available unsaved -> unavailable saved), but the panel spec says "three labelled groups, never one merged list" with Saved MRU-first. Reorder within Saved only, or merge into one list (overriding the spec)? - Indicate hotspot/metered WiFi in amber — "hotspot" is ambiguous (connected-to a phone hotspot vs the machine running an AP), and metered detection needs new nmcli reads on the status fast path (contract is "one nmcli call"). - Audio doctor mic/input health — points at docs/specs/2026-07-10-audio-doctor- input-side-spec.org (DRAFT, four decisions open). * Archsetup Open Work ** TODO [#B] Timeline spine test picks the wrong "next" event off Denver :bug:dotfiles:test: :PROPERTIES: :CREATED: [2026-08-24 Mon] :LAST_REVIEWED: 2026-08-24 :END: =make test= in dotfiles is red before any of this session's work. Two failures, both in =settings/faces/timeline-face-spine.test.mjs=: "event bars never leave the plot" and "exactly one event is marked as next, and it is the soonest ahead". NOT the bug =c96a216= fixed. Every =spineRows= call in that file is pinned to =JUL=, and =scene()= and =EVENTS()= both default to it, so the fixture side is already clean and the file's own guard test passes. TWO THINGS TO SETTLE, and they may be one bug or two: 1. =timeline-face-spine.js:466= — =const next = timedOnly(events).find((e) => e.s >= refMs)= takes the first array element starting at or after now, which is the *soonest* only if =events= is sorted by start time. The test's failure message is exactly that it is not: a bar ahead of the spine starts at x=1651.2 while the one marked =event-next= sits at x=2132.8. Either sort before the find, or use a min-by rather than a find. 2. Why it is red *here* and presumably green on ratio. The most recent commit to =timeline-face-spine.js= is =ffe43ab feat(settings): draw home where the machine is, not where its zone is=. This machine is =America/Denver= (Craig travelling); the tests pass =home("New Orleans")= explicitly. If a machine-resolved home overrides the explicit argument, the geometry drifts and the test is machine-dependent — which makes it useless as a gate, since it would only ever fail on the machine nobody runs it on. Confirm by running the faces suite with =TZ=America/Chicago= and again with =TZ=America/Denver=. If item 2 confirms, the design question is whether machine-resolved home belongs in the pure geometry layer at all, or whether the host should resolve it and pass it in — which is what the test already assumes. Not blocking the Lua port: =make test-faces= is disjoint from the hypr config and the three suites that work touches. ** TODO [#B] Qt apps render oversized on velox :bug:velox:solo: :PROPERTIES: :CREATED: [2026-08-19 Wed] :LAST_REVIEWED: 2026-08-19 :END: From the roam inbox, Craig's words: "qt apps look huge on velox. how do we make it look better on this particular machine, and not change ratio. it seems they should have different QT configs." The shape is per-machine Qt scaling. velox is a high-DPI Framework panel and ratio drives ordinary-DPI monitors, so one global Qt scale factor cannot suit both. The fix has to be host-scoped rather than a value written into the shared config, which is the same tier split the dotfiles already use. Grading: Minor severity (apps work, they are just the wrong size) x every user every time (every Qt app launch on velox) = P2 = [#B]. *** 2026-08-19 Wed @ 15:05:00 -0700 Root cause found and fixed; needs a logout to take effect velox's =conf.d/local.conf= scaled the panel twice. The monitor line sets =1.566667= and the same file exported =QT_SCALE_FACTOR,1.5= and =GDK_SCALE,1.5=, and Qt 6 on Wayland already takes its scale from the compositor, so the two multiplied. Measured rather than reasoned: with the override Qt reports a 960x640 logical screen, without it 1440x960, and 2256/1.566667 is exactly 1440. That is 1.5x too large, which matches "huge" precisely. Those env lines were not careless. The comment above them explains they existed to compensate for =xwayland:force_zero_scaling = true= in the shared hyprland.conf, which makes XWayland clients render unscaled and tiny. The approach was what failed: an env var reaches every app, so fixing XWayland broke every native Wayland client. Removing the vars alone would have traded "Qt huge" for "Zoom tiny", so velox now turns =force_zero_scaling= off for itself instead. XWayland scales through the compositor there, coming out correctly sized and slightly soft. ratio is untouched and needs nothing, its monitor being scale 1. =force_zero_scaling= took effect on =hyprctl reload=. The env removal will not: Hyprland applies =env== lines with setenv at parse time and never unsets them, so the running compositor still hands 1.5 to everything it spawns. Craig has to log out and back in. *** VERIFY Is CALIBRE_OVERRIDE_DPI still needed after the scaling fix? The same file pins =CALIBRE_OVERRIDE_DPI,96= with the comment "calibre renders oversized at the 1.57 compositor scale". Calibre is a Qt app, so that was almost certainly this same double-scaling seen through one application and worked around per-app rather than at the root. With the multiplier gone, the pin is probably redundant and may now render calibre too small. Left in place rather than removed on a guess, since it was validated at 96 on 2026-06-27 and calibre has its own DPI handling. Worth opening calibre after the next login and deciding by eye. The cursor entry in the same file records this identical failure a third time: "Pre-scaling it (the old 36 = 24 x 1.5) double-applied on top of the compositor's scale." Three instances of one mistake in one file, two previously fixed in isolation without anyone naming the pattern. ** TODO [#B] Function keys issue media actions instead of F-keys :bug:velox: :PROPERTIES: :CREATED: [2026-08-19 Wed] :LAST_REVIEWED: 2026-08-19 :END: From the roam inbox, Craig's words: "function keys should issue F+number functionality rather than their media functionality when the button is hit. currently it's reversed and I have to hit function and the f button for F+number functionality." Check first whether this belongs to archsetup at all. On a Framework the Fn-lock is a firmware-level toggle held in the keyboard itself (Fn+Esc on most revisions), not something the OS sets, in which case this is one keystroke rather than a change here. If it is instead a hid/keyboard-module quirk, it is ours. Grading: Minor severity (the keys work, they are on the wrong layer, and there is a workaround) x every user every time (every F-key press) = P2 = [#B]. ** TODO [#C] Waybar panels launch expanded instead of collapsed :bug:dotfiles:waybar: :PROPERTIES: :CREATED: [2026-08-19 Wed] :LAST_REVIEWED: 2026-08-19 :END: From the roam inbox, Craig's words: "waybar panels should start up collapsed. currently both the left and the right waybar panels launch expanded." Panel source is =~/.dotfiles=. Its heading in the roam inbox read "archsetup." with a period rather than a colon, so the routing prefix did not match cleanly; claimed on the plain reading of the text. Grading: Cosmetic severity (presentation only, nothing is lost) x every user every time (every session start) = P3 = [#C]. ** VERIFY [#C] The visible analog clock avoids being dragged :velox: :PROPERTIES: :CREATED: [2026-08-19 Wed] :LAST_REVIEWED: 2026-08-19 :END: From the roam inbox, captured verbatim: "the visible analog clock avoids being dragged. ask me about this." Filed as a VERIFY because the capture asks for a conversation rather than describing a defect. What is the clock avoiding being dragged by, and is the avoidance the bug or the intended behaviour? ** TODO [#C] A failed hostname lookup takes seven seconds :bug: :PROPERTIES: :CREATED: [2026-08-19 Wed] :LAST_REVIEWED: 2026-08-19 :END: =getent hosts fake-vm= takes about 7.2 seconds to return not-found on velox. Measured repeatedly with the cache flushed between runs. Anything that looks up a name that does not exist pays it: an ssh typo, shell completion, a script probing for a host. Not caused by the DNSSEC change. A/B measured today, cache flushed each time: 7691ms and 7232ms on =allow-downgrade= against 6804ms and 7482ms on =yes=, so the setting makes no difference and this predates it. The likely shape is the tailnet search domain (=search tailf3bb8c.ts.net=) being tried first, then the two DoT upstreams, each with its own timeout, before NXDOMAIN comes back. Found because it blew a 20-second timeout in =tests.net-scenarios.test_run_net_scenarios=, which shells out to ssh a deliberately-bogus =root@fake-vm=. That suite passes on its own and the failure did not recur, so the timeout needed this latency plus the DNS disruption from the clock testing running alongside it. Worth knowing that the suite sits close enough to the edge for a slow resolver to tip it. Grading: Minor severity (nothing behaves wrong, it just waits) x some users sometimes (every failed lookup, which is occasional rather than constant) = P3 = [#C]. ** TODO [#A] Reseat velox input-cover ribbon — phantom power button :bug:velox:hardware: DEADLINE: <2026-08-26 Wed> :PROPERTIES: :CREATED: [2026-08-13 Thu] :LAST_REVIEWED: 2026-08-13 :END: Machine off, lift the input cover (Framework QR-guided procedure, 5 fasteners), reseat its ribbon connector to the mainboard — disturbed in the 2026-08-13 board swap. Root cause of every "mystery reboot" that day: chassis flex (flash-drive touch, ethernet bump, lid partially lowered) fired phantom power-button presses — journalctl -b -1 showed "Power key pressed short." → orderly logind poweroff, then the glitching button powered it back on. While in there, reseat the USB expansion cards too — the flaky slot (two hard resets, one no-enumeration) is likely the same flex problem. THIRD SYMPTOM (2026-08-13 evening): touchpad delivers ZERO input events — 15s synchronized libinput debug-events capture while swiping caught nothing, though i2c enumeration and a driver rebind handshake are clean. Signature of a dead interrupt line on the same ribbon. Keyboard + power LED lines work; BT mouse is the interim pointer. ESCALATED 2026-08-13 21:00: a fourth event killed the machine THROUGH the shield. Previous boot's journal ends mid-line (tailscaled chatter) with no shutdown sequence at all — a hard power cut, not logind acting. So the glitch now reaches the EC/hardware power path, which no software setting can intercept. The reseat is the only fix, and this is a lose-work-without-warning failure mode, not an inconvenience. Interim shield (already live): /etc/systemd/logind.conf.d/powerkey.conf sets HandlePowerKey=ignore — phantom presses log but do nothing; EC-level 10s hold still force-cuts. Consider keeping it even after the repair. Verify after reseat: flex the chassis edges + partially lower the lid, then grep the journal for new "Power key pressed" lines — zero means fixed. Must be done before the Sunday flight — a phantom press mid-travel with the shield on is survivable, but the connector should not be trusted at 30,000 feet on the loose setting. *** 2026-08-19 Wed @ 14:40:00 -0700 Retracted: the RTC reset is not this task's, and I should not have filed it here I attributed the 2026-08-19 network outage to this ribbon earlier today. Craig pushed back — he reseated it before the trip to get the touchpad working — and he is right. The evidence does not support the attribution and some of it points the other way. What actually holds. Boot -3 ended at 01:33:18 with no shutdown sequence: no power-off target, no unmounting. The next boot's kernel line reads =rtc_cmos 00:01: setting system clock to 2025-01-01T00:00:16 UTC=, a firmware default, so the RTC was reset rather than drifted. No firmware update was applied (=fwupdmgr get-history= is empty) and the battery is fine. What refutes the ribbon. This boot logged *zero* =Power key pressed= events, and so did the four boots before it. The phantom-press symptom had genuinely stopped after 08-15, exactly as the 08-16 session recorded. The earlier events logged a power-key press and an orderly poweroff; this logged neither, which makes it a different signature, not a worse version of the same one. What I got wrong methodologically: I anchored on the most salient open hardware task and read association as evidence. I even wrote "I can't prove it is the same connector" and then filed it here anyway, which is the tell. Two things I checked and can rule out. There were no OOM kills — the 3,433 matching lines are a systemd unit named "Periodically re-score Claude Code processes for the OOM-killer" firing on a timer, not memory pressure, and there is not a single "Killed process" line. Thermal is clean; the only mentions are boot-time zone registration at 34C and 45C. One real thing the same window did surface, tracked separately: a python3 crash loop, 251 core dumps in the final ten minutes, SIGABRT with =XFreeThreads= and =PyEval_RestoreThread= in the trace. It does not explain the RTC, because software cannot clear it, but it is its own problem. The open question that would settle the RTC is for Craig, not the journal: a long power-button hold on a Framework triggers an EC-level reset that clears the RTC, which fits a wedged machine being forced off. A 4-second hold would not. *** 2026-08-17 Mon @ 19:57:42 -0700 Not done, and the two symptoms now disagree The reseat did not happen before the flight, and velox is travelling. The deadline blew past on 08-14. The two symptoms have separated, which is worth recording because it changes what the evidence proves. The phantom presses have stopped: fifteen "Power key pressed" entries between 08-14 04:29 and 08-15 20:04, then nothing at all across five boots including today's. The touchpad has not — there is still no touchpad node under =/dev/input/by-path/=, which is the same dead interrupt line the body describes. So the quiet power button is not evidence the connector reseated itself. The interrupt line is the symptom that cannot be masked in software, and it is still dead, so the ribbon is still unseated. The most likely reason the presses stopped is that the machine has been sitting on hotel surfaces instead of being carried and flexed. The interim shield is still live (=HandlePowerKey=ignore=), and the escalation note stands: an EC-level glitch cuts power below systemd regardless of it. *** 2026-08-15 Sat @ 23:05:00 -0500 The reseat did happen, and the touchpad came back — this contradicts the 08-17 read Recording this because a parallel session concluded on 08-17 that the reseat had not happened and the touchpad was still dead. Both halves were done and verified that night, so the two accounts disagree and the disagreement should be visible rather than silently resolved by whichever session committed last. What was done: the input-cover ribbon was reseated first, which fixed the phantom power button — the 22:09 boot logged zero =Power key pressed= lines after Craig flexed the chassis, against nine on the boot before. The touchpad did not change, because the input-cover ribbon is not its connector. The 4-pin connector beside the printed =TOUCHPAD= label is silkscreened =PIN 1-2 GND / PIN 3-4 VCC= — pure power, so it cannot carry i2c or an interrupt. Reseating the ribbon that actually crosses to the mainboard fixed it. Measured, not assumed: the touchpad interrupt (=amd_gpio= pin 8) went from 0 counts across all 24 CPUs to 1795, and =i2c_hid_acpi ... did not ack reset within 1000 ms= disappeared from the boot log. Craig confirmed the pointer moved. *Why the 08-17 probe likely misread it:* it checked for a node under =/dev/input/by-path/=. i2c-HID touchpads frequently get no =by-path= symlink even when fully working, so its absence is not evidence of a dead interrupt line. The falsifiable check is the interrupt count in =/proc/interrupts= while the pad is being touched, or the reset message in =dmesg=. *Left open rather than closed* — velox was refusing ssh at merge time on 08-20, so the current state could not be re-verified, and a later regression cannot be ruled out. One second of Craig's time settles it: move the pointer. If it works, close this; if it does not, the interrupt line went back down and that is new information. ** DOING [#A] Velox reinstall — DR test of archangel + archsetup :velox:chore: DEADLINE: <2026-08-15 Sat> :PROPERTIES: :CREATED: [2026-08-13 Thu] :LAST_REVIEWED: 2026-08-13 :END: Mainboard swapped Intel→AMD (Ryzen AI 9 HX 370); new NVRAM has no boot entry. Decision: full reinstall via archangel+archsetup, run deliberately as a disaster-recovery drill before the Sunday flight. Runbook (live checklist): [[file:working/velox-reinstall/velox-reinstall-runbook.org][working/velox-reinstall/velox-reinstall-runbook.org]] Done 2026-08-13: ISO rebuilt (archangel-2026-08-13, archsetup baked with AMD microcode detection, velox profiles at /root/, .ai/inbox excluded — build.sh edits pending commit in archangel), contents verified, dotfiles swept clean of Intel assumptions. Finding folded in: velox's truenas backups silently stopped ~Jul 6 (newest is DAILY.0 Jul 6; wolf.conf.gpg from Jul 29 is in NO backup). Salvage pass in the runbook is therefore REQUIRED before partitioning, and the fresh install must fix + verify the backup timer (runbook Phase 5). ** TODO [#B] Truenas session-host VM for long-running agent sessions :feature:tooling: :PROPERTIES: :CREATED: [2026-08-13 Thu] :LAST_REVIEWED: 2026-08-13 :END: A small VM on truenas (TrueNAS SCALE KVM) as the home for long-running / away-mode agent sessions. The case, per Craig 2026-08-13: truenas is the only machine on ethernet, so network recovery after an outage is automatic (wifi hosts may never reassociate unattended); it's UPS-backed through blip-to-hours outages; it has the Comet KVM for out-of-band recovery; and appliance uptime discipline means it doesn't reboot for workstation reasons. Tonight's live demonstration of anchor-staleness risk (39 min unlogged during a bare-metal recovery) is the motivating incident — the session's durability equals the anchor's lag at interruption, so a host that doesn't get interrupted is worth real money. Costs to engineer around: a third environment to keep synced (repos, rulesets, tailnet identity); credential provisioning — GATED on the secrets-repo work (the [#A] secrets task above): the VM should be the secrets bundle's second consumer after the personal ISO, not another hand-copied key sprawl; a firm RAM carve-out so builds don't fight the ZFS ARC; headless only — desktop-coupled sessions stay on ratio/velox. Build deliberately AFTER the vacation, not before Sunday. Companion idea (cheaper, complementary): put ratio on the UPS. ** TODO [#B] post-rebuild-check: route every probe through one guarded helper :refactor:solo: :PROPERTIES: :CREATED: [2026-08-17 Mon] :LAST_REVIEWED: 2026-08-17 :END: The script works and is well tested, but its shape keeps producing the same bug. Across three review rounds the reviewer found FOUR separate instances of "the probe failed and the check reported ok", each in a different place: =systemctl= in check 1, the enablement read in check 2, =find= in check 3, and =grep= in check 4. A fifth was latent in an unguarded staged write. Every one was individually fixed, and I only stopped finding more because someone kept looking. That is a design problem rather than four bugs. The script has five hand-written probes, and each one has to remember to branch on its own exit status. Nothing enforces it, nothing fails a review that forgets it, and the failure is invisible because the wrong behaviour is a clean "ok". Shape: one helper every probe must go through, which cannot return a value without an explicit success, so that "I could not read this" is unrepresentable as "nothing to report". Roughly: : probe "" # sets a value on success, records a finding otherwise Then each check consumes the helper's result rather than a raw command substitution, and a new check written later inherits the discipline instead of having to re-derive it. Worth pairing with a test that asserts no check can report ok when its probe exits non-zero, generically, so the fifth instance is caught by the suite rather than by a reviewer. Not urgent: the current version is correct as far as anyone has found, ships with 58 tests, and proved itself on a genuinely wedged machine. This is prevention. Grading: Minor severity (no known live defect, the risk is future) x most-users-frequently (every future edit to this script) = P3 = [#C]... except the failure mode is silent and the script's whole job is catching silent failures, so a regression here is uniquely undetectable. P2 = [#B]. :solo: — the surface is one script and its suite, the refactor is behaviour-preserving, and the existing 58 tests plus a mutation battery are the objective check that it stayed so. ** TODO [#B] velox's systemd --user spins at 96% and cannot resolve unit files :bug:velox: :PROPERTIES: :CREATED: [2026-08-17 Mon] :LAST_REVIEWED: 2026-08-17 :END: Live on velox 2026-08-17 from about 10:29. =systemd --user= (pid 2235) sits in state R at 96% CPU, measured over a 3-second sample rather than taken from the lifetime average. It stopped logging at 10:29, so its timers appear to have stopped firing too. The split is the diagnostic: =systemctl --user list-units= still returns instantly, while =is-enabled=, =cat=, =show=, and =list-unit-files= all hang indefinitely. So the manager answers from its in-memory unit list and wedges on anything that has to resolve unit files. It is spinning in userspace, not blocked on I/O (=/proc/2235/wchan= is 0, no syscall pending). Remedies tried, neither worked: =systemctl --user daemon-reexec= hangs like every other unit-file call, and the signal form (=kill -59=, SIGRTMIN+25) was accepted but changed nothing. The next step is a logout/login or reboot, which is Craig's call because it closes his running session. I deliberately did not kill the manager: that would tear down the graphical session and everything under it. Suspected cause is the powerprofilesctl crash loop filed above, whose repeated activation attempts against a masked unit are the only new load on this machine. I cannot prove it, and I have to name the other candidate honestly: my own =post-rebuild-check= runs called =systemctl --user is-enabled= roughly thirty times per run over several runs, and the wedge appeared during that window. The crash loop predates those runs by an hour and a half, which is why it is the leading suspect rather than the certain one. What it costs: unit-file operations are unavailable, user timers appear stopped, and a core is pinned on a laptop running on battery. Grading: Major severity (a pinned core and stopped user timers, invisible unless you look) x rare edge case (one machine, specific conditions) = P2 = [#B]... except that this is a live, ongoing drain on a travelling machine rather than a latent defect, so it takes [#A] until the machine is back to normal. Re-grade to [#B] once resolved and the question is only prevention. *** 2026-08-17 Mon @ 19:57:42 -0700 The reboot cleared it; re-graded [#A] to [#B] as the task instructed velox rebooted at 16:04. The wedge is gone: =systemctl --user is-enabled roam-sync.timer= now answers =enabled= in well under a second, where every unit-file call hung indefinitely before, and =list-timers= shows calendar-sync, roam-sync and agenda-render-cache all firing on schedule again. So the remedy the task named — a logout or reboot — was taken and worked. Nothing here was diagnosed further, which means the cause is still unproven and both candidates in the body stand. What is left is prevention, and the task's own grading says that is [#B]: the live-drain argument was the only thing holding it at [#A], and the drain has stopped. Re-graded per that instruction rather than by a fresh judgment. Reproducing it deliberately is the open question, and it is not obviously worth doing — it costs a wedged session to learn something the crash-loop fix may make moot. ** TODO [#B] post-rebuild-check needs a reference-host mode :feature:velox:solo: :PROPERTIES: :CREATED: [2026-08-17 Mon] :LAST_REVIEWED: 2026-08-17 :END: =scripts/post-rebuild-check= ships and works, but its first live run on velox 2026-08-17 showed the output is mostly steady state rather than drift. Of the 8 findings that survived three rounds of false-positive removal, comparing against ratio says exactly ONE is real: =obsbot-wb-guard= is enabled on ratio and merely linked on velox, which is the deliberate deferral recorded 2026-08-16. The other three unit findings (=emacs=, =geoclue-agent=, =obs-record-watchdog.timer=) are linked on ratio too, and the three =.claude= absences are absent on ratio too. So the signal-to-noise is about 1:7, and the thing that separates them is a comparison against the other daily driver — the same discipline that kept the 2026-08-16 session honest when check 4 read as nine projects missing =CLAUDE.md= and ratio turned out to be missing the identical files. Shape: =--reference-host = runs the same five checks on the far machine over tailscale (ssh, read-only) and reports only the *differences*. Findings present on both machines are steady state and get summarized as a count rather than listed. Falls back to the current standalone behavior when the reference host is unreachable, and says so. Grading: Minor severity (the tool works and its findings are accurate; they are just buried) x every use = P3 = [#C]... except that a check nobody reads is a check that isn't run, which is the failure mode the whole task existed to close. Most-users-frequently x Major = P2 = [#B]. :solo: — the checks exist, the ssh path is proven (the 2026-08-17 session ran exactly this comparison by hand), and correctness is verifiable locally by diffing the two reports. *** 2026-08-21 Fri @ 07:10:00 -0700 The premise moved: velox now reports 1 finding, not 8 Re-scope before building. The 1:7 ratio this task argues from is gone, and two of the three things it cites as noise are fixed at the source rather than filtered. =87ff0b7= gave check 2 a machine-local expected-disabled list, so the four unit findings are declared intent rather than noise, and an entry whose unit turns out to be enabled is itself reported so the list cannot rot. =3fbf3e0= dropped =.claude= from check 4's expected set, since the gitignore sweep writes that line into every project whether or not one exists. velox went 8 findings to 1. So the open question is no longer "how do we cut the noise" but whether a live reference-host diff still earns its place against a static declaration of intent. They are different tools: the list is offline, explicit, and states what a machine means; the diff is automatic and catches drift nobody declared. The reference-host comparison is still what *found* all of this, twice, by hand. That is an argument for it and not against. Worth knowing this task already contained the whole 8-to-1 analysis when it was filed 2026-08-17, and a session on 2026-08-20 re-derived it from scratch without reading it. Not :solo: any more — the design call above is Craig's. ** TODO [#C] screen-lock test suite red on ratio :bug:test:dotfiles: :PROPERTIES: :CREATED: [2026-08-13 Thu] :LAST_REVIEWED: 2026-08-13 :END: tests.screen-lock.test_screen_lock fails 21 of 23 (+1 error) on ratio, verified pre-existing with unrelated changes stashed (2026-08-13). Breaks the make test green bar for every dotfiles commit until triaged. Suspect environmental (the suite exercises hyprlock/relaunch behavior that may need session state this shell lacks) or a regression from a recent screen-lock commit — diagnose, then fix or mark/skip with a reason. Grading: Major severity (blinds the pre-commit gate for the whole repo) × every-commit frequency on this machine, but test-infra only, no user impact = [#C] judgment call rather than matrix-dictated. ** TODO [#C] Keyboard backlight binding + boot default :feature:dotfiles:velox: :PROPERTIES: :CREATED: [2026-08-13 Thu] :LAST_REVIEWED: 2026-08-13 :END: Velox's kbd backlight (chromeos::kbd_backlight since the AMD board) boots at 0 and the dotfiles carry no keyboard-brightness keybinding at all (swept 2026-08-13 — never existed). Add: Hyprland binds (XF86KbdBrightness* or a chord) driving brightnessctl -d "chromeos::kbd_backlight", a sane boot default, and a udev rule granting the video/input group write access so it works without sudo (a bare ssh session got EPERM). Check whether Fn+Space (EC-handled on Frameworks) already cycles it — if so the bind is a complement, not the only path. Ratio: n/a (desktop). ** TODO [#A] Post-rebuild verification pass :feature:velox: DEADLINE: <2026-08-16 Sun> :PROPERTIES: :CREATED: [2026-08-14 Fri] :LAST_REVIEWED: 2026-08-14 :END: A rebuilt machine looks finished and isn't. Five gaps surfaced on velox within two days of the 2026-08-13 reinstall, and three of them LOOKED fine: a stowed unit file, an enabled timer, a present git clone. From the .emacs.d handoffs 2026-08-14 (inbox, both PROCESSED) plus what this session found independently. The generalizable fix is one pass the installer runs at the end, or a =post-rebuild-check= script the checklist points at. Each item is cheap and turns a silent no-op into a visible line: 1. =systemctl --user list-units --state=failed= — calendar-sync had been failing every 15 minutes for two days with nobody watching. 2. Every stowed/linked user unit that is NOT enabled. roam-sync and signal-receive came back linked and inert; two others were never linked at all. A unit file being present is not the same as running. 3. Every tracked =*.local.el.example= (or =*.local.*=) with no sibling real file. Three exist in .emacs.d; all three were gone on velox. 4. Every gitignore-mode project missing its =.ai/=, =.claude/=, =CLAUDE.md=, =todo.org=, =inbox/=. A reinstall drops the entire working state of every such project — 374 files and 4.5 MB in .emacs.d's case — and nothing carries it: not git, not stow, not the bootstrap. 5. =signal-cli listAccounts= non-empty. velox lost its registration, and because agent-text relays to a hardcoded velox, that breaks the phone channel for the WHOLE FLEET, not just this machine. 6. =mbsync --list= parses. The Proton Bridge TLS cert (=~/.config/protonbridge.pem=, referenced by =~/.mbsyncrc=) is generated per *installation*, so it cannot be restored or copied between machines. Its absence aborts the config parse, which kills *every* account — gmail and dmail need no bridge and died anyway. The error names only the missing pem, so "no mail at all" and "this one file is missing" look unrelated. Re-derive it off the running bridge's own handshake, no GUI, no secrets: =openssl s_client -connect 127.0.0.1:1143 -starttls imap -showcerts ~/.config/protonbridge.pem= 7. The bridge password (=~/.config/.cmailpass=) is per-install too. It is a real file rather than a stow symlink, so it survived the rebuild holding the *previous* install's value — worse than absent, because it looks right. Diagnostic trap: the bridge answers a wrong password with =no such user=, which reads as "no account signed in" and sends you hunting a login problem that doesn't exist. Never treat =no such user= as evidence about account state. *The distinction that organizes all seven* (from the .emacs.d handoff 2026-08-14, inbox): every artifact that broke was generated on the machine by an application rather than carried by git, stow, or dotfiles. But they split two ways, and conflating them is what produces a file that exists, looks right, and authenticates against nothing: - *Restore* — the old value is still correct: gitignored tooling (1), roam clone state (3), =*.local.el= configs (5). - *Re-derive* — the old value is worthless because the application minted a new one: signal-cli registration (4), bridge cert (6), bridge password (7). So the checklist wants two columns, not one. Graded [#A] because item 5 is live right now and silently disables paging, and because the flight is Sunday. ** TODO [#B] Restoring a git repo from backup can resurrect a dangerous diff :bug: :PROPERTIES: :CREATED: [2026-08-14 Fri] :LAST_REVIEWED: 2026-08-14 :END: My 2026-08-14 restore of =~/org= from the salvage brought back roam's =.git= deliberately ("simpler, preserves everything exactly"). It also brought back a clone ten commits stale AND an uncommitted =inbox.org= emptied to zero bytes. roam-sync is the repo's only committer and commits whatever it finds, so enabling that timer would have committed the emptying and pushed it — deleting the live inbox items ON RATIO. A .emacs.d session caught it, verified ratio's copy was a strict superset, discarded the local diff, fast-forwarded, and only then enabled the timer. Lesson to encode somewhere durable: restoring a git repo from a backup is not the safe option it looks like. For any repo with a live remote, re-clone and carry only proven-needed work; where a backup copy is restored anyway, reconcile it against the remote BEFORE any auto-committing timer is enabled. The failure here would have been silent and landed on a different machine. ** TODO [#B] Nothing installs the .emacs.d systemd user units :bug:velox: :PROPERTIES: :CREATED: [2026-08-14 Fri] :LAST_REVIEWED: 2026-08-14 :END: =~/.emacs.d/systemd/= ships four user units (agenda-render-cache service+timer, calendar-sync service+timer). On ratio they are symlinked into =~/.config/systemd/user/= by hand. Nothing does that on a fresh machine: they are not stowed (they live in .emacs.d, not dotfiles) and archsetup does not link them. Consequence found on velox 2026-08-14: the world wallpaper face drew nothing, because it reads =~/.cache/settings/agenda.json= and the timer that exports it was never installed. Calendar sync was silently dead for the same reason — which is the second time that particular timer has gone missing (see the 2026-08-01 session, where its auto-start was the bug). Linked and enabled by hand on velox; export verified (316 bytes, 1 event). Fix belongs in whichever owns the seam: either .emacs.d gains an install step for its own units, or archsetup links them alongside the dotfiles stow. Prefer the former — the repo that ships a unit should install it. Grading: Major severity (two background services silently absent, and the failure looks like a data problem rather than a missing timer) x every fresh install = P2 = [#B]. ** TODO [#C] Panel can leave a channel selected with nothing to show :bug:dotfiles: :PROPERTIES: :CREATED: [2026-08-14 Fri] :LAST_REVIEWED: 2026-08-14 :END: velox's store carries channel "pair" with pair_sel unset, so channels.selected_pair() returns None and wallpaper.apply() fails every time. Found 2026-08-14 when the new session-start restore reported "unavailable" and fell through to the waypaper fallback — the machine still showed dark-lion, which looks exactly like the bug that was just fixed. ratio is fine (channel world, pair_sel 0). Two candidate fixes, needs a call: either the panel refuses to switch to a channel whose selection is empty, or apply() falls back to the first minted pair/set when the index is unset. The second is friendlier and matches "the store is the source of truth" — a channel with exactly one plausible reading should not be a dead end. Grading: Minor severity (one fallback still puts a wallpaper up) x some users sometimes = P3 = [#C]. ** TODO [#B] Land the rescued emacs-wttrin commit :chore:velox: :PROPERTIES: :CREATED: [2026-08-14 Fri] :LAST_REVIEWED: 2026-08-17 :END: bf0457f "feat: add wttrin-hide-follow-line to hide the wttr.in follow line" (2026-06-24) was the only genuinely unpushed commit anywhere on the old velox — 3 files, 103 insertions, with a test file. Rescued as a verified git bundle before the disk was wiped: [[file:working/velox-reinstall/wttrin-bf0457f.bundle][working/velox-reinstall/wttrin-bf0457f.bundle]] To land it: clone emacs-wttrin, =git fetch --branches=, review the commit, then push to git@cjennings.net:emacs-wttrin.git. Delete the bundle once it's on the remote. *** 2026-08-17 Mon @ 19:57:42 -0700 Re-checked: still unlanded, and the bundle is still the only copy Cloned the remote bare and asked it for the object directly: =git cat-file -t bf0457f= returns "Not a valid object name", so the commit has never reached =git@cjennings.net:emacs-wttrin.git=. Remote =main= is =ee8fdeb=. That makes =working/velox-reinstall/wttrin-bf0457f.bundle= the sole surviving copy of 103 insertions across three files, on one laptop that is travelling. Worth doing sooner than its =[#B]= suggests for that reason alone, and it also pins the working directory open — the reinstall task cannot file its artifacts away while this bundle is still load-bearing. ** TODO [#B] archsetup doesn't clone rulesets :bug:velox: DEADLINE: <2026-08-15 Sat> :PROPERTIES: :CREATED: [2026-08-14 Fri] :LAST_REVIEWED: 2026-08-14 :END: A fresh install has claude but no =ai=, no skills, no rules, no hooks, because =~/code/rulesets= is never cloned. Found on velox 2026-08-14 when =ai= wasn't on PATH. archsetup clones dotemacs, dotfiles, the suckless tools and itself, so rulesets is the one workstation repo it misses, and without it the whole agent tooling layer is absent on a rebuilt machine. Fix: clone it alongside the others (=RULESETS_REPO=, defaulting to git@cjennings.net:rulesets.git) and run =make install= afterwards, which is what links the 54 symlinks into ~/.claude and ~/.local/bin. Graded Major severity (a rebuilt machine silently loses every agent workflow) x most-users-frequently = P2 = [#B]. Worked around by hand on velox already; this is the durable half. ** TODO [#B] Hibernate in the settings dial power actions :feature:dotfiles: :PROPERTIES: :CREATED: [2026-08-13 Thu] :LAST_REVIEWED: 2026-08-13 :END: Add hibernate alongside suspend/lock in the settings module's dial power actions. The wlogout exit menu already carries it (keybind h) and needs no work; the dial is the remaining surface. Sequencing (Craig confirmed the dial placement 2026-08-13): 1. DONE 2026-08-14 00:14 — hibernate proven end to end on velox, driven from the exit menu so the wiring was exercised too. Evidence: the boot id was unchanged across the cycle (f14152f9…) and uptime kept counting 3h15m → 3h18m, so it genuinely resumed rather than rebooting; the journal carries "PM: hibernation: hibernation exit" and the HibernateLocation EFI variable being cleared. Took 9.3s wall. The whole chain works: suspend-to-disk into the LUKS-encrypted swap, resume via the keyfile embedded in the initramfs, one passphrase at ZBM. 2. Then consider suspend-then-hibernate as the default lid behavior (systemd sleep.conf HibernateDelaySec) — hibernate's savings with no button at all; possibly a "deep sleep" toggle in the module. 3. Then the dial action itself. Note: ratio has no swap partition, so hibernate stays velox-only until ratio gets one; the dial entry should degrade gracefully where there's no resume target. ** TODO [#A] Move secrets out of public dotfiles → private repo + combined personal ISO :feature:security:dotfiles: :PROPERTIES: :CREATED: [2026-08-11 Tue] :LAST_REVIEWED: 2026-08-11 :END: Structural fix for the root cause behind both 2026-08-09/10 leaks: secrets live in the *public* dotfiles repo and rely on in-place encryption, which two commits defeated. Design agreed with Craig and tabled before build 2026-08-11. Full detail + the pickup point are kept LOCAL (they map the setup, so not in this public repo): =.ai/private-design/2026-08-11-secrets-repo-and-combined-iso.org=. Shape in one line: move secrets to a private, off-public-scan-path repo, keep them encrypted even there (defense in depth), and deliver them to a fresh install as an encrypted bundle baked into a *personal* ISO — riding the combined archangel+archsetup ISO that's already ~80% built. Two ISO modes: generic (shareable) vs personal (encrypted secrets, private). [#A] because it closes a live-leak class, but *gated on the rotation VERIFY* — don't start the migration until the credentials are rotated. Not started. Not :solo: — repo standup and history rewrite are Craig's calls; promote to a real spec (spec-create) when work resumes. *Also bake the push-capable repo URLs into the personal ISO* (decided 2026-08-19). =archsetup:240= and =:245= default =archsetup_repo= and =dotfiles_repo= to =https://git.cjennings.net/...=, the anonymous read-only endpoint. That default is right for a stranger installing archsetup — no key on the server — and wrong for my machines, which have to push: velox came back from its rebuild unable to push either repo, and I only found out at a 403 four days later. I decided against detecting an ssh key in the installer, because archsetup never restores =~/.ssh= (I do that by hand), so key-presence at clone time depends on ordering the installer doesn't control, and a naive "any key means ssh" would break a stranger who happens to have one. The override already exists and is documented — =ARCHSETUP_REPO= / =DOTFILES_REPO= in =archsetup.conf.example= — so the personal ISO just needs to carry the ssh form of both, alongside the secrets bundle. The generic ISO keeps the https default untouched. The gap that leaves is a curl|bash or stock-ISO install, which takes the https default straight back. =post-rebuild-check= check 8 covers that path — it flags a working repo whose origin is the read-only endpoint — so the ISO value is the fix and the check is the net under it. ** TODO [#B] Settings toggles reset silently at session start :bug:dotfiles: :PROPERTIES: :CREATED: [2026-07-28 Tue] :LAST_REVIEWED: 2026-07-28 :END: Craig, from the roam inbox 2026-07-28: "launching into wayland doesn't honor previous caffeine settings ...or I expect any other settings in the desktop settings module." Captured right after the 08:59 reboot. Confirmed, and it generalizes past caffeine. The settings module splits cleanly into two halves, and only one of them persists. Persisted, in =~/.config/desktop-settings/state.json= (=store.py= =DEFAULTS=): program slots, idle-tripper stages, wallpaper. These come back correctly. Not persisted — every one is derived live from a process or a compositor runtime option, so a session restart resets it to whatever =hyprland.conf= establishes: - Caffeine — =caffeine_state()= is =pgrep -x hypridle= inverted, and =hyprland.conf:73= runs =exec-once = pkill -x hypridle; hypridle=. So every launch unconditionally starts hypridle, which means caffeine is *always* OFF after login. There is no code path that could restore it ON. - Auto-dim — =dim_state()= reads =hyprctl getoption decoration:dim_inactive=, a compositor runtime value that resets to the config default on restart. - Night light — =state()= is =pgrep -x gammastep=; the process dies with the session. - DND — =dunstctl=; dunst restarts fresh from =exec-once=. - Power profile / brightness — owned by powerprofilesctl and systemd-backlight, outside this module's scope. Verified live 10 minutes after the reboot: hypridle running (caffeine OFF), dim =false=, gammastep not running, dnd =false=, power =balanced=. Every toggle sat at its factory position. The failure is silent, which is what makes it bite: nothing tells you the value you set was discarded. That is the mechanism behind the 2026-07-27 lockout, where Craig believed caffeine was on and the screen locked anyway. Grading: Major severity (the panel's core promise is holding these values, and the reset is silent and total across all four toggles) x most users frequently (every session start resets them, though it only harms when a deliberate non-default was set) = P2 = [#B]. Not :solo: — the fix needs Craig's call on *which* toggles should persist and whether persistence is per-toggle opt-in. Restoring night light at 3pm or caffeine on a laptop are both plausibly wrong, so this is a preference question, not a derivable one. The mechanism itself (extend =store.py= with a =toggles= block, restore on session start) is mechanical once that's settled. Related: =[#B] Caffeine state is unreadable on both surfaces= covers display accuracy — whether the surfaces report the truth. This covers whether the value survives at all. Distinct bugs, same subsystem. *** Side finding — gammastep loses a startup race and nothing relaunches it =hyprland.conf:75= runs =exec-once = gammastep=, but no gammastep process is alive. Today's three launch logs tell the story: =gammastep-2026-07-28-090034.log= carries "Wayland connection experienced a fatal error: -1 / Temperature adjustment failed", and the other two are empty. Launched by hand afterward it runs fine and survives, so gammastep is not broken — it loses a race against compositor readiness at session start. Nothing relaunches it, so night light is simply off for the whole session, silently. (An earlier read of this said night light "has likely never worked from the config". That was wrong: the failure is a startup race, not a permanent break.) Worth its own task — the fix is a readiness wait or a retry around that exec-once, not a persistence change. Filed here for now because it surfaced during this investigation. ** VERIFY [#A] Pre-vacation fix list — morning review SCHEDULED: <2026-08-08 Sat> :PROPERTIES: :LAST_REVIEWED: 2026-08-08 :END: The full todo.org sweep you asked for before sleeping, ranked by what I'd fix before departure (~2026-08-15, velox travels). Approve, reorder, or strike; items needing your call say so. 1. Velox reliability (the anchor — [#A] sleep/suspend, rescheduled Wed 2026-08-12). Velox is out for repair/upgrade until Tuesday or Wednesday (Craig, 2026-08-08), so every velox item waits for its return — a tight but workable window before the ~08-15 departure. Riders already folded in: the tlp.d radio-enable line, a dotfiles pull, the touchpad-detection spot-check. 2. Velox machine health for travel (NEW — filed nowhere else): resolve the ~/code/auto-dim-other-buffers.el merge conflict (literal conflict markers in a loaded .el; its emacs suite has been red since 2026-08-01), clear the stale password prompt sitting on its screen since 2026-07-31, and run a maint doctor pass. 3. Remote access verified from OUTSIDE the LAN while you're still home: tailscale to ratio, truenas, and truenas-kvm from a phone hotspot. DECIDED (Craig, 2026-08-08): the wolf WireGuard profile gets set up on velox when it returns Tue/Wed — added to the velox-return riders. Cheap at home, expensive to debug from a hotel. 4. The cgit secrets/privacy audit ([#B] below): a world-readable secret standing while you're away is the worst timing. The repo-by-repo scan is mine to run; the public-vs-private call per repo is yours. The archsetup cgit move can wait unless the audit finds something. 5. Already scheduled today: osbot camera (needs the camera plugged in). Buildable any time: the podman socket + camera udev task (:solo:). 6. Optional travel niceties blocked on upfront-answerable design calls in their bodies (two for hotspot/metered WiFi in amber, one for network-panel ordering by availability). Answer the calls and I can build both. 7. Deliberately left off: offline LLM (you declined the vacation track), night-watch/lock-watchdog (ratio stays home with no user to relock; say so if you disagree). Found tonight, low priority: the orchestrator sequence pin can't see an added-but-unstubbed call (it caught drops only) — worth a harness hardening pass someday. ** TODO [#C] Re-apply the active program at session start :refactor:dotfiles:hyprland: :PROPERTIES: :CREATED: [2026-07-30 Thu] :LAST_REVIEWED: 2026-07-30 :END: Follow-up to the direction-aware persistence fix (dotfiles, 2026-07-30). I took the narrow repair first and deferred this deliberately. =settings restore= replays individual toggles from =store.DEFAULTS["toggles"]= — caffeine, DND, and now dim. But =active_program= is stored and read by =programs.py= and never re-applied as a unit. So the panel's claim that a program is active is only true to the extent that the individual toggle replays happen to reconstruct it. Right now the gap is small: dim, DND and caffeine are the fields that don't survive on their own, and all three now persist individually. Night light is deliberately excluded (time-of-day dependent) and the power profile is persisted by powerprofilesctl itself. So the observable behavior is close to correct today. What's wrong is the structure, not the current output. Two sources of truth describe the same intent — a program definition and a list of remembered toggles — and nothing keeps them in step. Add a field to a program and it silently won't be replayed; change what a program means and the replay still asserts the old fields. The drift is invisible until someone notices the desktop disagreeing with the panel, which is exactly how the dim bug surfaced. The change: have restore resolve the active program and apply it through =panel.apply_toggle=, with the remembered-toggle list becoming a fallback for a session with no active program. The =ASSERT_OFF= direction rule still applies per toggle and must survive the move — caffeine's ON-only assertion is a safety property, not an optimization (velox, 2026-07-22). Grading: Minor severity (no wrong behavior today; it's a latent drift that produces one on the next change to a program's fields) x some users sometimes (only bites when a program definition changes) = P3 = [#C]. :solo: — the design was settled when the fix was chosen, the surface is enumerable (=session.restore=, =programs.py=, =panel.apply_toggle=), and it is verifiable locally: the existing restore suite plus a test that a stored active program is reconstructed field by field after a simulated session start. ** TODO [#B] "SCREEN OFF" reads as a state, not a stage :bug:dotfiles:design: :PROPERTIES: :CREATED: [2026-07-29 Wed] :LAST_REVIEWED: 2026-07-29 :END: Craig, 2026-07-29: "The module said SCREEN OFF, which I took to mean the screensaver was off." =panel.py:43= labels the DPMS stage =SCREEN OFF=. It names the *action that stage performs* (power the display off), but it reads as a *status report* (the screen feature is off). Craig read the second meaning, concluded the screensaver was disabled, and reported the lock bug on that basis. It cost a wrong turn in a live diagnosis. Two compounding factors. Nothing in the panel or the bar uses the word "screensaver" at all, so the thing he calls the screensaver is labelled =WATCH= and is not findable by its own name. And the rail's other labels are unambiguous verbs or nouns (=DIM=, =LOCK=, =SUSPEND=), so =SCREEN OFF= is the only one that parses two ways. Fix direction: rename so the label names the action consistently with its siblings (=BLANK=, or =DISPLAY OFF=, or =SLEEP SCREEN=), and give =WATCH= a name that connects to what it is (=SCREENSAVER=, or =WATCH FACE=). Worth a look at the whole rail's vocabulary at once rather than patching one label. Grading: Major severity (the label doesn't just look wrong, it produces a confident false belief about whether a security-adjacent control is active, and that belief drove a wrong turn in a live investigation) x most users frequently (every reading of the rail) = P2 = [#B]. An earlier draft graded this Minor, arrived at [#C], and then wrote [#B] beside it anyway with a justification. That is overriding the letter, which the format rule specifically forbids because it turns the matrix into a formality. The input that was wrong is the severity band: a label that reliably misinforms about state is more than cosmetic. Corrected the band, and the letter now follows from it. Not :solo: — naming is Craig's taste call, and the rail's vocabulary should be decided as a set. ** TODO [#A] Night watch and the lock watchdog fight each other :bug:hyprland:dotfiles: DEADLINE: <2026-07-31 Fri> :PROPERTIES: :CREATED: [2026-07-29 Wed] :LAST_REVIEWED: 2026-07-29 :END: ROOT CAUSE of the lockdead screens, found 2026-07-29 00:50 within minutes of the relaunch logging going live. hyprlock is not crashing. It is being killed on purpose, by us. =settings/src/settings/watch.py:116= runs =pkill -x hyprlock=. That is deliberate and documented in the module's own header: the night watch *is* the lock screen, a normal window cannot paint over hyprlock's ext-session-lock surface, so =start()= maps the kiosk behind the lock and then kills hyprlock to reveal it already drawn. =stop()= re-locks first, then kills the kiosk, so the desktop never flashes. =screen-lock='s watchdog cannot tell that apart from a crash. Any non-zero exit means "died while still locked, relaunch it", and SIGTERM reports 143. The collision, on hypridle's own timings: - t=450s: =loginctl lock-session= → screen-lock → hyprlock up, watchdog waiting on it. - t=480s: =settings-watch start= maps the kiosk and kills hyprlock. Exit 143. *Two relaunches after the mitigation, unexplained (found 2026-08-04).* The watch stage was parked in =db5ac60= at 2026-07-29 05:59, and nine of the eleven entries in =screen-lock.log= fall before that (07-28 19:00 through 07-29 05:08, all rc=143, the collision as diagnosed). But two more landed on *2026-07-30 10:58:11 and 10:58:13* — rc=143 then rc=137, SIGTERM then SIGKILL, two seconds apart. Craig unlocked the keyring 30 seconds later. So parking the stage did not stop every relaunch. Either something else kills hyprlock, or that pair was Craig at the keyboard. The log cannot distinguish those, and the journal for that window shows only an unrelated settings tick. What would settle it: the watchdog does not record *who* sent the signal, only the exit code. Logging the killer (or at least distinguishing a session-initiated unlock from an external kill) would turn this from a guess into a reading. Worth doing before designing the handshake, since the handshake assumes the night watch is the only thing killing hyprlock and that assumption is now unproven. - t=480s + =LOCK_RELAUNCH_DELAY= (0.5s): the watchdog relaunches hyprlock, which comes back *on top of* the night watch. - In that half-second gap the session is locked with no client, so Hyprland draws lockdead. The replacement hyprlock then blurs it. That is exactly what Craig saw: lockdead text blurred behind a working lock screen, clearing on authentication. Two consequences, and the second is worse than the cosmetic one: 1. The lockdead artifact. 2. *The night watch is silently broken.* It reveals itself and is covered by a relaunched hyprlock half a second later, every time. The feature has not worked since the watchdog shipped. Evidence: =~/.local/var/log/screen-lock.log= carries nine entries, every one =rc=143=: three on 2026-07-28 (19:00:00, 22:53:56, 23:17:58) and six on 2026-07-29 (02:59:54 through 05:08:01). Zero entries from any other cause. The AMD theory is dead. =amdgpu.runpm=0= was live for all three, there is no DPMS idle rule, and no coredump exists because nothing crashed. The wf-recorder hypothesis is also unsupported: a deliberate lock at 00:50 with a recorder running produced no relaunch, because I unlocked at 12 seconds and never reached 480s. Grading: Major severity (the lock client is killed mid-lock on every cycle, the night-watch feature never actually shows, and a sustained collision can exhaust the watchdog's 30-attempt cap and leave the session locked with no client, recoverable only from another console) x most users frequently (every idle period reaching the watch stage; nine occurrences logged in eleven hours) = P2 = [#B]. An earlier draft graded this [#A] under the security carve-out. That was wrong and the correction is worth keeping. The carve-out covers privacy or security leaks, compliance violations, and safety issues. Nothing leaks here: the session stays locked throughout, the lockdead frame clears on authentication, and the harm is availability plus a silently broken feature. Invoking the carve-out inflated a P2 two bands into the always-visible [#A] gate, which is the exact abuse the matrix exists to prevent. Not :solo: — the fix is a design decision between two subsystems, both of which Craig owns: 1. Teach the watchdog that SIGTERM is not a crash (skip relaunch on 143). Simple, but it weakens the "a kill re-locks rather than unlocks" property the watchdog was written for. 2. Have =settings-watch= tell the watchdog to stand down before it kills, via the flag file it already maintains at =$XDG_RUNTIME_DIR/settings-watch-relock=. Keeps the kill-re-locks property intact and is the more honest handshake. 3. Stop killing hyprlock at all and find another way to reveal the kiosk. Option 2 is the one I would argue for, but it is Craig's call. Previous title and framing of this task, kept for the record: "hyprlock still exits mid-lock; the watchdog relaunch is silent". The instrumentation that closed that gap is dotfiles =5bbe2c3=, and it paid for itself in about six hours. ** TODO [#B] The wireguard gpg convention is inert; the installer can't read it :bug:security:network: :PROPERTIES: :CREATED: [2026-07-28 Tue] :LAST_REVIEWED: 2026-07-28 :END: Found 2026-07-28 by an independent review, while deciding whether to commit a newly-encrypted =wolf.conf.gpg=. =assets/wireguard-config/.gitignore:3= states: "Ship configs gpg-encrypted (*.conf.gpg) only; the installer decrypts at import." That is false. =scripts/import-wireguard-configs.sh:43= globs =$dir/*.conf= and nothing else, there is no =gpg= call anywhere in it, and line 67 hard-errors "no .conf files in $dir". A =.conf.gpg= in that directory is invisible to the importer. So the post-leak convention has a hole in the middle. The =.gitignore= permits tracking =*.conf.gpg= and tells you the installer will handle it, which invites committing encrypted secrets into a repo that cgit still serves anonymously, in exchange for a capability that does not exist. The encryption is real; the payoff is not. =README.org:12= disagrees with the =.gitignore= too: "Nothing here is tracked except this note and the .gitignore." Written 13 minutes after the =.gitignore= on 2026-07-20, so the stricter line is the later one. Three sources, two positions, no resolution. Grading: Major severity (the stated policy actively invites putting live key material into a world-cloneable repo on a false premise, and a leak here is unrecallable once cloned) x some users sometimes (only fires when someone adds a config) = P3 = [#C]... except this is the security carve-out, graded on severity alone because one occurrence with the right consequences is a showstopper. = [#B]. Not :solo: — the fix is a decision, not a build. Three options, and they interact with the open cgit move: 1. Teach the importer to decrypt =*.conf.gpg= (needs a passphrase or agent at install time, which on a fresh-install path may not exist — that is the likely reason it was never written). 2. Drop the =!*.conf.gpg= exception and keep the directory genuinely empty of secrets, tracked or not, until archsetup is off the public host. 3. Do the cgit move first, then revisit. Until it is resolved, do not commit any =*.conf.gpg=. The encrypted =wolf.conf.gpg= now lives at =~/.config/wireguard/wolf.conf.gpg= (mode 600), moved out of this repo entirely on 2026-07-29. Untracked-in-tree was the wrong resting place for it: a single =git add -A= puts a secret into a world-cloneable repo, and the directory's own policy is unusable until the importer can actually read encrypted configs. The live NetworkManager profile is unaffected. Related: =[#B] Move archsetup off cgit= and =[#B] Audit cgit-published repos for secrets and privacy=. Both are still open, and both argue for keeping new secrets out of this repo until they land. ** TODO [#B] Timer presets should start in one click :feature:dotfiles:timer: :PROPERTIES: :CREATED: [2026-07-28 Tue] :LAST_REVIEWED: 2026-07-28 :END: Craig, captured 2026-07-28, routed here by home's inbox-zero pass from the shared roam inbox. Verbatim: "timer/alarm: timer preset buttons should be one click. we will have to adjust so the user knows to name the timer first (have the label on top of the timer buttons), have the text field on the same row, and add below. if the user adds the time value in the text field and presses enter, that should also start the timer." Two changes in one. A preset button should start its timer on a single click rather than needing the name step first, and the input area gets rearranged so the naming flow is obvious: label above the preset buttons, text field on the same row, add below. Enter in the text field starts the timer too. Panel source is =~/.dotfiles/timer/src/timer/= (=gui.py= for the view, =panel.py= for the presenter, =viewmodel.py= for state). Not :solo: — the rearrangement is described but not settled, and the result is a visual judgment Craig has to see. The one-click behaviour is buildable on its own; the layout wants a pass in front of him. Per the UI-prototyping rule, sketch the arrangement before touching production code. Related: =[#C] Add a time selector to the timer panel= covers a duration picker for the same input area. Design them together when either is picked up. ** TODO [#C] Post-upgrade hooks: compositor restart reminder + font cache rebuild :feature:infra:ratio:solo: :PROPERTIES: :CREATED: [2026-07-25 Sat] :LAST_REVIEWED: 2026-07-25 :END: Handoff from home (2026-07-25), originally combining the 2026-06-07 stale-compositor incident and 2026-06-08 fontconfig crash diagnosis. Add two reproducible pacman =PostTransaction= hooks through archsetup; do not make one-off =/etc= edits: 1. On =Upgrade= of =hyprland=, =aquamarine=, or =mesa=, print a non-blocking reminder to log out and back in. Never restart the running compositor from the hook. This prevents a session from quietly continuing on a deleted Hyprland binary against newly installed libraries (ratio did so for 2.5 days before SIGABRT on 2026-06-04). 2. On =Upgrade= of =fontconfig=, =freetype2=, or =harfbuzz=, run =/usr/bin/fc-cache -f= after the transaction. The fontconfig 2.17→2.18 cache-format change left stale cache-9 files that crashed Qt6 apps in =FcCharSetHasChar= until the system font cache was rebuilt. Acceptance: hook files are source-controlled and installed by archsetup; package/operation/action fields are asserted from the generated hook text; the reminder is print-only and exits successfully; the font hook runs only after successful matching upgrades and invokes the absolute =fc-cache= path. Validate with the fast installer tests plus a disposable pacman-hook parser/install check when practical. ** VERIFY Should coredump entries group as one journal-digest row per binary? :maint: :PROPERTIES: :LAST_REVIEWED: 2026-07-24 :END: 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. 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.) 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. 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. 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. ** TODO [#C] World face: hour-format picker in the settings panel :feature:dotfiles:solo: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 :END: Put a 12/24-hour toggle for the world face in the desktop-settings panel. The knob already exists — =settings/faces/world.html= reads =hour12= from =SETTINGS= with a =?hour12== URI override, defaulting to 12 — but the only way to change it is to hand-edit a launch URI, which is not a way to change anything. Build: (1) a =world_hour12= field in the wallpaper state, default true; (2) =project.py build_uri("world")= appends =&hour12== read from state; (3) a 12/24 toggle in the world channel's config section (=gui.py _conf_projected=, currently just a preview); (4) TDD the URI-building and the state round-trip. Solo — buildable, agent-verifiable through the URI and state tests plus a headless render, and nothing left to decide. Re-scoped 2026-08-02, down from [#B]. As written on 2026-07-23 this also wanted an orientation picker, because the face then supported vertical and horizontal through =?layout==. The 2026-07-31 rebuild replaced that with the single vertical spine and its horizontal day axis, and no =layout= flag survives in =world.html= — so the orientation half has nothing left to pick between and is dropped rather than deferred. One toggle is parking-lot work, not this cycle's, hence [#C]. Not =:quick:= despite being small: four pieces with tests is a sitting rather than a spare moment. I said "probably quick" when recommending the re-scope and that was optimistic — the piece count didn't drop, only the field count within each. ** TODO [#C] Wallpaper channel: timed transitions as an alternative to sunrise/sunset :feature:dotfiles: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 :END: 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. 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] 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: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 :END: 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:quick:solo: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 :END: 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] Night-watch live telemetry :feature:maint: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 :END: 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 [#C] Wlogout screen review :bug:hyprland:dotfiles: :PROPERTIES: :LAST_REVIEWED: 2026-08-02 :END: 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 [#A] Audit cgit-published repos for secrets and privacy :bug:security: :PROPERTIES: :LAST_REVIEWED: 2026-08-09 :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). Raised [#B] → [#A] on 2026-08-09: the scan found a real live-credential leak (below), so this is now a confirmed exposure with an open rotation blocking, not a hypothetical. Drops back to [#B] once rotation is done and the visibility rulings are made. Not :solo: — needs Craig's decisions and the credential rotation. Steps: list repos under /var/git; for each, decide intended public vs private; scan each for secrets (done, below); 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. archsetup's own move is decided and tracked separately below. *** VERIFY [#A] Rotate the credentials exposed by the 2026-08-09 dotfiles leak SCHEDULED: <2026-08-10 Mon> A plaintext credential file was briefly public in the dotfiles repo and was confirmed pulled by an external crawler before the purge, so every credential in it must be rotated. Full list, forensic detail, and the remediation record are kept LOCAL, not in this public repo: =.ai/private-design/2026-08-09-cgit-secrets-audit.md=. A second, low-severity unencrypted token was also exposed and needs a re-auth + purge (same doc). Craig's action; the purge only stopped further copies. Desktop alarm set (=at= job 61, 15:00) as the backup nudge. *** 2026-08-09 Sun @ 12:05:00 -0500 Found + purged a public plaintext-credential leak A file that looked encrypted by its name was plaintext in one commit; a second, content-verifying triage caught it. Purged from history in both repos (the shareable dotfiles and this one) and reconciled the local clones; a fresh anonymous clone no longer serves it. Everything else the audit flagged was benign. The forensics, exact mechanism, and remediation steps are in the local doc above (not published, since they map the setup). Follow-ons: the rotation VERIFY above, velox reconcile on return, the secrets-repo split (top of Open Work), the wireguard =.gitignore= bug (line ~191), the cgit move (below), and a pre-receive secret-scan hook so this can't recur. *** 2026-08-17 Mon @ 19:57:42 -0700 Moot — the 08-13 wipe re-cloned velox from the rewritten remotes This asked velox to reconcile clones that no longer exist. The machine was wiped and reinstalled on 2026-08-13, so every repo on it was cloned fresh *after* the purge and never held the pre-rewrite history at all. The runbook anticipated this ("fresh clones automatically carry the post-purge rewritten git history"); nobody closed the task once the reinstall took that route. Verified rather than assumed: both repos are level with =origin/main= today — archsetup at =6faa31c=, dotfiles at =65940f2=, both trees clean. One thing the reinstall did leave, and it is filed separately: the installer cloned both repos =--depth 1=, so the history was present-but-truncated until today's =git fetch --unshallow= (see the shallow-clone =[#A]=). A reconcile against the rewritten remote was still unnecessary — a shallow clone of the right history is not a diverged clone of the wrong one. ** TODO [#B] Move archsetup off cgit to cjennings@cjennings.net :chore:security: :PROPERTIES: :LAST_REVIEWED: 2026-08-17 :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). *** 2026-08-17 Mon @ 19:57:42 -0700 Re-checked: unstarted, and the exposure is confirmed live Ran the task's own verification step as it stands today, which is the honest way to check an unstarted task rather than reading its body back. Anonymous =git ls-remote https://git.cjennings.net/archsetup.git= succeeded with no credentials and returned =6faa31c= — this afternoon's HEAD. So the repo is still world-cloneable and current to the commit, not a stale published snapshot. =origin= on this machine is still =git@cjennings.net:archsetup.git=, the cgit account, so nothing has moved. Everything in the plan stands unchanged. *** 2026-08-21 Fri @ 14:12:46 -0700 Recorded the publication mechanism: placement is the only control The work project verified its own repo reads "not served" against a control repo that reads PUBLIC, and reported the mechanism back: the host publishes via =GIT_HTTP_EXPORT_ALL= over =GIT_PROJECT_ROOT=/var/git=, so *publication is directory placement and nothing else* — there is no per-repo marker, no =git-daemon-export-ok= file, no opt-in flag to check. A repo is public because of where it sits. That is the durable hazard for this task's plan, and it cuts both ways. It confirms the approach — a bare repo created outside the scan-path is private by construction, which is exactly what the plan already specifies. It also means nothing in a repo itself records whether it is exposed, so any future move *into* =/var/git= publishes silently, with no local artifact to notice. Their own repo is private for this reason alone: it lives under =/var/cjennings/git/=, outside the served root. Caveat they raised and I agree with: any enumeration of the served set is a snapshot, not a standing fact. The set moved twice while three projects were measuring it. Verify placement at the time of the move rather than trusting a recorded list. ** 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). ** 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. ** TODO [#B] Reconcile panel keybindings around Super+N :feature:hyprland: :PROPERTIES: :LAST_REVIEWED: 2026-08-21 :END: Put every panel on one consistent chord family — net, bluetooth, audio, timer, and the maintenance console — as a shared modifier set plus a mnemonic letter per panel (N/B/A/T/M). Today they open by waybar click only, so a uniform family is what makes them keyboard-reachable and predictable. The immediate move is swapping the notification and networking bindings so the primary panels sit one Super-plus-letter chord away. Maintenance (M) is the chord I want first — it is the panel I keep reaching for without one. Constraints: - Super+Shift+A is already the PTT toggle, and the hold-to-talk grave bind is load-bearing. Audit every current hyprland bind for conflicts before proposing a family, and treat these two as fixed. - Both machines have to work the same way. Velox can't QMK-remap, so the chords have to be typable on a plain laptop keyboard. Steps: settle the modifier family, audit the existing binds for collisions, wire it through the dotfiles hyprland config, and document it in the keybind reference. The family itself is the one call I haven't made — the audit and the wiring follow from it, so that decision comes first rather than last. *** 2026-08-21 Fri @ 14:15:22 -0700 Merged the duplicate keybinding-family task into this one Two tasks were carrying one job: this one and =[#B] Consistent keybinding family for the panel console=, filed separately and both stalled. This one had the tighter framing and the more recent review; that one had the better body — the specific collisions, the velox plain-keyboard constraint, and maintenance-M as the priority chord. Folded its detail in here and cancelled it, since two half-specified tasks for one decision is plausibly why neither moved. Not =:solo:= and not =:quick:=: the modifier family is a preference call I have to make, judging what's load-bearing among the existing binds needs me too, and the audit plus wiring plus docs runs past thirty minutes on its own. ** 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. ** 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. ** DOING [#B] Widget gallery upgrades :feature:design: :PROPERTIES: :LAST_REVIEWED: 2026-08-23 :END: Usability + documentation pass over the [[file:docs/prototypes/panel-widget-gallery.html][panel widget gallery]], orthogonal to the component-generation spec work. Items 1-4 run as a no-approvals speedrun (Craig authorized 2026-07-12); item 5 is a joint brainstorm. The =gallery-upgrades= branch this originally described is gone — no local or remote ref, and every gallery commit since has landed straight on main. Whether it was squash-merged or abandoned, the branch stopped describing how this work runs, so the line came out at the 2026-08-23 review rather than being left to mislead. Work on main. *** 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. *Audited 2026-08-23 — the sweep covered the bulk and stopped short.* Ten commits on 2026-07-18 (=43725ff= … =1dd929d=) carried ~104 of the 112 builders over. Two criteria are fully met: 105 builders carry a real contract comment naming opts and the handle surface, and component CSS is wholly consolidated — the gallery's own =