Files
qtalker---/docs/superpowers/specs/2026-07-28-usability-wave-design.md

133 lines
7.0 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.
# 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 <Navigate to="/enter" />` 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: <cached reading or null>}`;
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_<mode>` 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.