mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-10 17:04:45 +02:00
feat: swpc multi-line wire + GUI wired to real adapter_config keys
swpc_handler.py: rewrite _render() to multi-line format (emoji + New: prefix + scale/type — detail line — SWPC · time tag). Extract message and time fields from envelope for line 2/3. Environment.tsx: replace empty SWPC panel with broadcast threshold controls — geomag Kp floor (G1-G5), flare class floor (M1-X10), proton pfu floor (S1-S4). Full adapter_config save/load/discard wiring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2aa528ae12
commit
be9bcfded4
5 changed files with 676 additions and 562 deletions
|
|
@ -72,6 +72,13 @@ interface AvalancheConfig {
|
|||
min_danger_level: number
|
||||
}
|
||||
|
||||
// SWPC adapter config shape
|
||||
interface SwpcConfig {
|
||||
geomag_kp_floor: number
|
||||
flare_class_floor: string
|
||||
proton_pfu_floor: number
|
||||
}
|
||||
|
||||
interface TomtomConfig {
|
||||
min_magnitude: number
|
||||
drop_non_present: boolean
|
||||
|
|
@ -288,6 +295,12 @@ export default function Environment() {
|
|||
min_danger_level: 3,
|
||||
})
|
||||
const [avalancheOriginal, setAvalancheOriginal] = useState<string>("")
|
||||
const [swpcConfig, setSwpcConfig] = useState<SwpcConfig>({
|
||||
geomag_kp_floor: 7.0,
|
||||
flare_class_floor: "X1",
|
||||
proton_pfu_floor: 10.0,
|
||||
})
|
||||
const [swpcOriginal, setSwpcOriginal] = useState<string>("")
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -403,6 +416,21 @@ export default function Environment() {
|
|||
}
|
||||
} catch { /* adapter-config optional */ }
|
||||
|
||||
// Load adapter-config for swpc
|
||||
try {
|
||||
const swpcRes = await fetch("/api/adapter-config/swpc")
|
||||
if (swpcRes.ok) {
|
||||
const swpcData = await swpcRes.json()
|
||||
const cfg: SwpcConfig = {
|
||||
geomag_kp_floor: swpcData.geomag_kp_floor?.value ?? 7.0,
|
||||
flare_class_floor: swpcData.flare_class_floor?.value ?? "X1",
|
||||
proton_pfu_floor: swpcData.proton_pfu_floor?.value ?? 10.0,
|
||||
}
|
||||
setSwpcConfig(cfg)
|
||||
setSwpcOriginal(JSON.stringify(cfg))
|
||||
}
|
||||
} catch { /* adapter-config optional */ }
|
||||
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to load config')
|
||||
} finally {
|
||||
|
|
@ -431,7 +459,8 @@ export default function Environment() {
|
|||
const hasWzdxChanges = JSON.stringify(wzdxConfig) !== wzdxOriginal
|
||||
const hasNwsChanges = JSON.stringify(nwsConfig) !== nwsOriginal
|
||||
const hasAvalancheChanges = JSON.stringify(avalancheConfig) !== avalancheOriginal
|
||||
const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes || hasWzdxChanges || hasNwsChanges || hasAvalancheChanges
|
||||
const hasSwpcChanges = JSON.stringify(swpcConfig) !== swpcOriginal
|
||||
const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes || hasWzdxChanges || hasNwsChanges || hasAvalancheChanges || hasSwpcChanges
|
||||
|
||||
|
||||
const saveAdapterConfig = async (adapterName: string, key: string, value: unknown) => {
|
||||
|
|
@ -565,6 +594,21 @@ const save = async () => {
|
|||
setAvalancheOriginal(JSON.stringify(avalancheConfig))
|
||||
}
|
||||
|
||||
// Save swpc adapter config changes
|
||||
if (hasSwpcChanges) {
|
||||
const orig = JSON.parse(swpcOriginal) as SwpcConfig
|
||||
if (swpcConfig.geomag_kp_floor !== orig.geomag_kp_floor) {
|
||||
await saveAdapterConfig("swpc", "geomag_kp_floor", swpcConfig.geomag_kp_floor)
|
||||
}
|
||||
if (swpcConfig.flare_class_floor !== orig.flare_class_floor) {
|
||||
await saveAdapterConfig("swpc", "flare_class_floor", swpcConfig.flare_class_floor)
|
||||
}
|
||||
if (swpcConfig.proton_pfu_floor !== orig.proton_pfu_floor) {
|
||||
await saveAdapterConfig("swpc", "proton_pfu_floor", swpcConfig.proton_pfu_floor)
|
||||
}
|
||||
setSwpcOriginal(JSON.stringify(swpcConfig))
|
||||
}
|
||||
|
||||
setSuccess('Config saved')
|
||||
setTimeout(() => setSuccess(null), 3000)
|
||||
} catch (e) {
|
||||
|
|
@ -583,6 +627,7 @@ const save = async () => {
|
|||
setWzdxConfig(JSON.parse(wzdxOriginal || JSON.stringify(wzdxConfig)))
|
||||
setNwsConfig(JSON.parse(nwsOriginal || JSON.stringify(nwsConfig)))
|
||||
setAvalancheConfig(JSON.parse(avalancheOriginal || JSON.stringify(avalancheConfig)))
|
||||
setSwpcConfig(JSON.parse(swpcOriginal || JSON.stringify(swpcConfig)))
|
||||
}
|
||||
const restart = async () => {
|
||||
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
|
||||
|
|
@ -642,7 +687,45 @@ const save = async () => {
|
|||
</div>
|
||||
)}
|
||||
</>)
|
||||
case 'swpc': return <div className="text-xs text-slate-500">No additional settings.</div>
|
||||
case 'swpc': return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
Broadcast Thresholds
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<SelectInput label="Geomag Kp Floor" value={String(swpcConfig.geomag_kp_floor)}
|
||||
onChange={(v) => setSwpcConfig({ ...swpcConfig, geomag_kp_floor: Number(v) })}
|
||||
options={[
|
||||
{ value: "5", label: "5 — G1 Minor" },
|
||||
{ value: "6", label: "6 — G2 Moderate" },
|
||||
{ value: "7", label: "7 — G3 Strong" },
|
||||
{ value: "8", label: "8 — G4 Severe" },
|
||||
{ value: "9", label: "9 — G5 Extreme" },
|
||||
]}
|
||||
helper="Kp at or above this triggers geomag broadcast" />
|
||||
<SelectInput label="Flare Class Floor" value={swpcConfig.flare_class_floor}
|
||||
onChange={(v) => setSwpcConfig({ ...swpcConfig, flare_class_floor: v })}
|
||||
options={[
|
||||
{ value: "M1", label: "M1 — R1 Minor" },
|
||||
{ value: "M5", label: "M5 — R2 Moderate" },
|
||||
{ value: "X1", label: "X1 — R3 Strong" },
|
||||
{ value: "X10", label: "X10 — R4 Severe" },
|
||||
]}
|
||||
helper="X-ray flare class floor for broadcast" />
|
||||
<SelectInput label="Proton pfu Floor" value={String(swpcConfig.proton_pfu_floor)}
|
||||
onChange={(v) => setSwpcConfig({ ...swpcConfig, proton_pfu_floor: Number(v) })}
|
||||
options={[
|
||||
{ value: "10", label: "10 — S1 Minor" },
|
||||
{ value: "100", label: "100 — S2 Moderate" },
|
||||
{ value: "1000", label: "1000 — S3 Strong" },
|
||||
{ value: "10000", label: "10000 — S4 Severe" },
|
||||
]}
|
||||
helper="Proton flux (pfu) at ≥10 MeV for broadcast" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'ducting': return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<NumberInput label="Tick Seconds" value={env.ducting.tick_seconds} onChange={(v) => up({ ducting: { ...env.ducting, tick_seconds: v } })} min={60} />
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ Three Central sub-adapters all route here:
|
|||
swpc_alerts -> parse alert payload (flare class, geomag, proton scale)
|
||||
swpc_protons -> check >=10 MeV proton flux threshold
|
||||
|
||||
Wire format (Matt's approved option C):
|
||||
🌌 Strong geomagnetic storm (G3/Kp7) -- HF degraded, aurora possible
|
||||
🔆 Major solar flare (R3/X1.2) -- HF radio fading ~30 min, GPS may glitch
|
||||
☢️ Solar radiation storm (S1) -- polar HF radio affected
|
||||
Wire format (multi-line, matches Fire/Quake/Avalanche style):
|
||||
Line 1: {emoji} New: {scale} {type} — {key fact}
|
||||
Line 2: supporting detail (impact summary / message, truncated 120 chars)
|
||||
Line 3: SWPC · {time tag}
|
||||
|
||||
Geomag: 🧲 New: G3 Geomagnetic Storm — Kp7
|
||||
Flare: ☀️ New: X1.2 Solar Flare — R3
|
||||
Proton: ☢️ New: S1 Radiation Storm — 10 pfu
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from meshai.adapter_config import adapter_config
|
||||
|
|
@ -293,33 +297,60 @@ def handle_swpc(envelope: dict, subject: str,
|
|||
"SELECT last_broadcast_at FROM swpc_events WHERE event_id=?",
|
||||
(event_id,)).fetchone()
|
||||
|
||||
# Extract optional detail and time tag for multi-line render.
|
||||
_detail = d.get("message") or d.get("description") or ""
|
||||
if isinstance(_detail, str):
|
||||
_detail = _detail.strip()[:120]
|
||||
else:
|
||||
_detail = ""
|
||||
_time_tag = ""
|
||||
_t_raw = d.get("time") or d.get("issued_at") or d.get("issue_time") or ""
|
||||
if isinstance(_t_raw, str) and _t_raw:
|
||||
_time_tag = _t_raw[:16].replace("T", " ")
|
||||
|
||||
if row is None:
|
||||
_upsert_swpc(conn, event_id=event_id, adapter=adapter,
|
||||
payload_json=payload_json, occurred_at=occurred_at or now,
|
||||
first_seen_at=now, set_last_broadcast=False)
|
||||
wire = _render(event_kind, scale_code, label, scalar_str)
|
||||
wire = _render(event_kind, scale_code, label, scalar_str,
|
||||
is_update=False, detail=_detail, time_tag=_time_tag)
|
||||
_attach_commit(data, event_id=event_id, event_log_row_id=log_id)
|
||||
return wire
|
||||
|
||||
if row["last_broadcast_at"] is None:
|
||||
wire = _render(event_kind, scale_code, label, scalar_str)
|
||||
wire = _render(event_kind, scale_code, label, scalar_str,
|
||||
is_update=False, detail=_detail, time_tag=_time_tag)
|
||||
_attach_commit(data, event_id=event_id, event_log_row_id=log_id)
|
||||
return wire
|
||||
|
||||
# Already broadcast — return None (no Update re-broadcast for SWPC;
|
||||
# space weather events are point-in-time, not evolving like fires).
|
||||
return None
|
||||
|
||||
|
||||
def _render(event_kind, scale_code, label, scalar_str) -> str:
|
||||
def _render(event_kind, scale_code, label, scalar_str,
|
||||
*, is_update: bool = False, detail: str = "",
|
||||
time_tag: str = "") -> str:
|
||||
prefix = "Update:" if is_update else "New:"
|
||||
|
||||
if event_kind == "geomag":
|
||||
return (f"🌌 {label.title()} geomagnetic storm ({scale_code}/{scalar_str}) "
|
||||
f"-- HF degraded, aurora possible")
|
||||
if event_kind == "flare":
|
||||
return (f"🔆 Major solar flare ({scale_code}/{scalar_str}) "
|
||||
f"-- HF radio fading ~30 min, GPS may glitch")
|
||||
if event_kind == "proton":
|
||||
return (f"☢️ Solar radiation storm ({scale_code}/{scalar_str}) "
|
||||
f"-- polar HF radio affected")
|
||||
return f"⚠️ Space weather event ({scale_code or '?'})"
|
||||
line1 = f"🧲 {prefix} {scale_code} Geomagnetic Storm — {scalar_str}"
|
||||
line2 = detail[:120] if detail else "HF degraded, aurora possible"
|
||||
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
||||
elif event_kind == "flare":
|
||||
line1 = f"☀️ {prefix} {scalar_str} Solar Flare — {scale_code}"
|
||||
line2 = detail[:120] if detail else "HF radio fading, GPS may glitch"
|
||||
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
||||
elif event_kind == "proton":
|
||||
line1 = f"☢️ {prefix} {scale_code} Radiation Storm — {scalar_str}"
|
||||
line2 = detail[:120] if detail else "Polar HF radio affected"
|
||||
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
||||
else:
|
||||
line1 = f"⚠️ {prefix} Space Weather Event — {scale_code or '?'}"
|
||||
line2 = detail[:120] if detail else None
|
||||
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
||||
|
||||
return "\n".join(l for l in [line1, line2, line3] if l)
|
||||
|
||||
|
||||
def _upsert_swpc(conn, *, event_id, adapter, payload_json, occurred_at,
|
||||
|
|
|
|||
543
meshai/dashboard/static/assets/index-CM6OazXs.js
Normal file
543
meshai/dashboard/static/assets/index-CM6OazXs.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,7 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-DzHEJ_0f.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CM6OazXs.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dp9XCfH-.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue