mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-06 12:28:05 +02:00
53ed63ffcf
Drop duplicate slow-tier weather/ukraine jobs, gate correlations when off, slim health probes, keyed layer-panel subscriptions, align backend layer defaults with the dashboard, and fetch CCTV/FIRMS/PSK/etc. synchronously on enable so toggles stay responsive without background prefetch waste.
174 lines
5.7 KiB
Python
174 lines
5.7 KiB
Python
import time as _time_mod
|
|
from fastapi import APIRouter, Request, Depends
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
from limiter import limiter
|
|
from auth import require_admin
|
|
from services.schemas import HealthResponse
|
|
import os
|
|
|
|
# Health/SLO probes only need counts + freshness — not a full dashboard deepcopy.
|
|
_HEALTH_DATA_KEYS: tuple[str, ...] = (
|
|
"last_updated",
|
|
"commercial_flights",
|
|
"military_flights",
|
|
"private_jets",
|
|
"ships",
|
|
"satellites",
|
|
"earthquakes",
|
|
"cctv",
|
|
"news",
|
|
"uavs",
|
|
"firms_fires",
|
|
"liveuamap",
|
|
"gdelt",
|
|
"uap_sightings",
|
|
"wastewater",
|
|
"fimi",
|
|
"space_weather",
|
|
"weather_alerts",
|
|
"volcanoes",
|
|
"prediction_markets",
|
|
)
|
|
|
|
|
|
def _health_data_snapshot() -> dict:
|
|
from services.fetchers._store import get_latest_data_subset_refs
|
|
from services.slo import SLO_REGISTRY
|
|
|
|
keys = tuple(dict.fromkeys((*_HEALTH_DATA_KEYS, *SLO_REGISTRY.keys())))
|
|
return get_latest_data_subset_refs(*keys)
|
|
|
|
|
|
def _health_row_count(value) -> int:
|
|
if value is None:
|
|
return 0
|
|
try:
|
|
return len(value)
|
|
except TypeError:
|
|
return 0
|
|
|
|
APP_VERSION = os.environ.get("_HEALTH_APP_VERSION", "0.9.82")
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _get_app_version() -> str:
|
|
# Import lazily to avoid circular import; main sets APP_VERSION before including routers
|
|
try:
|
|
import main as _main
|
|
return _main.APP_VERSION
|
|
except Exception:
|
|
return APP_VERSION
|
|
|
|
|
|
_start_time_ref: dict = {"value": None}
|
|
|
|
|
|
def _get_start_time() -> float:
|
|
if _start_time_ref["value"] is None:
|
|
try:
|
|
import main as _main
|
|
_start_time_ref["value"] = _main._start_time
|
|
except Exception:
|
|
_start_time_ref["value"] = _time_mod.time()
|
|
return _start_time_ref["value"]
|
|
|
|
|
|
@router.get("/api/health", response_model=HealthResponse)
|
|
@limiter.limit("30/minute")
|
|
async def health_check(request: Request):
|
|
from services.fetchers._store import get_source_timestamps_snapshot
|
|
from services.slo import compute_all_statuses, summarise_statuses
|
|
|
|
d = _health_data_snapshot()
|
|
last = d.get("last_updated")
|
|
timestamps = get_source_timestamps_snapshot()
|
|
slo_statuses = compute_all_statuses(d, timestamps)
|
|
slo_summary = summarise_statuses(slo_statuses)
|
|
# Top-level status reflects worst SLO result — "degraded" if any
|
|
# yellow, "error" if any red, "ok" otherwise. This is the single
|
|
# field an external probe / pager can watch.
|
|
top_status = "ok"
|
|
if slo_summary.get("red", 0) > 0:
|
|
top_status = "error"
|
|
elif slo_summary.get("yellow", 0) > 0:
|
|
top_status = "degraded"
|
|
|
|
# Issue #258: surface AIS proxy degraded TLS state so operators can see
|
|
# when the SPKI-pinned fallback is in effect. The data plane keeps
|
|
# flowing (this is by design — see ais_proxy.js comments) but observers
|
|
# who care about MITM-protection posture deserve a visible signal.
|
|
#
|
|
# Plus connectivity health (added 2026-05-23 when stream.aisstream.io
|
|
# went fully offline): ``connected`` tells the frontend whether ship
|
|
# data is actually flowing. When false, a banner explains that ships
|
|
# are unavailable due to an upstream outage — better than the user
|
|
# silently seeing an empty ocean and assuming we broke something.
|
|
ais_status: dict = {}
|
|
try:
|
|
from services.ais_stream import ais_proxy_status
|
|
ais_status = ais_proxy_status() or {}
|
|
except Exception:
|
|
ais_status = {}
|
|
if ais_status.get("degraded_tls") and top_status == "ok":
|
|
# Don't override a worse top-level status if SLOs already failed,
|
|
# but escalate ok -> degraded so the field surfaces in dashboards.
|
|
top_status = "degraded"
|
|
# AIS_API_KEY not configured is "feature off", not "system broken" —
|
|
# so we only escalate when the operator opted into AIS (key set) AND
|
|
# the stream is currently offline.
|
|
if (
|
|
os.environ.get("AIS_API_KEY")
|
|
and ais_status.get("connected") is False
|
|
and top_status == "ok"
|
|
):
|
|
top_status = "degraded"
|
|
|
|
runtime: dict = {}
|
|
try:
|
|
from services.runtime_profile import get_runtime_profile
|
|
from analytics.settings import gt_analytics_status
|
|
|
|
runtime = {
|
|
**get_runtime_profile(),
|
|
"gt_analytics": gt_analytics_status(),
|
|
}
|
|
except Exception:
|
|
runtime = {}
|
|
|
|
return {
|
|
"status": top_status,
|
|
"version": _get_app_version(),
|
|
"last_updated": last,
|
|
"sources": {
|
|
"flights": _health_row_count(d.get("commercial_flights")),
|
|
"military": _health_row_count(d.get("military_flights")),
|
|
"ships": _health_row_count(d.get("ships")),
|
|
"satellites": _health_row_count(d.get("satellites")),
|
|
"earthquakes": _health_row_count(d.get("earthquakes")),
|
|
"cctv": _health_row_count(d.get("cctv")),
|
|
"news": _health_row_count(d.get("news")),
|
|
"uavs": _health_row_count(d.get("uavs")),
|
|
"firms_fires": _health_row_count(d.get("firms_fires")),
|
|
"liveuamap": _health_row_count(d.get("liveuamap")),
|
|
"gdelt": _health_row_count(d.get("gdelt")),
|
|
"uap_sightings": _health_row_count(d.get("uap_sightings")),
|
|
},
|
|
"freshness": timestamps,
|
|
"uptime_seconds": round(_time_mod.time() - _get_start_time()),
|
|
"slo": slo_statuses,
|
|
"slo_summary": slo_summary,
|
|
"ais_proxy": ais_status,
|
|
"runtime": runtime or None,
|
|
}
|
|
|
|
|
|
@router.get("/api/debug-latest", dependencies=[Depends(require_admin)])
|
|
@limiter.limit("30/minute")
|
|
async def debug_latest_data(request: Request):
|
|
from services.fetchers._store import latest_data, _data_lock
|
|
|
|
with _data_lock:
|
|
return list(latest_data.keys())
|