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>
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.
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.
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>
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>
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>
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>
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.
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.
The gap-g-readme merge commit (57a8914) staged this fix but never
re-staged it after editing, so the merge landed with the pre-fix content —
the working tree had the correction but git didn't. No functional change,
just closing the gap between what was intended and what was committed.