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