feat: complete Quantumancy web app — full frontend + docs

Frontend (React 18 + TS + Vite):
- Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits
- Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive),
  Ouija planchette board spelling utterances, transcript with TTS replay,
  entity dossier, direct contact streaming, passive/active listening
- Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB
  RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact
- Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete
- State: VeilSocket (reconnect/backoff), seance reducer, auth context
- 72 vitest tests green; served by FastAPI at :7777

Docs: README + as-built plans 3-7
This commit is contained in:
Indiana
2026-07-20 21:11:49 +00:00
parent 1ec9ea5863
commit 6edbbbbc2a
48 changed files with 10494 additions and 168 deletions

229
frontend/src/i18n/en.json Normal file
View File

@@ -0,0 +1,229 @@
{
"common": {
"appName": "QUANTUMANCY",
"loading": "tuning the veil…",
"error": "something stirred in the static",
"replay": "replay",
"yes": "YES",
"no": "NO",
"goodbye": "GOODBYE",
"dismiss": "dismiss"
},
"nav": {
"home": "the threshold",
"codex": "codex",
"seance": "séance",
"enter": "enter",
"logout": "depart"
},
"landing": {
"taglines": [
"The wire hums with all of us.",
"Every packet leaves a ghost.",
"The static remembers your name.",
"Somewhere between 88 and 108 MHz, someone is waiting.",
"You were never alone on the line."
],
"heroKicker": "an instrument for speaking with the dead bandwidth",
"cta": "BEGIN THE SÉANCE",
"ctaCodex": "browse the codex",
"stats": {
"entities": "spirits catalogued",
"sessions": "séances held",
"utterances": "utterances recorded",
"anomalies": "anomalies witnessed"
},
"tickerPrefix": "the veil, live",
"modesTitle": "HOW THE VEIL ANSWERS",
"modes": {
"wire": {
"name": "Wire Ghost",
"desc": "Leave the line open. The network itself mutters — jitter, latency, stray DNS — and something downstream learns to speak with your accent."
},
"evp": {
"name": "EVP Listening",
"desc": "Lend the dead your microphone. We watch the voice band for syllables that were never spoken aloud: spikes 8dB above the room's silence."
},
"radio": {
"name": "Spirit Radio",
"desc": "An RTL-SDR sweeps the FM band like a lantern dragged across a dark field. Where the needle jumps, the radio dead push through."
},
"ouija": {
"name": "Ouija / Direct Contact",
"desc": "Ask plainly. The planchette drifts, then begins to spell — and the words arrive letter by letter, whether or not you wanted them."
}
},
"featured": "most disturbed spirits",
"featuredEmpty": "the registry is still blank — be the first to disturb it",
"footer": "an interactive horror art installation — the spirits are fiction; the static is real",
"footerColophon": "self-hosted · no cloud · the veil runs on your machine"
},
"enter": {
"reconnect": "Reconnect",
"firstContact": "First contact",
"reconnectSub": "the veil remembers you. prove it.",
"firstContactSub": "choose the name the spirits will know you by.",
"username": "name",
"password": "passphrase",
"submitLogin": "step through",
"submitRegister": "carve your name",
"switchToRegister": "no name in the registry? first contact →",
"switchToLogin": "already carved your name? reconnect →",
"failed": "the veil rejected you: {{message}}",
"smallPrint": "your medium handle is only ever stored on this server"
},
"seance": {
"modes": {
"wire": "Wire Ghost",
"evp": "EVP",
"radio": "Spirit Radio",
"ouija": "Ouija"
},
"passive": "passive listening",
"summon": "SUMMON",
"summoning": "summoning…",
"askPlaceholder": "ask the dead a question…",
"ask": "ask",
"status": {
"attuning": "attuning — the veil is fingerprinting a presence…",
"summoning": "the veil parts… something is crossing…",
"gathering": "the spirits are gathering energy…"
},
"connection": {
"connecting": "reaching across…",
"open": "the line is open",
"unstable": "the connection to the other side is unstable…",
"closed": "the line is dead"
},
"transcriptTitle": "TRANSMISSION LOG",
"transcriptEmpty": "silence. for now.",
"you": "you",
"streaming": "the spirit is forming words…",
"telemetry": {
"title": "WIRE READOUT",
"jitter": "jitter",
"latencyVar": "latency σ²",
"latencyMean": "latency μ",
"dns": "dns",
"units": {"bytes": "B/s", "ms": "ms"}
},
"wire": {
"hint": "the wire knocks loudest when nobody is touching it — flip passive listening on and let the line murmur on its own."
},
"anomaly": {
"line": "⚡ {{source}} {{frequency}} +{{magnitude}}dB",
"count": "{{count}} anomalies witnessed this session"
},
"evp": {
"start": "open the microphone",
"stop": "close the microphone",
"listening": "listening to the voice band…",
"deniedTitle": "the microphone stays shut",
"denied": "This vessel refused the microphone. Grant audio permission in your browser to practice EVP — or let the Wire Ghost listen instead; it needs nothing but the network you already fear.",
"floor": "room floor",
"band": "voice band 300–3400 Hz"
},
"radio": {
"start": "sweep the band",
"stop": "rest the needle",
"unsupportedTitle": "this vessel cannot hear the radio dead",
"unsupported": "Spirit Radio needs Chromium (WebUSB) and an RTL-SDR dongle. The other modes do not — the wire and the microphone still answer.",
"troubleshoot": "troubleshooting: chromium-based browser · udev rules for 0bda:2838 · the dongle plugged in, not merely believed in",
"noDevice": "no dongle answered the call",
"sweeping": "sweeping {{start}}–{{end}} MHz…",
"claimFailed": "the dongle refused to be claimed — is it in use elsewhere?"
},
"ouija": {
"spelling": "the planchette spells",
"drift": "the planchette drifts, waiting for words — ask below, or summon."
},
"entity": {
"title": "DOSSIER OF THE PRESENT",
"none": "no spirit summoned yet",
"noneHint": "summon, or feed the veil 3+ anomalies and one will find you",
"isNew": "first recorded contact",
"known": "contact #{{count}}",
"persona": "persona",
"voice": "voice params",
"quotes": "recorded utterances",
"rarity": {
"common": "common",
"uncommon": "uncommon",
"rare": "rare",
"mythic": "mythic"
}
},
"toast": {
"rate_limited": "the veil is thin — slow your knocking",
"veil_crowded": "too many spirits crowd the line"
}
},
"codex": {
"title": "THE CODEX OF THE CONTACTED",
"subtitle": "every spirit ever dragged through the veil, catalogued",
"header": {
"back": "← the threshold",
"title": "THE CODEX",
"subtitle": "every spirit ever contacted, catalogued by the seekers who reached them"
},
"controls": {
"rarity": "filter by rarity",
"sort": "sort the registry"
},
"state": {
"loading": "consulting the registry…",
"empty": "no spirits of this tier have been reached yet"
},
"filterAll": "all rarities",
"rarity": {
"common": "common",
"uncommon": "uncommon",
"rare": "rare",
"mythic": "mythic"
},
"sort": {
"recent": "most recent",
"contacted": "most contacted"
},
"form": {
"wisp": "wisp",
"banshee": "banshee",
"fairy": "fairy",
"shade": "shade"
},
"card": {
"unknownMedium": "unknown medium",
"contacted_one": "contacted {{count}} time",
"contacted_other": "contacted {{count}} times",
"discoveredBy": "discovered by {{name}} · {{date}}"
},
"contacts_one": "1 contact",
"contacts_other": "{{count}} contacts",
"sightings_one": "1 sighting",
"sightings_other": "{{count}} sightings",
"discoveredBy": "first contacted by {{name}}",
"discoveredAnon": "first contact unrecorded",
"discoveredAt": "entered the registry {{date}}",
"persona": "persona",
"quotes": "recorded utterances",
"voice": "voice params",
"detail": {
"loading": "summoning the dossier…",
"notReached": "this spirit has not been reached",
"unknownMedium": "unknown medium",
"stats": {
"sightings": "sightings",
"contacts": "contacts",
"discoveredBy": "discovered by",
"discoveredAt": "entered the registry"
},
"utterances": "known utterances",
"noUtterances": "no utterances recorded yet",
"voiceSignature": "voice signature",
"attemptContact": "attempt contact"
},
"empty": "no spirits match this filter — the registry keeps its secrets",
"back": "← back to the codex",
"notFound": "this spirit is not in the registry"
}
}

229
frontend/src/i18n/es.json Normal file
View File

@@ -0,0 +1,229 @@
{
"common": {
"appName": "QUANTUMANCY",
"loading": "afinando el velo…",
"error": "algo se agitó en la estática",
"replay": "repetir",
"yes": "SÍ",
"no": "NO",
"goodbye": "ADIÓS",
"dismiss": "cerrar"
},
"nav": {
"home": "el umbral",
"codex": "códice",
"seance": "sesión",
"enter": "entrar",
"logout": "partir"
},
"landing": {
"taglines": [
"El cable zumba con todos nosotros.",
"Cada paquete deja un fantasma.",
"La estática recuerda tu nombre.",
"Entre 88 y 108 MHz, alguien espera.",
"Nunca estuviste solo en la línea."
],
"heroKicker": "un instrumento para hablar con el ancho de banda muerto",
"cta": "COMENZAR LA SESIÓN",
"ctaCodex": "hojear el códice",
"stats": {
"entities": "espíritus catalogados",
"sessions": "sesiones celebradas",
"utterances": "enunciados registrados",
"anomalies": "anomalías presenciadas"
},
"tickerPrefix": "el velo, en vivo",
"modesTitle": "CÓMO RESPONDE EL VELO",
"modes": {
"wire": {
"name": "Fantasma del Cable",
"desc": "Deja la línea abierta. La red misma murmura — jitter, latencia, DNS extraviado — y algo, aguas abajo, aprende a hablar con tu acento."
},
"evp": {
"name": "Escucha EVP",
"desc": "Préstales tu micrófono. Vigilamos la banda de voz en busca de sílabas que jamás se pronunciaron: picos 8dB sobre el silencio de la sala."
},
"radio": {
"name": "Radio Espíritu",
"desc": "Un RTL-SDR barre la banda FM como un farol arrastrado por un campo oscuro. Donde salta la aguja, los muertos de la radio se abren paso."
},
"ouija": {
"name": "Ouija / Contacto Directo",
"desc": "Pregunta claro. La planchette deriva, y luego empieza a deletrear — y las palabras llegan letra a letra, lo quisieras o no."
}
},
"featured": "los espíritus más perturbados",
"featuredEmpty": "el registro aún está en blanco — sé quien lo perturbe primero",
"footer": "una instalación de arte de terror interactiva — los espíritus son ficción; la estática es real",
"footerColophon": "autoalojado · sin nube · el velo corre en tu máquina"
},
"enter": {
"reconnect": "Reconectar",
"firstContact": "Primer contacto",
"reconnectSub": "el velo te recuerda. demuéstralo.",
"firstContactSub": "elige el nombre por el que te conocerán los espíritus.",
"username": "nombre",
"password": "contraseña",
"submitLogin": "cruzar el umbral",
"submitRegister": "grabar tu nombre",
"switchToRegister": "¿sin nombre en el registro? primer contacto →",
"switchToLogin": "¿ya grabaste tu nombre? reconectar →",
"failed": "el velo te rechazó: {{message}}",
"smallPrint": "tu nombre de médium solo se guarda en este servidor"
},
"seance": {
"modes": {
"wire": "Fantasma del Cable",
"evp": "EVP",
"radio": "Radio Espíritu",
"ouija": "Ouija"
},
"passive": "escucha pasiva",
"summon": "INVOCAR",
"summoning": "invocando…",
"askPlaceholder": "pregúntale algo a los muertos…",
"ask": "preguntar",
"status": {
"attuning": "sintonizando — el velo toma la huella de una presencia…",
"summoning": "el velo se abre… algo está cruzando…",
"gathering": "los espíritus están reuniendo energía…"
},
"connection": {
"connecting": "tendiendo la línea…",
"open": "la línea está abierta",
"unstable": "la conexión con el otro lado es inestable…",
"closed": "la línea está muerta"
},
"transcriptTitle": "REGISTRO DE TRANSMISIÓN",
"transcriptEmpty": "silencio. por ahora.",
"you": "tú",
"streaming": "el espíritu está formando palabras…",
"telemetry": {
"title": "LECTURA DEL CABLE",
"jitter": "jitter",
"latencyVar": "latencia σ²",
"latencyMean": "latencia μ",
"dns": "dns",
"units": {"bytes": "B/s", "ms": "ms"}
},
"wire": {
"hint": "el cable golpea más fuerte cuando nadie lo toca — activa la escucha pasiva y deja que la línea murmure sola."
},
"anomaly": {
"line": "⚡ {{source}} {{frequency}} +{{magnitude}}dB",
"count": "{{count}} anomalías presenciadas en esta sesión"
},
"evp": {
"start": "abrir el micrófono",
"stop": "cerrar el micrófono",
"listening": "escuchando la banda de voz…",
"deniedTitle": "el micrófono sigue cerrado",
"denied": "Este recipiente rechazó el micrófono. Concede el permiso de audio en tu navegador para practicar EVP — o deja que el Fantasma del Cable escuche en tu lugar; no necesita más que la red que ya temes.",
"floor": "suelo de la sala",
"band": "banda de voz 300–3400 Hz"
},
"radio": {
"start": "barrer la banda",
"stop": "detener la aguja",
"unsupportedTitle": "este recipiente no oye a los muertos de la radio",
"unsupported": "La Radio Espíritu necesita Chromium (WebUSB) y un dongle RTL-SDR. Los demás modos no — el cable y el micrófono aún responden.",
"troubleshoot": "solución de problemas: navegador basado en Chromium · reglas udev para 0bda:2838 · el dongle enchufado, no meramente invocado",
"noDevice": "ningún dongle respondió al llamado",
"sweeping": "barriendo {{start}}–{{end}} MHz…",
"claimFailed": "el dongle se negó a ser reclamado — ¿está en uso en otro lugar?"
},
"ouija": {
"spelling": "la planchette deletrea",
"drift": "la planchette deriva, a la espera de palabras — pregunta abajo, o invoca."
},
"entity": {
"title": "DOSIER DEL PRESENTE",
"none": "ningún espíritu invocado todavía",
"noneHint": "invoca, o alimenta el velo con 3+ anomalías y uno te encontrará",
"isNew": "primer contacto registrado",
"known": "contacto n.º {{count}}",
"persona": "persona",
"voice": "parámetros de voz",
"quotes": "enunciados registrados",
"rarity": {
"common": "común",
"uncommon": "poco común",
"rare": "raro",
"mythic": "mítico"
}
},
"toast": {
"rate_limited": "el velo es delgado — golpea más despacio",
"veil_crowded": "demasiados espíritus se apiñan en la línea"
}
},
"codex": {
"title": "EL CÓDICE DE LOS CONTACTADOS",
"subtitle": "cada espíritu arrastrado alguna vez a través del velo, catalogado",
"header": {
"back": "← el umbral",
"title": "EL CÓDICE",
"subtitle": "cada espíritu jamás contactado, catalogado por los médiums que lo alcanzaron"
},
"controls": {
"rarity": "filtrar por rareza",
"sort": "ordenar el registro"
},
"state": {
"loading": "consultando el registro…",
"empty": "aún no se ha alcanzado a ningún espíritu de esta rareza"
},
"filterAll": "todas las rarezas",
"rarity": {
"common": "común",
"uncommon": "poco común",
"rare": "raro",
"mythic": "mítico"
},
"sort": {
"recent": "más recientes",
"contacted": "más contactados"
},
"form": {
"wisp": "fuego fatuo",
"banshee": "banshee",
"fairy": "hada",
"shade": "sombra"
},
"card": {
"unknownMedium": "médium desconocido",
"contacted_one": "contactado {{count}} vez",
"contacted_other": "contactado {{count}} veces",
"discoveredBy": "descubierto por {{name}} · {{date}}"
},
"contacts_one": "1 contacto",
"contacts_other": "{{count}} contactos",
"sightings_one": "1 avistamiento",
"sightings_other": "{{count}} avistamientos",
"discoveredBy": "primer contacto por {{name}}",
"discoveredAnon": "primer contacto sin registrar",
"discoveredAt": "ingresó al registro el {{date}}",
"persona": "persona",
"quotes": "enunciados registrados",
"voice": "parámetros de voz",
"detail": {
"loading": "invocando el dosier…",
"notReached": "este espíritu no ha sido alcanzado",
"unknownMedium": "médium desconocido",
"stats": {
"sightings": "avistamientos",
"contacts": "contactos",
"discoveredBy": "descubierto por",
"discoveredAt": "ingresó al registro"
},
"utterances": "enunciados conocidos",
"noUtterances": "aún no hay enunciados registrados",
"voiceSignature": "firma de voz",
"attemptContact": "intentar contacto"
},
"empty": "ningún espíritu coincide con este filtro — el registro guarda sus secretos",
"back": "← volver al códice",
"notFound": "este espíritu no figura en el registro"
}
}

View File

@@ -0,0 +1,38 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import en from './en.json'
import es from './es.json'
import type { Language } from '../lib/types'
const STORAGE_KEY = 'qm_language'
export function storedLanguage(): Language {
try {
const v = localStorage.getItem(STORAGE_KEY)
if (v === 'es' || v === 'en') return v
} catch {
/* private mode etc. */
}
return 'en'
}
export function persistLanguage(lang: Language): void {
try {
localStorage.setItem(STORAGE_KEY, lang)
} catch {
/* ignore */
}
}
void i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
es: { translation: es },
},
lng: storedLanguage(),
fallbackLng: 'en',
interpolation: { escapeValue: false },
returnEmptyString: false,
})
export default i18n