mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14:45 +02:00
Merge branch 'feature/mesh-intelligence'
This commit is contained in:
commit
98e3fcf675
18 changed files with 221 additions and 139 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en" class="dark">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/png" href="/meshai-icon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>MeshAI Dashboard</title>
|
<title>MeshAI Dashboard</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
|
|
||||||
BIN
dashboard-frontend/public/meshai-icon.png
Normal file
BIN
dashboard-frontend/public/meshai-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
dashboard-frontend/public/meshai-logo.png
Normal file
BIN
dashboard-frontend/public/meshai-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 414 KiB |
|
|
@ -93,15 +93,14 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-[220px] flex-shrink-0 bg-bg-card border-r border-border flex flex-col overflow-y-auto">
|
<aside className="w-[220px] flex-shrink-0 bg-bg-card border-r border-border flex flex-col overflow-y-auto">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="p-5 border-b border-border">
|
<div className="bg-[#000000] px-4 py-3 border-b border-border flex flex-col items-center">
|
||||||
<div className="flex items-center gap-3">
|
<img
|
||||||
<div className="w-[3px] h-8 bg-accent flex-shrink-0" />
|
src="/meshai-logo.png"
|
||||||
<div>
|
alt="MeshAI"
|
||||||
<div className="font-sans font-bold text-white text-[15px] leading-tight tracking-tight">MeshAI</div>
|
className="w-[190px] block"
|
||||||
<div className="text-xs font-mono text-[#666]">
|
/>
|
||||||
v{status?.version || '...'}
|
<div className="font-mono text-[10px] text-[#555] mt-1 self-start">
|
||||||
</div>
|
v{status?.version || '...'}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -116,14 +115,14 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
to={item.path}
|
to={item.path}
|
||||||
className={`flex items-center gap-3 px-5 py-3 text-sm font-sans transition-colors relative ${
|
className={`flex items-center gap-3 px-5 py-3 text-sm font-sans transition-colors relative ${
|
||||||
isActive
|
isActive
|
||||||
? 'text-white'
|
? 'text-white bg-transparent'
|
||||||
: 'text-[#777] hover:text-[#888]'
|
: 'text-[#777] hover:text-white hover:bg-bg-hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<div className="absolute right-0 top-0 bottom-0 w-0.5 bg-accent" />
|
<div className="absolute right-0 top-0 bottom-0 w-[2px] bg-[#f59e0b]" />
|
||||||
)}
|
)}
|
||||||
<Icon size={18} />
|
<Icon size={16} />
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,10 @@ function getSeverityStyles(severity: string) {
|
||||||
case 'routine':
|
case 'routine':
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
bg: 'bg-sky-400/10',
|
bg: 'bg-[#f59e0b]/10',
|
||||||
border: 'border-sky-400',
|
border: 'border-[#f59e0b]',
|
||||||
badge: 'bg-sky-400/20 text-sky-400',
|
badge: 'bg-[#f59e0b]/20 text-[#f59e0b]',
|
||||||
iconColor: 'text-sky-400',
|
iconColor: 'text-[#f59e0b]',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +207,7 @@ function AlertHistoryTable({
|
||||||
<select
|
<select
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
onChange={(e) => onTypeFilterChange(e.target.value)}
|
onChange={(e) => onTypeFilterChange(e.target.value)}
|
||||||
className="bg-bg border border-border rounded px-3 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-sky-400"
|
className="bg-bg border border-border rounded px-3 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-[#f59e0b]"
|
||||||
>
|
>
|
||||||
{alertTypes.map((t) => (
|
{alertTypes.map((t) => (
|
||||||
<option key={t} value={t}>
|
<option key={t} value={t}>
|
||||||
|
|
@ -218,7 +218,7 @@ function AlertHistoryTable({
|
||||||
<select
|
<select
|
||||||
value={severityFilter}
|
value={severityFilter}
|
||||||
onChange={(e) => onSeverityFilterChange(e.target.value)}
|
onChange={(e) => onSeverityFilterChange(e.target.value)}
|
||||||
className="bg-bg border border-border rounded px-3 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-sky-400"
|
className="bg-bg border border-border rounded px-3 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-[#f59e0b]"
|
||||||
>
|
>
|
||||||
{severities.map((s) => (
|
{severities.map((s) => (
|
||||||
<option key={s} value={s}>
|
<option key={s} value={s}>
|
||||||
|
|
@ -354,8 +354,8 @@ function SubscriptionCard({ subscription, nodes }: { subscription: Subscription;
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-bg-hover border border-border">
|
<div className="p-4 bg-bg-hover border border-border">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-sky-400/10 flex items-center justify-center">
|
<div className="w-10 h-10 bg-[#f59e0b]/10 flex items-center justify-center">
|
||||||
<Icon size={18} className="text-sky-400" />
|
<Icon size={18} className="text-[#f59e0b]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm text-slate-200 font-medium">
|
<div className="text-sm text-slate-200 font-medium">
|
||||||
|
|
@ -553,7 +553,7 @@ export default function Alerts() {
|
||||||
<div className="text-slate-500 py-4">
|
<div className="text-slate-500 py-4">
|
||||||
<p>No active subscriptions.</p>
|
<p>No active subscriptions.</p>
|
||||||
<p className="text-xs mt-2">
|
<p className="text-xs mt-2">
|
||||||
Manage subscriptions via <code className="text-sky-400">!subscribe</code> on mesh. Broadcasts arrive with one of three prefixes — <strong>New:</strong> (first sight), <strong>Update:</strong> (material change), or <strong>Active:</strong> (clock-driven reminder while the event is still live). See <a href="/reference#broadcast-types" className="text-sky-400 hover:underline">Broadcast Types</a> and <a href="/reference#reminders" className="text-sky-400 hover:underline">Reminder System</a> in Reference.
|
Manage subscriptions via <code className="text-[#f59e0b]">!subscribe</code> on mesh. Broadcasts arrive with one of three prefixes — <strong>New:</strong> (first sight), <strong>Update:</strong> (material change), or <strong>Active:</strong> (clock-driven reminder while the event is still live). See <a href="/reference#broadcast-types" className="text-[#f59e0b] hover:underline">Broadcast Types</a> and <a href="/reference#reminders" className="text-[#f59e0b] hover:underline">Reminder System</a> in Reference.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export default function GaugeSites() {
|
||||||
<h1 className="text-xl font-semibold text-slate-100">Gauge Sites</h1>
|
<h1 className="text-xl font-semibold text-slate-100">Gauge Sites</h1>
|
||||||
<span className="text-xs text-slate-500 ml-2">{rows.length} sites</span>
|
<span className="text-xs text-slate-500 ml-2">{rows.length} sites</span>
|
||||||
<button onClick={beginAdd}
|
<button onClick={beginAdd}
|
||||||
className="ml-auto flex items-center gap-1 px-3 py-1 bg-cyan-700 hover:bg-cyan-600 rounded text-white text-sm">
|
className="ml-auto flex items-center gap-1 px-3 py-1 bg-[#f59e0b] hover:bg-[#d97706] text-black font-sans font-medium text-sm">
|
||||||
<Plus className="w-4 h-4" /> Add site
|
<Plus className="w-4 h-4" /> Add site
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,30 +106,30 @@ export default function GaugeSites() {
|
||||||
|
|
||||||
{adding && <RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} adding feedSource={feedSource} />}
|
{adding && <RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} adding feedSource={feedSource} />}
|
||||||
|
|
||||||
<div className="bg-slate-800/60 border border-slate-700 overflow-x-auto">
|
<div className="bg-bg-card border border-border overflow-x-auto">
|
||||||
<table className="w-full text-sm text-slate-200">
|
<table className="w-full text-sm text-slate-200">
|
||||||
<thead className="bg-slate-900 text-xs text-slate-400 uppercase">
|
<thead className="bg-[#161616] border-b border-border">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 text-left">Site ID</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-left">Site ID</th>
|
||||||
<th className="px-3 py-2 text-left">Name</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-left">Name</th>
|
||||||
<th className="px-3 py-2 text-right">Lat,Lon</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-right">Lat,Lon</th>
|
||||||
<th className="px-3 py-2 text-right">Action</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-right">Action</th>
|
||||||
<th className="px-3 py-2 text-right">Minor</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-right">Minor</th>
|
||||||
<th className="px-3 py-2 text-right">Moderate</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-right">Moderate</th>
|
||||||
<th className="px-3 py-2 text-right">Major</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-right">Major</th>
|
||||||
<th className="px-3 py-2 text-center">On</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-center">On</th>
|
||||||
<th className="px-3 py-2"></th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666]"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-700/60">
|
<tbody className="divide-y divide-border">
|
||||||
{rows.map(r => editing === r.site_id ? (
|
{rows.map(r => editing === r.site_id ? (
|
||||||
<tr key={r.site_id} className="bg-slate-900/40">
|
<tr key={r.site_id} className="bg-bg-card border-b border-border hover:bg-bg-hover">
|
||||||
<td colSpan={9} className="px-3 py-2">
|
<td colSpan={9} className="px-3 py-2">
|
||||||
<RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} feedSource={feedSource} />
|
<RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} feedSource={feedSource} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
<tr key={r.site_id} className="hover:bg-slate-800/50">
|
<tr key={r.site_id} className="hover:bg-bg-hover">
|
||||||
<td className="px-3 py-2 font-mono text-xs">{r.site_id}</td>
|
<td className="px-3 py-2 font-mono text-xs">{r.site_id}</td>
|
||||||
<td className="px-3 py-2">{r.gauge_name}</td>
|
<td className="px-3 py-2">{r.gauge_name}</td>
|
||||||
<td className="px-3 py-2 text-right text-xs">{r.lat.toFixed(3)},{r.lon.toFixed(3)}</td>
|
<td className="px-3 py-2 text-right text-xs">{r.lat.toFixed(3)},{r.lon.toFixed(3)}</td>
|
||||||
|
|
@ -204,15 +204,15 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding, feedSource }: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 p-3 bg-slate-900/50 rounded">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 p-3 bg-[#1a1a1a]">
|
||||||
<label className="text-xs text-slate-400 col-span-2">
|
<label className="text-xs text-slate-400 col-span-2">
|
||||||
Site ID
|
Site ID
|
||||||
<div className="flex items-center gap-1 mt-1">
|
<div className="flex items-center gap-1 mt-1">
|
||||||
<input className="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100 font-mono text-xs"
|
<input className="flex-1 bg-bg border border-border px-2 py-1 text-slate-100 font-mono text-xs"
|
||||||
value={draft.site_id} onChange={e => upd('site_id', e.target.value)} disabled={!adding} />
|
value={draft.site_id} onChange={e => upd('site_id', e.target.value)} disabled={!adding} />
|
||||||
<button type="button" onClick={onLookup} disabled={lookupDisabled || lookupBusy}
|
<button type="button" onClick={onLookup} disabled={lookupDisabled || lookupBusy}
|
||||||
title={lookupTitle}
|
title={lookupTitle}
|
||||||
className="px-2 py-1 bg-slate-700 hover:bg-slate-600 disabled:opacity-30 disabled:cursor-not-allowed rounded text-xs text-slate-100 flex items-center gap-1">
|
className="px-2 py-1 bg-bg-hover hover:bg-[#333] disabled:opacity-30 disabled:cursor-not-allowed text-xs text-slate-100 flex items-center gap-1">
|
||||||
{lookupBusy ? <Loader2 className="w-3 h-3 animate-spin" /> : <Search className="w-3 h-3" />}
|
{lookupBusy ? <Loader2 className="w-3 h-3 animate-spin" /> : <Search className="w-3 h-3" />}
|
||||||
USGS lookup
|
USGS lookup
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -221,31 +221,31 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding, feedSource }: {
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400 col-span-2">
|
<label className="text-xs text-slate-400 col-span-2">
|
||||||
Gauge name
|
Gauge name
|
||||||
<input className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.gauge_name} onChange={e => upd('gauge_name', e.target.value)} />
|
value={draft.gauge_name} onChange={e => upd('gauge_name', e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Lat
|
<label className="text-xs text-slate-400">Lat
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.lat} onChange={e => upd('lat', parseFloat(e.target.value))} />
|
value={draft.lat} onChange={e => upd('lat', parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Lon
|
<label className="text-xs text-slate-400">Lon
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.lon} onChange={e => upd('lon', parseFloat(e.target.value))} />
|
value={draft.lon} onChange={e => upd('lon', parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Action ft
|
<label className="text-xs text-slate-400">Action ft
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.action_ft ?? ''} onChange={e => upd('action_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
value={draft.action_ft ?? ''} onChange={e => upd('action_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Minor flood ft
|
<label className="text-xs text-slate-400">Minor flood ft
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.flood_minor_ft ?? ''} onChange={e => upd('flood_minor_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
value={draft.flood_minor_ft ?? ''} onChange={e => upd('flood_minor_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Moderate flood ft
|
<label className="text-xs text-slate-400">Moderate flood ft
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.flood_moderate_ft ?? ''} onChange={e => upd('flood_moderate_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
value={draft.flood_moderate_ft ?? ''} onChange={e => upd('flood_moderate_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Major flood ft
|
<label className="text-xs text-slate-400">Major flood ft
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.flood_major_ft ?? ''} onChange={e => upd('flood_major_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
value={draft.flood_major_ft ?? ''} onChange={e => upd('flood_major_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-300 col-span-2 flex items-center gap-2 mt-2">
|
<label className="text-xs text-slate-300 col-span-2 flex items-center gap-2 mt-2">
|
||||||
|
|
@ -253,8 +253,8 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding, feedSource }: {
|
||||||
Enabled
|
Enabled
|
||||||
</label>
|
</label>
|
||||||
<div className="col-span-2 flex items-center justify-end gap-2 mt-2">
|
<div className="col-span-2 flex items-center justify-end gap-2 mt-2">
|
||||||
<button onClick={onCancel} className="px-3 py-1 text-slate-300 hover:bg-slate-700 rounded text-sm">Cancel</button>
|
<button onClick={onCancel} className="px-3 py-1 text-slate-300 hover:bg-bg-hover text-sm">Cancel</button>
|
||||||
<button onClick={onSave} className="px-3 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-sm">Save</button>
|
<button onClick={onSave} className="px-3 py-1 bg-[#f59e0b] hover:bg-[#d97706] text-black font-sans font-medium text-sm">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -929,7 +929,7 @@ function NotificationRuleCard({
|
||||||
title={rule.enabled ? 'Enabled' : 'Disabled'}
|
title={rule.enabled ? 'Enabled' : 'Disabled'}
|
||||||
/>
|
/>
|
||||||
{rule.trigger_type === 'schedule' ? (
|
{rule.trigger_type === 'schedule' ? (
|
||||||
<Clock size={14} className="text-sky-400 flex-shrink-0" />
|
<Clock size={14} className="text-[#f59e0b] flex-shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<Zap size={14} className="text-yellow-400 flex-shrink-0" />
|
<Zap size={14} className="text-yellow-400 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -976,7 +976,7 @@ function NotificationRuleCard({
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleTest() }}
|
onClick={(e) => { e.stopPropagation(); handleTest() }}
|
||||||
disabled={testing || !rule.name}
|
disabled={testing || !rule.name}
|
||||||
className="p-1.5 text-sky-400 hover:text-sky-300 hover:bg-sky-400/10 rounded disabled:opacity-50"
|
className="p-1.5 text-[#f59e0b] hover:text-[#d97706] hover:bg-[#f59e0b]/10 rounded disabled:opacity-50"
|
||||||
title="Test rule"
|
title="Test rule"
|
||||||
>
|
>
|
||||||
<Send size={14} />
|
<Send size={14} />
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export default function TownAnchors() {
|
||||||
<h1 className="text-xl font-semibold text-slate-100">Town Anchors</h1>
|
<h1 className="text-xl font-semibold text-slate-100">Town Anchors</h1>
|
||||||
<span className="text-xs text-slate-500 ml-2">{rows.length} towns</span>
|
<span className="text-xs text-slate-500 ml-2">{rows.length} towns</span>
|
||||||
<button onClick={beginAdd}
|
<button onClick={beginAdd}
|
||||||
className="ml-auto flex items-center gap-1 px-3 py-1 bg-cyan-700 hover:bg-cyan-600 rounded text-white text-sm">
|
className="ml-auto flex items-center gap-1 px-3 py-1 bg-[#f59e0b] hover:bg-[#d97706] text-black font-sans font-medium text-sm">
|
||||||
<Plus className="w-4 h-4" /> Add town
|
<Plus className="w-4 h-4" /> Add town
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -82,25 +82,25 @@ export default function TownAnchors() {
|
||||||
|
|
||||||
{adding && <RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} adding />}
|
{adding && <RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} adding />}
|
||||||
|
|
||||||
<div className="bg-slate-800/60 border border-slate-700 overflow-x-auto">
|
<div className="bg-bg-card border border-border overflow-x-auto">
|
||||||
<table className="w-full text-sm text-slate-200">
|
<table className="w-full text-sm text-slate-200">
|
||||||
<thead className="bg-slate-900 text-xs text-slate-400 uppercase">
|
<thead className="bg-[#161616] border-b border-border">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 text-left">Name</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-left">Name</th>
|
||||||
<th className="px-3 py-2 text-right">Lat</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-right">Lat</th>
|
||||||
<th className="px-3 py-2 text-right">Lon</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-right">Lon</th>
|
||||||
<th className="px-3 py-2 text-center">State</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-center">State</th>
|
||||||
<th className="px-3 py-2 text-center">On</th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666] text-center">On</th>
|
||||||
<th className="px-3 py-2"></th>
|
<th className="px-3 py-2 font-sans text-[9px] uppercase tracking-widest text-[#666]"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-700/60">
|
<tbody className="divide-y divide-border">
|
||||||
{rows.map(r => editing === r.anchor_id ? (
|
{rows.map(r => editing === r.anchor_id ? (
|
||||||
<tr key={r.anchor_id} className="bg-slate-900/40">
|
<tr key={r.anchor_id} className="bg-bg-card border-b border-border hover:bg-bg-hover">
|
||||||
<td colSpan={6} className="px-3 py-2"><RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} /></td>
|
<td colSpan={6} className="px-3 py-2"><RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} /></td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
<tr key={r.anchor_id} className="hover:bg-slate-800/50">
|
<tr key={r.anchor_id} className="hover:bg-bg-hover">
|
||||||
<td className="px-3 py-2 capitalize">{r.name}</td>
|
<td className="px-3 py-2 capitalize">{r.name}</td>
|
||||||
<td className="px-3 py-2 text-right text-xs">{r.lat.toFixed(4)}</td>
|
<td className="px-3 py-2 text-right text-xs">{r.lat.toFixed(4)}</td>
|
||||||
<td className="px-3 py-2 text-right text-xs">{r.lon.toFixed(4)}</td>
|
<td className="px-3 py-2 text-right text-xs">{r.lon.toFixed(4)}</td>
|
||||||
|
|
@ -126,13 +126,13 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding }: {
|
||||||
}) {
|
}) {
|
||||||
const upd = (k: keyof TownAnchor, v: unknown) => setDraft({ ...draft, [k]: v })
|
const upd = (k: keyof TownAnchor, v: unknown) => setDraft({ ...draft, [k]: v })
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 p-3 bg-slate-900/50 rounded">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 p-3 bg-[#1a1a1a]">
|
||||||
<label className="text-xs text-slate-400 col-span-2">Name (lowercased on save)
|
<label className="text-xs text-slate-400 col-span-2">Name (lowercased on save)
|
||||||
<input className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.name} onChange={e => upd('name', e.target.value)} disabled={!adding} />
|
value={draft.name} onChange={e => upd('name', e.target.value)} disabled={!adding} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">State
|
<label className="text-xs text-slate-400">State
|
||||||
<input className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.state ?? ''} onChange={e => upd('state', e.target.value)} />
|
value={draft.state ?? ''} onChange={e => upd('state', e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400 flex items-center gap-2">
|
<label className="text-xs text-slate-400 flex items-center gap-2">
|
||||||
|
|
@ -140,16 +140,16 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding }: {
|
||||||
Enabled
|
Enabled
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Lat
|
<label className="text-xs text-slate-400">Lat
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.lat} onChange={e => upd('lat', parseFloat(e.target.value))} />
|
value={draft.lat} onChange={e => upd('lat', parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<label className="text-xs text-slate-400">Lon
|
<label className="text-xs text-slate-400">Lon
|
||||||
<input type="number" step="any" className="block w-full mt-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-100"
|
<input type="number" step="any" className="block w-full mt-1 bg-bg border border-border px-2 py-1 text-slate-100"
|
||||||
value={draft.lon} onChange={e => upd('lon', parseFloat(e.target.value))} />
|
value={draft.lon} onChange={e => upd('lon', parseFloat(e.target.value))} />
|
||||||
</label>
|
</label>
|
||||||
<div className="col-span-2 flex items-center justify-end gap-2 mt-2">
|
<div className="col-span-2 flex items-center justify-end gap-2 mt-2">
|
||||||
<button onClick={onCancel} className="px-3 py-1 text-slate-300 hover:bg-slate-700 rounded text-sm">Cancel</button>
|
<button onClick={onCancel} className="px-3 py-1 text-slate-300 hover:bg-bg-hover text-sm">Cancel</button>
|
||||||
<button onClick={onSave} className="px-3 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-sm">Save</button>
|
<button onClick={onSave} className="px-3 py-1 bg-[#f59e0b] hover:bg-[#d97706] text-black font-sans font-medium text-sm">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import urllib.request
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from meshai.adapter_config import adapter_config
|
# Geocoder config is set via init_geocoder_config()
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -248,13 +248,31 @@ def _is_uninformative_road(road: Optional[str]) -> bool:
|
||||||
# 2026-06-04). It's the same Echo6-local Photon instance that backs Central's
|
# 2026-06-04). It's the same Echo6-local Photon instance that backs Central's
|
||||||
# NaviBackend reverse-geocoder. Photon takes osm_tag=place (KEY only, not
|
# NaviBackend reverse-geocoder. Photon takes osm_tag=place (KEY only, not
|
||||||
# key:value with comma-list -- that returns 0 features -- per probe).
|
# key:value with comma-list -- that returns 0 features -- per probe).
|
||||||
# v0.6-3b: photon endpoint settings live in adapter_config.geocoder.
|
# v0.6-3b: photon geocoder config - initialized via init_geocoder_config()
|
||||||
# Module-level names retained as backward-compat aliases so existing
|
# Defaults to public Komoot Photon; deployments override in config.yaml.
|
||||||
# test imports / monkeypatches still resolve.
|
|
||||||
PHOTON_BASE_URL = "http://100.64.0.24:2322"
|
class _GeocoderSettings:
|
||||||
PHOTON_TIMEOUT_S = 2.0
|
url: str = "https://photon.komoot.io"
|
||||||
PHOTON_RADIUS_KM = 80 # ≈ 50 miles
|
timeout_seconds: float = 2.0
|
||||||
PHOTON_LIMIT = 10
|
radius_km: float = 80.0
|
||||||
|
limit: int = 10
|
||||||
|
|
||||||
|
_geocoder = _GeocoderSettings()
|
||||||
|
|
||||||
|
|
||||||
|
def init_geocoder_config(url: str = None, timeout: float = None,
|
||||||
|
radius: float = None, limit: int = None) -> None:
|
||||||
|
"""Initialize geocoder settings from config.yaml values."""
|
||||||
|
if url is not None:
|
||||||
|
_geocoder.url = url
|
||||||
|
if timeout is not None:
|
||||||
|
_geocoder.timeout_seconds = timeout
|
||||||
|
if radius is not None:
|
||||||
|
_geocoder.radius_km = radius
|
||||||
|
if limit is not None:
|
||||||
|
_geocoder.limit = limit
|
||||||
|
|
||||||
|
|
||||||
# OSM place classes we accept as "town". Suburb included for metro coverage;
|
# OSM place classes we accept as "town". Suburb included for metro coverage;
|
||||||
# locality is rare but valid for tiny rural places.
|
# locality is rare but valid for tiny rural places.
|
||||||
_TOWN_OSM_VALUES = frozenset({"city", "town", "village"})
|
_TOWN_OSM_VALUES = frozenset({"city", "town", "village"})
|
||||||
|
|
@ -282,13 +300,13 @@ def _photon_reverse_places(lat: float, lon: float) -> list[dict]:
|
||||||
qs = urllib.parse.urlencode({
|
qs = urllib.parse.urlencode({
|
||||||
"lat": f"{lat:.6f}",
|
"lat": f"{lat:.6f}",
|
||||||
"lon": f"{lon:.6f}",
|
"lon": f"{lon:.6f}",
|
||||||
"radius": PHOTON_RADIUS_KM,
|
"radius": _geocoder.radius_km,
|
||||||
"osm_tag": "place",
|
"osm_tag": "place",
|
||||||
"limit": PHOTON_LIMIT,
|
"limit": _geocoder.limit,
|
||||||
})
|
})
|
||||||
url = f"{PHOTON_BASE_URL}/reverse?{qs}"
|
url = f"{_geocoder.url}/reverse?{qs}"
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(url, timeout=PHOTON_TIMEOUT_S) as resp:
|
with urllib.request.urlopen(url, timeout=_geocoder.timeout_seconds) as resp:
|
||||||
body = resp.read()
|
body = resp.read()
|
||||||
d = json.loads(body)
|
d = json.loads(body)
|
||||||
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError,
|
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError,
|
||||||
|
|
@ -308,7 +326,7 @@ def nearest_town(lat: float, lon: float, max_distance_mi: float = 50.0) -> Optio
|
||||||
event is N of the town. Returns None if no town within range or if
|
event is N of the town. Returns None if no town within range or if
|
||||||
Photon is unreachable.
|
Photon is unreachable.
|
||||||
|
|
||||||
Calls Photon /reverse?osm_tag=place at PHOTON_BASE_URL. Results are
|
Calls Photon /reverse?osm_tag=place at _geocoder.url. Results are
|
||||||
H3-cell-cached (resolution 7 ≈ 5 km cells) so the second event near
|
H3-cell-cached (resolution 7 ≈ 5 km cells) so the second event near
|
||||||
the same town is free.
|
the same town is free.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -468,6 +468,16 @@ class CentralConsumerConfig:
|
||||||
region: str = "us.id"
|
region: str = "us.id"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GeocoderConfig:
|
||||||
|
"""Photon reverse geocoder settings."""
|
||||||
|
|
||||||
|
url: str = "https://photon.komoot.io"
|
||||||
|
timeout_seconds: float = 2.0
|
||||||
|
radius_km: float = 80.0
|
||||||
|
limit: int = 10
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnvironmentalConfig:
|
class EnvironmentalConfig:
|
||||||
"""Environmental feeds settings."""
|
"""Environmental feeds settings."""
|
||||||
|
|
@ -486,6 +496,7 @@ class EnvironmentalConfig:
|
||||||
wzdx: WZDxConfig = field(default_factory=WZDxConfig)
|
wzdx: WZDxConfig = field(default_factory=WZDxConfig)
|
||||||
firms: FIRMSConfig = field(default_factory=FIRMSConfig)
|
firms: FIRMSConfig = field(default_factory=FIRMSConfig)
|
||||||
central: CentralConsumerConfig = field(default_factory=CentralConsumerConfig)
|
central: CentralConsumerConfig = field(default_factory=CentralConsumerConfig)
|
||||||
|
geocoder: GeocoderConfig = field(default_factory=GeocoderConfig)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
1
meshai/dashboard/static/assets/index-BC90GDxp.css
Normal file
1
meshai/dashboard/static/assets/index-BC90GDxp.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
|
|
@ -2,14 +2,14 @@
|
||||||
<html lang="en" class="dark">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/png" href="/meshai-icon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>MeshAI Dashboard</title>
|
<title>MeshAI Dashboard</title>
|
||||||
<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-ChPg5oDu.js"></script>
|
<script type="module" crossorigin src="/assets/index-D5IfmtDv.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-UYhE7jnf.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BC90GDxp.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
BIN
meshai/dashboard/static/meshai-icon.png
Normal file
BIN
meshai/dashboard/static/meshai-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
meshai/dashboard/static/meshai-logo.png
Normal file
BIN
meshai/dashboard/static/meshai-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 414 KiB |
120
meshai/env/store.py
vendored
120
meshai/env/store.py
vendored
|
|
@ -21,6 +21,7 @@ class EnvironmentalStore:
|
||||||
event_bus: Optional["EventBus"] = None,
|
event_bus: Optional["EventBus"] = None,
|
||||||
):
|
):
|
||||||
self._adapters = {} # name -> adapter instance
|
self._adapters = {} # name -> adapter instance
|
||||||
|
self._failed_adapters = {} # name -> last_error string
|
||||||
self._events = {} # (source, event_id) -> event dict
|
self._events = {} # (source, event_id) -> event dict
|
||||||
self._event_bus = event_bus # Pipeline EventBus for emission
|
self._event_bus = event_bus # Pipeline EventBus for emission
|
||||||
self._swpc_status = {} # Kp/SFI/scales snapshot
|
self._swpc_status = {} # Kp/SFI/scales snapshot
|
||||||
|
|
@ -28,56 +29,60 @@ class EnvironmentalStore:
|
||||||
self._mesh_zones = config.nws_zones or []
|
self._mesh_zones = config.nws_zones or []
|
||||||
self._region_anchors = region_anchors or []
|
self._region_anchors = region_anchors or []
|
||||||
|
|
||||||
# Create adapter instances based on config
|
# Create adapter instances with error isolation
|
||||||
if config.nws.enabled and config.nws.feed_source == "native":
|
self._register_adapter("nws", config.nws, ".nws", "NWSAlertsAdapter",
|
||||||
from .nws import NWSAlertsAdapter
|
lambda cfg: (cfg,))
|
||||||
self._adapters["nws"] = NWSAlertsAdapter(config.nws)
|
self._register_adapter("swpc", config.swpc, ".swpc", "SWPCAdapter",
|
||||||
|
lambda cfg: (cfg,))
|
||||||
if config.swpc.enabled and config.swpc.feed_source == "native":
|
self._register_adapter("ducting", config.ducting, ".ducting", "DuctingAdapter",
|
||||||
from .swpc import SWPCAdapter
|
lambda cfg: (cfg,))
|
||||||
self._adapters["swpc"] = SWPCAdapter(config.swpc)
|
self._register_adapter("nifc", config.fires, ".fires", "NICFFiresAdapter",
|
||||||
|
lambda cfg: (cfg, self._region_anchors))
|
||||||
if config.ducting.enabled and config.ducting.feed_source == "native":
|
self._register_adapter("avalanche", config.avalanche, ".avalanche", "AvalancheAdapter",
|
||||||
from .ducting import DuctingAdapter
|
lambda cfg: (cfg,))
|
||||||
self._adapters["ducting"] = DuctingAdapter(config.ducting)
|
self._register_adapter("usgs", config.usgs, ".usgs", "USGSStreamsAdapter",
|
||||||
|
lambda cfg: (cfg,))
|
||||||
if config.fires.enabled and config.fires.feed_source == "native":
|
self._register_adapter("usgs_quake", config.usgs_quake, ".usgs_quake", "USGSQuakeAdapter",
|
||||||
from .fires import NICFFiresAdapter
|
lambda cfg: (cfg,))
|
||||||
self._adapters["nifc"] = NICFFiresAdapter(config.fires, self._region_anchors)
|
self._register_adapter("traffic", config.traffic, ".traffic", "TomTomTrafficAdapter",
|
||||||
|
lambda cfg: (cfg,))
|
||||||
if config.avalanche.enabled and config.avalanche.feed_source == "native":
|
self._register_adapter("roads511", config.roads511, ".roads511", "Roads511Adapter",
|
||||||
from .avalanche import AvalancheAdapter
|
lambda cfg: (cfg,))
|
||||||
self._adapters["avalanche"] = AvalancheAdapter(config.avalanche)
|
|
||||||
|
|
||||||
if config.usgs.enabled and config.usgs.feed_source == "native":
|
|
||||||
from .usgs import USGSStreamsAdapter
|
|
||||||
self._adapters["usgs"] = USGSStreamsAdapter(config.usgs)
|
|
||||||
|
|
||||||
if config.usgs_quake.enabled and config.usgs_quake.feed_source == "native":
|
|
||||||
from .usgs_quake import USGSQuakeAdapter
|
|
||||||
self._adapters["usgs_quake"] = USGSQuakeAdapter(config.usgs_quake)
|
|
||||||
|
|
||||||
if config.traffic.enabled and config.traffic.feed_source == "native":
|
|
||||||
from .traffic import TomTomTrafficAdapter
|
|
||||||
self._adapters["traffic"] = TomTomTrafficAdapter(config.traffic)
|
|
||||||
|
|
||||||
if config.roads511.enabled and config.roads511.feed_source == "native":
|
|
||||||
from .roads511 import Roads511Adapter
|
|
||||||
self._adapters["roads511"] = Roads511Adapter(config.roads511)
|
|
||||||
|
|
||||||
# FIRMS needs reference to NIFC adapter for cross-referencing
|
# FIRMS needs reference to NIFC adapter for cross-referencing
|
||||||
if config.firms.enabled and config.firms.feed_source == "native":
|
if config.firms.enabled and config.firms.feed_source == "native":
|
||||||
from .firms import FIRMSAdapter
|
try:
|
||||||
fires_adapter = self._adapters.get("nifc")
|
from .firms import FIRMSAdapter
|
||||||
self._firms = FIRMSAdapter(config.firms, self._region_anchors, fires_adapter)
|
fires_adapter = self._adapters.get("nifc")
|
||||||
self._adapters["firms"] = self._firms
|
self._firms = FIRMSAdapter(config.firms, self._region_anchors, fires_adapter)
|
||||||
|
self._adapters["firms"] = self._firms
|
||||||
|
except Exception as e:
|
||||||
|
err_msg = f"{type(e).__name__}: {e}"
|
||||||
|
logger.warning("Failed to initialize firms adapter: %s", err_msg)
|
||||||
|
self._failed_adapters["firms"] = err_msg
|
||||||
|
|
||||||
_central = [n for n in ("nws", "swpc", "ducting", "fires", "avalanche", "usgs", "usgs_quake", "traffic", "roads511", "firms")
|
_central = [n for n in ("nws", "swpc", "ducting", "fires", "avalanche", "usgs", "usgs_quake", "traffic", "roads511", "firms")
|
||||||
if getattr(getattr(config, n, None), "feed_source", "native") == "central"]
|
if getattr(getattr(config, n, None), "feed_source", "native") == "central"]
|
||||||
if _central:
|
if _central:
|
||||||
logger.debug("Adapters sourced from Central (native skipped): %s", _central)
|
logger.debug("Adapters sourced from Central (native skipped): %s", _central)
|
||||||
|
if self._failed_adapters:
|
||||||
|
logger.warning("Failed adapters: %s", list(self._failed_adapters.keys()))
|
||||||
logger.info(f"EnvironmentalStore initialized with {len(self._adapters)} adapters")
|
logger.info(f"EnvironmentalStore initialized with {len(self._adapters)} adapters")
|
||||||
|
|
||||||
|
|
||||||
|
def _register_adapter(self, name: str, cfg, module_path: str, class_name: str, args_fn):
|
||||||
|
"""Register a single adapter with error isolation."""
|
||||||
|
if not cfg.enabled or cfg.feed_source != "native":
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
module = __import__(f"meshai.env{module_path}", fromlist=[class_name])
|
||||||
|
cls = getattr(module, class_name)
|
||||||
|
self._adapters[name] = cls(*args_fn(cfg))
|
||||||
|
except Exception as e:
|
||||||
|
err_msg = f"{type(e).__name__}: {e}"
|
||||||
|
logger.warning("Failed to initialize %s adapter: %s", name, err_msg)
|
||||||
|
self._failed_adapters[name] = err_msg
|
||||||
|
|
||||||
def refresh(self) -> bool:
|
def refresh(self) -> bool:
|
||||||
"""Called every second from main loop. Ticks each adapter.
|
"""Called every second from main loop. Ticks each adapter.
|
||||||
|
|
||||||
|
|
@ -302,6 +307,41 @@ class EnvironmentalStore:
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def get_status(self) -> list:
|
||||||
|
"""Get status of all adapters including failed ones."""
|
||||||
|
status = []
|
||||||
|
for name, adapter in self._adapters.items():
|
||||||
|
try:
|
||||||
|
hs = adapter.health_status
|
||||||
|
status.append({
|
||||||
|
"source": name,
|
||||||
|
"is_loaded": True,
|
||||||
|
"last_error": hs.get("last_error"),
|
||||||
|
"consecutive_errors": hs.get("consecutive_errors", 0),
|
||||||
|
"event_count": hs.get("event_count", 0),
|
||||||
|
"last_fetch": hs.get("last_fetch"),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
status.append({
|
||||||
|
"source": name,
|
||||||
|
"is_loaded": True,
|
||||||
|
"last_error": None,
|
||||||
|
"consecutive_errors": 0,
|
||||||
|
"event_count": 0,
|
||||||
|
"last_fetch": None,
|
||||||
|
})
|
||||||
|
for name, error in self._failed_adapters.items():
|
||||||
|
status.append({
|
||||||
|
"source": name,
|
||||||
|
"is_loaded": False,
|
||||||
|
"last_error": error,
|
||||||
|
"consecutive_errors": 0,
|
||||||
|
"event_count": 0,
|
||||||
|
"last_fetch": None,
|
||||||
|
})
|
||||||
|
return status
|
||||||
|
|
||||||
def get_source_health(self) -> list:
|
def get_source_health(self) -> list:
|
||||||
"""Get health status for all adapters."""
|
"""Get health status for all adapters."""
|
||||||
return [a.health_status for a in self._adapters.values()]
|
return [a.health_status for a in self._adapters.values()]
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from .commands.status import set_start_time
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .config_loader import load_config, get_config_dir_from_path
|
from .config_loader import load_config, get_config_dir_from_path
|
||||||
from .connector import MeshConnector, MeshMessage
|
from .connector import MeshConnector, MeshMessage
|
||||||
|
from .central_normalizer import init_geocoder_config
|
||||||
from .context import MeshContext
|
from .context import MeshContext
|
||||||
from .history import ConversationHistory
|
from .history import ConversationHistory
|
||||||
from .memory import ConversationSummary
|
from .memory import ConversationSummary
|
||||||
|
|
@ -245,6 +246,19 @@ class MeshAI:
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("persistence init_db failed at startup")
|
logger.exception("persistence init_db failed at startup")
|
||||||
|
|
||||||
|
# v0.6-3b: Initialize geocoder config from config.yaml
|
||||||
|
try:
|
||||||
|
gc = self.config.environmental.geocoder
|
||||||
|
init_geocoder_config(
|
||||||
|
url=gc.url,
|
||||||
|
timeout=gc.timeout_seconds,
|
||||||
|
radius=gc.radius_km,
|
||||||
|
limit=gc.limit
|
||||||
|
)
|
||||||
|
logger.info("Geocoder configured: %s", gc.url)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("geocoder init failed - using defaults")
|
||||||
|
|
||||||
# Conversation history
|
# Conversation history
|
||||||
self.history = ConversationHistory(self.config.history)
|
self.history = ConversationHistory(self.config.history)
|
||||||
await self.history.initialize()
|
await self.history.initialize()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue