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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user