mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 09:24:44 +02:00
feat(dashboard): v0.4 C.2 family-tab restructure -- 7 families x per-adapter feed_source toggle
Restructures the environmental config UI into the 7-family taxonomy on the
/environment page (Matt-approved Option C: unified families = config +
live status per adapter). The editable adapter config moves out of the
Config page's "Environmental" tab (now deprecated) onto /environment, where
each adapter sub-tab shows its AdapterPanel (on/off + feed_source + settings)
together with its live status (feed health + active events).
Frontend (dashboard-frontend/src):
- pages/Environment.tsx rewritten: 7 family tabs (Weather, Fire, RF
Propagation, Roads, Geohazards, Tracking, Mesh Health) -> per-family
adapter sub-tab strip -> AdapterPanel.
* AdapterPanel: header row = on/off Toggle + feed_source toggle
(native|central). When OFF, feed_source + all settings grey out
(disabled, not hidden).
* Native-only adapters (ducting, avalanche, roads511 -- no Central stream
per C.1's ADAPTER_SUBJECTS) show the feed_source toggle with 'central'
disabled + a 'Central not available for this adapter' tooltip/label.
* Missing-key adapters (firms, roads511) show an 'API key not configured
-- contact admin' notice; toggles still operate.
* Tracking = placeholder ('No adapters yet. ADS-B / AIS / satellite passes
are planned for v0.5.'). Mesh Health = no env adapters; a disabled
feed_source toggle with 'central' greyed for future migration.
* All existing per-adapter settings preserved verbatim (NWS zones/user_agent/
severity, ducting lat/lon, fires state, avalanche centers/season, USGS
sites, traffic corridors+key, roads511 base_url/key/endpoints/bbox, FIRMS
map_key/source/confidence/bbox). ALSO adds a usgs_quake panel
(tick/min_magnitude/region/bbox) -- usgs_quake (2.14) was never exposed in
the old GUI. feed_source field (C.1) now surfaced per adapter.
* Per-adapter live status: reuses /api/env/status feed health + /api/env/active
events (filtered by source; fires->nifc mapping). Refreshes every 30s.
- pages/Config.tsx: removed the now-duplicate 'Environmental' tab (SECTIONS
entry + render case + EnvironmentalSection function + unused Thermometer
import); exported the shared form primitives (Toggle, TextInput, NumberInput,
SelectInput, ListInput, NumberListInput, US_STATES) for reuse by Environment.tsx.
- Reuses the existing restart_required banner pattern.
- Rebuilt static: meshai/dashboard/static/{index.html, assets/index-9OZ6ZqzI.js,
index-B_J_Z7c8.css} (vite emptyOutDir replaced the old hashed bundle).
Rule 17 / no backend change: config is wired to the existing schema-driven
GET/PUT /api/config/environmental (the C.1 feed_source + central + usgs_quake
fields ride the generic dataclass coercion). No backend edited this phase.
Verification: (A) `npm run build` clean -- tsc strict + vite, only the
pre-existing >500kB single-chunk advisory (not introduced here). (B) static
committed; prod rebuilt picks it up. (C) GET / returns the new SPA shell
(index-9OZ6ZqzI.js); the bundle contains the new family strings (Geohazards,
RF Propagation, ADS-B, 'Central not available', 'API key not configured').
(D) GET /api/config/environmental returns all adapters with feed_source=native,
usgs_quake present, central{enabled:false} -- toggles bind to real data; all
native, nothing flipped. Rebuilt prod healthy.
*** BLOCKER FOUND (pre-existing, NOT introduced by C.2) -- flagged for C.2.1 ***
The save half of gate D fails: PUT /api/config/environmental returns
{"detail":"could not determine a constructor for the tag '!include' ..."}.
Root cause: the dashboard PUT handler (meshai/dashboard/api/config_routes.py)
calls meshai/config.py::save_config (monolithic; re-parses config.yaml with a
loader lacking the !include constructor) instead of the multi-file-aware
meshai/config_loader.py::save_section that exists for exactly the !include
layout. This breaks ALL GUI config saves in prod (every section, not just
environmental) and predates C.2 -- the old Config 'Environmental' tab had the
same broken save; C.1 did not touch save_config. The C.2 GET/render/toggle-bind
works; only persistence is blocked. Verified disk pristine (idempotent PUT
errored, wrote nothing; env_feeds.yaml md5 unchanged, restored from backup).
FIX (one line, but prod-wide blast radius -> wants its own phase + verification):
config_routes PUT should call config_loader.save_section(section, data, config_dir)
instead of config.save_config(...). Recommend C.2.1 backend fix before C.3.
C.3 (quake-to-central flip) should not proceed until C.2.1 unblocks GUI save,
since flipping feed_source from the GUI is the whole point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
73c007d227
commit
8eb0c6468c
7 changed files with 891 additions and 1641 deletions
|
|
@ -4,7 +4,7 @@ import ChannelPicker from '@/components/ChannelPicker'
|
|||
import {
|
||||
Settings, Bot, Wifi, MessageSquare, Database, Brain, Eye,
|
||||
Terminal, Cpu, Cloud, Radio, BookOpen, Layers, Activity,
|
||||
Thermometer, LayoutDashboard, Save, RotateCcw, RefreshCw,
|
||||
LayoutDashboard, Save, RotateCcw, RefreshCw,
|
||||
Plus, Trash2, ChevronDown, ChevronRight, AlertTriangle,
|
||||
Check, X, Eye as EyeIcon, EyeOff, ExternalLink
|
||||
} from 'lucide-react'
|
||||
|
|
@ -233,7 +233,6 @@ const SECTIONS: { key: SectionKey; label: string; icon: typeof Settings }[] = [
|
|||
{ key: 'knowledge', label: 'Knowledge', icon: BookOpen },
|
||||
{ key: 'mesh_sources', label: 'Mesh Sources', icon: Layers },
|
||||
{ key: 'mesh_intelligence', label: 'Intelligence', icon: Activity },
|
||||
{ key: 'environmental', label: 'Environmental', icon: Thermometer },
|
||||
{ key: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
]
|
||||
|
||||
|
|
@ -281,7 +280,7 @@ const AVAILABLE_COMMANDS = [
|
|||
]
|
||||
|
||||
// US States for dropdown
|
||||
const US_STATES = [
|
||||
export const US_STATES = [
|
||||
{ value: 'US-AL', label: 'Alabama' }, { value: 'US-AK', label: 'Alaska' },
|
||||
{ value: 'US-AZ', label: 'Arizona' }, { value: 'US-AR', label: 'Arkansas' },
|
||||
{ value: 'US-CA', label: 'California' }, { value: 'US-CO', label: 'Colorado' },
|
||||
|
|
@ -375,7 +374,7 @@ function SectionDescription({ text }: { text: string }) {
|
|||
}
|
||||
|
||||
// Form components
|
||||
function TextInput({ label, value, onChange, type = 'text', placeholder = '', helper = '', info = '', infoLink = '' }: {
|
||||
export function TextInput({ label, value, onChange, type = 'text', placeholder = '', helper = '', info = '', infoLink = '' }: {
|
||||
label: string
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
|
|
@ -417,7 +416,7 @@ function TextInput({ label, value, onChange, type = 'text', placeholder = '', he
|
|||
)
|
||||
}
|
||||
|
||||
function NumberInput({ label, value, onChange, min, max, step = 1, helper = '', info = '', infoLink = '' }: {
|
||||
export function NumberInput({ label, value, onChange, min, max, step = 1, helper = '', info = '', infoLink = '' }: {
|
||||
label: string
|
||||
value: number
|
||||
onChange: (v: number) => void
|
||||
|
|
@ -448,7 +447,7 @@ function NumberInput({ label, value, onChange, min, max, step = 1, helper = '',
|
|||
)
|
||||
}
|
||||
|
||||
function Toggle({ label, checked, onChange, helper = '', info = '', infoLink = '' }: {
|
||||
export function Toggle({ label, checked, onChange, helper = '', info = '', infoLink = '' }: {
|
||||
label: string
|
||||
checked: boolean
|
||||
onChange: (v: boolean) => void
|
||||
|
|
@ -482,7 +481,7 @@ function Toggle({ label, checked, onChange, helper = '', info = '', infoLink = '
|
|||
)
|
||||
}
|
||||
|
||||
function SelectInput({ label, value, onChange, options, helper = '', info = '', infoLink = '' }: {
|
||||
export function SelectInput({ label, value, onChange, options, helper = '', info = '', infoLink = '' }: {
|
||||
label: string
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
|
|
@ -537,7 +536,7 @@ function TextArea({ label, value, onChange, rows = 4, helper = '', info = '', in
|
|||
)
|
||||
}
|
||||
|
||||
function ListInput({ label, value, onChange, helper = '', info = '', infoLink = '' }: {
|
||||
export function ListInput({ label, value, onChange, helper = '', info = '', infoLink = '' }: {
|
||||
label: string
|
||||
value: string[]
|
||||
onChange: (v: string[]) => void
|
||||
|
|
@ -575,7 +574,7 @@ function ListInput({ label, value, onChange, helper = '', info = '', infoLink =
|
|||
)
|
||||
}
|
||||
|
||||
function NumberListInput({ label, value, onChange, helper = '', info = '', infoLink = '' }: {
|
||||
export function NumberListInput({ label, value, onChange, helper = '', info = '', infoLink = '' }: {
|
||||
label: string
|
||||
value: number[]
|
||||
onChange: (v: number[]) => void
|
||||
|
|
@ -1752,415 +1751,6 @@ function MeshIntelligenceSection({ data, onChange }: { data: MeshIntelligenceCon
|
|||
)
|
||||
}
|
||||
|
||||
function EnvironmentalSection({ data, onChange }: { data: EnvironmentalConfig; onChange: (d: EnvironmentalConfig) => void }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SectionDescription text={SECTION_DESCRIPTIONS.environmental} />
|
||||
<Toggle
|
||||
label="Enable Environmental Feeds"
|
||||
checked={data.enabled}
|
||||
onChange={(v) => onChange({ ...data, enabled: v })}
|
||||
helper="Activate live data polling"
|
||||
/>
|
||||
|
||||
{data.enabled && (
|
||||
<>
|
||||
<ListInput
|
||||
label="NWS Zones"
|
||||
value={data.nws_zones}
|
||||
onChange={(v) => onChange({ ...data, nws_zones: v })}
|
||||
helper="Zone IDs like IDZ016, IDZ030"
|
||||
info="NWS forecast zones covering your mesh area. Find yours at https://www.weather.gov/pimar/PubZone"
|
||||
infoLink="https://www.weather.gov/pimar/PubZone"
|
||||
/>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-slate-300">NWS Weather Alerts</span>
|
||||
<Toggle label="" checked={data.nws.enabled} onChange={(v) => onChange({ ...data, nws: { ...data.nws, enabled: v } })} />
|
||||
</div>
|
||||
{data.nws.enabled && (
|
||||
<>
|
||||
<TextInput
|
||||
label="User Agent"
|
||||
value={data.nws.user_agent}
|
||||
onChange={(v) => onChange({ ...data, nws: { ...data.nws, user_agent: v } })}
|
||||
placeholder="(MeshAI, your@email.com)"
|
||||
helper="Required format: (app_name, contact_email)"
|
||||
info="Required by NWS. You make it up - just use the format (app_name, your_email). No signup needed."
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.nws.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, nws: { ...data.nws, tick_seconds: v } })}
|
||||
min={30}
|
||||
helper="Polling interval"
|
||||
/>
|
||||
<SelectInput
|
||||
label="Min Severity"
|
||||
value={data.nws.severity_min}
|
||||
onChange={(v) => onChange({ ...data, nws: { ...data.nws, severity_min: v } })}
|
||||
options={[
|
||||
{ value: 'minor', label: 'Minor' },
|
||||
{ value: 'moderate', label: 'Moderate' },
|
||||
{ value: 'severe', label: 'Severe' },
|
||||
{ value: 'extreme', label: 'Extreme' },
|
||||
]}
|
||||
helper="Filter out lower severity alerts"
|
||||
info="Minimum severity level to display. 'Moderate' filters out minor advisories. 'Severe' shows only serious warnings."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">NOAA Space Weather (SWPC)</span>
|
||||
<p className="text-xs text-slate-600">Solar indices, geomagnetic storms, HF propagation</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.swpc.enabled} onChange={(v) => onChange({ ...data, swpc: { ...data.swpc, enabled: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">Tropospheric Ducting</span>
|
||||
<p className="text-xs text-slate-600">VHF/UHF extended range conditions</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.ducting.enabled} onChange={(v) => onChange({ ...data, ducting: { ...data.ducting, enabled: v } })} />
|
||||
</div>
|
||||
{data.ducting.enabled && (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.ducting.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, ducting: { ...data.ducting, tick_seconds: v } })}
|
||||
min={60}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Latitude"
|
||||
value={data.ducting.latitude}
|
||||
onChange={(v) => onChange({ ...data, ducting: { ...data.ducting, latitude: v } })}
|
||||
step={0.01}
|
||||
info="Center point of your mesh coverage area. The ducting adapter checks atmospheric conditions at this location."
|
||||
/>
|
||||
<NumberInput
|
||||
label="Longitude"
|
||||
value={data.ducting.longitude}
|
||||
onChange={(v) => onChange({ ...data, ducting: { ...data.ducting, longitude: v } })}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">NIFC Fire Perimeters</span>
|
||||
<p className="text-xs text-slate-600">Active wildfires from National Interagency Fire Center</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.fires.enabled} onChange={(v) => onChange({ ...data, fires: { ...data.fires, enabled: v } })} />
|
||||
</div>
|
||||
{data.fires.enabled && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.fires.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, fires: { ...data.fires, tick_seconds: v } })}
|
||||
min={60}
|
||||
/>
|
||||
<SelectInput
|
||||
label="State"
|
||||
value={data.fires.state}
|
||||
onChange={(v) => onChange({ ...data, fires: { ...data.fires, state: v } })}
|
||||
options={US_STATES}
|
||||
helper="Filter fires by state"
|
||||
info="Two-letter state code for NIFC wildfire filtering."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">Avalanche Advisories</span>
|
||||
<p className="text-xs text-slate-600">Backcountry avalanche danger ratings</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.avalanche.enabled} onChange={(v) => onChange({ ...data, avalanche: { ...data.avalanche, enabled: v } })} />
|
||||
</div>
|
||||
{data.avalanche.enabled && (
|
||||
<>
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.avalanche.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, avalanche: { ...data.avalanche, tick_seconds: v } })}
|
||||
min={60}
|
||||
/>
|
||||
<ListInput
|
||||
label="Center IDs"
|
||||
value={data.avalanche.center_ids}
|
||||
onChange={(v) => onChange({ ...data, avalanche: { ...data.avalanche, center_ids: v } })}
|
||||
helper="e.g., SNFAC, IPAC, FAC"
|
||||
info="Find your local center at https://avalanche.org/avalanche-centers/"
|
||||
infoLink="https://avalanche.org/avalanche-centers/"
|
||||
/>
|
||||
<NumberListInput
|
||||
label="Season Months"
|
||||
value={data.avalanche.season_months}
|
||||
onChange={(v) => onChange({ ...data, avalanche: { ...data.avalanche, season_months: v } })}
|
||||
helper="e.g., 12, 1, 2, 3, 4"
|
||||
info="Months when avalanche forecasts are active. Default Dec-Apr. Adjust for your region's season."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">USGS Stream Gauges</span>
|
||||
<p className="text-xs text-slate-600">River and stream water levels</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.usgs?.enabled || false} onChange={(v) => onChange({ ...data, usgs: { ...data.usgs, enabled: v, tick_seconds: data.usgs?.tick_seconds || 900, sites: data.usgs?.sites || [] } })} />
|
||||
</div>
|
||||
{data.usgs?.enabled && (
|
||||
<>
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.usgs.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, usgs: { ...data.usgs, tick_seconds: v } })}
|
||||
min={900}
|
||||
helper="Minimum 15 min (900s)"
|
||||
/>
|
||||
<ListInput
|
||||
label="Site IDs"
|
||||
value={data.usgs.sites}
|
||||
onChange={(v) => onChange({ ...data, usgs: { ...data.usgs, sites: v } })}
|
||||
helper="USGS gauge site numbers"
|
||||
info="Find site IDs at waterdata.usgs.gov/nwis"
|
||||
infoLink="https://waterdata.usgs.gov/nwis"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">TomTom Traffic</span>
|
||||
<p className="text-xs text-slate-600">Traffic flow on monitored corridors</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.traffic?.enabled || false} onChange={(v) => onChange({ ...data, traffic: { ...data.traffic, enabled: v, tick_seconds: data.traffic?.tick_seconds || 300, api_key: data.traffic?.api_key || '', corridors: data.traffic?.corridors || [] } })} />
|
||||
</div>
|
||||
{data.traffic?.enabled && (
|
||||
<>
|
||||
<TextInput
|
||||
label="API Key"
|
||||
value={data.traffic.api_key}
|
||||
onChange={(v) => onChange({ ...data, traffic: { ...data.traffic, api_key: v } })}
|
||||
type="password"
|
||||
helper="Get key at developer.tomtom.com"
|
||||
infoLink="https://developer.tomtom.com"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.traffic.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, traffic: { ...data.traffic, tick_seconds: v } })}
|
||||
min={60}
|
||||
/>
|
||||
<div className="text-xs text-slate-500 mt-2">Corridors (each with name, lat, lon):</div>
|
||||
{(data.traffic.corridors || []).map((c, i) => (
|
||||
<div key={i} className="grid grid-cols-4 gap-2 items-end">
|
||||
<TextInput label="Name" value={c.name} onChange={(v) => {
|
||||
const newCorridors = [...data.traffic.corridors]
|
||||
newCorridors[i] = { ...c, name: v }
|
||||
onChange({ ...data, traffic: { ...data.traffic, corridors: newCorridors } })
|
||||
}} />
|
||||
<NumberInput label="Lat" value={c.lat} onChange={(v) => {
|
||||
const newCorridors = [...data.traffic.corridors]
|
||||
newCorridors[i] = { ...c, lat: v }
|
||||
onChange({ ...data, traffic: { ...data.traffic, corridors: newCorridors } })
|
||||
}} step={0.01} />
|
||||
<NumberInput label="Lon" value={c.lon} onChange={(v) => {
|
||||
const newCorridors = [...data.traffic.corridors]
|
||||
newCorridors[i] = { ...c, lon: v }
|
||||
onChange({ ...data, traffic: { ...data.traffic, corridors: newCorridors } })
|
||||
}} step={0.01} />
|
||||
<button
|
||||
onClick={() => onChange({ ...data, traffic: { ...data.traffic, corridors: data.traffic.corridors.filter((_, j) => j !== i) } })}
|
||||
className="px-2 py-2 text-xs text-red-400 hover:text-red-300 border border-red-400/30 rounded"
|
||||
>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => onChange({ ...data, traffic: { ...data.traffic, corridors: [...(data.traffic.corridors || []), { name: '', lat: 0, lon: 0 }] } })}
|
||||
className="text-xs text-accent hover:underline"
|
||||
>+ Add Corridor</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">511 Road Conditions</span>
|
||||
<p className="text-xs text-slate-600">State DOT road events and closures</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.roads511?.enabled || false} onChange={(v) => onChange({ ...data, roads511: { ...data.roads511, enabled: v, tick_seconds: data.roads511?.tick_seconds || 300, api_key: data.roads511?.api_key || '', base_url: data.roads511?.base_url || '', endpoints: data.roads511?.endpoints || ['/get/event'], bbox: data.roads511?.bbox || [] } })} />
|
||||
</div>
|
||||
{data.roads511?.enabled && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Base URL"
|
||||
value={data.roads511.base_url}
|
||||
onChange={(v) => onChange({ ...data, roads511: { ...data.roads511, base_url: v } })}
|
||||
placeholder="https://511.yourstate.gov/api/v2"
|
||||
helper="State 511 API endpoint"
|
||||
/>
|
||||
<TextInput
|
||||
label="API Key"
|
||||
value={data.roads511.api_key}
|
||||
onChange={(v) => onChange({ ...data, roads511: { ...data.roads511, api_key: v } })}
|
||||
type="password"
|
||||
helper="Leave empty if not required"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.roads511.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, roads511: { ...data.roads511, tick_seconds: v } })}
|
||||
min={60}
|
||||
/>
|
||||
<ListInput
|
||||
label="Endpoints"
|
||||
value={data.roads511.endpoints}
|
||||
onChange={(v) => onChange({ ...data, roads511: { ...data.roads511, endpoints: v } })}
|
||||
helper="e.g., /get/event, /get/mountainpasses"
|
||||
/>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<NumberInput label="West" value={data.roads511.bbox?.[0] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.roads511.bbox || [0, 0, 0, 0])]
|
||||
bbox[0] = v
|
||||
onChange({ ...data, roads511: { ...data.roads511, bbox } })
|
||||
}} step={0.01} />
|
||||
<NumberInput label="South" value={data.roads511.bbox?.[1] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.roads511.bbox || [0, 0, 0, 0])]
|
||||
bbox[1] = v
|
||||
onChange({ ...data, roads511: { ...data.roads511, bbox } })
|
||||
}} step={0.01} />
|
||||
<NumberInput label="East" value={data.roads511.bbox?.[2] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.roads511.bbox || [0, 0, 0, 0])]
|
||||
bbox[2] = v
|
||||
onChange({ ...data, roads511: { ...data.roads511, bbox } })
|
||||
}} step={0.01} />
|
||||
<NumberInput label="North" value={data.roads511.bbox?.[3] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.roads511.bbox || [0, 0, 0, 0])]
|
||||
bbox[3] = v
|
||||
onChange({ ...data, roads511: { ...data.roads511, bbox } })
|
||||
}} step={0.01} />
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">Bounding box filter (leave all 0 to disable)</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">NASA FIRMS Satellite Fire Detection</span>
|
||||
<p className="text-xs text-slate-600">Near real-time thermal anomalies from satellites</p>
|
||||
</div>
|
||||
<Toggle label="" checked={data.firms?.enabled || false} onChange={(v) => onChange({ ...data, firms: { ...data.firms, enabled: v, tick_seconds: data.firms?.tick_seconds || 1800, map_key: data.firms?.map_key || '', source: data.firms?.source || 'VIIRS_SNPP_NRT', bbox: data.firms?.bbox || [], day_range: data.firms?.day_range || 1, confidence_min: data.firms?.confidence_min || 'nominal', proximity_km: data.firms?.proximity_km || 10 } })} />
|
||||
</div>
|
||||
{data.firms?.enabled && (
|
||||
<>
|
||||
<TextInput
|
||||
label="MAP Key"
|
||||
value={data.firms.map_key}
|
||||
onChange={(v) => onChange({ ...data, firms: { ...data.firms, map_key: v } })}
|
||||
type="password"
|
||||
helper="Get key at firms.modaps.eosdis.nasa.gov/api/area/"
|
||||
infoLink="https://firms.modaps.eosdis.nasa.gov/api/area/"
|
||||
/>
|
||||
<NumberInput
|
||||
label="Tick Seconds"
|
||||
value={data.firms.tick_seconds}
|
||||
onChange={(v) => onChange({ ...data, firms: { ...data.firms, tick_seconds: v } })}
|
||||
min={300}
|
||||
helper="Minimum 5 min (300s)"
|
||||
/>
|
||||
<SelectInput
|
||||
label="Satellite Source"
|
||||
value={data.firms.source}
|
||||
onChange={(v) => onChange({ ...data, firms: { ...data.firms, source: v } })}
|
||||
options={[
|
||||
{ value: 'VIIRS_SNPP_NRT', label: 'VIIRS SNPP (Near Real-Time)' },
|
||||
{ value: 'VIIRS_NOAA20_NRT', label: 'VIIRS NOAA-20 (Near Real-Time)' },
|
||||
{ value: 'MODIS_NRT', label: 'MODIS (Near Real-Time)' },
|
||||
]}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Day Range"
|
||||
value={data.firms.day_range}
|
||||
onChange={(v) => onChange({ ...data, firms: { ...data.firms, day_range: v } })}
|
||||
min={1}
|
||||
max={10}
|
||||
helper="1-10 days of data"
|
||||
/>
|
||||
<SelectInput
|
||||
label="Minimum Confidence"
|
||||
value={data.firms.confidence_min}
|
||||
onChange={(v) => onChange({ ...data, firms: { ...data.firms, confidence_min: v } })}
|
||||
options={[
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'nominal', label: 'Nominal' },
|
||||
{ value: 'high', label: 'High' },
|
||||
]}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Proximity (km)"
|
||||
value={data.firms.proximity_km}
|
||||
onChange={(v) => onChange({ ...data, firms: { ...data.firms, proximity_km: v } })}
|
||||
step={0.5}
|
||||
helper="Distance to match known fires"
|
||||
/>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<NumberInput label="West" value={data.firms.bbox?.[0] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.firms.bbox || [0, 0, 0, 0])]
|
||||
bbox[0] = v
|
||||
onChange({ ...data, firms: { ...data.firms, bbox } })
|
||||
}} step={0.01} />
|
||||
<NumberInput label="South" value={data.firms.bbox?.[1] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.firms.bbox || [0, 0, 0, 0])]
|
||||
bbox[1] = v
|
||||
onChange({ ...data, firms: { ...data.firms, bbox } })
|
||||
}} step={0.01} />
|
||||
<NumberInput label="East" value={data.firms.bbox?.[2] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.firms.bbox || [0, 0, 0, 0])]
|
||||
bbox[2] = v
|
||||
onChange({ ...data, firms: { ...data.firms, bbox } })
|
||||
}} step={0.01} />
|
||||
<NumberInput label="North" value={data.firms.bbox?.[3] || 0} onChange={(v) => {
|
||||
const bbox = [...(data.firms.bbox || [0, 0, 0, 0])]
|
||||
bbox[3] = v
|
||||
onChange({ ...data, firms: { ...data.firms, bbox } })
|
||||
}} step={0.01} />
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">Bounding box for monitoring area (required)</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DashboardSection({ data, onChange }: { data: DashboardConfig; onChange: (d: DashboardConfig) => void }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -2324,7 +1914,6 @@ export default function Config() {
|
|||
case 'knowledge': return <KnowledgeSection data={config.knowledge} onChange={(d) => updateSection('knowledge', d)} />
|
||||
case 'mesh_sources': return <MeshSourcesSection data={config.mesh_sources} onChange={(d) => updateSection('mesh_sources', d)} />
|
||||
case 'mesh_intelligence': return <MeshIntelligenceSection data={config.mesh_intelligence} onChange={(d) => updateSection('mesh_intelligence', d)} />
|
||||
case 'environmental': return <EnvironmentalSection data={config.environmental} onChange={(d) => updateSection('environmental', d)} />
|
||||
case 'dashboard': return <DashboardSection data={config.dashboard} onChange={(d) => updateSection('dashboard', d)} />
|
||||
default: return null
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue