diff options
| -rw-r--r-- | archive/task-archive.org | 510 | ||||
| -rw-r--r-- | todo.org | 590 |
2 files changed, 551 insertions, 549 deletions
diff --git a/archive/task-archive.org b/archive/task-archive.org index 662dd0f..1372d4f 100644 --- a/archive/task-archive.org +++ b/archive/task-archive.org @@ -872,3 +872,513 @@ CLOSED: [2026-07-01 Wed] Move the timer module to the rightmost position, just left of the systray, and move the battery/sysmonitor module to second-to-rightmost. Config edit in the waybar config (dotfiles hyprland tier). Origin: roam inbox capture. Done 2026-07-01 (dotfiles waybar config): =custom/timer= now sits just left of =tray= with =custom/sysmon= second-to-rightmost. waybar regenerated + reloaded live on velox; visual confirmation pending Craig. +** DONE [#B] Right-click date/time: ntp sync + timezone update :feature:waybar: +CLOSED: [2026-07-02 Thu] +Right-click on the date updates the clock from ntpd (or whatever keeps the clock in sync); right-click on the time runs the update-timezone script. Neither opens a terminal — both run transparently in the background. Errors surface as notifications. The module should detect whether we're online and, if not, show a message instead of running the script. Origin: roam inbox capture 2026-07-02. + +Shipped 2026-07-02 (dotfiles 2f7993d). Date right-click = clock-sync (chronyc makestep behind a connectivity gate; offline/captive get explanatory notifications). Time right-click = timezone-set, rewritten to prefer WiFi geolocation (whereami → timeapi.io) over IP lookup — the hotel IP geolocated two timezones off, and ipapi.co is paywalled now (ipinfo.io is the fallback). Both promptless (sudo -n), all outcomes notify, worldclock gained signal 16 for an instant refresh. 13 new tests across clock-sync + timezone-set; live-verified on velox (WiFi path resolved Rhode Island → America/New_York correctly). +** DONE [#B] Screenshot "view image" option :feature:hyprland: +CLOSED: [2026-07-02 Thu] +The screenshot flow should also offer a "view image" selection: saves the shot, opens it in a viewer, and puts the path on the clipboard. Origin: roam inbox capture 2026-07-02. + +Shipped 2026-07-02 (dotfiles 10e5961). View Image entry in the post-capture fuzzel menu: opens the shot via xdg-open (default viewer, currently feh) and puts the path on the clipboard. Script gained env seams + a nine-test suite (menu dispatch, both capture modes, cancel, failure). Live check pending: take a shot and pick View Image once. +** DONE [#C] Collapse-triangle buttons: dimmer, inlaid styling :waybar: +CLOSED: [2026-07-02 Thu] +The triangle collapse buttons should look embedded (inlaid) and be slightly dimmed so they don't compete for eye attention with the other components. Origin: roam inbox capture 2026-07-02. + +Shipped 2026-07-02 (dotfiles 15cb93c): muted color + dark inset well + smaller glyph; hover still brightens. Hudson theme carries the same shape. Screenshot-verified. +** DONE [#C] Contrast button ignores the white=on / red=off paradigm :bug:waybar: +CLOSED: [2026-07-02 Thu] +The contrast button doesn't respect the white=on, red=off color paradigm the other waybar modules follow. Cosmetic × every time = P3. Origin: roam inbox capture 2026-07-02. + +Shipped 2026-07-02 (dotfiles 15cb93c). The "contrast button" is the auto-dim module — its ON icon (nf-fa-adjust) is the classic contrast glyph. It showed gold when on and nothing when off; now on = default silver, off = terracotta, matching every other toggle. Screenshot-verified both states. +** DONE [#C] Off-state red inconsistent across waybar modules :bug:waybar: +CLOSED: [2026-07-02 Thu] +Terracotta red isn't applied uniformly for "off": the sleep icon is a different shade than the mouse/trackpad when off, and the dim indicator doesn't show red when off at all. Cosmetic × every time = P3. Origin: roam inbox capture 2026-07-02. + +Shipped 2026-07-02 (dotfiles 15cb93c + 7f1f334). The touchpad script's Pango-markup red predated the terracotta theme pass (#d47c59 vs the CSS's #cb6b4d) — unified on #cb6b4d. Dim-off now red (see the contrast task). Bonus find: waybar hands every pulseaudio instance the sink's .muted class, so a muted speaker also painted the mic red — scoped with :not(.mic) so each glyph keys on its own device. +** DONE [#B] Waybar volume/mic toggle like the touchpad module :feature:waybar: +CLOSED: [2026-07-02 Thu] +Make the volume/mic waybar component look and behave like the touchpad/mouse toggle. +- Move the mic to the other side of the volume so the percentage isn't in the way. The mic and speaker icons sit the same distance apart as the hand and mouse. +- One keybinding cycles the four states: volume on / mic on, volume on / mic off (red), volume off (red) / mic on, volume off (red) / mic off (red). +- Move the trackpad/mouse toggle to another keybinding (discuss an open mnemonic, e.g. =d= for disable) and assign Super+M to this module (for mute). +Origin: roam inbox capture. + +Shipped 2026-07-02 (dotfiles 7f1f334). New audio-cycle script walks the four-state ring in the exact order above (wpctl-backed, explicit set-mute so the pair can't desync, 6 tests) on Super+M; live-verified the full ring on velox. Mic moved left of the speaker and hugs it via paired margins, percentage on the outside. Touchpad toggle moved to Super+Shift+I ("input devices" — d-for-disable was taken at both levels by removemaster and dim-toggle); its tooltip and tests follow. +** DONE [#C] Timer end sends no notification :bug:waybar: +CLOSED: [2026-07-02 Thu] +The end of a wtimer timer didn't fire a desktop notification. Needs reproduction to confirm frequency; priority follows the severity-by-frequency matrix once known (a reliably-missing timer-end alert would rate higher). Origin: roam inbox capture. + +Root-caused 2026-07-02 (dotfiles ca35642). The pipeline works (a live 3s timer fired and persisted), but notify sent everything --urgency=normal and dunstrc delays normal-urgency popups while a fullscreen window has focus — a timer ending mid-video sat invisible until fullscreen exit. Alarms now go critical urgency, which rides the fullscreen_show_critical rule; verified CRITICAL in dunst history. The alarm sound (paplay, separate from dunst) was never affected. +** DONE [#C] Keybind hints in waybar module tooltips :waybar: +CLOSED: [2026-07-02 Thu] +Every module's hover tooltip should list its keyboard shortcut(s), for discoverability. Audit the modules and add the bindings to each tooltip. Origin: roam inbox capture. + +Shipped 2026-07-02 (dotfiles 4c32aec). Audited every module: arrows (Super+[ / Super+]), sysmon (Super+R), net (Super+Shift+N), layout (Super+Shift+←/→), menu (Super+Space / Super+Shift+Q, tooltip enabled), plus the already-hinted dim/caffeine/touchpad/mic/volume. Date/time tooltips document their new right-click actions. Workspaces/window/tray don't take custom tooltips; timer has no keybind. +** CANCELLED [#C] Smooth waybar expansion animation :waybar: +CLOSED: [2026-07-02 Thu] +The cluster expansion jumps instead of animating, and a few systray icons pop in one-by-one afterward, which reads as glitchy. Animate the expansion smoothly if waybar allows it — width transitions are limited, so feasibility is uncertain (hence [#C]). Origin: roam inbox capture. + +Assessed infeasible 2026-07-02: collapse works by config rewrite + SIGUSR2 reload, which rebuilds every widget — nothing survives to transition, GTK3 can't animate add/remove without Revealer (an upstream waybar change), and the tray pop-in is async StatusNotifier re-registration. Full findings + revisit conditions: [[file:docs/design/2026-07-02-waybar-expansion-animation-feasibility.org]]. +** DONE [#C] Optional label on timer/alarm/stopwatch items :feature:waybar: +CLOSED: [2026-07-02 Thu] +Let each wtimer item carry an optional short text label. The data model already supports it (=add_timer/add_alarm/add_stopwatch/add_pomodoro= all take =label=""=, and =_describe= shows =label or type=); the gap is the fuzzel-driven creation flow, which doesn't prompt for a label. Add the optional label prompt on create. Origin: roam inbox capture. + +Shipped 2026-07-02 (dotfiles ca35642): =wtimer new= gained a "label (optional)" fuzzel prompt after the type/value prompts; empty keeps the unlabeled default. 2 new tests (89 total in the suite). +** DONE [#B] Pocketbook finish-or-cancel decision :pocketbook: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-24 +:END: +Decided by Craig 2026-07-02, ahead of the scheduled checkpoint: remove pocketbook altogether. Executed same day — pip package uninstalled (user site clean), running instance killed, launcher gone, =pocketbook/= tree removed from the repo, Super+P rebound to toggle-touchpad (P for Pointers; Super+Shift+I unbound, waybar tooltip hint updated — dotfiles a750cb4). The org-capture popup remains the quick-notes surface. +** DONE [#B] Provision Eask in archsetup :tooling:eask: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-05-26 +:END: +Shipped 2026-07-02 (speedrun): npm global install block added after the nvm line — runs as $username with --prefix $HOME/.local, display/error_warn wrapped, output to $logfile, matching the claude-code block's shape. The npmrc decision went yes: dotfiles common/.npmrc pins prefix=${HOME}/.local (stowed; hand-linked live, npm config get prefix confirms ~/.local — dotfiles 01627cc). VM assertion added: ~/.local/bin/eask present + ~/.npmrc stowed. Live smoke: eask 0.12.9 on PATH. Full acceptance (fresh-install chime make setup/test) rides the next VM pass. +Add =@emacs-eask/cli= to archsetup's provisioning so fresh machines get it. Eask is installed by hand today and declared nowhere in archsetup or the dotfiles repo, yet both chime and linear-emacs depend on it (their =make setup/test/coverage= shell out to =eask=). Source: handoff from linear-emacs 2026-05-23. + +- Add a global npm install after the node block (=archsetup= ~2030, after =aur_install nvm=), modeled on the claude-code native-install block: run as =$username=, wrapped in =display=/=error_warn=, output to =$logfile=. Roughly =sudo -u "$username" bash -c 'npm install -g --prefix "$HOME/.local" @emacs-eask/cli'=. +- Pin the prefix to =~/.local= so eask lands at =~/.local/bin/eask= (already on PATH) and the install runs as the user, not root. On the current machine =npm config get prefix= returns =/usr=, so eask was installed with an explicit =--prefix=. +- Decision: also set a persistent user npm prefix (=~/.npmrc= with =prefix=${HOME}/.local=)? If yes, that =~/.npmrc= is a legitimate dotfile to stow; if no, rely on the explicit =--prefix= flag alone. =~/.eask/= is a regenerable cache — leave un-stowed. +- Acceptance: fresh run leaves =eask= on PATH at =~/.local/bin/eask= (no root); =cd ~/code/chime && make setup && make test= works. +** DONE [#C] Waybar timer dialog styling :waybar: +CLOSED: [2026-07-02 Thu] +From Craig's roam capture 2026-07-02: style the timer module dialogs like the screenshot dialog — tighter window, icons on the selections, colon+space after the prompt. + +Shipped 2026-07-02 (dotfiles 9ffcba7): dialogs size to content, type menu carries the kind glyphs, prompts end ": ". Three new tests; screenshot-verified live. +** DONE [#B] Waybar collapse jumps client windows :bug:waybar:hyprland: +CLOSED: [2026-07-02 Thu] +From Craig's roam capture 2026-07-02: collapsing/expanding (and any waybar teardown) snapped every tiled window up and back down; hold the clients still and let only the bar change. + +Shipped 2026-07-02 (dotfiles 4b1a4ec): waybar now runs exclusive:false and the new waybar-reserve script statically reserves the bar strip per monitor (wired as exec so config reloads re-apply it). Verified live: client geometry held constant through bar kill, relaunch, and a collapse round-trip. Eight new tests (script + pairing). +** DONE [#B] Bluetooth panel + bar module :feature:waybar:bluetooth: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:SPEC_ID: 1271a845-4463-4831-9902-990eda6b2265 +:END: +Spec: [[file:docs/specs/2026-07-02-bluetooth-panel-spec.org]] (IMPLEMENTED 2026-07-02 — all five phases shipped same day: engine eb2230f, panel 76b2c05, bar module e372de3, bt-priv + blueman retirement 2a026b1/d8d8c53, install wiring proven by VM assertions). Residual: the phase 4-5 VM assertions run on the next VM pass; ratio picks up the package removal + hand-links on its trip list. + +A bluetooth panel driving a CLI underneath (bluetoothctl one-shot verbs), consistent in look and feel with the net panel (GTK4 + layer-shell + Blueprint, humble-object presenter, verify-everything). Minimalistic interface, full functionality, plus a diagnostics/troubleshooting section mirroring the net panel's Diagnostics tab. Bar module glyph opens it. Craig's ask (2026-07-02): follow UX/UI best practices; where the net panel's patterns conflict with best practices, file a net-panel bug task rather than clone the flaw. + +*** 2026-07-02 Thu @ 13:30:42 -0400 Shipped phase 1 — the bt engine package (dotfiles eb2230f) +=bluetooth/src/bt/= mirrors the net engine's layout: btctl parsing boundary (show/devices/info, connect-error classifier), sysio rfkill/airplane, audio module over pw-dump/wpctl (HSP probe + A2DP switch repair with verify-after), redacted eventlog, six repair tiers, and the doctor chain (adapter → rfkill → service → powered → devices → audio profile) with safe auto-repairs behind =--fix= (never auto-connects; airplane blocks are named, not fought). 101 tests over fake binaries; 42 suites green (=make test= glob auto-discovered =tests/bt/= — gate check verified). Live read-only on velox: =bt status= + =bt doctor= read the real adapter/devices/audio graph; =~/.local/bin/bt= hand-linked (no restow under running Hyprland). Ground truth vs spec: profile inventory needs =pw-dump= (wpctl can't enumerate), and the card's =bluez5.profile= prop is unreliable — sink node's =api.bluez5.profile= is authoritative. Deferred INTO phase 2: the shared dupre css factoring (net's css is an inline string in =gui.py=, not an asset — factoring it without the bt-panel consumer just risks the working net panel). + +*** 2026-07-02 Thu @ 14:15:27 -0400 Shipped phase 2 — the GTK panel (dotfiles 76b2c05) +Cloned the net panel's shape over the phase 1 engine: GTK-free PanelModel + viewmodel (69 new tests, display-free), Blueprint pages (Devices with the adapter power row + Paired/Nearby sub-views; Diagnostics with the doctor cascade, inline fix buttons mapped from step =repair= keys, Advanced tool selector), gui.py controller (layer-shell OVERLAY 380x520 TOP+RIGHT, Esc closes, single-instance toggle, bg worker, passkey + confirm dialogs), pairing pty state machine (=bt/pairing.py=: confirm-passkey yes/no with default-deny, display-passkey, bounded deadline, tested over the fake's interactive mode), manage.py op envelopes shared by CLI and panel (cli refactored onto it; power + discoverable verbs added), =bt panel= subcommand + =bt-panel= toggle wrapper (hand-linked into =~/.local/bin=, no restow under live Hyprland). Shared dupre css factored: net's inline =_CSS= → =hyprland/.config/themes/dupre/panel.css= with =dupre-*= classes, both panels load it (stowed path first, repo-relative fallback; hand-linked =~/.config/themes/dupre/panel.css=). Super+Shift+B rebound blueman-manager → bt-panel (hyprctl reloaded, live). 43 suites green (=make test= exit 0). DEFERRED pending Zoom ending: the AT-SPI smoke (=make test-panel-bt=, written and wired) and any visual check of either panel with the factored css — both need a visible window. Gotcha for posterity: the old =test_panel_stub_exits_two= CLI test ran =bt panel= for real once cmd_panel was wired and launched the panel on the live compositor mid-meeting for ~30s before the test timeout killed it; it now asserts parser wiring only — never run =bt panel= inside =make test=. + +*** 2026-07-02 Thu @ 15:06:00 -0400 Shipped phase 3 — the bar module + blueman retirement (dotfiles e372de3) +=custom/bluetooth= over the engine: waybar-bt shim + =bt/indicator.py= (state-following glyph — slashed off/blocked/absent, plain dim idle, connected mark white; low-battery <15% adds a red pango percentage to the glyph; tooltip = connected devices with battery + Super+Shift+B hint). Signal 10; the panel pokes =pkill -RTMIN+10 -x waybar= after each status reload. Blueman retired from the Hyprland session: exec-once + both windowrules removed, applet killed live; waybar relaunched on the runtime config and the module verified on the bar (connected glyph, blueman tray icon gone). Theme drift guard caught that themes/*/waybar.css edits must mirror into the live =waybar/style.css= — all three updated. 43 suites green. ALSO closed this pass: the deferred phase 2 visual batch (bt AT-SPI smoke green after fixing its Connect/Disconnect state-following assertion — c1a8219; net smoke green on the factored css; both panels eyeballed correct in dupre). Left for phase 4: package removal (blueman out of archsetup), sxhkdrc's dwm blueman-manager binding decision rides that pass. + +*** 2026-07-02 Thu @ 15:16:51 -0400 Shipped phase 4 — bt-priv shim, blueman out, VM assertions +Dotfiles =2a026b1=: the stowed =bt-priv= shim over the phase-2 =bt.priv= module (one verb, =restart-bluetooth=; verified end-to-end against the symlinked fake-systemctl — rc 0 with =BT_SUDO= empty, rc 2 on bad verb/usage; hand-linked into =~/.local/bin=), and the sxhkd =Super+Shift+B= bind repointed from the retired blueman-manager to =st -e bluetoothctl= (the decided terminal fallback — the GTK panel is Wayland-only, and the bt CLI is hyprland-tier so dwm never gets it). 43 dotfiles suites green. archsetup: blueman dropped from the =desktop_environment= bluetooth loop (bluez + bluez-utils stay, solaar untouched); VM assertions added to =test_packages.py= (bluez/bluez-utils installed, blueman NOT installed as the retirement regression guard — collected 15, exercised on the next VM run since VM tests run committed code); =bash -n= + =py_compile= + =make test-unit= green. SUDOERS: no new rule needed, same conclusion as net-priv (2026-07-01 entry) — archsetup:1089 grants the primary user blanket =NOPASSWD: ALL=, which covers =systemctl restart bluetooth=; a narrow bt-priv rule would be dead config under the blanket grant, so phase 5's "sudoers placed" item is satisfied by the existing grant. LIVE: blueman package removed from velox (=pacman -Rns=, decision "drop it outright, both machines"); ratio needs the same + the bt-priv hand-link on its trip list. + +*** 2026-07-02 Thu @ 15:19:58 -0400 Shipped phase 5 — install-default wiring proven by VM assertions +No new install code was needed: the waybar =custom/bluetooth= module, the =Super+Shift+B= → =bt-panel= bind (hyprland.conf), and the shared =themes/dupre/panel.css= all live in the dotfiles hyprland tier, so the existing clone + =make stow hyprland= step lands them on a fresh install; sudoers is covered by the blanket grant (phase 4 conclusion). The phase's substance is the proof: =test_desktop.py= gained hyprland-gated assertions that the four bt bins (=bt=, =bt-panel=, =bt-priv=, =waybar-bt=) are stowed executable in =~/.local/bin= (either stow shape — per-file symlink or folded dir), the waybar config carries =custom/bluetooth=, hyprland.conf carries the =bt-panel= bind, and the stowed theme has =panel.css=. Collected 30 in =test_desktop.py=; exercised on the next VM run (VM tests run committed code). + +*** 2026-07-02 Thu @ 15:19:58 -0400 Test surface complete across all phases +Everything the surface named exists and is green: engine suites over fake binaries (phase 1, 101 tests — btctl parse, doctor chain, A2DP repair verify), PanelModel presenter suite (phase 2, 69 tests), pairing state-machine suite (passkey confirm / NoInputNoOutput / timeout, over the fake's interactive mode), bar-module suite (phase 3), gated AT-SPI smoke (=make test-panel-bt=, run green live), and the phase 4-5 VM assertions (=test_packages.py= bluetooth stack + blueman-absent guard; =test_desktop.py= panel wiring). 43 dotfiles suites green; VM assertions await the next VM run. +** DONE [#B] All error messages should be actionable with recovery steps +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-24 +:END: +Shipped 2026-07-02 (speedrun). Structural fix at the helper: =error_fatal= now takes an optional third recovery-hint arg and every fatal prints the last five log lines inline, the full log path, the per-site "Fix:" when given, and the resume pointer (step markers mean a re-run continues where it stopped) — so even a hint-less fatal is actionable. All 17 fatal call sites got specific hints (keyring reinit, mirrorlist switch, userdel/USERNAME conflict, base-devel for makepkg, DESKTOP_ENV values, dotfiles-dir cleanup, tmpfs sizing, aur.archlinux.org reachability). The end-of-run Error Summary now closes with the grep-the-log line and the fix-and-re-run pointer. =error_warn= already carried what-failed + exit code into the summary; unchanged. +** DONE [#B] Improve logging consistency +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-24 +:END: +Shipped 2026-07-02 (speedrun), paired with the actionable-errors task. Audit result: the install helpers (pacman_install/aur_install/retry_install/run_task/git_install/pipx) and error helpers already tee/append everything to $logfile — the gaps were direct mutations whose stderr went to the console and vanished. Swept every =sed -i= and file-write mutation lacking capture (locale.gen uncomment, pacman.conf ParallelDownloads/Color + multilib, waybar battery removal x3, wireless-regdom, geoclue BeaconDB, paccache, BRIO udev rule, fstab fmask, mkinitcpio HOOKS, sudoers append, ufw status read): each now sends stderr to $logfile, and the previously-silent ones (locale.gen, pacman.conf, multilib, waybar, regdom, geoclue, paccache, udev) gained =error_warn= handlers so failures land in the summary instead of passing silently. Verified: bash -n clean, 10 unit suites green, shellcheck warning-diff vs HEAD empty (no new findings). +** DONE [#B] Add NVIDIA preflight check for Hyprland +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-05-21 +:END: +Shipped 2026-07-02 (speedrun), TDD. =nvidia_preflight_report= is a pure sed-extractable core (same harness pattern as zig-pin): modalias scan for vendor 10DE — DRM first, PCI display-class (bc03) fallback so an NVIDIA audio function can't false-trigger — then the repo's =nvidia-utils= candidate major checked against 535. Prints the Wayland guidance + env vars (LIBVA_DRIVER_NAME, GBM_BACKEND, __GLX_VENDOR_LIBRARY_NAME, ELECTRON_OZONE_PLATFORM_HINT) and the pre-Turing/AUR-legacy note. preflight_checks aborts on <535/unknown (rc 11), prompts continue/abort on a healthy NVIDIA box (rc 10), silent on non-NVIDIA (rc 0). 9 Normal/Boundary/Error tests over fake modalias trees + a fake pacman (=tests/nvidia-preflight/=, glob-discovered by test-unit — 10 suites green). +** DONE [#C] Wlogout exit-menu buttons are rectangular, not square +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-24 +:END: +The wlogout exit menu renders its buttons taller than they are wide on velox, so the cells read as vertical rectangles instead of squares. They render square (centered) correctly on ratio, so this is a per-host / resolution difference, not a flat bug. Fix the button sizing in the wlogout style (=~/.dotfiles/hyprland/.config/wlogout/style.css=) so each cell is square on both hosts. Noticed 2026-05-21. Related: the [#D] VERIFY about wlogout sizing across displays. + +The wlogout config uses fixed pixel margins, which is the likely reason sizing differs across the two displays — adjusting them for the laptop screen is part of the fix (folded in from the former "Test wlogout menu on laptop" VERIFY, 2026-06-24). + +Add a regression test so the square-cell fix doesn't silently break on a resolution change: assert the rendered (or computed) wlogout button cells are square across ratio's and velox's resolutions. Dropped :quick: — the cross-host test pushes this past a spare-moment fix. + +Shipped 2026-07-02 (dotfiles 775771b). Keybind now calls a =wlogout-menu= wrapper computing centered margins from the focused monitor (the old fixed L/R 1200 exceeded velox's 1436 logical width). Also fixed two styling defects the geometry hid: invisible unfocused borders (now muted, so the square edge is visible) and hover/focus sharing one gold rule (lock button glowed at launch; focus is now a muted ring). Tests: unit margin-math suite across both hosts' resolutions + portrait + small + bad-geometry, CSS regression suite, and a compositor-gated =make test-wlogout= smoke that launches a no-op probe, screenshots, and measures squareness (velox: 361x361 px, PASS). Ratio's visual eyeball rides the pending ratio sync. +** DONE [#C] Net panel: error toasts auto-dismiss unread :bug:network:waybar: +CLOSED: [2026-07-02 Thu] +Fixed in dotfiles 0f017d4: viewmodel.toast_plan owns the toast policy — errors show sticky and ignore the post-op refresh's empty clear (worst case: a forget failure's error was wiped within ~2s by its own refresh), and the next real status replaces them. Successes keep the 4s fade. 7 policy tests added; 41 suites green. +** DONE [#C] Net panel: verify claimed keyboard navigation :test:network:waybar: +CLOSED: [2026-07-02 Thu] +Found during the bluetooth-panel UX pass (2026-07-02). The V2 spec claims tab-between-sections, arrow-key row navigation, and type-to-filter, but no custom keyboard code exists in the panel — arrows and type-ahead may ride GTK ListBox defaults, tab-between-sections likely doesn't. Verify each claim against the live panel (AT-SPI smoke can assert focus order); implement or strike the claims from the spec so spec and panel agree. + +*** 2026-07-02 Thu @ 13:05:00 -0400 Code-level pass done; live probe deferred (Craig in a Zoom meeting) +Code reality (dotfiles net/src/net): Esc close is wired (gui EventControllerKey); row-activated -> primary is wired for both connection lists (pages.py:122,126), so Enter-on-row rides the GTK ListBox activate binding; arrows ride ListBox defaults; NOTHING implements type-to-filter (no search/filter code exists — that claim is false as written); Tab is the plain GTK focus chain, widget by widget, not section jumps. Live AT-SPI probe plan: launch panel in test mode, drive keys via hyprctl dispatch sendshortcut targeted AT THE PANEL WINDOW (never the focused surface — wtype/ydotool absent anyway), gate every key on the panel holding focus, never send Enter on the available list (real connect risk). BLOCKED at 13:05: active window is zoom (meeting) — no test windows, no synthetic input until clear. Then: verify focus order + arrows + no-filter live, strike/reword the spec's keyboard bullet to match. + +*** 2026-07-02 Thu @ 14:57:18 -0400 Ran the live probe; spec bullet reworded to match reality +Zoom ended ~14:45; probe ran per plan (panel in test mode, hyprctl dispatch sendshortcut targeted at the panel address, every key gated on panel focus, Enter never sent). Verdicts: arrows move row focus and Enter rides the ListBox activate binding (TRUE — kept); Esc closes (TRUE — kept); Tab is the plain GTK widget-by-widget chain and inside a list crawls row by row, no section jumps (claim FALSE — struck); type-to-filter does not exist (claim FALSE — struck; typing into the 24-row Saved list filtered nothing). Spec's Keyboard bullet reworded with the live evidence and a note that section-jump Tab or filtering would be new work. Probe gotchas for reuse: AT-SPI list items have empty accessible names, so row identity needs get_index_in_parent(); a killed test panel can leave a windowless single-instance process that eats the next launch via D-Bus activation — pkill -9 -f 'net panel$' and wait before relaunching. +** CANCELLED [#C] Pocketbook development backlog :pocketbook: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-05-26 +:END: +Cancelled with the 2026-07-02 remove-pocketbook decision — the app and its in-tree package are gone. +Pocketbook (GTK4 layer-shell notes panel, toggled via waybar) was pulled from publication 2026-05-26 — github repo + cjennings.net repo deleted, mirror hook removed — and folded into this repo at =pocketbook/= until it's ready to spin back out. Src-layout Python package with pytest tests and a Makefile. Develop it in-tree; the backing modules are =store/note/panel/layer_shell/app/note_widget= + =style.css=. + +Backlog (unordered; promote items to their own dated tasks as they're picked up): + +- Configurable options, possibly a dedicated configuration panel. +- Lose-focus hides pocketbook — configurable on/off. +- Configurable display order: chronological by creation date (asc/desc), manual, alphabetical (asc/desc). +- Search / filter notes. +- Global toggle keybind (Hyprland =bind=) alongside the waybar click; document the waybar integration. +- Note CRUD polish (create/edit/delete) + optional markdown rendering. +- Pin / favorite notes. +- Tags or notebooks / categories. +- Persistence: confirm store format + =~/.local/share/pocketbook/= location, add versioning/migration, decide a backup/sync story. +- Theming: track the dupre/hudson theme system so =style.css= follows =set-theme=. +- Layer-shell geometry config (anchor edge, width, margins) + HiDPI / multi-monitor behavior — ties into [[file:docs/PLAN-per-host-overrides.org][per-host overrides]] scaling work. +- Config file format (toml) + reload-without-restart. +- Expand test coverage (TDD per testing standards; =tests/= already exists). +- Release prep for the eventual spin-back-out: pyproject metadata, version, license. +- Re-wire the archsetup install (gtk4-layer-shell dep + install step + post-install clone) when pocketbook ships. Removed 2026-05-26 — see git history of =archsetup= / =scripts/post-install.sh=. +** CANCELLED [#C] Fn+F9 toggles pocketbook — source unlocated :hyprland:pocketbook: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-23 +:END: +Retired with pocketbook itself (2026-07-02 removal) per this task's own exit condition — with the app uninstalled and unbound, whatever Fn+F9 emitted has nothing to toggle. +On velox, pressing Fn+F9 (physical function key) toggles the pocketbook panel. It shouldn't. Raised from a home-project session 2026-06-23. + +Investigated 2026-06-23 and could not locate the trigger in any config. Ruled out, three ways: +- No F9 bind (bare / $mod / keycode) in the live =hyprland.conf= (now a stow symlink), the velox host tier =conf.d/local.conf=, or the waybar config. +- =hyprctl binds= runtime (all 90 active binds, authoritative) execs pocketbook on ONLY =SUPER+P=. No F9/XF86 path reaches it. The old touchpad toggle that used to sit on =$mod+F9= was moved to =$mod+M=, so F9 is unbound in Hyprland. +- No input remapper (keyd/xremap/input-remapper) and no hotkey daemon (sxhkd/swhkd) running or configured; pocketbook's own source has no F9 / GlobalShortcuts / portal / dbus listener (its GTK ShortcutController binds only Esc/Ctrl-n/Ctrl-j/Ctrl-k/Del/Return). pocketbook is a single-instance Gtk.Application, so any path that re-runs =pocketbook= toggles it. + +Parked at Craig's call (not worth deeper investigation now). If it resurfaces, the one unfinished step is to capture what keysym Fn+F9 actually emits (=wev -f wl_keyboard:key=, press Fn+F9, read the =sym:= / =code:=) and grep for that. Most likely folds into removing pocketbook from the waybar setup — if pocketbook leaves the bar, retire this with it. +** CANCELLED [#C] Waybar emacs-service status + control :feature:waybar: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-24 +:END: +From the roam inbox (2026-06-22): with Emacs integrated into the system as file manager and instant note-taker, make bouncing it trivial. A waybar component showing the emacs service status, with detail on hover, that turns the server on / off / bounce via right-click. Pairs with running the Emacs daemon as a managed systemd user service. + +Cancelled 2026-07-02 per Craig during the task-batch pick: no current need. Re-add or pull back from Resolved if a need surfaces. +** DONE [#C] set-wallpaper detaches waypaper config from its stow symlink :bug:hyprland:quick:solo: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-28 +:END: +=set-wallpaper= persists with =mv "$tmp" "$CONFIG"=, which replaces the =~/.config/waypaper/config.ini= stow symlink with a real file. After the first run the live config is detached from =~/.dotfiles/hyprland/.config/waypaper/config.ini=, so a later =git pull= + restow won't update it and set-wallpaper changes never flow back to the repo. Fix: write in place rather than =mv= over the symlink — e.g. =cp "$tmp" "$CONFIG"= (follows the symlink to the real dotfiles file), or resolve the link target and write there. Lives in =~/.dotfiles/hyprland/.local/bin/set-wallpaper=; it has a test suite, so add a Boundary case for "CONFIG is a symlink". + +Shipped 2026-07-02 (dotfiles d826be4): write-back now redirects through the symlink instead of mv-ing over it; two boundary tests pin the invariant (replace + append paths). velox's live config was still a healthy symlink, so no repair needed. +** DONE [#B] Instrument-console rebuild: net + bluetooth panels :feature:waybar:network:bluetooth:solo: +CLOSED: [2026-07-03 Fri] +:PROPERTIES: +:SPEC_ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5 +:END: +The no-approvals speedrun build of the console design Craig approved through five prototype iterations (2026-07-02/03). Spec: [[file:docs/specs/2026-07-03-instrument-console-panels-spec.org]] — the interactive prototype [[file:docs/prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]] is the normative design reference. Folds three open tasks: network panel redesign, bt switch placement + title, bt rename devices. Code in ~/.dotfiles (net/, bluetooth/, themes/dupre/panel.css). Final step: flip the spec to IMPLEMENTED, write the findings summary to file, finalize session context. + +*** 2026-07-03 Fri @ 03:20:00 -0400 Phase 2 shipped: net GTK-free console layer + engine verbs +Dotfiles =81ec9c3= (TDD, 52 new tests, 581 net green). Pure presenter logic for the single-screen console, no view code touched: =viewmodel.net_faceplate= (state word + lamp + TUNNEL/AIRPLANE badges, wired-link-wins precedence), =network_console_rows= (ethernet pinned, radio-off note, active-then-signal sort, per-row lamp/caption/ladder/forget), =channel_headline= (wired device+speed / SSID+ladder+dBm / not-connected placeholder), =tunnel_console_rows=, dial-meter geometry (=meter_needle_deg= + =meter_scale= 100→1000 auto-relabel), =signal_bars=/=mbps_label=, and =panel.ArmState= (two-click arm-to-fire for forget/disconnect). Engine verbs: =manage.wifi_radio= (nmcli radio wifi on|off), =manage.device_up= (ethernet take-the-route), =sysio.link_speed_mbps= (/sys wired speed), =connections.ethernet_devices=, hidden flag on =manage.add=. + +*** 2026-07-03 Fri @ 06:02:32 -0400 Phases 3+4 shipped: net view rebuilt as the instrument console +Dotfiles =800ef60= (1197+/250-). =gui.py= rewritten as the single-screen console — no tabs, no Blueprint template (the dial meters and arm-to-fire rows are too dynamic, so the tree is built in Python; =pages.py= + the =*.blp/*.ui= are now orphaned, Phase 6 dead-code). Faceplate (lamp/word/TUNNEL+AIRPLANE badges/wifi-radio switch/close), engraved CHANNEL headline, scrolled NETWORKS + TUNNELS lamp rows, CONSOLE keys, two cairo dial meters, output well + dismiss ✕, toast. Interactions all wired: open network joins / secured opens the password dialog, active row arm-disconnects (gold), ✕ arm-forgets (terracotta), tunnel toggles, ethernet row takes/yields the route, radio switch flips the wifi radio (refuses under airplane with the way out), + hidden joins a non-broadcast SSID, DOCTOR streams diagnose+repair into the well, SPEED TEST sweeps both dials with the live rate then pins the final with HOLD (location/ping/final/tips in the well). =panel.css= grew the console classes (lamps+glow, b-face, engrave, chan, lamp-row + arm tints, c-btn, meter/mode/hold, output steps, toast). AT-SPI smoke + driver rewritten (anchor on the DOCTOR key). Phases 3 and 4 landed together because a view-only intermediate is a non-functional panel. Verified live on velox: full render screenshotted, console smoke green (faceplate/keys/sections/tunnels/DOCTOR/dismiss/close), DOCTOR streams real diagnose steps, SPEED TEST drove RX 36.6↓ / TX 90.7↑ then HELD. 581 net tests + full make test green. + +*** 2026-07-03 Fri @ 06:55:00 -0400 Phase 5 shipped: bt panel rebuilt as the instrument console +Bluetooth's turn, two commits mirroring net. Phase-5a (dotfiles =5318b34=, 47 new console tests): the GTK-free layer — =viewmodel.bt_faceplate= (POWERED/OFF/AIRPLANE word + lamp + LOW BATT/AIRPLANE badges), =paired_console_rows= / =nearby_console_rows= (lamp rows with connect/forget/rename affordances), =discoverable_chip=, count labels, =battery_gauges= (two dial slots, one per connected device, red under 15%, dim NO DEVICE / ADAPTER OFF empties), =STEP_NARRATION=, and =panel.ArmState= (the forget latch). Engine gaps: =btctl.set_alias= renames through the bluez D-Bus Alias via busctl (set-alias has no MAC-addressed one-shot; =device_path= discovers the controller node from the object tree), =manage.rename= wraps it with a verify-after read, =parse_info= reads the Alias as the display name (a rename lands there, not on Name; the MAC-shaped placeholder stays "unnamed"), and =doctor= grew =on_report=/=on_begin= callbacks. Phase-5b (dotfiles =66f03d9=): =gui.py= rewritten as the single-screen console — faceplate (lamp/word/LOW BATT+AIRPLANE badges/adapter-power switch/close), engraved ADAPTER line with the clickable discoverable chip, scrolled PAIRED + NEARBY lamp rows, CONSOLE keys DOCTOR / SCAN, two cairo battery dials, output well + toast. Interactions: paired rows toggle connect/disconnect, ✎ renames via a dialog, ✕ arm-forgets, nearby rows run the pair flow into a passkey-confirm dialog, the chip toggles discoverability, the switch powers the adapter, SCAN refreshes nearby, DOCTOR streams checks + repairs. =panel.css= gained =.chip= / =.pen= / =.o-passkey= (the rest already shared with net). AT-SPI smoke rewritten (anchor on the bt-only SCAN key). Verified live on velox: smoke green end to end, screenshot matches the prototype (POWERED faceplate, four paired audio devices, two NO DEVICE battery dials). 46 suites + full make test green. Phase 6 next: live both-panel verify, folded tasks closed, dead code removed, spec → IMPLEMENTED. + +*** 2026-07-03 Fri @ 06:49:45 -0400 Phase 6 shipped: build closed out, dead code removed, spec IMPLEMENTED +Live both-panel verify on velox: 46 suites + full make test green, and both AT-SPI smokes green end to end (net: faceplate NET·01/ONLINE, DOCTOR streams real diagnose steps, tunnels rows, close; bt: BT·01/POWERED, SCAN/DOCTOR keys, battery dials, close). The two =gui.py= files are byte-identical to their screenshot-verified commits (net =800ef60=, bt =66f03d9=), so the render carries over from the phase-3/4/5 screenshots — this pass touched no view code. Dead code removed (dotfiles =f4e688e=): both panels' orphaned =pages.py= + =ui/= (=*.blp/*.ui=) gone now that =gui.py= builds the tree in Python, the now-dead =make ui= Blueprint-compile target and its =.PHONY= entry dropped, and the stale =gui.py / pages.py= mention in bt =viewmodel.py= fixed; nothing imported the removed modules. Three folded tasks close with this build (network panel redesign, bt switch placement + title, bt rename devices). Build summary written to [[file:assets/2026-07-03-instrument-console-panels-build-summary.org][assets/2026-07-03-instrument-console-panels-build-summary.org]]. Spec =e73877f5= flipped DOING → IMPLEMENTED. Manual-test checklist for the real-device bt interactions filed under Manual testing and validation. +** DONE [#B] Net diagnostics: narrate every step :feature:network:solo: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-02 +:END: +Follow-on 2 (dotfiles =ebf24fe=, Craig's decision 2026-07-02, option 1 of the discussed policies): mutating tiers pre-check whether we're online before acting. dns-test/dns-override short-circuit with an "already online" step and touch nothing (live-verified on hotel wifi: 100 ms skip, DNS untouched); reset/bounce/nm-restart/resolved-restart proceed (reset has a legit online use — fresh MAC) but carry "(was online before ...)" in their evidence; the panel's repair confirm warns via the cached probe verdict (=probe.cached_online=, file read only); rfkill/dns-revert/tunnel-down already verify their own state, unchanged. 492 net tests / 45 suites / panel smoke green. + +Follow-on (dotfiles =50a7239=, Craig's ask 2026-07-02): the same requirements now cover every Advanced-dropdown action — all repair ids + portal narrate in the panel's step rows, =net repair= / =net portal= human-by-default (=--json= kept, added to portal), doctor's attempts render like checks, and mutating steps keep their next_action on pass (the fail/warn-only rule was dropping dns-override's revert pointer, portal-login's login click, and cleanup-unverified's manual revert). 479 net tests / 45 suites green; panel smoke shows narration in live rows. + +Shipped (dotfiles =7772427=): every diagnose step id carries a one-line narration (what it tests and why) in a viewmodel table, and both human renderers print every step — status, title, narration, evidence with timing, and the fix pointer on fail/warn — in =net doctor= and =net diagnose= alike. Bare =net diagnose= now prints the narrated report (was raw JSON; =--json= keeps the machine envelope; =net-fix= already used =--json= explicitly, the panel calls the engine in-process). A completeness test walks diag.py's step ids against the narration table so a new step can't land unnarrated. 470 net tests / 45 suites green; verified live on velox hotel wifi — both commands narrate the full probe sequence. Ratio picks it up with its queued dotfiles pull (source-imported, no restow-only step). + +Original ask (roam inbox, 2026-07-02, from a real net doctor run on hotel wifi): the output isn't enough to know what's being tested, why, and whether it passed — a failing run printed only the verdict, the fix pointer, and the failing rows: + +#+begin_example +% net doctor +net doctor: fixable + DNS not resolving + -> net repair dns-test + diagnose: + fail: DNS resolution — no resolution (portal may be stalling DNS) + fail: Internet — link up but no clean internet (DNS or egress issue) +#+end_example +** DONE [#B] Network panel: identify tunnel backends + richer connection info :feature:waybar:network: +CLOSED: [2026-07-02 Thu] +Shipped (dotfiles =405235f=). Identification: every Tunnels row's caption now leads with its backend — "tailscale", "WireGuard (NetworkManager)", "openvpn (NetworkManager)" (from the profile's =vpn.service-type=, resolved on the panel path only), "Proton VPN CLI" — via =viewmodel.tunnel_kind_label=. Found and fixed a real gap: NM vpn-type profiles (openvpn etc.) weren't listed at all, only wireguard type. Active tunnels now carry their device's IP4 address. Info page: the active connection's live subtitle gains IP, gateway, and DNS via =build_status(full=True)= (panel poll only — the bar's one-nmcli hot path is untouched). Live-verified on velox: all 9 tunnel rows correctly labeled (tailscale w/ tailnet + peers, 7 WireGuard NM profiles, Proton CLI), live subtitle shows IP/gw/DNS on hotel wifi. 523 net tests / 45 suites / panel smoke green. + +Craig's ask (roam inbox, 2026-07-02): the Tunnels rows all look alike — no way to tell which is tailscale, which is an NM wireguard/openvpn profile, and which is proton CLI without prior knowledge (e.g. when you want to bounce tailscale specifically). Second half: improve the stats under each connection — the panel is effectively a connection's info page. +** DONE [#B] Timer: alarm am/pm input silently fails :bug:waybar:solo: +CLOSED: [2026-07-02 Thu] +Fixed (dotfiles =8dd36c4=). Two root causes: =parse_alarm= only accepted 24h =HH:MM=, and =cmd_new= suppressed the ValueError, so any 12h input silently created nothing. Now accepts 24h (=14:30=, bare =14=) and all common 12h shapes (=2:30pm=, =2:30 PM=, =7:15p=, =7p=; any case, optional space, bare a/p; 12am = midnight), and input that still doesn't parse fires a fail notification instead of vanishing. 107 wtimer tests green (10 new parse cases + notify-on-error CLI tests). Manual test filed (live dialog run). + +Craig's report (roam inbox, 2026-07-02): when setting an alarm, entering am or pm in any fashion makes the timer silently fail. It should accept 24h and 12h variants — capitalization, spaces, bare "a"/"p" — all common forms. +** DONE [#B] Timer: escape doesn't cancel the dialog flow :bug:waybar:solo: +CLOSED: [2026-07-02 Thu] +Fixed (dotfiles =8dd36c4=). Root cause: =_fuzzel= ignored fuzzel's exit code, so Escape (fuzzel exits 2 on a dmenu abort — confirmed in its changelog) returned "" and the flow fed it onward to the next prompt. =_fuzzel= now returns None on any non-zero exit and =cmd_new= aborts the whole flow on None at any step (type, duration/alarm, label). Escape-at-each-step covered by CLI tests against a fake fuzzel exiting 2. Manual test filed (real keyboard Escape). + +Craig's report (roam inbox, 2026-07-02): hitting cancel via escape at the step after choosing "timer" does nothing but proceed to the next step — likely the same for the other dialog steps. +** DONE [#B] Network panel: stream speedtest results live :feature:waybar:network:solo: +CLOSED: [2026-07-02 Thu] +FIX-UP (dotfiles =60707be=, 2026-07-03, caught by Craig): the first shipped version didn't actually stream — speedtest-go buffers all phase lines to process exit when piped (per-line arrival timestamps proved it: 25s of silence, then everything at once; the original "live" verification never checked arrival timing). The stream now runs the binary under a pty, where terminal mode redraws continuously: in-flight rates tick (download climbing like a speedometer), ANSI/spinner noise is stripped, and on_update fires per changed value. CLI closes with a "final:" settled-numbers line. Re-verified with timestamps (server +1s, ping +2s, download first tick +4s, upload +19s, final +29s) AND an AT-SPI probe of the live panel that sampled the results box mid-run: ping filled at 4s, download ticking at 12s, upload at 24s, final rows + conditioned tips at the end. 529 net tests / 45 suites green. + +Shipped (dotfiles =38171e8=). =run_speedtest_stream= runs speedtest-go's plain mode, whose lines land one per completed phase (parser written against a real captured hotel-wifi run). Panel: a checklist fills in as ping → download → upload arrive, final rows at the end. =net speedtest= streams the same lines at the terminal (=--json= keeps the one-shot envelope). Bonus from the text mode: jitter (rides the Ping row) and packet loss (own row, warns >1%) — the JSON mode never reported either. The static Tip is gone; =speedtest_tips= derives guidance from the numbers (high ping >100ms, download < half of upload, <10 Mbps both ways, loss >1%), each tip naming its trigger values — that's the answer to Craig's "what criteria" question: the old tip had none, the new ones are stated rules. 509 net tests / 45 suites green; live CLI run streamed correctly and fired the asymmetric-download tip on real numbers (33 down / 76 up). Manual test filed for the in-panel run. + +Craig's ask (roam inbox, 2026-07-02): the speedtest only shows results at the end; typical speedtest UIs report the numbers as they come in. Stream the CLI's progress into the results box as it arrives, then the final numbers at the end. Screenshot: ~/pictures/screenshots/2026-07-02_225441.png. +** DONE [#C] Bluetooth bar icon: gray instead of the bar's white :bug:waybar:bluetooth:solo: +CLOSED: [2026-07-03 Fri] +Fixed (dotfiles =27d8eda=). Root cause: the =on= state sat in the css dim group with off/absent/degraded (a deliberate "idle dims" choice that read as broken). Removed =.on= from the dim rule in all three css copies (dupre, hudson, live style.css — the theme-drift guard suite pins them together); indicator docstring updated. Live-verified: SIGUSR2 css reload, bar screenshot shows the bt glyph in the bar's resting white alongside battery/text. + +Craig's report (roam inbox, 2026-07-03): the bluetooth waybar icon renders gray, not the same white as the other bar module icons. +** DONE [#B] Bluetooth panel: close button like the net panel :feature:waybar:bluetooth:solo: +CLOSED: [2026-07-03 Fri] +Shipped (dotfiles =42c93d6=): a flat circular Close button right of the tab switcher (accessible "Close" label, "Close (Esc)" tooltip), wired to window.close. The bt smoke asserts it exists AND that clicking it exits the panel (run green live). Plot twist answered in-session: the net panel had no close button either — Craig's leaner-chrome pass removed it 2026-07-01 (787b475) on the Esc-suffices theory; he asked where it went, so it was restored with the same tab-row button (=6a0aff7=, net smoke extended the same way). Both panels match again. + +Craig's ask (roam inbox, 2026-07-03): the bt panel needs a close button matching the network panel's. +** DONE [#B] Bluetooth panel: switch placement + panel title :feature:waybar:bluetooth:solo: +CLOSED: [2026-07-03 Fri] +Delivered by the instrument-console rebuild (spec e73877f5, phase 5). The adapter-power switch now sits on the faceplate above every console key, and the engraved ADAPTER line is the panel's title row with the clickable discoverable chip right-justified on it. + +Craig's ask (roam inbox, 2026-07-02): move the bluetooth on/off switch above all the buttons. "Bluetooth" becomes the panel's title, with the on/off switch right-justified on that title row. Panel code in ~/.dotfiles =bluetooth/= (GTK4 + Blueprint, phase-2 PanelModel/presenter — see the shipped panel task in Resolved). Presenter tests + the AT-SPI smoke likely need their layout assertions updated. +** DONE [#B] Bluetooth panel: rename devices :feature:waybar:bluetooth:solo: +CLOSED: [2026-07-03 Fri] +Delivered by the instrument-console rebuild (spec e73877f5, phase 5). =btctl.set_alias= renames through the bluez D-Bus Alias via busctl (no MAC-addressed one-shot exists; =device_path= finds the controller node from the object tree), =manage.rename= wraps it with a verify-after read, and =parse_info= reads the Alias as the display name. Each paired row carries a ✎ affordance opening a rename dialog. Live-probed the mechanism on velox before wiring it (rename + restore verified on the M650). + +Craig's ask (roam inbox, 2026-07-02): the panel should be able to rename a device. bluez supports per-device aliases (=bluetoothctl= device menu =set-alias=; the one-shot invocation shape needs verifying at the btctl boundary). Wire it through the engine (=bluetooth/src/bt/=) with a verify-after read, and surface a rename affordance on the device row consistent with the panel's existing patterns. +** DONE [#B] Network panel: other network interfaces (tailscale, VPNs, wireguard) :feature:waybar:network: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:SPEC_ID: 79a1075a-4b56-4f25-a861-b69f120a636a +:END: +Spec: [[file:docs/specs/2026-07-02-net-panel-other-interfaces-spec.org]] (DOING — reviewed READY and decomposed 2026-07-02 evening; all four decisions were resolved same morning, claims re-verified live at review: protonvpn binary, tailscale JSON shape, seven importable wireguard configs). + +Tunnels visible and controllable in the net panel: tailscale + NM wireguard + proton-vpn-cli probes, a Tunnels group in Connections, diagnose/doctor route-ownership awareness, a bar badge when a tunnel owns the default route, archsetup operator flag + package swap, and the one-time NM import of the seven Proton configs. Origin: roam inbox capture 2026-07-02. + +*** 2026-07-02 Thu @ 18:47:05 -0400 Shipped phase 1 — overlay probes (dotfiles 2d9d060) +=net/src/net/overlays.py=: one probe per backend, shared row shape ={kind, name, state, addr, detail, can_toggle}=. tailscale parses =status --json= (up/down/needs-login/stopped, tailnet + N/M peers online + exit node detail, first TailscaleIP); wireguard rows filter =nmcli connection show= by type with uuids for the existing up/down wrappers; proton drives the official CLI — ground truth sampled live before writing the parser: the GUI-running refusal prints to stdout and EXITS 0 (text-detected, =can_toggle false=), disconnected = "Status: Disconnected", and the CLI's account store is separate from the GTK app's (=protonvpn info= → Account 'None' — sign-in is a phase 6 migration step for Craig). =net status= gained a fast-path overlays section (tailscale + wireguard only; the python CLI's ~300ms startup stays out of the indicator poll, and an active proton tunnel surfaces as its NM wireguard row anyway), guarded so a probe crash yields =[]= not a dead indicator. 19 new tests over fake-tailscale/fake-protonvpn/fake-nmcli (45 suites green); live check on velox: tailscale row up, 5/6 peers, hot path 149ms. proton-vpn-cli 1.0.1 installed on velox (GTK app stays until phase 5). + +*** 2026-07-02 Thu @ 19:02:45 -0400 Shipped phase 2 — panel Tunnels sub-view (dotfiles 21db05a) +Connections gained a third sub-view (Available | Saved | Tunnels — a StackSwitcher page, the natural landing for the spec's "fourth group" in this UI): rows from =overlays.collect(fast=False)= with the vpn glyph, name, and a =tunnel_caption= (state · addr · backend detail); one primary button follows the selected row via =PanelModel.tunnel_primary()= — Bring Up/Bring Down when toggleable, disabled explainers for needs-login ("Sign in first: tailscale up") and the Proton GUI-running case. =manage.tunnel_up/down= dispatch by kind (wireguard rides the existing nmcli up envelope + =connection down=; tailscale/protonvpn shell their tools into a =_tool_result= envelope carrying stderr on failure); ops run on the worker thread, rows + bar reload on land. gui grew =refresh_tunnels()= (bg, full probe set) kicked from the list load. AT-SPI smoke extended (Tunnels tab, action button, rows — POLLING for the bg load; a fixed sleep raced it and false-failed). 22 new tests (45 suites green). LIVE on velox: smoke fully green, rows eyeballed in dupre (tailscale up caption with peers count; proton app-running row), =tailscale set --operator=cjennings= applied and the user-mode =tailscale down/up= round-trip verified (Self.Online back true). Gotcha reconfirmed: stray test panels leave a windowless single-instance process — =pkill -9 -f '[n]et panel'= + wait before relaunch. + +*** 2026-07-02 Thu @ 19:11:47 -0400 Shipped phase 3 — diagnose/doctor tunnel awareness (dotfiles 31ba056) +=overlays.default_route_owner()= classifies the default route's owner (tailscale prefix, wg/pvpn/proton/tun/tap prefixes, else the active NM connection's type — imports can name a wireguard device anything). diag's route step went three-way: overlay owner = informational pass row ("internet flows through the tailscale tunnel tailscale0"), other physical link = the old multi-homing warn. When the HTTP probe fails while a tunnel owns the route, a new "tunnel" edge row LEADS the evidence and the classifier returns fixable/action tunnel-down (the deferred-vpn verdict is retired — it was look-don't-touch, and it never caught tailscale at all since NM lists it unmanaged; an NM VPN that doesn't own the route now falls through to normal classification instead of being blamed). =repair_tunnel_down= dispatches by owner (tailscale CLI / protonvpn CLI for pvpn-named devs / nmcli connection down via active-connection lookup), verifies route ownership actually moved, and registered in ACTIONS so Get Me Online drives it. fake-ip gained FAKE_IP_DEFAULT_DEV_SEQ (head-first line consume, the UP_RC_SEQ idiom) so tests watch the owner change across the verify. 11 new tests, 2 old deferred-vpn pins rewritten to the new contract; 45 suites green; live read-only diagnose on velox clean (wlan owns the route — no tunnel rows, as designed). + +*** 2026-07-02 Thu @ 19:14:58 -0400 Shipped phase 4 — bar tunnel badge (dotfiles b4010bf) +=net status= carries =tunnel_route= ({dev, kind} via =overlays.default_route_owner=, exception-guarded like the overlays list, present on the no-device path too). The indicator appends a small nf-md-vpn badge after the state glyph, emits =["<state>", "tunnel"]= as a waybar class list (string class unchanged when no tunnel), and the tooltip names the owner ("Tunnel: default route via tailscale0 (tailscale)"). No css edit — presence is the signal, themes can hook the class later, and the waybar/style.css drift test stays untouched. 4 new tests; StatusHarness gained fake-ip so the machine's real route can't leak into assertions (462 net tests, 45 suites green). Live payload on velox verified badge-free (wlp170s0 owns the route — correct); a badge render awaits the first real tunnel-owned route (phase 6's wg import or a tailscale exit node). + +*** 2026-07-02 Thu @ 21:56:00 -0400 Shipped phase 5 — installer proton CLI swap + tailscale operator (archsetup 0389790); GTK app retired live on velox +The feat commit landed at 19:16 (the session died before this close-out): installer enables tailscaled with =--now= and grants =tailscale set --operator= to the primary user (brief retry while the daemon's socket comes up), proton-vpn-cli replaces proton-vpn-gtk-app, VM asserts the vpn stack + the retirement + the OperatorUser pref (format verified against a live daemon). Live velox application finished 21:55: the =protonvpn-app --start-minimized= exec-once removed (dotfiles b5c8442 — nothing replaces it, the CLI is on-demand from the panel), the running app killed, =pacman -Rns proton-vpn-gtk-app= (proton-vpn-daemon stays — separate package the CLI uses). CLI verified unblocked: =protonvpn status= → "Status: Disconnected", =protonvpn info= → Account 'None' (sign-in is Craig's step, filed under Manual testing and validation). + +*** 2026-07-02 Thu @ 21:57:00 -0400 Shipped phase 6 — wireguard import script + velox migration (scripts/import-wireguard-configs.sh) +The script stages each config through a =wgpvpn.conf= temp copy (NM's import name must be a valid <=15-char interface name; several config names are longer), renames by the UUID parsed from the import output (never by the transient name, so a stray same-named connection can't be hit), forces =autoconnect no= (full-tunnel AllowedIPs 0.0.0.0/0 must not arm itself at boot), skips already-imported names, and refuses to run past a stale =wgpvpn= connection (an earlier run that died between import and rename — it still has autoconnect on). =tests/import-wireguard-configs/=: 10 cases over a fake nmcli; writing them caught a real bug (under =set -e= the grep-for-UUID pipeline aborted before the error message printed). shellcheck clean; 11 unit suites green. Velox migration verified: the crashed session had already run the import, so tonight's run exercised the skip path live — all 7 connections confirmed wireguard type, autoconnect no, iface wgpvpn, no stale leftovers; =net status= overlays show tailscale + all 7 rows. Ratio runs the script on its trip (rides the archsetup pull). + +*** 2026-07-02 Thu @ 21:58:00 -0400 Test surface complete across the phases +Probe suites over fake tailscale/nmcli/protonvpn (19, phase 1), panel-model Tunnels coverage (22, phase 2), diag overlay-ownership cases (11, phase 3), badge suite (4, phase 4) — all in dotfiles; VM assertions for phase 5 in archsetup 0389790; the import-script suite (10, phase 6) closes the set. +** CANCELLED [#B] File-manager swallow pattern :feature:hyprland: +CLOSED: [2026-07-02 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-02 +:END: +Reassigned to .emacs.d 2026-07-02 (handoff: =~/.emacs.d/inbox/2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff.org=). The "file manager" is the dirvish popup (Super+F, an Emacs frame), not nautilus — so the fix is elisp in dirvish's external-open path (=cj/xdg-open=): spawn the handler directly with =start-process=, hide the popup frame, restore it from the process sentinel, notify on non-zero exit. The spec drafted here first ([[file:docs/specs/2026-07-02-file-manager-swallow-spec.org]], now CANCELLED) records the feasibility finding that stays useful: gio/xdg-open launches double-fork, so no PID-ancestry approach (Hyprland native swallow included) can ever connect viewer to launcher. + +When the file manager launches another app, it should hide to a special workspace (the "swallow" pattern) and return when that process ends, rather than vanishing. Today it disappears with no signal of whether it's coming back, so the user can't tell success from failure — they should quit explicitly instead. Origin: roam inbox capture. + +*** 2026-07-02 Thu @ 22:20:00 -0400 Feasibility ground truth: Hyprland native swallow ruled out +=misc:enable_swallow= would be the whole feature in two config lines, but it matches by PID ancestry, and nautilus's launch path (GLib =g_app_info_launch_default_for_uri=) orphans the handler — reproduced live on velox with a python-gi launcher: feh came up with PPID 1 while the launcher was still running. The spec's design is therefore an event-listener daemon (socket2 =openwindow=/=closewindow= while nautilus is active), the touchpad-auto shape. Handlers sampled: pdf → zathura, image → feh (X11 — flagged as a side task), video → mpv, text → emacsclient (exempt candidate, decision 2). +** DONE [#C] Open meeting links in the browser instead of the Zoom app :feature: +CLOSED: [2026-07-02 Thu] +Shipped 2026-07-02, mechanism per Craig ("the Linux zoom app is really terrible — one less dependency"): a =zoommtg://= URL handler, and the native app retired outright. =zoom-web= (dotfiles 187414a, 10 tests) registers as the xdg default for x-scheme-handler/zoommtg via =zoom-web.desktop=; Zoom's launch-page bounce rewrites deterministically to =https://<host>/wc/join/<confno>?pwd=…= in the default browser (subdomain hosts preserved, tracking params dropped, start action mapped, malformed URIs notify + exit 2). The registration landed in the stowed mimeapps.list, so it ships with dotfiles. Zoom uninstalled from velox (=pacman -Rns=), its windowrules removed from hyprland.conf, =aur_install zoom= dropped from archsetup, and the VM retired-package assertion now covers blueman + zoom. Known limit, accepted: a host who disabled join-from-browser blocks the web client — that meeting needs the native app installed ad hoc. Ratio trip: =pacman -Rns zoom= + the pull brings the handler; run =xdg-mime default zoom-web.desktop x-scheme-handler/zoommtg= if the stowed mimeapps.list doesn't take effect. +** DONE [#B] Network panel redesign — no terminals, verify-everything, full failure coverage :feature:waybar:network: +CLOSED: [2026-07-03 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-02 +:END: +Delivered by the instrument-console rebuild (spec e73877f5). The three locked decisions all landed: no terminals (the single-screen console renders every action and result in the output well — net-popup is gone), the passwordless privileged path (the net-priv helper + narrow NOPASSWD sudoers, shipped earlier and carried forward), and verify-every-action (arm-to-fire mutations plus doctor's re-probe). The failure-mode catalog below is the diagnose/repair contract, built out across the net-diagnostics tasks and this rebuild's DOCTOR path; the catalog stays here as the standing completeness reference for that path. + +Major evolution of the shipped =custom/net= module ([[file:docs/design/2026-06-29-waybar-network-module-spec.org]]). +Reverses the spec's "privileged tiers run in a net-popup terminal" decision. Origin: +design conversation 2026-06-30. + +*** Locked decisions +- *No terminals anywhere in the module.* Delete =net-popup= entirely. Every action and + every result renders in the panel. +- *Passwordless privileged path (the enabler).* A single root-owned helper runs net's + specific privileged commands (rfkill unblock, nmcli modify/up, networking off/on, + systemctl restart NetworkManager/systemd-resolved, resolvectl dns/revert, DoT toggle), + installed by archsetup with a narrow NOPASSWD sudoers rule scoped to that helper only + (never blanket mv/systemctl). =repair.py= calls =sudo <helper> <verb>=. This supersedes + and absorbs the earlier [#C] "Passwordless DoT toggle" follow-up. Without it an in-panel + worker thread can't prompt for a password, so this gates the whole no-terminal goal. +- *Verify every action.* Every mutating op confirms its effect before reporting success + (doctor already re-probes; generalize so each repair, connect, forget, add, and DNS + override re-checks and surfaces pass/fail in the panel). +- *Detect + respond to every failure mode below* (auto-fix where we can, else report the + helpful text), including the edge cases. + +*** Navigation (confirmed) +- Top tabs: =Connections= | =Diagnostics= | =Performance=. +- Connections: saved + in-range list, connect / add / forget. +- Diagnostics: sub-row =Diagnose= | =Get Me Online= | =Advanced=; shared area below shows + diagnose items AND streams repair progress (replacing the terminal). =Advanced= reveals + the individual repair buttons, renamed with tooltips describing each. +- Performance: Speedtest (+ live throughput later). + +*** Failure-mode catalog — detect / correct-or-report (the completeness backbone) +Organized by the connectivity stack, bottom-up. "Fix" = auto-correct + verify; "Report" = +the in-panel text when there's no safe auto-fix. Audit this list for completeness; it is the +contract for what diagnose must detect and what the panel must say. + +**** Radio / hardware +- rfkill soft block — Detect: rfkill soft. Fix: unblock + =nmcli radio wifi on=, verify radio unblocked. +- rfkill hard block — Detect: rfkill hard. Report: "WiFi is off at the hardware switch — flip the physical switch or Fn key." +- No WiFi adapter present — Detect: no wifi device in nmcli + rfkill absent. Report: "No WiFi adapter detected — use ethernet, or check the driver (dmesg | grep firmware)." +- Driver/firmware not loaded — Detect: device present but errored / no operational state. Report: "WiFi driver or firmware didn't load — check dmesg for the adapter." +- USB WiFi adapter unplugged — Detect: device disappeared since last scan. Report: "WiFi adapter was removed — reconnect it." +- Airplane mode on — Detect: airplane state file set. Fix: offer toggle off (Super+Shift+A), verify radios back. + +**** Association (L2 link) +- Not connected / disconnected — Detect: link down, device disconnected. Fix: reset (reconnect saved), verify link up. +- Stuck "connecting" — Detect: device state connecting > budget. Fix: reset, verify; if it persists Report: "Stuck connecting to <ssid> — the AP may be rejecting us." +- Weak signal / high loss — Detect: associated but signal below threshold (dBm) or heavy packet loss. Report: "Signal is weak (<dBm>) — move closer to the access point." +- Saved network not in range — Detect: profile active target not in scan. Report: "<ssid> isn't in range here." +- AP roaming flap — Detect: BSSID bouncing. Report: "Connection is unstable — switching between access points." + +**** Authentication +- Wrong WPA password / missing secret — Detect: NM state 120 (snapshot; live detection is a known limit). Report + in-panel re-enter: "Saved password for <ssid> was rejected — re-enter it." +- Enterprise / 802.1X cert or identity failure — Detect: 802.1X profile + activation failure. Report: "Enterprise auth failed — check the certificate or identity (edit the profile)." +- Randomized MAC rejected by AP — Detect: reset-with-random-MAC fails where a prior connect worked. Fix: retry reset with the permanent MAC, verify; else Report. +- WPA3/SAE incompatibility — Detect: SAE key-mgmt + association failure. Report: "This network needs WPA3 and the adapter or profile may not support it." + +**** IP / DHCP +- No IPv4 lease (DHCP timeout) — Detect: connected, no IP4.ADDRESS. Fix: reset → bounce, verify lease. +- APIPA / link-local only (169.254.x) — Detect: only a link-local IPv4. Fix: reset/bounce, verify real lease; else Report: "DHCP server didn't answer — switch network." +- IPv6-only network (no IPv4 by design) — Detect: no IPv4 but IPv6 address + online via v6. Report (not a failure): "Online over IPv6 (no IPv4 here)." Requires making diagnose IPv6-aware. +- IP but no gateway — Detect: IP4.ADDRESS present, IP4.GATEWAY empty. Fix: bounce, verify gateway; else Report. +- Duplicate IP / ARP conflict — Detect: kernel ARP-conflict signal. Report: "Another device is using our IP address — reconnect to get a new lease." (edge) + +**** Gateway (L3 local) +- Gateway unreachable — Detect: no route out, gateway no ICMP. Fix: try one bounce (renew route), verify online; else Report: "No route to the gateway — switch network." (closes the spec/code gap where bounce was never tried) + +**** DNS +- No resolver configured — Detect: IP4.DNS empty. Fix: bounce to re-pull DHCP DNS, verify; else Report. +- Venue DNS broken, public DNS works — Detect: name fails to resolve but 1.1.1.1 resolves (dns-test). Fix: set a PERSISTENT resolver override (1.1.1.1 / 9.9.9.9), verify resolution + online, offer revert. (closes gap #1 — today dns-test reverts and misreports as upstream.) +- DNS hijack (resolves to gateway / private IP) — Detect: classify_resolution hijack. Treat as captive → portal-login flow. +- DNSSEC validation failure — Detect: resolution fails with SERVFAIL where public resolver succeeds without DNSSEC. Report: "DNS security checks are failing on this network." (edge) +- Encrypted DNS (DoT/DoH) hiding the portal — Detect: captive suspected + DoT on. Fix: portal-login drops DoT, opens portal, auto-restores. (existing) + +**** Egress / internet +- Upstream / AP outage (no uplink) — Detect: link/IP/DNS fine, http-probe fail, not a redirect. Report: "This network has no internet — switch network or contact the venue." +- Captive portal (redirect) — Detect: probe redirected. Fix: portal-login opens the page; verify online after login. +- Captive blocked pre-auth (no portal URL) — Detect: probe blocked, no URL. Fix: fresh MAC + open trigger; verify. +- Proxy-required network — Detect: probe fails but a PAC/proxy is advertised (WPAD/env). Report: "This network requires a proxy — configure it in settings." (edge) +- MTU / MSS blackhole (PMTUD broken) — Detect: small probe ok, large transfer hangs. Fix: lower the interface MTU, verify; else Report. (edge) +- Clock skew breaking TLS — Detect: HTTPS/portal fails with cert-time errors + system clock far off. Fix: trigger a time sync, verify; else Report: "System clock is wrong — fix the date/time." (edge) + +**** Routing / multi-homing +- VPN owns the route, no internet through it — Detect: VPN device connected + http-probe fail. Report: "Internet is routed through a VPN (<dev>) — check the VPN, not WiFi." +- VPN up but dead — Detect: VPN device up, no traffic/handshake. Report: "The VPN is connected but not passing traffic." (Phase 5 territory) +- WiFi + tether/ethernet both active — Detect: which iface owns the default route + whether the system is online by any path. Report: "You're online through <other iface>; WiFi itself has no internet," or let the user pick. (closes gap #4) + +**** Infrastructure / system +- Wedged NetworkManager — Detect: nmcli fails / API unresponsive. Fix: restart NetworkManager (bounce escalation), verify. +- NetworkManager not running — Detect: service inactive. Fix: start it, verify; else Report. +- systemd-resolved down — Detect: resolved inactive / DNS via it fails. Fix: restart, verify. +- resolv.conf not resolved-managed — Detect: /etc/resolv.conf not the resolved stub. Report: "DNS isn't managed by systemd-resolved — manual resolv.conf in play." (edge) + +**** Tooling / environment +- nmcli / NM API unavailable — Detect: nmcli error or timeout. Report: "Can't reach NetworkManager — is it installed and running?" +- Slow / hung tool — Detect: step exceeds budget. Fix: degrade that step, retry within budget. +- Stale / corrupt cache — Detect: schema/age mismatch. Fix: self-heal (atomic write + invalidation). +- Missing speedtest backend — Detect: speedtest-go absent. Report: "Install speedtest-go to run a speed test." +- Privileged op fails (helper missing / sudo declined) — Detect: helper exits non-zero or absent. Report: "Couldn't get admin rights for this repair — <install/fix the helper>." + +*** 2026-07-01 Wed @ 13:02 -0400 net-priv helper landed (V2.1) +Craig's call: stowed (not root-owned), low security on locked-down single-user machines. +Shipped =net.priv= module + stowed =net-priv= bin (dotfiles =00aac1e=): a fixed 12-verb set +(rfkill/radio/mac-random/conn-up/net-off/net-on/restart-nm/dns-set/dns-revert/restart-resolved/ +dot-disable/dot-enable) with per-arg validation (uuid/iface/ipv4/resolved.conf.d-path, injection +rejected). =repair.py= now routes every privileged op through =priv.run(verb)= in-process instead +of scattered inline sudo — which also fixes the detached DoT-restore watcher (runs privileged ops +with no tty) and closes the gap where rfkill repair ran unprivileged. 244 net + 33 dotfiles suites +green. NO new sudoers needed: archsetup already grants =%<user> ALL=(ALL) NOPASSWD: ALL= +(archsetup:1089), so every build's primary user already runs net-priv's commands passwordless; +"replicate in archsetup" is already satisfied. net-priv rides =make stow hyprland=; hand-linked on +velox. The velox DoT-path reconcile (whether velox should run DoT at all) stays open — folded into +the deeper reconcile, low priority since the guard makes it a no-op. +*** 2026-07-01 Wed @ 14:05:47 -0400 Shipped V2.2 — merged Diagnostics panel + nav restructure, no terminals +Built the V2 panel (dotfiles =75ed825=, pushed): three top tabs Connections | +Diagnostics | Performance; Diagnostics merges the old Diagnose + Repair pages into a +sub-row (Diagnose | Get me online | Advanced) over a shared area that shows diagnose +rows AND streams repair progress in-panel. net-popup deleted entirely; repairs run on +a worker thread through net-priv (no tty). doctor grew an =on_step= callback so Get me +online streams each escalation step live. Connections groups Saved / Available now / +Wired with a golden group header and joins from a row (=join_plan= auth matrix + +=manage.join= one-step connect, secret to NM only); the Add modal became the hidden-network +affordance. Every diagnose/repair/speed run offers a Copy/Open redacted report +(=report.py=, MAC/IP scrubbed). Waybar visual contract applied (dark capsule, golden +border, monospace) via a CssProvider. =net-fix= opens the panel on Diagnostics instead of +a terminal; middle-click runs =net portal= directly. TDD: 34 new GTK-free tests (grouping, +join_plan, join, report, on_step, eventlog.tail); 278 net + 33 dotfiles suites green. +Live-verified: AT-SPI panel_smoke passes end-to-end + screenshots confirm both pages and +the visual contract. DAILY-DRIVER: waybar config + net-fix are stow symlinks (live on +disk); ratio needs =git pull= + waybar restart; velox waybar picks up on next restart. +**** 2026-06-30 Tue @ 17:36 -0400 Dispositioned the 4th-review findings into the spec +Codex's 9 fourth-review findings (8 accept, 1 modify) are folded into the spec's +"V2 panel UX — the target design" section (cookie [40/40]): single nav target, +saved-vs-available groups, join-from-row instead of Add, the auth-class join matrix, +progressive loading, future-tense + verified Forget, a findable redacted diagnostics +report, the Waybar visual contract, and a lightweight inline latency probe (full speed +test stays under Performance per decision 19). The V2 build below implements that +design: [[file:docs/design/2026-06-29-waybar-network-module-spec.org::*V2 panel UX][V2 panel UX]]. +*** 2026-07-01 Wed @ 22:01:38 -0400 Made diagnose IPv6-aware and multi-homing-aware (dotfiles c0d48e2) +IPv6-only networks pass the DHCP step ("IPv6 only: <addr>") with the v6 gateway standing in for the ping; a bare fe80:: doesn't count. A new route step fires only under multi-homing and names the interface that owns the default route (tether/ethernet/VPN). Also landed the adjacent IP-layer detects: APIPA 169.254 fails DHCP with a link-local explanation, address-without-gateway fails the gateway step as a bad DHCP answer, and a weak wifi signal (below fair) warns on the link step with the dBm. fake-nmcli grew IP6.* and a fake ip(8) serves the JSON route reads. TDD, 33 suites green. + +*** TODO Close every detect/correct gap in the catalog, with post-action verification +**** 2026-07-01 Wed @ 22:41:51 -0400 Closed the feasible edge rows (dotfiles d096b30, 241744b, fafefb6) +Three grouped commits, all TDD. Services/radio: dead NetworkManager and dead systemd-resolved get their own diagnose steps and verified restart repairs (resolved only when resolv.conf is resolved-managed; hand-managed DNS gets a heads-up row), airplane mode fails the link by name and classifies needs-user-action ahead of rfkill, and a missing WiFi adapter is named with the dmesg pointer. Association/auth: reset retries once with the permanent MAC when the randomized one is rejected (new mac-permanent net-priv verb), SAE/WPA3 activation failures classify sae-incompat, and stuck-connecting classifies fixable/reset. Egress edges (run only on an existing failure): DNSSEC validation failure named via resolvectl, clock skew off the probe's Date header, MTU/PMTUD blackhole via df-bit pings, and proxy detection (env vars or an advertised WPAD name). Deferred as infeasible without state the engine doesn't keep: AP roaming flap (needs BSSID history), duplicate-IP/ARP conflict (needs the kernel log), and the USB-unplug transition (its end state is the no-adapter row). Still open here: generalized post-action verification for connect/forget/add. + +**** 2026-07-01 Wed @ 22:01:38 -0400 Closed the two named correct gaps (dotfiles 7819f58) +Gateway unreachable now earns one bounce before the upstream verdict (classifier returns fixable/bounce on gateway warn/fail + probe fail; reachable-gateway keeps the honest upstream call, DNS failure still outranks it). Venue-DNS-broken-but-public-works now ends online: the dns-test chain escalates to a persistent dns-override (1.1.1.1 on the link, dies on reconnect, offered dns-revert undo; a useless override reverts itself) instead of auto-reverting into a misreported upstream outage. Override-aware getent/curl fakes model the venue end to end. Remaining: the edge rows (DNSSEC, proxy, MTU blackhole, clock skew, ARP conflict, roaming flap, stuck-connecting budget, USB-adapter unplug, driver/firmware, WPA3/SAE, randomized-MAC retry, NM-not-running, resolved-down, unmanaged resolv.conf) and the generalized post-action verification for connect/forget/add. +*** TODO Automatic diagnostic verbose-capture (failing diagnose + Advanced toggle) +On =overall: fail=, elevate the underlying stack (NM =WIFI,DHCP,DNS,CORE= / systemd-resolved / +wpa_supplicant) to debug at runtime, run the escalation, capture the journal + dmesg window + +=curl -v=, then restore every level. Also a manual "Debug on/off" toggle in Advanced for +reproducing intermittent failures. HARD: restore is guaranteed (try/finally) AND crash-guarded +(next run detects a left-elevated stack and restores it, like the DoT-restore watcher); the +captured journal is REDACTED before the bundle is written/shown (raw wpa_supplicant/NM debug +carries the PSK/EAP secret in cleartext) with a secret-leak test; log-level toggles run via the +V2 sudo-helper. Bonus: wpa_supplicant debug catches wrong-password/EAP failures the current NM +state-120 snapshot misses, so it also closes the auth live-detection gap. Spec: Observability → +"Automatic diagnostic verbose-capture". Origin: Craig 2026-06-30. + +*** VERIFY Dead-GUI console recovery vs "no terminals" — keep =make online= or replace it? :network: +The cj comment (2026-07-01) said scrub every terminal the module uses to report to or get input +from the user, and I folded that into decision 15 (all module UX is in-panel). The one place it +collides: the deliberate console-recovery path — =make online= / =net doctor --fix= run from a +bare TTY when waybar and the GUI are *down* — is the whole point of the CLI being usable with no +GUI. That's a terminal reporting to the user, but only because there's no panel to use. Keep it +as an explicit carve-out (recovery-only, not terminal-as-UI), or replace it with something else +(a TTY text UI still counts as a terminal)? Your call settles whether the Makefile/CLI recovery +targets stay in the spec. +** DONE [#B] Audio panel spec :feature:waybar:audio:solo: +CLOSED: [2026-07-03 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-02 +:END: +Went past the spec to a full build in a no-approvals speedrun. Spec is now IMPLEMENTED ([[file:docs/specs/2026-07-03-audio-panel-spec.org]], all 5 Decisions resolved). The panel shipped in the dotfiles repo (branch panel-bugfixing, commits 65e5bb0..9601420): pactl engine, GTK-free presenter, GTK instrument-console panel (OUTPUTS/INPUTS device rows with faders + per-device mute, LIVE/MUTED/PUSH·TALK mic keys, twin VU gauges, master quick-mute), Hyprland-bind push-to-talk, bar indicator, and the bar/keybind wiring (Super+A → panel, XF86AudioMute → master quick-mute). 102 unit tests + a passing AT-SPI smoke on velox. Live-eyeball validation filed under Manual testing and validation. Apply steps + follow-ups handed to the dotfiles project inbox. + +Original ask (roam inbox, 2026-07-02): net/bt-panel kin — change default output/input, volume for both, push-to-talk mic mode for meetings, master quick-mute, bar sound-glyph state. Related bindings: Super+M audio-cycle ring, Super+Shift+A mic-toggle. Prototype: =docs/prototypes/2026-07-03-sound-panel-prototype.html=. @@ -21,6 +21,10 @@ The vocabulary is open — topic tags are coined as needed — so these are conv - *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. * Archsetup Open Work +** TODO [#C] Net panel: Enterprise error never dismisses :bug:dotfiles:network: +The enterprise-auth error banner in the network panel stays no matter what; selecting another action should clear it (from roam inbox, 2026-07-11). Repro screenshot: =~/pictures/screenshots/2026-07-10_195911.png=. Minor severity × some users, sometimes (only enterprise-network users who hit the error) = P3 = [#C]. Distinct from the net doctor's enterprise auth verdict shipped this session — that's the doctor's terminal message; this is the panel's stuck error banner. +** TODO [#C] Notification/capture popup grows too large :bug:hyprland: +A popup (Craig calls it the notification/capture popup) gets too large; it needs a max on its scale so it never exceeds a threshold — his guess is roughly X by 120 (from roam inbox, 2026-07-11). Confirm which popup and window rule. Minor severity × most users, frequently = P3 = [#C]. ** TODO [#C] Net diagnostics leak connection names + SSIDs into copyable report and --json :bug:dotfiles:network:solo: The net doctor's copyable report (=report.py=, =scrub_text=) scrubs only MAC/IP, and =net diag/doctor --json= (=cli.py=) dumps the raw dict with no redaction. SSID redaction lives only in the event log (=redact_event=, gated on =redact_ssid=, default off). So a connection name (usually the SSID) appears in the clear in the link-step evidence and in every =--json= consumer — the copyable report is exactly the text a user pastes into a bug report. Secrets (PSK/password/token/portal URL) are already stripped, so this is names, not credentials: Minor severity, graded on severity alone per the privacy carve-out. @@ -33,7 +37,7 @@ Split out of the 2026-07-11 net-doctor-expansion spec review: that spec's new ri Build the [[file:docs/specs/2026-07-11-net-doctor-expansion-spec.org][net doctor expansion]] (DOING). 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. Phases 1-2 are gated on the shared cross-panel run-time privilege model, which doesn't exist yet; Phase 0 builds now. *** 2026-07-11 Sat @ 02:47:47 -0500 Built the read-only control-plane probe New module =net/src/net/control_plane.py= plus =diag.py= wiring, on dotfiles main (=21ca3ff=, pushed). =net diagnose= now carries a =control_plane= key in its envelope with three read-only signals: NetworkManager state (masked/failed/stopped/active, finer than the plain =is-active= the ladder used), any rival manager active alongside NM (dhcpcd, systemd-networkd, iwd via =systemctl is-active=), and the active profile's keyfile permissions (=stat= under an env-overridable =NET_NM_CONNECTIONS= root, default =/etc/NetworkManager/system-connections=). Detection only: no classifier change, no new step, no bearing on =overall=. Surfaced via =net diag --json=. The masked/failed read also runs in the nmcli-down early-return path, or a masked NM would short-circuit before the probe. Every read bounded through =cmd.run='s timeout; degrades to unknown / no-rival / None on an unreadable tool or unstattable file, so a probe that can't see never invents a fault. As a normal user the real 0700 root:root dir is unstattable, so the keyfile signal reads unknown (readable only under root, e.g. the doctor's privileged path). 21 new tests, full =make test= green; =/review-code= approved (two Minor items, both Phase 1 concerns already tracked in the spec: link-scoping the rivals and guarding the keyfile path in the chmod fix). Inbox note sent to dotfiles. -*** DOING [#C] Phase 1 — control-plane verdicts + privilege model +*** DOING [#B] Phase 1 — control-plane verdicts + privilege model Unblocked now that panelkit ships. =classify= gains =rival-manager=/=nm-masked=/=keyfile-perms= (all =fixable=), ordered ahead of the generic not-running rule, with the masked check reachable in the NM-down early-return path. Fixes register as =VERBS= + =ACTIONS= (=disable-rival=/=unmask-nm=/=chmod-keyfile=), each narrowly scoped. Live verification needs a real rival/masked/bad-keyfile state (a VM or a deliberately-broken host), so not solo. **** 2026-07-11 Sat @ 04:32:06 -0500 nm-masked verdict shipped + first panelkit integration On dotfiles main (=d73eba6=, pushed). The clean, unambiguous verdict of the three, done first to prove the panelkit-in-net rails: =classify= reads =control_plane.nm_state=; on =masked= it returns a fixable =unmask-nm= verdict (=remedy_class="privileged"=) ahead of the generic nm-service rule (=nm-restart= can't start a masked unit). New priv verb =unmask-nm= (=systemctl unmask NetworkManager=), =repair_unmask_nm= (unmask → start → verify), narration entry. The doctor resolves a privileged verdict through =panelkit.resolve()= before running: can't-elevate (GUI, no passwordless, no tty) degrades to the manual guide instead of attempting; existing tiers (no remedy_class) run unchanged. The =net= shim + test add =panelkit/src= to sys.path. Design call shipped (flagged for Craig): CLI =net doctor --fix= is the deliberate act satisfying the Confirm floor, so no CLI prompt added; panelkit contributes the run/prompt/guide degradation on the CLI; the GUI arm-press stays its confirm (panel wiring pending). 765 net tests + 65 suites green, review-code clean. Inbox note sent. @@ -43,7 +47,7 @@ On dotfiles main (=c9f8604=, pushed). Craig approved proceeding with the default The harness is BUILT (archsetup =a364c1e=): =scripts/testing/net-scenarios/{10-nm-masked,20-rival-manager,30-keyfile-perms}.sh= (break/fix/assert + the expected diagnose verdict) and =run-net-scenarios.sh= (rsyncs net+panelkit into a target over ssh, drives the scenarios). Scenario logic reviewed; the ssh transport plumbing is UNEXERCISED (marked first-draft) and wants a shakeout on the first real run. What remains needs Craig: a booted VM (a real kernel + network stack — NM does NOT run reliably in nspawn, so a container only shows the mask-symlink/keyfile-mode half, not "NM active") with NetworkManager + dhcpcd installed, reachable over ssh as root. Then =run-net-scenarios.sh --target root@HOST --profile <saved-profile>=. The two live questions: disable-rival was already chained to a follow-on reset (=be15a81=), so the run confirms whether the reset fires or NM auto-recovers; and the keyfile root-owned pass (only reproduces under root). The by-hand equivalent is the "Manual testing and validation" → "Net doctor privileged fixes" checklist. *** 2026-07-11 Sat @ 06:31:29 -0500 Built the sharpened auth verdict On dotfiles main (=12e3e76=, pushed). =gather_context= derives an auth cause on an auth failure from the saved profile's key-mgmt + the scanned SECURITY flags for the SSID (not GENERAL.REASON, which only marks *that* auth failed): sae / hidden / enterprise / generic. =classify= turns sae and hidden into =fixable= Privileged profile-modifies (=key-mgmt sae= + PMF, or the hidden flag) routed through =panelkit.resolve()= like the Phase 1 control-plane verdicts; enterprise and generic stay =needs-user-action= with a cause-named message. Two new priv verbs (=conn-sae=, =conn-hidden=, uuid-validated single =nmcli connection modify= commands) + =repair_auth_sae=/=repair_auth_hidden=, each chained into a reset (FIX_CHAINS) so NM reconnects with the corrected profile. Hidden fires only on a non-empty scan missing the SSID, so a failed scan can't fabricate it. Tightened the =fake-nmcli= key-mgmt hook to require the =-g= prefix so the conn-sae modify isn't read back as a get_value. Pairwise (reason × profile-state) covered in =TestAuthCauseDerivation=. 813 net tests + 65 suites green, review-code Approve, voice. Inbox note sent. Live half (real WPA3/hidden profile-modify) is the VM/manual checklist. -*** TODO [#C] Phase 3 — flip the net spec to IMPLEMENTED +*** TODO [#B] Phase 3 — flip the net spec to IMPLEMENTED On v1 completion: flip the spec's status heading to IMPLEMENTED (dated history line + mirror) and log the vNext items (flaky/drops cluster, DoT/DNSSEC verdict, profile hygiene) to todo.org. ** TODO [#B] Bt doctor expansion v1 — build the READY spec :feature:dotfiles:bluetooth: @@ -57,7 +61,7 @@ New module =bluetooth/src/bt/probes.py= plus =doctor.py= wiring, on dotfiles mai On dotfiles main (=f05a9b4=, pushed). The no-adapter step now names the blob: a new =_no_adapter_step= consults =probes.firmware_hint()= on a genuine no-adapter fault and, on a per-vendor signature match, sets =evidence= to "no Bluetooth adapter found — <Vendor> firmware <blob> failed to load" and =next_action= to "update linux-firmware and reboot", tagged with a new =code="no-adapter-firmware"= so a =--json= consumer can branch without string-matching. A clean log keeps the generic hardware/driver verdict. A Guide, not a repair: the step carries no =repair= action, so =--fix= never touches it, and it needs no privilege model (so Phase 1 lands independently of the shared cross-panel model). A missing =bluetoothctl= (=BtctlError=) short-circuits before the firmware read, so the verdict fires only on a real no-adapter fault, not a broken install — this also tightened Phase 0 (which read the log on both None branches) to the genuine no-adapter case. =_mk= gained a uniform =code= key (default None) added to every diagnose step, mirroring the existing =repair= key; no test asserts an exact step key-set, verified. =format_doctor_human= already renders =evidence=/=next_action=, so no formatter change. 133 bt tests (+8), full =make test= green; =/review-code= clean. Inbox note sent to dotfiles. *** 2026-07-11 Sat @ 06:48:21 -0500 Built the persistent-power verdict + fix On dotfiles main (=d7d859f=, pushed). =_powered_step= consumes the bt Phase 0 boot-enablement probe: AutoEnable explicitly false, service disabled at boot, or TLP listing bluetooth → =powered-off-persistent= (code + evidence naming the cause) carrying the =persist-power= repair; absent/unset config reads as auto-enable-on (bluez default) → the plain =power-on=, so healthy machines can't false-positive. The =persist-power= repair fixes only the causes set (AutoEnable=true, =systemctl enable bluetooth=, drop bluetooth from the TLP list), powers the adapter on now, and verifies each cause cleared. Config edits are pure idempotent text transforms in a new =bootconf= module (comments preserved), staged and installed via a fixed-destination =cp= verb so the write needs root but the mutation is unit-testable. =priv.py= gained three narrow verbs (=enable-bluetooth=, =write-main-conf=, =write-tlp-conf=). The repair is Privileged and resolves through =panelkit= before running: can't-elevate degrades to the guide. The bt shim gained =panelkit= on its path. 287 bt tests + 65 make-test suites green, review-code Approve, voice. Inbox note sent. Live half (real reboot persistence) is the VM/manual checklist. -*** TODO [#C] Phase 3 — flip the bt spec to IMPLEMENTED +*** TODO [#B] Phase 3 — flip the bt spec to IMPLEMENTED On v1 completion: flip the spec's status heading to IMPLEMENTED (dated history line + mirror) and log the vNext items (stale-bond signature, connection-parameter hints, bt-audio-profile expansion) to todo.org. ** TODO [#D] Maintenance console vNext :feature: @@ -68,17 +72,6 @@ snapshot_retention_repair (WRITE SANE LIMITS — =snapper -c <config> set-config Two fix shapes, decide at work time: (1) wire a digest key — e.g. on the timeline row when the config's installed TIMELINE_LIMIT_* drift from the TOML values (needs the probe to read the installed limits for comparison); or (2) keep it CLI-only deliberately and special-case the roster wording for remedies with no panel key ("CLI only — maint fix"). Option 2 is a few lines; option 1 makes limit drift visible on the board, which is the 2026-05-26 pile-up's root cause. Severity minor × rare edge = P4 per the bug matrix. -** DONE [#C] minimal tier zsh login PATH gap :bug:dotfiles:quick:solo: -CLOSED: [2026-07-09 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-09 -:END: -Fixed (dotfiles =c6a7878=). =minimal/.zprofile= added, mirroring =common/='s PATH-only prepend and its rationale comment. - -Tested the invariant rather than the file, because the bug *was* a per-file fix missing a tier: any stow tier shipping a =.zshrc= must ship a =.zprofile= that prepends =~/.local/bin=. Red first, and it named =minimal=. Verified behaviorally as the task asked — stowed the tier into a throwaway HOME and read a real =zsh -lc 'echo $PATH'=, which now leads with =~/.local/bin=. Full =make test= green. - -The 2026-07-08 fix gave common/ a .zprofile (zsh logins never read .profile, so ~/.local/bin was missing from TTY/ssh PATH — dotfiles 39cef40). The minimal/ tier has the same gap: it carries .zshrc and .profile but no .zprofile, so headless installs (DESKTOP_ENV=none stows minimal/ instead of common/) still get zsh logins without ~/.local/bin. Add a matching minimal/.zprofile (PATH-only prepend, same rationale comment). Verify: fresh minimal stow, zsh -lc 'echo $PATH' shows ~/.local/bin first. The installer itself is clean — it writes no shell profiles; the stow layer owns this. - ** TODO [#C] zfs base VM image build failure: ZFS DKMS module missing :bug:zfs: :PROPERTIES: :LAST_REVIEWED: 2026-07-09 @@ -1280,31 +1273,6 @@ 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. -** DONE [#B] Doctor button for the audio panel :feature:audio: -CLOSED: [2026-07-10 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-09 -:END: -What happens when pipewire or pulseaudio is broken — would we know? Spec: [[file:docs/specs/2026-07-09-audio-doctor-spec.org][docs/specs/2026-07-09-audio-doctor-spec.org]] — *READY*, all eight decisions closed 2026-07-09, Phase 0 shipped. Buildable: Phase 1 diag, Phase 2 classify, Phase 3 repair + doctor, Phase 4 the DOCTOR key and wall. Grounded in a live survey of ratio's stack: there is no PulseAudio (pipewire + wireplumber + pipewire-pulse, all user-scope, so no sudo anywhere), =pactl= hangs against a server that accepts and never answers, and the panel can't diagnose itself because =pactl= is the layer most likely to be down. Mirrors the net panel's diag/classify/repair/doctor split, not the maint console's priv-verb table. - -Re-graded =[#C]= → =[#B]= by the 2026-07-09 audit: the spec is written, Phase 0 shipped, and six decisions are all that stand between this and a build. Active backlog, not parking lot. - -*** 2026-07-09 Thu @ 14:12:00 -0500 Fixed: waybar's audio module died against a half-wedged server -My first read of this was wrong. =pactl.run()= already carries =timeout=8= and raises =PactlTimeout=, so the panel degrades and does not freeze. Correcting the record rather than leaving the claim standing. - -The real bug, found by driving it: =build_status()= guarded its two device-list reads but called =default_sink()= and =default_source()= outside the guard. A server that answered the lists and then stopped answering raised =PactlTimeout= straight out of a function whose whole contract is to degrade to =ok: False=. =waybar-audio= calls it with nothing catching it, so the bar's audio module died instead of dimming; the GTK panel survived only because its background worker catches everything. - -A server that hangs on the *first* call never exposes this — =list_sinks= raises inside the guard and masks the unguarded reads behind it — so the obvious regression test passes against broken code. =fake-pactl= grew =FAKE_PACTL_SLEEP_DEFAULTS= to express "answers the lists, hangs on the defaults". Verified end to end: =waybar-audio= now emits =class: degraded= / "Audio graph unavailable" and exits 0. dotfiles =4d42eb3=; full =make test= green. - -*** 2026-07-10 Fri @ 06:41:00 -0500 Shipped: the audio doctor, phases 1 through 4 -=audio diag=, =audio doctor=, =audio doctor --fix=, and the panel's DOCTOR section header with DIAGNOSE / FIX keys streaming into a results wall (copy + close). Spec is IMPLEMENTED. dotfiles =01a1d80=, =76857e3=, =7223c51=, =1550ac8=, =4320255=, =bd33440=. - -Live verification on ratio replaced the planned VM run for remedies 1, 4 and 5, and found two bugs no fake could surface: =pactl get-default-sink= prints the literal =@DEFAULT_SINK@= placeholder when no default is set, and remedy 1 re-probed before WirePlumber had re-created the devices, calling a repaired stack broken. Both fixed. - -Left open, not a v1 gap: mpv played silently while the doctor read healthy. The stack was genuinely fine, and per-application stream routing is an explicit spec Non-Goal. Worth a task only if it recurs. - -Second sighting, 2026-07-10: Chrome stopped recognizing the microphone while the stack was healthy (Shure MV7+ default, unmuted, 82%, PTT off). Restarting Chrome fixed it; nothing on the machine was touched. Both sightings share a shape the doctor cannot see: the graph is fine and one client cannot use it. A third sighting turns this into a design question, namely whether the doctor should say "the stack is fine, the fault is in the application" rather than a bare healthy. - ** TODO [#B] The audio doctor never checks the microphone :bug:audio:solo: 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. @@ -1331,516 +1299,6 @@ The maintenance console's coredump metric flagged telega-server on ratio (8 core * Archsetup Resolved -** DONE [#B] Right-click date/time: ntp sync + timezone update :feature:waybar: -CLOSED: [2026-07-02 Thu] -Right-click on the date updates the clock from ntpd (or whatever keeps the clock in sync); right-click on the time runs the update-timezone script. Neither opens a terminal — both run transparently in the background. Errors surface as notifications. The module should detect whether we're online and, if not, show a message instead of running the script. Origin: roam inbox capture 2026-07-02. - -Shipped 2026-07-02 (dotfiles 2f7993d). Date right-click = clock-sync (chronyc makestep behind a connectivity gate; offline/captive get explanatory notifications). Time right-click = timezone-set, rewritten to prefer WiFi geolocation (whereami → timeapi.io) over IP lookup — the hotel IP geolocated two timezones off, and ipapi.co is paywalled now (ipinfo.io is the fallback). Both promptless (sudo -n), all outcomes notify, worldclock gained signal 16 for an instant refresh. 13 new tests across clock-sync + timezone-set; live-verified on velox (WiFi path resolved Rhode Island → America/New_York correctly). -** DONE [#B] Screenshot "view image" option :feature:hyprland: -CLOSED: [2026-07-02 Thu] -The screenshot flow should also offer a "view image" selection: saves the shot, opens it in a viewer, and puts the path on the clipboard. Origin: roam inbox capture 2026-07-02. - -Shipped 2026-07-02 (dotfiles 10e5961). View Image entry in the post-capture fuzzel menu: opens the shot via xdg-open (default viewer, currently feh) and puts the path on the clipboard. Script gained env seams + a nine-test suite (menu dispatch, both capture modes, cancel, failure). Live check pending: take a shot and pick View Image once. -** DONE [#C] Collapse-triangle buttons: dimmer, inlaid styling :waybar: -CLOSED: [2026-07-02 Thu] -The triangle collapse buttons should look embedded (inlaid) and be slightly dimmed so they don't compete for eye attention with the other components. Origin: roam inbox capture 2026-07-02. - -Shipped 2026-07-02 (dotfiles 15cb93c): muted color + dark inset well + smaller glyph; hover still brightens. Hudson theme carries the same shape. Screenshot-verified. -** DONE [#C] Contrast button ignores the white=on / red=off paradigm :bug:waybar: -CLOSED: [2026-07-02 Thu] -The contrast button doesn't respect the white=on, red=off color paradigm the other waybar modules follow. Cosmetic × every time = P3. Origin: roam inbox capture 2026-07-02. - -Shipped 2026-07-02 (dotfiles 15cb93c). The "contrast button" is the auto-dim module — its ON icon (nf-fa-adjust) is the classic contrast glyph. It showed gold when on and nothing when off; now on = default silver, off = terracotta, matching every other toggle. Screenshot-verified both states. -** DONE [#C] Off-state red inconsistent across waybar modules :bug:waybar: -CLOSED: [2026-07-02 Thu] -Terracotta red isn't applied uniformly for "off": the sleep icon is a different shade than the mouse/trackpad when off, and the dim indicator doesn't show red when off at all. Cosmetic × every time = P3. Origin: roam inbox capture 2026-07-02. - -Shipped 2026-07-02 (dotfiles 15cb93c + 7f1f334). The touchpad script's Pango-markup red predated the terracotta theme pass (#d47c59 vs the CSS's #cb6b4d) — unified on #cb6b4d. Dim-off now red (see the contrast task). Bonus find: waybar hands every pulseaudio instance the sink's .muted class, so a muted speaker also painted the mic red — scoped with :not(.mic) so each glyph keys on its own device. -** DONE [#B] Waybar volume/mic toggle like the touchpad module :feature:waybar: -CLOSED: [2026-07-02 Thu] -Make the volume/mic waybar component look and behave like the touchpad/mouse toggle. -- Move the mic to the other side of the volume so the percentage isn't in the way. The mic and speaker icons sit the same distance apart as the hand and mouse. -- One keybinding cycles the four states: volume on / mic on, volume on / mic off (red), volume off (red) / mic on, volume off (red) / mic off (red). -- Move the trackpad/mouse toggle to another keybinding (discuss an open mnemonic, e.g. =d= for disable) and assign Super+M to this module (for mute). -Origin: roam inbox capture. - -Shipped 2026-07-02 (dotfiles 7f1f334). New audio-cycle script walks the four-state ring in the exact order above (wpctl-backed, explicit set-mute so the pair can't desync, 6 tests) on Super+M; live-verified the full ring on velox. Mic moved left of the speaker and hugs it via paired margins, percentage on the outside. Touchpad toggle moved to Super+Shift+I ("input devices" — d-for-disable was taken at both levels by removemaster and dim-toggle); its tooltip and tests follow. -** DONE [#C] Timer end sends no notification :bug:waybar: -CLOSED: [2026-07-02 Thu] -The end of a wtimer timer didn't fire a desktop notification. Needs reproduction to confirm frequency; priority follows the severity-by-frequency matrix once known (a reliably-missing timer-end alert would rate higher). Origin: roam inbox capture. - -Root-caused 2026-07-02 (dotfiles ca35642). The pipeline works (a live 3s timer fired and persisted), but notify sent everything --urgency=normal and dunstrc delays normal-urgency popups while a fullscreen window has focus — a timer ending mid-video sat invisible until fullscreen exit. Alarms now go critical urgency, which rides the fullscreen_show_critical rule; verified CRITICAL in dunst history. The alarm sound (paplay, separate from dunst) was never affected. -** DONE [#C] Keybind hints in waybar module tooltips :waybar: -CLOSED: [2026-07-02 Thu] -Every module's hover tooltip should list its keyboard shortcut(s), for discoverability. Audit the modules and add the bindings to each tooltip. Origin: roam inbox capture. - -Shipped 2026-07-02 (dotfiles 4c32aec). Audited every module: arrows (Super+[ / Super+]), sysmon (Super+R), net (Super+Shift+N), layout (Super+Shift+←/→), menu (Super+Space / Super+Shift+Q, tooltip enabled), plus the already-hinted dim/caffeine/touchpad/mic/volume. Date/time tooltips document their new right-click actions. Workspaces/window/tray don't take custom tooltips; timer has no keybind. -** CANCELLED [#C] Smooth waybar expansion animation :waybar: -CLOSED: [2026-07-02 Thu] -The cluster expansion jumps instead of animating, and a few systray icons pop in one-by-one afterward, which reads as glitchy. Animate the expansion smoothly if waybar allows it — width transitions are limited, so feasibility is uncertain (hence [#C]). Origin: roam inbox capture. - -Assessed infeasible 2026-07-02: collapse works by config rewrite + SIGUSR2 reload, which rebuilds every widget — nothing survives to transition, GTK3 can't animate add/remove without Revealer (an upstream waybar change), and the tray pop-in is async StatusNotifier re-registration. Full findings + revisit conditions: [[file:docs/design/2026-07-02-waybar-expansion-animation-feasibility.org]]. -** DONE [#C] Optional label on timer/alarm/stopwatch items :feature:waybar: -CLOSED: [2026-07-02 Thu] -Let each wtimer item carry an optional short text label. The data model already supports it (=add_timer/add_alarm/add_stopwatch/add_pomodoro= all take =label=""=, and =_describe= shows =label or type=); the gap is the fuzzel-driven creation flow, which doesn't prompt for a label. Add the optional label prompt on create. Origin: roam inbox capture. - -Shipped 2026-07-02 (dotfiles ca35642): =wtimer new= gained a "label (optional)" fuzzel prompt after the type/value prompts; empty keeps the unlabeled default. 2 new tests (89 total in the suite). -** DONE [#B] Pocketbook finish-or-cancel decision :pocketbook: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-24 -:END: -Decided by Craig 2026-07-02, ahead of the scheduled checkpoint: remove pocketbook altogether. Executed same day — pip package uninstalled (user site clean), running instance killed, launcher gone, =pocketbook/= tree removed from the repo, Super+P rebound to toggle-touchpad (P for Pointers; Super+Shift+I unbound, waybar tooltip hint updated — dotfiles a750cb4). The org-capture popup remains the quick-notes surface. -** DONE [#B] Provision Eask in archsetup :tooling:eask: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-05-26 -:END: -Shipped 2026-07-02 (speedrun): npm global install block added after the nvm line — runs as $username with --prefix $HOME/.local, display/error_warn wrapped, output to $logfile, matching the claude-code block's shape. The npmrc decision went yes: dotfiles common/.npmrc pins prefix=${HOME}/.local (stowed; hand-linked live, npm config get prefix confirms ~/.local — dotfiles 01627cc). VM assertion added: ~/.local/bin/eask present + ~/.npmrc stowed. Live smoke: eask 0.12.9 on PATH. Full acceptance (fresh-install chime make setup/test) rides the next VM pass. -Add =@emacs-eask/cli= to archsetup's provisioning so fresh machines get it. Eask is installed by hand today and declared nowhere in archsetup or the dotfiles repo, yet both chime and linear-emacs depend on it (their =make setup/test/coverage= shell out to =eask=). Source: handoff from linear-emacs 2026-05-23. - -- Add a global npm install after the node block (=archsetup= ~2030, after =aur_install nvm=), modeled on the claude-code native-install block: run as =$username=, wrapped in =display=/=error_warn=, output to =$logfile=. Roughly =sudo -u "$username" bash -c 'npm install -g --prefix "$HOME/.local" @emacs-eask/cli'=. -- Pin the prefix to =~/.local= so eask lands at =~/.local/bin/eask= (already on PATH) and the install runs as the user, not root. On the current machine =npm config get prefix= returns =/usr=, so eask was installed with an explicit =--prefix=. -- Decision: also set a persistent user npm prefix (=~/.npmrc= with =prefix=${HOME}/.local=)? If yes, that =~/.npmrc= is a legitimate dotfile to stow; if no, rely on the explicit =--prefix= flag alone. =~/.eask/= is a regenerable cache — leave un-stowed. -- Acceptance: fresh run leaves =eask= on PATH at =~/.local/bin/eask= (no root); =cd ~/code/chime && make setup && make test= works. -** DONE [#C] Waybar timer dialog styling :waybar: -CLOSED: [2026-07-02 Thu] -From Craig's roam capture 2026-07-02: style the timer module dialogs like the screenshot dialog — tighter window, icons on the selections, colon+space after the prompt. - -Shipped 2026-07-02 (dotfiles 9ffcba7): dialogs size to content, type menu carries the kind glyphs, prompts end ": ". Three new tests; screenshot-verified live. -** DONE [#B] Waybar collapse jumps client windows :bug:waybar:hyprland: -CLOSED: [2026-07-02 Thu] -From Craig's roam capture 2026-07-02: collapsing/expanding (and any waybar teardown) snapped every tiled window up and back down; hold the clients still and let only the bar change. - -Shipped 2026-07-02 (dotfiles 4b1a4ec): waybar now runs exclusive:false and the new waybar-reserve script statically reserves the bar strip per monitor (wired as exec so config reloads re-apply it). Verified live: client geometry held constant through bar kill, relaunch, and a collapse round-trip. Eight new tests (script + pairing). -** DONE [#B] Bluetooth panel + bar module :feature:waybar:bluetooth: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:SPEC_ID: 1271a845-4463-4831-9902-990eda6b2265 -:END: -Spec: [[file:docs/specs/2026-07-02-bluetooth-panel-spec.org]] (IMPLEMENTED 2026-07-02 — all five phases shipped same day: engine eb2230f, panel 76b2c05, bar module e372de3, bt-priv + blueman retirement 2a026b1/d8d8c53, install wiring proven by VM assertions). Residual: the phase 4-5 VM assertions run on the next VM pass; ratio picks up the package removal + hand-links on its trip list. - -A bluetooth panel driving a CLI underneath (bluetoothctl one-shot verbs), consistent in look and feel with the net panel (GTK4 + layer-shell + Blueprint, humble-object presenter, verify-everything). Minimalistic interface, full functionality, plus a diagnostics/troubleshooting section mirroring the net panel's Diagnostics tab. Bar module glyph opens it. Craig's ask (2026-07-02): follow UX/UI best practices; where the net panel's patterns conflict with best practices, file a net-panel bug task rather than clone the flaw. - -*** 2026-07-02 Thu @ 13:30:42 -0400 Shipped phase 1 — the bt engine package (dotfiles eb2230f) -=bluetooth/src/bt/= mirrors the net engine's layout: btctl parsing boundary (show/devices/info, connect-error classifier), sysio rfkill/airplane, audio module over pw-dump/wpctl (HSP probe + A2DP switch repair with verify-after), redacted eventlog, six repair tiers, and the doctor chain (adapter → rfkill → service → powered → devices → audio profile) with safe auto-repairs behind =--fix= (never auto-connects; airplane blocks are named, not fought). 101 tests over fake binaries; 42 suites green (=make test= glob auto-discovered =tests/bt/= — gate check verified). Live read-only on velox: =bt status= + =bt doctor= read the real adapter/devices/audio graph; =~/.local/bin/bt= hand-linked (no restow under running Hyprland). Ground truth vs spec: profile inventory needs =pw-dump= (wpctl can't enumerate), and the card's =bluez5.profile= prop is unreliable — sink node's =api.bluez5.profile= is authoritative. Deferred INTO phase 2: the shared dupre css factoring (net's css is an inline string in =gui.py=, not an asset — factoring it without the bt-panel consumer just risks the working net panel). - -*** 2026-07-02 Thu @ 14:15:27 -0400 Shipped phase 2 — the GTK panel (dotfiles 76b2c05) -Cloned the net panel's shape over the phase 1 engine: GTK-free PanelModel + viewmodel (69 new tests, display-free), Blueprint pages (Devices with the adapter power row + Paired/Nearby sub-views; Diagnostics with the doctor cascade, inline fix buttons mapped from step =repair= keys, Advanced tool selector), gui.py controller (layer-shell OVERLAY 380x520 TOP+RIGHT, Esc closes, single-instance toggle, bg worker, passkey + confirm dialogs), pairing pty state machine (=bt/pairing.py=: confirm-passkey yes/no with default-deny, display-passkey, bounded deadline, tested over the fake's interactive mode), manage.py op envelopes shared by CLI and panel (cli refactored onto it; power + discoverable verbs added), =bt panel= subcommand + =bt-panel= toggle wrapper (hand-linked into =~/.local/bin=, no restow under live Hyprland). Shared dupre css factored: net's inline =_CSS= → =hyprland/.config/themes/dupre/panel.css= with =dupre-*= classes, both panels load it (stowed path first, repo-relative fallback; hand-linked =~/.config/themes/dupre/panel.css=). Super+Shift+B rebound blueman-manager → bt-panel (hyprctl reloaded, live). 43 suites green (=make test= exit 0). DEFERRED pending Zoom ending: the AT-SPI smoke (=make test-panel-bt=, written and wired) and any visual check of either panel with the factored css — both need a visible window. Gotcha for posterity: the old =test_panel_stub_exits_two= CLI test ran =bt panel= for real once cmd_panel was wired and launched the panel on the live compositor mid-meeting for ~30s before the test timeout killed it; it now asserts parser wiring only — never run =bt panel= inside =make test=. - -*** 2026-07-02 Thu @ 15:06:00 -0400 Shipped phase 3 — the bar module + blueman retirement (dotfiles e372de3) -=custom/bluetooth= over the engine: waybar-bt shim + =bt/indicator.py= (state-following glyph — slashed off/blocked/absent, plain dim idle, connected mark white; low-battery <15% adds a red pango percentage to the glyph; tooltip = connected devices with battery + Super+Shift+B hint). Signal 10; the panel pokes =pkill -RTMIN+10 -x waybar= after each status reload. Blueman retired from the Hyprland session: exec-once + both windowrules removed, applet killed live; waybar relaunched on the runtime config and the module verified on the bar (connected glyph, blueman tray icon gone). Theme drift guard caught that themes/*/waybar.css edits must mirror into the live =waybar/style.css= — all three updated. 43 suites green. ALSO closed this pass: the deferred phase 2 visual batch (bt AT-SPI smoke green after fixing its Connect/Disconnect state-following assertion — c1a8219; net smoke green on the factored css; both panels eyeballed correct in dupre). Left for phase 4: package removal (blueman out of archsetup), sxhkdrc's dwm blueman-manager binding decision rides that pass. - -*** 2026-07-02 Thu @ 15:16:51 -0400 Shipped phase 4 — bt-priv shim, blueman out, VM assertions -Dotfiles =2a026b1=: the stowed =bt-priv= shim over the phase-2 =bt.priv= module (one verb, =restart-bluetooth=; verified end-to-end against the symlinked fake-systemctl — rc 0 with =BT_SUDO= empty, rc 2 on bad verb/usage; hand-linked into =~/.local/bin=), and the sxhkd =Super+Shift+B= bind repointed from the retired blueman-manager to =st -e bluetoothctl= (the decided terminal fallback — the GTK panel is Wayland-only, and the bt CLI is hyprland-tier so dwm never gets it). 43 dotfiles suites green. archsetup: blueman dropped from the =desktop_environment= bluetooth loop (bluez + bluez-utils stay, solaar untouched); VM assertions added to =test_packages.py= (bluez/bluez-utils installed, blueman NOT installed as the retirement regression guard — collected 15, exercised on the next VM run since VM tests run committed code); =bash -n= + =py_compile= + =make test-unit= green. SUDOERS: no new rule needed, same conclusion as net-priv (2026-07-01 entry) — archsetup:1089 grants the primary user blanket =NOPASSWD: ALL=, which covers =systemctl restart bluetooth=; a narrow bt-priv rule would be dead config under the blanket grant, so phase 5's "sudoers placed" item is satisfied by the existing grant. LIVE: blueman package removed from velox (=pacman -Rns=, decision "drop it outright, both machines"); ratio needs the same + the bt-priv hand-link on its trip list. - -*** 2026-07-02 Thu @ 15:19:58 -0400 Shipped phase 5 — install-default wiring proven by VM assertions -No new install code was needed: the waybar =custom/bluetooth= module, the =Super+Shift+B= → =bt-panel= bind (hyprland.conf), and the shared =themes/dupre/panel.css= all live in the dotfiles hyprland tier, so the existing clone + =make stow hyprland= step lands them on a fresh install; sudoers is covered by the blanket grant (phase 4 conclusion). The phase's substance is the proof: =test_desktop.py= gained hyprland-gated assertions that the four bt bins (=bt=, =bt-panel=, =bt-priv=, =waybar-bt=) are stowed executable in =~/.local/bin= (either stow shape — per-file symlink or folded dir), the waybar config carries =custom/bluetooth=, hyprland.conf carries the =bt-panel= bind, and the stowed theme has =panel.css=. Collected 30 in =test_desktop.py=; exercised on the next VM run (VM tests run committed code). - -*** 2026-07-02 Thu @ 15:19:58 -0400 Test surface complete across all phases -Everything the surface named exists and is green: engine suites over fake binaries (phase 1, 101 tests — btctl parse, doctor chain, A2DP repair verify), PanelModel presenter suite (phase 2, 69 tests), pairing state-machine suite (passkey confirm / NoInputNoOutput / timeout, over the fake's interactive mode), bar-module suite (phase 3), gated AT-SPI smoke (=make test-panel-bt=, run green live), and the phase 4-5 VM assertions (=test_packages.py= bluetooth stack + blueman-absent guard; =test_desktop.py= panel wiring). 43 dotfiles suites green; VM assertions await the next VM run. -** DONE [#B] All error messages should be actionable with recovery steps -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-24 -:END: -Shipped 2026-07-02 (speedrun). Structural fix at the helper: =error_fatal= now takes an optional third recovery-hint arg and every fatal prints the last five log lines inline, the full log path, the per-site "Fix:" when given, and the resume pointer (step markers mean a re-run continues where it stopped) — so even a hint-less fatal is actionable. All 17 fatal call sites got specific hints (keyring reinit, mirrorlist switch, userdel/USERNAME conflict, base-devel for makepkg, DESKTOP_ENV values, dotfiles-dir cleanup, tmpfs sizing, aur.archlinux.org reachability). The end-of-run Error Summary now closes with the grep-the-log line and the fix-and-re-run pointer. =error_warn= already carried what-failed + exit code into the summary; unchanged. -** DONE [#B] Improve logging consistency -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-24 -:END: -Shipped 2026-07-02 (speedrun), paired with the actionable-errors task. Audit result: the install helpers (pacman_install/aur_install/retry_install/run_task/git_install/pipx) and error helpers already tee/append everything to $logfile — the gaps were direct mutations whose stderr went to the console and vanished. Swept every =sed -i= and file-write mutation lacking capture (locale.gen uncomment, pacman.conf ParallelDownloads/Color + multilib, waybar battery removal x3, wireless-regdom, geoclue BeaconDB, paccache, BRIO udev rule, fstab fmask, mkinitcpio HOOKS, sudoers append, ufw status read): each now sends stderr to $logfile, and the previously-silent ones (locale.gen, pacman.conf, multilib, waybar, regdom, geoclue, paccache, udev) gained =error_warn= handlers so failures land in the summary instead of passing silently. Verified: bash -n clean, 10 unit suites green, shellcheck warning-diff vs HEAD empty (no new findings). -** DONE [#B] Add NVIDIA preflight check for Hyprland -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-05-21 -:END: -Shipped 2026-07-02 (speedrun), TDD. =nvidia_preflight_report= is a pure sed-extractable core (same harness pattern as zig-pin): modalias scan for vendor 10DE — DRM first, PCI display-class (bc03) fallback so an NVIDIA audio function can't false-trigger — then the repo's =nvidia-utils= candidate major checked against 535. Prints the Wayland guidance + env vars (LIBVA_DRIVER_NAME, GBM_BACKEND, __GLX_VENDOR_LIBRARY_NAME, ELECTRON_OZONE_PLATFORM_HINT) and the pre-Turing/AUR-legacy note. preflight_checks aborts on <535/unknown (rc 11), prompts continue/abort on a healthy NVIDIA box (rc 10), silent on non-NVIDIA (rc 0). 9 Normal/Boundary/Error tests over fake modalias trees + a fake pacman (=tests/nvidia-preflight/=, glob-discovered by test-unit — 10 suites green). -** DONE [#C] Wlogout exit-menu buttons are rectangular, not square -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-24 -:END: -The wlogout exit menu renders its buttons taller than they are wide on velox, so the cells read as vertical rectangles instead of squares. They render square (centered) correctly on ratio, so this is a per-host / resolution difference, not a flat bug. Fix the button sizing in the wlogout style (=~/.dotfiles/hyprland/.config/wlogout/style.css=) so each cell is square on both hosts. Noticed 2026-05-21. Related: the [#D] VERIFY about wlogout sizing across displays. - -The wlogout config uses fixed pixel margins, which is the likely reason sizing differs across the two displays — adjusting them for the laptop screen is part of the fix (folded in from the former "Test wlogout menu on laptop" VERIFY, 2026-06-24). - -Add a regression test so the square-cell fix doesn't silently break on a resolution change: assert the rendered (or computed) wlogout button cells are square across ratio's and velox's resolutions. Dropped :quick: — the cross-host test pushes this past a spare-moment fix. - -Shipped 2026-07-02 (dotfiles 775771b). Keybind now calls a =wlogout-menu= wrapper computing centered margins from the focused monitor (the old fixed L/R 1200 exceeded velox's 1436 logical width). Also fixed two styling defects the geometry hid: invisible unfocused borders (now muted, so the square edge is visible) and hover/focus sharing one gold rule (lock button glowed at launch; focus is now a muted ring). Tests: unit margin-math suite across both hosts' resolutions + portrait + small + bad-geometry, CSS regression suite, and a compositor-gated =make test-wlogout= smoke that launches a no-op probe, screenshots, and measures squareness (velox: 361x361 px, PASS). Ratio's visual eyeball rides the pending ratio sync. -** DONE [#C] Net panel: error toasts auto-dismiss unread :bug:network:waybar: -CLOSED: [2026-07-02 Thu] -Fixed in dotfiles 0f017d4: viewmodel.toast_plan owns the toast policy — errors show sticky and ignore the post-op refresh's empty clear (worst case: a forget failure's error was wiped within ~2s by its own refresh), and the next real status replaces them. Successes keep the 4s fade. 7 policy tests added; 41 suites green. -** DONE [#C] Net panel: verify claimed keyboard navigation :test:network:waybar: -CLOSED: [2026-07-02 Thu] -Found during the bluetooth-panel UX pass (2026-07-02). The V2 spec claims tab-between-sections, arrow-key row navigation, and type-to-filter, but no custom keyboard code exists in the panel — arrows and type-ahead may ride GTK ListBox defaults, tab-between-sections likely doesn't. Verify each claim against the live panel (AT-SPI smoke can assert focus order); implement or strike the claims from the spec so spec and panel agree. - -*** 2026-07-02 Thu @ 13:05:00 -0400 Code-level pass done; live probe deferred (Craig in a Zoom meeting) -Code reality (dotfiles net/src/net): Esc close is wired (gui EventControllerKey); row-activated -> primary is wired for both connection lists (pages.py:122,126), so Enter-on-row rides the GTK ListBox activate binding; arrows ride ListBox defaults; NOTHING implements type-to-filter (no search/filter code exists — that claim is false as written); Tab is the plain GTK focus chain, widget by widget, not section jumps. Live AT-SPI probe plan: launch panel in test mode, drive keys via hyprctl dispatch sendshortcut targeted AT THE PANEL WINDOW (never the focused surface — wtype/ydotool absent anyway), gate every key on the panel holding focus, never send Enter on the available list (real connect risk). BLOCKED at 13:05: active window is zoom (meeting) — no test windows, no synthetic input until clear. Then: verify focus order + arrows + no-filter live, strike/reword the spec's keyboard bullet to match. - -*** 2026-07-02 Thu @ 14:57:18 -0400 Ran the live probe; spec bullet reworded to match reality -Zoom ended ~14:45; probe ran per plan (panel in test mode, hyprctl dispatch sendshortcut targeted at the panel address, every key gated on panel focus, Enter never sent). Verdicts: arrows move row focus and Enter rides the ListBox activate binding (TRUE — kept); Esc closes (TRUE — kept); Tab is the plain GTK widget-by-widget chain and inside a list crawls row by row, no section jumps (claim FALSE — struck); type-to-filter does not exist (claim FALSE — struck; typing into the 24-row Saved list filtered nothing). Spec's Keyboard bullet reworded with the live evidence and a note that section-jump Tab or filtering would be new work. Probe gotchas for reuse: AT-SPI list items have empty accessible names, so row identity needs get_index_in_parent(); a killed test panel can leave a windowless single-instance process that eats the next launch via D-Bus activation — pkill -9 -f 'net panel$' and wait before relaunching. -** CANCELLED [#C] Pocketbook development backlog :pocketbook: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-05-26 -:END: -Cancelled with the 2026-07-02 remove-pocketbook decision — the app and its in-tree package are gone. -Pocketbook (GTK4 layer-shell notes panel, toggled via waybar) was pulled from publication 2026-05-26 — github repo + cjennings.net repo deleted, mirror hook removed — and folded into this repo at =pocketbook/= until it's ready to spin back out. Src-layout Python package with pytest tests and a Makefile. Develop it in-tree; the backing modules are =store/note/panel/layer_shell/app/note_widget= + =style.css=. - -Backlog (unordered; promote items to their own dated tasks as they're picked up): - -- Configurable options, possibly a dedicated configuration panel. -- Lose-focus hides pocketbook — configurable on/off. -- Configurable display order: chronological by creation date (asc/desc), manual, alphabetical (asc/desc). -- Search / filter notes. -- Global toggle keybind (Hyprland =bind=) alongside the waybar click; document the waybar integration. -- Note CRUD polish (create/edit/delete) + optional markdown rendering. -- Pin / favorite notes. -- Tags or notebooks / categories. -- Persistence: confirm store format + =~/.local/share/pocketbook/= location, add versioning/migration, decide a backup/sync story. -- Theming: track the dupre/hudson theme system so =style.css= follows =set-theme=. -- Layer-shell geometry config (anchor edge, width, margins) + HiDPI / multi-monitor behavior — ties into [[file:docs/PLAN-per-host-overrides.org][per-host overrides]] scaling work. -- Config file format (toml) + reload-without-restart. -- Expand test coverage (TDD per testing standards; =tests/= already exists). -- Release prep for the eventual spin-back-out: pyproject metadata, version, license. -- Re-wire the archsetup install (gtk4-layer-shell dep + install step + post-install clone) when pocketbook ships. Removed 2026-05-26 — see git history of =archsetup= / =scripts/post-install.sh=. -** CANCELLED [#C] Fn+F9 toggles pocketbook — source unlocated :hyprland:pocketbook: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-23 -:END: -Retired with pocketbook itself (2026-07-02 removal) per this task's own exit condition — with the app uninstalled and unbound, whatever Fn+F9 emitted has nothing to toggle. -On velox, pressing Fn+F9 (physical function key) toggles the pocketbook panel. It shouldn't. Raised from a home-project session 2026-06-23. - -Investigated 2026-06-23 and could not locate the trigger in any config. Ruled out, three ways: -- No F9 bind (bare / $mod / keycode) in the live =hyprland.conf= (now a stow symlink), the velox host tier =conf.d/local.conf=, or the waybar config. -- =hyprctl binds= runtime (all 90 active binds, authoritative) execs pocketbook on ONLY =SUPER+P=. No F9/XF86 path reaches it. The old touchpad toggle that used to sit on =$mod+F9= was moved to =$mod+M=, so F9 is unbound in Hyprland. -- No input remapper (keyd/xremap/input-remapper) and no hotkey daemon (sxhkd/swhkd) running or configured; pocketbook's own source has no F9 / GlobalShortcuts / portal / dbus listener (its GTK ShortcutController binds only Esc/Ctrl-n/Ctrl-j/Ctrl-k/Del/Return). pocketbook is a single-instance Gtk.Application, so any path that re-runs =pocketbook= toggles it. - -Parked at Craig's call (not worth deeper investigation now). If it resurfaces, the one unfinished step is to capture what keysym Fn+F9 actually emits (=wev -f wl_keyboard:key=, press Fn+F9, read the =sym:= / =code:=) and grep for that. Most likely folds into removing pocketbook from the waybar setup — if pocketbook leaves the bar, retire this with it. -** CANCELLED [#C] Waybar emacs-service status + control :feature:waybar: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-24 -:END: -From the roam inbox (2026-06-22): with Emacs integrated into the system as file manager and instant note-taker, make bouncing it trivial. A waybar component showing the emacs service status, with detail on hover, that turns the server on / off / bounce via right-click. Pairs with running the Emacs daemon as a managed systemd user service. - -Cancelled 2026-07-02 per Craig during the task-batch pick: no current need. Re-add or pull back from Resolved if a need surfaces. -** DONE [#C] set-wallpaper detaches waypaper config from its stow symlink :bug:hyprland:quick:solo: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-28 -:END: -=set-wallpaper= persists with =mv "$tmp" "$CONFIG"=, which replaces the =~/.config/waypaper/config.ini= stow symlink with a real file. After the first run the live config is detached from =~/.dotfiles/hyprland/.config/waypaper/config.ini=, so a later =git pull= + restow won't update it and set-wallpaper changes never flow back to the repo. Fix: write in place rather than =mv= over the symlink — e.g. =cp "$tmp" "$CONFIG"= (follows the symlink to the real dotfiles file), or resolve the link target and write there. Lives in =~/.dotfiles/hyprland/.local/bin/set-wallpaper=; it has a test suite, so add a Boundary case for "CONFIG is a symlink". - -Shipped 2026-07-02 (dotfiles d826be4): write-back now redirects through the symlink instead of mv-ing over it; two boundary tests pin the invariant (replace + append paths). velox's live config was still a healthy symlink, so no repair needed. -** DONE [#B] Instrument-console rebuild: net + bluetooth panels :feature:waybar:network:bluetooth:solo: -CLOSED: [2026-07-03 Fri] -:PROPERTIES: -:SPEC_ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5 -:END: -The no-approvals speedrun build of the console design Craig approved through five prototype iterations (2026-07-02/03). Spec: [[file:docs/specs/2026-07-03-instrument-console-panels-spec.org]] — the interactive prototype [[file:docs/prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]] is the normative design reference. Folds three open tasks: network panel redesign, bt switch placement + title, bt rename devices. Code in ~/.dotfiles (net/, bluetooth/, themes/dupre/panel.css). Final step: flip the spec to IMPLEMENTED, write the findings summary to file, finalize session context. - -*** 2026-07-03 Fri @ 03:20:00 -0400 Phase 2 shipped: net GTK-free console layer + engine verbs -Dotfiles =81ec9c3= (TDD, 52 new tests, 581 net green). Pure presenter logic for the single-screen console, no view code touched: =viewmodel.net_faceplate= (state word + lamp + TUNNEL/AIRPLANE badges, wired-link-wins precedence), =network_console_rows= (ethernet pinned, radio-off note, active-then-signal sort, per-row lamp/caption/ladder/forget), =channel_headline= (wired device+speed / SSID+ladder+dBm / not-connected placeholder), =tunnel_console_rows=, dial-meter geometry (=meter_needle_deg= + =meter_scale= 100→1000 auto-relabel), =signal_bars=/=mbps_label=, and =panel.ArmState= (two-click arm-to-fire for forget/disconnect). Engine verbs: =manage.wifi_radio= (nmcli radio wifi on|off), =manage.device_up= (ethernet take-the-route), =sysio.link_speed_mbps= (/sys wired speed), =connections.ethernet_devices=, hidden flag on =manage.add=. - -*** 2026-07-03 Fri @ 06:02:32 -0400 Phases 3+4 shipped: net view rebuilt as the instrument console -Dotfiles =800ef60= (1197+/250-). =gui.py= rewritten as the single-screen console — no tabs, no Blueprint template (the dial meters and arm-to-fire rows are too dynamic, so the tree is built in Python; =pages.py= + the =*.blp/*.ui= are now orphaned, Phase 6 dead-code). Faceplate (lamp/word/TUNNEL+AIRPLANE badges/wifi-radio switch/close), engraved CHANNEL headline, scrolled NETWORKS + TUNNELS lamp rows, CONSOLE keys, two cairo dial meters, output well + dismiss ✕, toast. Interactions all wired: open network joins / secured opens the password dialog, active row arm-disconnects (gold), ✕ arm-forgets (terracotta), tunnel toggles, ethernet row takes/yields the route, radio switch flips the wifi radio (refuses under airplane with the way out), + hidden joins a non-broadcast SSID, DOCTOR streams diagnose+repair into the well, SPEED TEST sweeps both dials with the live rate then pins the final with HOLD (location/ping/final/tips in the well). =panel.css= grew the console classes (lamps+glow, b-face, engrave, chan, lamp-row + arm tints, c-btn, meter/mode/hold, output steps, toast). AT-SPI smoke + driver rewritten (anchor on the DOCTOR key). Phases 3 and 4 landed together because a view-only intermediate is a non-functional panel. Verified live on velox: full render screenshotted, console smoke green (faceplate/keys/sections/tunnels/DOCTOR/dismiss/close), DOCTOR streams real diagnose steps, SPEED TEST drove RX 36.6↓ / TX 90.7↑ then HELD. 581 net tests + full make test green. - -*** 2026-07-03 Fri @ 06:55:00 -0400 Phase 5 shipped: bt panel rebuilt as the instrument console -Bluetooth's turn, two commits mirroring net. Phase-5a (dotfiles =5318b34=, 47 new console tests): the GTK-free layer — =viewmodel.bt_faceplate= (POWERED/OFF/AIRPLANE word + lamp + LOW BATT/AIRPLANE badges), =paired_console_rows= / =nearby_console_rows= (lamp rows with connect/forget/rename affordances), =discoverable_chip=, count labels, =battery_gauges= (two dial slots, one per connected device, red under 15%, dim NO DEVICE / ADAPTER OFF empties), =STEP_NARRATION=, and =panel.ArmState= (the forget latch). Engine gaps: =btctl.set_alias= renames through the bluez D-Bus Alias via busctl (set-alias has no MAC-addressed one-shot; =device_path= discovers the controller node from the object tree), =manage.rename= wraps it with a verify-after read, =parse_info= reads the Alias as the display name (a rename lands there, not on Name; the MAC-shaped placeholder stays "unnamed"), and =doctor= grew =on_report=/=on_begin= callbacks. Phase-5b (dotfiles =66f03d9=): =gui.py= rewritten as the single-screen console — faceplate (lamp/word/LOW BATT+AIRPLANE badges/adapter-power switch/close), engraved ADAPTER line with the clickable discoverable chip, scrolled PAIRED + NEARBY lamp rows, CONSOLE keys DOCTOR / SCAN, two cairo battery dials, output well + toast. Interactions: paired rows toggle connect/disconnect, ✎ renames via a dialog, ✕ arm-forgets, nearby rows run the pair flow into a passkey-confirm dialog, the chip toggles discoverability, the switch powers the adapter, SCAN refreshes nearby, DOCTOR streams checks + repairs. =panel.css= gained =.chip= / =.pen= / =.o-passkey= (the rest already shared with net). AT-SPI smoke rewritten (anchor on the bt-only SCAN key). Verified live on velox: smoke green end to end, screenshot matches the prototype (POWERED faceplate, four paired audio devices, two NO DEVICE battery dials). 46 suites + full make test green. Phase 6 next: live both-panel verify, folded tasks closed, dead code removed, spec → IMPLEMENTED. - -*** 2026-07-03 Fri @ 06:49:45 -0400 Phase 6 shipped: build closed out, dead code removed, spec IMPLEMENTED -Live both-panel verify on velox: 46 suites + full make test green, and both AT-SPI smokes green end to end (net: faceplate NET·01/ONLINE, DOCTOR streams real diagnose steps, tunnels rows, close; bt: BT·01/POWERED, SCAN/DOCTOR keys, battery dials, close). The two =gui.py= files are byte-identical to their screenshot-verified commits (net =800ef60=, bt =66f03d9=), so the render carries over from the phase-3/4/5 screenshots — this pass touched no view code. Dead code removed (dotfiles =f4e688e=): both panels' orphaned =pages.py= + =ui/= (=*.blp/*.ui=) gone now that =gui.py= builds the tree in Python, the now-dead =make ui= Blueprint-compile target and its =.PHONY= entry dropped, and the stale =gui.py / pages.py= mention in bt =viewmodel.py= fixed; nothing imported the removed modules. Three folded tasks close with this build (network panel redesign, bt switch placement + title, bt rename devices). Build summary written to [[file:assets/2026-07-03-instrument-console-panels-build-summary.org][assets/2026-07-03-instrument-console-panels-build-summary.org]]. Spec =e73877f5= flipped DOING → IMPLEMENTED. Manual-test checklist for the real-device bt interactions filed under Manual testing and validation. -** DONE [#B] Net diagnostics: narrate every step :feature:network:solo: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-02 -:END: -Follow-on 2 (dotfiles =ebf24fe=, Craig's decision 2026-07-02, option 1 of the discussed policies): mutating tiers pre-check whether we're online before acting. dns-test/dns-override short-circuit with an "already online" step and touch nothing (live-verified on hotel wifi: 100 ms skip, DNS untouched); reset/bounce/nm-restart/resolved-restart proceed (reset has a legit online use — fresh MAC) but carry "(was online before ...)" in their evidence; the panel's repair confirm warns via the cached probe verdict (=probe.cached_online=, file read only); rfkill/dns-revert/tunnel-down already verify their own state, unchanged. 492 net tests / 45 suites / panel smoke green. - -Follow-on (dotfiles =50a7239=, Craig's ask 2026-07-02): the same requirements now cover every Advanced-dropdown action — all repair ids + portal narrate in the panel's step rows, =net repair= / =net portal= human-by-default (=--json= kept, added to portal), doctor's attempts render like checks, and mutating steps keep their next_action on pass (the fail/warn-only rule was dropping dns-override's revert pointer, portal-login's login click, and cleanup-unverified's manual revert). 479 net tests / 45 suites green; panel smoke shows narration in live rows. - -Shipped (dotfiles =7772427=): every diagnose step id carries a one-line narration (what it tests and why) in a viewmodel table, and both human renderers print every step — status, title, narration, evidence with timing, and the fix pointer on fail/warn — in =net doctor= and =net diagnose= alike. Bare =net diagnose= now prints the narrated report (was raw JSON; =--json= keeps the machine envelope; =net-fix= already used =--json= explicitly, the panel calls the engine in-process). A completeness test walks diag.py's step ids against the narration table so a new step can't land unnarrated. 470 net tests / 45 suites green; verified live on velox hotel wifi — both commands narrate the full probe sequence. Ratio picks it up with its queued dotfiles pull (source-imported, no restow-only step). - -Original ask (roam inbox, 2026-07-02, from a real net doctor run on hotel wifi): the output isn't enough to know what's being tested, why, and whether it passed — a failing run printed only the verdict, the fix pointer, and the failing rows: - -#+begin_example -% net doctor -net doctor: fixable - DNS not resolving - -> net repair dns-test - diagnose: - fail: DNS resolution — no resolution (portal may be stalling DNS) - fail: Internet — link up but no clean internet (DNS or egress issue) -#+end_example -** DONE [#B] Network panel: identify tunnel backends + richer connection info :feature:waybar:network: -CLOSED: [2026-07-02 Thu] -Shipped (dotfiles =405235f=). Identification: every Tunnels row's caption now leads with its backend — "tailscale", "WireGuard (NetworkManager)", "openvpn (NetworkManager)" (from the profile's =vpn.service-type=, resolved on the panel path only), "Proton VPN CLI" — via =viewmodel.tunnel_kind_label=. Found and fixed a real gap: NM vpn-type profiles (openvpn etc.) weren't listed at all, only wireguard type. Active tunnels now carry their device's IP4 address. Info page: the active connection's live subtitle gains IP, gateway, and DNS via =build_status(full=True)= (panel poll only — the bar's one-nmcli hot path is untouched). Live-verified on velox: all 9 tunnel rows correctly labeled (tailscale w/ tailnet + peers, 7 WireGuard NM profiles, Proton CLI), live subtitle shows IP/gw/DNS on hotel wifi. 523 net tests / 45 suites / panel smoke green. - -Craig's ask (roam inbox, 2026-07-02): the Tunnels rows all look alike — no way to tell which is tailscale, which is an NM wireguard/openvpn profile, and which is proton CLI without prior knowledge (e.g. when you want to bounce tailscale specifically). Second half: improve the stats under each connection — the panel is effectively a connection's info page. -** DONE [#B] Timer: alarm am/pm input silently fails :bug:waybar:solo: -CLOSED: [2026-07-02 Thu] -Fixed (dotfiles =8dd36c4=). Two root causes: =parse_alarm= only accepted 24h =HH:MM=, and =cmd_new= suppressed the ValueError, so any 12h input silently created nothing. Now accepts 24h (=14:30=, bare =14=) and all common 12h shapes (=2:30pm=, =2:30 PM=, =7:15p=, =7p=; any case, optional space, bare a/p; 12am = midnight), and input that still doesn't parse fires a fail notification instead of vanishing. 107 wtimer tests green (10 new parse cases + notify-on-error CLI tests). Manual test filed (live dialog run). - -Craig's report (roam inbox, 2026-07-02): when setting an alarm, entering am or pm in any fashion makes the timer silently fail. It should accept 24h and 12h variants — capitalization, spaces, bare "a"/"p" — all common forms. -** DONE [#B] Timer: escape doesn't cancel the dialog flow :bug:waybar:solo: -CLOSED: [2026-07-02 Thu] -Fixed (dotfiles =8dd36c4=). Root cause: =_fuzzel= ignored fuzzel's exit code, so Escape (fuzzel exits 2 on a dmenu abort — confirmed in its changelog) returned "" and the flow fed it onward to the next prompt. =_fuzzel= now returns None on any non-zero exit and =cmd_new= aborts the whole flow on None at any step (type, duration/alarm, label). Escape-at-each-step covered by CLI tests against a fake fuzzel exiting 2. Manual test filed (real keyboard Escape). - -Craig's report (roam inbox, 2026-07-02): hitting cancel via escape at the step after choosing "timer" does nothing but proceed to the next step — likely the same for the other dialog steps. -** DONE [#B] Network panel: stream speedtest results live :feature:waybar:network:solo: -CLOSED: [2026-07-02 Thu] -FIX-UP (dotfiles =60707be=, 2026-07-03, caught by Craig): the first shipped version didn't actually stream — speedtest-go buffers all phase lines to process exit when piped (per-line arrival timestamps proved it: 25s of silence, then everything at once; the original "live" verification never checked arrival timing). The stream now runs the binary under a pty, where terminal mode redraws continuously: in-flight rates tick (download climbing like a speedometer), ANSI/spinner noise is stripped, and on_update fires per changed value. CLI closes with a "final:" settled-numbers line. Re-verified with timestamps (server +1s, ping +2s, download first tick +4s, upload +19s, final +29s) AND an AT-SPI probe of the live panel that sampled the results box mid-run: ping filled at 4s, download ticking at 12s, upload at 24s, final rows + conditioned tips at the end. 529 net tests / 45 suites green. - -Shipped (dotfiles =38171e8=). =run_speedtest_stream= runs speedtest-go's plain mode, whose lines land one per completed phase (parser written against a real captured hotel-wifi run). Panel: a checklist fills in as ping → download → upload arrive, final rows at the end. =net speedtest= streams the same lines at the terminal (=--json= keeps the one-shot envelope). Bonus from the text mode: jitter (rides the Ping row) and packet loss (own row, warns >1%) — the JSON mode never reported either. The static Tip is gone; =speedtest_tips= derives guidance from the numbers (high ping >100ms, download < half of upload, <10 Mbps both ways, loss >1%), each tip naming its trigger values — that's the answer to Craig's "what criteria" question: the old tip had none, the new ones are stated rules. 509 net tests / 45 suites green; live CLI run streamed correctly and fired the asymmetric-download tip on real numbers (33 down / 76 up). Manual test filed for the in-panel run. - -Craig's ask (roam inbox, 2026-07-02): the speedtest only shows results at the end; typical speedtest UIs report the numbers as they come in. Stream the CLI's progress into the results box as it arrives, then the final numbers at the end. Screenshot: ~/pictures/screenshots/2026-07-02_225441.png. -** DONE [#C] Bluetooth bar icon: gray instead of the bar's white :bug:waybar:bluetooth:solo: -CLOSED: [2026-07-03 Fri] -Fixed (dotfiles =27d8eda=). Root cause: the =on= state sat in the css dim group with off/absent/degraded (a deliberate "idle dims" choice that read as broken). Removed =.on= from the dim rule in all three css copies (dupre, hudson, live style.css — the theme-drift guard suite pins them together); indicator docstring updated. Live-verified: SIGUSR2 css reload, bar screenshot shows the bt glyph in the bar's resting white alongside battery/text. - -Craig's report (roam inbox, 2026-07-03): the bluetooth waybar icon renders gray, not the same white as the other bar module icons. -** DONE [#B] Bluetooth panel: close button like the net panel :feature:waybar:bluetooth:solo: -CLOSED: [2026-07-03 Fri] -Shipped (dotfiles =42c93d6=): a flat circular Close button right of the tab switcher (accessible "Close" label, "Close (Esc)" tooltip), wired to window.close. The bt smoke asserts it exists AND that clicking it exits the panel (run green live). Plot twist answered in-session: the net panel had no close button either — Craig's leaner-chrome pass removed it 2026-07-01 (787b475) on the Esc-suffices theory; he asked where it went, so it was restored with the same tab-row button (=6a0aff7=, net smoke extended the same way). Both panels match again. - -Craig's ask (roam inbox, 2026-07-03): the bt panel needs a close button matching the network panel's. -** DONE [#B] Bluetooth panel: switch placement + panel title :feature:waybar:bluetooth:solo: -CLOSED: [2026-07-03 Fri] -Delivered by the instrument-console rebuild (spec e73877f5, phase 5). The adapter-power switch now sits on the faceplate above every console key, and the engraved ADAPTER line is the panel's title row with the clickable discoverable chip right-justified on it. - -Craig's ask (roam inbox, 2026-07-02): move the bluetooth on/off switch above all the buttons. "Bluetooth" becomes the panel's title, with the on/off switch right-justified on that title row. Panel code in ~/.dotfiles =bluetooth/= (GTK4 + Blueprint, phase-2 PanelModel/presenter — see the shipped panel task in Resolved). Presenter tests + the AT-SPI smoke likely need their layout assertions updated. -** DONE [#B] Bluetooth panel: rename devices :feature:waybar:bluetooth:solo: -CLOSED: [2026-07-03 Fri] -Delivered by the instrument-console rebuild (spec e73877f5, phase 5). =btctl.set_alias= renames through the bluez D-Bus Alias via busctl (no MAC-addressed one-shot exists; =device_path= finds the controller node from the object tree), =manage.rename= wraps it with a verify-after read, and =parse_info= reads the Alias as the display name. Each paired row carries a ✎ affordance opening a rename dialog. Live-probed the mechanism on velox before wiring it (rename + restore verified on the M650). - -Craig's ask (roam inbox, 2026-07-02): the panel should be able to rename a device. bluez supports per-device aliases (=bluetoothctl= device menu =set-alias=; the one-shot invocation shape needs verifying at the btctl boundary). Wire it through the engine (=bluetooth/src/bt/=) with a verify-after read, and surface a rename affordance on the device row consistent with the panel's existing patterns. -** DONE [#B] Network panel: other network interfaces (tailscale, VPNs, wireguard) :feature:waybar:network: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:SPEC_ID: 79a1075a-4b56-4f25-a861-b69f120a636a -:END: -Spec: [[file:docs/specs/2026-07-02-net-panel-other-interfaces-spec.org]] (DOING — reviewed READY and decomposed 2026-07-02 evening; all four decisions were resolved same morning, claims re-verified live at review: protonvpn binary, tailscale JSON shape, seven importable wireguard configs). - -Tunnels visible and controllable in the net panel: tailscale + NM wireguard + proton-vpn-cli probes, a Tunnels group in Connections, diagnose/doctor route-ownership awareness, a bar badge when a tunnel owns the default route, archsetup operator flag + package swap, and the one-time NM import of the seven Proton configs. Origin: roam inbox capture 2026-07-02. - -*** 2026-07-02 Thu @ 18:47:05 -0400 Shipped phase 1 — overlay probes (dotfiles 2d9d060) -=net/src/net/overlays.py=: one probe per backend, shared row shape ={kind, name, state, addr, detail, can_toggle}=. tailscale parses =status --json= (up/down/needs-login/stopped, tailnet + N/M peers online + exit node detail, first TailscaleIP); wireguard rows filter =nmcli connection show= by type with uuids for the existing up/down wrappers; proton drives the official CLI — ground truth sampled live before writing the parser: the GUI-running refusal prints to stdout and EXITS 0 (text-detected, =can_toggle false=), disconnected = "Status: Disconnected", and the CLI's account store is separate from the GTK app's (=protonvpn info= → Account 'None' — sign-in is a phase 6 migration step for Craig). =net status= gained a fast-path overlays section (tailscale + wireguard only; the python CLI's ~300ms startup stays out of the indicator poll, and an active proton tunnel surfaces as its NM wireguard row anyway), guarded so a probe crash yields =[]= not a dead indicator. 19 new tests over fake-tailscale/fake-protonvpn/fake-nmcli (45 suites green); live check on velox: tailscale row up, 5/6 peers, hot path 149ms. proton-vpn-cli 1.0.1 installed on velox (GTK app stays until phase 5). - -*** 2026-07-02 Thu @ 19:02:45 -0400 Shipped phase 2 — panel Tunnels sub-view (dotfiles 21db05a) -Connections gained a third sub-view (Available | Saved | Tunnels — a StackSwitcher page, the natural landing for the spec's "fourth group" in this UI): rows from =overlays.collect(fast=False)= with the vpn glyph, name, and a =tunnel_caption= (state · addr · backend detail); one primary button follows the selected row via =PanelModel.tunnel_primary()= — Bring Up/Bring Down when toggleable, disabled explainers for needs-login ("Sign in first: tailscale up") and the Proton GUI-running case. =manage.tunnel_up/down= dispatch by kind (wireguard rides the existing nmcli up envelope + =connection down=; tailscale/protonvpn shell their tools into a =_tool_result= envelope carrying stderr on failure); ops run on the worker thread, rows + bar reload on land. gui grew =refresh_tunnels()= (bg, full probe set) kicked from the list load. AT-SPI smoke extended (Tunnels tab, action button, rows — POLLING for the bg load; a fixed sleep raced it and false-failed). 22 new tests (45 suites green). LIVE on velox: smoke fully green, rows eyeballed in dupre (tailscale up caption with peers count; proton app-running row), =tailscale set --operator=cjennings= applied and the user-mode =tailscale down/up= round-trip verified (Self.Online back true). Gotcha reconfirmed: stray test panels leave a windowless single-instance process — =pkill -9 -f '[n]et panel'= + wait before relaunch. - -*** 2026-07-02 Thu @ 19:11:47 -0400 Shipped phase 3 — diagnose/doctor tunnel awareness (dotfiles 31ba056) -=overlays.default_route_owner()= classifies the default route's owner (tailscale prefix, wg/pvpn/proton/tun/tap prefixes, else the active NM connection's type — imports can name a wireguard device anything). diag's route step went three-way: overlay owner = informational pass row ("internet flows through the tailscale tunnel tailscale0"), other physical link = the old multi-homing warn. When the HTTP probe fails while a tunnel owns the route, a new "tunnel" edge row LEADS the evidence and the classifier returns fixable/action tunnel-down (the deferred-vpn verdict is retired — it was look-don't-touch, and it never caught tailscale at all since NM lists it unmanaged; an NM VPN that doesn't own the route now falls through to normal classification instead of being blamed). =repair_tunnel_down= dispatches by owner (tailscale CLI / protonvpn CLI for pvpn-named devs / nmcli connection down via active-connection lookup), verifies route ownership actually moved, and registered in ACTIONS so Get Me Online drives it. fake-ip gained FAKE_IP_DEFAULT_DEV_SEQ (head-first line consume, the UP_RC_SEQ idiom) so tests watch the owner change across the verify. 11 new tests, 2 old deferred-vpn pins rewritten to the new contract; 45 suites green; live read-only diagnose on velox clean (wlan owns the route — no tunnel rows, as designed). - -*** 2026-07-02 Thu @ 19:14:58 -0400 Shipped phase 4 — bar tunnel badge (dotfiles b4010bf) -=net status= carries =tunnel_route= ({dev, kind} via =overlays.default_route_owner=, exception-guarded like the overlays list, present on the no-device path too). The indicator appends a small nf-md-vpn badge after the state glyph, emits =["<state>", "tunnel"]= as a waybar class list (string class unchanged when no tunnel), and the tooltip names the owner ("Tunnel: default route via tailscale0 (tailscale)"). No css edit — presence is the signal, themes can hook the class later, and the waybar/style.css drift test stays untouched. 4 new tests; StatusHarness gained fake-ip so the machine's real route can't leak into assertions (462 net tests, 45 suites green). Live payload on velox verified badge-free (wlp170s0 owns the route — correct); a badge render awaits the first real tunnel-owned route (phase 6's wg import or a tailscale exit node). - -*** 2026-07-02 Thu @ 21:56:00 -0400 Shipped phase 5 — installer proton CLI swap + tailscale operator (archsetup 0389790); GTK app retired live on velox -The feat commit landed at 19:16 (the session died before this close-out): installer enables tailscaled with =--now= and grants =tailscale set --operator= to the primary user (brief retry while the daemon's socket comes up), proton-vpn-cli replaces proton-vpn-gtk-app, VM asserts the vpn stack + the retirement + the OperatorUser pref (format verified against a live daemon). Live velox application finished 21:55: the =protonvpn-app --start-minimized= exec-once removed (dotfiles b5c8442 — nothing replaces it, the CLI is on-demand from the panel), the running app killed, =pacman -Rns proton-vpn-gtk-app= (proton-vpn-daemon stays — separate package the CLI uses). CLI verified unblocked: =protonvpn status= → "Status: Disconnected", =protonvpn info= → Account 'None' (sign-in is Craig's step, filed under Manual testing and validation). - -*** 2026-07-02 Thu @ 21:57:00 -0400 Shipped phase 6 — wireguard import script + velox migration (scripts/import-wireguard-configs.sh) -The script stages each config through a =wgpvpn.conf= temp copy (NM's import name must be a valid <=15-char interface name; several config names are longer), renames by the UUID parsed from the import output (never by the transient name, so a stray same-named connection can't be hit), forces =autoconnect no= (full-tunnel AllowedIPs 0.0.0.0/0 must not arm itself at boot), skips already-imported names, and refuses to run past a stale =wgpvpn= connection (an earlier run that died between import and rename — it still has autoconnect on). =tests/import-wireguard-configs/=: 10 cases over a fake nmcli; writing them caught a real bug (under =set -e= the grep-for-UUID pipeline aborted before the error message printed). shellcheck clean; 11 unit suites green. Velox migration verified: the crashed session had already run the import, so tonight's run exercised the skip path live — all 7 connections confirmed wireguard type, autoconnect no, iface wgpvpn, no stale leftovers; =net status= overlays show tailscale + all 7 rows. Ratio runs the script on its trip (rides the archsetup pull). - -*** 2026-07-02 Thu @ 21:58:00 -0400 Test surface complete across the phases -Probe suites over fake tailscale/nmcli/protonvpn (19, phase 1), panel-model Tunnels coverage (22, phase 2), diag overlay-ownership cases (11, phase 3), badge suite (4, phase 4) — all in dotfiles; VM assertions for phase 5 in archsetup 0389790; the import-script suite (10, phase 6) closes the set. -** CANCELLED [#B] File-manager swallow pattern :feature:hyprland: -CLOSED: [2026-07-02 Thu] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-02 -:END: -Reassigned to .emacs.d 2026-07-02 (handoff: =~/.emacs.d/inbox/2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff.org=). The "file manager" is the dirvish popup (Super+F, an Emacs frame), not nautilus — so the fix is elisp in dirvish's external-open path (=cj/xdg-open=): spawn the handler directly with =start-process=, hide the popup frame, restore it from the process sentinel, notify on non-zero exit. The spec drafted here first ([[file:docs/specs/2026-07-02-file-manager-swallow-spec.org]], now CANCELLED) records the feasibility finding that stays useful: gio/xdg-open launches double-fork, so no PID-ancestry approach (Hyprland native swallow included) can ever connect viewer to launcher. - -When the file manager launches another app, it should hide to a special workspace (the "swallow" pattern) and return when that process ends, rather than vanishing. Today it disappears with no signal of whether it's coming back, so the user can't tell success from failure — they should quit explicitly instead. Origin: roam inbox capture. - -*** 2026-07-02 Thu @ 22:20:00 -0400 Feasibility ground truth: Hyprland native swallow ruled out -=misc:enable_swallow= would be the whole feature in two config lines, but it matches by PID ancestry, and nautilus's launch path (GLib =g_app_info_launch_default_for_uri=) orphans the handler — reproduced live on velox with a python-gi launcher: feh came up with PPID 1 while the launcher was still running. The spec's design is therefore an event-listener daemon (socket2 =openwindow=/=closewindow= while nautilus is active), the touchpad-auto shape. Handlers sampled: pdf → zathura, image → feh (X11 — flagged as a side task), video → mpv, text → emacsclient (exempt candidate, decision 2). -** DONE [#C] Open meeting links in the browser instead of the Zoom app :feature: -CLOSED: [2026-07-02 Thu] -Shipped 2026-07-02, mechanism per Craig ("the Linux zoom app is really terrible — one less dependency"): a =zoommtg://= URL handler, and the native app retired outright. =zoom-web= (dotfiles 187414a, 10 tests) registers as the xdg default for x-scheme-handler/zoommtg via =zoom-web.desktop=; Zoom's launch-page bounce rewrites deterministically to =https://<host>/wc/join/<confno>?pwd=…= in the default browser (subdomain hosts preserved, tracking params dropped, start action mapped, malformed URIs notify + exit 2). The registration landed in the stowed mimeapps.list, so it ships with dotfiles. Zoom uninstalled from velox (=pacman -Rns=), its windowrules removed from hyprland.conf, =aur_install zoom= dropped from archsetup, and the VM retired-package assertion now covers blueman + zoom. Known limit, accepted: a host who disabled join-from-browser blocks the web client — that meeting needs the native app installed ad hoc. Ratio trip: =pacman -Rns zoom= + the pull brings the handler; run =xdg-mime default zoom-web.desktop x-scheme-handler/zoommtg= if the stowed mimeapps.list doesn't take effect. -** DONE [#B] Network panel redesign — no terminals, verify-everything, full failure coverage :feature:waybar:network: -CLOSED: [2026-07-03 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-02 -:END: -Delivered by the instrument-console rebuild (spec e73877f5). The three locked decisions all landed: no terminals (the single-screen console renders every action and result in the output well — net-popup is gone), the passwordless privileged path (the net-priv helper + narrow NOPASSWD sudoers, shipped earlier and carried forward), and verify-every-action (arm-to-fire mutations plus doctor's re-probe). The failure-mode catalog below is the diagnose/repair contract, built out across the net-diagnostics tasks and this rebuild's DOCTOR path; the catalog stays here as the standing completeness reference for that path. - -Major evolution of the shipped =custom/net= module ([[file:docs/design/2026-06-29-waybar-network-module-spec.org]]). -Reverses the spec's "privileged tiers run in a net-popup terminal" decision. Origin: -design conversation 2026-06-30. - -*** Locked decisions -- *No terminals anywhere in the module.* Delete =net-popup= entirely. Every action and - every result renders in the panel. -- *Passwordless privileged path (the enabler).* A single root-owned helper runs net's - specific privileged commands (rfkill unblock, nmcli modify/up, networking off/on, - systemctl restart NetworkManager/systemd-resolved, resolvectl dns/revert, DoT toggle), - installed by archsetup with a narrow NOPASSWD sudoers rule scoped to that helper only - (never blanket mv/systemctl). =repair.py= calls =sudo <helper> <verb>=. This supersedes - and absorbs the earlier [#C] "Passwordless DoT toggle" follow-up. Without it an in-panel - worker thread can't prompt for a password, so this gates the whole no-terminal goal. -- *Verify every action.* Every mutating op confirms its effect before reporting success - (doctor already re-probes; generalize so each repair, connect, forget, add, and DNS - override re-checks and surfaces pass/fail in the panel). -- *Detect + respond to every failure mode below* (auto-fix where we can, else report the - helpful text), including the edge cases. - -*** Navigation (confirmed) -- Top tabs: =Connections= | =Diagnostics= | =Performance=. -- Connections: saved + in-range list, connect / add / forget. -- Diagnostics: sub-row =Diagnose= | =Get Me Online= | =Advanced=; shared area below shows - diagnose items AND streams repair progress (replacing the terminal). =Advanced= reveals - the individual repair buttons, renamed with tooltips describing each. -- Performance: Speedtest (+ live throughput later). - -*** Failure-mode catalog — detect / correct-or-report (the completeness backbone) -Organized by the connectivity stack, bottom-up. "Fix" = auto-correct + verify; "Report" = -the in-panel text when there's no safe auto-fix. Audit this list for completeness; it is the -contract for what diagnose must detect and what the panel must say. - -**** Radio / hardware -- rfkill soft block — Detect: rfkill soft. Fix: unblock + =nmcli radio wifi on=, verify radio unblocked. -- rfkill hard block — Detect: rfkill hard. Report: "WiFi is off at the hardware switch — flip the physical switch or Fn key." -- No WiFi adapter present — Detect: no wifi device in nmcli + rfkill absent. Report: "No WiFi adapter detected — use ethernet, or check the driver (dmesg | grep firmware)." -- Driver/firmware not loaded — Detect: device present but errored / no operational state. Report: "WiFi driver or firmware didn't load — check dmesg for the adapter." -- USB WiFi adapter unplugged — Detect: device disappeared since last scan. Report: "WiFi adapter was removed — reconnect it." -- Airplane mode on — Detect: airplane state file set. Fix: offer toggle off (Super+Shift+A), verify radios back. - -**** Association (L2 link) -- Not connected / disconnected — Detect: link down, device disconnected. Fix: reset (reconnect saved), verify link up. -- Stuck "connecting" — Detect: device state connecting > budget. Fix: reset, verify; if it persists Report: "Stuck connecting to <ssid> — the AP may be rejecting us." -- Weak signal / high loss — Detect: associated but signal below threshold (dBm) or heavy packet loss. Report: "Signal is weak (<dBm>) — move closer to the access point." -- Saved network not in range — Detect: profile active target not in scan. Report: "<ssid> isn't in range here." -- AP roaming flap — Detect: BSSID bouncing. Report: "Connection is unstable — switching between access points." - -**** Authentication -- Wrong WPA password / missing secret — Detect: NM state 120 (snapshot; live detection is a known limit). Report + in-panel re-enter: "Saved password for <ssid> was rejected — re-enter it." -- Enterprise / 802.1X cert or identity failure — Detect: 802.1X profile + activation failure. Report: "Enterprise auth failed — check the certificate or identity (edit the profile)." -- Randomized MAC rejected by AP — Detect: reset-with-random-MAC fails where a prior connect worked. Fix: retry reset with the permanent MAC, verify; else Report. -- WPA3/SAE incompatibility — Detect: SAE key-mgmt + association failure. Report: "This network needs WPA3 and the adapter or profile may not support it." - -**** IP / DHCP -- No IPv4 lease (DHCP timeout) — Detect: connected, no IP4.ADDRESS. Fix: reset → bounce, verify lease. -- APIPA / link-local only (169.254.x) — Detect: only a link-local IPv4. Fix: reset/bounce, verify real lease; else Report: "DHCP server didn't answer — switch network." -- IPv6-only network (no IPv4 by design) — Detect: no IPv4 but IPv6 address + online via v6. Report (not a failure): "Online over IPv6 (no IPv4 here)." Requires making diagnose IPv6-aware. -- IP but no gateway — Detect: IP4.ADDRESS present, IP4.GATEWAY empty. Fix: bounce, verify gateway; else Report. -- Duplicate IP / ARP conflict — Detect: kernel ARP-conflict signal. Report: "Another device is using our IP address — reconnect to get a new lease." (edge) - -**** Gateway (L3 local) -- Gateway unreachable — Detect: no route out, gateway no ICMP. Fix: try one bounce (renew route), verify online; else Report: "No route to the gateway — switch network." (closes the spec/code gap where bounce was never tried) - -**** DNS -- No resolver configured — Detect: IP4.DNS empty. Fix: bounce to re-pull DHCP DNS, verify; else Report. -- Venue DNS broken, public DNS works — Detect: name fails to resolve but 1.1.1.1 resolves (dns-test). Fix: set a PERSISTENT resolver override (1.1.1.1 / 9.9.9.9), verify resolution + online, offer revert. (closes gap #1 — today dns-test reverts and misreports as upstream.) -- DNS hijack (resolves to gateway / private IP) — Detect: classify_resolution hijack. Treat as captive → portal-login flow. -- DNSSEC validation failure — Detect: resolution fails with SERVFAIL where public resolver succeeds without DNSSEC. Report: "DNS security checks are failing on this network." (edge) -- Encrypted DNS (DoT/DoH) hiding the portal — Detect: captive suspected + DoT on. Fix: portal-login drops DoT, opens portal, auto-restores. (existing) - -**** Egress / internet -- Upstream / AP outage (no uplink) — Detect: link/IP/DNS fine, http-probe fail, not a redirect. Report: "This network has no internet — switch network or contact the venue." -- Captive portal (redirect) — Detect: probe redirected. Fix: portal-login opens the page; verify online after login. -- Captive blocked pre-auth (no portal URL) — Detect: probe blocked, no URL. Fix: fresh MAC + open trigger; verify. -- Proxy-required network — Detect: probe fails but a PAC/proxy is advertised (WPAD/env). Report: "This network requires a proxy — configure it in settings." (edge) -- MTU / MSS blackhole (PMTUD broken) — Detect: small probe ok, large transfer hangs. Fix: lower the interface MTU, verify; else Report. (edge) -- Clock skew breaking TLS — Detect: HTTPS/portal fails with cert-time errors + system clock far off. Fix: trigger a time sync, verify; else Report: "System clock is wrong — fix the date/time." (edge) - -**** Routing / multi-homing -- VPN owns the route, no internet through it — Detect: VPN device connected + http-probe fail. Report: "Internet is routed through a VPN (<dev>) — check the VPN, not WiFi." -- VPN up but dead — Detect: VPN device up, no traffic/handshake. Report: "The VPN is connected but not passing traffic." (Phase 5 territory) -- WiFi + tether/ethernet both active — Detect: which iface owns the default route + whether the system is online by any path. Report: "You're online through <other iface>; WiFi itself has no internet," or let the user pick. (closes gap #4) - -**** Infrastructure / system -- Wedged NetworkManager — Detect: nmcli fails / API unresponsive. Fix: restart NetworkManager (bounce escalation), verify. -- NetworkManager not running — Detect: service inactive. Fix: start it, verify; else Report. -- systemd-resolved down — Detect: resolved inactive / DNS via it fails. Fix: restart, verify. -- resolv.conf not resolved-managed — Detect: /etc/resolv.conf not the resolved stub. Report: "DNS isn't managed by systemd-resolved — manual resolv.conf in play." (edge) - -**** Tooling / environment -- nmcli / NM API unavailable — Detect: nmcli error or timeout. Report: "Can't reach NetworkManager — is it installed and running?" -- Slow / hung tool — Detect: step exceeds budget. Fix: degrade that step, retry within budget. -- Stale / corrupt cache — Detect: schema/age mismatch. Fix: self-heal (atomic write + invalidation). -- Missing speedtest backend — Detect: speedtest-go absent. Report: "Install speedtest-go to run a speed test." -- Privileged op fails (helper missing / sudo declined) — Detect: helper exits non-zero or absent. Report: "Couldn't get admin rights for this repair — <install/fix the helper>." - -*** 2026-07-01 Wed @ 13:02 -0400 net-priv helper landed (V2.1) -Craig's call: stowed (not root-owned), low security on locked-down single-user machines. -Shipped =net.priv= module + stowed =net-priv= bin (dotfiles =00aac1e=): a fixed 12-verb set -(rfkill/radio/mac-random/conn-up/net-off/net-on/restart-nm/dns-set/dns-revert/restart-resolved/ -dot-disable/dot-enable) with per-arg validation (uuid/iface/ipv4/resolved.conf.d-path, injection -rejected). =repair.py= now routes every privileged op through =priv.run(verb)= in-process instead -of scattered inline sudo — which also fixes the detached DoT-restore watcher (runs privileged ops -with no tty) and closes the gap where rfkill repair ran unprivileged. 244 net + 33 dotfiles suites -green. NO new sudoers needed: archsetup already grants =%<user> ALL=(ALL) NOPASSWD: ALL= -(archsetup:1089), so every build's primary user already runs net-priv's commands passwordless; -"replicate in archsetup" is already satisfied. net-priv rides =make stow hyprland=; hand-linked on -velox. The velox DoT-path reconcile (whether velox should run DoT at all) stays open — folded into -the deeper reconcile, low priority since the guard makes it a no-op. -*** 2026-07-01 Wed @ 14:05:47 -0400 Shipped V2.2 — merged Diagnostics panel + nav restructure, no terminals -Built the V2 panel (dotfiles =75ed825=, pushed): three top tabs Connections | -Diagnostics | Performance; Diagnostics merges the old Diagnose + Repair pages into a -sub-row (Diagnose | Get me online | Advanced) over a shared area that shows diagnose -rows AND streams repair progress in-panel. net-popup deleted entirely; repairs run on -a worker thread through net-priv (no tty). doctor grew an =on_step= callback so Get me -online streams each escalation step live. Connections groups Saved / Available now / -Wired with a golden group header and joins from a row (=join_plan= auth matrix + -=manage.join= one-step connect, secret to NM only); the Add modal became the hidden-network -affordance. Every diagnose/repair/speed run offers a Copy/Open redacted report -(=report.py=, MAC/IP scrubbed). Waybar visual contract applied (dark capsule, golden -border, monospace) via a CssProvider. =net-fix= opens the panel on Diagnostics instead of -a terminal; middle-click runs =net portal= directly. TDD: 34 new GTK-free tests (grouping, -join_plan, join, report, on_step, eventlog.tail); 278 net + 33 dotfiles suites green. -Live-verified: AT-SPI panel_smoke passes end-to-end + screenshots confirm both pages and -the visual contract. DAILY-DRIVER: waybar config + net-fix are stow symlinks (live on -disk); ratio needs =git pull= + waybar restart; velox waybar picks up on next restart. -**** 2026-06-30 Tue @ 17:36 -0400 Dispositioned the 4th-review findings into the spec -Codex's 9 fourth-review findings (8 accept, 1 modify) are folded into the spec's -"V2 panel UX — the target design" section (cookie [40/40]): single nav target, -saved-vs-available groups, join-from-row instead of Add, the auth-class join matrix, -progressive loading, future-tense + verified Forget, a findable redacted diagnostics -report, the Waybar visual contract, and a lightweight inline latency probe (full speed -test stays under Performance per decision 19). The V2 build below implements that -design: [[file:docs/design/2026-06-29-waybar-network-module-spec.org::*V2 panel UX][V2 panel UX]]. -*** 2026-07-01 Wed @ 22:01:38 -0400 Made diagnose IPv6-aware and multi-homing-aware (dotfiles c0d48e2) -IPv6-only networks pass the DHCP step ("IPv6 only: <addr>") with the v6 gateway standing in for the ping; a bare fe80:: doesn't count. A new route step fires only under multi-homing and names the interface that owns the default route (tether/ethernet/VPN). Also landed the adjacent IP-layer detects: APIPA 169.254 fails DHCP with a link-local explanation, address-without-gateway fails the gateway step as a bad DHCP answer, and a weak wifi signal (below fair) warns on the link step with the dBm. fake-nmcli grew IP6.* and a fake ip(8) serves the JSON route reads. TDD, 33 suites green. - -*** TODO Close every detect/correct gap in the catalog, with post-action verification -**** 2026-07-01 Wed @ 22:41:51 -0400 Closed the feasible edge rows (dotfiles d096b30, 241744b, fafefb6) -Three grouped commits, all TDD. Services/radio: dead NetworkManager and dead systemd-resolved get their own diagnose steps and verified restart repairs (resolved only when resolv.conf is resolved-managed; hand-managed DNS gets a heads-up row), airplane mode fails the link by name and classifies needs-user-action ahead of rfkill, and a missing WiFi adapter is named with the dmesg pointer. Association/auth: reset retries once with the permanent MAC when the randomized one is rejected (new mac-permanent net-priv verb), SAE/WPA3 activation failures classify sae-incompat, and stuck-connecting classifies fixable/reset. Egress edges (run only on an existing failure): DNSSEC validation failure named via resolvectl, clock skew off the probe's Date header, MTU/PMTUD blackhole via df-bit pings, and proxy detection (env vars or an advertised WPAD name). Deferred as infeasible without state the engine doesn't keep: AP roaming flap (needs BSSID history), duplicate-IP/ARP conflict (needs the kernel log), and the USB-unplug transition (its end state is the no-adapter row). Still open here: generalized post-action verification for connect/forget/add. - -**** 2026-07-01 Wed @ 22:01:38 -0400 Closed the two named correct gaps (dotfiles 7819f58) -Gateway unreachable now earns one bounce before the upstream verdict (classifier returns fixable/bounce on gateway warn/fail + probe fail; reachable-gateway keeps the honest upstream call, DNS failure still outranks it). Venue-DNS-broken-but-public-works now ends online: the dns-test chain escalates to a persistent dns-override (1.1.1.1 on the link, dies on reconnect, offered dns-revert undo; a useless override reverts itself) instead of auto-reverting into a misreported upstream outage. Override-aware getent/curl fakes model the venue end to end. Remaining: the edge rows (DNSSEC, proxy, MTU blackhole, clock skew, ARP conflict, roaming flap, stuck-connecting budget, USB-adapter unplug, driver/firmware, WPA3/SAE, randomized-MAC retry, NM-not-running, resolved-down, unmanaged resolv.conf) and the generalized post-action verification for connect/forget/add. -*** TODO Automatic diagnostic verbose-capture (failing diagnose + Advanced toggle) -On =overall: fail=, elevate the underlying stack (NM =WIFI,DHCP,DNS,CORE= / systemd-resolved / -wpa_supplicant) to debug at runtime, run the escalation, capture the journal + dmesg window + -=curl -v=, then restore every level. Also a manual "Debug on/off" toggle in Advanced for -reproducing intermittent failures. HARD: restore is guaranteed (try/finally) AND crash-guarded -(next run detects a left-elevated stack and restores it, like the DoT-restore watcher); the -captured journal is REDACTED before the bundle is written/shown (raw wpa_supplicant/NM debug -carries the PSK/EAP secret in cleartext) with a secret-leak test; log-level toggles run via the -V2 sudo-helper. Bonus: wpa_supplicant debug catches wrong-password/EAP failures the current NM -state-120 snapshot misses, so it also closes the auth live-detection gap. Spec: Observability → -"Automatic diagnostic verbose-capture". Origin: Craig 2026-06-30. - -*** VERIFY Dead-GUI console recovery vs "no terminals" — keep =make online= or replace it? :network: -The cj comment (2026-07-01) said scrub every terminal the module uses to report to or get input -from the user, and I folded that into decision 15 (all module UX is in-panel). The one place it -collides: the deliberate console-recovery path — =make online= / =net doctor --fix= run from a -bare TTY when waybar and the GUI are *down* — is the whole point of the CLI being usable with no -GUI. That's a terminal reporting to the user, but only because there's no panel to use. Keep it -as an explicit carve-out (recovery-only, not terminal-as-UI), or replace it with something else -(a TTY text UI still counts as a terminal)? Your call settles whether the Makefile/CLI recovery -targets stay in the spec. -** DONE [#B] Audio panel spec :feature:waybar:audio:solo: -CLOSED: [2026-07-03 Fri] -:PROPERTIES: -:LAST_REVIEWED: 2026-07-02 -:END: -Went past the spec to a full build in a no-approvals speedrun. Spec is now IMPLEMENTED ([[file:docs/specs/2026-07-03-audio-panel-spec.org]], all 5 Decisions resolved). The panel shipped in the dotfiles repo (branch panel-bugfixing, commits 65e5bb0..9601420): pactl engine, GTK-free presenter, GTK instrument-console panel (OUTPUTS/INPUTS device rows with faders + per-device mute, LIVE/MUTED/PUSH·TALK mic keys, twin VU gauges, master quick-mute), Hyprland-bind push-to-talk, bar indicator, and the bar/keybind wiring (Super+A → panel, XF86AudioMute → master quick-mute). 102 unit tests + a passing AT-SPI smoke on velox. Live-eyeball validation filed under Manual testing and validation. Apply steps + follow-ups handed to the dotfiles project inbox. - -Original ask (roam inbox, 2026-07-02): net/bt-panel kin — change default output/input, volume for both, push-to-talk mic mode for meetings, master quick-mute, bar sound-glyph state. Related bindings: Super+M audio-cycle ring, Super+Shift+A mic-toggle. Prototype: =docs/prototypes/2026-07-03-sound-panel-prototype.html=. ** DONE [#B] Panels moveable + resizable by drag :feature:waybar:network:bluetooth: CLOSED: [2026-07-04 Sat] Resolved by the 2026-07-03 instrument-console rebuild (dotfiles e993c3f): both net + bt panels switched from anchored gtk4-layer-shell overlays to normal floating windows (set_decorated(False), positioned by the net.cjennings.netpanel window rule), so Hyprland moves them on drag and resizes on corner-drag natively. That was exactly the "switch to a normal floating window" approach the design note flagged as the required decision. @@ -1993,3 +1451,37 @@ CLOSED: [2026-07-09 Thu] Not waybar — the tmux status line. status-left-length was never set, so tmux clipped the drawn status-left to its default of 10 columns: "[aiv-archsetup] " came out "[aiv-archs", losing the closing bracket and the separating space, which is why the window list ran into it. Set to 40. Applied to the live tmux server too. (dotfiles dc3c27f) The session section truncates; give it more characters, and keep the window sections that follow from running into it. Screenshot: [[file:/home/cjennings/pictures/screenshots/2026-07-07_145236.png][2026-07-07_145236.png]] +** DONE [#C] minimal tier zsh login PATH gap :bug:dotfiles:quick:solo: +CLOSED: [2026-07-09 Thu] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-09 +:END: +Fixed (dotfiles =c6a7878=). =minimal/.zprofile= added, mirroring =common/='s PATH-only prepend and its rationale comment. + +Tested the invariant rather than the file, because the bug *was* a per-file fix missing a tier: any stow tier shipping a =.zshrc= must ship a =.zprofile= that prepends =~/.local/bin=. Red first, and it named =minimal=. Verified behaviorally as the task asked — stowed the tier into a throwaway HOME and read a real =zsh -lc 'echo $PATH'=, which now leads with =~/.local/bin=. Full =make test= green. + +The 2026-07-08 fix gave common/ a .zprofile (zsh logins never read .profile, so ~/.local/bin was missing from TTY/ssh PATH — dotfiles 39cef40). The minimal/ tier has the same gap: it carries .zshrc and .profile but no .zprofile, so headless installs (DESKTOP_ENV=none stows minimal/ instead of common/) still get zsh logins without ~/.local/bin. Add a matching minimal/.zprofile (PATH-only prepend, same rationale comment). Verify: fresh minimal stow, zsh -lc 'echo $PATH' shows ~/.local/bin first. The installer itself is clean — it writes no shell profiles; the stow layer owns this. +** DONE [#B] Doctor button for the audio panel :feature:audio: +CLOSED: [2026-07-10 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-09 +:END: +What happens when pipewire or pulseaudio is broken — would we know? Spec: [[file:docs/specs/2026-07-09-audio-doctor-spec.org][docs/specs/2026-07-09-audio-doctor-spec.org]] — *READY*, all eight decisions closed 2026-07-09, Phase 0 shipped. Buildable: Phase 1 diag, Phase 2 classify, Phase 3 repair + doctor, Phase 4 the DOCTOR key and wall. Grounded in a live survey of ratio's stack: there is no PulseAudio (pipewire + wireplumber + pipewire-pulse, all user-scope, so no sudo anywhere), =pactl= hangs against a server that accepts and never answers, and the panel can't diagnose itself because =pactl= is the layer most likely to be down. Mirrors the net panel's diag/classify/repair/doctor split, not the maint console's priv-verb table. + +Re-graded =[#C]= → =[#B]= by the 2026-07-09 audit: the spec is written, Phase 0 shipped, and six decisions are all that stand between this and a build. Active backlog, not parking lot. + +*** 2026-07-09 Thu @ 14:12:00 -0500 Fixed: waybar's audio module died against a half-wedged server +My first read of this was wrong. =pactl.run()= already carries =timeout=8= and raises =PactlTimeout=, so the panel degrades and does not freeze. Correcting the record rather than leaving the claim standing. + +The real bug, found by driving it: =build_status()= guarded its two device-list reads but called =default_sink()= and =default_source()= outside the guard. A server that answered the lists and then stopped answering raised =PactlTimeout= straight out of a function whose whole contract is to degrade to =ok: False=. =waybar-audio= calls it with nothing catching it, so the bar's audio module died instead of dimming; the GTK panel survived only because its background worker catches everything. + +A server that hangs on the *first* call never exposes this — =list_sinks= raises inside the guard and masks the unguarded reads behind it — so the obvious regression test passes against broken code. =fake-pactl= grew =FAKE_PACTL_SLEEP_DEFAULTS= to express "answers the lists, hangs on the defaults". Verified end to end: =waybar-audio= now emits =class: degraded= / "Audio graph unavailable" and exits 0. dotfiles =4d42eb3=; full =make test= green. + +*** 2026-07-10 Fri @ 06:41:00 -0500 Shipped: the audio doctor, phases 1 through 4 +=audio diag=, =audio doctor=, =audio doctor --fix=, and the panel's DOCTOR section header with DIAGNOSE / FIX keys streaming into a results wall (copy + close). Spec is IMPLEMENTED. dotfiles =01a1d80=, =76857e3=, =7223c51=, =1550ac8=, =4320255=, =bd33440=. + +Live verification on ratio replaced the planned VM run for remedies 1, 4 and 5, and found two bugs no fake could surface: =pactl get-default-sink= prints the literal =@DEFAULT_SINK@= placeholder when no default is set, and remedy 1 re-probed before WirePlumber had re-created the devices, calling a repaired stack broken. Both fixed. + +Left open, not a v1 gap: mpv played silently while the doctor read healthy. The stack was genuinely fine, and per-application stream routing is an explicit spec Non-Goal. Worth a task only if it recurs. + +Second sighting, 2026-07-10: Chrome stopped recognizing the microphone while the stack was healthy (Shure MV7+ default, unmuted, 82%, PTT off). Restarting Chrome fixed it; nothing on the machine was touched. Both sightings share a shape the doctor cannot see: the graph is fine and one client cannot use it. A third sighting turns this into a design question, namely whether the doctor should say "the stack is fine, the fault is in the application" rather than a bare healthy. |
