docs: contract spec for the usability wave (guest access, legibility, recap, device bridge)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
132
docs/superpowers/specs/2026-07-28-usability-wave-design.md
Normal file
132
docs/superpowers/specs/2026-07-28-usability-wave-design.md
Normal file
@@ -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 <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.
|
||||||
Reference in New Issue
Block a user