mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
fire: remove fire digest feature and drop out-of-coverage fires at ingest (#107)
Two fire-scope cleanups: 1. Remove the fire digest feature entirely -- scheduler (notifications/scheduled/fire_digest.py), pipeline wiring, the fires.digest_* adapter_config key registrations, and the Fire Digest dashboard UI (ScheduledBroadcasts / Environment / Reference / AdapterConfig / ActivityLog). The unrelated generic per-rule notification digest is kept. Orphaned fires.digest_* config rows and the fire_digest_broadcasts table are left as inert data (v16 migration untouched). 2. Add a coverage-scope gate at fire ingest: _ingest_fires now skips any fire whose coordinates fall outside all configured coverage areas (same areas_from_config + classify_geom_areas membership the dispatch-level CoverageFilter uses), so out-of-coverage fires are never stored, tracked, alerted, reminded, or re-ingested. Fails open when coverage is disabled, has no areas, or excludes the fires adapter. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
af826319c8
commit
b0b0697bac
16 changed files with 189 additions and 960 deletions
|
|
@ -112,7 +112,6 @@ const CATEGORIES = [
|
|||
{ value: 'all', label: 'All types' },
|
||||
{ value: 'nws_alerts', label: 'Weather' },
|
||||
{ value: 'fires', label: 'Fires' },
|
||||
{ value: 'fire_digest_broadcasts', label: 'Fire digest' },
|
||||
{ value: 'satpass_events', label: 'Satellite' },
|
||||
{ value: 'band_conditions_broadcasts', label: 'Band' },
|
||||
{ value: 'traffic_events', label: 'Traffic' },
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import {
|
|||
// Advanced (raw) view to avoid double-editing.
|
||||
export const CURATED_KEYS: Record<string, string[]> = {
|
||||
wfigs: ['allowed_incident_types', 'freshness_seconds', 'cooldown_seconds', 'broadcast_on_acres', 'broadcast_on_contained'],
|
||||
fires: ['digest_enabled', 'digest_schedule', 'digest_timezone'],
|
||||
tomtom_incidents: ['min_magnitude', 'drop_non_present', 'drop_zero_magnitude'],
|
||||
itd_511: ['min_severity', 'enabled_categories', 'enabled_sub_types'],
|
||||
wzdx: ['broadcast', 'min_severity', 'sub_types'],
|
||||
|
|
|
|||
|
|
@ -74,13 +74,6 @@ interface WfigsConfig {
|
|||
broadcast_on_contained: boolean
|
||||
}
|
||||
|
||||
// Fires adapter config shape (digest settings)
|
||||
interface FiresConfig {
|
||||
digest_enabled: boolean
|
||||
digest_schedule: string[]
|
||||
digest_timezone: string
|
||||
}
|
||||
|
||||
// ITD 511 adapter config shape
|
||||
interface Roads511Config {
|
||||
min_severity: string
|
||||
|
|
@ -345,12 +338,6 @@ export default function Environment() {
|
|||
broadcast_on_contained: true,
|
||||
})
|
||||
const [wfigsOriginal, setWfigsOriginal] = useState<string>("")
|
||||
const [firesConfig, setFiresConfig] = useState<FiresConfig>({
|
||||
digest_enabled: true,
|
||||
digest_schedule: ["06:00", "18:00"],
|
||||
digest_timezone: "America/Boise",
|
||||
})
|
||||
const [firesOriginal, setFiresOriginal] = useState<string>("")
|
||||
const [tomtomConfig, setTomtomConfig] = useState<TomtomConfig>({
|
||||
min_magnitude: 4,
|
||||
drop_non_present: true,
|
||||
|
|
@ -457,21 +444,6 @@ export default function Environment() {
|
|||
}
|
||||
} catch { /* adapter-config optional */ }
|
||||
|
||||
// Load adapter-config for fires/digest (array → object fix: line ~367)
|
||||
try {
|
||||
const firesRes = await fetch("/api/adapter-config/fires")
|
||||
if (firesRes.ok) {
|
||||
const firesData = toMap(await firesRes.json())
|
||||
const cfg: FiresConfig = {
|
||||
digest_enabled: (firesData.digest_enabled?.value as boolean) ?? true,
|
||||
digest_schedule: (firesData.digest_schedule?.value as string[]) ?? ["06:00", "18:00"],
|
||||
digest_timezone: (firesData.digest_timezone?.value as string) ?? "America/Boise",
|
||||
}
|
||||
setFiresConfig(cfg)
|
||||
setFiresOriginal(JSON.stringify(cfg))
|
||||
}
|
||||
} catch { /* adapter-config optional */ }
|
||||
|
||||
// Load adapter-config for tomtom_incidents (array → object fix: line ~382)
|
||||
try {
|
||||
const ttRes = await fetch("/api/adapter-config/tomtom_incidents")
|
||||
|
|
@ -658,7 +630,6 @@ export default function Environment() {
|
|||
|
||||
const hasEnvChanges = env !== null && JSON.stringify(env) !== original
|
||||
const hasWfigsChanges = JSON.stringify(wfigsConfig) !== wfigsOriginal
|
||||
const hasFiresChanges = JSON.stringify(firesConfig) !== firesOriginal
|
||||
const hasTomtomChanges = JSON.stringify(tomtomConfig) !== tomtomOriginal
|
||||
const hasRoads511Changes = JSON.stringify(roads511Config) !== roads511Original
|
||||
const hasWzdxChanges = JSON.stringify(wzdxConfig) !== wzdxOriginal
|
||||
|
|
@ -666,7 +637,7 @@ export default function Environment() {
|
|||
const hasAvalancheChanges = JSON.stringify(avalancheConfig) !== avalancheOriginal
|
||||
const hasSwpcChanges = JSON.stringify(swpcConfig) !== swpcOriginal
|
||||
const hasSatpassChanges = JSON.stringify(satpassConfig) !== satpassOriginal
|
||||
const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes || hasWzdxChanges || hasNwsChanges || hasAvalancheChanges || hasSwpcChanges || hasSatpassChanges
|
||||
const hasChanges = hasEnvChanges || hasWfigsChanges || hasTomtomChanges || hasRoads511Changes || hasWzdxChanges || hasNwsChanges || hasAvalancheChanges || hasSwpcChanges || hasSatpassChanges
|
||||
|
||||
|
||||
const saveAdapterConfig = async (adapterName: string, key: string, value: unknown) => {
|
||||
|
|
@ -731,21 +702,6 @@ const save = async () => {
|
|||
setWfigsOriginal(JSON.stringify(wfigsConfig))
|
||||
}
|
||||
|
||||
// Save fires adapter config changes (digest)
|
||||
if (hasFiresChanges) {
|
||||
const orig = JSON.parse(firesOriginal) as FiresConfig
|
||||
if (firesConfig.digest_enabled !== orig.digest_enabled) {
|
||||
await saveAdapterConfig("fires", "digest_enabled", firesConfig.digest_enabled)
|
||||
}
|
||||
if (JSON.stringify(firesConfig.digest_schedule) !== JSON.stringify(orig.digest_schedule)) {
|
||||
await saveAdapterConfig("fires", "digest_schedule", firesConfig.digest_schedule)
|
||||
}
|
||||
if (firesConfig.digest_timezone !== orig.digest_timezone) {
|
||||
await saveAdapterConfig("fires", "digest_timezone", firesConfig.digest_timezone)
|
||||
}
|
||||
setFiresOriginal(JSON.stringify(firesConfig))
|
||||
}
|
||||
|
||||
// Save tomtom adapter config changes
|
||||
if (hasTomtomChanges) {
|
||||
const orig = JSON.parse(tomtomOriginal) as TomtomConfig
|
||||
|
|
@ -863,7 +819,6 @@ const save = async () => {
|
|||
const discard = () => {
|
||||
if (env) setEnv(JSON.parse(original))
|
||||
setWfigsConfig(JSON.parse(wfigsOriginal || JSON.stringify(wfigsConfig)))
|
||||
setFiresConfig(JSON.parse(firesOriginal || JSON.stringify(firesConfig)))
|
||||
setTomtomConfig(JSON.parse(tomtomOriginal || JSON.stringify(tomtomConfig)))
|
||||
setRoads511Config(JSON.parse(roads511Original || JSON.stringify(roads511Config)))
|
||||
setWzdxConfig(JSON.parse(wzdxOriginal || JSON.stringify(wzdxConfig)))
|
||||
|
|
@ -1136,32 +1091,6 @@ const save = async () => {
|
|||
<NumberInput label="Update Cooldown (hours)" value={Math.round(wfigsConfig.cooldown_seconds / 3600)} onChange={(v) => setWfigsConfig({ ...wfigsConfig, cooldown_seconds: v * 3600 })} min={0} helper="Minimum hours between updates for the same fire" />
|
||||
<NumberInput label="Freshness Window (hours)" value={Math.round(wfigsConfig.freshness_seconds / 3600)} onChange={(v) => setWfigsConfig({ ...wfigsConfig, freshness_seconds: v * 3600 })} min={0} helper="0 = always broadcast regardless of event age" />
|
||||
</div>
|
||||
<div className="border-t border-border pt-4 mt-2">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#666] mb-3">Fire Digest</div>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">Enable daily digest</span>
|
||||
<input type="checkbox" checked={firesConfig.digest_enabled}
|
||||
onChange={(e) => setFiresConfig({ ...firesConfig, digest_enabled: e.target.checked })}
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
</label>
|
||||
{firesConfig.digest_enabled && (
|
||||
<div className="mt-3 space-y-3">
|
||||
<ListInput label="Schedule (HH:MM)" value={firesConfig.digest_schedule}
|
||||
onChange={(v) => setFiresConfig({ ...firesConfig, digest_schedule: v })}
|
||||
helper="Digest times in HH:MM format, e.g. 06:00 and 18:00" />
|
||||
<SelectInput label="Timezone" value={firesConfig.digest_timezone}
|
||||
onChange={(v) => setFiresConfig({ ...firesConfig, digest_timezone: v })}
|
||||
options={[
|
||||
{ value: 'America/Boise', label: 'Mountain — America/Boise' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific — America/Los_Angeles' },
|
||||
{ value: 'America/Denver', label: 'Mountain — America/Denver' },
|
||||
{ value: 'America/Chicago', label: 'Central — America/Chicago' },
|
||||
{ value: 'America/New_York', label: 'Eastern — America/New_York' },
|
||||
{ value: 'UTC', label: 'UTC' },
|
||||
]} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'avalanche': return (
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ export default function Reference() {
|
|||
</ul>
|
||||
</TopicSection>
|
||||
|
||||
{/* Fire Tracker (v0.7 fusion: FIRMS + WFIGS + LLM digest) */}
|
||||
{/* Fire Tracker (v0.7 fusion: FIRMS + WFIGS) */}
|
||||
<TopicSection id="fire-tracker" title="Fire Tracker (Fusion)">
|
||||
<p>
|
||||
FIRMS hotspots are fast but noisy; WFIGS incidents are accurate but slow.
|
||||
|
|
@ -402,16 +402,6 @@ export default function Reference() {
|
|||
]}
|
||||
/>
|
||||
|
||||
<SectionHeader>Daily LLM digest</SectionHeader>
|
||||
<p>
|
||||
Twice a day (default 06:00 and 18:00 Mountain Time) the bot runs an LLM
|
||||
summary across every active fire and the last 24 h of growth + spotting
|
||||
events, then broadcasts one terse line to the mesh. Shape:{' '}
|
||||
<span className="text-amber-300">"Fires today: Cache Peak 1,847 ac +200 NE; Twin Peaks 320 ac stable; possible new fire 15 mi from Cache Peak."</span>{' '}
|
||||
Configure the schedule and timezone under <Mono>fires.digest_*</Mono>{' '}
|
||||
keys on the Adapter Config page.
|
||||
</p>
|
||||
|
||||
<SectionHeader>How attribution works</SectionHeader>
|
||||
<p>
|
||||
When a FIRMS hotspot lands, the bot walks every active fire (those not
|
||||
|
|
@ -465,10 +455,6 @@ export default function Reference() {
|
|||
[<Mono>halt_minimum_seconds</Mono>, '43,200 (12 h)', 'Minimum elapsed seconds since the most recent attributed pixel before wildfire_halted can fire.'],
|
||||
[<Mono>spotting_distance_threshold_mi</Mono>, '1.5 mi', 'Distance from prior-pass perimeter that fires wildfire_spotting.'],
|
||||
[<Mono>spotting_cooldown_seconds</Mono>, '3,600 (1 h)', 'Minimum seconds between consecutive spotting broadcasts per fire.'],
|
||||
[<Mono>digest_enabled</Mono>, 'true', 'Master toggle for the twice-daily digest.'],
|
||||
[<Mono>digest_schedule</Mono>, '["06:00","18:00"]', 'Local-time slots for the digest.'],
|
||||
[<Mono>digest_timezone</Mono>, 'America/Boise', 'IANA tz for digest_schedule.'],
|
||||
[<Mono>digest_max_chars</Mono>, '200', 'Hard cap on the digest wire (the LLM is told to fit; the chunker enforces).'],
|
||||
]}
|
||||
/>
|
||||
</TopicSection>
|
||||
|
|
|
|||
|
|
@ -4,17 +4,10 @@ import { fetchConfig as apiFetchConfig, updateConfig as apiUpdateConfig } from '
|
|||
import { useDirty } from '@/context/DirtyContext'
|
||||
import { notifyRestartRequired } from '@/components/RestartBanner'
|
||||
import {
|
||||
Toggle, NumberInput, TimeInput, InfoButton,
|
||||
Toggle, NumberInput, TimeInput,
|
||||
type NotificationsConfig,
|
||||
} from '@/pages/Notifications'
|
||||
|
||||
// Fires adapter config shape (digest settings)
|
||||
interface FiresConfig {
|
||||
digest_enabled: boolean
|
||||
digest_schedule: string[]
|
||||
digest_timezone: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
family?: 'meshtastic' | 'meshcore'
|
||||
}
|
||||
|
|
@ -26,14 +19,6 @@ export default function ScheduledBroadcasts({ family = 'meshtastic' }: Props) {
|
|||
const [notifConfig, setNotifConfig] = useState<NotificationsConfig | null>(null)
|
||||
const [originalNotifConfig, setOriginalNotifConfig] = useState<NotificationsConfig | null>(null)
|
||||
|
||||
// Fires adapter config state
|
||||
const [firesConfig, setFiresConfig] = useState<FiresConfig>({
|
||||
digest_enabled: true,
|
||||
digest_schedule: ['06:00', '18:00'],
|
||||
digest_timezone: 'America/Boise',
|
||||
})
|
||||
const [originalFiresConfig, setOriginalFiresConfig] = useState<string>('')
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
|
@ -49,24 +34,6 @@ export default function ScheduledBroadcasts({ family = 'meshtastic' }: Props) {
|
|||
setNotifConfig(notif)
|
||||
setOriginalNotifConfig(JSON.parse(JSON.stringify(notif)))
|
||||
|
||||
// Load fires adapter config
|
||||
try {
|
||||
const firesRes = await fetch('/api/adapter-config/fires')
|
||||
if (firesRes.ok) {
|
||||
const firesData = await firesRes.json()
|
||||
const fires: FiresConfig = {
|
||||
digest_enabled: firesData.digest_enabled?.value ?? true,
|
||||
digest_schedule: firesData.digest_schedule?.value ?? ['06:00', '18:00'],
|
||||
digest_timezone: firesData.digest_timezone?.value ?? 'America/Boise',
|
||||
}
|
||||
setFiresConfig(fires)
|
||||
setOriginalFiresConfig(JSON.stringify(fires))
|
||||
}
|
||||
} catch {
|
||||
// adapter-config optional — proceed with defaults
|
||||
setOriginalFiresConfig(JSON.stringify(firesConfig))
|
||||
}
|
||||
|
||||
setHasChanges(false)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load config')
|
||||
|
|
@ -83,28 +50,15 @@ export default function ScheduledBroadcasts({ family = 'meshtastic' }: Props) {
|
|||
useEffect(() => {
|
||||
if (notifConfig && originalNotifConfig) {
|
||||
const notifChanged = JSON.stringify(notifConfig) !== JSON.stringify(originalNotifConfig)
|
||||
const firesChanged = JSON.stringify(firesConfig) !== originalFiresConfig
|
||||
setHasChanges(notifChanged || firesChanged)
|
||||
setHasChanges(notifChanged)
|
||||
}
|
||||
}, [notifConfig, originalNotifConfig, firesConfig, originalFiresConfig])
|
||||
}, [notifConfig, originalNotifConfig])
|
||||
|
||||
useEffect(() => {
|
||||
setDirty(hasChanges)
|
||||
return () => setDirty(false)
|
||||
}, [hasChanges, setDirty])
|
||||
|
||||
const saveAdapterKey = async (adapter: string, key: string, value: unknown) => {
|
||||
const res = await fetch(`/api/adapter-config/${adapter}/${key}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ value }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}))
|
||||
throw new Error(err.detail || `Failed to save ${adapter}.${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!notifConfig) return
|
||||
setSaving(true)
|
||||
|
|
@ -116,19 +70,6 @@ export default function ScheduledBroadcasts({ family = 'meshtastic' }: Props) {
|
|||
setOriginalNotifConfig(JSON.parse(JSON.stringify(notifConfig)))
|
||||
if (result.restart_required) notifyRestartRequired([])
|
||||
|
||||
// Save fires adapter config — only changed keys
|
||||
const origFires = originalFiresConfig ? (JSON.parse(originalFiresConfig) as FiresConfig) : null
|
||||
if (!origFires || firesConfig.digest_enabled !== origFires.digest_enabled) {
|
||||
await saveAdapterKey('fires', 'digest_enabled', firesConfig.digest_enabled)
|
||||
}
|
||||
if (!origFires || JSON.stringify(firesConfig.digest_schedule) !== JSON.stringify(origFires.digest_schedule)) {
|
||||
await saveAdapterKey('fires', 'digest_schedule', firesConfig.digest_schedule)
|
||||
}
|
||||
if (!origFires || firesConfig.digest_timezone !== origFires.digest_timezone) {
|
||||
await saveAdapterKey('fires', 'digest_timezone', firesConfig.digest_timezone)
|
||||
}
|
||||
setOriginalFiresConfig(JSON.stringify(firesConfig))
|
||||
|
||||
setHasChanges(false)
|
||||
setDirty(false)
|
||||
setSuccess('Scheduled broadcasts saved successfully')
|
||||
|
|
@ -142,7 +83,6 @@ export default function ScheduledBroadcasts({ family = 'meshtastic' }: Props) {
|
|||
|
||||
const discardChanges = () => {
|
||||
if (originalNotifConfig) setNotifConfig(JSON.parse(JSON.stringify(originalNotifConfig)))
|
||||
if (originalFiresConfig) setFiresConfig(JSON.parse(originalFiresConfig))
|
||||
setHasChanges(false)
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +175,7 @@ export default function ScheduledBroadcasts({ family = 'meshtastic' }: Props) {
|
|||
label="Enable scheduled band-conditions broadcasts"
|
||||
checked={notifConfig.band_conditions_enabled ?? true}
|
||||
onChange={(v) => setNotifConfig({ ...notifConfig, band_conditions_enabled: v })}
|
||||
helper="3x/day HF propagation summary (Day/Night ratings per band group). The daily fire digest (twice-daily LLM summary of active fires + the last 24h of growth/spotting) is configured separately under Adapter Config -> fires.digest_*. See Reference -> Fire Tracker (Fusion) and Reference -> Broadcast Types for the New/Update/Active prefix system."
|
||||
helper="3x/day HF propagation summary (Day/Night ratings per band group). See Reference -> Fire Tracker (Fusion) and Reference -> Broadcast Types for the New/Update/Active prefix system."
|
||||
info="Source priority: (1) recent SWPC readings persisted locally; (2) HamQSL.com fallback; (3) silent skip if both fail. Persistence rows are written either way for an audit trail."
|
||||
/>
|
||||
{(notifConfig.band_conditions_enabled ?? true) && (
|
||||
|
|
@ -274,57 +214,6 @@ export default function ScheduledBroadcasts({ family = 'meshtastic' }: Props) {
|
|||
)}
|
||||
<p className="text-xs text-slate-600">All times are Mountain Time (America/Boise). DST handled automatically.</p>
|
||||
</div>
|
||||
|
||||
{/* Fire Digest */}
|
||||
<div className="bg-bg-card border border-border p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-slate-500 uppercase tracking-wide">
|
||||
Fire Digest
|
||||
<InfoButton info="Twice-daily LLM summary of active fires + last 24h of growth/spotting events. Configured per the fires adapter." />
|
||||
</label>
|
||||
</div>
|
||||
<Toggle
|
||||
label="Enable fire digest broadcasts"
|
||||
checked={firesConfig.digest_enabled}
|
||||
onChange={(v) => setFiresConfig({ ...firesConfig, digest_enabled: v })}
|
||||
helper="Send a twice-daily digest of active fire conditions to the mesh"
|
||||
/>
|
||||
{firesConfig.digest_enabled && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<TimeInput
|
||||
label="Digest Slot 1"
|
||||
value={(firesConfig.digest_schedule ?? ['06:00', '18:00'])[0] || '06:00'}
|
||||
onChange={(v) => {
|
||||
const s = [...(firesConfig.digest_schedule ?? ['06:00', '18:00'])]
|
||||
s[0] = v
|
||||
setFiresConfig({ ...firesConfig, digest_schedule: s })
|
||||
}}
|
||||
helper="Morning digest (default 06:00 MT)"
|
||||
/>
|
||||
<TimeInput
|
||||
label="Digest Slot 2"
|
||||
value={(firesConfig.digest_schedule ?? ['06:00', '18:00'])[1] || '18:00'}
|
||||
onChange={(v) => {
|
||||
const s = [...(firesConfig.digest_schedule ?? ['06:00', '18:00'])]
|
||||
s[1] = v
|
||||
setFiresConfig({ ...firesConfig, digest_schedule: s })
|
||||
}}
|
||||
helper="Evening digest (default 18:00 MT)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs text-slate-500 uppercase tracking-wide">Timezone</label>
|
||||
<input
|
||||
type="text"
|
||||
value={firesConfig.digest_timezone}
|
||||
onChange={(e) => setFiresConfig({ ...firesConfig, digest_timezone: e.target.value })}
|
||||
placeholder="America/Boise"
|
||||
className="w-full px-3 py-2 bg-[#0a0e17] border border-[#1e2a3a] rounded text-sm text-slate-200 focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<p className="text-xs text-slate-600">IANA timezone name (e.g. America/Boise). DST handled automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue