Files
qtalker---/backend/app/routes/conditions.py
Indiana eaa28b20e8 feat: legibility wave — veil conditions, first-run hints, kinder errors
Workstream B of the usability wave (#2 hints, #3 conditions, #4 errors):

- GET /api/conditions (new backend/app/routes/conditions.py): composes
  celestial veil_thinness with the cached NOAA Kp reading; lenient lon
  parsing (junk degrades to moon-only, never 422); geomagnetic may be
  null on a cold cache. Route tests stub the cache — no live NOAA calls.
- VeilConditions strip in the séance side column: moon glyph + phase,
  % lit, veil-thinness phrase, Kp line only when data exists. Polls
  every 10 min; renders nothing while loading; no error state.
- ModeHint: per-mode in-fiction one-liner after ~15s idle, suppressed
  once the mode's sensor runs this session, dismissal persisted in
  localStorage (qm_hint_<mode>). One mount line per panel.
- Error copy upgraded to detect-and-redirect: mic denied points at site
  settings and the ouija board/wire; WebUSB-unsupported suggests EVP.
- i18n en/es parity for every new string; coverage-check template
  domains extended for the new template-key call sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:31:10 +00:00

47 lines
1.5 KiB
Python

"""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(),
}