docs: add continuation handoff — toolbar deploy confirmed live, visual gated to ≤900px

Closes the open gate from 2026-04-24-195210 handoff. Captures:
- two-host deploy topology trap (steel141 vs caddy CT) and corrected workflow
- viewport-gated visual: toolbar is display:none above 900px, so desktop
  reloads were byte-for-byte identical regardless of toolbar.js version
- diagnostic chain that ruled out cache, service worker, and CDN before
  finding the viewport gate
- remaining work: real-device mobile acceptance test only

Validator score: 90/100 (READY).
This commit is contained in:
Mortdecai
2026-05-03 22:17:43 -04:00
parent 06c1ebbc7f
commit 345bf5ea15
@@ -0,0 +1,178 @@
# Handoff: sethmux toolbar deploy confirmed live — visual gated to ≤900px viewport
## Session Metadata
- Created: 2026-05-03 22:13:56
- Project: /home/claude/bin/sethmux
- Branch: main
- Session duration: continuation of 2026-04-24 deploy session, ~30 min of follow-up debugging
### Recent Commits (for context)
- 06c1ebb fix(docs): correct deploy topology — static assets live on caddy CT, not steel141
- 8a9d89b docs: add session handoff for toolbar refresh deploy
- b8a7810 chore: archive design handoff bundle for toolbar refresh
- 2b375b0 chore: add detect-secrets baseline
- 3b06ce7 fix: keep tmux selection alive after mouse-drag in copy mode
- 8e70875 feat: Workspace dark refresh + mobile compose bar
## Handoff Chain
- **Continues from**: [2026-04-24-195210-toolbar-workspace-dark-refresh-deploy.md](2026-04-24-195210-toolbar-workspace-dark-refresh-deploy.md)
- **Supersedes**: None (prior handoff is still accurate; this one closes its open gates)
## Current State Summary
The 2026-04-24 toolbar refresh + compose bar work is **fully shipped and verified live** on `mux.sethpc.xyz`. The previous handoff left one open gate: "mobile-browser acceptance test." That gate is partially closed — the deployed file matches the design hash, the file is reachable through Caddy, and direct fetch of `https://mux.sethpc.xyz/toolbar.js` returns the new content. What's *not* yet confirmed is the actual mobile-device feel of the compose bar interacting with Gboard/iOS autocorrect; that requires a real phone session by Seth and is the only remaining work.
A long detour mid-session uncovered two real gotchas that are now committed to docs: (1) the deploy is **two-host split** (Caddy CT serves static assets from its own filesystem; steel141 serves the index page and runs ttyd) — initial deploy went only to steel141, leaving the live site unchanged for ~30 minutes. (2) The toolbar is **gated behind `@media (max-width:900px)`**, so on a desktop viewport the page is byte-for-byte identical regardless of toolbar.js version. This is what caused the "looks the same after clearing cookies and site data" symptom: Seth was reloading on a Linux desktop browser ≥ 900px wide where the toolbar is `display:none` either way.
## Codebase Understanding
## Architecture Overview
Two-host serving topology, both happen to use the path `/opt/sethmux/`:
```
Browser
Caddy on caddy CT (192.168.0.185)
├── /toolbar.js, /manifest.json, /icon-*.png → file_server from /opt/sethmux/ on caddy CT
├── /api/* → reverse_proxy 192.168.0.141:7684 (notify-server.py on steel141)
└── / → reverse_proxy 192.168.0.141:7683 (ttyd on steel141)
└── ttyd serves --index /opt/sethmux/index.html (steel141 disk)
```
Auth (`import google_auth` in the Caddyfile vhost) wraps everything including `/toolbar.js`, but content is served directly off Caddy's disk — not proxied to an upstream. This is critical: the same path `/opt/sethmux/` exists on both hosts; only the caddy-CT copy of `toolbar.js` is what users actually see.
DNS: `mux.sethpc.xyz` is a CNAME to `sethpc.xyz`, which is an A record to `71.178.159.217` (Seth's home WAN). Traffic flows directly to the caddy CT's exposed port. **No CDN, no Cloudflare** — header `server: Caddy` confirms.
## Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| `static/toolbar.js` | Source of truth for the mobile toolbar — Workspace dark + compose bar | Hash `6aee4388...`. Must `scp` to caddy CT to ship. |
| `caddy:/opt/sethmux/toolbar.js` | What Caddy actually serves | Must match the source hash above. As of 2026-04-25 00:16 UTC, it does. |
| `/opt/sethmux/toolbar.js` (steel141) | Unused — but kept in sync for parity | Updating this alone does **nothing** to live. Easy trap. |
| `/opt/sethmux/index.html` (steel141) | ttyd's `--index` — the page shell that loads `/toolbar.js` via `<script src>` | Lives on steel141 because ttyd is here. NOT on caddy CT. |
| `/etc/caddy/Caddyfile` (caddy CT) | The vhost block for `mux.sethpc.xyz` | `import google_auth` + path-handle blocks for static assets + catch-all reverse proxy. |
| `DECISIONS.md` | Project decision log | Has the two-host topology now (commit `06c1ebb`). Read before any deploy. |
| `claude-design/design_handoff_sethmux_toolbar/README.md` | Spec + acceptance checklist | Lines 134146 are the mobile acceptance criteria. |
| `.secrets.baseline` | detect-secrets allowlist | Required so SRI integrity hashes don't trip the global pre-commit hook. Regenerate with `--all-files` if adding more SRI tags. |
## Key Patterns Discovered
- **`import google_auth` in Caddyfile applies before path-handle blocks**, so OAuth gates `/toolbar.js` even though it's a static asset. Unauthenticated curl gets 302 → `auth.sethpc.xyz/oauth2/start` → Google. To verify the file directly, log in via browser and visit `/toolbar.js` as a URL.
- **Caddy default access logging is errors-only.** Successful 200s for `/toolbar.js` don't appear in `journalctl -u caddy`. To debug serving issues, watch for 5xx errors only — absence of a 200 in logs is normal, not diagnostic.
- **Mobile gating is a single CSS rule:** `@media (max-width: 900px) { #mb { display: flex } }`. Outside that breakpoint, `#mb` is `display:none`. Implication: any visual change to the toolbar is invisible at desktop widths. Test with DevTools device emulation (Ctrl+Shift+M, set width 375px) or on a real phone.
- **The PWA is "installable" but not offline-capable.** No service worker registration anywhere (no `navigator.serviceWorker` in `index.html`, no `sw.js`, manifest is bare-bones). So a "clear cookies and site data" really does drop all client-side cache for this origin — no PWA cache to chase.
## Work Completed
### Tasks Finished (this continuation)
- [x] Diagnosed "still looks the same" — initially suspected browser cache (wrong), then service worker (none), then CDN (none), finally found the deploy went to wrong host
- [x] Found `caddy:/opt/sethmux/toolbar.js` was still the Mar 26 file (`b578baeb...`, 4938 bytes)
- [x] `scp static/toolbar.js caddy:/opt/sethmux/` — now hash `6aee4388...` (matches design)
- [x] Backed up old caddy-CT file to `caddy:/opt/sethmux/.backup/toolbar.js.<ts>`
- [x] Updated `DECISIONS.md` with the correct two-host deploy topology (commit `06c1ebb`)
- [x] Amended prior handoff with the wrong-path correction (also in `06c1ebb`)
- [x] Stopped `sethmux.service` on steel141 to confirm Seth was hitting live infra (got 502 — confirmed), restarted immediately
- [x] Confirmed via direct URL fetch (Seth's browser) that `/toolbar.js` returns the new file
- [x] Final root cause of "no visible change": **viewport > 900px** — toolbar is mobile-only, identical on desktop regardless of version
## Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| `caddy:/opt/sethmux/toolbar.js` | scp from `static/toolbar.js` | Actual deploy that ships to users |
| `DECISIONS.md` | Added two-host deploy topology + commands | Don't repeat the wrong-host trap |
| `.claude/handoffs/2026-04-24-195210-...md` | Amended "Important Context" with the wrong-path correction | Diagnostic record so the trap is documented even if DECISIONS.md is missed |
## Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Document the wrong-path in DECISIONS.md + handoff amendment + dedicated commit | Silent-fix the deploy and move on | Per global "no wrong-path coverup" rule. Future-self needs to know the trap exists. |
| Leave steel141:/opt/sethmux/toolbar.js as the new version (parity) | Revert to old to make the asymmetry obvious | Parity is less surprising. The file is harmless either way; only caddy CT's copy is served. |
| Don't add cache-busting query string to `<script src>` yet | Add `?v=<git-hash>` and bump on each deploy | The cache theory turned out wrong — viewport gating was the issue. No-cache header / version-string is unnecessary churn until a real cache problem appears. |
| Don't change the 900px breakpoint | Lower to e.g. 600px so desktop windows ~700-900px wide also see the toolbar | Out of scope. The design explicitly targets ≤900px and the toolbar is mobile-first. |
## Pending Work
## Immediate Next Steps
1. **Mobile acceptance test on a real device** — open `mux.sethpc.xyz` on your phone, walk the checklist in `claude-design/design_handoff_sethmux_toolbar/README.md` lines 134146. Particular attention to: `Type` button toggles compose row, Enter sends + clears, autocorrect/swipe in compose input actually works on Gboard/iOS, Save fills green for 1500ms, Sel toggles to "Done" and dims terminal. If everything passes, this work stream is genuinely done.
2. **If autocorrect doesn't feel native in the compose input**, escalate to the deferred "nuclear option" in DECISIONS.md (`inputmode="none"` on helper textarea + on-screen keyboard toggle). Don't escalate without a concrete failure mode — the compose bar is meant to make autocorrect work; if it fails, that's diagnostic info the design needs.
3. **Decide on `rdp_guac.txt`** — still untracked, 200KB Claude Code transcript dump. Either move under a `docs/` subdir or `rm`. Not blocking anything.
## Blockers/Open Questions
- [ ] None blocking. Real-device test is the only outstanding action.
### Deferred Items
- **Cache-Control `no-cache` header on `/toolbar.js`** — was going to add this when I thought the issue was browser caching. It wasn't, so no change made. Could be added defensively for future deploys, but the file is small and ETag-revalidated; only worth doing if a real cache-staleness incident occurs.
- **Auto-deploy script** — manual `scp` to caddy CT is the friction that caused this whole detour. Worth a `make deploy` or git post-commit hook that pushes to `caddy:/opt/sethmux/`. Not done yet.
## Important Context
- **`/opt/sethmux/` exists on TWO hosts. Only the caddy-CT copy of `toolbar.js`, `manifest.json`, `icon-*.png` is served to users.** Steel141's copy of `toolbar.js` is purely cosmetic (parity with the source repo) — modifying it alone has zero user-visible effect. This is the single biggest trap in this codebase. The Mar 26 → Apr 24 "drift" of three undeployed toolbar fixes was the same trap firing for ~30 days unnoticed.
Correct deploy:
```
scp static/toolbar.js static/manifest.json static/icon-*.png caddy:/opt/sethmux/
sudo cp static/index.html /opt/sethmux/ # steel141, only when index changes
sudo cp notify-server.py /opt/sethmux/ # steel141, only when notify changes
```
- **The toolbar is mobile-only.** Any "I deployed but nothing changed" report needs viewport check first: ask viewport width, or have the user open DevTools device emulation. On Linux/Mac/Win desktop browsers above 900px wide, both old and new toolbars are `display:none` — the page is byte-for-byte identical at the visual layer.
- **`import google_auth` wraps `/toolbar.js`.** Direct curl from outside (or even from inside the caddy CT with `Host: mux.sethpc.xyz`) returns 302/empty without OAuth cookies. Verification path for "is the new file actually served" is **logged-in browser opening the URL directly**, not curl.
- **Caddy access logs are errors-only by default.** Don't expect to see a 200 for `/toolbar.js` in `journalctl -u caddy`. Absence of an error means it served fine.
## Assumptions Made
- The design's `toolbar.js` matches what the visual mockups demonstrate. (Verified by hash equality with `claude-design/design_handoff_sethmux_toolbar/toolbar.js`.)
- Seth's browser sessions to `mux.sethpc.xyz` carry valid OAuth cookies after his prior login. (Implicit — direct URL fetch worked.)
- The 900px breakpoint is intentional and not subject to change. (Per design spec.)
## Potential Gotchas
- **Don't restart `caddy.service` on the caddy CT** for static-asset changes. Caddy reads files on each request; no reload needed. Restarting Caddy interrupts every other sethpc.xyz subdomain (50+ services).
- **Don't restart `sethmux.service` on steel141** for static-asset changes either. Only restart when changing ttyd args (in the unit file) or `index.html`.
- **Don't run `git commit --no-verify`.** The pre-commit hook runs detect-secrets-hook against `.secrets.baseline`. SRI integrity hashes (`integrity="sha384-..."`) trip it as high-entropy strings. Regenerate baseline with `detect-secrets scan --all-files --exclude-files '\.git/|\.secrets\.baseline$' > .secrets.baseline` instead.
- **Watching mux.sethpc.xyz logs is harder than expected.** Caddy errors-only means a 200 for /toolbar.js never shows. To trace a request end-to-end, either add a `log` directive to the vhost (don't do this casually — log volume is high), or use browser DevTools Network tab.
## Environment State
### Tools/Services Used
- `gitea` CLI — `~/bin/gitea`, pushes to `https://git.sethpc.xyz/Seth/sethmux.git` with token from `~/.config/gitea/token`
- `ssh caddy` (192.168.0.185, CT 600 on node-241) — has direct write access to `/opt/sethmux/`; deploys go via `scp ... caddy:/opt/sethmux/`
- `ssh pve241` — Caddy CT's host node (only relevant if Caddy itself needs systemd-level work, which is rare)
- `detect-secrets` — at `/home/claude/.local/bin/detect-secrets` for baseline regeneration
### Active Processes (sethmux-relevant)
- `sethmux.service` on steel141 — ttyd on `:7683`, attaches `tmux -t sethmux`, user `rdp`. Running.
- `sethmux-notify.service` on steel141 — `notify-server.py` on `:7684`. Running.
- `caddy.service` on caddy CT — vhost for `mux.sethpc.xyz` + ~50 other subdomains. Running.
- `hal-terminal.service` on steel141 — separate fork, port `:7685`, own static dir. Unrelated; do not touch.
### Environment Variables
- `HOMELAB_PASSWORD` — used elsewhere in `~/bin`, not needed for sethmux deploys (key auth to `caddy` works)
- No new env vars added or required this session
## Related Resources
- Design spec + acceptance checklist: `claude-design/design_handoff_sethmux_toolbar/README.md`
- Project decision log: `DECISIONS.md`
- Helper-textarea autocomplete fix rationale: `AUTOCOMPLETE_FIX.md`
- Repo: https://git.sethpc.xyz/Seth/sethmux
- Live URL (auth-gated): https://mux.sethpc.xyz
- Caddyfile: `ssh caddy "cat /etc/caddy/Caddyfile"` — search the `mux.sethpc.xyz` vhost block
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.