Files
sethmux/.claude/handoffs/2026-04-24-195210-toolbar-workspace-dark-refresh-deploy.md
T
Mortdecai 06c1ebbc7f fix(docs): correct deploy topology — static assets live on caddy CT, not steel141
Wrong-path correction: previous DECISIONS.md and handoff said
toolbar.js deploys to /opt/sethmux/ 'on this host'. Caddy's
'root * /opt/sethmux' resolves against Caddy's filesystem, which is
on the caddy CT (192.168.0.185), not steel141. Deployed copy on
steel141 was harmless but unused; the served file came from caddy CT.

Symptom: 'mux.sethpc.xyz looks the same' after a successful steel141
'cp'. Resolution: scp static/toolbar.js caddy:/opt/sethmux/.

DECISIONS.md now documents the two-host split (Caddy serves static
assets from its own disk; ttyd on steel141 serves --index).
2026-04-25 00:17:00 -04:00

171 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff: sethmux mobile toolbar — Workspace dark refresh + compose bar deploy
## Session Metadata
- Created: 2026-04-24 19:52:10
- Project: /home/claude/bin/sethmux
- Branch: main
- Session duration: ~30 min
### Recent Commits (for context)
- 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
- 451a055 docs: document mobile autocomplete-disable fix
- 6a27828 chore: ignore .backup/ directories
## Handoff Chain
- **Continues from**: None (fresh start)
- **Supersedes**: None
## Current State Summary
Shipped the design handoff in `claude-design/SethMux_4-24-26.zip`: re-skinned the mobile toolbar to Google Workspace dark vocabulary (keeping `#D35400` accent) and added a third "compose bar" row that gives Gboard/iOS keyboards a real `<input>` to operate on, sidestepping xterm.js's per-keystroke input model. Replaced `static/toolbar.js` with the design's byte-for-byte version, deployed to `/opt/sethmux/toolbar.js`, and pushed 6 clean conventional commits to git.sethpc.xyz/Seth/sethmux. The deployed file is live; **browser-side acceptance testing on a phone is the only remaining check** — Seth should reload mux.sethpc.xyz on a mobile device, hard-refresh past browser cache, and walk the acceptance checklist in `claude-design/design_handoff_sethmux_toolbar/README.md`.
## Codebase Understanding
### Architecture Overview
Sethmux is `ttyd` + `tmux` glued together with a hand-rolled mobile toolbar. The serving stack:
```
mobile browser → Caddy (mux.sethpc.xyz, auth via Authentik) →
/toolbar.js, /manifest.json, /icon-*.png → file_server from /opt/sethmux/
/api/* → reverse_proxy localhost:7684 (notify-server.py)
/ → reverse_proxy localhost:7683 (ttyd)
```
ttyd serves only `--index /opt/sethmux/index.html` and the websocket — **toolbar.js is served by Caddy directly**, NOT ttyd. That's why a `cp` to `/opt/sethmux/toolbar.js` is enough to deploy (no daemon restart needed; static asset, ETag-revalidated).
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| `static/toolbar.js` | The mobile toolbar — buttons, compose bar, key sending, selection mode | The entire UI surface for mobile. **The only file that ships from the design bundle.** |
| `/opt/sethmux/toolbar.js` | Deployed copy (root-owned, served by Caddy) | Manual `cp` from `static/`. Currently matches the design hash `6aee4388...`. |
| `claude-design/design_handoff_sethmux_toolbar/README.md` | Spec — colors, geometry, behaviors, acceptance checklist | Source of truth if you need to verify what was supposed to ship. |
| `static/index.html` | ttyd custom index (xterm.js wrapper) | NOT changed this session. Keep in mind for future toolbar↔terminal coupling. |
| `config/tmux.conf` | tmux session config | One drag-select fix landed this session. |
| `systemd/sethmux.service` | ttyd unit on port 7683, runs as user `rdp` | Don't restart for static-asset changes; only for ttyd/tmux config changes. |
| `DECISIONS.md` | Project-local decision log (created this session) | **Read first** when resuming — has the rationale for the compose-bar design and what was rejected. |
| `AUTOCOMPLETE_FIX.md` | Pre-existing rationale for `disableMobileAutocomplete()` | Still load-bearing; the new toolbar.js preserves that fix verbatim. |
| `.secrets.baseline` | detect-secrets allowlist | Required by global pre-commit hook to avoid SRI hashes being flagged as secrets. |
### Key Patterns Discovered
- **Send-to-stdin chain in `toolbar.js#send()`** — tries `term._core.coreService.triggerDataEvent(k)` first, falls back to `term._core._onData.fire(k)`, then `term.input(k)`. This shape is the canonical way to inject keystrokes into a hosted xterm.js when you don't own its event loop. Preserved in the new design.
- **MutationObserver for late-binding xterm DOM** — xterm.js creates `.xterm-helper-textarea` after page load. The observer is the only reliable hook to set `autocomplete=off` on it. Same observer also runs `relayout()` so terminal pane height tracks toolbar height when the compose row toggles.
- **Sethmux sits behind Authentik** — unauthenticated curl gets 302'd. So *automated* HTTP checks against `mux.sethpc.xyz` need an auth token; for static-asset deploys, file-hash check on disk is the practical verification.
## Work Completed
### Tasks Finished
- [x] Read context/handoff/project files (`claude-design/SethMux_4-24-26.zip` + project state)
- [x] Verified design preserves all recent fixes (tabIndex=-1, _core data-event chain, helper-textarea hardening)
- [x] Replaced `static/toolbar.js` with design version (hash `6aee4388...`)
- [x] Deployed to `/opt/sethmux/toolbar.js` with backup of old file at `/opt/sethmux/.backup/toolbar.js.<ts>`
- [x] Backed up old `static/toolbar.js` to `static/.backup/toolbar.js.<ts>`
- [x] Created project-local `DECISIONS.md` with the design rationale + rejected alternatives
- [x] Added `.gitignore` entry for `.backup/`
- [x] Generated `.secrets.baseline` to allowlist SRI integrity hashes
- [x] 6 clean commits pushed to git.sethpc.xyz/Seth/sethmux
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| `static/toolbar.js` | Full replacement with design version | Workspace dark visual + compose bar |
| `/opt/sethmux/toolbar.js` | `cp` from `static/` | Deploy |
| `config/tmux.conf` | `MouseDragEnd1Pane`: `copy-selection-and-cancel``copy-selection` | Keep copy mode active after drag |
| `.gitignore` | Added `.backup/` | Don't track local backups |
| `DECISIONS.md` | Created | Project-local decision log per global convention |
| `AUTOCOMPLETE_FIX.md` | Committed (was untracked) | Documents the helper-textarea fix preserved in new toolbar |
| `.secrets.baseline` | Created | Allowlist SRI hashes for pre-commit hook |
| `claude-design/` | Added (8 files) | Archive design handoff bundle in-repo |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Compose bar instead of patching xterm.js IME handling | Fork xterm.js to handle `inputType: "insertReplacementText"` events | Compose bar is smaller, preserves upstream xterm.js, one-shot string send is more obviously correct than interleaved IME state machine |
| Manual deploy via `cp` | Auto-deploy on push, rsync watcher | Static asset; explicit `cp` keeps in-progress edits from accidentally shipping |
| Six separate commits, not one squash | Single bundled commit | Per Gitea convention: no batching unrelated concerns. Each commit is a record. |
| Baseline SRI hashes via `.secrets.baseline` | Inline `pragma: allowlist secret` per line, or `--no-verify` bypass | Baseline is precise (only allowlists *current* findings) and discoverable in repo. Per global "escalation brake" rule, never `--no-verify`. |
## Pending Work
## Immediate Next Steps
1. **Mobile browser acceptance test** — Seth needs to load `mux.sethpc.xyz` on his phone, hard-refresh (or new private tab) to bust the toolbar.js cache, and walk the checklist in `claude-design/design_handoff_sethmux_toolbar/README.md` (lines 134146). Specifically: confirm Workspace-dark colors render, `Type` button toggles compose row, Enter in compose flushes + clears, Save button fills green for 1500ms, Sel button toggles to "Done" with terminal dim, no console errors.
2. **Test mobile autocorrect end-to-end** — open compose bar on Gboard or iOS, type a sentence using swipe + autocorrect predictions, hit Send. The whole point of the work is that this should now feel native; if it doesn't, escalate to the "nuclear option" deferred in DECISIONS.md (`inputmode="none"` on helper textarea + on-screen keyboard toggle).
3. **Decide on `rdp_guac.txt`** — 200KB Claude Code session-transcript dump, untracked. Either move it under `docs/reference/` if useful, or `rm` it. Not committed deliberately.
### Blockers/Open Questions
- [ ] None blocking. Acceptance test is the only gate.
### Deferred Items
- **Forking xterm.js for `insertReplacementText` handling** — rejected; compose bar wins. See DECISIONS.md.
- **`inputmode="none"` on helper textarea** — deferred; only revisit if compose bar doesn't fix the autocorrect UX in real-world testing.
- **Auto-deploy from `static/` to `/opt/sethmux/`** — explicit `cp` stays for now; revisit if deploy drift becomes a recurring pain.
## Context for Resuming Agent
## Important Context
- **Deployments are MANUAL and split across TWO hosts.** Static assets Caddy serves directly (`toolbar.js`, `manifest.json`, `icon-*.png`) live in `/opt/sethmux/` on **caddy CT** (192.168.0.185). Index and notify-server live in `/opt/sethmux/` on **steel141**. Same path, different filesystems. **First deploy this session went to the wrong host** (steel141 only) — symptom was "looks the same" because Caddy was still serving the old file from its own /opt/sethmux/. Correct deploy: `scp static/toolbar.js caddy:/opt/sethmux/`. The Mar 26 → Mar 28 drift this session uncovered (3 toolbar fixes committed but never deployed) suggests this footgun has been hit before — same root cause.
- **toolbar.js is served by Caddy from caddy-CT's `/opt/sethmux/`, not by ttyd.** The systemd unit's `--index /opt/sethmux/index.html` (on steel141) only sets ttyd's index page; static assets are `file_server`'d by Caddy, on Caddy's own filesystem. No daemon restart on toolbar changes.
- **The pre-commit hook (`detect-secrets-hook`, configured at `~/.config/git/hooks/pre-commit`) flags SRI hashes as base64 high-entropy strings.** When adding new HTML with `integrity="sha384-..."` script tags, regenerate baseline with `detect-secrets scan --all-files --exclude-files '\.git/|\.secrets\.baseline$' > .secrets.baseline` before committing. NEVER use `--no-verify` — global rule.
- **The compose bar and the helper-textarea hardening are complementary, not redundant.** Compose bar = autocorrect-friendly typing surface. Helper-textarea hardening = prevents Gboard from corrupting per-keystroke chord/arrow taps. Both stay.
- **Authentik blocks unauthenticated curl** to mux.sethpc.xyz. To verify deploys via HTTP, you'd need an auth token; otherwise, file-hash on disk is the verification path.
### Assumptions Made
- The design's `toolbar.js` is high-fidelity per its README, so byte-for-byte replacement is correct (vs. cherry-picking changes).
- Browser cache will bust on hard-refresh; no cache-busting query string was added.
- `rdp_guac.txt` is unrelated session content (first 2 lines confirmed it's a Claude Code transcript header), so it stays out of the commits.
### Potential Gotchas
- **Don't restart `sethmux.service` for static-asset changes** — pointless, and it'll boot users out of their tmux session.
- **`/opt/sethmux/` is root-owned** — deploys need `sudo cp`. Ownership is intentional (ttyd runs as user `rdp`, not `claude`).
- **`hal-terminal.service` (port 7685) is a separate fork** with its own `static/` at `/home/claude/bin/hal/terminal/static/`. Don't confuse it with sethmux.
- **The `.backup/` dirs are local-only** (gitignored). Don't expect them to round-trip across hosts.
- **DECISIONS.md is project-local**; cross-cutting decisions go to `~/bin/DECISIONS.md`. Don't conflate.
## Environment State
### Tools/Services Used
- `gitea` CLI — pushes to `https://git.sethpc.xyz/Seth/sethmux.git` with token from `~/.config/gitea/token`
- `detect-secrets` — at `/home/claude/.local/bin/detect-secrets`, used for `.secrets.baseline` regeneration
- `sudo cp` — required for `/opt/sethmux/` writes
- `systemctl` — only used to verify `sethmux.service` and `hal-terminal.service` running; nothing was restarted
### Active Processes
- `sethmux.service` (ttyd on `:7683`, user `rdp`, attaches `tmux -t sethmux`) — running, **untouched** this session
- `sethmux-notify.service` (notify-server.py on `:7684`) — running, untouched
- `hal-terminal.service` (separate fork on `:7685`) — running, untouched
### Environment Variables
- `HOMELAB_PASSWORD` — used elsewhere in `~/bin`, not relevant to this session
- No new env vars added
## Related Resources
- Design spec: `claude-design/design_handoff_sethmux_toolbar/README.md` — has the acceptance checklist
- Design previews (visual reference, not production): `claude-design/design_handoff_sethmux_toolbar/preview-{desktop,mobile}.html`
- Decision rationale: `DECISIONS.md` (project-local)
- Helper-textarea fix rationale: `AUTOCOMPLETE_FIX.md`
- Repo: https://git.sethpc.xyz/Seth/sethmux
- Live URL (auth-gated): https://mux.sethpc.xyz
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.