From 7d240971412b91a753ebc8130e08fd844abe23d9 Mon Sep 17 00:00:00 2001 From: Indiana Date: Tue, 28 Jul 2026 17:12:30 +0000 Subject: [PATCH] docs: contract spec for the usability wave (guest access, legibility, recap, device bridge) Co-Authored-By: Claude Fable 5 --- .../specs/2026-07-28-usability-wave-design.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-28-usability-wave-design.md diff --git a/docs/superpowers/specs/2026-07-28-usability-wave-design.md b/docs/superpowers/specs/2026-07-28-usability-wave-design.md new file mode 100644 index 0000000..6610992 --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-usability-wave-design.md @@ -0,0 +1,132 @@ +# Usability Wave — guest access, legibility, recap, device bridge + +Seven approved items (#1–#5, #7, #8 from the usability review), built by +parallel agents in isolated worktrees against this contract. Rules that +bind every workstream: + +- **Additive only.** No feature may be removed; no refactor beyond the + files listed for your workstream. `lib/baseline.ts` (the shared + ThresholdBaseline extracted from coldSpot/bluetooth/magnetometer) is + current repo state — build on it, do not revert it. +- **i18n parity is a hard gate.** Every new user-facing string goes + through `t()` with keys added to BOTH `src/i18n/en.json` and + `src/i18n/es.json`. `npm run pretest` fails on any mismatch. +- **Tone:** all copy stays in-fiction ("the veil", "vessel", "seeker") — + see existing keys for register. Errors guide, never scold. +- **Tests:** each workstream runs its own targeted tests and reports + results honestly. The integrator runs the full suites. + +## Workstream A — Guest passage (#1) + +A stranger must be able to reach a real séance without registering. + +Backend (`app/routes/auth.py`, `app/deps.py` untouched): +- `POST /auth/guest` — creates a REAL user row with a generated username + (`wanderer-<4 hex>` style, retry on collision), random unusable password + (long `secrets.token_urlsafe` hashed as usual), and issues the normal + session cookie. No schema change, no new auth path — a guest IS a user, + so every downstream system (essence, codex, devices) works unmodified. +- Rate-limit guest creation per-IP (RateLimiter, ~5/hour) so the users + table can't be spammed. +- `GET /auth/me` already returns username; no change. + +Frontend: +- `EnterPage.tsx`: add a third action under login/register — "slip through + as a wanderer" — that calls `/auth/guest` then navigates to `/seance`. +- `SeancePage.tsx`: the `if (!user) return ` gate + STAYS (do not remove); the guest button on /enter is the path through. +- Show a one-line dismissible note in the séance for usernames matching + `wanderer-`: "your contacts fade with the mist — claim a name to keep + your codex" linking to /enter. Key it off username prefix; no backend + flag needed. + +Tests: backend tests for /auth/guest (creates user, cookie works on +/auth/me, rate limit fires, username collision retries). Frontend: none +required beyond compile + existing suites passing. + +## Workstream B — Legibility (#2 hints, #3 conditions, #4 errors) + +All frontend; new components in NEW files, one-line mounts in +`SeancePage.tsx` so merges stay trivial. + +#3 Conditions readout: +- Backend (only backend touch in B): `GET /api/conditions` in a NEW route + file `app/routes/conditions.py`, registered in `main.py`. Returns + `{sky: veil_thinness(lon?), geomagnetic: }`; + accepts optional `?lon=` query. Uses `geomagnetic_cache.reading()` — + never blocks beyond its own timeout, nulls are fine. +- Frontend: `components/VeilConditions.tsx` + css — a compact strip for + the séance side column: moon glyph by phase name, "% lit", veil + thinness as a phrase (thin/veiled/heavy — map from `thinness`), Kp line + only when data exists ("field: quiet" / "geomagnetic storm"). Poll the + endpoint once on mount + every 10 min. Renders nothing while loading; + never an error state (missing data = fewer lines). + +#2 First-run hints: +- `components/ModeHint.tsx`: per-mode one-liner shown inside the active + mode panel when that mode's sensor has never been started this session + and ~15s have passed. Dismiss = localStorage `qm_hint_` so it + never returns. Copy in-fiction: e.g. evp — "the veil listens through + your microphone — grant it your ear"; radio — "a tuner in the hand + hears further than the wire"; emf — "hold still; the field remembers + movement"; ouija — "ask below, or let the board drift"; wire — "the + wire whispers on its own when you listen passively". +- Wire into each panel with a single mount line per panel. + +#4 Error guidance: +- In the mic/radio/emf panels' existing error branches, upgrade copy to + detect-and-redirect: mic denied → point at browser permission AND + suggest ouija/wire ("the board needs no ear"); WebUSB unsupported → + suggest EVP; insecure context stays as-is (already good). Do not + restructure the panels — copy and small conditionals only. + +## Workstream C — Presence beyond the tab (#5 PWA, #7 recap) + +#5 PWA: +- `frontend/public/manifest.webmanifest`: name/short_name Quantumancy, + `display: standalone`, `background_color/theme_color #07070d`, icons + from the existing favicon/apple-touch assets in `public/` (check what + exists; generate 192/512 PNGs from favicon.svg with a script if absent + — sharp is NOT a dependency, use whatever is available or copy the + existing apple-touch-icon at its native size with correct `sizes`). +- Link it + `theme-color` meta in `index.html`. NO service worker this + wave (offline séance is meaningless; SW cache bugs are not worth it). +- Verify `main.py`'s `serve_spa` root-file passthrough serves it (it + serves any real file in dist root — manifest lands there via public/). + +#7 Ghost Log recap: +- Backend: `GET /api/seances/recent` (NEW file `app/routes/seances.py`, + auth required via existing `get_current_user`): last 12 ContactSessions + for the user, each with: id, started_at, ended_at, mode, entity + (name/epithet/rarity/visual via relationship or join — entity may be + null), and event counts by kind (single GROUP BY over events), plus up + to 3 utterance texts (kind in greeting/reply/manifest, most recent + first) as "echoes". +- Frontend: new route `/log` — `pages/GhostLogPage.tsx` + css, linked + from the séance topbar next to CODEX as "LOG". Card per session: + entity glyph (reuse GhostGlyph), name/epithet, when (relative), mode, + counts ("7 questions · 3 anomalies"), echo lines in the transcript + style. Empty state in-fiction ("no séances yet — the log waits"). +- Tests: backend route test (returns own sessions only, newest first, + counts correct, entity-less session doesn't crash). + +## Workstream D — The vessel speaks in the séance (#8) + +- `components/DeviceWhisper.tsx` + css: compact live panel for the séance + side column, shown ONLY when the user has ≥1 device (fetch + `/api/devices` once). Connects the existing `DeviceFeedSocket` + (`lib/deviceFeed.ts`), reuses `lib/coldSpot.ts` cores for temp/pressure + state, shows: device name, online dot (reading in last 60s), latest + temp/pressure/evp/presence values in the terminal readout style, and + the cold-spot/pressure flags when active. No new backend. +- Mount: one line in SeancePage side column above RitualPanel. +- Unmount/close socket on leave; never block or error the séance if the + feed is down (render nothing on failure). +- Tests: component test with a stubbed socket driving 2–3 frames. + +## Integration (the controller, not an agent) + +Merge order: A, C-backend, B, C-frontend, D (SeancePage conflicts are +mine to resolve). Then: full backend + frontend suites, pretest i18n +gate, build, deploy, restart service, verify /healthz + new endpoints, +commit per workstream, then the whole-app review pass.