mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14: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
|
min_danger_level: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SWPC adapter config shape
|
||||||
|
interface SwpcConfig {
|
||||||
|
geomag_kp_floor: number
|
||||||
|
flare_class_floor: string
|
||||||
|
proton_pfu_floor: number
|
||||||
|
}
|
||||||
|
|
||||||
interface TomtomConfig {
|
interface TomtomConfig {
|
||||||
min_magnitude: number
|
min_magnitude: number
|
||||||
drop_non_present: boolean
|
drop_non_present: boolean
|
||||||
|
|
@ -288,6 +295,12 @@ export default function Environment() {
|
||||||
min_danger_level: 3,
|
min_danger_level: 3,
|
||||||
})
|
})
|
||||||
const [avalancheOriginal, setAvalancheOriginal] = useState<string>("")
|
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(() => {
|
useEffect(() => {
|
||||||
|
|
@ -403,6 +416,21 @@ export default function Environment() {
|
||||||
}
|
}
|
||||||
} catch { /* adapter-config optional */ }
|
} 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) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Failed to load config')
|
setError(e instanceof Error ? e.message : 'Failed to load config')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -431,7 +459,8 @@ export default function Environment() {
|
||||||
const hasWzdxChanges = JSON.stringify(wzdxConfig) !== wzdxOriginal
|
const hasWzdxChanges = JSON.stringify(wzdxConfig) !== wzdxOriginal
|
||||||
const hasNwsChanges = JSON.stringify(nwsConfig) !== nwsOriginal
|
const hasNwsChanges = JSON.stringify(nwsConfig) !== nwsOriginal
|
||||||
const hasAvalancheChanges = JSON.stringify(avalancheConfig) !== avalancheOriginal
|
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) => {
|
const saveAdapterConfig = async (adapterName: string, key: string, value: unknown) => {
|
||||||
|
|
@ -565,6 +594,21 @@ const save = async () => {
|
||||||
setAvalancheOriginal(JSON.stringify(avalancheConfig))
|
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')
|
setSuccess('Config saved')
|
||||||
setTimeout(() => setSuccess(null), 3000)
|
setTimeout(() => setSuccess(null), 3000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -583,6 +627,7 @@ const save = async () => {
|
||||||
setWzdxConfig(JSON.parse(wzdxOriginal || JSON.stringify(wzdxConfig)))
|
setWzdxConfig(JSON.parse(wzdxOriginal || JSON.stringify(wzdxConfig)))
|
||||||
setNwsConfig(JSON.parse(nwsOriginal || JSON.stringify(nwsConfig)))
|
setNwsConfig(JSON.parse(nwsOriginal || JSON.stringify(nwsConfig)))
|
||||||
setAvalancheConfig(JSON.parse(avalancheOriginal || JSON.stringify(avalancheConfig)))
|
setAvalancheConfig(JSON.parse(avalancheOriginal || JSON.stringify(avalancheConfig)))
|
||||||
|
setSwpcConfig(JSON.parse(swpcOriginal || JSON.stringify(swpcConfig)))
|
||||||
}
|
}
|
||||||
const restart = async () => {
|
const restart = async () => {
|
||||||
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
|
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
|
||||||
|
|
@ -642,7 +687,45 @@ const save = async () => {
|
||||||
</div>
|
</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 (
|
case 'ducting': return (
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<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} />
|
<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_alerts -> parse alert payload (flare class, geomag, proton scale)
|
||||||
swpc_protons -> check >=10 MeV proton flux threshold
|
swpc_protons -> check >=10 MeV proton flux threshold
|
||||||
|
|
||||||
Wire format (Matt's approved option C):
|
Wire format (multi-line, matches Fire/Quake/Avalanche style):
|
||||||
🌌 Strong geomagnetic storm (G3/Kp7) -- HF degraded, aurora possible
|
Line 1: {emoji} New: {scale} {type} — {key fact}
|
||||||
🔆 Major solar flare (R3/X1.2) -- HF radio fading ~30 min, GPS may glitch
|
Line 2: supporting detail (impact summary / message, truncated 120 chars)
|
||||||
☢️ Solar radiation storm (S1) -- polar HF radio affected
|
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 __future__ import annotations
|
||||||
from meshai.adapter_config import adapter_config
|
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=?",
|
"SELECT last_broadcast_at FROM swpc_events WHERE event_id=?",
|
||||||
(event_id,)).fetchone()
|
(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:
|
if row is None:
|
||||||
_upsert_swpc(conn, event_id=event_id, adapter=adapter,
|
_upsert_swpc(conn, event_id=event_id, adapter=adapter,
|
||||||
payload_json=payload_json, occurred_at=occurred_at or now,
|
payload_json=payload_json, occurred_at=occurred_at or now,
|
||||||
first_seen_at=now, set_last_broadcast=False)
|
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)
|
_attach_commit(data, event_id=event_id, event_log_row_id=log_id)
|
||||||
return wire
|
return wire
|
||||||
|
|
||||||
if row["last_broadcast_at"] is None:
|
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)
|
_attach_commit(data, event_id=event_id, event_log_row_id=log_id)
|
||||||
return wire
|
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
|
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":
|
if event_kind == "geomag":
|
||||||
return (f"🌌 {label.title()} geomagnetic storm ({scale_code}/{scalar_str}) "
|
line1 = f"🧲 {prefix} {scale_code} Geomagnetic Storm — {scalar_str}"
|
||||||
f"-- HF degraded, aurora possible")
|
line2 = detail[:120] if detail else "HF degraded, aurora possible"
|
||||||
if event_kind == "flare":
|
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
||||||
return (f"🔆 Major solar flare ({scale_code}/{scalar_str}) "
|
elif event_kind == "flare":
|
||||||
f"-- HF radio fading ~30 min, GPS may glitch")
|
line1 = f"☀️ {prefix} {scalar_str} Solar Flare — {scale_code}"
|
||||||
if event_kind == "proton":
|
line2 = detail[:120] if detail else "HF radio fading, GPS may glitch"
|
||||||
return (f"☢️ Solar radiation storm ({scale_code}/{scalar_str}) "
|
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
||||||
f"-- polar HF radio affected")
|
elif event_kind == "proton":
|
||||||
return f"⚠️ Space weather event ({scale_code or '?'})"
|
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,
|
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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dp9XCfH-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue