aboutsummaryrefslogtreecommitdiff
path: root/archive
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-10-01 22:59:51 -0500
committerCraig Jennings <c@cjennings.net>2026-10-01 22:59:51 -0500
commitd535b4362f07df83c805f1924ccdf663eb978ae1 (patch)
treec088714a10046f33eefb46d62d690e6a61655150 /archive
parentb4a713c1f47ac5bb3cd056d9b3bdafadff935b4d (diff)
downloadarchsetup-d535b4362f07df83c805f1924ccdf663eb978ae1.tar.gz
archsetup-d535b4362f07df83c805f1924ccdf663eb978ae1.zip
chore(tasks): archive the closed waybar net module and emacs pull tasksHEADmain
Diffstat (limited to 'archive')
-rw-r--r--archive/task-archive.org754
1 files changed, 754 insertions, 0 deletions
diff --git a/archive/task-archive.org b/archive/task-archive.org
index dfd273d..f2edf58 100644
--- a/archive/task-archive.org
+++ b/archive/task-archive.org
@@ -3104,3 +3104,757 @@ the stage defaults; render to a non-stowed path and have hypridle read
that; or keep it tracked but commit a machine-neutral render. The first
looks right — the store already holds the real source of truth, and the
rendered file is a build artifact.
+** DONE [#A] Reseat velox input-cover ribbon — phantom power button :bug:velox:hardware:
+CLOSED: [2026-08-26 Wed] DEADLINE: <2026-08-26 Wed>
+:PROPERTIES:
+:CREATED: [2026-08-13 Thu]
+:LAST_REVIEWED: 2026-08-13
+:END:
+Machine off, lift the input cover (Framework QR-guided procedure, 5
+fasteners), reseat its ribbon connector to the mainboard — disturbed in the
+2026-08-13 board swap. Root cause of every "mystery reboot" that day:
+chassis flex (flash-drive touch, ethernet bump, lid partially lowered)
+fired phantom power-button presses — journalctl -b -1 showed "Power key
+pressed short." → orderly logind poweroff, then the glitching button
+powered it back on. While in there, reseat the USB expansion cards too —
+the flaky slot (two hard resets, one no-enumeration) is likely the same
+flex problem.
+THIRD SYMPTOM (2026-08-13 evening): touchpad delivers ZERO input events —
+15s synchronized libinput debug-events capture while swiping caught
+nothing, though i2c enumeration and a driver rebind handshake are clean.
+Signature of a dead interrupt line on the same ribbon. Keyboard + power
+LED lines work; BT mouse is the interim pointer.
+ESCALATED 2026-08-13 21:00: a fourth event killed the machine THROUGH the
+shield. Previous boot's journal ends mid-line (tailscaled chatter) with no
+shutdown sequence at all — a hard power cut, not logind acting. So the
+glitch now reaches the EC/hardware power path, which no software setting
+can intercept. The reseat is the only fix, and this is a
+lose-work-without-warning failure mode, not an inconvenience.
+Interim shield (already live): /etc/systemd/logind.conf.d/powerkey.conf
+sets HandlePowerKey=ignore — phantom presses log but do nothing; EC-level
+10s hold still force-cuts. Consider keeping it even after the repair.
+Verify after reseat: flex the chassis edges + partially lower the lid, then
+grep the journal for new "Power key pressed" lines — zero means fixed.
+Must be done before the Sunday flight — a phantom press mid-travel with the
+shield on is survivable, but the connector should not be trusted at 30,000
+feet on the loose setting.
+
+*** 2026-08-19 Wed @ 14:40:00 -0700 Retracted: the RTC reset is not this task's, and I should not have filed it here
+I attributed the 2026-08-19 network outage to this ribbon earlier today. Craig
+pushed back — he reseated it before the trip to get the touchpad working — and
+he is right. The evidence does not support the attribution and some of it points
+the other way.
+
+What actually holds. Boot -3 ended at 01:33:18 with no shutdown sequence: no
+power-off target, no unmounting. The next boot's kernel line reads =rtc_cmos
+00:01: setting system clock to 2025-01-01T00:00:16 UTC=, a firmware default, so
+the RTC was reset rather than drifted. No firmware update was applied
+(=fwupdmgr get-history= is empty) and the battery is fine.
+
+What refutes the ribbon. This boot logged *zero* =Power key pressed= events, and
+so did the four boots before it. The phantom-press symptom had genuinely stopped
+after 08-15, exactly as the 08-16 session recorded. The earlier events logged a
+power-key press and an orderly poweroff; this logged neither, which makes it a
+different signature, not a worse version of the same one.
+
+What I got wrong methodologically: I anchored on the most salient open hardware
+task and read association as evidence. I even wrote "I can't prove it is the
+same connector" and then filed it here anyway, which is the tell.
+
+Two things I checked and can rule out. There were no OOM kills — the 3,433
+matching lines are a systemd unit named "Periodically re-score Claude Code
+processes for the OOM-killer" firing on a timer, not memory pressure, and there
+is not a single "Killed process" line. Thermal is clean; the only mentions are
+boot-time zone registration at 34C and 45C.
+
+One real thing the same window did surface, tracked separately: a python3 crash
+loop, 251 core dumps in the final ten minutes, SIGABRT with =XFreeThreads= and
+=PyEval_RestoreThread= in the trace. It does not explain the RTC, because
+software cannot clear it, but it is its own problem.
+
+The open question that would settle the RTC is for Craig, not the journal: a
+long power-button hold on a Framework triggers an EC-level reset that clears the
+RTC, which fits a wedged machine being forced off. A 4-second hold would not.
+
+*** 2026-08-17 Mon @ 19:57:42 -0700 Not done, and the two symptoms now disagree
+The reseat did not happen before the flight, and velox is travelling. The
+deadline blew past on 08-14.
+
+The two symptoms have separated, which is worth recording because it changes
+what the evidence proves. The phantom presses have stopped: fifteen "Power key
+pressed" entries between 08-14 04:29 and 08-15 20:04, then nothing at all
+across five boots including today's. The touchpad has not — there is still no
+touchpad node under =/dev/input/by-path/=, which is the same dead interrupt
+line the body describes.
+
+So the quiet power button is not evidence the connector reseated itself. The
+interrupt line is the symptom that cannot be masked in software, and it is
+still dead, so the ribbon is still unseated. The most likely reason the
+presses stopped is that the machine has been sitting on hotel surfaces instead
+of being carried and flexed.
+
+The interim shield is still live (=HandlePowerKey=ignore=), and the escalation
+note stands: an EC-level glitch cuts power below systemd regardless of it.
+*** 2026-08-15 Sat @ 23:05:00 -0500 The reseat did happen, and the touchpad came back — this contradicts the 08-17 read
+Recording this because a parallel session concluded on 08-17 that the reseat had
+not happened and the touchpad was still dead. Both halves were done and verified
+that night, so the two accounts disagree and the disagreement should be visible
+rather than silently resolved by whichever session committed last.
+
+What was done: the input-cover ribbon was reseated first, which fixed the
+phantom power button — the 22:09 boot logged zero =Power key pressed= lines
+after Craig flexed the chassis, against nine on the boot before. The touchpad
+did not change, because the input-cover ribbon is not its connector. The 4-pin
+connector beside the printed =TOUCHPAD= label is silkscreened =PIN 1-2 GND /
+PIN 3-4 VCC= — pure power, so it cannot carry i2c or an interrupt. Reseating the
+ribbon that actually crosses to the mainboard fixed it.
+
+Measured, not assumed: the touchpad interrupt (=amd_gpio= pin 8) went from 0
+counts across all 24 CPUs to 1795, and =i2c_hid_acpi ... did not ack reset
+within 1000 ms= disappeared from the boot log. Craig confirmed the pointer moved.
+
+*Why the 08-17 probe likely misread it:* it checked for a node under
+=/dev/input/by-path/=. i2c-HID touchpads frequently get no =by-path= symlink
+even when fully working, so its absence is not evidence of a dead interrupt
+line. The falsifiable check is the interrupt count in =/proc/interrupts= while
+the pad is being touched, or the reset message in =dmesg=.
+
+*Left open rather than closed* — velox was refusing ssh at merge time on 08-20,
+so the current state could not be re-verified, and a later regression cannot be
+ruled out. One second of Craig's time settles it: move the pointer. If it works,
+close this; if it does not, the interrupt line went back down and that is new
+information.
+
+*** 2026-08-26 Wed @ 22:30:46 -0600 Closed: the reseat was done on 08-15 and the task was never marked
+I reseated the ribbon on 2026-08-15 and never closed this. The 08-15 entry
+above already records the verification: zero =Power key pressed= lines on the
+22:09 boot after flexing the chassis, the touchpad interrupt count back up
+once the right connector was reseated. This boot shows zero presses as well.
+The interim shield (=HandlePowerKey=ignore= in
+=/etc/systemd/logind.conf.d/powerkey.conf=) is still live; I'm leaving it in
+place, since a phantom press with it on costs nothing and without it costs
+the session.
+** CANCELLED [#B] agent-text relay reports success for a message that went nowhere :bug:
+CLOSED: [2026-08-19 Wed]
+:PROPERTIES:
+:CREATED: [2026-08-19 Wed]
+:LAST_REVIEWED: 2026-08-19
+:END:
+
+Not a defect. rulesets refuted it with measurements and I reproduced theirs
+before accepting: on velox, whose account store is empty,
+=signal-cli -a +15550000000 send= exits 1 with "User +15550000000 is not
+registered", and =ssh 100.71.182.1 'exit 7'= returns 7, so a non-zero code
+propagates faithfully back through the relay. The loop's
+=[ "$rc" -eq 0 ] && break= therefore advances to the next host exactly as
+intended. signal-cli fails closed.
+
+I filed this off a conditional in their handoff — ".emacs.d raised a case
+neither of you tested ... *if* signal-cli send exits zero against an empty
+account store" — and turned the "if" into a graded [#B] with a =:blocked:= tag
+on another project, without running the one command that settles it. The
+machine that proves it was in front of me the whole time. Their ask is fair and
+I am recording it rather than the outcome alone: verify before filing a defect
+against someone else's work, especially one carrying a blocking tag.
+** DONE [#B] Clock/DNS bootstrap deadlock — recovery needs a second device :bug:velox:
+CLOSED: [2026-08-19 Wed]
+:PROPERTIES:
+:CREATED: [2026-08-19 Wed]
+:LAST_REVIEWED: 2026-08-19
+:END:
+
+The installer wrote both halves of a deadlock. =configure_dns= pins
+=DNSOverTLS=yes= with =DNSSEC=yes=, and both validate against the wall clock;
+the chrony step enables chronyd without writing a config, so the machine runs
+Arch's stock one whose only source is =pool 2.arch.pool.ntp.org= — a hostname.
+Boot with a wrong clock and DoT certificate validation fails, so nothing
+resolves; chrony then cannot resolve its pool, so the clock stays wrong.
+Neither side moves. It caught velox on the road 2026-08-19 and had to be
+diagnosed from a phone.
+
+Fixed at the root: the installer now writes
+=/etc/chrony.d/10-bootstrap-ip-ntp.conf= with two IP-addressed Cloudflare
+sources and points stock chrony.conf at the drop-in. An address needs no DNS
+and carries no certificate, so the escape hatch holds whatever broke the clock.
+velox has the same drop-in applied live, verified with =chronyc -n sources=
+(=162.159.200.1= selected) and =timedatectl= reporting synchronized.
+
+What is left here is the part I could not verify: the decisive test is a full
+power-down and cold boot, confirming the clock corrects itself untouched. See
+the manual-testing entry. Until that runs, the fix is sound by construction
+rather than demonstrated.
+
+Grading: Critical severity (total loss of network — no DNS means no egress, and
+recovery needs a second device) x some users sometimes (only machines that boot
+with a wrong clock, which is any RTC fault, BIOS reset, or drained cell) = P2 =
+[#B]. Graded on the being-in-it, not the getting-into-it: once the machine is in
+this state it is fully offline with no local path out.
+
+*** 2026-08-19 Wed @ 12:25:00 -0700 Reproduced it, and the mechanism was not what either of us said
+I wound velox's clock back 27 days with chronyd stopped and watched it fail.
+Resolution died outright, and plain UDP/53 to 1.1.1.1 kept answering throughout
+— the discriminator the doctor keys on, confirmed live rather than reasoned.
+
+The cause is DNSSEC, not DNS-over-TLS. resolved logged =signature-expired=
+against the root DNSKEY and every DS beneath it. The DoT handshake to
+=1.1.1.1:853= verified clean at that same clock, and the Cloudflare certificate
+runs Dec 2025 to Dec 2026, so it was never outside its window. An RRSIG window
+is days to weeks and a certificate is good for a year, so a skew that breaks
+DNSSEC normally leaves DoT untouched. The phone session blamed the certificate
+and I carried that forward into the first commit; both were wrong.
+
+=DNSSEC=allow-downgrade= does not rescue it either, which matters because it is
+the obvious reach and it is what ratio runs. resolved downgrades when a server
+lacks DNSSEC support, and a signature-window failure is a validation failure, so
+no downgrade fires. Six retries over eighteen seconds plus
+=resolvectl reset-server-features=, all dead. I briefly believed otherwise off a
+test whose success was a cache hit (=Data from: cache network=).
+
+So ratio was exposed after all, and I have given it the same drop-in. Its
+=162.159.200.1= is selected and its clock is synchronized.
+
+The fix itself is verified end to end: with the clock wound back and no DNS at
+all, chronyd reached the IP-addressed source and stepped the clock from
+2026-07-23 straight back to 2026-08-19. That is the whole claim, demonstrated
+rather than argued.
+
+Also settled: the clock landed on 2026-07-23 because that is systemd 261.2's
+build date to the minute (=/usr/lib/systemd/systemd=, 10:43:59), and systemd
+advances a garbage RTC to its own build epoch at boot. Not timesyncd's
+last-good-sync timestamp, which cannot be it — timesyncd is disabled here. That
+also confirms the RTC really was reading earlier than that, so the coin cell
+stays the prime suspect.
+
+*** 2026-08-19 Wed @ 10:12:00 -0700 Root fix, doctor verdict, and taxonomy entry landed
+The installer carries the drop-in; =post-rebuild-check= grew a sixth check that
+fails a machine whose every NTP source is a hostname; the net failure taxonomy
+gained the mode in its DNS layer plus a cluster 5 triage line, and its existing
+egress-layer clock entry now says outright that its remedy does not apply when
+DoT or DNSSEC is on.
+
+The doctor half is in dotfiles: =classify.py= reached "DNS not resolving → net
+repair dns-test" here, which cannot help, because every public resolver fails
+the same clock-sensitive validation — so the doctor sent you round a loop. It
+now emits a =clock-dns= row ahead of the generic DNS verdict. Detection is
+deliberately DNS-free: a local =timedatectl= read for sync state, and a bypass
+query addressed by IP over plain UDP/53 to tell "resolved is refusing to
+validate" apart from "DNS is genuinely dead".
+** DONE [#C] DNSSEC strictness on the travelling laptop :velox:
+CLOSED: [2026-08-19 Wed]
+:PROPERTIES:
+:CREATED: [2026-08-19 Wed]
+:LAST_REVIEWED: 2026-08-19
+:END:
+
+Craig chose =allow-downgrade= everywhere. Applied to velox, ratio, and the
+installer, and ratio's =DNSOverTLS= tightened from =opportunistic= to =yes= in
+the same pass, so all three now agree: encrypted DNS always, validation
+best-effort.
+
+The reasoning that settled it: the deadlock is fixed by the IP-addressed NTP
+source, and =allow-downgrade= was measured not to help with it at all. What
+=allow-downgrade= does buy is the venue-resolver case the taxonomy documents,
+where =yes= turns a resolver that mangles DNSSEC records into no answer at all.
+That is a hotel and airport problem, so it is velox's problem, and the
+encryption is the half worth being strict about.
+** CANCELLED [#C] Branch network policy on laptop vs desktop in the installer :feature:
+CLOSED: [2026-08-19 Wed]
+:PROPERTIES:
+:CREATED: [2026-08-19 Wed]
+:LAST_REVIEWED: 2026-08-19
+:END:
+
+Cancelled because the decision above emptied it. All three motivating cases now
+want the same value on every machine: =DNSSEC=allow-downgrade=, a stable
+per-network wifi MAC, and an IP-addressed NTP source. A branch with nothing to
+put on either side is machinery built for a divergence that does not exist, and
+it would be the kind of scaffolding that rots unread.
+
+Worth keeping the observation, which is the part with a shelf life: when a
+network default does need to differ by machine class, the test already exists.
+=ls /sys/class/power_supply/BAT*= is what =prune_waybar_battery=, the ppd mask,
+and the TLP config all key on. Reopen this then rather than building it now.
+** DONE [#C] Automate the clock/DNS deadlock repair in the net doctor :feature:
+CLOSED: [2026-08-19 Wed]
+:PROPERTIES:
+:CREATED: [2026-08-19 Wed]
+:LAST_REVIEWED: 2026-08-19
+:END:
+
+Shipped as the =clock-ip-ntp= repair, and the verdict is =fixable= rather than
+terminal. Both open questions got answered by driving a real deadlock instead of
+reasoning about it: =chronyc add server= returns =200 OK= against a running
+chronyd, and =makestep= needs a sample to land, so it took four calls and about
+eight seconds rather than working on the first. The repair retries accordingly.
+
+Verified end to end on velox against a genuine deadlock (wrong clock, chronyd
+running with only an unresolvable hostname source, DNS dead): the repair
+corrected the clock in 6.1 seconds and DNS came back.
+
+The live run also caught a defect no unit test would have. The doctor reported
+"Saved password for SpectrumSetup-3C was rejected" — because
+=_recent_auth_failure= greps =journalctl --since -5min=, and a clock weeks off
+windows onto a different incident's entries. It would have sent Craig to
+re-enter a password that was never wrong. Fixed twice over: the journal half is
+now skipped when the clock is untrustworthy, and the clock verdict is ordered
+above the auth verdict, since everything below it reasons over timestamps that
+only mean something once the clock is right. Airplane mode and hard rfkill stay
+above, being physical states the clock has no bearing on.
+** DONE [#D] net-scenarios harness times out under back-to-back suite runs :test:tooling:
+CLOSED: [2026-08-19 Wed]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+=tests/net-scenarios/test_run_net_scenarios.py= errored on all 5 tests twice during round 12, each time =subprocess.TimeoutExpired= after its 20s budget on =scripts/testing/run-net-scenarios.sh --target root@fake-vm=. Both occurrences were in =make test-unit= runs launched immediately after a previous full run. It then passed 6 runs in a row (3 on a pristine tree, 3 with the round-12 change), and standalone it finishes in 0.09s, so this is not a regression from any code change.
+
+The harness stubs =ssh=, =rsync= and =jq= onto =PATH=, so nothing should touch the network at all — which is what makes a 20s timeout suspicious rather than merely slow. Worth reproducing under load before deciding whether the fix is a larger timeout or a real hang in the script. Evidence logs from the round: =/tmp/tu.log= and =/tmp/tu2.log= (tmpfs, gone after reboot).
+
+Not graded on the bug matrix: it is test infrastructure, not the shipped codebase.
+
+*** 2026-08-08 Sat @ 05:05:00 -0500 Recurred under concurrent load, same signature
+All 5 tests hit the 20s TimeoutExpired again during a =make test-unit= run
+that overlapped two review subagents running their own suites on the box.
+Standalone immediately after: 0.095s, all pass; the following quiet-machine
+full run was clean. Confirms the load-sensitivity read — reproduce under
+deliberate load before choosing between a bigger budget and a real hang.
+
+
+*** 2026-08-19 Wed @ 14:50:00 -0700 Root-caused and fixed: inherited stdin, not load
+Not load, and not the network. The harness stubs ssh as =cat >/dev/null=, which
+drains stdin to EOF. With no explicit stdin the stub inherits whatever the test
+runner had, so it returned instantly when stdin was redirected and blocked
+forever when it was a terminal or a live pipe. All five tests then burned their
+20-second budget.
+
+That is why it looked like a load effect: a run launched immediately after
+another inherited a different stdin than a standalone invocation. A/B measured
+today — =make test-unit </dev/null= exits 0, the same target with an open pipe
+on stdin hangs on all five. The note above guessed at "a larger timeout or a
+real hang in the script" and it was neither.
+
+Fixed by pinning =stdin=subprocess.DEVNULL= in =run_script=. Verified both ways:
+the previously-failing open-pipe case and the redirected case both pass in
+0.08s, and a full =make test-unit= under a live pipe is clean across 50 suites.
+** DONE [#A] powerprofilesctl crashes on a loop since ppd was masked :bug:velox:dotfiles:
+CLOSED: [2026-08-21 Fri]
+:PROPERTIES:
+:CREATED: [2026-08-17 Mon]
+:LAST_REVIEWED: 2026-08-17
+:END:
+Something polls power state every 10-30 seconds, and each poll runs
+=powerprofilesctl get=, which SIGABRTs. 47 coredumps on velox on 2026-08-17
+alone, the earliest at 08:34, four in one minute while I was watching.
+
+Cause is the 2026-08-16 fix that masked =power-profiles-daemon= so TLP
+survives on laptops. That fix is right and stays. What it did not account for
+is the settings module's power backing
+(=~/.dotfiles/settings/src/settings/power.py=), which shells out to
+=powerprofilesctl=. Against a masked unit the D-Bus activation fails with
+=NameHasNoOwner ... unit is masked=, and the caller aborts rather than
+degrading.
+
+Run by hand the same command exits 0 and prints the error, so the abort is
+context-dependent and the caller needs finding before the fix is written.
+Ratio does not mask ppd, which is why this is velox-only and why it appeared
+the day after the masking.
+
+Costs: journal spam, coredump disk churn, and repeated failed D-Bus
+activations on a travelling laptop's battery. It is also the leading suspect
+for the wedged user manager filed below.
+
+Fix shape: =power.py= should treat a masked or unavailable ppd as a
+first-class "no profile control here" state rather than an error path, and
+the poller should stop retrying a unit it has been told is masked. The
+machine-level half is already correct.
+
+Grading: Major severity (a crash loop burning battery and filling the
+journal, silently) x every user every time on any laptop with the TLP fix
+applied = P1 = [#A].
+
+*** 2026-08-17 Mon @ 19:57:42 -0700 The loop stopped at the reboot; the defect did not
+velox rebooted at 16:04 and there have been zero coredumps since, against 47
+in the twelve hours before it. So the loop is not currently burning anything.
+
+That is not a fix, and the distinction matters for whoever picks this up.
+=powerprofilesctl get= still fails exactly as recorded — =NameHasNoOwner ...
+unit is masked= — so every precondition for the loop is intact and it returns
+whenever the caller next polls. What the reboot cleared is the caller's state,
+not the bug.
+
+Narrowed the search the body asks for: =power.py= is the *only* file in
+dotfiles that shells out to =powerprofilesctl= (=SETTINGS_POWERPROFILESCTL=,
+line 14), so the caller is inside the settings module rather than waybar or a
+timer. Worth knowing that the coredumps are =powerprofilesctl= itself aborting
+— it is a python script, which is why they log as =/usr/bin/python3.14=
+SIGABRT rather than under its own name.
+
+Grade unchanged. The matrix inputs did not move: the severity is what happens
+while the machine is in that state, and the frequency row is every laptop
+carrying the TLP fix. A quiet interval since a reboot is not a frequency
+change.
+
+Fixed in dotfiles =e89d9db=. The caller was =waybar.py=, using =panel.read_state()= (the full snapshot of every control) to read one boolean, four bar modules deep on a 2-second interval. Two fixes, each needed alone: =panel.read_control()= reads a single control's backing, and =power.masked()= checks the mask symlink before shelling out. Verified with a logging stub: full snapshot unmasked calls powerprofilesctl once, masked calls it zero, and a waybar poll calls it zero even unmasked.
+** DONE [#A] The installer clones my two working repos shallow and read-only :bug:velox:
+CLOSED: [2026-08-21 Fri]
+:PROPERTIES:
+:CREATED: [2026-08-17 Mon]
+:LAST_REVIEWED: 2026-08-17
+:END:
+=archsetup:1432= clones the user's archsetup repo and =archsetup:1445= clones
+dotfiles, both with =--depth 1=. Those are not build directories. They are the
+two repos I actively develop in, and on velox they came back from the
+2026-08-13 rebuild with 7 commits of history each instead of 851.
+
+Found 2026-08-17, and found the worst way: I ran the credential-file history
+check that the GitHub-release task asks for, and it reported all five files
+absent from history with a clean exit. The real answer is that this clone
+cannot see the history those files live in. A shallow clone does not error on
+=git log -- <path>=, it answers "no commits" — so a security question came back
+falsely clean, and nothing about the output said otherwise.
+
+Everything else it breaks is quieter: =git log=, =blame=, =bisect=, and any
+archaeology past the boundary. The tree looks completely normal, which is why
+this survived four days on the machine.
+
+The right shape is already in the codebase. =scripts/post-install.sh:42-51=
+takes depth as a per-repo argument and defaults to a full clone, so wallpaper
+gets =--depth 1= and org does not. The AUR build clones (=archsetup:855=,
+=:1673=, =:1677=) are correctly shallow and stay that way. Only the two
+user-repo sites change.
+
+*Second defect, same two lines, found 2026-08-17 while pushing:* the dotfiles
+clone could not push at all. =archsetup:245= defaults =dotfiles_repo= to
+=https://git.cjennings.net/dotfiles.git=, the public read-only endpoint, so
+=git push= returned 403. Ratio uses =git@cjennings.net:dotfiles.git= and
+archsetup's own clone uses the matching ssh form, so velox was the odd one out
+purely because it was the machine rebuilt by the installer. Repointed velox's
+remote and pushed.
+
+That half needs a decision rather than a fix, which is why this task is no
+longer =:solo:=. The https default is *correct for a stranger* installing
+archsetup, who has no ssh key on the server, and this repo is being prepared
+for public release. It is wrong for my own machines, which need to push. The
+override already exists (=DOTFILES_REPO=, documented in
+=archsetup.conf.example=), so the question is only where my personal value
+lives: a config the personal ISO bakes in, a post-install step, or a detection
+that prefers ssh when a key is present. Craig's call.
+
+*Decided 2026-08-19: the ISO bakes the value, and a check nets the rest.*
+=archsetup:240= has the identical default for =archsetup_repo=, so this was
+always two repos rather than one. I ruled out detection — archsetup never
+restores =~/.ssh=, so key-presence at clone time depends on ordering it
+doesn't control, and "any key means ssh" would break a stranger who has an
+unrelated one. I ruled out a bare post-install step for the reason this whole
+class of bug exists: manual steps don't get run, which is why this sat four
+days. So the personal ISO carries =ARCHSETUP_REPO= / =DOTFILES_REPO= in the
+ssh form (noted on the secrets/ISO task), and =post-rebuild-check= check 8
+flags any working repo still on the read-only endpoint — covering curl|bash
+and stock-ISO installs, which the ISO value cannot reach.
+
+Repair on a machine already built: =git fetch --unshallow= in each repo, and
+=git remote set-url origin git@cjennings.net:<repo>.git= for dotfiles.
+
+Grading: Major severity (two working repos silently missing their history on
+the machine I develop on, and it returns confidently wrong answers to history
+questions rather than failing) x every user every time (every fresh install,
+both daily drivers) = P1 = [#A].
+
+Not :solo:. The depth half is (two lines plus tests in the existing
+=tests/installer-steps/= shape, verifiable by asserting the clone command
+carries no =--depth= for these two repos). The remote-URL half needs the
+decision above, so the task as a whole waits on it. Split it in two if the
+depth fix is wanted sooner.
+*** 2026-08-19 Wed @ 23:05:00 -0700 Dropped --depth from both user-repo clones
+=archsetup:1462= and =:1475= now clone full history;
+=tests/installer-steps/test_clone_user_repos.py= covers it with 8 cases, and
+one of them asserts the AUR build clones still carry =--depth 1= so the fix
+can't be over-applied by a careless repo-wide sed. Both my repos on velox were
+already unshallowed by hand last session, so this is prevention rather than
+repair.
+*** 2026-08-19 Wed @ 23:05:00 -0700 Settled the remote-URL half and netted it
+See the decision recorded above. The ISO half is a note on the secrets/ISO
+task; the net is =post-rebuild-check= check 8, which ships now.
+
+Both halves resolved. Depth: =a028aa5= drops =--depth 1= from both user-repo clones, with 8 tests including one asserting the AUR build clones stay shallow. Remote URL: decided 2026-08-19 (see above) — the personal ISO carries the ssh form, and =post-rebuild-check= check 8 (=87ff0b7=) flags any working repo still on the read-only endpoint, covering the install paths the ISO cannot reach.
+** DONE [#D] Worldclock tooltip blanks on one bad timezone row :bug:dotfiles:waybar:quick:solo:
+CLOSED: [2026-08-21 Fri]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-25
+:END:
+Found by sentry (2026-07-25), verified by exercising. =hyprland/.local/bin/waybar-worldclock= builds each zone with =ZoneInfo(tz)= inside the loop (line ~99) with no guard, so a single malformed timezone row in =worldclock.conf= raises =ZoneInfoNotFoundError= and crashes the whole python pass. The tooltip then renders empty and *every* zone is lost, not just the bad row; the traceback only reaches stderr, where waybar never surfaces it.
+Repro: a conf with =America/Chicago|Home=, =Not/AZone|Bad=, =Europe/London|London= renders =tooltip: ""= (Home and London gone too).
+Grade: minor severity (one module's tooltip blanks, no data loss) x rare edge case (a malformed conf row) = P4 = [#D].
+Fix: wrap the per-row =ZoneInfo=/=datetime= in a try/except and =continue=, so a typo drops only that row and the valid zones still render. Solo + quick: the script already has an env-override test harness (=WAYBAR_TIME_EPOCH=, =WAYBAR_WORLDCLOCK_CONF=), so a red-first test is cheap.
+
+Fixed in dotfiles =8f692f5=. The per-row =ZoneInfo= is guarded, so a malformed row drops itself and the valid zones still render. Five cases, including a bad row first — the ordering that looks least like one typo and most like the module being broken. Caught the broad =except Exception= rather than =ZoneInfoNotFoundError=, because the row also parses floats and calls strftime and the contract wanted is "a bad row costs only itself".
+** DONE [#C] obsbot-wb-guard polls forever on machines with no OBSBOT :bug:dotfiles:quick:solo:
+CLOSED: [2026-08-21 Fri]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-08-16
+:END:
+=obsbot-wb-guard.service= is =WantedBy=graphical-session.target= and lives in the shared =common/= stow tier, so it starts on every machine. Its main path is =while :; do check_once; sleep 2; done=, and =check_once= returns early when the camera node is absent. On a machine with no OBSBOT attached that is a process waking every two seconds forever to do nothing, which on a laptop is battery spend for zero benefit. No restart loop, though: the loop never exits, so =Restart=on-failure= never fires.
+
+Found 2026-08-16 on velox, after enabling it to match ratio and then having to disable it again by hand. A per-machine disable is the wrong shape, because it drifts velox from ratio permanently and a re-stow or a future audit will just put it back.
+
+Fix: give the unit =ConditionPathExists= on the camera node (=/dev/v4l/by-id/usb-Remo_Tech_Co.__Ltd._OBSBOT_PW106-video-index0=, the same default the script uses) so systemd skips it on any machine without the camera and starts it normally on ratio. Then re-enable it on velox, where it will simply be skipped. Note the limit: a camera plugged in later will not start it until the next login, which is the right trade against a permanent poll.
+
+Careful when disabling by hand in the meantime: =systemctl --user disable= on a *linked* unit deletes the unit symlink, and that symlink is stow-managed, so a bare disable silently removes a file from the dotfiles stow tree. Restore the link afterward or re-stow.
+
+Grade: minor severity (wasted wakeups and battery, no data loss, no failure) x every boot on any machine without the camera = P3 = [#C].
+
+Solo: buildable here (archsetup owns dotfiles end-to-end), verifiable by the agent (assert the unit is skipped on velox and still active on ratio), and no design call left open.
+
+Fixed in dotfiles =566dd14=. =ConditionPathExists= on the camera node, so systemd skips the unit where the camera is absent. velox is now =enabled= like ratio and reports =ConditionResult=no=; the stow symlink is untouched. Found while doing it: ratio has a Logitech BRIO and no OBSBOT on USB at all, so the 2-second poll was pointless on the desktop too, not merely costing laptop battery. A test asserts the unit's condition path and the script's =OBSBOT_WB_DEVICE= default stay equal, since drift there is invisible in both directions.
+** DONE [#C] Spine face tests decay against the wall clock :bug:test:dotfiles:solo:
+CLOSED: [2026-08-21 Fri]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-08-02
+:END:
+=settings/faces/timeline-face-spine.test.mjs= has thirteen =SP.spineRows(g, h)= calls that omit the third argument, so =ref= falls back to its =new Date()= default while the file's events fixture is pinned to =JUL= (2026-07-31 18:30 UTC). Any assertion that depends on how much room the day needs is then measured against today's clock, and rots as the fixture recedes.
+
+One of them, "spacing is uniform everywhere except the gap home opens", had already rotted: green on 07-31 because that was the fixture's own date, red by 08-02. Fixed in place on 2026-08-02 by pinning =JUL=; the remaining thirteen pass today by luck. The measurement, for whoever picks this up — with =ref=now= the even step is 85.21 and home's gaps are 129.10 / 65.40 (the lower one collapses below a plain gap); with =ref=JUL= the step is 78.54 and the gaps are 129.10 / 145.46. Only the lower gap moves, because =up= does not depend on events and =down= does.
+
+Six other calls in the same file already pass =JUL= explicitly, so the convention exists and this is a miss, not a gap in the design. Fix: pass =JUL= at every call whose assertion reads geometry. Leave the call around line 747 alone — it sweeps =new Date(t0)= deliberately.
+
+Grade: minor severity (dev-facing only; no product behavior is wrong, the face itself is fine) x some users, sometimes (each call rots independently, whenever the fixture drifts far enough) = P3 = [#C]. Not merely cosmetic though: a suite that goes red for no real reason is how a genuine regression gets waved through.
+
+Solo — mechanical, an existing convention to copy, and verifiable by running the suite plus re-running it under a faked clock to prove the determinism actually holds.
+
+
+Fixed in dotfiles =c96a216=. All thirteen bare calls now pass =JUL=. The task's "line 747" was stale (the deliberate =t0= sweep is at 893 and already passed its own ref, so it was never at risk), and the continuation-form call closes its arguments on the next line, which is why a naive grep counts fourteen. Added a guard that reads the file and fails with the offending line numbers, and verified it bites by stripping =JUL= from one call and confirming it went red naming that line.
+** DONE [#A] Velox still carries the install placeholder passwords :bug:security:velox:
+CLOSED: [2026-08-23 Sun] SCHEDULED: <2026-08-20 Thu>
+:PROPERTIES:
+:CREATED: [2026-08-20 Thu]
+:LAST_REVIEWED: 2026-08-20
+:END:
+Closed 2026-08-23: I'd already rotated all three on the 08-14 bringup day, so
+this task was never live. Verified on velox before closing — =chage -l= puts the
+last password change for both =cjennings= and =root= at Aug 14 2026, and
+=/etc/zfs/zroot.key= was rewritten 2026-08-14 05:29 and no longer holds the
+placeholder (checked with a =grep -qx= that returns a yes/no without reading the
+key into a transcript).
+
+The premise below was wrong, and it's worth naming how. Nothing ever tested the
+credentials: the claim came from an unticked runbook item plus the archangel
+session handing back the values the *installer* had set, which reads as "these
+are current" only if you assume nobody changed them in between. An inference
+about a security exposure got recorded in the same voice as a measurement. The
+one command that settles it costs a second.
+
+Original body follows.
+
+The 2026-08-13 reinstall set placeholder credentials and the runbook's Phase 5
+item to replace them (=passwd=, =zfs change-key zroot=) was never ticked.
+Believed still live 2026-08-20 via the archangel handoff, which had to hand
+them back to Craig to get into the machine: =welcome1= for the pool, =welcome=
+for the accounts.
+
+So velox's full-disk encryption is currently protected by a dictionary word
+with a digit, on the machine that travels. Anyone who picks it up owns the pool
+and every account on it — the encryption is doing no work at all.
+
+Two commands, both on velox:
+- =passwd= for each account.
+- =zfs change-key zroot= for the pool passphrase. Note this is the ZBM unlock
+ passphrase, so get it right before rebooting.
+
+Grading: *severity-alone carve-out* — this is a security exposure, so the
+frequency row does not discount it (=todo-format.md=). Critical severity: total
+compromise of an encrypted-at-rest laptop from a guessable string, with the
+device leaving the house. = P1 = [#A].
+
+Distinct from the =VERIFY [#A] Rotate the credentials exposed by the 2026-08-09
+dotfiles leak= under the cgit audit — that one covers credentials a crawler
+already took from a public repo. This one is a local default never changed. Both
+are rotation work; neither substitutes for the other.
+** CANCELLED [#B] Consistent keybinding family for the panel console :feature:hyprland:
+CLOSED: [2026-08-21 Fri]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
+Merged into =[#B] Reconcile panel keybindings around Super+N=, which now carries
+this body's detail: the collision list, the velox plain-keyboard constraint, and
+maintenance-M as the priority chord. Cancelled rather than done — the work is
+still open, just tracked in one place instead of two.
+
+Consider putting every panel (net, bluetooth, audio, timer, and the coming maintenance console) on one consistent chord family — a shared modifier set (Super+Shift, Control+Alt, or similar) plus a mnemonic letter per panel (N/B/A/T/M). Today the panels open via waybar clicks only; a uniform chord family makes them keyboard-reachable and predictable. Watch for collisions with existing binds: Super+Shift+A is already PTT toggle, and the hold-to-talk grave bind is load-bearing. Decide the family, audit current hyprland binds for conflicts, wire via the dotfiles hyprland config, and document in the keybind reference. Both machines (velox can't QMK-remap, so chords must work on a plain laptop keyboard).
+*** 2026-07-14 Tue @ 00:31:36 -0500 Folded Craig's ask for a maintenance-panel keybinding; bumped [#C] → [#B]
+Craig asked (in session, 2026-07-14) for a maintenance keybinding specifically — the panel he's reaching for without one. Maintenance (M) is the priority chord when this task gets worked. The capture graduated the task from parking lot to active backlog.
+** DONE [#B] Waybar network module — custom/net :feature:waybar:network:
+CLOSED: [2026-08-21 Fri]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-09
+:END:
+Closed 2026-08-21: the module shipped and is in daily use. Phases 1-4 all landed
+in dotfiles, and the tunnels track absorbed most of what Phase 5 originally
+covered. The one piece genuinely left — the =net vpn= CLI subcommand — is now its
+own task below, so the residual is tracked at its real size instead of holding a
+finished umbrella open.
+Unifies the old wifi-no-internet indicator (was =[#C]=) and the network-manager
+dropdown (was =[#B]=) into one =custom/net= module: a tested Python =net= engine
+(nmcli + diagnostics), a thin bar indicator, and a GTK4 layer-shell panel. Code
+lives in the dotfiles repo (hyprland tier + a =net/= package like pocketbook);
+archsetup only installs deps. Secrets stay in NetworkManager's own store (no
+separate credential store). The =captive= script becomes the diagnostics engine.
+Full design, acceptance criteria, and the failure-mode coverage table:
+[[file:docs/design/2026-06-29-waybar-network-module-spec.org][2026-06-29-waybar-network-module-spec.org]].
+
+Phases below, dependency order. Engine/unit work is agent-verifiable (=unittest=
++ fakes on PATH, coverage via venv); the live-network and visual states need real
+conditions, filed under "Manual testing and validation".
+
+*** 2026-06-29 Mon @ 20:19:11 -0400 Phase 1 shipped — indicator + console recovery
+Shipped to the dotfiles repo (10 commits, =5254bd8=..=c095a22=, pushed to main).
+The =net= engine is a src-layout Python package in-tree, imported by a bin shim
+that resolves the stow symlink back to the repo — so it runs from a bare TTY with
+no install, which the recovery path depends on.
+
+Landed: =net status= (fast path, one nmcli call + sysfs, degraded fallback in
+budget) + =net probe= (native captive probe, single-flight flock, atomic cache,
+fresh/stale/expired/unknown classes, iface/SSID/UUID invalidation); =waybar-net=
+replacing =custom/netspeed=, throughput → tooltip, CSS states in both themes +
+live; =net diagnose= (read-only steps) + =net repair= (rfkill/reset/bounce/
+dns-test, cleanup-verified) + =net doctor [--fix]= with the four terminal
+classifications; =net portal= + the =captive --probe-json= refactor; redacted
+JSONL event log; Makefile recovery targets (=make online= etc.); =~/.config/net/
+config=. Verified live: =make net-status= reads the real wlp170s0 / @Hyatt_WiFi.
+
+Airplane (Craig's call, option 1): =custom/net= absorbs only the *display* — net
+reads the airplane-mode state file and shows an airplane state/glyph. The
+airplane-mode toggle stays (it's a low-power mode — radios + CPU + brightness +
+services — not a radio switch), now on =custom/net='s right-click + signal 15.
+Deleted: =waybar-airplane=, =waybar-netspeed=, =custom/airplane=, their tests +
+css. =airplane-mode= kept.
+
+Tests: 160 in =tests/net/= (fake nmcli/curl/rfkill/resolvectl/ping/getent/
+systemctl on a temp PATH; doctor-classification fixtures; degraded-under-slow-
+nmcli benchmark) + the =captive= probe-mode tests; full dotfiles suite green (32
+suites). Coverage-gap pass via throwaway venv: pure modules ≥90% branch
+(classify 100%), IO-error branches excused in the test docstring.
+Deferred to Phase 2/3: archsetup deps (gtk4-layer-shell/python-gobject Phase 2,
+speedtest-go-bin Phase 3 — not added before the code that needs them).
+Verify (manual, live): see Manual testing and validation.
+
+*** 2026-06-29 Mon @ 22:19:25 -0400 Phase 2 shipped — panel shell + connection management
+Shipped to dotfiles (commits =4e7740f=..=24bcac5=, pushed). Engine: =net list= (saved
+MRU + in-range wifi scan, infrastructure types filtered), =net up/down= (UUID-keyed,
+mutation safety — keep prior link until target activates, classify wrong-password vs
+generic, report auto-reactivation), =net add/edit/remove/rescan= (open + WPA-PSK;
+enterprise activate-only; secret to NM's store, never our JSON/log — tested).
+
+Panel: a GTK-free PanelModel (selection, four state machines, the UX-flow enable
+rules, terminal states) + a GTK4 gtk4-layer-shell window (=net panel=) anchored
+top-right under the bar — Connections section with MRU list, active marked, signal
+glyph, row-click select, Connect/Add/Forget/Rescan, confirm-on-forget, worker-thread
+engine calls via GLib.idle_add. GTK imported lazily so the CLI/tests stay GTK-free.
+
+Bar interactions (settled with Craig over live iteration): left = =net-panel= toggle,
+middle = =net portal=, right = =net-fix= (notify the doctor result when one-way; open
+a terminal only when the outcome is fixable — the sudo/interactive case). Airplane on
+Super+Shift+A. archsetup adds =gtk4-layer-shell= + =python-gobject= (this commit);
+already on velox.
+
+Tests: 204 in tests/net (merge ordering/dedup, up/down mutation safety, no-secret-leak
+on add/edit, panel model + state machines, gui row-format helpers). Full dotfiles suite
+green (32 suites). Live-verified on velox: panel opens/toggles, list shows real 24
+profiles, right-click notification delivers (Craig confirmed). Phase 3 (diagnose/repair/
+speedtest IN the panel) is next; the engine for it already exists from Phase 1.
+
+*** 2026-06-29 Mon @ 22:43:40 -0400 Phase 3 shipped — diagnostics + speed test in the panel
+Shipped to dotfiles (=91277cf=..=691abcb=) + archsetup (=48052d6=, speedtest-go-bin),
+pushed. Engine: =net speedtest= (parses speedtest-go --json → ping from latency ns,
+down/up from per-server byte rates; missing-backend / offline / malformed → error
+envelope per the failure table). Panel grew a section switcher with four pages:
+- Connections (Phase 2).
+- Diagnose: =net diagnose= on a worker thread, each step a row (✓/✗/… glyph + title +
+ redacted evidence), read-only; Open-portal button when captive.
+- Repair: "Get me online" (=net doctor --fix=) + tiers (rfkill/reset/bounce/dns-test)
+ + force portal. Confirmations in-panel with the spec's exact wording; the privileged
+ tiers run via =net-popup= terminal (where the sudo prompt + step output, incl.
+ cleanup-verified, show) — a panel has no tty, and pkexec would mean a prompt per op.
+- Speed test: in-process =net speedtest= (no privilege → inline result: ↓/↑ Mbps + ping
+ + server), Run/Cancel (Cancel pkills the child), error envelope shown.
+
+213 net tests; pure helpers (step_indicator, format_speedtest) unit-tested. Full
+dotfiles suite green (32 suites). One unverified assumption: speedtest-go's dl/ul unit
+(taken as bytes/s; =BYTES_PER_SEC= flips it) — needs one real run vs a reference. The
+in-panel repair streaming (vs terminal) is a named future polish once the GUI-privilege
+story settles.
+
+The waybar network module ([#B] parent) is now COMPLETE through Phase 3. Phase 4
+(in-app help + user guide) and Phase 5 (VPN/WireGuard) remain as future work; the core
+feature (indicator + recovery + panel + diagnostics + speed test) is done.
+Verify (manual, live): see Manual testing and validation.
+
+*** 2026-07-09 Thu @ 16:32:54 -0500 Audit reconcile: Phase 4 is filed on the dotfiles side, waiting on them
+The dotfiles project accepted the Phase 4 handoff and filed it as a =[#C]= task in their own =todo.org= (their note, 2026-07-08 16:56): the help-text audit + panel help affordance, the user-guide/README, and the ratio rollout doc. Not started there. They ping when it lands, and this task's Phase 4 child closes then. Nothing to do here meanwhile.
+
+*** 2026-08-17 Mon @ 19:57:42 -0700 Landed on the dotfiles side; the block is cleared
+dotfiles shipped it as =138da7b= and closed its own task, so this one closes
+with it and the =:blocked:= tag comes off. Found by checking their =todo.org=
+rather than waiting for the ping — their close-out note says "archsetup pinged
+so its Phase 4 task can close", so the handoff worked and only this end was
+left open.
+
+All three acceptance criteria are met on their side: the help audit found and
+fixed a stale =net repair= action list (nine of nineteen actions were named;
+both the CLI help and =repair.py='s docstring now generate from the ACTIONS
+registry), =net/README.md= covers every command plus the recovery targets, and
+the ratio rollout is documented with both daily drivers verified current.
+
+They split the panel help affordance out rather than inventing it — no sibling
+panel has one, so its shape is a design call. It is tracked on their side, not
+here.
+
+Original deliverable, for the record: in-app help (=net --help= + per-command,
+panel help affordance); README/user-guide; archsetup Hyprland dep install
+(=gtk4-layer-shell=, =python-gobject=, =speedtest-go-bin=); ratio manual dep +
+stow step. Handed off 2026-07-04 with the archsetup deps already confirmed
+installed.
+
+*** 2026-08-21 Fri @ 14:18:03 -0700 Promoted the Phase 5 residual out to its own task
+Rescoped 2026-07-04 (audit): the tunnels track already shipped most of the original Phase 5. Panel tunnel bring-up/down and detection landed (dotfiles 2d9d060 probes tailscale/NM-wireguard/Proton; 21db05a brings overlays up/down from the panel's Tunnels sub-view; 31ba056 diagnose/doctor understand tunnel routes; archsetup 2e40781 wireguard config import; the net-panel-other-interfaces spec is IMPLEMENTED). What remains for Phase 5 is only the =net vpn ...= CLI subcommand — cli.py still has no vpn/tunnel parser. Fold the panel's existing tunnel operations into a CLI surface; spec separately when picked up.
+** DONE [#A] Ratio: pull .emacs.d before upgrading Emacs to 31.1 :chore:ratio:emacs:
+CLOSED: [2026-08-25 Tue]
+:PROPERTIES:
+:CREATED: [2026-08-25 Tue]
+:LAST_REVIEWED: 2026-08-25
+:END:
+Emacs 31.1's warnings.el defers daemon-startup warnings into a closure holding
+the =*Warnings*= buffer; the config's dashboard-only sweep killed that buffer,
+so the first client frame of every fresh 31.1 daemon failed on Wayland and
+emacsclient silently fell back to =$DISPLAY= (XWayland, pgtk warning dialog).
+Fixed in =.emacs.d= commit =63831060= (2026-08-25, velox verified live:
+=GdkWaylandDisplay=). Ratio is still on 30.2, which lacks the deferring code,
+so it is fine until it upgrades — then it hits the same trap once per daemon
+start unless the fix is pulled first.
+
+Order on ratio: =git -C ~/.emacs.d pull= (the push from velox is the telega
+session's; confirm =63831060= is on origin first), then the =pacman -Syu= that
+brings =emacs-wayland 31.1=, then restart the daemon. Check afterwards:
+=emacsclient -e '(pgtk-backend-display-class)'= → =GdkWaylandDisplay=.
+
+*** 2026-08-25 18:10 — pull already landed; the upgrade half remains
+Checked ratio over tailscale: =~/.emacs.d= is clean at =91fbac72= (= =origin/main=),
+and =63831060= is an ancestor of HEAD — =modules/undead-buffers.el= carries the
+=*Warnings*= entry. Ratio is on =emacs-wayland 30.2-3= with =31.1-1= pending among
+720 updates (last full upgrade 2026-08-01; kernel 7.1.5 → 7.1.9 also pending,
+btrfs root, uptime 3.5 weeks). The daemon is a plain =emacs --daemon= (not a
+user unit) holding 2 live frames, so the restart step will drop those frames.
+What remains: the =pacman -Syu= on ratio, the daemon restart, and the
+=(pgtk-backend-display-class)= check.
+
+*** 2026-08-25 Tue @ 18:35:00 -0600 Upgraded ratio to Emacs 31.1 and verified the Wayland backend
+Ran the upgrade over tailscale as a transient unit (=ratio-upgrade.service=,
+log at =/var/log/ratio-upgrade.log=): 714 packages, =--ignore= on the six
+packages the live-update guard would have blocked (aquamarine, hyprland,
+hyprutils, mesa, vulkan-radeon, wayland — still pending, apply from a TTY
+before the reboot). One orphan cleared first: =qemu-block-gluster= had been
+dropped from the repo and pinned =qemu-common=; the new =qemu-full= no
+longer needs it. Killed the plain =emacs --daemon= (no modified buffers, no
+graphical frames), started =emacs.service= instead so the daemon carries the
+systemd user environment, and probed from a throwaway frame:
+=(pgtk-backend-display-class)= → =GdkWaylandDisplay=, =*Warnings*= alive.
+Ratio still wants a reboot for =linux 7.1.9=. Pacnews to review there:
+=/etc/ssh/sshd_config.pacnew= and two =/etc/tpm2-tss/fapi-profiles/*.json=.