aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--archive/task-archive.org38
-rw-r--r--docs/2026-08-13-velox-reinstall-runbook.org (renamed from working/velox-reinstall/velox-reinstall-runbook.org)23
-rw-r--r--docs/2026-08-15-velox-uefi-boot-entry-reference.org (renamed from working/velox-reinstall/velox-uefi-boot-entry-reference.org)2
-rw-r--r--docs/design/2026-07-15-velox-boot-failure-handoff.org61
-rw-r--r--docs/design/2026-08-14-velox-reinstall-gaps-1.org137
-rw-r--r--docs/design/2026-08-14-velox-reinstall-gaps-2.org63
-rw-r--r--docs/design/2026-08-14-velox-reinstall-gaps-3.org98
-rw-r--r--docs/workflows/system-health-check.org4
-rw-r--r--todo.org309
-rw-r--r--working/clock-display-references/2026-07-30-maeda-cosmos-notes.org11
-rw-r--r--working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html422
-rw-r--r--working/clock-display-references/2026-07-30-maeda-line-notes.org11
-rw-r--r--working/clock-display-references/2026-07-30-maeda-line-standalone.html457
-rw-r--r--working/velox-reinstall/wttrin-bf0457f.bundlebin1826602 -> 0 bytes
14 files changed, 1495 insertions, 141 deletions
diff --git a/archive/task-archive.org b/archive/task-archive.org
index 3811cc6..4f2fdaa 100644
--- a/archive/task-archive.org
+++ b/archive/task-archive.org
@@ -2930,3 +2930,41 @@ CLOSED: [2026-08-08 Sat]
Killed at the 2026-08-08 task review: an undated annual intention that never
fired — pain points get surfaced organically as they bite.
Once-yearly systematic inventory of known deficiencies and friction points in current toolset
+** DONE [#B] Podman API socket and camera-passthrough udev rule :feature:solo:
+CLOSED: [2026-08-09 Sun]
+:PROPERTIES:
+:CREATED: [2026-08-07 Fri]
+:LAST_REVIEWED: 2026-08-07
+:END:
+Shipped 2026-08-09: the installer enables the rootless podman socket at
+install time (enable_user_service grew a wants-target arg so socket units
+land in sockets.target.wants) and ships
+=72-usb-passthrough-cameras.rules= — numbered below 73 per the winvm
+rule-ordering correction, GROUP/MODE as the verified grant, uaccess tag kept.
+Applied live on ratio (socket enabled+active, 99- file retired, udev
+reloaded); velox apply rides the velox-return riders on the sleep/suspend
+task. The uaccess-alone hypothesis stays untested until a camera is attached.
+From winvm 2026-08-07 (ratio). Two one-time machine-level setups, both live on
+ratio and absent on velox; full evidence and rationale in
+[[file:docs/design/2026-08-07-podman-socket-and-camera-udev.md]].
+
+- Enable the rootless podman socket at install time
+ (=systemctl --user enable --now podman.socket=). Socket-activated, zero idle
+ cost; every podman GUI/API client needs it, and its absence fails silently
+ (Pods opens to an empty window). The installer already carries the
+ "=systemctl --user enable= fails during install" workaround pattern
+ (=archsetup:1270=, =:2722=) — use it.
+- Ship a udev rule granting GROUP="video", MODE="0660" on the OBSBOT
+ (3564:ff02) and BRIO (046d:085e) USB nodes so =usbredirect= can claim them
+ for VM passthrough. CORRECTED (winvm, 2026-08-08): the original "uaccess
+ can't ACL raw USB nodes" claim was wrong — the mechanism is rule ordering.
+ The ACL is applied by =73-seat-late.rules=, so a =99-= rule adds the tag
+ after that already ran; distro rules that add the tag all sort at or below
+ 70. [@70] So number our file below 73 (e.g. =72-usb-passthrough-cameras.rules=),
+ keep the verified GROUP/MODE grant, and keep the tag — correctly ordered it
+ may make uaccess work on its own (untested hypothesis; a tighter grant if
+ it holds, needs the camera plugged in to verify). Reconcile ratio's
+ existing =99-= file (winvm installed it) when the installer version lands.
+
+Scope: installer step + rule file + tests per existing shapes, and apply both
+live to velox over tailscale (daily-driver sync — neither exists there today).
diff --git a/working/velox-reinstall/velox-reinstall-runbook.org b/docs/2026-08-13-velox-reinstall-runbook.org
index 0c14946..2d99fc9 100644
--- a/working/velox-reinstall/velox-reinstall-runbook.org
+++ b/docs/2026-08-13-velox-reinstall-runbook.org
@@ -9,6 +9,29 @@ pool. Decision: full reinstall via archangel + archsetup, run deliberately as
a disaster-recovery test of the ISO and scripts before the Sunday flight.
Recent backup in hand; ratio available as the working machine.
+* Outcome (recorded 2026-09-13 Sun)
+
+The drill ran on 2026-08-13 and 14 and velox came back as a working daily
+driver: fresh install from the archangel ISO, keys and data restored from
+the salvage backup, 23 repos re-cloned, rsyncshot reinstalled, hibernate
+proven end to end. The checklist below was the live plan; it was not ticked
+as the phases ran, so read it as the plan, not a log of each step.
+
+What the drill found, each filed as its own task rather than fixed in place:
+velox's truenas backups had silently stopped on 2026-07-06 (found on 08-13
+before partitioning, which is what made the salvage pass required); four phantom
+reboots were a ribbon disturbed by the board swap; a fresh install never
+clones rulesets, never links the .emacs.d systemd user units, ships no
+brightness udev rule, and loses gcalcli and the signal-cli registration.
+Those live in archsetup's todo as the post-rebuild verification pass and its
+siblings. The one commit that existed only on the old disk (emacs-wttrin
+bf0457f) was rescued as a bundle and has its own task.
+
+Companion documents: the UEFI boot-entry recovery reference
+([[file:2026-08-15-velox-uefi-boot-entry-reference.org][2026-08-15-velox-uefi-boot-entry-reference.org]])
+and the three gap reports under docs/design (2026-08-14-velox-reinstall-gaps-1
+to 3).
+
Fallback ordering if the test finds a real gap:
- Before partitioning starts: the old system is intact — the ZBM repair
route (efibootmgr entry pointing at the ZBM loader on the ESP, then
diff --git a/working/velox-reinstall/velox-uefi-boot-entry-reference.org b/docs/2026-08-15-velox-uefi-boot-entry-reference.org
index 53e6511..1eacfe5 100644
--- a/working/velox-reinstall/velox-uefi-boot-entry-reference.org
+++ b/docs/2026-08-15-velox-uefi-boot-entry-reference.org
@@ -31,7 +31,7 @@ below apply.
Before any future firmware update, record both =efibootmgr -v= and the
Secure Boot state so the post-reboot diagnosis is a comparison, not a guess.
The pre-firmware-update checklist in
-[[file:../../docs/workflows/system-health-check.org][docs/workflows/system-health-check.org]]
+[[file:workflows/system-health-check.org][docs/workflows/system-health-check.org]]
(Phase 3) carries the steps.
Still open: velox's ESP has no removable-media fallback (=/efi/EFI/BOOT=
diff --git a/docs/design/2026-07-15-velox-boot-failure-handoff.org b/docs/design/2026-07-15-velox-boot-failure-handoff.org
new file mode 100644
index 0000000..5ec996f
--- /dev/null
+++ b/docs/design/2026-07-15-velox-boot-failure-handoff.org
@@ -0,0 +1,61 @@
+#+TITLE: Velox boot failure — ZBM found no bootable kernel; diagnosis in progress, recovery plan attached
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-15
+
+* Why this is coming to archsetup
+
+Velox fails to boot: ZFSBootMenu reports it can't find a bootable environment with a kernel. Craig reports the last working velox session was an archsetup health-check run that included the pacman upgrade — so the breakage most likely happened inside archsetup's own workflow, and Craig wants the diagnosis + retrospective to continue here with full context. The .emacs.d session (where this was triaged, only because that's where Craig was sitting) hands off everything below.
+
+A phone photo of the zfs list output from velox's ZBM recovery shell accompanies this note in the inbox.
+
+* Timeline
+
+- 2026-07-13 ~23:50 CDT — velox last seen on the tailnet (per tailscale status read 2026-07-14 ~17:50).
+- During that last session: archsetup health-check workflow ran, including a pacman upgrade (Craig's recollection — pacman.log will confirm exact times).
+- 2026-07-14 late evening — Craig boots velox; ZBM: no bootable environment with a kernel.
+- 2026-07-14/15 — triage from the ZBM recovery shell, Craig driving, guided from the .emacs.d session.
+
+* Facts established so far (from the ZBM recovery shell)
+
+- zroot imported, health ONLINE. Every dataset's keystatus is "available" — encryption unlocked, not a key problem.
+- Layout confirmed from zfs list: zroot/ROOT/default (mountpoint /), separate datasets for home, home/root, media, var, var/cache, var/lib, var/lib/docker plus many docker layer children (legacy mountpoints). NOTE: no separate zroot/var/log dataset — /var/log lives inside zroot/var. That differs from the sanoid dataset list in archsetup's configure_zfs_snapshots (which configures zroot/var/log and zroot/var/lib/pacman as their own datasets) — worth reconciling in the retrospective.
+- Mounted the BE read-only style: mkdir -p /mnt/be && mount -t zfs -o zfsutil zroot/ROOT/default /mnt/be.
+- THE FINDING: /mnt/be/boot contains ONLY intel-ucode.img. vmlinuz-linux, initramfs-linux.img, and initramfs-linux-fallback.img are all gone.
+
+* Working hypothesis
+
+A kernel upgrade during the health-check run removed the old kernel files and never completed installing the new ones (interrupted transaction, mkinitcpio failure, or a /boot shadowing issue), and the machine was powered off with /boot empty. Arch's upgrade removes the running kernel's files at package-replace time, so a failure between "remove old" and "install new + mkinitcpio" leaves exactly this state: microcode present, kernel and initramfs absent.
+
+* Remaining diagnosis steps (not yet run — velox is sitting at the ZBM shell)
+
+1. Read pacman's log (on the zroot/var dataset):
+ #+begin_src sh
+ mkdir -p /mnt/var
+ mount -t zfs -o zfsutil zroot/var /mnt/var
+ tail -60 /mnt/var/log/pacman.log
+ #+end_src
+ Expect the failed/interrupted kernel transaction near the end; note its timestamp.
+2. List recovery candidates:
+ #+begin_src sh
+ zfs list -t snapshot zroot/ROOT/default | tail -20
+ #+end_src
+ Sanoid is configured for hourly=6/daily=7 on the ROOT dataset, so a pre-damage snapshot should exist. Check whether any pre-pacman_* snapshots appear — that tells us whether the 2026-06-29 pre-pacman hook design is actually installed on velox.
+
+* Recovery plan (agreed with Craig, pending the log read)
+
+1. Pick the newest zroot/ROOT/default snapshot that predates the failed transaction.
+2. If the pool is imported read-only (zpool get readonly zroot): zpool export zroot && zpool import -f -N zroot.
+3. zfs rollback -r zroot/ROOT/default@<snapshot> (the -r discards snapshots newer than the target; home/var/media are separate datasets and untouched).
+4. zpool export zroot, reboot — ZBM should now see the kernel.
+5. After first boot: re-run pacman -Syu attended, and confirm /boot holds vmlinuz-linux + initramfs-linux.img before any shutdown.
+
+* Retrospective candidates for archsetup
+
+- Does the health-check / upgrade flow verify /boot contents (kernel + initramfs present, mkinitcpio exit status) after a kernel upgrade? This failure would have been caught by a one-line post-upgrade assertion.
+- Is the pre-pacman snapshot hook (2026-06-29 design, zroot/ROOT/default@pre-pacman_<ts>) installed on velox? The snapshot listing in step 2 above answers this empirically.
+- The sanoid config vs actual dataset layout mismatch (var/log, var/lib/pacman) noted above.
+- Whether the upgrade step should refuse to end the session (or page Craig) when a kernel transaction errors.
+
+* Related loose end already in your inbox
+
+A separate note (2026-07-14-1751) asks to add inetutils to the install base; velox also still needs that package installed once it boots again.
diff --git a/docs/design/2026-08-14-velox-reinstall-gaps-1.org b/docs/design/2026-08-14-velox-reinstall-gaps-1.org
new file mode 100644
index 0000000..cf0d723
--- /dev/null
+++ b/docs/design/2026-08-14-velox-reinstall-gaps-1.org
@@ -0,0 +1,137 @@
+#+TITLE: What the velox reinstall left behind — four gaps the install could close
+#+AUTHOR: Craig Jennings
+
+* Heads-up: this was found from a .emacs.d session
+
+I opened a .emacs.d session on velox this morning, two days after the fresh
+Arch install, and the first thing it did was fail: there was no =.ai/=
+directory to read. Chasing that turned up four separate things the reinstall
+did not restore. Three I repaired from the session; one needs me at my phone.
+
+None of this is a .emacs.d bug. They are all install-side gaps, which is why
+they are landing in your inbox. Machine is velox; ratio was the reference for
+every comparison below.
+
+* Gap 1 — the gitignored tooling layer does not survive a reinstall
+
+=~/.emacs.d= was re-cloned on 2026-08-13. Git brought back every tracked file
+and none of the agent tooling, because =.gitignore= deliberately excludes it:
+=.ai/=, =.claude/=, =CLAUDE.md=, =todo.org=, and =inbox/= were all simply
+absent. That is the correct ignore policy — this repo relays to a public
+mirror — but it means a reinstall silently drops the entire working state of
+every gitignore-mode project.
+
+The damage on velox was total rather than partial: 374 files, 4.5 MB,
+including =todo.org= (556 KB) and 184 archived session files. Nothing carries
+it. Not git, not stow, not the bootstrap.
+
+I recovered it by rsyncing the set from ratio over the tailnet. Ratio was
+authoritative and velox held nothing, so there was no merge to adjudicate —
+which is luck, not design. Had velox held a few days of divergent state, this
+would have been a hand reconciliation. It has been one before: 2026-07-31, when
+the two machines' =.ai/= trees had forked to zero files in common.
+
+Worth knowing: this is fleet-general. Every project on the box that gitignores
+its =.ai/= has the same hole, not just =.emacs.d=.
+
+What the install could do: after cloning a project, check whether a sibling
+daily driver holds a =.ai/= for it, and offer to pull it across. Or at minimum,
+list the projects whose tooling layer is missing so the gap is visible on day
+one instead of at the first session that trips over it.
+
+* Gap 2 — stowed user timers come back linked but not enabled
+
+The unit files all arrived correctly through the dotfiles stow, symlinked into
+=~/.config/systemd/user/= and resolving fine. But being present is not being
+enabled, and the reinstall enabled only some of them:
+
+| unit | velox after reinstall | ratio |
+|---------------------------+-----------------------+----------|
+| calendar-sync.timer | enabled, active | enabled |
+| agenda-render-cache.timer | enabled, active | enabled |
+| roam-sync.timer | *linked, inactive* | enabled |
+| signal-receive.timer | *linked, inactive* | enabled |
+| emacs.service | linked, inactive | linked |
+
+=emacs.service= reads the same on both machines, so I take that one as
+intentional and left it alone. The other two are real drift: =systemctl --user
+enable= writes a =timers.target.wants= symlink into =~/.config/systemd/user/=,
+and that symlink is not stow-managed, so nothing in the dotfiles repo carries
+it. A stowed unit file is inert until something enables it.
+
+I enabled both with =systemctl --user enable --now=. Both fired immediately and
+exited clean, and both now show a next elapse.
+
+What the install could do: enable the units it stows, explicitly, as a named
+step. The inconsistency is the tell — two of four came back enabled, which
+suggests something enables a subset and nothing enumerates the rest.
+
+* Gap 3 — the roam clone was stale, and held a diff that would have destroyed data
+
+This one has an ordering constraint, so it matters more than its size suggests.
+
+velox's =~/org/roam= was ten commits behind ratio, stuck at the 2026-08-04
+auto-sync while ratio was at 2026-08-14 — a direct consequence of gap 2, since
+=roam-sync.timer= was never enabled here.
+
+The dangerous part: velox's clone also carried an *uncommitted* =inbox.org=
+that had been emptied. Seventeen deletions, file down to zero bytes, holding a
+pre-2026-08-04 state whose captures were long since processed on ratio.
+
+So the naive repair — enable =roam-sync.timer= and let it catch up — would have
+committed that emptying and pushed it, deleting the four live inbox items on
+ratio. The timer is the repo's only committer and it commits whatever it finds.
+
+I checked ratio's =inbox.org= first and confirmed it was a strict superset of
+velox's HEAD version (same three items plus an 2026-08-09 capture), which made
+the local change provably worthless. Then discarded it, fast-forwarded to
+=a411b43=, and only then enabled the timer. Clone is clean and current, first
+sync ran green.
+
+What the install could do: if it ever enables =roam-sync= on a rebuilt machine,
+reconcile the clone *before* enabling, not after. An auto-committing timer
+pointed at a stale dirty clone is a data-loss path, and the failure is silent
+and remote — it lands on the *other* machine.
+
+* Gap 4 — signal-cli lost its registration, and that breaks the whole fleet
+
+=signal-receive.service= ran for the first time and reported:
+
+: signal-receive: +15045173983 not registered on this machine — nothing to do
+
+velox's signal-cli data dir holds a 39-byte empty =accounts.json=. Ratio still
+has both numbers. So the reinstall wiped the registration, and per the design
+notes velox was supposed to be the *primary* — ratio is the linked device.
+
+The effect is wider than velox, because of how =agent-text= dispatches: if the
+local signal-cli holds the account it sends directly, otherwise it ssh-relays to
+a hardcoded velox. Velox no longer holds it, so a send from here relays to
+itself and fails; a send from any third machine relays to velox and fails the
+same way. Only ratio still works, and only via the direct branch. The error text
+blames "velox down or unreachable", which is misleading — velox is up and on the
+tailnet, it just is not registered.
+
+This is the one I could not repair from the session: re-linking needs me at my
+phone (Signal → Settings → Linked Devices, scanning the QR from =signal-cli
+link -n velox=). Filed in .emacs.d's todo.org as [#B].
+
+What the install could do: verify =signal-cli listAccounts= is non-empty after a
+rebuild and say so loudly if it is not. Silent loss of the phone channel is
+exactly the kind of thing nobody notices until the page that mattered never
+arrives.
+
+* Summary of what I changed on velox
+
+- Restored =.ai/=, =.claude/=, =CLAUDE.md=, =todo.org=, =inbox/= to
+ =~/.emacs.d= by rsync from ratio.
+- Discarded the stale local =inbox.org= diff in =~/org/roam= and fast-forwarded
+ the clone to current.
+- Enabled and started =roam-sync.timer= and =signal-receive.timer=.
+
+Left alone, deliberately: =emacs.service= (matches ratio), and velox's Signal
+registration (needs the phone).
+
+One unrelated thing I noticed while comparing the machines: ratio's signal-cli
+warns its messages were last received twelve days ago, even though its
+=signal-receive.timer= is enabled and active. That may be nothing, but the
+receive cadence there is worth a look.
diff --git a/docs/design/2026-08-14-velox-reinstall-gaps-2.org b/docs/design/2026-08-14-velox-reinstall-gaps-2.org
new file mode 100644
index 0000000..95842ac
--- /dev/null
+++ b/docs/design/2026-08-14-velox-reinstall-gaps-2.org
@@ -0,0 +1,63 @@
+#+TITLE: Fifth reinstall gap — machine-local .local.el config, and a general shape
+#+AUTHOR: Craig Jennings
+
+* Follow-up to this morning's handoff
+
+Sent you four gaps an hour ago
+([[file:2026-08-14-velox-reinstall-gaps-1.org][the first report]]). Here is a fifth,
+found straight afterwards when I noticed calendar sync was dead on velox.
+
+* What was broken
+
+=calendar-sync.timer= was enabled and firing every fifteen minutes, and failing
+every time with exit 255:
+
+: calendar-sync: No calendars configured (set calendar-sync-calendars)
+
+The three output files sat at zero bytes. The cause is that
+=~/.emacs.d/calendar-sync.local.el= is gitignored, so the reinstall deleted it
+along with everything else untracked, and the module's loader treats a missing
+file as a *silent* no-op. So the config vanished quietly and the only symptom
+was a failing unit nobody was watching.
+
+Cheap to fix once found: the repo tracks =calendar-sync.local.el.example=, and
+that template already encodes the shape velox uses — feeds resolved by
+=:secret-host= against =authinfo.gpg= rather than inlined. The authinfo entries
+had survived, because =~/.authinfo.gpg= is a stow symlink into the dotfiles repo.
+So rebuilding was one copy, and all three feeds now sync clean and land
+byte-identical to ratio's.
+
+* The general shape, which is the part worth acting on
+
+This is the same failure as gap 1, one layer down, and it is worth stating
+generally because the install can act on it:
+
+- A tracked =*.local.el.example= template plus a gitignored =*.local.el= is a
+ deliberate pattern in this config, not a one-off. =.gitignore= lines 56-58
+ list three of them: =calendar-sync.local.el=, =signal-config.local.el=,
+ =google-keep.local.el=. Every one of those is gone on velox right now. I have
+ only repaired the calendar one.
+- Secrets held *by reference* survive a rebuild; secrets held *inline* do not.
+ The calendar config came back for free because the tokens were in
+ =authinfo.gpg=, which is stow-managed and therefore travels. Ratio's copy of
+ the same file inlines its URLs, and had ratio been the machine rebuilt, those
+ three feed tokens would simply have been gone.
+- The failure was silent by design. A missing local config is a no-op, which is
+ right for a machine that never configured the feature and wrong for one that
+ just lost it.
+
+* What the install could do
+
+- After a rebuild, enumerate every tracked =*.local.el.example= in a project and
+ report which have no corresponding =*.local.el=. That is a one-line find and it
+ turns a silent no-op into a visible checklist item.
+- Same for any =*.local.*= convention elsewhere in the fleet — the pattern is not
+ specific to Emacs.
+- Worth pairing with gap 2: a unit that is enabled and failing every fifteen
+ minutes for two days is its own signal. A post-rebuild pass over
+ =systemctl --user list-units --state=failed= would have caught this one
+ without knowing anything about calendars.
+
+That last one generalizes best. Of the five gaps I have sent you, three were
+things that *looked* fine — a stowed unit file, an enabled timer, a present
+clone — and were not.
diff --git a/docs/design/2026-08-14-velox-reinstall-gaps-3.org b/docs/design/2026-08-14-velox-reinstall-gaps-3.org
new file mode 100644
index 0000000..9675973
--- /dev/null
+++ b/docs/design/2026-08-14-velox-reinstall-gaps-3.org
@@ -0,0 +1,98 @@
+#+TITLE: Reinstall gaps, part three — per-install certs and credentials, and one failure that hid the others
+#+AUTHOR: Craig Jennings
+
+* Third handoff today
+
+Two earlier notes covered five gaps
+([[file:2026-08-14-velox-reinstall-gaps-1.org][the first report]] and
+[[file:2026-08-14-velox-reinstall-gaps-2.org][the follow-up]]).
+Email was the last thing broken on velox after the 2026-08-13 rebuild, and it
+turned up two more — both the same shape, and one of them with a property worth
+generalizing.
+
+Email is fully working now: three accounts, 21,853 messages, 4.0 GB indexed.
+
+* Gap 6 — the Proton Bridge TLS cert is per-install, and its absence disabled every account
+
+=~/.mbsyncrc= carries =CertificateFile /home/cjennings/.config/protonbridge.pem=.
+That file did not exist after the rebuild, and it cannot be restored from backup
+or copied from the other machine: Proton Bridge generates a fresh self-signed
+cert per installation. Velox's is issued 2026-08-13 23:44 with a different
+fingerprint from ratio's 2026-01-30 one.
+
+*The part worth acting on is the blast radius.* mbsync parses its entire config
+before doing any work, so a missing =CertificateFile= referenced by *one* account
+aborts the run for *all* of them. Gmail and dmail need no bridge and no cert, and
+both were dead anyway. The error names only the missing pem, so the symptom
+("no mail at all") and the message ("this one file is missing") look unrelated.
+
+Recovery does not need the bridge GUI. The running bridge presents the cert on
+its own IMAP port, so it can be pulled straight off the handshake:
+
+: openssl s_client -connect 127.0.0.1:1143 -starttls imap -showcerts </dev/null \
+: | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > ~/.config/protonbridge.pem
+
+That is a two-second, fully scriptable step, which makes it a good candidate for
+the install rather than a runbook line.
+
+* Gap 7 — the bridge password is per-install too, and reports a stale value misleadingly
+
+=~/.mbsyncrc= resolves the cmail password with =cat ~/.config/.cmailpass=. That
+file is plaintext and, unusually for my setup, a real file rather than a stow
+symlink — so it is not in the dotfiles repo, not encrypted, and not carried to a
+new machine.
+
+The file survived the rebuild but held the *previous* install's password, because
+the bridge regenerates it per installation. Ratio's and velox's differ by sha256,
+confirmed today.
+
+*The diagnostic trap:* Proton Bridge answers a wrong password with =no such
+user=. I read that as "the bridge has no account signed in" and went looking for
+a login problem. The account was configured the whole time. If the install ever
+validates bridge connectivity, it should not treat =no such user= as evidence
+about account state.
+
+* The generalization
+
+Gaps 6 and 7 are the same as 1 through 5, sharpened. Everything that broke in
+this rebuild was *generated on the machine by an application* rather than carried
+by git, stow, or the dotfiles repo:
+
+| gap | artifact | why it did not travel |
+| 1 | =.ai/=, =todo.org=, =CLAUDE.md= | gitignored |
+| 2 | =timers.target.wants= symlinks | written by systemctl enable |
+| 3 | roam clone state | local working tree |
+| 4 | signal-cli registration | per-device identity |
+| 5 | =*.local.el= configs | gitignored |
+| 6 | bridge TLS cert | per-install, regenerated |
+| 7 | bridge password | per-install, regenerated |
+
+Gaps 6 and 7 add a distinction the earlier note missed. For 1, 3 and 5 the old
+value is still correct, so *restoring* fixes them. For 4, 6 and 7 the old value is
+*worthless* — the application has generated a new one, and only *re-deriving*
+from the live system fixes them. An install that tries to restore these will
+produce exactly what happened here: a file that exists, looks right, and
+authenticates against nothing.
+
+So the install's post-rebuild checklist wants two columns, not one: what to
+restore, and what to re-derive.
+
+* What the install could do
+
+- Re-derive the bridge cert from the running bridge with the =openssl s_client=
+ line above. Scriptable, no GUI, no secrets.
+- Re-derive the bridge password from the bridge rather than expecting the file to
+ be right, and rewrite =.cmailpass=. (I have filed a task on my side to make
+ =PassCmd= ask the bridge directly, which would remove the file entirely.)
+- Add a cheap post-rebuild validation that =mbsync --list= parses. Config-parse
+ failures disable every account at once and say nothing about mail, so they are
+ worth catching explicitly rather than via "no new mail" hours later.
+- More generally: keep the restore list and the re-derive list separate, per the
+ table above.
+
+* Unrelated, but noticed while comparing the machines
+
+=~/.config/.gmailpass.gpg= and =~/.config/.dmailpass.gpg= resolve to mode 777 in
+the dotfiles repo, on both machines. They are gpg-encrypted so the contents are
+safe, but world-writable is wrong for a credential file. That is a dotfiles fix,
+not an archsetup one — noting it here only because it surfaced in the same pass.
diff --git a/docs/workflows/system-health-check.org b/docs/workflows/system-health-check.org
index 777e724..43aeff5 100644
--- a/docs/workflows/system-health-check.org
+++ b/docs/workflows/system-health-check.org
@@ -244,7 +244,7 @@ Firmware stays a manual step (=topgrade.toml= keeps =[firmware] upgrade = false=
1. =sudo efibootmgr -v= — every boot entry with its loader path and command line, pasted into the session's context file.
2. Secure Boot state — =bootctl status 2>/dev/null | grep -i 'secure boot'=.
-After the flash, if the machine reports no bootable device, check Secure Boot *first*. The Framework 04.02 update on velox re-enabled it, which rejects the unsigned ZFSBootMenu loader and reads as "Default Boot Device Missing" rather than a security violation; the boot entries were untouched (see the Known Issues Log, 2026-09-12). Only when Secure Boot is off and =efibootmgr -v= from a stick shows the entry gone does the boot-entry recreate apply (velox: =working/velox-reinstall/velox-uefi-boot-entry-reference.org= in archsetup).
+After the flash, if the machine reports no bootable device, check Secure Boot *first*. The Framework 04.02 update on velox re-enabled it, which rejects the unsigned ZFSBootMenu loader and reads as "Default Boot Device Missing" rather than a security violation; the boot entries were untouched (see the Known Issues Log, 2026-09-12). Only when Secure Boot is off and =efibootmgr -v= from a stick shows the entry gone does the boot-entry recreate apply (velox: =docs/2026-08-15-velox-uefi-boot-entry-reference.org= in archsetup).
*** Two-Stage Reboot Pattern (MANDATORY if Phase 3 installed kernel / iproute2 / systemd / NetworkManager)
@@ -1059,7 +1059,7 @@ Each entry is scoped to one host (or =any=). When Phase 1 cross-references findi
** 2026-09-12: any — fwupdmgr activates passim, a public LAN listener
:host: any
- Symptom: running =fwupdmgr= (refresh, update) D-Bus-activates =passim.service=, fwupd's LAN metadata-sharing daemon, which listens on =0.0.0.0:27500= and trips the maint listeners check to crit.
-- The unit is static (no =[Install]= section), so =systemctl disable= is a no-op and it comes back on the next fwupdmgr run. Masking is what holds: =systemctl mask passim.service=. Ratio has been masked since 2026-07-21; velox was only stopped and disabled on 2026-09-12 and still needs the mask. The installer masks it as part of installing fwupd. =P2pPolicy=nothing= under =[fwupd]= in =/etc/fwupd/fwupd.conf= also works, but that file is pacman-owned and invites pacnew churn, so the mask is the form in use.
+- The unit is static (no =[Install]= section), so =systemctl disable= is a no-op and it comes back on the next fwupdmgr run. Masking is what holds: =systemctl mask passim.service=. Ratio has been masked since 2026-07-21; velox was stopped and disabled on 2026-09-12 (the disable being the no-op) and got the mask on 2026-09-13. The installer masks it as part of installing fwupd. =P2pPolicy=nothing= under =[fwupd]= in =/etc/fwupd/fwupd.conf= also works, but that file is pacman-owned and invites pacnew churn, so the mask is the form in use.
- Classification: KNOWN — a passim listener means a machine that predates the mask or lost it; mask it, don't allowlist it.
** 2026-09-12: velox — topgrade containers step fails on locally built images
diff --git a/todo.org b/todo.org
index 6c087a0..d330a44 100644
--- a/todo.org
+++ b/todo.org
@@ -46,6 +46,34 @@ below):
input-side-spec.org (DRAFT, four decisions open).
* Archsetup Open Work
+** TODO [#B] Visual separator between adjacent waybar modules :feature:waybar:dotfiles:quick:
+:PROPERTIES:
+:CREATED: [2026-09-13 Sun]
+:LAST_REVIEWED: 2026-09-13
+:END:
+Captured by Craig 2026-07-20 and routed here from the roam inbox, then lost
+in the processed pile: the wind (weather) value runs straight into the date
+with no visual stop, so the wind figure reads as the start of the date. Add
+a light separator or spacing between adjacent modules so each one's edge is
+unmistakable. Check the current bar first (the mic/PTT merge and the weather
+chip grouping landed after the capture), then decide the form: a thin rule,
+a dot glyph, or just margin. That choice is mine, so not solo; the CSS itself
+is a quick change in the dotfiles waybar stylesheet.
+
+** TODO [#C] Saving and restoring a window configuration :feature:hyprland:research:
+:PROPERTIES:
+:CREATED: [2026-09-13 Sun]
+:LAST_REVIEWED: 2026-09-13
+:END:
+Research idea captured by Craig 2026-07-24 (the one item of that batch that
+never got filed): when I want a specific window orientation, I indicate it
+and the window-plus-app configuration reappears. What would we need to know
+or store to make that happen? Is there another desktop or OS that does it,
+what information do they keep, and what are their rules? Explore how far
+Hyprland can get, document thoroughly, and review the findings with me before
+building anything. The deliverable is a research note under docs/design, so
+this is not solo.
+
** TODO [#C] maint backup_freshness probe blind to backup_run remedy runs :bug:maint:dotfiles:solo:
:PROPERTIES:
:CREATED: [2026-09-12 Sat]
@@ -195,31 +223,6 @@ to read file" on stderr and nothing on stdout; piped into =grep -c=, that
empty stdout reads as 0 and looks like a missing module. The gate has to
run as root and check the exit status, not just the count.
-** TODO [#A] Lock-screen clock stale after a real sleep :bug:hyprland:dotfiles:velox:
-SCHEDULED: <2026-08-25 Tue>
-:PROPERTIES:
-:CREATED: [2026-08-25 Tue]
-:LAST_REVIEWED: 2026-08-25
-:END:
-After waking velox from a real sleep, the hyprlock clock shows a stale time
-(Craig confirmed 2026-08-24: the wake-from-sleep case, not an idle-locked
-screen). Three isolated tests on 2026-08-24 failed to reproduce it — hyprlock
-0.9.6 repainted within a second of a display power-cycle, a three-minute
-SIGSTOP, and both together with the screenshot background — so it needs a real
-suspend on the real hardware.
-
-Grading: Minor severity (cosmetic-to-confusing, the screen still unlocks) ×
-most users frequently (every wake) = P3 = [#C] by the matrix; held at [#A] at
-Craig's direction on 2026-08-25 so it gets run while velox is the daily driver
-on the road. Revisit the letter once the manual check has an answer.
-
-Not :solo: — the distinguishing observation is Craig's. The check lives under
-Manual testing and validation: "Lock screen after a real sleep: is the clock
-frozen, or is all of hyprlock frozen?". Its three outcomes each name a
-different fix: stale-then-corrects → repaint interval; frozen with live input
-→ clock rendering; frozen with dead input → hyprlock hung, a crash/hang
-recovery bug the =screen-lock= watchdog doesn't cover.
-
** TODO [#C] post-rebuild-check: probe that Emacs frames come up Wayland-native :feature:emacs:velox:solo:quick:
:PROPERTIES:
:CREATED: [2026-08-25 Tue]
@@ -467,24 +470,6 @@ Alternatives if it drags on: change Signal's tray setting so it keeps a
window (=~/.config/Signal/ephemeral.json= =system-tray-setting=), or run a
waybar carrying the #5240 fallback.
-** DOING [#A] Velox reinstall — DR test of archangel + archsetup :velox:chore:
-DEADLINE: <2026-08-15 Sat>
-:PROPERTIES:
-:CREATED: [2026-08-13 Thu]
-:LAST_REVIEWED: 2026-08-13
-:END:
-Mainboard swapped Intel→AMD (Ryzen AI 9 HX 370); new NVRAM has no boot entry.
-Decision: full reinstall via archangel+archsetup, run deliberately as a
-disaster-recovery drill before the Sunday flight. Runbook (live checklist):
-[[file:working/velox-reinstall/velox-reinstall-runbook.org][working/velox-reinstall/velox-reinstall-runbook.org]]
-Done 2026-08-13: ISO rebuilt (archangel-2026-08-13, archsetup baked with AMD
-microcode detection, velox profiles at /root/, .ai/inbox excluded — build.sh
-edits pending commit in archangel), contents verified, dotfiles swept clean of
-Intel assumptions.
-Finding folded in: velox's truenas backups silently stopped ~Jul 6 (newest is
-DAILY.0 Jul 6; wolf.conf.gpg from Jul 29 is in NO backup). Salvage pass in the
-runbook is therefore REQUIRED before partitioning, and the fresh install must
-fix + verify the backup timer (runbook Phase 5).
** TODO [#B] Truenas session-host VM for long-running agent sessions :feature:tooling:
:PROPERTIES:
:CREATED: [2026-08-13 Thu]
@@ -747,6 +732,14 @@ So the checklist wants two columns, not one.
Graded [#A] because item 5 is live right now and silently disables
paging, and because the flight is Sunday.
+
+*** 2026-09-13 Sun @ 07:14:54 -0500 Moved the three gap reports into the reinstall working dir
+The 2026-08-14 reports that define the five gaps moved out of inbox/ into
+[[file:docs/design/2026-08-14-velox-reinstall-gaps-1.org][gaps 1-4]],
+[[file:docs/design/2026-08-14-velox-reinstall-gaps-2.org][gap 5]] and
+[[file:docs/design/2026-08-14-velox-reinstall-gaps-3.org][the two email-side gaps]] (the Bridge cert and the Bridge password).
+They file with the rest of the reinstall artifacts when that task closes.
+
** TODO [#B] Restoring a git repo from backup can resurrect a dangerous diff :bug:
:PROPERTIES:
:CREATED: [2026-08-14 Fri]
@@ -806,30 +799,6 @@ matches "the store is the source of truth" — a channel with exactly one
plausible reading should not be a dead end.
Grading: Minor severity (one fallback still puts a wallpaper up) x some
users sometimes = P3 = [#C].
-** TODO [#B] Land the rescued emacs-wttrin commit :chore:velox:
-:PROPERTIES:
-:CREATED: [2026-08-14 Fri]
-:LAST_REVIEWED: 2026-08-17
-:END:
-bf0457f "feat: add wttrin-hide-follow-line to hide the wttr.in follow line"
-(2026-06-24) was the only genuinely unpushed commit anywhere on the old
-velox — 3 files, 103 insertions, with a test file. Rescued as a verified
-git bundle before the disk was wiped:
-[[file:working/velox-reinstall/wttrin-bf0457f.bundle][working/velox-reinstall/wttrin-bf0457f.bundle]]
-To land it: clone emacs-wttrin, =git fetch <bundle> --branches=, review the
-commit, then push to git@cjennings.net:emacs-wttrin.git. Delete the bundle
-once it's on the remote.
-
-*** 2026-08-17 Mon @ 19:57:42 -0700 Re-checked: still unlanded, and the bundle is still the only copy
-Cloned the remote bare and asked it for the object directly: =git cat-file -t
-bf0457f= returns "Not a valid object name", so the commit has never reached
-=git@cjennings.net:emacs-wttrin.git=. Remote =main= is =ee8fdeb=.
-
-That makes =working/velox-reinstall/wttrin-bf0457f.bundle= the sole surviving
-copy of 103 insertions across three files, on one laptop that is travelling.
-Worth doing sooner than its =[#B]= suggests for that reason alone, and it also
-pins the working directory open — the reinstall task cannot file its artifacts
-away while this bundle is still load-bearing.
** TODO [#B] archsetup doesn't clone rulesets :bug:velox:
DEADLINE: <2026-08-15 Sat>
:PROPERTIES:
@@ -1280,6 +1249,12 @@ split zroot/var/log and zroot/var/lib/pacman out as configure_zfs_snapshots
assumes, or change the config to match the layout velox actually has. That is
a call I have not made, so the task stays [#B] and not solo.
+*** 2026-09-13 Sun @ 07:14:54 -0500 Filed the original boot-failure handoff under docs/design
+The 2026-07-15 diagnosis and recovery plan that opened this task sat in
+inbox/ as a processed file; it now lives at
+[[file:docs/design/2026-07-15-velox-boot-failure-handoff.org][docs/design/2026-07-15-velox-boot-failure-handoff.org]]
+so the timeline survives the inbox sweep.
+
** TODO [#C] Assess a Hyprland left-drag window gesture :feature:hyprland:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
@@ -1615,6 +1590,14 @@ Restyle the audio panel's GTK CSS onto =tokens-waybar.css= + the banked composit
After ~5 hand ports, weigh widget-level codegen with evidence (mechanical duplication vs judgment per port). Recorded as a dated decision in the spec; go spawns its own spec.
*** TODO Flip the spec to IMPLEMENTED
When the phases above close: status heading keyword → =IMPLEMENTED=, dated history line with the reason, Metadata =Status= mirror. Three lines, one file.
+
+*** 2026-09-13 Sun @ 07:21:18 -0500 Filed the two Maeda applets into the clock display references
+The Line (C2, 1997) and Cosmos (C1, 1995) standalone applets sent from the
+website project on 2026-07-30 sat in inbox/ as processed files; they and
+their notes now live beside the other references as
+=2026-07-30-maeda-{line,cosmos}-standalone.html= and =-notes.org=.
+Reference only, regenerate rather than edit.
+
** TODO [#B] Net doctor expansion v1 — VM live verification :feature:dotfiles:network:solo:
:PROPERTIES:
:SPEC_ID: ce29b103-ed9d-4f56-bf8c-9ed8fe680ff3
@@ -2312,38 +2295,10 @@ fi
Expected: "stays gone". If it regenerates, Hyprland is still resolving its config
to the .conf path and the port is not actually live — stop and investigate.
-*** Lock screen after a real sleep: is the clock frozen, or is all of hyprlock frozen?
-What we're verifying: which of two very different faults produces the stale
-lock-screen clock Craig sees after waking the machine. Three isolated tests on
-2026-08-24 all failed to reproduce it — hyprlock v0.9.6 repainted correctly
-within one second of a display power-cycle, of a three-minute SIGSTOP, and of
-both together with the =path = screenshot= background. So the cause needs a real
-suspend on real AMD hardware, which only Craig can run.
-
-The distinguishing question is whether *anything* on the lock screen is alive.
-A frozen clock with live input is a rendering bug in the clock. A frozen clock
-with dead input means hyprlock is hung and the compositor is simply holding its
-last pre-suspend frame — a different bug, in the crash/hang recovery layer.
-
-Run this the next time the machine has actually slept (30 min idle, or a lid
-close), not on a screen you just locked by hand.
-
-- Wake the machine and do NOT type your password yet.
-- Read the clock. Note the time it shows and the real time.
-- Type one character into the password field.
-Expected (record which): either the dots appear normally — input is alive, only
-the clock is stale — or nothing appears at all, in which case hyprlock is hung.
-- Keep watching for a full 90 seconds without touching anything else.
-Expected (record which): the clock corrects itself to the current time within a
-minute, or it never changes at all.
-- Note whether the blurred background looks like your desktop or like a
- previous lock screen (a lock screen photographed behind a lock screen means a
- second hyprlock was started on top of the first).
-
-Expected: enough to pick one of three — stale-then-corrects (a repaint-interval
-fix), frozen-with-live-input (a rendering fix), or frozen-with-dead-input
-(hyprlock hung; the =screen-lock= watchdog only catches a hyprlock that
-*exits*, and its own header says it is blind to one that hangs).
+*** 2026-09-13 Sun @ 07:57:32 -0500 Retired the lock-screen clock check: fixed, per Craig
+Craig reported on 2026-09-13 Sun that the stale clock after a real sleep no longer
+happens on velox, so the three-outcome check never needed running. The
+parent bug task is closed with the same note.
*** Lock keybind has no crash or hang recovery
What we're verifying: that a hand-lock is as recoverable as an idle lock.
@@ -3596,7 +3551,7 @@ than it looked when it was filed sight-unseen; still worth doing (unlisted
fixes happen, and ACPI tables change), just no longer the front-runner.
Deliberately deferred past the flight — a cleared NVRAM is the failure that
started this whole rebuild. Boot-entry recovery reference captured at
-[[file:working/velox-reinstall/velox-uefi-boot-entry-reference.org][working/velox-reinstall/velox-uefi-boot-entry-reference.org]].
+[[file:docs/2026-08-15-velox-uefi-boot-entry-reference.org][docs/2026-08-15-velox-uefi-boot-entry-reference.org]].
Order of attack on return, cheapest first: reseat the touchpad's *own* press
connector at the mainboard (free, untried) → BIOS 04.02 → fit the replacement
@@ -3649,44 +3604,6 @@ 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 [#B] Podman API socket and camera-passthrough udev rule :feature:solo:
-CLOSED: [2026-08-09 Sun]
-:PROPERTIES:
-:CREATED: [2026-08-07 Fri]
-:LAST_REVIEWED: 2026-08-07
-:END:
-Shipped 2026-08-09: the installer enables the rootless podman socket at
-install time (enable_user_service grew a wants-target arg so socket units
-land in sockets.target.wants) and ships
-=72-usb-passthrough-cameras.rules= — numbered below 73 per the winvm
-rule-ordering correction, GROUP/MODE as the verified grant, uaccess tag kept.
-Applied live on ratio (socket enabled+active, 99- file retired, udev
-reloaded); velox apply rides the velox-return riders on the sleep/suspend
-task. The uaccess-alone hypothesis stays untested until a camera is attached.
-From winvm 2026-08-07 (ratio). Two one-time machine-level setups, both live on
-ratio and absent on velox; full evidence and rationale in
-[[file:docs/design/2026-08-07-podman-socket-and-camera-udev.md]].
-
-- Enable the rootless podman socket at install time
- (=systemctl --user enable --now podman.socket=). Socket-activated, zero idle
- cost; every podman GUI/API client needs it, and its absence fails silently
- (Pods opens to an empty window). The installer already carries the
- "=systemctl --user enable= fails during install" workaround pattern
- (=archsetup:1270=, =:2722=) — use it.
-- Ship a udev rule granting GROUP="video", MODE="0660" on the OBSBOT
- (3564:ff02) and BRIO (046d:085e) USB nodes so =usbredirect= can claim them
- for VM passthrough. CORRECTED (winvm, 2026-08-08): the original "uaccess
- can't ACL raw USB nodes" claim was wrong — the mechanism is rule ordering.
- The ACL is applied by =73-seat-late.rules=, so a =99-= rule adds the tag
- after that already ran; distro rules that add the tag all sort at or below
- 70. [@70] So number our file below 73 (e.g. =72-usb-passthrough-cameras.rules=),
- keep the verified GROUP/MODE grant, and keep the tag — correctly ordered it
- may make uaccess work on its own (untested hypothesis; a tighter grant if
- it holds, needs the camera plugged in to verify). Reconcile ratio's
- existing =99-= file (winvm installed it) when the installer version lands.
-
-Scope: installer step + rule file + tests per existing shapes, and apply both
-live to velox over tailscale (daily-driver sync — neither exists there today).
** CANCELLED [#B] agent-text relay reports success for a message that went nowhere :bug:
CLOSED: [2026-08-19 Wed]
:PROPERTIES:
@@ -4338,3 +4255,119 @@ OS-side knob exists. One keystroke: Fn+Esc toggles Fn Lock; Craig confirmed
F1-F12 now send F-keys by default. The EC holds the state across reboots; it
reverts only if the EC loses power (battery disconnect or mainboard reset),
which is likely why it flipped around the August reinstall.
+** DONE [#A] Lock-screen clock stale after a real sleep :bug:hyprland:dotfiles:velox:
+CLOSED: [2026-09-13 Sun] SCHEDULED: <2026-08-25 Tue>
+:PROPERTIES:
+:CREATED: [2026-08-25 Tue]
+:LAST_REVIEWED: 2026-08-25
+:END:
+After waking velox from a real sleep, the hyprlock clock shows a stale time
+(Craig confirmed 2026-08-24: the wake-from-sleep case, not an idle-locked
+screen). Three isolated tests on 2026-08-24 failed to reproduce it — hyprlock
+0.9.6 repainted within a second of a display power-cycle, a three-minute
+SIGSTOP, and both together with the screenshot background — so it needs a real
+suspend on the real hardware.
+
+Grading: Minor severity (cosmetic-to-confusing, the screen still unlocks) ×
+most users frequently (every wake) = P3 = [#C] by the matrix; held at [#A] at
+Craig's direction on 2026-08-25 so it gets run while velox is the daily driver
+on the road. Revisit the letter once the manual check has an answer.
+
+Not :solo: — the distinguishing observation is Craig's. The check lives under
+Manual testing and validation: "Lock screen after a real sleep: is the clock
+frozen, or is all of hyprlock frozen?". Its three outcomes each name a
+different fix: stale-then-corrects → repaint interval; frozen with live input
+→ clock rendering; frozen with dead input → hyprlock hung, a crash/hang
+recovery bug the =screen-lock= watchdog doesn't cover.
+
+*** 2026-09-13 Sun @ 07:57:04 -0500 Closed: fixed, per Craig
+Craig confirmed on 2026-09-13 Sun that the stale clock after a real sleep is fixed on
+velox. I could not identify the commit from here (nothing in the dotfiles
+or archsetup log since 2026-08-24 names hyprlock, the lock clock, sleep or
+resume), so this closes on his report rather than a cited change; the
+manual-testing check for it is retired with it.
+** DONE [#A] Velox reinstall — DR test of archangel + archsetup :velox:chore:
+CLOSED: [2026-09-13 Sun] DEADLINE: <2026-08-15 Sat>
+:PROPERTIES:
+:CREATED: [2026-08-13 Thu]
+:LAST_REVIEWED: 2026-08-13
+:END:
+Mainboard swapped Intel→AMD (Ryzen AI 9 HX 370); new NVRAM has no boot entry.
+Decision: full reinstall via archangel+archsetup, run deliberately as a
+disaster-recovery drill before the Sunday flight. Runbook (live checklist):
+[[file:docs/2026-08-13-velox-reinstall-runbook.org][docs/2026-08-13-velox-reinstall-runbook.org]]
+Done 2026-08-13: ISO rebuilt (archangel-2026-08-13, archsetup baked with AMD
+microcode detection, velox profiles at /root/, .ai/inbox excluded — build.sh
+edits pending commit in archangel), contents verified, dotfiles swept clean of
+Intel assumptions.
+Finding folded in: velox's truenas backups silently stopped ~Jul 6 (newest is
+DAILY.0 Jul 6; wolf.conf.gpg from Jul 29 is in NO backup). Salvage pass in the
+runbook is therefore REQUIRED before partitioning, and the fresh install must
+fix + verify the backup timer (runbook Phase 5).
+
+*** 2026-09-13 Sun @ 07:16:27 -0500 Applied the two live convergence steps on velox
+Ratio got both by hand on 2026-09-12 while velox was off the tailnet; velox
+came back on 2026-09-13 and got them over ssh: =systemctl disable --now
+wsdd.service= (no Samba host to advertise; 43acf51 stops the installer
+enabling it) and =systemctl mask passim.service= (the unit is static, so the
+09-12 disable was a no-op; 38b1758 masks it in the installer, but
+supplemental_software is a completed step there and doesn't re-run).
+Verified after: wsdd inactive/disabled, passim inactive/masked, zero
+listeners on 5357 and 27500. Same pass fast-forwarded velox's dotfiles to
+f56fd1a and archsetup to dc00a62, and confirmed the headless Proton Bridge
+service is still disabled there.
+
+*** 2026-09-13 Sun @ 07:56:37 -0500 Closed the drill and filed its working-dir artifacts
+The reinstall itself finished on 2026-08-14; every finding it surfaced is
+its own task, and the live convergence steps landed on 2026-09-13, so nothing
+was left in this task but the record. Filed per the working-files
+convention: the runbook to docs/2026-08-13-velox-reinstall-runbook.org (with
+an Outcome section, since the checklist was never ticked during the run),
+the boot-entry reference to docs/2026-08-15-velox-uefi-boot-entry-reference.org,
+the three gap reports to docs/design/2026-08-14-velox-reinstall-gaps-1 to 3,
+and the wttrin bundle to working/emacs-wttrin-rescue/ under the task that
+owns it. working/velox-reinstall/ is gone and every inbound link repointed.
+** DONE [#B] Land the rescued emacs-wttrin commit :chore:velox:
+CLOSED: [2026-09-13 Sun]
+:PROPERTIES:
+:CREATED: [2026-08-14 Fri]
+:LAST_REVIEWED: 2026-08-17
+:END:
+bf0457f "feat: add wttrin-hide-follow-line to hide the wttr.in follow line"
+(2026-06-24) was the only genuinely unpushed commit anywhere on the old
+velox — 3 files, 103 insertions, with a test file. Rescued as a verified
+git bundle before the disk was wiped (wttrin-bf0457f.bundle, deleted on
+2026-09-13 once the commit was on the remote):
+To land it: clone emacs-wttrin, =git fetch <bundle> --branches=, review the
+commit, then push to git@cjennings.net:emacs-wttrin.git. Delete the bundle
+once it's on the remote.
+
+*** 2026-08-17 Mon @ 19:57:42 -0700 Re-checked: still unlanded, and the bundle is still the only copy
+Cloned the remote bare and asked it for the object directly: =git cat-file -t
+bf0457f= returns "Not a valid object name", so the commit has never reached
+=git@cjennings.net:emacs-wttrin.git=. Remote =main= is =ee8fdeb=.
+
+That makes =working/emacs-wttrin-rescue/wttrin-bf0457f.bundle= (moved there 2026-09-13 when the reinstall task filed its artifacts) the sole surviving
+copy of 103 insertions across three files, on one laptop that is travelling.
+Worth doing sooner than its =[#B]= suggests for that reason alone. It also
+pinned the reinstall working directory open until 2026-09-13, when the bundle
+moved into its own working dir here and the reinstall task closed.
+
+*** 2026-09-13 Sun @ 09:47:20 -0500 Landed on emacs-wttrin release/0.4.0 and deleted the bundle
+Cherry-picked unchanged onto release/0.4.0 (which already contains main; the
+remote's default branch is still main, so main doesn't carry it yet) as
+cb70193, patch-id identical to bf0457f. Review turned up two
+defects the rescued commit carried, both fixed test-first and pushed with it:
+f9449f6 makes the existing-F check case-sensitive (case-fold-search defaults
+to t, so a lowercase f read as F), and 9c8d23e keys the cache on the effective
+display options (toggling the setting kept serving a cached buffer with the
+Follow line, against the 944a52f rule). Pushed 37e1c94..9c8d23e; full suite
+green at 73 files.
+
+Two things checked before landing: an old emacs-wttrin note that F broke ANSI
+colour is stale (wttr.in sends identical colour codes with and without F), and
+the suite's smoke failure was only missing Emacs 31.1 eask deps. The bundle's
+main and release/0.4.0 heads are on the remote as-is, and its only other commit
+(bf0457f) is there as the patch-identical cb70193, so the bundle and its
+working dir are deleted.
+emacs-wttrin has a handoff note in its inbox covering all of it.
diff --git a/working/clock-display-references/2026-07-30-maeda-cosmos-notes.org b/working/clock-display-references/2026-07-30-maeda-cosmos-notes.org
new file mode 100644
index 0000000..04d8629
--- /dev/null
+++ b/working/clock-display-references/2026-07-30-maeda-cosmos-notes.org
@@ -0,0 +1,11 @@
+#+TITLE: Companion to the Line applet sent a few minutes ago: a copy
+#+SOURCE: from website
+#+DATE: 2026-07-30 18:48:00 -0500
+
+Companion to the Line applet sent a few minutes ago: a copy of Cosmos (C1, 1995), the orbits piece — 'numbers in orbit'. Delivered as 2026-07-30-maeda-cosmos-standalone.html in this inbox.
+
+Same shape as the Line copy: the <maeda-cosmos> web component plus its model/step/render modules esbuild-bundled inline as an IIFE, so it opens straight from file:// with no server and no network. Verified in Chrome from file:// — canvas 400x300, the month's days riding the rotated ellipse with today red on the sweep hand, seconds odometer live at the right edge, zero console errors.
+
+Controls: hover reshapes every orbit; press-drag pulls a new loop out of the system; the mark at bottom-left clears them and replays the fly-in credit; click the canvas then ESC toggles strip-move mode. Note this one has no readout event, unlike Line.
+
+Source of truth stays ~/code/maeda-tribute (src/pieces/cosmos/); regenerate rather than edit. Reference only — no action needed.
diff --git a/working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html b/working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html
new file mode 100644
index 0000000..bfa9fbb
--- /dev/null
+++ b/working/clock-display-references/2026-07-30-maeda-cosmos-standalone.html
@@ -0,0 +1,422 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Cosmos (C1, 1995) — Maeda × Shiseido tribute</title>
+<style>
+:root{
+ --ground:#151311;
+ --panel:#100f0f;
+ --well:#0a0c0d;
+ --raise:#1a1917;
+ --silver:#bfc4d0;
+ --cream:#f3e7c5;
+ --steel:#969385;
+ --dim:#7c838a;
+ --wash:#2c2f32;
+ --gold:#e2a038;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground);font-size:112%}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.wrap{max-width:820px;margin:0 auto}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{font-size:1.5rem;color:var(--cream);font-weight:600;margin:.35rem 0 .2rem}
+.sub{color:var(--dim);font-size:.9rem;margin-bottom:1.6rem}
+.card{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;
+ border-radius:12px;padding:1.2rem;margin-bottom:1.4rem}
+.stage{background:var(--well);border:1px solid #22201d;border-radius:8px;padding:1rem;
+ display:flex;justify-content:center}
+.wnote{color:var(--dim);font-size:.85rem;margin-top:1rem}
+.wnote b{color:var(--steel);font-weight:600}
+.igrid{display:grid;grid-template-columns:11rem 1fr;gap:.35rem .9rem;margin-top:1rem;
+ font-size:.82rem}
+.ik{color:var(--steel)}
+.iv{color:var(--dim)}
+</style>
+</head>
+<body>
+<div class="wrap">
+ <p class="eyebrow">Waste Time Beautifully · C1</p>
+ <h1>Cosmos — numbers in orbit</h1>
+ <p class="sub">John Maeda for Shiseido, 1995 (Cal0.class) — rebuilt as a web component. Standalone copy, no build step.</p>
+
+ <div class="card">
+ <div class="stage"><maeda-cosmos id="piece"></maeda-cosmos></div>
+ <p class="wnote"><b>Controls:</b> hover to reshape every orbit — the axes track 1.5× the
+ cursor's distance from each orbit's center; press and drag to pull a new loop out of the
+ system; the mark at bottom-left clears them and replays the fly-in credit. Click the
+ canvas first, then <b>ESC</b> toggles strip-move mode (the ground goes orange and the
+ clock strip comes free).</p>
+ <p class="wnote">The days of the month ride a rotated ellipse, today in red on the sweep
+ hand, with a seconds odometer scrolling at the right edge. The month as a gravitational
+ system, days as bodies in orbit, the user as a hand that perturbs the heavens. Time is
+ periodic, not linear.</p>
+ </div>
+
+ <div class="card">
+ <p class="eyebrow">Spec</p>
+ <div class="igrid">
+ <span class="ik">original</span><span class="iv">Cal0.class + UniverseCal, orbit, eint, efloatsin (Java 1.1)</span>
+ <span class="ik">dimensions</span><span class="iv">400×300, black ground, Helvetica 10</span>
+ <span class="ik">collection</span><span class="iv">SFMOMA 99.550</span>
+ <span class="ik">revolution</span><span class="iv">7,500 ms per sweep, phase = wall clock mod cycle — every orbit in lockstep forever; orientation wobbles ±10° on a 15,000 ms sine</span>
+ <span class="ik">interaction</span><span class="iv">every mouse move (not just drag) restyles all orbits; press spawns a zero-axis orbit that inflates as the cursor pulls away</span>
+ <span class="ik">period quirks</span><span class="iv">a cursor-driven rotation angle is computed and stored but never drawn — a dead store in the 1995 bytecode; Mac Java Date bug detected and adjusted</span>
+ <span class="ik">credit</span><span class="iv">chars fly in at 800+80i ms from random scatter, hold 1.5 s, vanish; logo click replays</span>
+ <span class="ik">tests</span><span class="iv">38 (Vitest + fast-check)</span>
+ </div>
+ </div>
+</div>
+
+<script>
+(() => {
+ // src/pieces/cosmos/model.js
+ var TAU = Math.PI * 2;
+ var CREDIT = "designed by john maeda";
+ var FAITHFUL_1995 = {
+ faceW: 400,
+ // applet tag WIDTH=400 HEIGHT=300
+ faceH: 300,
+ cycleMs: 7500,
+ // one revolution, wall-clock modulo
+ wobbleAmp: 10 * Math.PI / 180,
+ // efloatsin(0, 10°, 15000)
+ wobblePeriodMs: 15e3,
+ stretch: 1.5,
+ // axes = 1.5× cursor distance from center
+ initialAxes: [300, 100],
+ // the startup orbit at panel center
+ fontSize: 10,
+ // applet param default; Helvetica plain
+ stripWidth: 20,
+ // stringWidth("59")+6 at 10 px, rounded
+ clockReach: 120,
+ // readout + weekday row extent left of the strip
+ logo: { dx: 8, dy: 8, w: 70, h: 16 }
+ // shiseido.gif box, bottom-left
+ };
+ function sweepPhase(cfg, nowMs) {
+ return nowMs % cfg.cycleMs / cfg.cycleMs * TAU;
+ }
+ function wobbleAngle(cfg, elapsedMs) {
+ return cfg.wobbleAmp * Math.sin(elapsedMs * TAU / cfg.wobblePeriodMs);
+ }
+ function orbitPoint(o, theta, alpha) {
+ const ct = Math.cos(theta);
+ const st = Math.sin(theta);
+ const ca = Math.cos(alpha);
+ const sa = Math.sin(alpha);
+ return {
+ x: o.lmaj * ct * ca - o.lmin * st * sa + o.h,
+ y: o.lmaj * ct * sa + o.lmin * st * ca + o.k
+ };
+ }
+ function dayTheta(day, today, ndays, phase) {
+ return phase + (day - today) * TAU / ndays;
+ }
+ function makeOrbit(x, y, lmaj = 0, lmin = 0) {
+ return { h: x + 0.5, k: y + 0.5, lmaj, lmin, wobbleT: 0 };
+ }
+ function stretchOrbit(cfg, o, mx, my) {
+ return { ...o, lmaj: (mx - o.h) * cfg.stretch, lmin: (my - o.k) * cfg.stretch };
+ }
+ function creditDurations(len) {
+ const chars = [];
+ for (let i = 0; i < len; i++) chars.push(800 + 80 * i);
+ return { chars, sentinel: 800 + 80 * len + 1500 };
+ }
+ function easeLinear(t, a, b, dur) {
+ if (t <= 0) return a;
+ if (t >= dur) return b;
+ return a + (b - a) * t / dur;
+ }
+ function creditDone(len, t) {
+ return t >= creditDurations(len).sentinel;
+ }
+ function stripOffset(cfg, sec, msWithin) {
+ const ld = cfg.fontSize + 1;
+ const siddy = 3 + cfg.fontSize;
+ return -ld * sec - msWithin * ld / 1e3 + siddy - ld;
+ }
+ function logoHit(cfg, x, y) {
+ const { dx, dy, w, h } = cfg.logo;
+ return x < dx + w && y > cfg.faceH - h - dy;
+ }
+ function clockHit(cfg, strip, x, y) {
+ const ld = cfg.fontSize + 1;
+ const left = cfg.faceW - strip.sidex - cfg.clockReach;
+ const right = cfg.faceW - strip.sidex + cfg.stripWidth;
+ return x > left && x < right && y < strip.siddy && y > strip.siddy - ld;
+ }
+
+ // src/pieces/cosmos/step.js
+ function scatterStarts(cfg, rng) {
+ const starts = [];
+ for (let i = 0; i < CREDIT.length; i++) {
+ starts.push({
+ x: Math.floor(rng() * cfg.faceW) * (rng() < 0.5 ? 1 : -1) + cfg.faceW / 2,
+ y: Math.floor(rng() * cfg.faceH) * (rng() < 0.5 ? 1 : -1) + cfg.faceH / 2
+ });
+ }
+ return starts;
+ }
+ function initState(cfg, rng = Math.random) {
+ return {
+ orbits: [makeOrbit(cfg.faceW / 2, cfg.faceH / 2, cfg.initialAxes[0], cfg.initialAxes[1])],
+ credit: { t: 0, starts: scatterStarts(cfg, rng) },
+ strip: { sidex: cfg.stripWidth, siddy: 3 + cfg.fontSize },
+ killPending: false,
+ movable: false,
+ draggingStrip: false,
+ prevHeld: false,
+ prevCursor: null,
+ rng
+ };
+ }
+ function toggleMovable(state) {
+ state.movable = !state.movable;
+ return state;
+ }
+ function step(cfg, state, dtMs, input) {
+ const s = { ...state, strip: { ...state.strip }, credit: { ...state.credit } };
+ const { x, y, held } = input;
+ s.credit.t += dtMs;
+ s.orbits = s.orbits.map((o) => ({ ...o, wobbleT: o.wobbleT + dtMs }));
+ if (x == null) {
+ s.prevHeld = held;
+ return s;
+ }
+ if (held && !s.prevHeld) {
+ if (logoHit(cfg, x, y)) {
+ s.killPending = true;
+ s.credit = { t: 0, starts: scatterStarts(cfg, s.rng) };
+ } else if (clockHit(cfg, s.strip, x, y) || s.movable) {
+ s.draggingStrip = true;
+ } else {
+ s.orbits = [...s.orbits, makeOrbit(x, y)];
+ }
+ }
+ if (held && s.draggingStrip && s.prevCursor) {
+ s.strip.sidex -= x - s.prevCursor.x;
+ s.strip.siddy += y - s.prevCursor.y;
+ }
+ s.orbits = s.orbits.map((o) => stretchOrbit(cfg, o, x, y));
+ if (!held && s.prevHeld) {
+ if (s.killPending) {
+ s.orbits = [s.orbits[0]];
+ s.strip = { sidex: cfg.stripWidth, siddy: 3 + cfg.fontSize };
+ }
+ s.killPending = false;
+ s.draggingStrip = false;
+ }
+ s.prevHeld = held;
+ s.prevCursor = { x, y };
+ return s;
+ }
+
+ // src/lib/calendar.js
+ function isLeapYear(y) {
+ return y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
+ }
+ var MDAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+ function daysInMonth(y, m) {
+ return m === 2 && isLeapYear(y) ? 29 : MDAYS[m - 1];
+ }
+
+ // src/pieces/cosmos/render.js
+ var FACE = { w: 400, h: 300 };
+ var MONTHS = ["jan ", "feb ", "mar ", "apr ", "may ", "jun ", "jul ", "aug ", "sep ", "oct ", "nov ", "dec "];
+ var WEEKDAYS = ["S ", "M ", "T ", "W ", "T ", "F ", "S "];
+ function font(cfg) {
+ return `${cfg.fontSize}px Helvetica, Arial, sans-serif`;
+ }
+ function dateParts(nowMs) {
+ const d = new Date(nowMs);
+ return {
+ year: d.getFullYear(),
+ month: d.getMonth() + 1,
+ // 1-12
+ day: d.getDate(),
+ weekday: d.getDay(),
+ hours: d.getHours(),
+ minutes: d.getMinutes(),
+ seconds: d.getSeconds(),
+ msWithin: nowMs % 1e3
+ };
+ }
+ function drawOrbit(ctx, cfg, o, dp, phase, dim) {
+ const ndays = daysInMonth(dp.year, dp.month);
+ const alpha = wobbleAngle(cfg, o.wobbleT);
+ const daystr = `${dp.day} ${dp.year}`;
+ const monthstr = MONTHS[dp.month - 1];
+ for (let d = 1; d <= ndays; d++) {
+ const p = orbitPoint(o, dayTheta(d, dp.day, ndays, phase), alpha);
+ if (d === dp.day) {
+ ctx.fillStyle = "#f00";
+ ctx.fillText(daystr, p.x, p.y);
+ ctx.fillText(monthstr, p.x - ctx.measureText(monthstr).width, p.y);
+ } else {
+ ctx.fillStyle = dim ? "#808080" : "#fff";
+ ctx.fillText(String(d), p.x, p.y);
+ }
+ }
+ }
+ function drawStrip(ctx, cfg, state, dp) {
+ const ld = cfg.fontSize + 1;
+ const { sidex, siddy } = state.strip;
+ const x = FACE.w - sidex;
+ const tileH = ld * 60;
+ const off = stripOffset(cfg, dp.seconds, dp.msWithin) + (siddy - (3 + cfg.fontSize));
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(x, 0, cfg.stripWidth, FACE.h);
+ ctx.clip();
+ ctx.fillStyle = "#404040";
+ ctx.fillRect(x, 0, cfg.stripWidth, FACE.h);
+ ctx.fillStyle = "#808080";
+ for (const tile of [-1, 0, 1]) {
+ for (let i = 0; i < 60; i++) {
+ const y = off + tile * tileH + i * ld + ld;
+ if (y > -ld && y < FACE.h + ld) {
+ ctx.fillText(String(i).padStart(2, "0"), x + 3, y);
+ }
+ }
+ }
+ ctx.restore();
+ ctx.fillStyle = "#f00";
+ ctx.fillText(String(dp.seconds).padStart(2, "0"), x + 3, siddy);
+ const h12 = dp.hours % 12 === 0 ? 12 : dp.hours % 12;
+ const clock = `${h12}${dp.minutes < 10 ? ":0" : ":"}${dp.minutes}:`;
+ const cx = x + 3 - ctx.measureText(clock).width;
+ ctx.fillText(clock, cx, siddy);
+ let wx = cx - ctx.measureText("S M T W T F S ").width;
+ for (let i = 0; i < 7; i++) {
+ ctx.fillStyle = i === dp.weekday ? "#f00" : "#808080";
+ ctx.fillText(WEEKDAYS[i], wx, siddy);
+ wx += ctx.measureText(WEEKDAYS[i]).width;
+ }
+ }
+ function drawCredit(ctx, cfg, state) {
+ const { t, starts } = state.credit;
+ if (creditDone(CREDIT.length, t)) return;
+ const durs = creditDurations(CREDIT.length);
+ let fx = (FACE.w - ctx.measureText(CREDIT).width) / 2 + 1;
+ const fy = FACE.h / 2 + cfg.fontSize / 2;
+ ctx.fillStyle = "#fff";
+ for (let i = 0; i < CREDIT.length; i++) {
+ const ch = CREDIT[i];
+ const x = easeLinear(t, starts[i].x, fx, durs.chars[i]);
+ const y = easeLinear(t, starts[i].y, fy, durs.chars[i]);
+ ctx.fillText(ch, x, y);
+ fx += ctx.measureText(ch).width;
+ }
+ }
+ function drawLogo(ctx, cfg) {
+ const { dx, dy, h } = cfg.logo;
+ ctx.fillStyle = "#808080";
+ ctx.fillText("shiseido", dx, FACE.h - dy - h / 2 + cfg.fontSize / 2);
+ }
+ function draw(ctx, cfg, state, nowMs) {
+ const dp = dateParts(nowMs);
+ ctx.fillStyle = state.movable ? "#e08000" : "#000";
+ ctx.fillRect(0, 0, FACE.w, FACE.h);
+ ctx.font = font(cfg);
+ ctx.textBaseline = "alphabetic";
+ drawStrip(ctx, cfg, state, dp);
+ const phase = sweepPhase(cfg, nowMs);
+ state.orbits.forEach((o, i) => {
+ drawOrbit(ctx, cfg, o, dp, phase, state.killPending && i > 0);
+ });
+ drawLogo(ctx, cfg);
+ drawCredit(ctx, cfg, state);
+ ctx.strokeStyle = "#fff";
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0.5, 0.5, FACE.w - 1, FACE.h - 1);
+ }
+
+ // src/lib/loop.js
+ var STEP_MS = 30;
+ function startLoop({ stepFn, renderFn, stepMs = STEP_MS }) {
+ let acc = 0;
+ let last = null;
+ let raf = null;
+ let running = true;
+ function frame(ts) {
+ if (!running) return;
+ if (last === null) last = ts;
+ acc += Math.min(ts - last, 250);
+ last = ts;
+ while (acc >= stepMs) {
+ stepFn(stepMs);
+ acc -= stepMs;
+ }
+ renderFn();
+ raf = requestAnimationFrame(frame);
+ }
+ raf = requestAnimationFrame(frame);
+ return () => {
+ running = false;
+ if (raf) cancelAnimationFrame(raf);
+ };
+ }
+
+ // src/pieces/cosmos/index.js
+ var MaedaCosmos = class extends HTMLElement {
+ connectedCallback() {
+ const canvas = document.createElement("canvas");
+ canvas.width = FACE.w;
+ canvas.height = FACE.h;
+ canvas.style.cssText = "display:block;width:100%;max-width:400px;cursor:crosshair;touch-action:none";
+ canvas.tabIndex = 0;
+ this.appendChild(canvas);
+ const ctx = canvas.getContext("2d");
+ const frozen = this.getAttribute("frozen-now");
+ const nowMs = () => frozen ? Number(frozen) * 1e3 : Date.now();
+ const cfg = FAITHFUL_1995;
+ let state = initState(cfg);
+ const input = { x: null, y: null, held: false };
+ const toFace = (ev) => {
+ const r = canvas.getBoundingClientRect();
+ return {
+ x: (ev.clientX - r.left) * (FACE.w / r.width),
+ y: (ev.clientY - r.top) * (FACE.h / r.height)
+ };
+ };
+ canvas.addEventListener("pointermove", (ev) => {
+ Object.assign(input, toFace(ev));
+ });
+ canvas.addEventListener("pointerdown", (ev) => {
+ canvas.setPointerCapture(ev.pointerId);
+ canvas.focus();
+ Object.assign(input, toFace(ev));
+ input.held = true;
+ });
+ const release = () => {
+ input.held = false;
+ };
+ canvas.addEventListener("pointerup", release);
+ canvas.addEventListener("pointercancel", release);
+ canvas.addEventListener("keydown", (ev) => {
+ if (ev.key === "Escape") toggleMovable(state);
+ });
+ this._stop = startLoop({
+ stepFn: (dtMs) => {
+ state = step(cfg, state, dtMs, input);
+ },
+ renderFn: () => draw(ctx, cfg, state, nowMs())
+ });
+ }
+ disconnectedCallback() {
+ if (this._stop) this._stop();
+ }
+ };
+ customElements.define("maeda-cosmos", MaedaCosmos);
+})();
+
+</script>
+</body>
+</html>
diff --git a/working/clock-display-references/2026-07-30-maeda-line-notes.org b/working/clock-display-references/2026-07-30-maeda-line-notes.org
new file mode 100644
index 0000000..ba50a80
--- /dev/null
+++ b/working/clock-display-references/2026-07-30-maeda-line-notes.org
@@ -0,0 +1,11 @@
+#+TITLE: Copy of the Line applet (C2, 1997) from the Maeda x Shiseido
+#+SOURCE: from website
+#+DATE: 2026-07-30 18:38:41 -0500
+
+Copy of the Line applet (C2, 1997) from the Maeda x Shiseido tribute — the diagonal timeline piece, 'zoom through time'. Delivered as 2026-07-30-1838-from-website-2026-07-30-maeda-line-standalone.html in this inbox.
+
+It is a single self-contained file: the <maeda-line> web component and its model/step/render modules are esbuild-bundled inline as an IIFE, so it opens straight from file:// with no server, no build step, and no network. Verified rendering in Chrome from file:// — canvas 460x400, zero console errors, live readout wired.
+
+Controls: move the pointer along the diagonal to aim, press and hold to zoom continuously into that moment, release to float back out. Full zoom is exactly 60 seconds and the release is symmetric.
+
+Source of truth stays ~/code/maeda-tribute (src/pieces/line/); this copy is a snapshot, so regenerate rather than edit it if the piece changes. Sent for reference — no action needed unless you want it wired into the panel-widget gallery.
diff --git a/working/clock-display-references/2026-07-30-maeda-line-standalone.html b/working/clock-display-references/2026-07-30-maeda-line-standalone.html
new file mode 100644
index 0000000..783939c
--- /dev/null
+++ b/working/clock-display-references/2026-07-30-maeda-line-standalone.html
@@ -0,0 +1,457 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Line (C2, 1997) — Maeda × Shiseido tribute</title>
+<style>
+:root{
+ --ground:#151311;
+ --panel:#100f0f;
+ --well:#0a0c0d;
+ --raise:#1a1917;
+ --silver:#bfc4d0;
+ --cream:#f3e7c5;
+ --steel:#969385;
+ --dim:#7c838a;
+ --wash:#2c2f32;
+ --gold:#e2a038;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground);font-size:112%}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.wrap{max-width:820px;margin:0 auto}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{font-size:1.5rem;color:var(--cream);font-weight:600;margin:.35rem 0 .2rem}
+.sub{color:var(--dim);font-size:.9rem;margin-bottom:1.6rem}
+.card{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;
+ border-radius:12px;padding:1.2rem;margin-bottom:1.4rem}
+.stage{background:var(--well);border:1px solid #22201d;border-radius:8px;padding:1rem;
+ display:flex;justify-content:center}
+.readout{margin-top:.8rem;color:var(--gold);font-size:.82rem;letter-spacing:.06em}
+.wnote{color:var(--dim);font-size:.85rem;margin-top:1rem}
+.wnote b{color:var(--steel);font-weight:600}
+.igrid{display:grid;grid-template-columns:11rem 1fr;gap:.35rem .9rem;margin-top:1rem;
+ font-size:.82rem}
+.ik{color:var(--steel)}
+.iv{color:var(--dim)}
+</style>
+</head>
+<body>
+<div class="wrap">
+ <p class="eyebrow">Waste Time Beautifully · C2</p>
+ <h1>Line — zoom through time</h1>
+ <p class="sub">John Maeda for Shiseido, 1997 (cal1.class) — rebuilt as a web component. Standalone copy, no build step.</p>
+
+ <div class="card">
+ <div class="stage"><maeda-line id="piece"></maeda-line></div>
+ <div class="readout" id="readout">scale — · counter —</div>
+ <p class="wnote"><b>Controls:</b> move the pointer along the diagonal to aim; press and
+ hold to zoom continuously into that moment; release and you float back out. Full zoom
+ is exactly 60 seconds, and the release is symmetric.</p>
+ <p class="wnote">Time as a single line — <b>diagonal</b>, bottom-left to top-right, the
+ detail every written description of this applet gets wrong. Maeda's note: it reflects
+ the relativity and comparability of time intervals. The zoom is the content.</p>
+ </div>
+
+ <div class="card">
+ <p class="eyebrow">Spec</p>
+ <div class="igrid">
+ <span class="ik">original</span><span class="iv">cal1.class, 12,339 bytes, single class, self-contained (Java 1.1)</span>
+ <span class="ik">dimensions</span><span class="iv">460×400, black ground, Helvetica throughout</span>
+ <span class="ik">collection</span><span class="iv">SFMOMA 99.554</span>
+ <span class="ik">zoom plateaus</span><span class="iv">72px per day / hour / minute / second — scn 1 → 72 → 1,728 → 103,680 → 6,220,800 px/day</span>
+ <span class="ik">easing</span><span class="iv">counter at 20 increments/sec over segments [150, 250, 350, 450]</span>
+ <span class="ik">labels</span><span class="iv">font = min(72, unit spacing); text under 5px degrades to a line; gray ramps 128→255 as a unit matures; the current unit is always red</span>
+ <span class="ik">period quirks</span><span class="iv">Mac JVM patch capped zoom at hour level; an unused HAPPY NEW YEAR string ships in the bytecode</span>
+ <span class="ik">tests</span><span class="iv">33 (Vitest + fast-check)</span>
+ </div>
+ </div>
+</div>
+
+<script>
+(() => {
+ // src/lib/calendar.js
+ function epochDayFromCivil(y, m, d) {
+ const yy = y - (m <= 2 ? 1 : 0);
+ const era = Math.floor(yy / 400);
+ const yoe = yy - era * 400;
+ const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
+ return era * 146097 + doe - 719468;
+ }
+ function civilFromEpochDay(ed) {
+ const z = ed + 719468;
+ const era = Math.floor(z / 146097);
+ const doe = z - era * 146097;
+ const yoe = Math.floor(
+ (doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365
+ );
+ const y = yoe + era * 400;
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));
+ const mp = Math.floor((5 * doy + 2) / 153);
+ const d = doy - Math.floor((153 * mp + 2) / 5) + 1;
+ const m = mp + (mp < 10 ? 3 : -9);
+ return { y: y + (m <= 2 ? 1 : 0), m, d };
+ }
+
+ // src/pieces/line/model.js
+ var DAY = 86400;
+ var FAITHFUL_1997 = {
+ stops: [1, 72, 1728, 103680, 6220800],
+ segments: [150, 250, 350, 450],
+ incsPerSec: 20
+ };
+ function totalIncrements(cfg) {
+ return cfg.segments.reduce((a, b) => a + b, 0);
+ }
+ function scaleForCounter(cfg, counter) {
+ const total = totalIncrements(cfg);
+ const c = Math.max(0, Math.min(total, counter));
+ let acc = 0;
+ for (let i = 0; i < cfg.segments.length; i++) {
+ const seg = cfg.segments[i];
+ if (c <= acc + seg) {
+ const f = (c - acc) / seg;
+ return cfg.stops[i] + f * (cfg.stops[i + 1] - cfg.stops[i]);
+ }
+ acc += seg;
+ }
+ return cfg.stops[cfg.stops.length - 1];
+ }
+ function advanceCounter(cfg, counter, dtMs, held) {
+ const d = cfg.incsPerSec * dtMs / 1e3;
+ const next = held ? counter + d : counter - d;
+ return Math.max(0, Math.min(totalIncrements(cfg), next));
+ }
+ function pxOfTime(view, t) {
+ return (t - view.focus) * view.scale / DAY + view.anchorPx;
+ }
+ function timeOfPx(view, px) {
+ return (px - view.anchorPx) * DAY / view.scale + view.focus;
+ }
+ var MAX_FONT = 72;
+ var MIN_TEXT_PX = 5;
+ function fontSizeFor(spacingPx) {
+ return Math.min(MAX_FONT, spacingPx);
+ }
+ function textVisible(fontSize) {
+ return fontSize >= MIN_TEXT_PX;
+ }
+ function grayFor(fontSize) {
+ return Math.floor(Math.min(MAX_FONT, fontSize) * 127 / MAX_FONT) + 128;
+ }
+ var FIXED_UNITS = { second: 1, minute: 60, hour: 3600, day: DAY };
+ function* calendarTicks(unit, t0, t1) {
+ let { y, m } = civilFromEpochDay(Math.floor(t0 / DAY));
+ const stride = unit === "decade" ? 10 : unit === "century" ? 100 : 1;
+ if (unit === "month") {
+ for (; ; ) {
+ const t = epochDayFromCivil(y, m, 1) * DAY;
+ if (t >= t0) break;
+ m++;
+ if (m > 12) m = 1, y++;
+ }
+ for (; ; ) {
+ const t = epochDayFromCivil(y, m, 1) * DAY;
+ if (t > t1) return;
+ yield t;
+ m++;
+ if (m > 12) m = 1, y++;
+ }
+ } else {
+ let yy = Math.ceil(y / stride) * stride;
+ if (epochDayFromCivil(yy, 1, 1) * DAY < t0) yy += stride;
+ while (epochDayFromCivil(yy - stride, 1, 1) * DAY >= t0) yy -= stride;
+ for (; ; ) {
+ const t = epochDayFromCivil(yy, 1, 1) * DAY;
+ if (t > t1) return;
+ if (t >= t0) yield t;
+ yy += stride;
+ }
+ }
+ }
+ function ticksInRange(unit, t0, t1) {
+ if (unit in FIXED_UNITS) {
+ const w = FIXED_UNITS[unit];
+ const out = [];
+ for (let t = Math.ceil(t0 / w) * w; t <= t1; t += w) out.push(t);
+ return out;
+ }
+ return [...calendarTicks(unit, t0, t1)];
+ }
+ var MONTHS = [
+ "JANUARY",
+ "FEBRUARY",
+ "MARCH",
+ "APRIL",
+ "MAY",
+ "JUNE",
+ "JULY",
+ "AUGUST",
+ "SEPTEMBER",
+ "OCTOBER",
+ "NOVEMBER",
+ "DECEMBER"
+ ];
+ function formatMonth(m) {
+ return MONTHS[m - 1];
+ }
+ function formatDay(m, d) {
+ return `${m}/${d}`;
+ }
+ function formatHour(h) {
+ const twelve = h % 12 === 0 ? 12 : h % 12;
+ return { text: ` ${twelve}`, suffix: h >= 12 ? "pm" : "am" };
+ }
+ function formatMinute(mm) {
+ return `:${String(mm).padStart(2, "0")}`;
+ }
+
+ // src/pieces/line/step.js
+ function initState(cfg, range, lineLenPx) {
+ return {
+ counter: 0,
+ scale: scaleForCounter(cfg, 0),
+ focus: 0,
+ // epoch seconds under the anchor pixel
+ anchorPx: 0,
+ // pixel along the line where focus projects
+ range,
+ // {tMin, tMax} or null for the unbounded twist
+ lineLenPx
+ };
+ }
+ function step(cfg, s, dtMs, input) {
+ const cursorPx = Math.max(0, Math.min(s.lineLenPx, input.cursorPx));
+ let { focus, anchorPx } = s;
+ if (input.held || s.counter > 0) {
+ focus = timeOfPx({ focus: s.focus, scale: s.scale, anchorPx: s.anchorPx }, cursorPx);
+ anchorPx = cursorPx;
+ }
+ const counter = advanceCounter(cfg, s.counter, dtMs, input.held);
+ const scale = scaleForCounter(cfg, counter);
+ if (!input.held && counter < cfg.segments[0]) {
+ const target = cursorPx * DAY;
+ focus += (target - focus) / (counter + 1);
+ anchorPx = cursorPx;
+ }
+ if (s.range) {
+ focus = Math.max(s.range.tMin, Math.min(s.range.tMax, focus));
+ }
+ return { ...s, counter, scale, focus, anchorPx };
+ }
+
+ // src/pieces/line/render.js
+ var LEVELS = [
+ { unit: "month", secs: 30 * DAY },
+ { unit: "day", secs: DAY },
+ { unit: "hour", secs: 3600 },
+ { unit: "minute", secs: 60 },
+ { unit: "second", secs: 1 }
+ ];
+ var FACE = { w: 460, h: 400 };
+ function lineOrigin(state) {
+ const margin = (FACE.h - state.lineLenPx) / 2;
+ return { mx: FACE.w - FACE.h + margin, my: margin };
+ }
+ function labelFor(unit, t) {
+ const dayIdx = Math.floor(t / DAY);
+ const civ = civilFromEpochDay(dayIdx);
+ const rem = t - dayIdx * DAY;
+ const hh = Math.floor(rem / 3600);
+ const mm = Math.floor(rem % 3600 / 60);
+ const ss = Math.floor(rem % 60);
+ switch (unit) {
+ case "month":
+ return formatMonth(civ.m);
+ case "day":
+ return formatDay(civ.m, civ.d);
+ case "hour": {
+ const h = formatHour(hh);
+ return h.text + h.suffix;
+ }
+ case "minute":
+ return formatMinute(mm);
+ case "second":
+ return formatMinute(ss);
+ default:
+ return "";
+ }
+ }
+ function sameTick(unit, t, now) {
+ const w = { second: 1, minute: 60, hour: 3600, day: DAY }[unit];
+ if (w) return Math.floor(t / w) === Math.floor(now / w);
+ const a = civilFromEpochDay(Math.floor(t / DAY));
+ const b = civilFromEpochDay(Math.floor(now / DAY));
+ return a.y === b.y && a.m === b.m;
+ }
+ function draw(ctx, state, nowSec) {
+ const { mx, my } = lineOrigin(state);
+ ctx.fillStyle = "#000";
+ ctx.fillRect(0, 0, FACE.w, FACE.h);
+ const view = { focus: state.focus, scale: state.scale, anchorPx: state.anchorPx };
+ const tA = Math.max(
+ state.range ? state.range.tMin : -Infinity,
+ state.focus - state.anchorPx * DAY / state.scale
+ );
+ const tB = Math.min(
+ state.range ? state.range.tMax : Infinity,
+ state.focus + (state.lineLenPx - state.anchorPx) * DAY / state.scale
+ );
+ ctx.strokeStyle = "#666";
+ ctx.beginPath();
+ ctx.moveTo(mx, FACE.h - my);
+ ctx.lineTo(mx + state.lineLenPx, FACE.h - my - state.lineLenPx);
+ ctx.stroke();
+ const colWidth = {};
+ for (let i = LEVELS.length - 1; i >= 0; i--) {
+ const { unit, secs } = LEVELS[i];
+ const spacing = secs * state.scale / DAY;
+ if (spacing < 2 && unit !== "month") {
+ colWidth[unit] = 0;
+ continue;
+ }
+ const size = Math.floor(fontSizeFor(spacing));
+ colWidth[unit] = textVisible(size) ? size * 2.2 : 0;
+ }
+ for (let i = LEVELS.length - 1; i >= 0; i--) {
+ const { unit, secs } = LEVELS[i];
+ const spacing = secs * state.scale / DAY;
+ if (spacing < 2 && unit !== "month") continue;
+ const size = unit === "month" ? Math.floor(Math.min(72, 11 + state.scale)) : Math.floor(fontSizeFor(spacing));
+ const gray = unit === "month" ? 255 : grayFor(size);
+ let offset = 0;
+ for (let j = LEVELS.length - 1; j > i; j--) offset += colWidth[LEVELS[j].unit];
+ ctx.font = `${Math.max(size, 1)}px Helvetica, Arial, sans-serif`;
+ for (const t of ticksInRange(unit, tA, tB)) {
+ const n = pxOfTime(view, t);
+ if (n < 0 || n > state.lineLenPx) continue;
+ const x = mx + n;
+ const y = FACE.h - my - n;
+ const isNow = sameTick(unit, t, nowSec);
+ ctx.strokeStyle = ctx.fillStyle = isNow ? "#f00" : `rgb(${gray},${gray},${gray})`;
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ ctx.lineTo(x + 3, y);
+ ctx.stroke();
+ const text = labelFor(unit, t);
+ if (textVisible(size)) {
+ const w = ctx.measureText(text).width;
+ ctx.fillText(text, x - w - offset, y);
+ } else {
+ ctx.beginPath();
+ ctx.moveTo(x - 4 - offset, y);
+ ctx.lineTo(x - offset, y);
+ ctx.stroke();
+ }
+ }
+ }
+ ctx.fillStyle = "#ccc";
+ ctx.font = "15px Helvetica, Arial, sans-serif";
+ const label = `${Math.round(state.scale)}X${state.counter >= 1200 ? " (MAX)" : ""}`;
+ ctx.fillText(label, 3, 16);
+ }
+
+ // src/lib/loop.js
+ var STEP_MS = 30;
+ function startLoop({ stepFn, renderFn, stepMs = STEP_MS }) {
+ let acc = 0;
+ let last = null;
+ let raf = null;
+ let running = true;
+ function frame(ts) {
+ if (!running) return;
+ if (last === null) last = ts;
+ acc += Math.min(ts - last, 250);
+ last = ts;
+ while (acc >= stepMs) {
+ stepFn(stepMs);
+ acc -= stepMs;
+ }
+ renderFn();
+ raf = requestAnimationFrame(frame);
+ }
+ raf = requestAnimationFrame(frame);
+ return () => {
+ running = false;
+ if (raf) cancelAnimationFrame(raf);
+ };
+ }
+
+ // src/pieces/line/index.js
+ var MaedaLine = class extends HTMLElement {
+ connectedCallback() {
+ const canvas = document.createElement("canvas");
+ canvas.width = FACE.w;
+ canvas.height = FACE.h;
+ canvas.style.cssText = "display:block;width:100%;max-width:460px;cursor:crosshair;touch-action:none";
+ this.appendChild(canvas);
+ const ctx = canvas.getContext("2d");
+ const frozen = this.getAttribute("frozen-now");
+ const nowSec = () => frozen ? Number(frozen) : Date.now() / 1e3;
+ const { y } = (() => {
+ const d = new Date(nowSec() * 1e3);
+ return { y: d.getUTCFullYear() };
+ })();
+ const t0 = epochDayFromCivil(y, 1, 1) * DAY;
+ const t1 = epochDayFromCivil(y + 1, 1, 1) * DAY;
+ const lineLen = Math.round((t1 - t0) / DAY);
+ const cfg = FAITHFUL_1997;
+ let state = initState(cfg, { tMin: 0, tMax: t1 - t0 }, lineLen);
+ state.focus = nowSec() - t0;
+ state.anchorPx = state.focus / DAY;
+ const input = { cursorPx: state.anchorPx, held: false };
+ const toLinePx = (ev) => {
+ const r = canvas.getBoundingClientRect();
+ const sx = FACE.w / r.width;
+ const x = (ev.clientX - r.left) * sx;
+ const yy = (ev.clientY - r.top) * (FACE.h / r.height);
+ const margin = (FACE.h - lineLen) / 2;
+ const mx = FACE.w - FACE.h + margin;
+ return (x - mx + (FACE.h - margin - yy)) / 2;
+ };
+ canvas.addEventListener("pointermove", (ev) => {
+ input.cursorPx = toLinePx(ev);
+ });
+ canvas.addEventListener("pointerdown", (ev) => {
+ canvas.setPointerCapture(ev.pointerId);
+ input.cursorPx = toLinePx(ev);
+ input.held = true;
+ });
+ const release = () => {
+ input.held = false;
+ };
+ canvas.addEventListener("pointerup", release);
+ canvas.addEventListener("pointercancel", release);
+ this._stop = startLoop({
+ stepFn: (dtMs) => {
+ state = step(cfg, state, dtMs, input);
+ },
+ renderFn: () => {
+ draw(ctx, state, nowSec() - t0);
+ this.dispatchEvent(new CustomEvent("readout", {
+ detail: { scale: state.scale, counter: state.counter }
+ }));
+ }
+ });
+ }
+ disconnectedCallback() {
+ if (this._stop) this._stop();
+ }
+ };
+ customElements.define("maeda-line", MaedaLine);
+})();
+
+</script>
+<script>
+document.getElementById("piece").addEventListener("readout", (e) => {
+ const { scale, counter } = e.detail;
+ document.getElementById("readout").textContent =
+ "scale " + Number(scale).toFixed(3) + " · counter " + Math.round(counter);
+});
+</script>
+</body>
+</html>
diff --git a/working/velox-reinstall/wttrin-bf0457f.bundle b/working/velox-reinstall/wttrin-bf0457f.bundle
deleted file mode 100644
index 5921866..0000000
--- a/working/velox-reinstall/wttrin-bf0457f.bundle
+++ /dev/null
Binary files differ