aboutsummaryrefslogtreecommitdiff
path: root/docs/specs
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs')
-rw-r--r--docs/specs/2026-07-02-bluetooth-panel-spec.org476
-rw-r--r--docs/specs/2026-07-02-desktop-settings-panel-spec.org134
-rw-r--r--docs/specs/2026-07-02-file-manager-swallow-spec.org147
-rw-r--r--docs/specs/2026-07-02-net-panel-other-interfaces-spec.org195
-rw-r--r--docs/specs/2026-07-02-timer-panel-spec.org119
-rw-r--r--docs/specs/2026-07-03-audio-panel-spec.org166
-rw-r--r--docs/specs/2026-07-03-instrument-console-panels-spec.org164
7 files changed, 1401 insertions, 0 deletions
diff --git a/docs/specs/2026-07-02-bluetooth-panel-spec.org b/docs/specs/2026-07-02-bluetooth-panel-spec.org
new file mode 100644
index 0000000..f1b3ac1
--- /dev/null
+++ b/docs/specs/2026-07-02-bluetooth-panel-spec.org
@@ -0,0 +1,476 @@
+#+TITLE: Bluetooth Panel — CLI-Driven, Net-Panel Kin
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-02
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Bluetooth Panel — CLI-Driven, Net-Panel Kin
+:PROPERTIES:
+:ID: 8af6a76a-5665-4d20-9efd-ffdf7460c981
+:END:
+- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Shipped through phase 3; build task DONE and manual tests filed.)
+
+* IMPLEMENTED Status
+:PROPERTIES:
+:ID: 1271a845-4463-4831-9902-990eda6b2265
+:END:
+- [2026-07-02 Thu] IMPLEMENTED — all five phases shipped the same day
+ (dotfiles eb2230f / 76b2c05 / e372de3 / 2a026b1; archsetup d8d8c53):
+ engine, panel, bar module + blueman retirement, bt-priv + package swap,
+ install wiring proven by VM assertions. 43 dotfiles suites green, both
+ AT-SPI smokes green, panels verified live; the phase 4-5 VM assertions
+ run on the next VM pass.
+- [2026-07-02 Thu] DOING — spec-response decomposed the five phases into
+ build sub-tasks under the todo.org parent (:SPEC_ID: bound); build
+ started same day per Craig ("4 first, then 1" — bugs then bluetooth).
+- [2026-07-02 Thu] READY — spec-review passed the gate: all four
+ decisions resolved, phases decomposable, CLI verbs verified against
+ bluez 5.86. Two non-blocking findings recorded and dispositioned in
+ the same pass (donor-pattern answers).
+- [2026-07-02 Thu] DRAFT — initial spec from Craig's request: a bluetooth
+ module driving a CLI underneath, consistent with the net panel, minimal
+ interface, full functionality, diagnostics section, visual mockups.
+
+* Metadata
+
+| Field | Value |
+|--------+---------------------------------------------------------------------------------|
+| Status | implemented |
+|--------+---------------------------------------------------------------------------------|
+| Owner | Craig Jennings |
+|--------+---------------------------------------------------------------------------------|
+| Repo | dotfiles (bt module); archsetup (packages, sudoers, keybind defaults) |
+|--------+---------------------------------------------------------------------------------|
+| Kin | net panel (architecture donor), desktop-settings panel (same donor, shared css) |
+|--------+---------------------------------------------------------------------------------|
+
+* Problem
+
+Bluetooth on both daily drivers runs through blueman: a tray applet plus a
+GTK3 manager window (Super+Shift+B). It's the odd one out on the desktop —
+a foreign visual style next to the dupre-themed panels, a tray icon where
+every other indicator is a first-class waybar module, and no diagnostics
+story at all. When the BT mouse fails to reconnect at boot (a recurring
+gotcha — touchpad-auto exists because of it) or headphones pair but route
+no audio, the fix is a terminal séance: bluetoothctl, rfkill, systemctl,
+wpctl, in whatever order folklore suggests.
+
+The net panel proved the shape that fixes this: a minimal layer-shell
+popup over a GTK-free engine that drives a CLI, with a diagnostics tab
+that names the failure and offers the repair. Bluetooth is the same
+problem with a smaller surface: one adapter, a handful of devices, a
+short list of well-known failure modes.
+
+* Goals
+
+1. Visibility: adapter power state and every known device with live state
+ (connected, battery, signal) in one glance — panel and bar module agree.
+2. Control: power, scan, pair, connect, disconnect, forget — full
+ functionality from the panel, zero terminals (the net panel's V2
+ contract).
+3. Diagnostics: a doctor that walks the known failure chain (adapter →
+ rfkill → service → power → device → audio profile), names the broken
+ link in evidence rows, and offers tiered repairs.
+4. Consistency: same stack, same window shape, same interaction grammar,
+ same palette as the net panel. A user who knows one panel knows both.
+
+Audio-profile switching is in scope for v1 (Craig, 2026-07-02 — "bitten
+by this too many times to count"): the doctor's audio-profile step
+carries a one-click repair, not just a diagnosis, and connected audio
+devices surface their active profile (details in the doctor chain below).
+
+Non-goals (this iteration): OBEX file transfer, multi-adapter support
+(both machines have one controller), BLE sensor/GATT browsing.
+
+* Design sketch
+
+** Architecture — the net panel's stack, verbatim
+
+- GTK4 + gtk4-layer-shell, Blueprint .blp compiled to committed .ui
+ (=make ui=), PyGObject at runtime.
+- Humble-object split: GTK-free =PanelModel= presenter (unit-tested like
+ net's), thin composite-widget pages, =bg(work, done)= worker-thread
+ helper for every slow call.
+- Engine: a new =bt= package in dotfiles (=bluetooth/src/bt/=, sibling of
+ =net/=), CLI entry =bt= with =bt status= / =bt panel= / =bt doctor= —
+ the same cmd/cli layout as net.
+- Layer-shell OVERLAY popup anchored TOP+RIGHT, 380x520, Esc closes,
+ focus-out auto-hides, single-instance toggle via a =bt-panel= wrapper.
+ Dupre palette css shared with the net panel (the factored css asset the
+ desktop-settings spec calls for — three consumers now, so the factoring
+ happens in this project's phase 1 if settings hasn't landed it).
+- Testing: engine TDD with fake binaries on a temp PATH (fake-bluetoothctl,
+ fake-rfkill, fake-systemctl, fake-wpctl); PanelModel unit suite; one
+ gated AT-SPI smoke (=make test-panel= pattern).
+
+** CLI backing — bluetoothctl one-shot verbs
+
+bluez 5.86 (installed) supports everything non-interactive:
+
+- Adapter: =bluetoothctl show= (powered, discoverable, pairable),
+ =bluetoothctl power on|off=.
+- Device lists: =bluetoothctl devices Paired|Connected|Trusted= — the
+ Paired view is a merge of Paired + Connected states; =bluetoothctl info
+ <mac>= per row fills caption detail (battery percentage rides bluez's
+ built-in Battery1 profile and appears in info output; RSSI appears
+ during discovery).
+- Scan: =bluetoothctl --timeout N scan on= (bounded discovery burst),
+ then =devices= diffed against Paired for the Nearby list. The panel
+ scans in 8s bursts with a live "Scanning…" state rather than an
+ unbounded scan.
+- Connect/disconnect/forget: =bluetoothctl connect|disconnect|remove <mac>=.
+- Pairing: the one interactive corner. =bluetoothctl pair <mac>= can demand
+ a passkey confirmation. The engine drives bluetoothctl's line protocol
+ over a pty with a bounded state machine (expect "Confirm passkey",
+ reply yes/no); a passkey prompt surfaces as a panel dialog showing the
+ six digits, mirroring the net panel's password dialog. NoInputNoOutput
+ devices (mice, most headphones) sail through without the dialog.
+- rfkill: the user is in the =rfkill= group, so block/unblock is
+ unprivileged (=rfkill unblock bluetooth=).
+- Privileged path: exactly one verb needs root — =systemctl restart
+ bluetooth= — so =bt-priv= is a one-verb closed helper with its own
+ NOPASSWD sudoers rule placed by archsetup, cloning net-priv's
+ regex-validated pattern rather than widening net-priv's scope.
+
+** Panel anatomy
+
+Two tabs. Devices is the panel; Diagnostics is the escape hatch.
+
+Devices tab, Paired sub-view (the default — daily use is reconnecting
+known devices, not discovering new ones):
+
+#+begin_example
+╭──────────────────────────────────────────────╮
+│ [ Devices ] [ Diagnostics ] │ ← top switcher
+│ │
+│ Bluetooth ●──○ hci0 on │ ← adapter row: power switch
+│ ──────────────────────────────────────────── │
+│ [ Paired ] [ Nearby ] │ ← sub-view switcher
+│ ┌──────────────────────────────────────────┐ │
+│ │ 󰍽 MX Master 3 │ │
+│ │ Connected · battery 80% │ │
+│ │ 󰋋 WH-1000XM4 │ │
+│ │ Paired, not connected │ │
+│ │ 󰌌 K380 Keyboard │ │
+│ │ Paired, not connected │ │
+│ │ │ │
+│ └──────────────────────────────────────────┘ │
+│ [ Disconnect ] [ Forget ] │ ← acts on selected row
+╰──────────────────────────────────────────────╯
+#+end_example
+
+The primary button is one control with a state-following label:
+"Connect" when the selection is disconnected (suggested-action styling),
+"Disconnect" when connected. Row-activate (Enter / double-click)
+connects — never disconnects — matching the net panel's asymmetry.
+Captions carry the human state line; the MAC lives in the row tooltip,
+not the visible caption.
+
+Devices tab, Nearby sub-view:
+
+#+begin_example
+╭──────────────────────────────────────────────╮
+│ [ Devices ] [ Diagnostics ] │
+│ │
+│ Bluetooth ●──○ hci0 on │
+│ ──────────────────────────────────────────── │
+│ [ Paired ] [ Nearby ] │
+│ ┌──────────────────────────────────────────┐ │
+│ │ Scanning… (6s) │ │ ← overlay state label
+│ │ 󰋋 JBL Flip 6 −58 dBm │ │
+│ │ 󰄜 Pixel 9 −71 dBm │ │
+│ │ 󰂱 (unnamed) 74:A5:… −83 dBm │ │
+│ └──────────────────────────────────────────┘ │
+│ [ Pair ] [ Rescan ] [ Discoverable ⊙ ] │
+╰──────────────────────────────────────────────╯
+#+end_example
+
+Pair does the whole intended thing — pair, then trust, then connect —
+because pairing a device means "use it now and reconnect on its own
+later" (decision below). Discoverable is a toggle for the inbound case
+(pairing a phone TO the laptop), off by default, auto-off with bluez's
+discoverable-timeout. Rows sort by RSSI, strongest first; named devices
+above unnamed ones.
+
+Diagnostics tab (mirrors the net panel's shape: one big verb + streaming
+evidence rows + tiered repairs behind confirmation):
+
+#+begin_example
+╭──────────────────────────────────────────────╮
+│ [ Devices ] [ Diagnostics ] │
+│ │
+│ [ Get Bluetooth Working ] [ Advanced ▸]│
+│ ┌──────────────────────────────────────────┐ │
+│ │ ✓ Adapter present (hci0) │ │
+│ │ ✓ Not blocked (rfkill clear) │ │
+│ │ ✓ bluetooth.service active │ │
+│ │ ✓ Adapter powered │ │
+│ │ ✗ MX Master 3: paired but unreachable │ │
+│ │ … Re-pair suggested — see below │ │
+│ │ │ │
+│ │ Fix: [ Reconnect ] [ Re-pair device ] │ │
+│ └──────────────────────────────────────────┘ │
+│ power-cycle · restart service · unblock │ ← tiered repairs (confirm)
+╰──────────────────────────────────────────────╯
+#+end_example
+
+The doctor chain, in order, each an evidence row:
+
+1. Adapter present — =bluetoothctl list= / rfkill has an hci entry.
+ Absent → hardware/driver verdict, no repair offered.
+2. rfkill state — soft-blocked names the likely cause when the
+ airplane-mode state file says airplane is on ("Blocked by airplane
+ mode — turn airplane mode off"), otherwise offers Unblock (no root
+ needed, rfkill group).
+3. bluetooth.service — inactive/failed → offer restart (the one bt-priv
+ verb), evidence quotes the last journal line.
+4. Adapter powered — off → offer power on (and note if a boot-time
+ policy keeps turning it off).
+5. Per-device reachability — paired-but-connect-fails distinguishes
+ "device off/out of range" (RSSI absent in a scan burst) from "bond
+ corrupt" (connect error string), and only the latter suggests the
+ re-pair repair (remove + pair + trust + connect, confirmed first —
+ it's the destructive tier).
+6. Audio profile (audio devices only) — device connected but no wpctl
+ sink/source, or the card stuck in HSP/HFP when A2DP is expected:
+ evidence names the active profile and offers the repair inline —
+ "Switch to A2DP" drives =wpctl set-profile <card> <index>= (profile
+ inventory from =pw-dump= — ground truth 2026-07-02: wpctl can't
+ enumerate a card's profiles, and the card's =bluez5.profile= prop
+ reads "off" mid-stream; the card's Profile param and the sink node's
+ =api.bluez5.profile= are authoritative), verifies the sink came back
+ in the expected profile, and reports fixed or no-change. In v1 per
+ Craig (2026-07-02): this failure mode has bitten repeatedly, so it
+ gets the one-click fix, not just a diagnosis. Connected audio-device
+ row captions also show the profile when it's the degraded one
+ ("Connected · mic mode (HSP)") so the state is visible before the
+ doctor runs.
+
+Repairs confirm with the net panel's future-tense scope copy ("This will
+restart the Bluetooth service. Connected devices will drop and
+reconnect."), run on the worker thread, verify after (re-read state,
+report "fixed" or "no change"), and never chain silently.
+
+** Bar module
+
+=custom/bluetooth= replacing the blueman-applet tray icon: the panel's
+glanceable layer, one glyph, state-following like =custom/net=:
+
+#+begin_example
+ 󰂲 off / blocked (dim; red slash variant when rfkill-blocked)
+ 󰂯 on, nothing connected (dim)
+ 󰂱 connected (white; tooltip lists devices + battery)
+#+end_example
+
+Tooltip carries device names, battery percentages, and the keybind hints
+(the module-tooltip convention shipped 2026-07-02). Click opens the
+panel (=bt-panel= toggle wrapper); the existing Super+Shift+B bind moves
+from blueman-manager to =bt panel=. Low-battery on a connected device
+(<15%) adds a red percentage to the glyph text — the mouse dying
+mid-meeting is the one state worth surfacing unprompted.
+
+** UX conformance notes
+
+Named against the heuristics the panel family follows (Nielsen's ten,
+plus the rulesets patterns catalog):
+
+- Visibility of status: live captions, scan countdown, elapsed ticker on
+ long ops, verify-after-repair rows.
+- Match to the real world: device-kind glyphs + plain state lines; MACs
+ demoted to tooltips; "Forget" not "Remove bond".
+- User control: Esc closes, Rescan is idempotent, scan bursts are
+ bounded, repairs confirm, running ops show a Stop where stoppable.
+- Consistency: interaction grammar is the net panel's — same switcher
+ layout, same primary-button contract, same confirm copy shape.
+- Error prevention: Forget and Re-pair confirm; power-off while devices
+ are connected states the consequence in the confirm body.
+- Recognition over recall: every action is a visible button; no context
+ menus, no hidden gestures (transient-state-buttons pattern).
+- Minimalism: two tabs, one primary action per view, detail behind
+ tooltips and the Advanced reveal.
+- Help users recover: the doctor's evidence rows name the broken link
+ and carry the repair inline (default-most-common-friction-proportional:
+ the likely fix is one click, the destructive one is confirmed).
+
+Tension found with the net panel while writing this (filed as todo.org
+tasks per Craig's instruction, 2026-07-02): transient error toasts
+auto-dismiss in 4s, and the V2 spec's keyboard-navigation claims
+(tab-between-sections, arrow rows, type-to-filter) aren't verifiably
+implemented. Both filed against the net panel rather than cloned here;
+this panel adopts whatever resolution those tasks land on.
+
+* Decisions (Craig) [4/4]
+
+** DONE Pair implies trust + connect?
+CLOSED: [2026-07-02 Thu]
+Decided (Craig, 2026-07-02): yes — one Pair verb does pair → trust →
+connect. A device that shouldn't auto-reconnect gets untrusted later; a
+per-device auto-reconnect toggle can ride a later pass.
+
+** DONE Retire blueman entirely?
+CLOSED: [2026-07-02 Thu]
+Decided (Craig, 2026-07-02): drop it outright, no bake-in period — the
+package leaves archsetup and both machines once phase 2 lands,
+bluetoothctl stays as the terminal fallback. Craig's framing: any issue
+after retirement is a signal the doctor needs another check or the panel
+has a real bug, and it gets fixed there rather than papered over by
+keeping blueman around.
+
+** DONE Battery in the row caption or tooltip only?
+CLOSED: [2026-07-02 Thu]
+Approved (Craig, 2026-07-02): caption when the device reports it
+("Connected · battery 80%"), tooltip otherwise.
+
+** DONE Scan burst length and auto-rescan?
+CLOSED: [2026-07-02 Thu]
+Approved (Craig, 2026-07-02): 8s bursts, no auto-repeat — Rescan stays
+explicit, matching the net panel's Available view.
+
+* Review findings [2/2]
+
+** DONE Empty-state and no-adapter presentation copy undefined :nonblocking:
+CLOSED: [2026-07-02 Thu]
+The mockups show populated lists; the spec didn't say what an empty Paired
+list, an empty post-scan Nearby list, or a machine with no adapter shows
+in the panel and on the bar glyph. Dispositioned same pass: clone the
+donor — the net panel's in-box overlay message pattern (=show_loading= /
+placeholder label) carries the copy. Paired empty: "No paired devices —
+switch to Nearby to pair one." Nearby post-scan empty: "Nothing found —
+Rescan, or make the device discoverable." No adapter: adapter row reads
+"No Bluetooth adapter", Devices controls disable, Diagnostics stays
+usable (the doctor's step 1 names the hardware/driver verdict); bar
+glyph shows the off/blocked state. Non-blocking; recorded so the
+implementer doesn't invent copy mid-build.
+
+** DONE Logging/redaction carry-over unstated :nonblocking:
+CLOSED: [2026-07-02 Thu]
+The spec says "the net panel's stack, verbatim" but didn't name whether
+the engine adopts net's =eventlog= (structured op log) and =redact=
+(sensitive-field scrubbing) modules. Dispositioned same pass: yes, both
+carry over — every mutating verb (pair/connect/forget/repair) logs an
+eventlog entry, and MACs are the redaction surface (device names stay,
+MACs redact in copied reports, mirroring net's report redaction).
+Non-blocking; it's the donor default made explicit.
+
+* Implementation phases
+
+1. Engine =bt= package: adapter/device/scan probes over fake-bluetoothctl,
+ status + doctor chain (rfkill, service, powered, reachability, audio
+ profile probe + A2DP switch repair over fake-wpctl) — pure TDD, no
+ GTK. =bt status= and =bt doctor= work from a terminal. Shared dupre
+ css factored to the common asset if the settings panel hasn't already
+ done it.
+2. Panel: PanelModel presenter + Blueprint pages (Devices with
+ Paired/Nearby, Diagnostics), worker-thread wiring, pairing-dialog
+ state machine, bt-panel toggle wrapper, AT-SPI smoke. Super+Shift+B
+ rebind.
+3. Bar module =custom/bluetooth= (glyph states, tooltip, low-battery
+ surface, refresh signal), waybar config + suite coverage; blueman
+ retirement per the decision.
+4. bt-priv one-verb helper + sudoers rule in archsetup; package-list
+ swap (blueman out per decision, bluez-utils stays); VM test
+ assertions.
+5. archsetup keybind/config defaults so a fresh install lands the panel
+ wired (waybar module present, bind set, sudoers placed).
+
+* Review and iteration history
+
+** 2026-07-02 Thu @ 15:19:58 -0400 — Claude Code (archsetup) — phase 5 builder, spec closed
+- *What changed or was recommended:* Phase 5 shipped and the spec flipped
+ to IMPLEMENTED. No new install code was needed — the waybar module, the
+ =Super+Shift+B= bind, and the shared panel css all ride the dotfiles
+ hyprland tier that a fresh install already clones and stows, and sudoers
+ is covered by the blanket grant. The phase's substance is proof:
+ =test_desktop.py= gained hyprland-gated assertions for the four stowed
+ bt bins, the =custom/bluetooth= waybar entry, the =bt-panel= keybind,
+ and the stowed =panel.css=.
+- *Why:* Final phase of the DOING decomposition; with it the todo parent
+ closed and the lifecycle keyword flipped with a history line.
+- *Artifacts:* archsetup =scripts/testing/tests/test_desktop.py=; todo.org
+ parent DONE + dated phase 5 / test-surface entries; this spec's Status
+ heading.
+
+** 2026-07-02 Thu @ 15:16:51 -0400 — Claude Code (archsetup) — phase 4 builder
+- *What changed or was recommended:* Phase 4 shipped. Dotfiles =2a026b1=:
+ the stowed =bt-priv= shim (one verb, verified against the fake-systemctl)
+ and the sxhkd =Super+Shift+B= bind repointed from blueman-manager to
+ =st -e bluetoothctl= (terminal fallback per the retirement decision — the
+ panel is Wayland-only). archsetup: blueman dropped from the
+ =desktop_environment= package loop; VM assertions added (bluez/bluez-utils
+ present, blueman absent). blueman also removed live from velox.
+- *Why:* Build order per the DOING decomposition. The spec's "sudoers rule"
+ item resolved as net-priv's did: archsetup already grants the primary
+ user blanket =NOPASSWD: ALL= (archsetup:1089), so a narrow bt-priv rule
+ would be dead config — no new sudoers needed, and phase 5's "sudoers
+ placed" is satisfied by the existing grant.
+- *Artifacts:* dotfiles =hyprland/.local/bin/bt-priv=,
+ =common/.config/sxhkd/sxhkdrc=; archsetup =archsetup= (bluetooth loop),
+ =scripts/testing/tests/test_packages.py=; dated phase 4 entry under the
+ todo.org parent.
+
+** 2026-07-02 Thu @ 15:06:00 -0400 — Claude Code (archsetup) — phase 3 builder
+- *What changed or was recommended:* Phase 3 shipped (dotfiles =e372de3=):
+ the =custom/bluetooth= bar module (state-following glyph, low-battery red
+ percentage, device+battery tooltip with the keybind hint, signal 10 with
+ the panel poking it after each reload) and the blueman retirement from the
+ Hyprland session (exec-once + windowrules removed, applet killed live).
+ The phase 2 deferred items also closed this pass: both AT-SPI smokes green
+ (the bt smoke's primary-button assertion fixed for the state-following
+ label, =c1a8219=), both panels eyeballed correct in dupre, and the
+ net-panel keyboard claims verified live (archsetup =e80df2b= — false
+ claims struck from the net spec).
+- *Why:* Build order per the DOING decomposition; the Zoom meeting ended,
+ unblocking the visual work. Phases 4-5 (bt-priv/sudoers/packages, install
+ defaults — archsetup side) remain.
+- *Artifacts:* dotfiles =bluetooth/src/bt/indicator.py=, =waybar-bt=,
+ waybar config + three css files; dated phase 3 entry under the todo.org
+ parent.
+
+** 2026-07-02 Thu @ 14:15:27 -0400 — Claude Code (archsetup) — phase 2 builder
+- *What changed or was recommended:* Phase 2 shipped (dotfiles =76b2c05=):
+ the GTK panel — PanelModel/viewmodel presenter pair (69 tests), Blueprint
+ pages, pairing pty state machine with default-deny passkey confirms,
+ manage.py op envelopes shared by CLI and panel (power + discoverable verbs
+ added), =bt-panel= toggle, Super+Shift+B rebind. The shared dupre css
+ factoring landed as planned: net's inline =_CSS= became
+ =themes/dupre/panel.css= with =dupre-*= classes, both panels consume it.
+ 43 suites green. The AT-SPI smoke (=make test-panel-bt=) is written but
+ not yet run live — a Zoom meeting occupied the compositor; it runs when
+ the meeting ends, along with a visual check of both panels.
+- *Why:* Build order per the DOING decomposition; phases 3-5 (bar module,
+ bt-priv/sudoers, install defaults) remain.
+- *Artifacts:* dotfiles =bluetooth/src/bt/{panel,viewmodel,pairing,manage,
+ gui,pages}.py=, =ui/*.blp=, =tests/bt/test_btpanel.py=, the panel smoke;
+ dated phase 2 entry under the todo.org parent.
+
+** 2026-07-02 Thu @ 13:31:00 -0400 — Claude Code (archsetup) — phase 1 builder
+- *What changed or was recommended:* Phase 1 shipped (dotfiles =eb2230f=):
+ the =bt= engine package, 101 tests over fakes, live-verified read-only
+ on velox. Two spec corrections from ground truth: profile inventory
+ comes from =pw-dump= (wpctl can't enumerate profiles), and the active
+ profile reads from the card's Profile param / sink's
+ =api.bluez5.profile= (the card's =bluez5.profile= prop is unreliable).
+ The shared-css factoring moved into phase 2 — net's css is an inline
+ string in its =gui.py=, so extracting it belongs with the first second
+ consumer rather than as a standalone poke at the working net panel.
+- *Why:* Build order per the DOING decomposition; corrections keep the
+ spec honest for the phase 2 implementer.
+- *Artifacts:* dotfiles =bluetooth/src/bt/=, =tests/bt/=, the stowed
+ =bt= shim; dated phase 1 entry under the todo.org parent.
+
+** 2026-07-02 Thu @ 13:10:00 -0400 — Claude Code (archsetup) — reviewer + responder
+- *What changed or was recommended:* Ran the spec-review gate: passed.
+ All four decisions were already DONE (cookie added to the heading);
+ the five phases are each a clean single-session stop; CLI verbs are
+ verified against installed bluez 5.86. Two non-blocking findings
+ recorded and dispositioned in the same fused pass (empty-state /
+ no-adapter copy, eventlog + redaction carry-over) — both resolve to
+ "clone the net-panel donor," now stated explicitly. Flipped DRAFT →
+ READY → DOING and decomposed the phases into build sub-tasks under the
+ todo.org parent with :SPEC_ID: bound.
+- *Why:* Craig queued the build ("4 first, then 1", 2026-07-02) after
+ resolving all decisions the same morning; the gate held nothing back,
+ so review and response fused to keep the speedrun moving.
+- *Artifacts:* Findings in =* Review findings [2/2]= above; build parent
+ in todo.org ("Bluetooth panel + bar module"); net-panel toast fix the
+ UX-conformance note references landed as dotfiles =0f017d4=.
diff --git a/docs/specs/2026-07-02-desktop-settings-panel-spec.org b/docs/specs/2026-07-02-desktop-settings-panel-spec.org
new file mode 100644
index 0000000..d147249
--- /dev/null
+++ b/docs/specs/2026-07-02-desktop-settings-panel-spec.org
@@ -0,0 +1,134 @@
+#+TITLE: Desktop-Settings Dropdown Panel
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-02
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Desktop-Settings Dropdown Panel
+:PROPERTIES:
+:ID: d6bb1e73-ec90-4327-85ee-bfa762da5bce
+:END:
+- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to DRAFT (evidence-based, human-confirmed)
+
+* DRAFT Status
+:PROPERTIES:
+:ID: fb7eec22-a214-4568-82c4-903612f4832f
+:END:
+- [2026-07-02 Thu] DRAFT — initial spec from the todo.org task "Desktop-settings
+ dropdown panel" (2026-06-24 review), updated for the Blueprint/GTK4 pipeline
+ the net panel stood up 2026-07-01.
+
+* Metadata
+
+| Field | Value |
+|--------+----------------------------------------------|
+| Status | draft |
+|--------+----------------------------------------------|
+| Owner | Craig Jennings |
+|--------+----------------------------------------------|
+| Repo | dotfiles |
+|--------+----------------------------------------------|
+| Kin | net panel (architecture donor), theme studio |
+|--------+----------------------------------------------|
+
+* Problem
+
+Desktop toggles are scattered: dim, caffeine/idle, touchpad/mouse, airplane
+mode each own a bar module and a keybind; brightness and keyboard-backlight
+have keybinds but no visible control or level readout. The bar is running out
+of glanceable width (hence the collapse arrows), and sliders can't live in
+waybar at all. One settings dropdown — a gear glyph opening a small panel —
+gathers them.
+
+* Goals
+
+1. One panel with every desktop toggle + slider: auto-dim, idle/caffeine,
+ touchpad, mouse, airplane (laptop-only), screen brightness, keyboard
+ backlight.
+2. Conditional rows appear only when the hardware/context applies (mouse
+ present, trackpad present, battery present) — reuse the detection the
+ airplane/touchpad indicators already do.
+3. Every control reflects live state and verifies its action took (the net
+ panel's verify-everything contract).
+4. Bar stays the quick layer: which standalone indicators survive is a
+ decision below.
+
+* Design sketch
+
+** Architecture — clone the net panel's proven stack
+
+- GTK4 + gtk4-layer-shell, Blueprint .blp sources compiled to committed .ui
+ (=make ui=; dev-only build dependency, fresh clones run without the
+ compiler).
+- Humble-object split: a GTK-free PanelModel presenter (unit-tested to 100%
+ like the net PanelModel) + thin composite-widget pages. Backing actions in
+ a GTK-free settings.py that shells out to brightnessctl / hyprctl / the
+ existing toggle scripts, TDD'd with fake binaries like every dotfiles
+ suite.
+- One gated AT-SPI smoke (the run-panel-smoke.sh pattern), no bespoke
+ headless widget suite.
+- Dupre WIP palette CSS, shared with the net panel — factor the palette
+ block into a common css asset both panels load rather than duplicating
+ (feeds the theme-studio task later).
+
+** Controls and their backings
+
+| Control | Backing |
+|--------------------+----------------------------------------------|
+| Auto-dim toggle | hyprctl decoration:dim_inactive (dim-toggle) |
+|--------------------+----------------------------------------------|
+| Idle / caffeine | hypridle start/stop (caffeine-toggle) |
+|--------------------+----------------------------------------------|
+| Touchpad toggle | toggle-touchpad + touchpad-state file |
+|--------------------+----------------------------------------------|
+| Mouse toggle | same mechanism, mouse-state file |
+|--------------------+----------------------------------------------|
+| Airplane mode | airplane-mode script (laptop-only row) |
+|--------------------+----------------------------------------------|
+| Screen brightness | brightnessctl (backlight class), slider + % |
+|--------------------+----------------------------------------------|
+| Keyboard backlight | brightnessctl (kbd_backlight class), slider |
+|--------------------+----------------------------------------------|
+
+Slider changes apply live (throttled) and read back the actual level after
+apply — verify-everything. Toggles re-read their source of truth after
+firing, same as the bar indicators do, and the bar modules get their refresh
+signals so both surfaces agree.
+
+** Open/close behavior
+
+Gear glyph module on the bar right cluster; click toggles the panel
+(layer-shell anchored under the bar, right-aligned). Focus-out auto-hide +
+Close button, matching the net panel. Keybind decision below.
+
+* Decisions (Craig)
+
+** TODO Which standalone bar indicators collapse into the panel?
+Options per module (dim, touchpad, caffeine): keep on bar + mirrored in
+panel; or panel-only (frees bar width). Recommendation: keep touchpad and
+caffeine visible on the bar (state you glance at), move dim into the panel
+(you set it rarely), keep airplane where it is.
+
+** TODO Keybind for the panel?
+Super+Shift+G (gear) is free. Or no keybind — mouse-only surface.
+
+** TODO Where does the code live?
+Recommendation: dotfiles =settings/= sibling to =net/= (same src-layout,
+tests in tests/settings/), sharing the palette css. In-tree pocketbook-style
+was the old note; the net panel is the better donor now.
+
+** TODO Slider granularity and floor
+brightnessctl exposes 0-100%; a 5% floor stops "screen went black in a dark
+room" lockouts. Confirm the floor (or allow 0 with a long-press escape
+hatch).
+
+* Implementation phases
+
+1. settings.py backings (brightness get/set, kbd backlight, toggle
+ state readers) — pure engine, TDD with fake brightnessctl/hyprctl.
+2. PanelModel presenter (rows, conditional visibility, verify-after-apply
+ semantics) — unit-tested, no GTK.
+3. Blueprint UI + gear bar module + open/close wiring; palette css factored
+ to a shared asset; AT-SPI smoke.
+4. Bar-module consolidation per the decision above (drop/keep indicators,
+ refresh-signal wiring, keybind).
diff --git a/docs/specs/2026-07-02-file-manager-swallow-spec.org b/docs/specs/2026-07-02-file-manager-swallow-spec.org
new file mode 100644
index 0000000..b898f11
--- /dev/null
+++ b/docs/specs/2026-07-02-file-manager-swallow-spec.org
@@ -0,0 +1,147 @@
+#+TITLE: File-Manager Swallow Pattern
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-02
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* CANCELLED File-Manager Swallow Pattern
+:PROPERTIES:
+:ID: 179a1cd2-7a02-4c44-a09d-685c5a154895
+:END:
+- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to CANCELLED (reason: Native swallow ruled out by test; reassigned to .emacs.d dirvish handling.)
+
+* CANCELLED Status
+:PROPERTIES:
+:ID: d92e0074-f594-4e83-81a0-faf282e15ed0
+:END:
+- [2026-07-02 Thu] CANCELLED — targeted the wrong file manager. Craig's ask
+ is about the dirvish popup (Super+F, an Emacs frame), not nautilus (the
+ Super+Shift+F bind that misled the grounding). For dirvish the right
+ design is elisp-side and strictly better: Emacs is the launcher, so it
+ can spawn the handler directly (=start-process=), hide the popup frame,
+ and restore it from a process sentinel — exact exit tracking plus a
+ failure notify, no window-event heuristics. Reassigned to .emacs.d via
+ its inbox (2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff).
+ The gio double-fork finding below stands for any gio-launching file
+ manager; the daemon design is kept for reference only.
+- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture ("when the
+ file manager launches another app, it should hide and return when that
+ process ends"). Feasibility ground truth sampled live on velox same
+ evening: Hyprland's native swallow cannot work here (see Problem), so the
+ design is an event-listener daemon.
+
+* Metadata
+
+| Field | Value |
+|--------+----------------------------------------------------|
+| Status | cancelled |
+|--------+----------------------------------------------------|
+| Owner | Craig Jennings |
+|--------+----------------------------------------------------|
+| Repo | dotfiles (daemon + config); archsetup (none) |
+|--------+----------------------------------------------------|
+| Kin | touchpad-auto (socket-listener donor), |
+| | hypr-refocus-scratchpad (event-daemon sibling) |
+|--------+----------------------------------------------------|
+
+* Problem
+
+Opening a file from nautilus (Super+Shift+F, tiled, class
+=org.gnome.Nautilus=) spawns a viewer window while nautilus stays in the
+layout. The wanted behavior is the swallow pattern: the file manager hides
+while the app it launched runs, and returns when that app exits. Today
+there's no signal connecting the two windows — the viewer lands wherever
+the layout puts it, nautilus lingers, and quitting is manual.
+
+*Hyprland's native swallow is ruled out — measured, not assumed.*
+=misc:enable_swallow= + =swallow_regex= would be exactly this feature in two
+config lines, but it matches by walking the new window's PID ancestry to
+the swallow candidate's PID. Nautilus launches handlers through GLib
+(=g_app_info_launch_default_for_uri=), and that path orphans the child:
+reproduced live on velox 2026-07-02 with a python-gi launcher — feh came up
+with PPID 1 (reparented to init) while the launcher was still alive. The
+ancestry walk hits init before it hits nautilus, every time, for every
+handler. Any design that depends on PID parentage is dead on arrival; the
+signal has to come from window events instead.
+
+Ground truth on handlers (velox, 2026-07-02): pdf → zathura, image → feh,
+video → mpv, text/code → emacsclient (window belongs to the emacs daemon).
+Side-note, out of scope here: feh is X11 — an XWayland viewer on a
+no-XWayland-by-preference setup; a default-handler review is its own task.
+
+* Goals
+
+- Double-click a file in nautilus → the viewer takes its place; nautilus is
+ gone (special workspace, not killed — state and tabs survive).
+- Quit the viewer → nautilus returns and has focus.
+- Nothing else changes: terminals, scratchpads, and every other window keep
+ their current behavior.
+- Config-driven, testable logic, one small daemon — the touchpad-auto shape.
+
+* Design sketch
+
+A =hypr-swallow= daemon (dotfiles, =hyprland/.local/bin/=) listening on the
+Hyprland IPC event socket (socket2), same as =touchpad-auto=:
+
+- Track the active window (=activewindow>>= events carry class + title;
+ =activewindowv2>>= carries the address).
+- On =openwindow>>= (address, workspace, class, title) while the active
+ window's class is a configured *parent* (nautilus): dispatch
+ =movetoworkspacesilent special:swallow,address:0x<parent>=, record
+ child-address → {parent-address, origin workspace}.
+- On =closewindow>>= of a recorded child: bring the parent back
+ (=movetoworkspace=) and focus it; drop the record.
+- On =closewindow>>= of a hidden parent (nautilus quit while hidden): drop
+ the record, nothing to restore.
+- Exception classes (fuzzel, dunst, scratchpad classes, the panels) never
+ trigger a swallow even when they open over nautilus.
+- Pure event-machine core (parse lines → state transitions → dispatch list),
+ unit-tested against recorded event streams; a thin socket loop around it.
+
+Known edge, handled: Super+Shift+F while nautilus is hidden re-runs
+=nautilus=, which activates the existing (hidden) instance instead of
+opening a window. The daemon (or the bind) must restore-and-untrack in that
+case so the bind never appears dead.
+
+Known limitation, accepted: the emacsclient case never swallows — the
+window belongs to the long-running emacs daemon and =closewindow= for it
+means a frame closed, not "the file is done." The parent-class trigger plus
+exception list naturally leaves it alone only if we exclude it explicitly —
+see decision 2.
+
+* Decisions (Craig)
+
+** TODO Trigger breadth: any new window while nautilus is active, or an allowlist of viewer classes?
+"Any window" is simple and catches every handler, but a false positive
+exists: an app you launched seconds earlier from elsewhere finishes starting
+while you're focused on nautilus → nautilus gets swallowed by an unrelated
+window. An allowlist (zathura, mpv, imv, feh, …) can't be surprised but
+needs maintaining. Recommendation: any-window + exception list — the false
+positive is rare and self-healing (close the window or refocus).
+
+** TODO The emacs frame case: swallow or exempt?
+Opening a text file from nautilus raises/creates an emacs frame. Swallowing
+nautilus under it "works" going in, but the restore fires when *any* frame
+closes, which may be much later or never. Recommendation: exempt =emacs= —
+text files just open, nautilus stays.
+
+** TODO Restore destination: the workspace nautilus came from, or the one you're on when the viewer closes?
+If you move the viewer to another workspace and quit it there, "origin"
+teleports you back; "current" brings nautilus to you. Recommendation:
+current workspace — the restore should land where your attention is.
+
+** TODO Multiple children: refcount or single-slot?
+You can only launch a second file after restoring nautilus manually, so
+overlap is rare — but a fast double-launch can record two children.
+Recommendation: refcount — restore when the last tracked child closes.
+
+* Implementation phases
+
+1. =hypr-swallow= core: pure event-machine (TDD over recorded event
+ streams; fake hyprctl for dispatch assertions), config block at the top
+ (parent classes, exception classes), unittest suite in =tests/=.
+2. Socket loop + wiring: exec-once in hyprland.conf, the Super+Shift+F
+ restore-if-hidden interplay, daemon single-instance guard.
+3. Live verification on velox (zathura + mpv round-trips, the emacs case,
+ the false-positive probe) + manual-testing entries; ratio rides the
+ dotfiles pull.
diff --git a/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org b/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org
new file mode 100644
index 0000000..0d63feb
--- /dev/null
+++ b/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org
@@ -0,0 +1,195 @@
+#+TITLE: Net Panel — Tailscale, VPN, and WireGuard Interfaces
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-02
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Net Panel — Tailscale, VPN, and WireGuard Interfaces
+:PROPERTIES:
+:ID: 09f4cd40-f391-4eba-a4ff-c22bad00ad7f
+:END:
+- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Tunnels track shipped detection, diagnose, and panel bring-up; build task DONE.)
+
+* IMPLEMENTED Status
+:PROPERTIES:
+:ID: 79a1075a-4b56-4f25-a861-b69f120a636a
+:END:
+- [2026-07-02 Thu] IMPLEMENTED — all six phases shipped (dotfiles 2d9d060,
+ 21db05a, 31ba056, b4010bf, b5c8442; archsetup 0389790 + the wireguard
+ import script): probes, panel Tunnels view, diagnose/doctor route
+ awareness, bar badge, installer swap + operator, velox config migration.
+ Residual human steps filed under todo.org "Manual testing and
+ validation": proton CLI sign-in (per machine) and the first live
+ badge/tunnel round-trip. Ratio picks up the import + package swap on its
+ trip.
+- [2026-07-02 Thu] DOING — decomposed into six build phases under the
+ todo.org parent (:SPEC_ID: bound); build started same evening per Craig
+ ("tunnels build now + audio-panel spec alongside").
+- [2026-07-02 Thu] READY — fused review passed the gate: 4/4 decisions
+ resolved, phases decomposable, claims re-verified live (proton-vpn-cli
+ 1.0.1 in extra, binary =/usr/bin/protonvpn=, no package conflict with the
+ GTK app; =tailscale status --json= shape confirmed on velox — Self/Peer/
+ CurrentTailnet.Name/MagicDNSSuffix; zero NM wireguard connections yet,
+ seven configs in assets awaiting the phase 6 import).
+- [2026-07-02 Thu] DRAFT — initial spec from the roam capture "other network
+ interfaces (tailscale, VPNs, wireguard)" filed in todo.org 2026-07-02.
+
+* Metadata
+
+| Field | Value |
+|--------+---------------------------------------------------|
+| Status | implemented |
+|--------+---------------------------------------------------|
+| Owner | Craig Jennings |
+|--------+---------------------------------------------------|
+| Repo | dotfiles (net module); archsetup (packages) |
+|--------+---------------------------------------------------|
+| Parent | Waybar network module spec (2026-06-29), V2 panel |
+|--------+---------------------------------------------------|
+
+* Problem
+
+The net panel's Connections tab shows what NetworkManager knows: WiFi networks
+and wired links. The machines also run overlay and tunnel interfaces the panel
+is blind to:
+
+- Tailscale (tailscaled, both daily drivers; the tailnet is how the machines
+ reach each other; not an NM device)
+- WireGuard configs (assets/wireguard-config/ carries Proton VPN configs;
+ importable as NM connections of type wireguard or run via wg-quick)
+- Commercial VPN clients (Proton VPN GTK app is installed on velox; owns its
+ own tunnel device)
+
+When one of these is up it changes routing, DNS, and reachability — exactly
+the things the Diagnostics tab reasons about — yet the panel neither shows nor
+controls them, and the doctor can misattribute a VPN-caused failure to the
+underlying link.
+
+* Goals
+
+1. Visibility: the Connections tab shows overlay/tunnel interfaces with live
+ state (up/down, address, and for tailscale the tailnet peers summary).
+2. Control: bring each up or down from the panel row, same interaction shape
+ as Join/Disconnect on WiFi rows (no terminals — V2 contract).
+3. Diagnostics awareness: diagnose/doctor know when a tunnel owns the default
+ route or DNS, name it in evidence rows, and stop misattributing its
+ failures to the physical link.
+
+Non-goals (this iteration): installing or configuring VPN providers, tailnet
+ACL management, exit-node selection UI (a "use exit node" affordance can ride
+a later pass), kill-switch management (tracked separately in the spec's
+failure catalog).
+
+* Design sketch
+
+** Data sources — one probe per backend, engine-side
+
+New GTK-free module net/src/net/overlays.py with one probe per backend,
+each returning the same small dict shape ({kind, name, state, addr, detail,
+can_toggle}):
+
+- tailscale: =tailscale status --json= (rich: self, peers, exit node, health
+ messages). Daemon down → state "stopped". Binary absent → backend absent.
+- wireguard-nm: =nmcli -t connection show= filtered to type wireguard —
+ up/down via the existing nmcli wrapper (activate/deactivate connection).
+ The seven Proton configs in assets/wireguard-config/ import cleanly
+ (=nmcli connection import type wireguard file <conf>=, then
+ =connection.autoconnect no= immediately — imports default to autoconnect
+ yes). They use only PrivateKey/Address/DNS + PublicKey/AllowedIPs/Endpoint,
+ no PostUp/PostDown anywhere, so no wg-quick path is needed at all
+ (Craig, 2026-07-02). All are full-tunnel (AllowedIPs 0.0.0.0/0) — the
+ panel should treat them as mutually exclusive.
+- proton: drive the official proton-vpn-cli (Arch extra repo, v1.0.x,
+ stable since 2026-04) — connect/disconnect/status verbs. It drives NM
+ underneath (python-proton-vpn-network-manager), so the panel still sees
+ connection events through NM. Runtime-exclusive with the GTK app, which
+ gets dropped from the install. The imported NM wireguard configs remain
+ a raw fallback when the CLI/API path is down; the CLI stays primary
+ because the raw configs lack kill switch, port forwarding, and server
+ rotation.
+
+** Panel
+
+A fourth Connections group "Tunnels" (after Saved / Available now / Wired)
+using the existing group-header + row machinery. Row: glyph per kind, name,
+state caption; primary action Up/Down where can_toggle, else Open app.
+Tailscale row detail (subtitle or tooltip): tailnet name, peer count online,
+exit node if any.
+
+** Privileged path
+
+- tailscale up/down: needs root or operator — =tailscale set --operator= at
+ install time (archsetup) makes the user an operator, so no sudo needed at
+ runtime. Fallback: the V2 net-priv helper gains tailscale-up/down verbs.
+- NM wireguard connections: no privilege needed (NM polkit default for the
+ active user).
+
+** Diagnostics awareness
+
+- diag gains an "overlay owns default route/DNS" detection step: when the
+ default route or resolv.conf points at a tunnel interface, evidence names
+ it ("default route via tailscale0") and failure classification runs the
+ physical-link checks against the underlying device instead.
+- doctor: a tunnel-caused egress failure (VPN up but its endpoint dead)
+ classifies fixable with next_action "bring the tunnel down / reconnect",
+ not a WiFi reset.
+
+** Bar indicator
+
+Part of v1 (Craig, 2026-07-02 — "shouldn't be optional"): a small overlay
+badge on the net glyph when a tunnel owns the default route. Rides the same
+route/DNS-ownership detection the diagnostics step adds.
+
+* Decisions (Craig)
+
+** DONE Which backends ship in the first pass?
+CLOSED: [2026-07-02 Thu]
+Approved (Craig, 2026-07-02): tailscale + NM-managed wireguard. Craig asked
+whether the wireguard configs can be ported to NM so wg-quick drops out
+entirely — yes: all seven configs in assets/wireguard-config/ use only the
+six directives NM imports cleanly (verified 2026-07-02; import command and
+autoconnect caveat now in the design sketch). wg-quick is out of the spec,
+not deferred. Proton control is CLI-driven per the Proton decision below,
+superseding the detection-only recommendation here.
+
+** DONE Tailscale control path: operator flag at install vs net-priv verbs?
+CLOSED: [2026-07-02 Thu]
+Approved (Craig, 2026-07-02): =tailscale set --operator=$USER= in archsetup's
+tailscale step (declarative, no sudo at runtime); net-priv verbs only if
+operator mode proves insufficient (e.g. up with flags).
+** DONE Does "Tunnels" belong in Connections or its own tab?
+CLOSED: [2026-07-02 Thu]
+Approved (Craig, 2026-07-02): a Connections group. A fourth top tab dilutes
+the V2 nav for three rows.
+
+** DONE Proton VPN: detect-only or drive its CLI?
+CLOSED: [2026-07-02 Thu]
+Decided (Craig, 2026-07-02): drive it through a CLI. Research (2026-07-02):
+Proton shipped an official Linux CLI — first release 2025-11, stable v1.0.0
+2026-04, packaged in Arch extra as proton-vpn-cli (1.0.1 at check time),
+with kill switch, port forwarding, NetShield, server selection, and a
+status command. It drives NM underneath, so the panel sees its connections
+through the existing NM event path. Spec changes: the proton backend calls
+protonvpn connect/disconnect/status instead of device-detection
+(can_toggle true); archsetup installs proton-vpn-cli and drops
+proton-vpn-gtk-app (the two can't run concurrently per the project README —
+untested locally); the imported NM wireguard configs stay as a raw fallback.
+Sources: [[https://protonvpn.com/support/linux-cli][Proton Linux CLI guide]],
+[[https://protonvpn.com/support/release-notes-linux-cli][CLI release notes]],
+[[https://github.com/ProtonVPN/proton-vpn-cli][proton-vpn-cli repo]].
+* Implementation phases
+
+1. overlays.py probes (tailscale JSON, nmcli wireguard filter, proton-vpn-cli
+ status) — pure engine, TDD with fake binaries; =net status= grows an
+ overlays section.
+2. Panel Tunnels group + Up/Down wiring through the worker thread; AT-SPI
+ smoke extension.
+3. Diagnose/doctor overlay awareness (route/DNS ownership step, classifier
+ rows, evidence text) — TDD against the diag harness.
+4. waybar-net tunnel badge on the net glyph (v1 per the bar-indicator
+ decision), riding phase 3's route-ownership detection; suite coverage.
+5. archsetup: tailscale operator flag in the tailscale install step;
+ proton-vpn-cli replaces proton-vpn-gtk-app in the package list; VM test
+ assertions.
+6. One-time per-machine migration: import the seven assets/wireguard-config
+ configs into NM with autoconnect off (scriptable; both daily drivers).
diff --git a/docs/specs/2026-07-02-timer-panel-spec.org b/docs/specs/2026-07-02-timer-panel-spec.org
new file mode 100644
index 0000000..c0dbd2c
--- /dev/null
+++ b/docs/specs/2026-07-02-timer-panel-spec.org
@@ -0,0 +1,119 @@
+#+TITLE: Timer GTK Panel
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-02
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Timer GTK Panel
+:PROPERTIES:
+:ID: 25ed5321-f035-42b3-b115-69364d775f41
+:END:
+- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to DRAFT (evidence-based, human-confirmed)
+
+* DRAFT Status
+:PROPERTIES:
+:ID: 1770af2e-b093-4024-a512-ae4324a2869f
+:END:
+- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture "give the
+ timer a gtk UI/UX like the network panel. spec this out."
+
+* Metadata
+
+| Field | Value |
+|--------+---------------------------------------------------|
+| Status | draft |
+|--------+---------------------------------------------------|
+| Owner | Craig Jennings |
+|--------+---------------------------------------------------|
+| Repo | dotfiles |
+|--------+---------------------------------------------------|
+| Kin | net panel (architecture donor), wtimer (backing), |
+| | desktop-settings panel spec (sibling) |
+|--------+---------------------------------------------------|
+
+* Problem
+
+The timer's whole UI is a chain of three fuzzel prompts (type, value, label)
+plus a fourth for cancel. That flow can't show what's already running while
+you create, can't offer one-tap presets, gives no feedback on a typo until
+the add silently fails, and pomodoro state (phase, cycle) is only visible in
+a tooltip. The 2026-07-02 styling pass made the dialogs presentable, but the
+shape is still four blind modals for what is really one small control
+surface.
+
+* Goals
+
+1. One panel, opened from the bar's timer module, that shows everything
+ running (live countdowns, pomodoro phase/cycle, paused state) and creates
+ new items without leaving it.
+2. One-tap presets for the common cases (tea, pomodoro, quick alarm) next to
+ freeform entry, with inline validation before the add.
+3. Per-item controls: pause/resume, cancel, promote to primary (the bar
+ glyph slot).
+4. wtimer stays the single owner of timer state and the notification path;
+ the panel is a view over it, never a second engine.
+
+* Design sketch
+
+** Architecture — clone the net panel's proven stack
+
+- GTK4 + gtk4-layer-shell dropdown anchored under the timer module, Blueprint
+ .blp compiled to committed .ui (=make ui=; compiler is dev-only).
+- Humble-object split: GTK-free PanelModel presenter, unit-tested to 100%,
+ with thin widget bindings; one gated AT-SPI smoke via the
+ run-panel-smoke.sh pattern.
+- Backing: shell out to the existing wtimer CLI (=add=, =toggle=, =cancel=,
+ =cycle=, =render=). =render= already emits a JSON payload; the panel polls
+ it (or subscribes to the same RTMIN+14 refresh signal) for live state.
+ wtimer's 89-case suite keeps owning the logic; panel tests fake the CLI
+ like every dotfiles suite fakes binaries.
+- Dupre WIP palette CSS shared with the net panel (same factoring the
+ desktop-settings spec calls for — one palette asset, three panels).
+
+** Layout sketch
+
+- Header row: running-item count + a Clear All button (maps to cancel-all).
+- Item list: one row per item — type glyph, label, live countdown / clock
+ time / phase+cycle for pomodoro, pause and cancel buttons, click-to-promote.
+- Create strip: four type buttons (the wtimer glyphs), preset chips per type
+ (e.g. 5m / 15m / 25m / 60m for timers), a freeform entry validated with
+ wtimer's own parsers, an optional label field.
+- Empty state: the create strip alone, centered.
+
+** What happens to the fuzzel flow
+
+The keybind/fuzzel path stays as the keyboard-fast lane (it's now styled and
+tested); the panel replaces the click-driven path on the bar module. Whether
+the fuzzel chain eventually retires is a decision below.
+
+* Decisions (Craig)
+
+** TODO Panel scope: standalone timer panel, or a page in the desktop-settings panel?
+The desktop-settings spec (sibling DRAFT) could host timers as a page.
+Standalone matches the net panel's one-domain-one-panel shape and keeps the
+timer dropdown small; folding in means one panel binary fewer. Recommend
+standalone, sharing the palette/css asset.
+
+** TODO Fuzzel flow: keep as keyboard fast lane, or retire once the panel lands?
+Keeping both costs two creation paths to maintain (though the fuzzel chain is
+small and freshly tested). Recommend keep until the panel proves itself, then
+revisit.
+
+** TODO Presets: which chips per type?
+Strawman: timer 5m/15m/25m/60m; alarm +30m/top-of-hour/07:00; pomodoro
+default cycle only; stopwatch needs none. Adjust to taste.
+
+** TODO Live updates: poll render (1s, like the bar) or a wtimer "watch" mode?
+Polling reuses what exists and matches the bar's cadence; a watch/subscribe
+mode is cleaner but grows wtimer. Recommend polling first.
+
+* Implementation phases
+
+1. PanelModel presenter + CLI-backing seam (TDD, GTK-free, 100% like the net
+ PanelModel).
+2. Blueprint UI: item list + create strip, wired to the presenter; palette
+ css factored to the shared asset.
+3. Bar integration: timer module left-click opens the panel (replacing the
+ fuzzel menu binding there), RTMIN+14 refresh keeps bar and panel in step.
+4. AT-SPI smoke + manual-testing checklist; decide the fuzzel flow's future
+ after a week of real use.
diff --git a/docs/specs/2026-07-03-audio-panel-spec.org b/docs/specs/2026-07-03-audio-panel-spec.org
new file mode 100644
index 0000000..82041ed
--- /dev/null
+++ b/docs/specs/2026-07-03-audio-panel-spec.org
@@ -0,0 +1,166 @@
+#+TITLE: Audio Panel — the pulsemixer console
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-03
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Audio Panel — the pulsemixer console
+:PROPERTIES:
+:ID: 9175e017-46ad-4887-ae45-887e9551c005
+:END:
+- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Shipped; build task DONE, manual tests filed.)
+
+* IMPLEMENTED Status
+:PROPERTIES:
+:ID: 71f556c6-ee02-47cc-a3be-68c8289380f3
+:END:
+- [2026-07-03 Fri] IMPLEMENTED — built in a no-approvals speedrun in the
+ dotfiles repo (branch panel-bugfixing): engine (pactl), presenter, GTK
+ panel, PTT arming, bar indicator, and the bar/keybind wiring, across four
+ commits 65e5bb0..9601420. 102 unit tests + a passing AT-SPI smoke on velox.
+ All five Decisions below resolved. Live-eyeball validation (visual polish,
+ PTT-in-a-meeting, fader feel, the master-mute hardware key) is the one open
+ follow-up, tracked as a manual-testing task in todo.org.
+- [2026-07-03 Fri] DRAFT — stub from the todo.org task "Audio panel spec"
+ (roam ask 2026-07-02) plus the 2026-07-03 waybar/sound design discussion.
+ Written to iterate alongside the prototype
+ (=working/sound-panel/sound-panel-prototype.html=). Spine is present; the
+ Decisions and Design detail get filled in as we go.
+
+* Metadata
+
+| Field | Value |
+|--------+---------------------------------------------------|
+| Status | implemented |
+|--------+---------------------------------------------------|
+| Owner | Craig Jennings |
+|--------+---------------------------------------------------|
+| Repo | dotfiles |
+|--------+---------------------------------------------------|
+| Kin | net panel + bt panel (architecture + aesthetic |
+| | donors), desktop-settings panel (sibling) |
+|--------+---------------------------------------------------|
+
+* Problem
+
+Audio control today is the pyprland audio scratchpad (Super+A) — a floating
+pulsemixer TUI — plus scattered bar affordances: =pulseaudio= (volume, click
+to mute sink), =pulseaudio#mic= (mic glyph + mic-toggle), Super+M audio-cycle
+ring, Super+Shift+A mic-toggle. There's no single glanceable surface that
+shows every sink and source, lets you set the default output/input, and
+carries the meeting-grade mic controls Craig wants (a clean muted mode and a
+hold-to-talk mode). The net + bluetooth panels set the pattern for exactly
+this shape; audio is the third instrument in the family.
+
+* Goals
+
+1. One panel, opened from the bar's sound glyph, exposing the full pulsemixer
+ surface: every sink and source, per-device volume, per-device mute, and
+ switching the default output and input.
+2. Replace the pyprland audio scratchpad (Super+A) as the primary audio UI.
+3. Mic modes for meetings: *live*, *muted*, and *push-to-talk* (mic stays
+ muted except while Space is held, releasing re-mutes).
+4. A *master quick-mute* — one action mutes all output — reachable from the
+ faceplate and a keybind.
+5. Instrument-console aesthetic and architecture consistent with net + bt:
+ same faceplate, lamps, engraved sections, console keys, needle gauges,
+ verify-everything contract.
+6. The bar glyph reflects live state: speaker + three arcs normally, a
+ speaker-with-✕ when muted (Craig's called glyphs).
+
+* Design sketch
+
+Prototype: =working/sound-panel/sound-panel-prototype.html= (the reference for
+layout + idioms below).
+
+** Surface (from the prototype)
+
+- *Faceplate* — status lamp, sound glyph, state word (PLAYBACK / MUTED), a
+ MUTED badge, the SND·01 unit label, and the *master quick-mute switch*
+ (same switch idiom as net wifi / bt power), plus the close ✕.
+- *OUTPUTS section* — one row per sink. Row body click = set default (gold
+ DEF tag). A machined fader sets that sink's volume; the trailing glyph
+ mutes just that device. Active/default row is emphasized (cream name, gold
+ lamp/glyph).
+- *INPUTS section* — one row per source, same idioms.
+- *Mic mode* — three console keys: LIVE / MUTED / PUSH·TALK. Push-to-talk
+ keeps the mic muted (red IN needle) and un-mutes only while Space is held.
+- *Twin VU needles* — output level + input level, the sound analog of net
+ throughput and bt battery gauges. Needle goes red when its side is muted.
+
+** Architecture — clone the net/bt panel stack
+
+- GTK4 + gtk4-layer-shell, Blueprint =.blp= → committed =.ui= (=make ui=,
+ dev-only build dep).
+- Humble-object split: a GTK-free PanelModel presenter (unit-tested like the
+ net/bt PanelModels) + thin composite-widget pages. Backing actions in a
+ GTK-free =audio.py= that shells to the audio control layer (pactl /
+ wpctl / pulsemixer — pick below), TDD'd with fake binaries.
+- One gated AT-SPI smoke (=run-panel-smoke.sh= pattern).
+- Shared instrument-console palette CSS asset (the one net/bt/settings all
+ load) — do not duplicate the palette block.
+- Code lives in dotfiles =audio/= sibling to =net/= (src-layout, tests in
+ =tests/audio/=).
+
+* Decisions (Craig)
+
+** DONE Audio control backend — pactl vs wpctl vs pulsemixer
+CLOSED: [2026-07-03 Fri]
+Resolved: =pactl= (the engine module is =pactl.py=). Both ratio and velox run
+PipeWire with the pipewire-pulse compat layer and no PulseAudio daemon, so
+pactl and wpctl hit the same graph — but =pactl -f json= gives structured,
+name-addressable output where wpctl offers only a volatile-id tree. Reads go
+through =pactl -f json list sinks|sources= + =get-default-*=; writes target
+devices by stable name behind an argv-charset guard.
+
+** DONE Push-to-talk mechanism under Wayland (feasibility — phase 1)
+CLOSED: [2026-07-03 Fri]
+Resolved: route (a), Hyprland dynamic binds. The phase-1 spike confirmed all
+three primitives on velox (Hyprland 0.55.4): =hyprctl keyword bind/unbind=
+adds and removes a bind live, =bindr= fires on release, and =pactl
+set-source-mute @DEFAULT_SOURCE@ 0|1= toggles the mic cleanly. =ptt.py= arms a
+press bind (un-mute) + a bindr (re-mute) on entering PTT mode and unbinds on
+leaving, so the talk key isn't grabbed globally otherwise. No evdev needed.
+Documented behavior: while PTT is armed, the talk key is the talk key.
+
+** DONE Quick-mute keybind + scope
+CLOSED: [2026-07-03 Fri]
+Resolved: the XF86AudioMute hardware key (Super+Shift+M turned out to be taken
+by the monocle-layout bind, so the spec's assumption was wrong). The mute key
+now runs =audio quick-mute=, which mutes every output (master), not just the
+default sink — identical on a single-sink machine, correct on a multi-sink
+one. Also reachable from the faceplate master switch and the panel. Scope:
+master mute of all sinks, with verify-after-apply per sink.
+
+** DONE Bar glyph click map
+CLOSED: [2026-07-03 Fri]
+Resolved with the low-regret wiring: kept the existing =pulseaudio= waybar
+module (left-click mute, scroll volume — no regression) and repointed its
+right-click from the retired pulsemixer scratchpad to =audio-panel=. So: left
+= mute, right = open panel, scroll = volume. A fuller =custom/audio= indicator
+(state-following speaker glyph + its own click map) is built and tested
+(=indicator.py= + =waybar-audio=) but stays unwired until the new bar glyph
+gets a live eyeball — the swap is a one-line waybar edit when Craig's ready.
+
+** DONE Fate of the existing audio affordances
+CLOSED: [2026-07-03 Fri]
+Resolved: Super+A repurposed from =pypr toggle audio= (the pulsemixer
+scratchpad) to =audio-panel= — the panel is the primary audio UI now, so the
+scratchpad is retired. Its definition still sits in the machine-local
+=pyprland.toml= (not stowed) and can be deleted by hand. Kept: =pulseaudio= +
+=pulseaudio#mic= waybar modules (glance + scroll + the mic-mute glance),
+Super+M cycle, Super+Shift+A + XF86AudioMicMute mic-toggle. Changed:
+XF86AudioMute → master quick-mute (see the quick-mute decision above).
+
+* Implementation phases
+
+1. Push-to-talk feasibility spike (decision above) — the one unknown; settle
+ the mechanism before committing the mic-mode design.
+2. =audio.py= backings (list/get/set/mute/default for sinks + sources) —
+ pure engine, TDD with a fake audio backend.
+3. PanelModel presenter (rows, default tracking, mic modes, master mute,
+ verify-after-apply) — unit-tested, no GTK.
+4. Blueprint UI + sound bar glyph (normal / muted / ptt states) + open/close
+ wiring; shared palette css; AT-SPI smoke.
+5. Bar-affordance consolidation per the decision above; retire the Super+A
+ scratchpad; keybinds.
diff --git a/docs/specs/2026-07-03-instrument-console-panels-spec.org b/docs/specs/2026-07-03-instrument-console-panels-spec.org
new file mode 100644
index 0000000..c0a0c56
--- /dev/null
+++ b/docs/specs/2026-07-03-instrument-console-panels-spec.org
@@ -0,0 +1,164 @@
+#+TITLE: Instrument-console rebuild — net + bluetooth panels
+#+DATE: 2026-07-03
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Instrument-console rebuild — net + bluetooth panels
+:PROPERTIES:
+:ID: ac23e996-a51a-466b-ad80-2faff46447bf
+:END:
+- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Panel rebuild shipped (dotfiles e993c3f); build task DONE.)
+
+* IMPLEMENTED Status
+:PROPERTIES:
+:ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5
+:END:
+- 2026-07-03 Fri @ 06:49 -0400 :: DOING → IMPLEMENTED: all six phases shipped
+ (net GTK-free layer 81ec9c3, net view 800ef60; bt GTK-free layer 5318b34, bt
+ view 66f03d9; phase-6 dead-code removal f4e688e). Both panels are single-screen
+ instrument consoles, verified live on velox — 46 suites + full make test green,
+ both AT-SPI smokes green end to end, render matching the approved prototype. The
+ three folded tasks (network panel redesign, bt switch placement + title, bt
+ rename devices) closed with the build.
+- 2026-07-03 Fri @ 02:07 -0400 :: DRAFT → READY → DOING in one stroke: Craig
+ approved the design through five interactive prototype iterations and
+ authorized the no-approvals speedrun ("let's build them now... go"). The
+ review gate was the live prototype session itself.
+- 2026-07-03 Fri @ 02:07 -0400 :: Created (DRAFT) from the prototype session.
+
+* Metadata
+
+| Field | Value |
+|---------------------+------------------------------------------------------------|
+| Status | implemented |
+|---------------------+------------------------------------------------------------|
+| Owner | Craig Jennings |
+|---------------------+------------------------------------------------------------|
+| Repos | dotfiles (net/, bluetooth/, themes), archsetup |
+|---------------------+------------------------------------------------------------|
+| Normative reference | [[file:../../assets/2026-07-03-instrument-console-panels-prototype.html][assets/2026-07-03-instrument-console-panels-prototype.html]] |
+|---------------------+------------------------------------------------------------|
+
+* Summary
+
+Rebuild both GTK layer-shell panels (net, bluetooth) from the tabbed layout
+to the instrument-console design: one screen, no tabs, a faceplate with a
+state word + badges + radio switch + close, engraved section labels, lamp
+rows that act on click, dial meters under the console keys, and a doctor
+that does it all. The interactive prototype =panel-console-v3.html= is the
+normative design reference — when this spec and the prototype disagree on a
+visual or interaction, the prototype wins.
+
+* Decisions (all resolved — Craig, prototype session 2026-07-02/03)
+
+- Replace the tabbed panels outright. No fallback flag; git history is the
+ rollback. Net panel first, bluetooth second.
+- Advanced repair tiers leave the panel entirely. DOCTOR runs the full
+ diagnose → classify → lightest-repair → re-verify escalation (the engine
+ already does this). The surgical tiers stay CLI-only (=net repair ...=).
+- Faceplate (both panels): state lamp + state word, badges, unit label
+ (NET·01 / BT·01), radio switch (wifi radio / bt adapter power), close ✕.
+ Badges: TUNNEL (gold, net), AIRPLANE (gold, both), LOW BATT (red, bt).
+- Sections in order — net: CHANNEL, NETWORKS (+ hidden action), TUNNELS,
+ CONSOLE (DOCTOR / SPEED TEST keys), meters, output. bt: ADAPTER (with
+ clickable =discoverable= chip), PAIRED, NEARBY (+ scanning note), CONSOLE
+ (DOCTOR / SCAN), battery gauges, output.
+- Section row budgets, half-row peek, internal scroll (thin slate
+ scrollbar): NETWORKS 5.5 rows, TUNNELS 4.5, PAIRED 5.5, NEARBY 4.5.
+ In-range networks sort active-first then strongest-signal-first. Counts on
+ the engraved headers ("networks · 12 in range", "tunnels · 1 up of 9",
+ "paired · 3", "nearby · 12"). The panel silhouette never grows with list
+ length; only the output well is variable and it caps at ~170px.
+- Lamp-row grammar: green = live/connected, gold = available/actionable,
+ off = down/stored, red = failed; busy = pulsing gold during transitions.
+ Rows act on click (tunnels toggle, networks join, paired devices
+ connect/disconnect toggle, nearby devices pair).
+- Arm-first for anything disruptive or destructive, 3s auto-disarm:
+ - forget (network or bt device): hover reveals ✕; first click arms the
+ row terracotta ("forget? click ✕ again"), second fires. No dialog.
+ - disconnect (active network): click the active row; first click arms in
+ GOLD ("disconnect? click again") — disruptive, not destructive — second
+ fires.
+- Meters (net): two dials, RX·DOWN / TX·UP, gold needles, mode tag top-left
+ (LIVE green / TEST gold), HOLD tag top-right. Idle: live link throughput.
+ Speed test: cards flash gold, needles sweep the measured rate, then PIN
+ the final value with HOLD; clicking a held meter releases it to LIVE.
+ Scale 0–100 Mbps, auto-relabel to 0–1000 when a reading exceeds 100.
+ Dial top inset ~13px so the corner tags never touch the arc.
+- Speed test output well gets ONLY: location line ("location: <city> by
+ <sponsor>"), ping (+jitter), final line, conditioned tip(s). The rates
+ live in the meters, not the text.
+- Battery gauges (bt): same dial chrome; one per connected device (two
+ slots; empty slot dim "NO DEVICE"/"ADAPTER OFF"); needle+value red under
+ 15% and the LOW BATT faceplate badge lights.
+- Output well: doctor streams the checks with their narration lines
+ (viewmodel.STEP_NARRATION) and repair steps in gold; verdict line closes
+ (olive for pass/fixed). A dismiss ✕ appears in the well's corner whenever
+ it has content. Both panels.
+- WiFi radio switch: =nmcli radio wifi on|off=. Off empties NETWORKS to one
+ dim "wifi radio off" row, drops the connection, kills tailscale rows'
+ reachability; on rejoins the last network (NM autoconnect does this for
+ real). Airplane mode is system-level (Super+Shift+A owns it): both panels
+ reflect it (state word AIRPLANE, gold badge, switches down); a switch
+ flipped under airplane refuses with a toast naming the exit. A routed
+ ethernet link keeps the net panel ONLINE through airplane mode.
+- Ethernet: presence-based row pinned atop NETWORKS when a cable is up
+ ("enp… · active · wired · 1.0 Gbps" / "wired · standby"); CHANNEL swaps
+ the signal ladder for "wired · <speed> full-duplex" when routed; clicking
+ the row toggles route ownership via device disconnect/connect.
+- Pairing (bt): nearby row click → busy → passkey-confirm dialog (large
+ gold digits) → device moves to PAIRED and connects. SCAN key refreshes
+ with a "scanning…" note on the header.
+- Rename (bt): hover ✎ on a paired row → dialog prefilled → bluez
+ =set-alias= (closes the filed rename task).
+- Tooltips: any ellipsized row label carries its full text as the tooltip.
+- Dialogs (join / hidden SSID / passkey / rename) keep the in-panel dupre
+ dialog style (gold border, dark well inputs, gold caret).
+- Close: ✕ on the faceplate + Esc (already shipped; keep).
+- Folded tasks: "Network panel redesign", "Bluetooth panel: switch placement
+ + panel title", "Bluetooth panel: rename devices" — all close with this
+ build's phases.
+
+* Engine gaps (small, close during phases)
+
+- radio verb: =nmcli radio wifi on|off= helper (manage or sysio) + tests.
+- hidden-SSID join: =manage.add= grows a hidden flag
+ (=802-11-wireless.hidden yes=).
+- ethernet: device rows from =nmcli dev= (type ethernet) + disconnect/
+ connect verbs (device-level; =net down --iface= already disconnects).
+- bt rename: btctl =set-alias= one-shot verb + verify-after read.
+- bt battery: already exposed (indicator uses it).
+- speedtest meters: =run_speedtest_stream= on_update already ticks (pty).
+- link speed for wired channel line: =ethtool=-free read from
+ =/sys/class/net/<dev>/speed=.
+
+* Implementation phases
+
+1. [X] Spec + task wiring (this file; todo.org parent task with :SPEC_ID:).
+2. [X] Net GTK-free layer (TDD): viewmodel row composers for the console
+ sections (network rows sorted+counted, tunnel rows, channel facts,
+ faceplate state word derivation, meter scale logic, arm state machines
+ for forget/disconnect), PanelModel restructure (sections, no tabs).
+ Engine gaps: radio verb, hidden join, ethernet rows, wired link speed.
+3. [X] Net view rebuild: gui.py single-page console built in Python
+ (faceplate, engraved scrolled sections, console keys, cairo dial meters
+ with mode/hold tags, output well + dismiss), panel.css additions
+ (engrave, lamps, dial, badges, arm tints). AT-SPI smoke + driver
+ rewritten for the console layout. Shipped with phase 4 (dotfiles
+ 800ef60): a view-only intermediate is a broken panel (rows and switches
+ that do nothing), so view + interactions landed together.
+4. [X] Net interactions: join/hidden/forget (arm terracotta)/disconnect
+ (arm gold)/radio switch/ethernet toggle/doctor stream/speed-test-drives-
+ meters, toasts. Verified live on velox (DOCTOR streams, SPEED TEST sweeps
+ both dials then HOLD). Shipped in dotfiles 800ef60 with phase 3.
+5. [X] Bluetooth panel: same treatment end to end (faceplate + power
+ switch, adapter chip, paired/nearby lamp rows, pair passkey flow,
+ rename via set-alias, forget arm, battery gauges + LOW BATT, DOCTOR /
+ SCAN keys, output). bt smoke rewritten. Shipped in two commits mirroring
+ net: dotfiles 5318b34 (GTK-free layer + engine gaps) and 66f03d9 (view +
+ interactions + smoke). rename lands on the bluez Alias via busctl
+ (set-alias has no MAC-addressed one-shot); verified live on velox (smoke
+ green end to end, screenshot matches the prototype).
+6. [X] Live verification both panels on velox + all suites + smokes green;
+ summary of findings written to file; folded tasks closed; dead code
+ removed; session context finalized.