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

7.0 KiB
Raw Permalink Blame History

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.