Add EMF field mode, Armory shop/waitlist, and ambient haunting layer

Three self-contained features, verified complete and cross-wired
end-to-end (audited: backend 54/54 tests, frontend 110/110 tests,
tsc --noEmit clean, i18n coverage script clean):

- EMF mode: DeviceMotion/DeviceOrientation-based field-meter sensing,
  a fifth séance channel alongside Wire/EVP/Radio/Ouija, with its own
  fragment prompt persona and full frontend gauge UI.
- Armory (shop/waitlist): pre-order capture page for the future
  Ultimate Quantum Box hardware line, rate-limited public endpoint,
  explicitly no payment collection.
- Haunting layer: ambient possession effects (dread-bed audio, title
  glitching, idle-paced whispers/manifests), respects
  prefers-reduced-motion, mounted once at the app root.

Plus WebUSB robustness fixes in lib/sdr.ts (Terratec vendor ID,
explicit selectConfiguration, isSecureContext gate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PphXq1s43DNRj1uWKGXof
This commit is contained in:
Indiana
2026-07-21 02:28:14 +00:00
parent c372427ced
commit 0756e677b9
33 changed files with 3148 additions and 48 deletions

View File

@@ -81,6 +81,13 @@ def fragment_prompt(source: str, anomaly: dict, language: str = "en") -> str:
f"{anomaly.get('magnitude', '???')} dB over the room's floor). "
"What single word was hidden in it?"
)
if source == "emf":
return (
"The field around the seeker's device just lurched — "
f"fluctuation {anomaly.get('magnitude', '???')} above baseline "
f"near {anomaly.get('frequency', '???')} µT. Something passed "
"through. What word did it leave?"
)
return "A pulse moves through the wires. What word does it carry?"

View File

@@ -67,7 +67,12 @@ class SpiritService:
settings.ollama_fast_model,
prompts.fragment_prompt(source, anomaly, language),
system=prompts.fragment_system(
"a spirit box" if source == "radio" else "an EVP recorder", language
{
"radio": "a spirit box",
"evp": "an EVP recorder",
"emf": "an EMF field meter",
}.get(source, "the veil"),
language,
),
options={"num_predict": 16, "temperature": 0.95},
)

View File

@@ -10,6 +10,7 @@ from app.config import settings
from app.db import Base, engine
from app.routes.auth import router as auth_router
from app.routes.codex import router as codex_router
from app.routes.shop import router as shop_router
from app.ws import AUDIO_DIR
from app.ws import router as ws_router
@@ -27,6 +28,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Quantumancy", lifespan=lifespan)
app.include_router(auth_router)
app.include_router(codex_router)
app.include_router(shop_router)
app.include_router(ws_router)

View File

@@ -4,6 +4,7 @@ from app.models.entity import Entity
from app.models.entity_sighting import EntitySighting
from app.models.event import Event
from app.models.user import User
from app.models.waitlist_entry import WaitlistEntry
__all__ = [
"User",
@@ -12,4 +13,5 @@ __all__ = [
"Entity",
"EntitySighting",
"Event",
"WaitlistEntry",
]

View File

@@ -0,0 +1,20 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
class WaitlistEntry(Base):
"""A seeker who wants the Ultimate Quantum Box hardware when it ships."""
__tablename__ = "waitlist_entries"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
interest: Mapped[str | None] = mapped_column(String(64), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)

View File

@@ -0,0 +1,50 @@
"""Shop endpoints: the waitlist for the Ultimate Quantum Box hardware."""
import re
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_db
from app.models.waitlist_entry import WaitlistEntry
from app.rate_limit import RateLimiter
router = APIRouter(prefix="/api/shop", tags=["shop"])
waitlist_limiter = RateLimiter(max_requests=5, window_seconds=3600)
_EMAIL_RE = re.compile(r"^[^@\s]{1,64}@[^@\s]{1,255}\.[^@\s]{2,}$")
@router.post("/waitlist", status_code=status.HTTP_201_CREATED)
async def join_waitlist(
payload: dict, request: Request, db: AsyncSession = Depends(get_db)
):
email = str(payload.get("email", "")).strip().lower()
interest = payload.get("interest")
if not _EMAIL_RE.match(email):
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY, "that address does not reach us"
)
client_ip = request.client.host if request.client else "unknown"
if not waitlist_limiter.allow(client_ip):
raise HTTPException(
status.HTTP_429_TOO_MANY_REQUESTS,
"the veil is crowded — try again later",
)
existing = await db.scalar(
select(WaitlistEntry).where(WaitlistEntry.email == email)
)
if existing is not None:
# Idempotent: re-registering the same address is a no-op.
return {"status": "already_listed", "email": email}
entry = WaitlistEntry(
email=email, interest=str(interest)[:64] if interest else None
)
db.add(entry)
await db.commit()
return {"status": "listed", "email": email}

View File

@@ -46,7 +46,7 @@ router = APIRouter()
# Alias so tests can swap in the NullPool test session maker.
session_maker = _default_session_maker
MODES = {"wire", "evp", "radio", "ouija"}
MODES = {"wire", "evp", "radio", "ouija", "emf"}
# Per-user limiters for every LLM-triggering message type (spec §5).
fragment_limiter = RateLimiter(max_requests=30, window_seconds=60)

View File

@@ -0,0 +1,29 @@
import pytest
@pytest.mark.asyncio
async def test_waitlist_join_and_idempotent_rejoin(client):
response = await client.post(
"/api/shop/waitlist", json={"email": "hunter@example.com", "interest": "quantum-box"}
)
assert response.status_code == 201
assert response.json()["status"] == "listed"
again = await client.post(
"/api/shop/waitlist", json={"email": "hunter@example.com"}
)
assert again.status_code == 201
assert again.json()["status"] == "already_listed"
@pytest.mark.asyncio
async def test_waitlist_rejects_bad_email(client):
response = await client.post("/api/shop/waitlist", json={"email": "not-an-email"})
assert response.status_code == 422
@pytest.mark.asyncio
async def test_waitlist_is_public(client):
# No auth cookie — the shop is open to every passing seeker.
response = await client.post("/api/shop/waitlist", json={"email": "ghost@example.org"})
assert response.status_code == 201