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

@@ -1,6 +1,6 @@
from datetime import datetime, timezone
from fastapi import APIRouter, Cookie, Depends, HTTPException, Response, status
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -34,7 +34,12 @@ async def register(payload: RegisterRequest, db: AsyncSession = Depends(get_db))
@router.post("/login", response_model=UserOut)
async def login(payload: LoginRequest, response: Response, db: AsyncSession = Depends(get_db)):
async def login(
payload: LoginRequest,
request: Request,
response: Response,
db: AsyncSession = Depends(get_db),
):
user = await db.scalar(select(User).where(User.username == payload.username))
if user is None:
verify_password(payload.password, _DUMMY_PASSWORD_HASH)
@@ -51,12 +56,15 @@ async def login(payload: LoginRequest, response: Response, db: AsyncSession = De
db.add(session)
await db.commit()
# The app is reached two ways: https via the Cloudflare Tunnel (Secure
# required) and plain http on the LAN (a Secure cookie would be dropped
# by the browser entirely, silently breaking the séance socket).
response.set_cookie(
SESSION_COOKIE_NAME,
raw_token,
httponly=True,
samesite="lax",
secure=True,
secure=request.url.scheme == "https",
max_age=int(SESSION_TTL.total_seconds()),
)
return user
@@ -64,6 +72,7 @@ async def login(payload: LoginRequest, response: Response, db: AsyncSession = De
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout(
request: Request,
response: Response,
qm_session: str | None = Cookie(default=None, alias=SESSION_COOKIE_NAME),
db: AsyncSession = Depends(get_db),
@@ -74,7 +83,12 @@ async def logout(
if session is not None:
await db.delete(session)
await db.commit()
response.delete_cookie(SESSION_COOKIE_NAME, httponly=True, samesite="lax", secure=True)
response.delete_cookie(
SESSION_COOKIE_NAME,
httponly=True,
samesite="lax",
secure=request.url.scheme == "https",
)
@router.get("/me", response_model=UserOut)

View File

@@ -111,3 +111,24 @@ async def sample_network(period_s: float = 1.0) -> TelemetrySample:
if dns_times:
sample.dns_ms = sum(dns_times) / len(dns_times)
return sample
def detect_wire_spike(
history: list[float], current: float, *, min_samples: int = 6
) -> float | None:
"""Return the spike ratio when `current` is anomalous vs the rolling
baseline of jitter samples — the Wire Ghost noticing something move.
Three guards so idle-hour noise doesn't cry ghost: enough history to
have a baseline, an absolute floor (20 KB/s) so near-silent links stay
silent, and a statistical (3σ) + relative (2.5× mean) threshold.
"""
if len(history) < min_samples:
return None
mean = sum(history) / len(history)
if mean <= 0 or current <= 20_000:
return None
std = (sum((x - mean) ** 2 for x in history) / len(history)) ** 0.5
if current > mean + 3 * std and current > mean * 2.5:
return current / mean
return None

View File

@@ -17,6 +17,7 @@ the wire.
import asyncio
import contextlib
import random
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
@@ -36,7 +37,7 @@ from app.models.entity import Entity
from app.models.entity_sighting import EntitySighting
from app.models.event import Event
from app.rate_limit import RateLimiter
from app.telemetry import sample_network
from app.telemetry import detect_wire_spike, sample_network
from app.tts.piper import synthesize_spirit_voice
from app.tts.voices import pick_voice
@@ -71,6 +72,8 @@ class SeanceState:
anomalies: list[dict] = field(default_factory=list)
history: list[dict] = field(default_factory=list)
ambient_task: asyncio.Task | None = None
wire_jitter_history: list[float] = field(default_factory=list)
last_wire_anomaly_at: float = 0.0
def serialize_entity(entity: Entity) -> dict:
@@ -324,16 +327,36 @@ async def _handle_question(state: SeanceState, text: str) -> None:
async def _ambient_loop(state: SeanceState) -> None:
"""The Wire Ghost's pulse: telemetry every few seconds, a whisper only
when the LLM box has been quiet long enough."""
"""The Wire Ghost's pulse: telemetry every few seconds. Spikes in the
jitter baseline become first-class anomaly events (driving fragments and
the client's data views); quieter ticks may earn an ambient whisper."""
try:
while True:
await asyncio.sleep(random.uniform(6, 10))
await asyncio.sleep(random.uniform(3, 5))
try:
sample = await sample_network(period_s=1.0)
except Exception:
continue
await state.send_queue.put({"type": "telemetry", **sample.as_dict()})
jitter = sample.jitter_bytes_per_s
ratio = detect_wire_spike(state.wire_jitter_history, jitter)
state.wire_jitter_history = (state.wire_jitter_history + [jitter])[-60:]
now = time.monotonic()
if ratio is not None and now - state.last_wire_anomaly_at > 20:
state.last_wire_anomaly_at = now
anomaly = {
"source": "wire",
"frequency": round(jitter, 1),
"magnitude": round(ratio * 10, 1),
}
# Tell the client first so its matrix/graph views light up,
# then run the anomaly through the usual séance pipeline.
await state.send_queue.put({"type": "anomaly", **anomaly})
await _handle_anomaly(state, anomaly)
continue
if not spirit_service.ambient_ready():
continue
whisper = await spirit_service.wire_whisper(sample.as_dict(), state.language)

View File

@@ -0,0 +1,24 @@
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.mark.asyncio
async def test_login_cookie_secure_only_over_https(client):
await client.post("/auth/register", json={"username": "schemer", "password": "spookyspooky"})
https_login = await client.post(
"/auth/login", json={"username": "schemer", "password": "spookyspooky"}
)
assert "secure" in https_login.headers["set-cookie"].lower()
# Plain-http (LAN) access: a Secure cookie would be dropped by the
# browser and silently break the séance socket.
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
http_login = await http_client.post(
"/auth/login", json={"username": "schemer", "password": "spookyspooky"}
)
cookie = http_login.headers["set-cookie"].lower()
assert "qm_session=" in cookie
assert "secure" not in cookie

View File

@@ -1,4 +1,4 @@
from app.telemetry import parse_proc_net_dev
from app.telemetry import detect_wire_spike, parse_proc_net_dev
PROC_NET_DEV = """Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
@@ -17,3 +17,32 @@ def test_parse_proc_net_dev_extracts_counters():
def test_parse_proc_net_dev_ignores_malformed_lines():
assert parse_proc_net_dev("garbage\nno colon here\n") == {}
def test_spike_needs_history():
assert detect_wire_spike([], 1_000_000) is None
assert detect_wire_spike([10_000.0] * 3, 1_000_000) is None
def test_spike_fires_on_real_surge():
history = [20_000.0, 25_000.0, 22_000.0, 21_000.0, 23_000.0, 24_000.0, 22_500.0]
ratio = detect_wire_spike(history, 400_000.0)
assert ratio is not None
assert ratio > 10
def test_spike_ignores_normal_fluctuation():
history = [20_000.0, 25_000.0, 22_000.0, 21_000.0, 23_000.0, 24_000.0, 22_500.0]
assert detect_wire_spike(history, 30_000.0) is None
def test_spike_has_absolute_floor_for_silent_links():
# A nearly idle link jittering by a few KB/s must never cry ghost.
history = [100.0, 150.0, 120.0, 90.0, 110.0, 130.0, 140.0]
assert detect_wire_spike(history, 5_000.0) is None
def test_spike_adapts_to_loud_baseline():
# Once the line is genuinely busy, the same surge is no longer anomalous.
history = [350_000.0, 380_000.0, 360_000.0, 370_000.0, 355_000.0, 375_000.0, 365_000.0]
assert detect_wire_spike(history, 400_000.0) is None