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

@@ -0,0 +1,324 @@
// GraphView — real-time scrolling time-series of the wire flood on canvas 2D.
// Three polylines (jitter green, latency-variance violet, dns dim gray) scroll
// right-to-left over a rolling window of the last ~180 telemetry frames, each
// with a glowing last-point dot and a mono current-value label. Anomalies from
// the session fire as vertical ticks on the shared time axis, height ∝
// magnitude, colored by source; hovering a marker draws its frequency and
// magnitude as a tooltip. A top row of flickering mono counters tracks the
// session's anomaly total, the contacted entity and the current jitter.
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import type { AnomalyEntry } from '../state/seance'
import type { Telemetry } from '../lib/types'
import './GraphView.css'
/** Rolling history length (telemetry frames). */
export const HISTORY_CAP = 180
export type TelemetrySample = {
at: number
jitter: number
variance: number
dns: number
}
// ---- pure helpers (unit-tested without canvas) ----
/** Append a sample to the rolling history, dropping the oldest when full. */
export function pushHistory(
history: TelemetrySample[],
sample: TelemetrySample,
cap = HISTORY_CAP,
): TelemetrySample[] {
const out = history.length >= cap ? history.slice(history.length - cap + 1) : [...history]
out.push(sample)
return out
}
/** Adaptive y-scale: the series max with 15% headroom, never below `floor`. */
export function adaptiveMax(values: number[], floor: number): number {
let max = floor
for (const v of values) if (v > max) max = v
return max * 1.15
}
/** Marker color per anomaly source: wire green, evp violet, radio orange. */
export function anomalyColor(source: AnomalyEntry['source']): string {
switch (source) {
case 'wire':
return '#7cffb2'
case 'evp':
return '#b26bff'
case 'radio':
return '#ff9d4a'
}
}
// ---- component ----
export type GraphViewProps = {
telemetry: Telemetry | null
anomalies: AnomalyEntry[]
anomalyCount: number
entityName: string | null
/** When false, the rAF loop is paused (view hidden). */
active: boolean
}
const MARGIN = { top: 14, right: 96, bottom: 22, left: 10 }
type Series = {
key: 'jitter' | 'variance' | 'dns'
color: string
floor: number
label: (s: TelemetrySample) => string
}
const SERIES: Series[] = [
{ key: 'jitter', color: '#7cffb2', floor: 10, label: (s) => `JIT ${s.jitter.toFixed(1)}` },
{ key: 'variance', color: '#b26bff', floor: 2, label: (s) => `VAR ${s.variance.toFixed(2)}` },
{ key: 'dns', color: 'rgba(211, 233, 219, 0.4)', floor: 5, label: (s) => `DNS ${s.dns.toFixed(1)}` },
]
export function GraphView({ telemetry, anomalies, anomalyCount, entityName, active }: GraphViewProps) {
const { t } = useTranslation()
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const historyRef = useRef<TelemetrySample[]>([])
const anomaliesRef = useRef(anomalies)
anomaliesRef.current = anomalies
const hoverRef = useRef<{ x: number; y: number; anomaly: AnomalyEntry } | null>(null)
// Rolling history: one timestamped sample per telemetry frame.
useEffect(() => {
if (!telemetry) return
historyRef.current = pushHistory(historyRef.current, {
at: Date.now(),
jitter: telemetry.jitter_bytes_per_s,
variance: telemetry.latency_variance_ms,
dns: telemetry.dns_ms,
})
}, [telemetry])
// Marker hit-testing: remember the anomaly nearest the cursor.
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const pick = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect()
const mx = e.clientX - rect.left
const my = e.clientY - rect.top
const history = historyRef.current
hoverRef.current = null
if (history.length < 2) return
const w = rect.width
const h = rect.height
const plotW = w - MARGIN.left - MARGIN.right
const plotH = h - MARGIN.top - MARGIN.bottom
const t0 = history[0].at
const t1 = history[history.length - 1].at
const span = Math.max(1, t1 - t0)
let best: { x: number; y: number; anomaly: AnomalyEntry } | null = null
for (const a of anomaliesRef.current) {
if (a.at < t0 || a.at > t1) continue
const x = MARGIN.left + ((a.at - t0) / span) * plotW
if (Math.abs(x - mx) > 6) continue
const tickH = Math.min(plotH, 8 + Math.abs(a.magnitude) * 3)
const y = MARGIN.top + plotH - tickH
if (my < y - 6) continue
if (!best || Math.abs(x - mx) < Math.abs(best.x - mx)) best = { x, y, anomaly: a }
}
hoverRef.current = best
}
const clear = () => {
hoverRef.current = null
}
canvas.addEventListener('mousemove', pick)
canvas.addEventListener('mouseleave', clear)
return () => {
canvas.removeEventListener('mousemove', pick)
canvas.removeEventListener('mouseleave', clear)
}
}, [])
useEffect(() => {
if (!active) return
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
let raf = 0
let w = 0
let h = 0
const resize = () => {
const parent = canvas.parentElement
if (!parent) return
const dpr = Math.min(window.devicePixelRatio, 2)
w = parent.clientWidth
h = parent.clientHeight
if (w === 0 || h === 0) return
canvas.width = w * dpr
canvas.height = h * dpr
canvas.style.width = `${w}px`
canvas.style.height = `${h}px`
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
}
resize()
const ro = new ResizeObserver(resize)
if (canvas.parentElement) ro.observe(canvas.parentElement)
const draw = () => {
raf = requestAnimationFrame(draw)
if (w === 0 || h === 0) return
const history = historyRef.current
const plotW = w - MARGIN.left - MARGIN.right
const plotH = h - MARGIN.top - MARGIN.bottom
ctx.fillStyle = '#07070d'
ctx.fillRect(0, 0, w, h)
// faint horizontal grid
ctx.strokeStyle = 'rgba(124, 255, 178, 0.07)'
ctx.lineWidth = 1
for (let i = 1; i <= 3; i++) {
const y = MARGIN.top + (plotH * i) / 4
ctx.beginPath()
ctx.moveTo(MARGIN.left, y)
ctx.lineTo(MARGIN.left + plotW, y)
ctx.stroke()
}
ctx.font = '10px "IBM Plex Mono", monospace'
ctx.textBaseline = 'middle'
if (history.length >= 2) {
const t0 = history[0].at
const t1 = history[history.length - 1].at
const span = Math.max(1, t1 - t0)
const xAt = (at: number) => MARGIN.left + ((at - t0) / span) * plotW
// --- anomaly markers: vertical ticks, height ∝ magnitude ---
for (const a of anomaliesRef.current) {
if (a.at < t0 || a.at > t1) continue
const x = xAt(a.at)
const tickH = Math.min(plotH, 8 + Math.abs(a.magnitude) * 3)
const color = anomalyColor(a.source)
ctx.strokeStyle = color
ctx.globalAlpha = 0.85
ctx.beginPath()
ctx.moveTo(x, MARGIN.top + plotH)
ctx.lineTo(x, MARGIN.top + plotH - tickH)
ctx.stroke()
ctx.globalAlpha = 0.25
ctx.beginPath()
ctx.moveTo(x, MARGIN.top + plotH - tickH)
ctx.lineTo(x, MARGIN.top)
ctx.stroke()
ctx.globalAlpha = 1
}
// --- series polylines + glowing last point + value label ---
ctx.textAlign = 'left'
SERIES.forEach((s, si) => {
const max = adaptiveMax(
history.map((p) => p[s.key]),
s.floor,
)
const yFor = (v: number) => MARGIN.top + plotH - (Math.min(v, max) / max) * plotH
ctx.strokeStyle = s.color
ctx.lineWidth = 1.4
ctx.beginPath()
history.forEach((p, i) => {
const x = xAt(p.at)
const y = yFor(p[s.key])
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
})
ctx.stroke()
const lastP = history[history.length - 1]
const lx = xAt(lastP.at)
const ly = yFor(lastP[s.key])
ctx.fillStyle = s.color
ctx.shadowColor = s.color
ctx.shadowBlur = 10
ctx.beginPath()
ctx.arc(lx, ly, 3, 0, Math.PI * 2)
ctx.fill()
ctx.shadowBlur = 0
ctx.fillText(s.label(lastP), MARGIN.left + plotW + 10, MARGIN.top + 12 + si * 16)
})
// --- hover tooltip for the marker under the cursor ---
const hover = hoverRef.current
if (hover) {
const lines = [
`${hover.anomaly.frequency.toFixed(1)} MHZ`,
`${hover.anomaly.magnitude >= 0 ? '+' : '-'}${Math.abs(hover.anomaly.magnitude).toFixed(1)} DB`,
]
ctx.font = '11px "IBM Plex Mono", monospace'
const tw = Math.max(...lines.map((l) => ctx.measureText(l).width)) + 16
const th = 34
const tx = Math.min(Math.max(hover.x - tw / 2, MARGIN.left), MARGIN.left + plotW - tw)
const ty = Math.max(MARGIN.top + 4, hover.y - th - 10)
ctx.fillStyle = 'rgba(16, 16, 26, 0.94)'
ctx.strokeStyle = anomalyColor(hover.anomaly.source)
ctx.lineWidth = 1
ctx.beginPath()
ctx.rect(tx, ty, tw, th)
ctx.fill()
ctx.stroke()
ctx.fillStyle = '#d3e9db'
ctx.textAlign = 'center'
ctx.fillText(lines[0], tx + tw / 2, ty + 11)
ctx.fillText(lines[1], tx + tw / 2, ty + 24)
}
} else {
// Empty state: the grid alone would be a blank box — say we're listening.
ctx.textAlign = 'center'
ctx.fillStyle = 'rgba(178, 107, 255, 0.75)'
ctx.font = '12px "IBM Plex Mono", monospace'
ctx.fillText('· · ·', MARGIN.left + plotW / 2, MARGIN.top + plotH / 2)
}
}
raf = requestAnimationFrame(draw)
return () => {
cancelAnimationFrame(raf)
ro.disconnect()
}
}, [active])
return (
<div className="graph-view">
<div className="graph-counters" aria-live="polite">
<span className="graph-counter flicker-soft">
<span className="counter-key">{t('seance.views.graph.anomalies', { defaultValue: 'ANOMALIES' })}</span>
<span className="counter-val">{anomalyCount}</span>
</span>
<span className="graph-counter flicker-soft">
<span className="counter-key">{t('seance.views.graph.entity', { defaultValue: 'ENTITY' })}</span>
<span className="counter-val counter-entity">{entityName ?? '—'}</span>
</span>
<span className="graph-counter flicker-soft">
<span className="counter-key">{t('seance.views.graph.jitter', { defaultValue: 'JITTER' })}</span>
<span className="counter-val">
{telemetry ? `${telemetry.jitter_bytes_per_s.toFixed(1)} KB/S` : '—'}
</span>
</span>
</div>
<div className="graph-canvas-wrap">
<canvas ref={canvasRef} />
{!telemetry && (
<p className="graph-listening flicker" role="status">
{t('seance.views.listening', { defaultValue: 'listening…' })}
</p>
)}
</div>
</div>
)
}