feat: legibility — conditions, hints, kinder errors (Workstream B)
GET /api/conditions surfaces what the backend already computes: moon phase, veil thinness, geomagnetic state — junk lon degrades to moon-only instead of a 422, missing NOAA data means fewer lines, never an error. VeilConditions renders it in the séance side column, polling every 10 min. ModeHint: one in-fiction line per mode after 15s of an unused sensor, dismissed forever via localStorage. Error copy in evp/radio now detect-and-redirects (mic denied -> 'the board needs no ear'; no WebUSB -> try EVP) instead of dead-ending. No geolocation prompt from the conditions strip — asking for location from a passive readout would be hostile; ?lon= stays supported for callers that have it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> # Conflicts: # frontend/src/pages/SeancePage.tsx
This commit is contained in:
@@ -13,6 +13,7 @@ from app.config import settings
|
||||
from app.db import Base, async_session_maker, engine
|
||||
from app.routes.auth import router as auth_router
|
||||
from app.routes.codex import router as codex_router
|
||||
from app.routes.conditions import router as conditions_router
|
||||
from app.routes.device import router as device_router
|
||||
from app.routes.inventory import router as inventory_router
|
||||
from app.routes.seances import router as seances_router
|
||||
@@ -96,6 +97,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(conditions_router)
|
||||
app.include_router(device_router)
|
||||
app.include_router(inventory_router)
|
||||
app.include_router(seances_router)
|
||||
|
||||
46
backend/app/routes/conditions.py
Normal file
46
backend/app/routes/conditions.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Veil conditions readout — the sky and the field, in one call.
|
||||
|
||||
Composes the two real-world signals the app already computes:
|
||||
|
||||
sky - app.celestial.veil_thinness(): moon phase/illumination and
|
||||
(when a longitude is offered) proximity to true solar
|
||||
midnight. Pure math, always present.
|
||||
geomagnetic - app.geomagnetic.geomagnetic_cache.reading(): NOAA's
|
||||
planetary Kp from cache. May be None on a cold cache or
|
||||
when NOAA is unreachable — that is a valid answer, not an
|
||||
error, and the frontend simply shows fewer lines.
|
||||
|
||||
`lon` is accepted as a raw string and parsed by hand so a malformed or
|
||||
out-of-range value degrades to the moon-only reading instead of a 422 —
|
||||
the conditions strip must never fail because a browser sent junk.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.celestial import veil_thinness
|
||||
from app.geomagnetic import geomagnetic_cache
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["conditions"])
|
||||
|
||||
|
||||
def _parse_longitude(raw: str | None) -> float | None:
|
||||
"""A usable longitude in [-180, 180], or None for anything else."""
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
lon = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if not math.isfinite(lon) or not -180.0 <= lon <= 180.0:
|
||||
return None
|
||||
return lon
|
||||
|
||||
|
||||
@router.get("/conditions")
|
||||
async def get_conditions(lon: str | None = None):
|
||||
return {
|
||||
"sky": veil_thinness(_parse_longitude(lon)),
|
||||
"geomagnetic": await geomagnetic_cache.reading(),
|
||||
}
|
||||
Reference in New Issue
Block a user