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

@@ -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}