mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-07 23:03:54 +02:00
2dc1fcc778
What this release does
----------------------
1. Establishes a fresh Tauri updater signing keypair. The previous keypair
(pubkey baked into v0.9.79 / v0.9.8) had no matching private key on
any maintainer-controlled machine — every prior release shipped
without signatures, so auto-update has never actually worked. v0.9.81
rotates to a new pubkey and ships signed installers + latest.json so
every release from here is a one-click upgrade.
2. Fixes the ``admin_session_required`` race in TopRightControls.tsx.
The updateAction state used to default to ``auto_apply`` at React-init
time. A click on the Update button before the async runtime probe
completed went down the auto_apply path (POST /api/system/update),
which throws ``admin_session_required`` on fresh sessions. Desktop
installs now default to ``manual_download`` based on synchronous
``window.__TAURI__`` detection at useState init.
One-time cost for current installs
----------------------------------
Anyone on v0.9.79 or v0.9.8 will see the in-app Update button still
trigger the broken path on their existing install (the fix only takes
effect once they're ON v0.9.81). The MANUAL DOWNLOAD button in the
update dialog opens the GitHub release page, where they grab the .msi
and run it. After that one manual hop, all future updates are seamless.
Release artifacts
-----------------
ShadowBroker_v0.9.81.zip 6.06 MB
42f8a51f9a5690d1e7349d90d8ecf2d163c9061d6cf90c69ee03647a785437ff
ShadowBroker_0.9.81_x64_en-US.msi 122.4 MB
a45b177c26c95d2b28d71592d7147e88ff4e104865f214fde11249d311ec9e25
ShadowBroker_0.9.81_x64-setup.exe 76.5 MB
eca884b9d37eeccd0f11c91dcc6f6ae1b3609d9dee72bd73c37c9a427babfef2
Plus .sig files for the .msi and .exe, plus a signed latest.json for
the Tauri updater endpoint.
Sizes match the v0.9.79 / v0.9.8 reference shape within drift for
the new TopRightControls patch.
release_digests.json keeps v0.9.79 + v0.9.8 blocks alongside v0.9.81
so operators still on those versions continue to validate cleanly
during the rollout transition.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
118 lines
4.3 KiB
Python
118 lines
4.3 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.data_fetcher import get_latest_data
|
|
from services.schemas import HealthResponse
|
|
import os
|
|
|
|
APP_VERSION = os.environ.get("_HEALTH_APP_VERSION", "0.9.81")
|
|
|
|
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 = get_latest_data()
|
|
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"
|
|
|
|
return {
|
|
"status": top_status,
|
|
"version": _get_app_version(),
|
|
"last_updated": last,
|
|
"sources": {
|
|
"flights": len(d.get("commercial_flights", [])),
|
|
"military": len(d.get("military_flights", [])),
|
|
"ships": len(d.get("ships", [])),
|
|
"satellites": len(d.get("satellites", [])),
|
|
"earthquakes": len(d.get("earthquakes", [])),
|
|
"cctv": len(d.get("cctv", [])),
|
|
"news": len(d.get("news", [])),
|
|
"uavs": len(d.get("uavs", [])),
|
|
"firms_fires": len(d.get("firms_fires", [])),
|
|
"liveuamap": len(d.get("liveuamap", [])),
|
|
"gdelt": len(d.get("gdelt", [])),
|
|
"uap_sightings": len(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,
|
|
}
|
|
|
|
|
|
@router.get("/api/debug-latest", dependencies=[Depends(require_admin)])
|
|
@limiter.limit("30/minute")
|
|
async def debug_latest_data(request: Request):
|
|
return list(get_latest_data().keys())
|