Merge branch 'feature/mesh-intelligence'

This commit is contained in:
Matt Johnson (via Claude) 2026-06-10 06:15:43 +00:00
commit 825df8a3a8
6 changed files with 197 additions and 74 deletions

View file

@ -237,6 +237,110 @@ function BandConditionsCard({ bandConditions }: { bandConditions: BandConditions
) )
} }
// Hepburn Tropospheric Forecast Card
const TROPO_REGIONS: { code: string; label: string }[] = [
{ code: 'wam', label: 'Western North America' },
{ code: 'eam', label: 'Eastern North America' },
{ code: 'enp', label: 'Eastern North Pacific' },
{ code: 'esp', label: 'Eastern South Pacific' },
{ code: 'gca', label: 'Gulf-Caribbean' },
{ code: 'nsa', label: 'Northern South America' },
{ code: 'csa', label: 'Central South America' },
{ code: 'sat', label: 'South Atlantic' },
{ code: 'nat', label: 'North Atlantic' },
{ code: 'ena', label: 'Eastern North Atlantic' },
{ code: 'nwe', label: 'Northwestern Europe' },
{ code: 'eur', label: 'Europe' },
{ code: 'eeu', label: 'Eastern Europe' },
{ code: 'saf', label: 'South Africa' },
{ code: 'mde', label: 'Middle East' },
{ code: 'nca', label: 'North Central Asia' },
{ code: 'ind', label: 'Indian Ocean' },
{ code: 'sea', label: 'Southeast Asia' },
{ code: 'fea', label: 'Far East' },
{ code: 'esi', label: 'Eastern Siberia' },
{ code: 'anz', label: 'Australia & New Zealand' },
{ code: 'oce', label: 'Oceania' },
{ code: 'wnp', label: 'Western North Pacific' },
]
function HepburnTropoCard() {
const [region, setRegion] = useState('wam')
const [imgError, setImgError] = useState(false)
const [saving, setSaving] = useState(false)
// Load persisted region from adapter_config on mount
useEffect(() => {
fetch('/api/adapter-config/dashboard/tropo_region')
.then(r => r.ok ? r.json() : null)
.then(d => {
if (d?.value && typeof d.value === 'string') {
setRegion(d.value)
}
})
.catch(() => {})
}, [])
const handleRegionChange = (newRegion: string) => {
setRegion(newRegion)
setImgError(false)
setSaving(true)
fetch('/api/adapter-config/dashboard/tropo_region', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value: newRegion }),
})
.catch(() => {})
.finally(() => setSaving(false))
}
const cacheBust = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const imgUrl = `https://www.dxinfocentre.com/tr_map/fcst/${region}006.png?v${cacheBust}`
const regionLabel = TROPO_REGIONS.find(r => r.code === region)?.label || region
return (
<div className="bg-bg-card border border-border rounded-lg p-4 flex flex-col">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-slate-400 flex items-center gap-2">
<Radio size={14} />
Tropo Forecast (Hepburn)
</h2>
<div className="flex items-center gap-2">
{saving && <span className="text-xs text-slate-500">saving...</span>}
<select
value={region}
onChange={e => handleRegionChange(e.target.value)}
className="text-xs bg-bg-hover border border-border rounded px-2 py-1 text-slate-300 focus:outline-none focus:border-slate-500"
>
{TROPO_REGIONS.map(r => (
<option key={r.code} value={r.code}>{r.label}</option>
))}
</select>
</div>
</div>
<div className="text-xs text-slate-500 mb-2">{regionLabel} 6-day forecast</div>
{imgError ? (
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
Failed to load forecast image
</div>
) : (
<img
src={imgUrl}
alt={`Hepburn tropo forecast — ${regionLabel}`}
className="w-full rounded border border-border"
onError={() => setImgError(true)}
/>
)}
<div className="text-xs text-slate-600 mt-2">
Source: <a href="https://www.dxinfocentre.com/tropo.html" target="_blank" rel="noopener noreferrer" className="text-slate-500 hover:text-slate-300">dxinfocentre.com</a>
</div>
</div>
)
}
// Source icon mapping // Source icon mapping
const SOURCE_ICONS: Record<string, { icon: typeof Cloud; color: string; label: string }> = { const SOURCE_ICONS: Record<string, { icon: typeof Cloud; color: string; label: string }> = {
nws: { icon: Cloud, color: 'text-blue-400', label: 'NWS' }, nws: { icon: Cloud, color: 'text-blue-400', label: 'NWS' },
@ -591,6 +695,11 @@ export default function Dashboard() {
{/* Live Event Feed */} {/* Live Event Feed */}
<LiveEventFeed events={envEvents} envStatus={envStatus} /> <LiveEventFeed events={envEvents} envStatus={envStatus} />
</div> </div>
{/* Bottom row: Tropo Forecast */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<HepburnTropoCard />
</div>
</div> </div>
) )
} }

View file

@ -591,6 +591,15 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
"description": "Minimum danger level to broadcast (3=Considerable, 4=High, 5=Extreme).", "description": "Minimum danger level to broadcast (3=Considerable, 4=High, 5=Extreme).",
}, },
# =================================================================
# DASHBOARD -- UI-only settings persisted for the operator
# =================================================================
("dashboard", "tropo_region"): {
"default": "wam",
"type": "str",
"description": "Hepburn tropo forecast region code displayed on dashboard.",
},
} }
@ -720,6 +729,11 @@ ADAPTER_META: dict[str, dict[str, Any]] = {
"reminder_enabled": True, "reminder_enabled": True,
"description": "Subset of itd_511 traffic_events filtered to work-zone sub_type, used as the reminder target.", "description": "Subset of itd_511 traffic_events filtered to work-zone sub_type, used as the reminder target.",
}, },
"dashboard": {
"display_name": "Dashboard UI settings",
"include_in_llm_context": False,
"description": "Operator UI preferences persisted to adapter_config (region selectors, display options).",
},
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,8 +8,8 @@
<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-DcZj_ef-.js"></script> <script type="module" crossorigin src="/assets/index-DVlb83LX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-eNVU4AZQ.css"> <link rel="stylesheet" crossorigin href="/assets/index-kJaaQ570.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>