Commit Graph

21 Commits

Author SHA1 Message Date
Indiana
eaa299f683 fix: stale-frame race, stuck verdict buttons, and 3 more frontend defects
seance.tsx / JudgmentPanel.tsx: neither `ritual_complete` nor
`judgment_result` carries an entity id, and both were applied
unconditionally — so a response still in flight when the seeker summoned a
fresh entity landed on whatever entity happened to be current when it
arrived, leaking the *previous* entity's hidden traits into the new one's
revealed-traits UI. Both frames are now gated on our still waiting for one
(ritual.status === 'in_progress' / judgmentPending), which the 'entity'
case clears the moment a new presence arrives, so a late answer for the old
entity is dropped instead of misattributed.

JudgmentPanel also had `pending` in component-local state that only cleared
when judgmentResult became a *new* truthy object. If the entity changed
while judgmentResult was already null, the reset was a no-op (null === null)
and pending stayed stuck, permanently disabling all four verdict buttons.
It now reads the shared judgmentPending flag, which the reducer resets.

coldSpot.ts: severity was ungated by `warm` while isColdSpot/
isPressureAnomaly were correctly gated. ColdSpotPanel feeds severity
straight into the composite disturbance gauge with no boolean gate of its
own, so a freshly-paired device could show "disturbance rising" off its 2nd
reading — exactly what the minSamples warm-up exists to prevent.

PlanchetteBoard.tsx: the first GOODBYE deadline was a bare
randomBetween(120,300) compared against `t`, which is seconds since
performance.timeOrigin (page load), not since mount. Every later reschedule
correctly offsets from `t`. On a tab open >5min before the board mounted
(or any remount via navigation), t was already past the deadline and the
planchette snapped to GOODBYE on the first frame.

sdr.ts: close() and setFrequency() inside the sweep loop were not wrapped in
withTimeout despite the file's own header claiming every stalling USB call
is. A dongle going unresponsive mid-sweep or during teardown hung forever —
the same silent-hang symptom withTimeout was added to eliminate.

InventoryPanel.tsx: essence was decremented client-side using a possibly
stale fallback price and never reconciled. The server already returns the
real post-purchase balance in PurchaseOut; use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:47:39 +00:00
Indiana
825f6aa510 fix: RTL-SDR bulk reads could hang forever with zero user feedback
Real bug report: user selects their RTL-SDR dongle in the WebUSB picker,
"nothing happens" — no error, no sweep, no visible change at all.

Root cause: WebUSB's transferIn/controlTransfer calls have no built-in
timeout. readSamples()'s bulk transferIn (called every sweep step, twice —
once to discard PLL-settle samples, once for real) had nothing bounding
it, so if the dongle doesn't actually stream data for any reason (this
init sequence has never been verified against real hardware), that
promise just never settles — indistinguishable from the page being frozen,
forever, with no way for the UI to ever surface an error.

Added withTimeout(), applied to: the bulk IQ read (4s — the one most
likely to actually hang during sweeping) and the whole open()/init
sequence as one unit (15s, since it's ~20 sequential unverified register
pokes). Also stopped silently falling back to default
interface/endpoint values when the device's USB descriptor doesn't expose
a bulk-IN endpoint as expected — now logs a warning so a real endpoint
mismatch is at least visible in DevTools instead of only surfacing as a
downstream hang.

4 new unit tests for withTimeout(). 306/306 frontend tests pass.
2026-07-25 18:38:59 +00:00
Indiana
53239a923d feat: planchette wander reaches YES/NO, periodically rests on GOODBYE
Wander bounds were tied to the letter ring's radius, which sits inside
YES/NO's corner positions — widened to derive bounds directly from the
board's actual outermost fixed waypoints (YES/NO for the top/sides, the
number row for the bottom) so idle drift covers the whole interactive
board, corners included.

Added a periodic deliberate visit to GOODBYE: every 2-5 minutes
(randomized so multiple open tabs don't sync up), the planchette glides
down and rests there for 3-5 seconds with the same restrained glow used
for ambiently-brushed letters, then resumes normal wander. Purely a
visual beat — no session/game state changes, distinct from an actual
goodbye action.
2026-07-25 17:47:59 +00:00
Indiana
a4103818d3 fix: planchette idle wander was confined to a tiny central zone
The wander amplitude (w*0.16, h*0.14) was a fixed fraction of the canvas
with no relationship to the outer letter ring's actual radius
(min(w*0.4, h*0.52) — set in computeLayout's arc() calls), so idle drift
could never reach anywhere near the outer letters (A/M/N/Z, the arc tops).
Tied the wander radius directly to that same ring radius/squash factor
instead, so it genuinely roams the whole board.

Also added an ambient "letter brushing" effect: while idly wandering (not
actively spelling a real reply), a letter the planchette drifts near gets
a faint glow — visibly dimmer than the bright hover/dwell glow used for
real spelled letters, so a passing brush never reads as the spirit
actually saying something. Purely cosmetic, no game-state changes.
2026-07-25 17:35:42 +00:00
Indiana
917cd7f89e feat: Cold Spot Detector / Atmospheric Disturbance Index on Device Bay
Real-time anomaly visualization for temperature/pressure readings on the
device-feed dashboard, folklore's two most iconic paranormal markers:
sudden cold spots and rapid barometric swings.

- lib/coldSpot.ts: pure, directly-testable rolling-baseline tracker
  (time-aware EMA, since hardware doesn't report on a fixed schedule),
  cold-spot and pressure-anomaly classifiers, and a composite
  Atmospheric Disturbance Index that rewards correlated anomalies
  (a lone signal caps at 50/100; only both deviating together can
  reach 100) — modeled on evilMeter.ts's threaded-state pattern.
- components/ColdSpotPanel.tsx/.css: frost treatment + sparkline for
  temperature, ripple treatment for pressure, and a crescent-arc
  composite gauge (GhostLog's evil-meter gauge as the visual family
  reference) that only appears once a device has reported both sensors.
- DevicesPage.tsx: owns per-device baseline state, feeds it from
  `reading` frames, falls back to the existing generic row for any
  non-numeric temperature/pressure value.

26 new coldSpot.test.ts cases (warm-up, genuine vs. fluctuation,
correlated-vs-solo index, gappy/out-of-order data) and 7 new
DevicesPage integration tests. Full suite: 302/302 passing, tsc clean,
i18n coverage clean (en/es).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 22:00:35 +00:00
Indiana
58b1ac397c Merge Workstream H: device pairing UI + live dashboard
Resolved conflicts in App.tsx/SeancePage.tsx (both F and H added new
routes/nav links, kept both) and i18n files (both added sibling top-level
keys — inventory + devices, fixed nesting after conflict markers removed).

Also fixed a real integration gap: DevicesPage.test.tsx's mock User object
predated Workstream F's unlocks/essence additions to the User type (the
two workstreams built in parallel isolation and couldn't see each other's
changes). 269/269 frontend tests pass, tsc clean.
2026-07-24 20:47:57 +00:00
Indiana
30c0da1eaf Merge Workstream F: unlocks/inventory UI + sigil designer + listening tool 2026-07-24 09:39:19 +00:00
Indiana
98864976b1 Merge Workstream E: ritual mini-game + judgment UI + consequences
Resolved conflict in types.ts: dropped the duplicate EntityTraits
definition (D and E both added it identically) and combined both
workstreams' new ServerFrame variants (tell/ritual_complete from D,
judgment_result from E). 223/223 frontend tests pass, tsc clean.
2026-07-24 09:39:08 +00:00
Indiana
e886c94d40 feat(frontend): device pairing UI + live telemetry dashboard (Workstream H)
Adds /devices — pair an ESP32 sensor node (POST /api/device), reveal its
raw pairing token exactly once with a hard-to-miss "cannot be shown again"
warning (styled like a real API-key-reveal UI), then a live dashboard
subscribing to /ws/device-feed: the initial `devices` frame seeds paired
devices, and `reading` frames update one row per distinct sensor_type in
place. sensor_type/value/unit are rendered fully generically per the
contract (free-form, open-ended) — an unrecognized sensor_type renders
safely with no special-casing.

- frontend/src/lib/deviceFeed.ts: reconnecting WS client for
  /ws/device-feed, mirroring VeilSocket's backoff shape (receive-only, no
  outbox needed).
- frontend/src/pages/DevicesPage.{tsx,css}: pairing form + one-time token
  reveal + live device-card grid. Leans into "hacker" terminal styling
  (monospace readouts, terminal device cards) over the app's usual gothic
  chrome, per the design spec, while keeping the existing dark/violet
  palette tokens from App.css.
- Route + nav link wired into App.tsx / SeancePage.tsx.
- i18n: new `devices.*` / `nav.devices` keys in en.json + es.json; added a
  coverage-check.mjs domain rule for the dynamic connection-state key,
  mirroring the existing `seance.connection.` rule.

Tests: deviceFeed.test.ts (backoff/reconnect/frame delivery) and
DevicesPage.test.tsx (empty state, name validation, one-time token reveal
and dismissal, live frame updates in place without duplicating rows,
multi-device/multi-sensor rendering, and a mocked unrecognized sensor_type
that must not crash). Full suite: 154 passed (137 pre-existing + 17 new).
`npx tsc -b` and `npm run build` both clean.

Assumption (undocumented in spec): POST /api/device's JSON response shape
is inferred as `{id, name, token, last_seen_at}` since the Contract section
only describes the endpoint in prose. GET /api/device is intentionally not
called — the live dashboard is fully seeded by /ws/device-feed's initial
`devices` frame per the contract, so it's redundant for this page's scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 01:10:24 +00:00
Indiana
e4a834620f feat: Ghost Log HUD + evil meter + tells (Workstream D)
App-shell HUD (mounted in App.tsx alongside HauntingLayer, visible on
every screen) that shows ambient idle status until a séance is active,
then streams `tell` WS frames as terminal-style log lines with a
"hacker witch" crescent-arc evil-meter gauge (occult sigils/runes fused
with Transcript.tsx's monospace log vocabulary).

- lib/evilMeter.ts: pure function computing a malevolent<->benevolent
  belief from the accumulated tell history — starts wide/uncertain,
  narrows geometrically and shifts per tell (deterministic hash of the
  tell text, since tells never leak ground truth), and snaps to
  definitive certainty on a successful ritual_complete's
  revealed.alignment. Fully unit tested (narrowing, ordering,
  determinism, ritual override, idle/empty history).
- lib/ghostLogBus.ts: tiny typed event bus (mirrors lib/haunting.ts's
  HauntBus) so the app-shell-level GhostLog can react to live séance
  frames — SeanceProvider is only mounted inside the séance route, so a
  shell-level sibling can't read its context directly.
- state/seance.tsx: publish entity/tell/ritual_complete onto the bus,
  and session_end on provider teardown so the HUD falls back to idle
  when the seeker leaves the séance page.
- lib/types.ts: add the `tell` and `ritual_complete` server frames from
  the Character Depth spec's Contract section (only what this
  workstream consumes).
- components/GhostLog.tsx/.css: the HUD itself, plus i18n keys in
  en.json/es.json.

168/168 frontend tests pass (137 pre-existing + 20 evilMeter + 11
GhostLog); tsc -b and the i18n coverage pretest are clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 16:28:33 +00:00
Indiana
ebe6a4b0b8 feat(frontend): unlocks/inventory UI, sigil designer, listening-tool EVP threshold
Workstream F of the character-depth/ghost-log spec.

- InventoryPanel.tsx: essence balance, owned unlocks/items (terminal
  listing + Codex-style rarity-glow borders on items), and a buy flow
  for a small fixed unlock catalog (currently just "listening_tool",
  the only key the contract names) with afford/can't-afford button
  states. Prices are fetched from a best-guess /api/inventory/catalog
  endpoint and fall back to a flagged "(est.)" price sourced from the
  spec's own worked example when that endpoint isn't available yet —
  the contract doesn't define a price-list REST shape.

- SigilDesigner.tsx + lib/sigil.ts: constrained geometric builder —
  points snap to 24 fixed clock-face slots around a circle, capped at
  12 to match the backend's payload limit, connected in placement
  order. Five hand-drawn stroke-only rune glyphs (eye/crescent/key/
  spiral/thorn) overlay the center. Point-cap enforcement, rune
  selection and payload-shape building are pure functions in
  lib/sigil.ts, unit-tested directly. Saves via POST
  /api/inventory/sigils (path inferred; payload shape matches the
  contract exactly: {"points": [[x,y],...], "rune": str}). Art
  direction: the builder itself reads like a plotting/debug tool
  (crosshair cursor, monospace coordinate HUD) while the rendered
  lines + rune glow violet, consistent with GhostGlyph's conventions.

- lib/evp.ts: new evpThresholdDb(hasListeningTool) pure function and
  EvpListener.start() now accepts { hasListeningTool } to lower the
  EVP anomaly threshold (8dB -> 4dB) when the unlock is owned — wired
  from useAuth().user.unlocks in SeancePage's EvpPanel, a real
  gameplay effect on which faint signals register as anomalies.

- api.ts User type gains unlocks/essence per the contract's /auth/me
  extension; new InventoryPage.tsx mounts both components behind auth
  at /inventory, linked from the séance nav.

166/166 tests pass (137 pre-existing + 29 new), i18n coverage check
clean, tsc -b clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 16:26:18 +00:00
Indiana
a52a781880 feat(frontend): ritual mini-game, judgment UI, and consequences (Workstream E)
Implements the Character Depth / Ghost Log spec's Workstream E:

- RitualPanel.tsx: a 4-step "focus the channel" hold-to-charge sequence
  (align/breathe/trace/lock), sending ritual_step frames as the seeker
  progresses. Success reveals the entity's true hidden traits; failure
  reveals nothing. Sequencing/timing logic lives in the pure, unit-tested
  lib/ritual.ts rather than inline in the component.
- JudgmentPanel.tsx: Trust/Banish/Cross Over/Test verdict buttons with
  rune-style SVG icons, sending the judgment frame and rendering a
  distinct treatment per judgment_result consequence — reward,
  escalation (also spikes the ambient haunting), withdrawal, resisted,
  neutral, and a calm glyph-fade farewell for crossed_over (deliberately
  not the reward treatment, since it's a goodbye).
- lib/haunting.ts: IdleEscalator gains forceEscalate()/forcedUntil so a
  judgment's "escalation" consequence can spike the ambient haunting
  immediately instead of waiting on the 90s idle clock; exports a
  sharedIdleEscalator singleton and a forceEscalate() free function.
  HauntingLayer now paces itself off that shared instance instead of a
  private one, so the forced spike actually reaches the running layer.
- state/seance.tsx: new ritual/judgmentResult state, a local_ritual_start
  action, and reducer cases for the ritual_complete/judgment_result server
  frames; SeanceContext exported for component testing; startRitual/
  sendRitualStep/sendJudgment added to the provider API.
- lib/types.ts: EntityTraits/JudgmentVerdict/JudgmentConsequence types and
  the new client/server WS frames, per the spec's Contract section.
- i18n: seance.ritual.* / seance.judgment.* keys in en.json and es.json.

Fast-forwarded this worktree's branch onto master first — it had been
created from a stale ancestor commit predating the frontend scaffold
entirely, with zero commits of its own ahead of that point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:29:29 +00:00
Indiana
6d8c6f2496 rename: The Armory -> The Reliquary; extend spec with essence + cross-over
Renaming: "Armory" read too militaristic for a séance app. Landed on "The
Reliquary" (not "The Threshold" — that name was already taken by the
landing-page back-link).

Spec addendum: added a visible essence currency (earned per summon, spent
on unlocks — distinct from the hidden favor score) and a fourth judgment
verdict, cross_over, for compassionately helping a genuinely benevolent
"stuck" spirit move on rather than just trusting or banishing it. Updates
workstreams B/C/E/F accordingly before any of them are dispatched.
2026-07-23 11:06:07 +00:00
Indiana
58c30b7273 feat: possession presentation layer — glitchy text + degraded audio
First sub-project of the "make contact feel real" arc (spec:
docs/superpowers/specs/2026-07-23-possession-presentation-design.md).
Direct Contact replies now feel like a spirit fighting through static to
hold the channel rather than a plain chat bubble:

- backend/app/possession.py: compute_stability(rarity, magnitude, rng) —
  a 0.05-0.98 score per reply (rarer entity + stronger triggering anomaly =
  cleaner signal), rng injectable for a later quantum-RNG source.
- ws.py sends stability on reply_start; audio synthesis for that reply gets
  noise/bitcrush scaled by instability (1 - stability) via a new
  instability param on synthesize_spirit_voice — effects.py itself is
  untouched, only the params fed into it.
- frontend/src/lib/possession.ts: renderPossessedText — pure, deterministic
  (tick-seeded, no Math.random) text corruption with self-correcting
  glitch bursts, wired into Transcript.tsx's streaming reply display.

Stored transcript/reply text is unaffected — this is presentation only.
78/78 backend, 137/137 frontend tests passing.
2026-07-23 05:48:02 +00:00
Indiana
b56f5b8108 Merge gap-e-sdr-tests: SpectrumAnomalyDetector unit test coverage 2026-07-23 00:33:28 +00:00
Indiana
2b83c7cb7d test: add coverage for SpectrumAnomalyDetector
Pure rolling-floor/spike-detection logic in sdr.ts had zero tests,
unlike its EVP/EMF detector siblings. No production code changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-21 03:45:52 +00:00
Indiana
f0ee7dbdab refactor: use TelemetryReadout and EntityCard components
Both were built and tested but never wired in — SeancePage.tsx was
reimplementing the same markup inline. Removes the duplication.
2026-07-21 03:45:11 +00:00
Indiana
0756e677b9 Add EMF field mode, Armory shop/waitlist, and ambient haunting layer
Three self-contained features, verified complete and cross-wired
end-to-end (audited: backend 54/54 tests, frontend 110/110 tests,
tsc --noEmit clean, i18n coverage script clean):

- EMF mode: DeviceMotion/DeviceOrientation-based field-meter sensing,
  a fifth séance channel alongside Wire/EVP/Radio/Ouija, with its own
  fragment prompt persona and full frontend gauge UI.
- Armory (shop/waitlist): pre-order capture page for the future
  Ultimate Quantum Box hardware line, rate-limited public endpoint,
  explicitly no payment collection.
- Haunting layer: ambient possession effects (dread-bed audio, title
  glitching, idle-paced whispers/manifests), respects
  prefers-reduced-motion, mounted once at the app root.

Plus WebUSB robustness fixes in lib/sdr.ts (Terratec vendor ID,
explicit selectConfiguration, isSecureContext gate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
2026-07-21 02:28:14 +00:00
Indiana
c372427ced feat: matrix/graph data views, wire-generated anomalies, http cookie fix
- auth: session cookie Secure only over https — plain-http LAN access was
  silently dropping the cookie, killing WS auth ('connection unstable')
- wire ghost: server-side spike detection on jitter baseline (3σ + 2.5×mean,
  20KB/s floor, 20s throttle) — wire anomalies now flood every session with
  zero hardware, pushed to clients as {type:'anomaly'} frames
- telemetry cadence 3-5s for live graphs; ambient whispers unchanged
- frontend: MATRIX view (data-rain interleaved with live utterances/anomaly/
  telemetry strings), GRAPHS view (scrolling jitter/variance/dns lines +
  anomaly markers + counters), BOARD/MATRIX/GRAPHS switcher
- connection banner: 'connecting' is now neutral 'tuning the veil…', only
  unstable/closed warns
- db: recreated quantumancy(+_test) as UTF8 (was SQL_ASCII — crashed on
  non-ASCII spirit text); README quickstart updated
- i18n: seance.views.* EN/ES
2026-07-21 00:02:41 +00:00
Indiana
6edbbbbc2a feat: complete Quantumancy web app — full frontend + docs
Frontend (React 18 + TS + Vite):
- Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits
- Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive),
  Ouija planchette board spelling utterances, transcript with TTS replay,
  entity dossier, direct contact streaming, passive/active listening
- Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB
  RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact
- Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete
- State: VeilSocket (reconnect/backoff), seance reducer, auth context
- 72 vitest tests green; served by FastAPI at :7777

Docs: README + as-built plans 3-7
2026-07-20 21:11:49 +00:00
Indiana
6bcdf83c4d feat: scaffold React frontend with login/register UI 2026-07-20 17:42:44 +00:00