mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14:45 +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
513
meshai/dashboard/static/assets/index-9OZ6ZqzI.js
Normal file
513
meshai/dashboard/static/assets/index-9OZ6ZqzI.js
Normal file
File diff suppressed because one or more lines are too long
1
meshai/dashboard/static/assets/index-B_J_Z7c8.css
Normal file
1
meshai/dashboard/static/assets/index-B_J_Z7c8.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -8,8 +8,8 @@
|
|||
<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-Bildyb1E.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-QhNRb-ap.css">
|
||||
<script type="module" crossorigin src="/assets/index-9OZ6ZqzI.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B_J_Z7c8.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue