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
This commit is contained in:
Indiana
2026-07-21 00:02:41 +00:00
parent 6edbbbbc2a
commit c372427ced
16 changed files with 1086 additions and 16 deletions

View File

@@ -19,6 +19,8 @@ import type { Toast } from '../state/seance'
import { GhostCanvas } from '../three/GhostCanvas'
import type { GhostMood, GhostVisualState } from '../three/GhostScene'
import { PlanchetteBoard } from '../components/PlanchetteBoard'
import { MatrixView } from '../components/MatrixView'
import { GraphView } from '../components/GraphView'
import { Transcript } from '../components/Transcript'
import { PlanchetteMachine } from '../lib/planchette'
import type { PlanchetteSnapshot } from '../lib/planchette'
@@ -35,6 +37,14 @@ import type { Language, Mode } from '../lib/types'
import './SeancePage.css'
const MODES: readonly Mode[] = ['wire', 'evp', 'radio', 'ouija']
// Board-area views; labels fall back to these until the i18n JSONs land.
const VIEWS = [
{ id: 'board', fallback: 'BOARD' },
{ id: 'matrix', fallback: 'MATRIX' },
{ id: 'graphs', fallback: 'GRAPHS' },
] as const
type BoardView = (typeof VIEWS)[number]['id']
const TOAST_TTL_MS = 6000
/** SDR sample rate must match RtlSdr.open()'s default. */
const SDR_SAMPLE_RATE_HZ = 2_048_000
@@ -84,6 +94,7 @@ function SeanceSession({ username }: { username: string }) {
const [departed, setDeparted] = useState(false)
const [question, setQuestion] = useState('')
const [dossierOpen, setDossierOpen] = useState(true)
const [view, setView] = useState<BoardView>('board')
// The planchette machine lives for the whole session; the board ticks it.
const machineRef = useRef(new PlanchetteMachine())
@@ -248,8 +259,17 @@ function SeanceSession({ username }: { username: string }) {
</div>
</header>
{/* ---- unstable-veil banner ---- */}
{state.connection !== 'open' && (
{/* ---- veil banner: 'connecting' is a neutral tuning state;
only an unstable or dead line raises the warning ---- */}
{state.connection === 'connecting' && (
<div className="seance-conn-banner conn-connecting" role="status">
<span className="conn-glyph" aria-hidden>
⟁
</span>
{t('seance.views.tuning', { defaultValue: 'tuning the veil…' })}
</div>
)}
{(state.connection === 'unstable' || state.connection === 'closed') && (
<div className={`seance-conn-banner conn-${state.connection}`} role="alert">
<span className="conn-glyph" aria-hidden>
⟁
@@ -261,7 +281,42 @@ function SeanceSession({ username }: { username: string }) {
{/* ---- main: board + side column ---- */}
<main className="seance-main">
<section className="seance-board-wrap">
<PlanchetteBoard machine={machineRef.current} hue={entityHue} />
{/* view switcher: BOARD / MATRIX / GRAPHS (styles in MatrixView.css,
buttons reuse the .mode-tab look) */}
<nav className="seance-view-tabs" aria-label="views">
{VIEWS.map((v) => (
<button
key={v.id}
type="button"
className={`mode-tab ${view === v.id ? 'active' : ''}`}
onClick={() => setView(v.id)}
>
{t(`seance.views.${v.id}`, { defaultValue: v.fallback })}
</button>
))}
</nav>
{/* The PlanchetteMachine lives in this page, so unmounting the board
component for another view loses nothing. */}
{view === 'board' && <PlanchetteBoard machine={machineRef.current} hue={entityHue} />}
{view === 'matrix' && (
<MatrixView
anomalies={state.anomalies}
utterances={state.utterances}
telemetry={state.telemetry}
entityName={state.entity?.name ?? null}
active={view === 'matrix'}
/>
)}
{view === 'graphs' && (
<GraphView
telemetry={state.telemetry}
anomalies={state.anomalies}
anomalyCount={state.anomalyCount}
entityName={state.entity?.name ?? null}
active={view === 'graphs'}
/>
)}
{state.status && (
<div className="seance-status-overlay" aria-live="polite">