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
7de460804f
19 changed files with 1472 additions and 1472 deletions
|
|
@ -131,7 +131,7 @@ export default function ChannelPicker(props: ChannelPickerProps) {
|
|||
return (
|
||||
<div className="space-y-1">
|
||||
<label className="block text-xs text-slate-500 uppercase tracking-wide">{label}</label>
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-2 space-y-1">
|
||||
<div className="border border-[#1e2a3a] p-2 space-y-1">
|
||||
{enabledChannels.map((ch) => (
|
||||
<label
|
||||
key={ch.index}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ export default function GeoMap({
|
|||
}, [selectedNodeId, edges])
|
||||
|
||||
return (
|
||||
<div className="relative bg-bg-card rounded-lg border border-border overflow-hidden">
|
||||
<div className="relative bg-bg-card border border-border overflow-hidden">
|
||||
<MapContainer
|
||||
center={defaultCenter}
|
||||
zoom={7}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||
<div className="flex items-center gap-3">
|
||||
<div className="w-[3px] h-8 bg-accent flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-sans font-bold text-white text-lg">MeshAI</div>
|
||||
<div className="font-sans font-bold text-white text-[15px] leading-tight tracking-tight">MeshAI</div>
|
||||
<div className="text-xs font-mono text-[#333]">
|
||||
v{status?.version || '...'}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export default function NodeDetail({
|
|||
<div className="p-4 border-b border-border grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-0.5">Role</div>
|
||||
<div className={`text-sm font-medium ${isInfra ? 'text-cyan-400' : 'text-slate-300'}`}>
|
||||
<div className={`text-sm font-medium ${isInfra ? 'text-accent' : 'text-slate-300'}`}>
|
||||
{node.role}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -187,7 +187,7 @@ export default function NodeDetail({
|
|||
href={`https://www.google.com/maps?q=${node.latitude},${node.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300"
|
||||
className="flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300"
|
||||
>
|
||||
<ExternalLink size={10} />
|
||||
Google Maps
|
||||
|
|
@ -196,7 +196,7 @@ export default function NodeDetail({
|
|||
href={`https://www.openstreetmap.org/?mlat=${node.latitude}&mlon=${node.longitude}&zoom=14`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300"
|
||||
className="flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300"
|
||||
>
|
||||
<ExternalLink size={10} />
|
||||
OSM
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export default function NodePicker({
|
|||
{isOpen && !loading && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
||||
<div className="absolute left-0 right-0 top-full mt-1 z-50 max-h-64 overflow-y-auto bg-[#0a0e17] border border-[#1e2a3a] rounded-lg shadow-xl">
|
||||
<div className="absolute left-0 right-0 top-full mt-1 z-50 max-h-64 overflow-y-auto bg-[#0a0e17] border border-[#1e2a3a] shadow-xl">
|
||||
{filteredNodes.length === 0 ? (
|
||||
<div className="p-3 text-sm text-slate-500 text-center">
|
||||
No nodes found
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export default function NodeTable({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="bg-bg-card border border-border rounded-lg overflow-hidden">
|
||||
<div className="bg-bg-card border border-border overflow-hidden">
|
||||
{/* Filter bar */}
|
||||
<div className="p-3 border-b border-border flex items-center gap-3">
|
||||
{/* Search */}
|
||||
|
|
@ -254,7 +254,7 @@ export default function NodeTable({
|
|||
<span
|
||||
className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${
|
||||
isInfra
|
||||
? 'bg-cyan-500/20 text-cyan-400'
|
||||
? 'bg-cyan-500/20 text-accent'
|
||||
: 'bg-slate-500/20 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ function getSeverityStyles(severity: string) {
|
|||
}
|
||||
default:
|
||||
return {
|
||||
bg: 'bg-blue-500/10',
|
||||
border: 'border-blue-500',
|
||||
bg: 'bg-sky-400/10',
|
||||
border: 'border-sky-400',
|
||||
icon: Info,
|
||||
iconColor: 'text-blue-500',
|
||||
iconColor: 'text-sky-400',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ function ToastItem({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.bg} border ${styles.border} rounded-lg shadow-lg overflow-hidden animate-slide-in cursor-pointer`}
|
||||
className={`${styles.bg} border ${styles.border} shadow-lg overflow-hidden animate-slide-in cursor-pointer`}
|
||||
onClick={onNavigate}
|
||||
role="alert"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ export default function TopologyGraph({
|
|||
}, [option])
|
||||
|
||||
return (
|
||||
<div className="relative bg-bg-card rounded-lg border border-border overflow-hidden">
|
||||
<div className="relative bg-bg-card border border-border overflow-hidden">
|
||||
<ReactECharts
|
||||
ref={chartRef}
|
||||
option={option}
|
||||
|
|
@ -302,11 +302,11 @@ export default function TopologyGraph({
|
|||
<div className="text-xs text-slate-400 font-medium mb-2">Node Type</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-sky-400" />
|
||||
<span className="text-xs text-slate-500">Infrastructure</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-900 border-2 border-blue-500" />
|
||||
<div className="w-3 h-3 rounded-full bg-gray-900 border-2 border-sky-400" />
|
||||
<span className="text-xs text-slate-500">Client</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ export default function AdapterConfig() {
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6 flex items-center gap-2 text-slate-400">
|
||||
<div className="p-6 flex items-center gap-2 text-[#444]">
|
||||
<Loader2 className="w-5 h-5 animate-spin" /> Loading adapter config…
|
||||
</div>
|
||||
)
|
||||
|
|
@ -179,14 +179,14 @@ export default function AdapterConfig() {
|
|||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center gap-2 text-slate-200">
|
||||
<div className="flex items-center gap-2 text-white">
|
||||
<Sliders className="w-5 h-5" />
|
||||
<h1 className="text-xl font-semibold">Adapter Config</h1>
|
||||
<span className="text-xs text-slate-500 ml-2">
|
||||
<span className="text-xs text-[#333] ml-2">
|
||||
{Object.values(config).reduce((n, l) => n + l.length, 0)} settings across {allAdapters.length} adapters
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 max-w-3xl">
|
||||
<p className="text-xs text-[#444] max-w-3xl">
|
||||
Per-adapter tunables (thresholds, freshness windows, toggles, curation lists).
|
||||
Changes take effect on the next handler call -- no container restart needed.
|
||||
Sentence templates, emoji, and translation maps live in code by design — see the CODE rule under <a href="/reference#adapter-config" className="text-accent hover:underline">Adapter Config & the CODE Rule</a> in Reference. The <strong>LLM context</strong> toggle on each card gates whether that adapter's data lands in the system prompt when you DM the bot; broadcasts are unaffected.
|
||||
|
|
@ -203,12 +203,12 @@ export default function AdapterConfig() {
|
|||
const metaId = `meta:${adapter}`
|
||||
const metaStatus = saveStatus[metaId] || 'idle'
|
||||
return (
|
||||
<div key={adapter} className="bg-slate-800/60 border border-slate-700 rounded-lg">
|
||||
<div key={adapter} className="bg-bg-card border border-border">
|
||||
{/* Card header */}
|
||||
<div className="p-4 flex items-start gap-4">
|
||||
<button
|
||||
onClick={() => setExpanded((e) => ({ ...e, [adapter]: !e[adapter] }))}
|
||||
className="text-slate-400 hover:text-white"
|
||||
className="text-[#444] hover:text-white"
|
||||
aria-label="toggle expand"
|
||||
>
|
||||
{isExpanded
|
||||
|
|
@ -217,27 +217,27 @@ export default function AdapterConfig() {
|
|||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-base font-semibold text-slate-100">{m.display_name}</h2>
|
||||
<code className="text-xs text-slate-500">{adapter}</code>
|
||||
<h2 className="text-base font-semibold text-white">{m.display_name}</h2>
|
||||
<code className="text-xs text-[#333]">{adapter}</code>
|
||||
{rows.length > 0 && (
|
||||
<span className="text-xs text-slate-400 ml-1">({rows.length} settings)</span>
|
||||
<span className="text-xs text-[#444] ml-1">({rows.length} settings)</span>
|
||||
)}
|
||||
{rows.length === 0 && (
|
||||
<span className="text-xs text-slate-500 ml-1 italic">(meta only)</span>
|
||||
<span className="text-xs text-[#333] ml-1 italic">(meta only)</span>
|
||||
)}
|
||||
</div>
|
||||
{m.description && (
|
||||
<p className="text-xs text-slate-400 mt-1">{m.description}</p>
|
||||
<p className="text-xs text-[#444] mt-1">{m.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* include_in_llm_context toggle */}
|
||||
<label className="flex items-center gap-2 text-xs text-slate-300 select-none">
|
||||
<label className="flex items-center gap-2 text-xs text-[#e0e0e0] select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={m.include_in_llm_context}
|
||||
onChange={(e) => putMeta(adapter, { include_in_llm_context: e.target.checked })}
|
||||
className="w-4 h-4 accent-cyan-500"
|
||||
className="w-4 h-4 accent-[#f59e0b]"
|
||||
/>
|
||||
LLM context
|
||||
<SaveBadge status={metaStatus} error={saveError[metaId]} />
|
||||
|
|
@ -246,7 +246,7 @@ export default function AdapterConfig() {
|
|||
|
||||
{/* Expanded body */}
|
||||
{isExpanded && rows.length > 0 && (
|
||||
<div className="border-t border-slate-700 divide-y divide-slate-700/60">
|
||||
<div className="border-t border-border divide-y divide-border">
|
||||
{rows.map((row) => (
|
||||
<KeyRow
|
||||
key={row.key}
|
||||
|
|
@ -302,14 +302,14 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
|
|||
<div className="px-6 py-3 flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono text-cyan-300">{row.key}</code>
|
||||
<span className="text-xs text-slate-500">[{row.type}]</span>
|
||||
<code className="text-sm font-mono text-accent">{row.key}</code>
|
||||
<span className="text-xs text-[#333]">[{row.type}]</span>
|
||||
{!isDefault && (
|
||||
<span className="text-xs text-amber-400">edited</span>
|
||||
<span className="text-xs text-accent">edited</span>
|
||||
)}
|
||||
</div>
|
||||
{row.description && (
|
||||
<p className="text-xs text-slate-400 mt-1">{row.description}</p>
|
||||
<p className="text-xs text-[#444] mt-1">{row.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -319,11 +319,11 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
|
|||
type="checkbox"
|
||||
checked={row.value === true}
|
||||
onChange={(e) => onCommit(e.target.checked)}
|
||||
className="w-5 h-5 accent-cyan-500"
|
||||
className="w-5 h-5 accent-[#f59e0b]"
|
||||
/>
|
||||
) : row.type === 'json' ? (
|
||||
<textarea
|
||||
className="w-72 h-20 bg-slate-900 border border-slate-700 rounded px-2 py-1 text-xs font-mono text-slate-100"
|
||||
className="w-72 h-20 bg-[#0d0d0d] border border-border px-2 py-1 text-xs font-mono text-white"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={commit}
|
||||
|
|
@ -332,7 +332,7 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
|
|||
<input
|
||||
type={row.type === 'int' || row.type === 'float' ? 'number' : 'text'}
|
||||
step={row.type === 'float' ? 'any' : '1'}
|
||||
className="w-48 bg-slate-900 border border-slate-700 rounded px-2 py-1 text-sm text-slate-100"
|
||||
className="w-48 bg-[#0d0d0d] border border-border px-2 py-1 text-sm text-white"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={commit}
|
||||
|
|
@ -345,7 +345,7 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
|
|||
<button
|
||||
onClick={onReset}
|
||||
disabled={isDefault}
|
||||
className="text-slate-400 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
className="text-[#444] hover:text-white disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Reset to default"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
|
|
@ -360,14 +360,14 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
|
|||
|
||||
|
||||
function SaveBadge({ status, error, dirty }: { status: SaveStatus; error?: string; dirty?: boolean }) {
|
||||
if (status === 'saving') return <Loader2 className="w-4 h-4 text-cyan-400 animate-spin" />
|
||||
if (status === 'saved') return <Check className="w-4 h-4 text-emerald-400" />
|
||||
if (status === 'saving') return <Loader2 className="w-4 h-4 text-accent animate-spin" />
|
||||
if (status === 'saved') return <Check className="w-4 h-4 text-green-500" />
|
||||
if (status === 'error') return (
|
||||
<span title={error} className="text-red-400 cursor-help">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
</span>
|
||||
)
|
||||
if (dirty) return <span className="w-2 h-2 bg-amber-400 rounded-full" title="unsaved" />
|
||||
if (dirty) return <span className="w-2 h-2 bg-accent rounded-full" title="unsaved" />
|
||||
return <span className="w-4 h-4" />
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,10 +76,10 @@ function getSeverityStyles(severity: string) {
|
|||
case 'routine':
|
||||
default:
|
||||
return {
|
||||
bg: 'bg-blue-500/10',
|
||||
border: 'border-blue-500',
|
||||
badge: 'bg-blue-500/20 text-blue-400',
|
||||
iconColor: 'text-blue-500',
|
||||
bg: 'bg-sky-400/10',
|
||||
border: 'border-sky-400',
|
||||
badge: 'bg-sky-400/20 text-sky-400',
|
||||
iconColor: 'text-sky-400',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ function ActiveAlertCard({
|
|||
const Icon = getAlertIcon(alert.type)
|
||||
|
||||
return (
|
||||
<div className={`p-4 rounded-lg ${styles.bg} border-l-4 ${styles.border}`}>
|
||||
<div className={`p-4 ${styles.bg} border-l-4 ${styles.border}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Icon size={20} className={styles.iconColor} />
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
@ -197,7 +197,7 @@ function AlertHistoryTable({
|
|||
const severities = ["all", "immediate", "priority", "routine"]
|
||||
|
||||
return (
|
||||
<div className="bg-bg-card border border-border rounded-lg">
|
||||
<div className="bg-bg-card border border-border">
|
||||
{/* Filters */}
|
||||
<div className="p-4 border-b border-border flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -207,7 +207,7 @@ function AlertHistoryTable({
|
|||
<select
|
||||
value={typeFilter}
|
||||
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-blue-500"
|
||||
className="bg-bg border border-border rounded px-3 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-sky-400"
|
||||
>
|
||||
{alertTypes.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
|
|
@ -218,7 +218,7 @@ function AlertHistoryTable({
|
|||
<select
|
||||
value={severityFilter}
|
||||
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-blue-500"
|
||||
className="bg-bg border border-border rounded px-3 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-sky-400"
|
||||
>
|
||||
{severities.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
|
|
@ -352,10 +352,10 @@ function SubscriptionCard({ subscription, nodes }: { subscription: Subscription;
|
|||
const Icon = getTypeIcon()
|
||||
|
||||
return (
|
||||
<div className="p-4 rounded-lg 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="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
|
||||
<Icon size={18} className="text-blue-400" />
|
||||
<div className="w-10 h-10 bg-sky-400/10 flex items-center justify-center">
|
||||
<Icon size={18} className="text-sky-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-slate-200 font-medium">
|
||||
|
|
@ -490,7 +490,7 @@ export default function Alerts() {
|
|||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Active Alerts */}
|
||||
<div className="bg-bg-card border border-border rounded-lg p-6">
|
||||
<div className="bg-bg-card border border-border p-6">
|
||||
<h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
|
||||
<AlertTriangle size={14} />
|
||||
Active Alerts ({visibleAlerts.length})
|
||||
|
|
@ -538,7 +538,7 @@ export default function Alerts() {
|
|||
</div>
|
||||
|
||||
{/* Subscriptions */}
|
||||
<div className="bg-bg-card border border-border rounded-lg p-6">
|
||||
<div className="bg-bg-card border border-border p-6">
|
||||
<h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
|
||||
<Users size={14} />
|
||||
Mesh Subscriptions ({subscriptions.length})
|
||||
|
|
@ -553,7 +553,7 @@ export default function Alerts() {
|
|||
<div className="text-slate-500 py-4">
|
||||
<p>No active subscriptions.</p>
|
||||
<p className="text-xs mt-2">
|
||||
Manage subscriptions via <code className="text-blue-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-blue-400 hover:underline">Broadcast Types</a> and <a href="/reference#reminders" className="text-blue-400 hover:underline">Reminder System</a> in Reference.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ function InfoButton({ info, link, linkText = 'Learn more' }: { info: string; lin
|
|||
?
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute left-0 top-6 z-50 w-72 p-3 bg-[#1a2332] border border-[#2a3a4a] rounded-lg shadow-xl text-xs text-slate-300 leading-relaxed">
|
||||
<div className="absolute left-0 top-6 z-50 w-72 p-3 bg-[#1a2332] border border-[#2a3a4a] shadow-xl text-xs text-slate-300 leading-relaxed">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
|
|
@ -628,7 +628,7 @@ function AlertRuleToggle({ label, description, checked, onChange, threshold, onT
|
|||
thresholdSuffix?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-3 space-y-2">
|
||||
<div className="border border-[#1e2a3a] p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<span className="text-sm text-slate-300">{label}</span>
|
||||
|
|
@ -1299,7 +1299,7 @@ function MeshSourceCard({ source, onChange, onDelete }: {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="border border-[#1e2a3a] rounded-lg overflow-hidden">
|
||||
<div className="border border-[#1e2a3a] overflow-hidden">
|
||||
<div
|
||||
className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
|
|
@ -1402,7 +1402,7 @@ function MeshSourcesSection({ data, onChange }: { data: MeshSourceConfig[]; onCh
|
|||
))}
|
||||
<button
|
||||
onClick={addSource}
|
||||
className="w-full py-2 border border-dashed border-[#1e2a3a] rounded-lg text-slate-500 hover:text-slate-300 hover:border-accent flex items-center justify-center gap-2 transition-colors"
|
||||
className="w-full py-2 border border-dashed border-[#1e2a3a] text-slate-500 hover:text-slate-300 hover:border-accent flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Plus size={16} /> Add Source
|
||||
</button>
|
||||
|
|
@ -1498,7 +1498,7 @@ function MeshIntelligenceSection({ data, onChange }: { data: MeshIntelligenceCon
|
|||
<InfoButton info="Regions group mesh nodes by geographic area. Each region has an anchor point (lat/lon) and nodes within the region radius are automatically assigned. Regions enable localized reports, alerts, and health scoring." />
|
||||
</label>
|
||||
{data.regions.map((region, i) => (
|
||||
<div key={i} className="border border-[#1e2a3a] rounded-lg overflow-hidden">
|
||||
<div key={i} className="border border-[#1e2a3a] overflow-hidden">
|
||||
<div
|
||||
className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer"
|
||||
onClick={() => setExpandedRegion(expandedRegion === i ? null : i)}
|
||||
|
|
@ -1580,7 +1580,7 @@ function MeshIntelligenceSection({ data, onChange }: { data: MeshIntelligenceCon
|
|||
onChange({ ...data, regions: [...data.regions, newRegion] })
|
||||
setExpandedRegion(data.regions.length)
|
||||
}}
|
||||
className="w-full py-2 border border-dashed border-[#1e2a3a] rounded-lg text-slate-500 hover:text-slate-300 hover:border-accent flex items-center justify-center gap-2 transition-colors"
|
||||
className="w-full py-2 border border-dashed border-[#1e2a3a] text-slate-500 hover:text-slate-300 hover:border-accent flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Plus size={16} /> Add Region
|
||||
</button>
|
||||
|
|
@ -1974,7 +1974,7 @@ export default function Config() {
|
|||
</div>
|
||||
|
||||
{restartRequired && (
|
||||
<div className="flex items-center justify-between p-3 mb-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
||||
<div className="flex items-center justify-between p-3 mb-4 bg-amber-500/10 border border-amber-500/30">
|
||||
<div className="flex items-center gap-2 text-amber-400">
|
||||
<AlertTriangle size={16} />
|
||||
<span className="text-sm">Restart required for changes to take effect</span>
|
||||
|
|
@ -1989,21 +1989,21 @@ export default function Config() {
|
|||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 mb-4 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400">
|
||||
<div className="flex items-center gap-2 p-3 mb-4 bg-red-500/10 border border-red-500/30 text-red-400">
|
||||
<X size={16} />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="flex items-center gap-2 p-3 mb-4 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400">
|
||||
<div className="flex items-center gap-2 p-3 mb-4 bg-green-500/10 border border-green-500/30 text-green-400">
|
||||
<Check size={16} />
|
||||
<span className="text-sm">{success}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-2">
|
||||
<div className="bg-bg-card border border-border rounded-lg p-6">
|
||||
<div className="bg-bg-card border border-border p-6">
|
||||
{renderSection()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -94,18 +94,18 @@ function FeedStatusCard({ feed }: { feed: FeedHealth }) {
|
|||
const text = !feed.is_loaded ? 'Not loaded' : feed.consecutive_errors > 0 ? `${feed.consecutive_errors} errors` : 'Healthy'
|
||||
const lastFetch = feed.last_fetch ? new Date(feed.last_fetch * 1000).toLocaleTimeString() : 'Never'
|
||||
return (
|
||||
<div className="bg-bg-hover rounded-lg p-4">
|
||||
<div className="bg-bg-hover p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${color}`} />
|
||||
<span className="text-sm font-medium text-slate-200 uppercase">{feed.source}</span>
|
||||
<span className="text-sm font-medium text-white uppercase">{feed.source}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">{text}</span>
|
||||
<span className="text-xs text-[#444]">{text}</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 space-y-1">
|
||||
<div className="text-xs font-mono text-[#333] space-y-1">
|
||||
<div>Events: {feed.event_count}</div>
|
||||
<div>Last fetch: {lastFetch}</div>
|
||||
{feed.last_error && <div className="text-amber-500 truncate">{feed.last_error}</div>}
|
||||
{feed.last_error && <div className="text-accent truncate">{feed.last_error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -116,19 +116,19 @@ function EventCard({ event }: { event: EnvEvent }) {
|
|||
const styles = (sev === 'extreme' || sev === 'severe' || sev === 'immediate')
|
||||
? { bg: 'bg-red-500/10', border: 'border-red-500', Icon: AlertCircle, color: 'text-red-500' }
|
||||
: (sev === 'moderate' || sev === 'warning' || sev === 'priority')
|
||||
? { bg: 'bg-amber-500/10', border: 'border-amber-500', Icon: AlertTriangle, color: 'text-amber-500' }
|
||||
: { bg: 'bg-blue-500/10', border: 'border-blue-500', Icon: Info, color: 'text-blue-500' }
|
||||
? { bg: 'bg-accent/10', border: 'border-amber-500', Icon: AlertTriangle, color: 'text-accent' }
|
||||
: { bg: 'bg-sky-400/10', border: 'border-sky-400', Icon: Info, color: 'text-sky-400' }
|
||||
const Icon = styles.Icon
|
||||
return (
|
||||
<div className={`p-3 rounded-lg ${styles.bg} border-l-2 ${styles.border}`}>
|
||||
<div className={`p-3 ${styles.bg} border-l-2 ${styles.border}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Icon size={16} className={styles.color} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-slate-200">{event.event_type}</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${styles.bg} ${styles.color}`}>{event.severity}</span>
|
||||
<span className="text-sm font-medium text-white">{event.event_type}</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 ${styles.bg} ${styles.color}`}>{event.severity}</span>
|
||||
</div>
|
||||
<div className="text-sm text-slate-300">{event.headline}</div>
|
||||
<div className="text-sm font-sans text-[#e0e0e0]">{event.headline}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -141,19 +141,19 @@ function FeedSourceToggle({ value, onChange, disabled, centralDisabled }: {
|
|||
}) {
|
||||
const base = 'px-2 py-1 text-xs transition-colors'
|
||||
return (
|
||||
<div className={`flex rounded border border-[#1e2a3a] overflow-hidden ${disabled ? 'opacity-40' : ''}`}>
|
||||
<div className={`flex border border-border overflow-hidden ${disabled ? 'opacity-40' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onChange('native')}
|
||||
className={`${base} ${value === 'native' ? 'bg-accent text-white' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
className={`${base} ${value === 'native' ? 'bg-accent text-white' : 'text-[#444] hover:text-white'}`}
|
||||
>native</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || centralDisabled}
|
||||
title={centralDisabled ? 'Central not available for this adapter' : ''}
|
||||
onClick={() => { if (!centralDisabled) onChange('central') }}
|
||||
className={`${base} ${centralDisabled ? 'text-slate-600 cursor-not-allowed' : value === 'central' ? 'bg-accent text-white' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
className={`${base} ${centralDisabled ? 'text-[#333] cursor-not-allowed' : value === 'central' ? 'bg-accent text-white' : 'text-[#444] hover:text-white'}`}
|
||||
>central</button>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -169,35 +169,35 @@ function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedS
|
|||
}) {
|
||||
const centralDisabled = nativeOnly || !hasCentral
|
||||
return (
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="border border-border p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">{title}</span>
|
||||
{subtitle && <p className="text-xs text-slate-600">{subtitle}</p>}
|
||||
<span className="text-sm font-medium text-[#e0e0e0]">{title}</span>
|
||||
{subtitle && <p className="text-xs text-[#333]">{subtitle}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] uppercase tracking-wide text-slate-600">source</span>
|
||||
<span className="text-[10px] uppercase tracking-wide text-[#333]">source</span>
|
||||
<FeedSourceToggle value={feedSource} onChange={onFeedSource} disabled={!enabled} centralDisabled={centralDisabled} />
|
||||
</div>
|
||||
<Toggle label="" checked={enabled} onChange={onEnabled} />
|
||||
</div>
|
||||
</div>
|
||||
{!hasKey && (
|
||||
<div className="text-xs text-amber-400 bg-amber-500/10 rounded p-2">
|
||||
<div className="text-xs text-accent bg-accent/10 p-2">
|
||||
API key not configured — contact admin
|
||||
</div>
|
||||
)}
|
||||
{nativeOnly && (
|
||||
<div className="text-[11px] text-slate-600">Central not available for this adapter — native only</div>
|
||||
<div className="text-[11px] text-[#333]">Central not available for this adapter — native only</div>
|
||||
)}
|
||||
<div className={enabled ? 'space-y-3' : 'space-y-3 opacity-40 pointer-events-none select-none'}>
|
||||
{children}
|
||||
</div>
|
||||
{(health || (events && events.length > 0)) && (
|
||||
<div className="pt-2 border-t border-[#1e2a3a] space-y-3">
|
||||
<div className="text-[10px] uppercase tracking-wide text-slate-600">Live status</div>
|
||||
{health ? <FeedStatusCard feed={health} /> : <div className="text-xs text-slate-600">No status reported.</div>}
|
||||
<div className="pt-2 border-t border-border space-y-3">
|
||||
<div className="text-[10px] uppercase tracking-wide text-[#333]">Live status</div>
|
||||
{health ? <FeedStatusCard feed={health} /> : <div className="text-xs text-[#333]">No status reported.</div>}
|
||||
{events && events.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{events.slice(0, 5).map((e, i) => <EventCard key={i} event={e} />)}
|
||||
|
|
@ -636,7 +636,7 @@ const save = async () => {
|
|||
|
||||
const up = (patch: Partial<EnvConfig>) => env && setEnv({ ...env, ...patch })
|
||||
|
||||
if (loading) return <div className="flex items-center justify-center h-64 text-slate-400">Loading environmental config…</div>
|
||||
if (loading) return <div className="flex items-center justify-center h-64 text-[#444]">Loading environmental config…</div>
|
||||
if (!env) return <div className="flex items-center justify-center h-64 text-red-400">{error || 'No config'}</div>
|
||||
|
||||
const healthFor = (key: AdapterKey): FeedHealth | undefined =>
|
||||
|
|
@ -663,10 +663,10 @@ const save = async () => {
|
|||
</>
|
||||
)}
|
||||
{env.nws.feed_source === 'central' && (
|
||||
<div className="border-t border-slate-700/50 pt-4 mt-4">
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Filters</div>
|
||||
<div className="border-t border-border pt-4 mt-4">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">Broadcast Filters</div>
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-slate-400 mb-2">Severities to broadcast</div>
|
||||
<div className="text-xs font-sans text-[#444] mb-2">Severities to broadcast</div>
|
||||
<div className="flex gap-6">
|
||||
{['Extreme', 'Severe', 'Moderate', 'Minor'].map((sev) => (
|
||||
<label key={sev} className="flex items-center gap-2 cursor-pointer">
|
||||
|
|
@ -675,8 +675,8 @@ const save = async () => {
|
|||
const cur = nwsConfig.broadcast_severities
|
||||
setNwsConfig({ ...nwsConfig, broadcast_severities: e.target.checked ? [...cur, sev] : cur.filter(s => s !== sev) })
|
||||
}}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300">{sev}</span>
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">{sev}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -690,7 +690,7 @@ const save = async () => {
|
|||
case 'swpc': return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">
|
||||
Broadcast Thresholds
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
|
|
@ -741,28 +741,28 @@ const save = async () => {
|
|||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Incident Types</div>
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">Incident Types</div>
|
||||
<div className="flex gap-6">
|
||||
{[['WF', 'Wildfire'], ['RX', 'Prescribed Burn'], ['OTHER', 'Other']].map(([val, label]) => (
|
||||
<label key={val} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={wfigsConfig.allowed_incident_types?.includes(val) ?? val === 'WF'}
|
||||
onChange={(e) => { const cur = wfigsConfig.allowed_incident_types ?? ['WF']; setWfigsConfig({ ...wfigsConfig, allowed_incident_types: e.target.checked ? [...cur, val] : cur.filter(t => t !== val) }) }}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300">{label}</span>
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Triggers</div>
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">Broadcast Triggers</div>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-300">Broadcast on acres increase</span>
|
||||
<input type="checkbox" checked={wfigsConfig.broadcast_on_acres} onChange={(e) => setWfigsConfig({ ...wfigsConfig, broadcast_on_acres: e.target.checked })} className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">Broadcast on acres increase</span>
|
||||
<input type="checkbox" checked={wfigsConfig.broadcast_on_acres} onChange={(e) => setWfigsConfig({ ...wfigsConfig, broadcast_on_acres: e.target.checked })} className="w-4 h-4 accent-[#f59e0b]" />
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-300">Broadcast on containment increase</span>
|
||||
<input type="checkbox" checked={wfigsConfig.broadcast_on_contained} onChange={(e) => setWfigsConfig({ ...wfigsConfig, broadcast_on_contained: e.target.checked })} className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">Broadcast on containment increase</span>
|
||||
<input type="checkbox" checked={wfigsConfig.broadcast_on_contained} onChange={(e) => setWfigsConfig({ ...wfigsConfig, broadcast_on_contained: e.target.checked })} className="w-4 h-4 accent-[#f59e0b]" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -788,7 +788,7 @@ const save = async () => {
|
|||
onChange={(v) => up({ avalanche: { ...env.avalanche, center_ids: v } })}
|
||||
helper="e.g., SNFAC" infoLink="https://avalanche.org/avalanche-centers/" />
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">
|
||||
Broadcast Settings
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
|
@ -820,7 +820,7 @@ const save = async () => {
|
|||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">
|
||||
Magnitude Thresholds
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
|
@ -839,10 +839,10 @@ const save = async () => {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">
|
||||
PAGER Alert Levels
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mb-2">
|
||||
<div className="text-xs text-[#333] mb-2">
|
||||
Broadcast at any magnitude when USGS PAGER alert reaches these levels
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
|
|
@ -857,8 +857,8 @@ const save = async () => {
|
|||
? [...cur, level]
|
||||
: cur.filter((l) => l !== level) }})
|
||||
}}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300 capitalize">{level}</span>
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0] capitalize">{level}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -868,42 +868,42 @@ const save = async () => {
|
|||
case 'traffic': return (<>
|
||||
<TextInput label="API Key" value={env.traffic.api_key} onChange={(v) => up({ traffic: { ...env.traffic, api_key: v } })} type="password" helper="developer.tomtom.com" />
|
||||
<NumberInput label="Tick Seconds" value={env.traffic.tick_seconds} onChange={(v) => up({ traffic: { ...env.traffic, tick_seconds: v } })} min={60} />
|
||||
<div className="text-xs text-slate-500 mt-2">Corridors:</div>
|
||||
<div className="text-xs text-[#333] mt-2">Corridors:</div>
|
||||
{(env.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 n = [...env.traffic.corridors]; n[i] = { ...c, name: v }; up({ traffic: { ...env.traffic, corridors: n } }) }} />
|
||||
<NumberInput label="Lat" value={c.lat} onChange={(v) => { const n = [...env.traffic.corridors]; n[i] = { ...c, lat: v }; up({ traffic: { ...env.traffic, corridors: n } }) }} step={0.01} />
|
||||
<NumberInput label="Lon" value={c.lon} onChange={(v) => { const n = [...env.traffic.corridors]; n[i] = { ...c, lon: v }; up({ traffic: { ...env.traffic, corridors: n } }) }} step={0.01} />
|
||||
<button onClick={() => up({ traffic: { ...env.traffic, corridors: env.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>
|
||||
<button onClick={() => up({ traffic: { ...env.traffic, corridors: env.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">Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => up({ traffic: { ...env.traffic, corridors: [...(env.traffic.corridors || []), { name: '', lat: 0, lon: 0 }] } })} className="text-xs text-accent hover:underline">+ Add Corridor</button>
|
||||
<div className="border-t border-slate-700/50 pt-4 mt-4">
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Filters</div>
|
||||
<div className="border-t border-border pt-4 mt-4">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">Broadcast Filters</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 mb-1 block">Minimum Magnitude</label>
|
||||
<label className="text-xs font-sans text-[#444] mb-1 block">Minimum Magnitude</label>
|
||||
<select
|
||||
value={tomtomConfig.min_magnitude}
|
||||
onChange={(e) => setTomtomConfig({...tomtomConfig, min_magnitude: parseInt(e.target.value)})}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm"
|
||||
className="w-full bg-[#0d0d0d] border border-border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value={1}>1 — Minor (all)</option>
|
||||
<option value={2}>2 — Moderate (yellow+)</option>
|
||||
<option value={3}>3 — Major (orange+)</option>
|
||||
<option value={4}>4 — Severe (red only)</option>
|
||||
</select>
|
||||
<p className="text-xs text-slate-500 mt-1">Drop TomTom incidents below this severity level</p>
|
||||
<p className="text-xs text-[#333] mt-1">Drop TomTom incidents below this severity level</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-300">Drop non-present time validity</span>
|
||||
<input type="checkbox" checked={tomtomConfig.drop_non_present} onChange={(e) => setTomtomConfig({...tomtomConfig, drop_non_present: e.target.checked})} className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">Drop non-present time validity</span>
|
||||
<input type="checkbox" checked={tomtomConfig.drop_non_present} onChange={(e) => setTomtomConfig({...tomtomConfig, drop_non_present: e.target.checked})} className="w-4 h-4 accent-[#f59e0b]" />
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-300">Drop zero-magnitude events</span>
|
||||
<input type="checkbox" checked={tomtomConfig.drop_zero_magnitude} onChange={(e) => setTomtomConfig({...tomtomConfig, drop_zero_magnitude: e.target.checked})} className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">Drop zero-magnitude events</span>
|
||||
<input type="checkbox" checked={tomtomConfig.drop_zero_magnitude} onChange={(e) => setTomtomConfig({...tomtomConfig, drop_zero_magnitude: e.target.checked})} className="w-4 h-4 accent-[#f59e0b]" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -918,45 +918,45 @@ const save = async () => {
|
|||
<NumberInput key={lbl} label={lbl} value={env.roads511.bbox?.[i] ?? 0} onChange={(v) => { const b = [...(env.roads511.bbox || [0, 0, 0, 0])]; b[i] = v; up({ roads511: { ...env.roads511, bbox: b } }) }} step={0.01} />
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-slate-700/50 pt-4 mt-4">
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Filters</div>
|
||||
<div className="border-t border-border pt-4 mt-4">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">Broadcast Filters</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 mb-1 block">Minimum Severity</label>
|
||||
<label className="text-xs font-sans text-[#444] mb-1 block">Minimum Severity</label>
|
||||
<select
|
||||
value={roads511Config.min_severity}
|
||||
onChange={(e) => setRoads511Config({...roads511Config, min_severity: e.target.value})}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm"
|
||||
className="w-full bg-[#0d0d0d] border border-border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="None">None (all)</option>
|
||||
<option value="Minor">Minor+</option>
|
||||
<option value="Major">Major only</option>
|
||||
</select>
|
||||
<p className="text-xs text-slate-500 mt-1">Drop ITD 511 events below this severity</p>
|
||||
<p className="text-xs text-[#333] mt-1">Drop ITD 511 events below this severity</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="text-xs text-slate-400 mb-2">Categories</div>
|
||||
<div className="text-xs font-sans text-[#444] mb-2">Categories</div>
|
||||
<div className="flex gap-6">
|
||||
{([['incident', 'Incident'], ['closure', 'Closure'], ['special_event', 'Special Event']] as const).map(([val, label]) => (
|
||||
<label key={val} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={roads511Config.enabled_categories.includes(val)}
|
||||
onChange={(e) => { const cur = roads511Config.enabled_categories; setRoads511Config({...roads511Config, enabled_categories: e.target.checked ? [...cur, val] : cur.filter(c => c !== val)}) }}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300">{label}</span>
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="text-xs text-slate-400 mb-2">Sub-types</div>
|
||||
<div className="text-xs font-sans text-[#444] mb-2">Sub-types</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{([['accident', 'Crash'], ['road_closed', 'Road Closed'], ['lane_closed', 'Lane Closure'], ['vehicle_on_fire', 'Vehicle Fire'], ['flooding', 'Flooding'], ['debris', 'Debris'], ['road_works', 'Road Works'], ['disabled_vehicle', 'Disabled Vehicle']] as const).map(([val, label]) => (
|
||||
<label key={val} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={roads511Config.enabled_sub_types.includes(val)}
|
||||
onChange={(e) => { const cur = roads511Config.enabled_sub_types; setRoads511Config({...roads511Config, enabled_sub_types: e.target.checked ? [...cur, val] : cur.filter(s => s !== val)}) }}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300">{label}</span>
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -975,25 +975,25 @@ const save = async () => {
|
|||
<NumberInput key={lbl} label={lbl} value={env.wzdx?.bbox?.[i] ?? 0} onChange={(v) => { const b = [...(env.wzdx?.bbox || [0, 0, 0, 0])]; b[i] = v; up({ wzdx: { ...env.wzdx!, bbox: b } }) }} step={0.01} />
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">Bounding box [W,S,E,N] geographic filter</div>
|
||||
<div className="text-xs text-[#333]">Bounding box [W,S,E,N] geographic filter</div>
|
||||
</>
|
||||
)}
|
||||
<div className="border-t border-slate-700/50 pt-4 mt-4">
|
||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Settings</div>
|
||||
<div className="border-t border-border pt-4 mt-4">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">Broadcast Settings</div>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-300">Broadcast work zone events</span>
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">Broadcast work zone events</span>
|
||||
<input type="checkbox" checked={wzdxConfig.broadcast}
|
||||
onChange={(e) => setWzdxConfig({...wzdxConfig, broadcast: e.target.checked})}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
</label>
|
||||
{wzdxConfig.broadcast ? (
|
||||
<div className="space-y-3 mt-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 mb-1 block">Min Severity</label>
|
||||
<label className="text-xs font-sans text-[#444] mb-1 block">Min Severity</label>
|
||||
<select
|
||||
value={wzdxConfig.min_severity}
|
||||
onChange={(e) => setWzdxConfig({...wzdxConfig, min_severity: e.target.value})}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm"
|
||||
className="w-full bg-[#0d0d0d] border border-border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="None">None (all)</option>
|
||||
<option value="Minor">Minor+</option>
|
||||
|
|
@ -1001,21 +1001,21 @@ const save = async () => {
|
|||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-400 mb-2">Sub-types</div>
|
||||
<div className="text-xs font-sans text-[#444] mb-2">Sub-types</div>
|
||||
<div className="flex gap-6">
|
||||
{([['road_works', 'Road Works'], ['lane_closed', 'Lane Closure'], ['road_closed', 'Road Closed']] as const).map(([val, label]) => (
|
||||
<label key={val} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={wzdxConfig.sub_types.includes(val)}
|
||||
onChange={(e) => { const cur = wzdxConfig.sub_types; setWzdxConfig({...wzdxConfig, sub_types: e.target.checked ? [...cur, val] : cur.filter(s => s !== val)}) }}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300">{label}</span>
|
||||
className="w-4 h-4 accent-[#f59e0b]" />
|
||||
<span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-slate-500 mt-2">Work zone events stored for LLM context only {'\u2014'} no mesh broadcasts.</p>
|
||||
<p className="text-xs text-[#333] mt-2">Work zone events stored for LLM context only {'\u2014'} no mesh broadcasts.</p>
|
||||
)}
|
||||
</div>
|
||||
</>)
|
||||
|
|
@ -1047,15 +1047,15 @@ const save = async () => {
|
|||
<div className="space-y-6">
|
||||
{/* Header + master enable + save bar */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-slate-200">Environment</h1>
|
||||
<h1 className="text-xl font-semibold text-white">Environment</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<Toggle label="Feeds Enabled" checked={env.enabled} onChange={(v) => up({ enabled: v })} />
|
||||
{hasChanges && (
|
||||
<>
|
||||
<button onClick={discard} className="flex items-center gap-1 px-3 py-1.5 text-sm text-slate-400 hover:text-slate-200 border border-border rounded">
|
||||
<button onClick={discard} className="flex items-center gap-1 px-3 py-1.5 text-sm text-[#444] hover:text-white border border-border">
|
||||
<RotateCcw size={14} /> Discard
|
||||
</button>
|
||||
<button onClick={save} disabled={saving} className="flex items-center gap-1 px-3 py-1.5 text-sm bg-accent text-white rounded disabled:opacity-50">
|
||||
<button onClick={save} disabled={saving} className="flex items-center gap-1 px-3 py-1.5 text-sm bg-accent text-white disabled:opacity-50">
|
||||
<Save size={14} /> {saving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</>
|
||||
|
|
@ -1063,12 +1063,12 @@ const save = async () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-sm text-red-400 bg-red-500/10 rounded p-3">{error}</div>}
|
||||
{success && <div className="text-sm text-green-400 bg-green-500/10 rounded p-3">{success}</div>}
|
||||
{error && <div className="text-sm text-red-400 bg-red-500/10 p-3">{error}</div>}
|
||||
{success && <div className="text-sm text-green-400 bg-green-500/10 p-3">{success}</div>}
|
||||
{restartRequired && (
|
||||
<div className="flex items-center justify-between text-sm text-amber-400 bg-amber-500/10 border border-amber-500/30 rounded p-3">
|
||||
<div className="flex items-center justify-between text-sm text-accent bg-accent/10 border border-accent/30 p-3">
|
||||
<span className="flex items-center gap-2"><RefreshCw size={14} /> A restart is required for some changes to take effect.</span>
|
||||
<button onClick={restart} className="px-3 py-1 bg-amber-500/20 hover:bg-amber-500/30 rounded">Restart now</button>
|
||||
<button onClick={restart} className="px-3 py-1 bg-accent/20 hover:bg-amber-500/30">Restart now</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1077,7 +1077,7 @@ const save = async () => {
|
|||
<div className="flex gap-1 border-b border-border overflow-x-auto">
|
||||
{FAMILIES.map(({ key, label, icon: Icon }) => (
|
||||
<button key={key} onClick={() => { setFamily(key); const f = FAMILIES.find((x) => x.key === key)!; setAdapter(f.adapters[0] ?? null) }}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm whitespace-nowrap border-b-2 -mb-px transition-colors ${family === key ? 'border-accent text-accent' : 'border-transparent text-slate-400 hover:text-slate-200'}`}>
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm whitespace-nowrap border-b-2 -mb-px transition-colors ${family === key ? 'border-accent text-accent' : 'border-transparent text-[#444] hover:text-white'}`}>
|
||||
<Icon size={15} /> {label}
|
||||
</button>
|
||||
))}
|
||||
|
|
@ -1085,11 +1085,11 @@ const save = async () => {
|
|||
|
||||
{/* Central Connection tab */}
|
||||
{family === 'central' && env.central && (
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="border border-border p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">Central Connection</span>
|
||||
<p className="text-xs text-slate-600">NATS JetStream source for any adapter set to "central"</p>
|
||||
<span className="text-sm font-medium text-[#e0e0e0]">Central Connection</span>
|
||||
<p className="text-xs text-[#333]">NATS JetStream source for any adapter set to "central"</p>
|
||||
</div>
|
||||
<Toggle label="" checked={!!env.central.enabled}
|
||||
onChange={(v) => up({ central: { ...env.central!, enabled: v } })} />
|
||||
|
|
@ -1113,25 +1113,25 @@ const save = async () => {
|
|||
{/* Tracking placeholder */}
|
||||
{family === 'tracking' && (
|
||||
<div className="flex flex-col items-center justify-center h-[40vh] text-center">
|
||||
<Satellite size={32} className="text-slate-600 mb-4" />
|
||||
<p className="text-slate-500 max-w-md">No adapters yet. ADS-B / AIS / satellite passes are planned for v0.5.</p>
|
||||
<Satellite size={32} className="text-[#333] mb-4" />
|
||||
<p className="text-[#333] max-w-md">No adapters yet. ADS-B / AIS / satellite passes are planned for v0.5.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mesh Health (no env adapters; central greyed for future migration) */}
|
||||
{family === 'mesh' && (
|
||||
<div className="border border-[#1e2a3a] rounded-lg p-4 space-y-3">
|
||||
<div className="border border-border p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-slate-300">Mesh Health</span>
|
||||
<p className="text-xs text-slate-600">Node/infra telemetry — sourced from the mesh, not an environmental feed.</p>
|
||||
<span className="text-sm font-medium text-[#e0e0e0]">Mesh Health</span>
|
||||
<p className="text-xs text-[#333]">Node/infra telemetry — sourced from the mesh, not an environmental feed.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] uppercase tracking-wide text-slate-600">source</span>
|
||||
<span className="text-[10px] uppercase tracking-wide text-[#333]">source</span>
|
||||
<FeedSourceToggle value="native" onChange={() => {}} disabled={false} centralDisabled={true} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-600">Central not available — reserved for a future migration.</div>
|
||||
<div className="text-[11px] text-[#333]">Central not available — reserved for a future migration.</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1142,7 +1142,7 @@ const save = async () => {
|
|||
<div className="flex gap-1">
|
||||
{fam.adapters.map((k) => (
|
||||
<button key={k} onClick={() => setAdapter(k)}
|
||||
className={`px-3 py-1.5 text-sm rounded ${activeAdapter === k ? 'bg-bg-hover text-slate-100' : 'text-slate-400 hover:text-slate-200'}`}>
|
||||
className={`px-3 py-1.5 text-sm ${activeAdapter === k ? 'bg-bg-hover text-white' : 'text-[#444] hover:text-white'}`}>
|
||||
{META[k].label}
|
||||
</button>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default function GaugeSites() {
|
|||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Droplets className="w-5 h-5 text-cyan-400" />
|
||||
<Droplets className="w-5 h-5 text-accent" />
|
||||
<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>
|
||||
<button onClick={beginAdd}
|
||||
|
|
@ -106,7 +106,7 @@ export default function GaugeSites() {
|
|||
|
||||
{adding && <RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} adding feedSource={feedSource} />}
|
||||
|
||||
<div className="bg-slate-800/60 border border-slate-700 rounded-lg overflow-x-auto">
|
||||
<div className="bg-slate-800/60 border border-slate-700 overflow-x-auto">
|
||||
<table className="w-full text-sm text-slate-200">
|
||||
<thead className="bg-slate-900 text-xs text-slate-400 uppercase">
|
||||
<tr>
|
||||
|
|
@ -139,7 +139,7 @@ export default function GaugeSites() {
|
|||
<td className="px-3 py-2 text-right">{r.flood_major_ft ?? '-'}</td>
|
||||
<td className="px-3 py-2 text-center">{r.enabled ? <Check className="w-4 h-4 text-emerald-400 inline" /> : <X className="w-4 h-4 text-slate-500 inline" />}</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<button onClick={() => beginEdit(r)} className="text-cyan-400 hover:text-cyan-300 text-xs mr-3">Edit</button>
|
||||
<button onClick={() => beginEdit(r)} className="text-accent hover:text-accent text-xs mr-3">Edit</button>
|
||||
<button onClick={() => remove(r.site_id)} className="text-red-400 hover:text-red-300"><Trash2 className="w-4 h-4 inline" /></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -249,7 +249,7 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding, feedSource }: {
|
|||
value={draft.flood_major_ft ?? ''} onChange={e => upd('flood_major_ft', e.target.value === '' ? null : parseFloat(e.target.value))} />
|
||||
</label>
|
||||
<label className="text-xs text-slate-300 col-span-2 flex items-center gap-2 mt-2">
|
||||
<input type="checkbox" checked={draft.enabled} onChange={e => upd('enabled', e.target.checked)} className="accent-cyan-500" />
|
||||
<input type="checkbox" checked={draft.enabled} onChange={e => upd('enabled', e.target.checked)} className="accent-[#f59e0b]" />
|
||||
Enabled
|
||||
</label>
|
||||
<div className="col-span-2 flex items-center justify-end gap-2 mt-2">
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export default function Mesh() {
|
|||
</div>
|
||||
|
||||
{/* View toggle */}
|
||||
<div className="flex items-center bg-bg-card border border-border rounded-lg p-1">
|
||||
<div className="flex items-center bg-bg-card border border-border p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('topo')}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ function InfoButton({ info }: { info: string }) {
|
|||
{open && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
||||
<div className="absolute left-0 top-6 z-50 w-72 p-3 bg-[#1a2332] border border-[#2a3a4a] rounded-lg shadow-xl text-xs text-slate-300 leading-relaxed">
|
||||
<div className="absolute left-0 top-6 z-50 w-72 p-3 bg-[#1a2332] border border-[#2a3a4a] shadow-xl text-xs text-slate-300 leading-relaxed">
|
||||
{info}
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -584,7 +584,7 @@ function SeveritySelector({ value, onChange }: {
|
|||
{isOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
||||
<div className="absolute left-0 right-0 top-full mt-1 z-50 bg-[#0a0e17] border border-[#1e2a3a] rounded-lg shadow-xl overflow-hidden">
|
||||
<div className="absolute left-0 right-0 top-full mt-1 z-50 bg-[#0a0e17] border border-[#1e2a3a] shadow-xl overflow-hidden">
|
||||
{SEVERITY_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
|
|
@ -915,7 +915,7 @@ function NotificationRuleCard({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={`border rounded-lg overflow-hidden ${rule.enabled ? 'border-[#1e2a3a]' : 'border-slate-700 opacity-60'}`}>
|
||||
<div className={`border overflow-hidden ${rule.enabled ? 'border-[#1e2a3a]' : 'border-slate-700 opacity-60'}`}>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer"
|
||||
|
|
@ -929,7 +929,7 @@ function NotificationRuleCard({
|
|||
title={rule.enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
{rule.trigger_type === 'schedule' ? (
|
||||
<Clock size={14} className="text-blue-400 flex-shrink-0" />
|
||||
<Clock size={14} className="text-sky-400 flex-shrink-0" />
|
||||
) : (
|
||||
<Zap size={14} className="text-yellow-400 flex-shrink-0" />
|
||||
)}
|
||||
|
|
@ -976,7 +976,7 @@ function NotificationRuleCard({
|
|||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleTest() }}
|
||||
disabled={testing || !rule.name}
|
||||
className="p-1.5 text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 rounded disabled:opacity-50"
|
||||
className="p-1.5 text-sky-400 hover:text-sky-300 hover:bg-sky-400/10 rounded disabled:opacity-50"
|
||||
title="Test rule"
|
||||
>
|
||||
<Send size={14} />
|
||||
|
|
@ -1034,7 +1034,7 @@ function NotificationRuleCard({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...rule, trigger_type: 'condition' })}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 border transition-colors ${
|
||||
rule.trigger_type !== 'schedule'
|
||||
? 'bg-accent/10 border-accent text-accent'
|
||||
: 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200'
|
||||
|
|
@ -1046,7 +1046,7 @@ function NotificationRuleCard({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...rule, trigger_type: 'schedule' })}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 border transition-colors ${
|
||||
rule.trigger_type === 'schedule'
|
||||
? 'bg-accent/10 border-accent text-accent'
|
||||
: 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200'
|
||||
|
|
@ -1065,7 +1065,7 @@ function NotificationRuleCard({
|
|||
|
||||
{/* WHEN section - Condition trigger */}
|
||||
{rule.trigger_type !== 'schedule' && (
|
||||
<div className="space-y-4 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="space-y-4 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-slate-300">
|
||||
<AlertTriangle size={14} />
|
||||
WHEN (Condition)
|
||||
|
|
@ -1106,7 +1106,7 @@ function NotificationRuleCard({
|
|||
|
||||
{/* WHEN section - Schedule trigger */}
|
||||
{rule.trigger_type === 'schedule' && (
|
||||
<div className="space-y-4 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="space-y-4 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-slate-300">
|
||||
<Calendar size={14} />
|
||||
WHEN (Schedule)
|
||||
|
|
@ -1197,7 +1197,7 @@ function NotificationRuleCard({
|
|||
)}
|
||||
|
||||
{/* REGIONS section — scope rule to specific regions; empty = all regions */}
|
||||
<div className="space-y-2 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="space-y-2 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-slate-300">
|
||||
<MapPin size={14} />
|
||||
REGIONS
|
||||
|
|
@ -1235,7 +1235,7 @@ function NotificationRuleCard({
|
|||
</div>
|
||||
|
||||
{/* SEND VIA section */}
|
||||
<div className="space-y-4 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="space-y-4 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-slate-300">
|
||||
<Send size={14} />
|
||||
SEND VIA
|
||||
|
|
@ -1262,7 +1262,7 @@ function NotificationRuleCard({
|
|||
|
||||
{/* No delivery warning */}
|
||||
{!rule.delivery_type && (
|
||||
<div className="flex items-start gap-2 p-3 bg-amber-500/10 border border-amber-500/20 rounded-lg">
|
||||
<div className="flex items-start gap-2 p-3 bg-amber-500/10 border border-amber-500/20">
|
||||
<AlertCircle size={16} className="text-amber-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-amber-300">
|
||||
Rule will log matches but not deliver until a delivery method is configured.
|
||||
|
|
@ -1402,7 +1402,7 @@ function NotificationRuleCard({
|
|||
{rule.trigger_type !== 'schedule' && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500 uppercase tracking-wide">Example Message</label>
|
||||
<div className="p-3 bg-[#1e2a3a]/50 rounded-lg border border-[#1e2a3a]">
|
||||
<div className="p-3 bg-[#1e2a3a]/50 border border-[#1e2a3a]">
|
||||
<p className="text-sm text-slate-300 font-mono">{getExampleMessage()}</p>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">This is an example of what this rule would send.</p>
|
||||
|
|
@ -1532,7 +1532,7 @@ function GroupedCategoryPicker({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="max-h-96 overflow-y-auto border border-[#1e2a3a] rounded-lg p-2 space-y-2">
|
||||
<div className="max-h-96 overflow-y-auto border border-[#1e2a3a] p-2 space-y-2">
|
||||
{TOGGLE_FAMILY_META.map(f => renderGroup(f.key, f.label, f.Icon, byFamily.get(f.key) || []))}
|
||||
{renderGroup('other', 'Other', null, other)}
|
||||
</div>
|
||||
|
|
@ -1561,7 +1561,7 @@ function MasterToggles({ toggles, onChange }: {
|
|||
const chanCount = Object.values(t.severity_channels || {}).reduce((n, arr) => n + ((arr as string[])?.length || 0), 0)
|
||||
const regionCount = (t.regions || []).length
|
||||
return (
|
||||
<div key={key} className="border border-[#1e2a3a] rounded-lg p-3">
|
||||
<div key={key} className="border border-[#1e2a3a] p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<button type="button" onClick={() => setExpanded(isOpen ? null : key)}
|
||||
className="flex items-center gap-2 text-sm text-slate-200">
|
||||
|
|
@ -1826,7 +1826,7 @@ export default function Notifications() {
|
|||
{/* Test Dialog */}
|
||||
{testDialog.open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-[#1a2332] border border-[#2a3a4a] rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[85vh] overflow-auto">
|
||||
<div className="bg-[#1a2332] border border-[#2a3a4a] shadow-xl max-w-2xl w-full mx-4 max-h-[85vh] overflow-auto">
|
||||
<div className="p-4 border-b border-[#2a3a4a] flex items-center justify-between sticky top-0 bg-[#1a2332]">
|
||||
<h3 className="text-lg font-semibold">Test Notification Rule</h3>
|
||||
<button onClick={closeTestDialog} className="text-slate-500 hover:text-slate-300">
|
||||
|
|
@ -2033,19 +2033,19 @@ export default function Notifications() {
|
|||
|
||||
{/* Status messages */}
|
||||
{error && (
|
||||
<div className="p-3 rounded-lg text-sm bg-red-500/10 text-red-400 border border-red-500/20">
|
||||
<div className="p-3 text-sm bg-red-500/10 text-red-400 border border-red-500/20">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="p-3 rounded-lg text-sm bg-green-500/10 text-green-400 border border-green-500/20">
|
||||
<div className="p-3 text-sm bg-green-500/10 text-green-400 border border-green-500/20">
|
||||
<Check size={14} className="inline mr-2" />
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<div className="bg-bg-card border border-border rounded-lg p-6 space-y-6">
|
||||
<div className="bg-bg-card border border-border p-6 space-y-6">
|
||||
<Toggle
|
||||
label="Enable Notifications"
|
||||
checked={config.enabled}
|
||||
|
|
@ -2056,7 +2056,7 @@ export default function Notifications() {
|
|||
|
||||
{config.enabled && (
|
||||
<> {/* Cold-start grace -- v0.5.8b */}
|
||||
<div className="space-y-3 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="space-y-3 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-slate-500 uppercase tracking-wide">Cold-start grace</label>
|
||||
</div>
|
||||
|
|
@ -2072,7 +2072,7 @@ export default function Notifications() {
|
|||
</div>
|
||||
|
||||
{/* Band Conditions -- v0.5.11 */}
|
||||
<div className="space-y-3 p-4 bg-[#0a0e17] rounded-lg border border-[#1e2a3a]">
|
||||
<div className="space-y-3 p-4 bg-[#0a0e17] border border-[#1e2a3a]">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-slate-500 uppercase tracking-wide">Band Conditions (HF propagation)</label>
|
||||
</div>
|
||||
|
|
@ -2164,21 +2164,21 @@ export default function Notifications() {
|
|||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={addRule}
|
||||
className="flex-1 py-3 border border-dashed border-[#1e2a3a] rounded-lg text-slate-500 hover:text-slate-300 hover:border-accent flex items-center justify-center gap-2 transition-colors"
|
||||
className="flex-1 py-3 border border-dashed border-[#1e2a3a] text-slate-500 hover:text-slate-300 hover:border-accent flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Plus size={16} /> Add Rule
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowTemplates(!showTemplates)}
|
||||
className="py-3 px-4 border border-dashed border-[#1e2a3a] rounded-lg text-slate-500 hover:text-slate-300 hover:border-accent flex items-center gap-2 transition-colors"
|
||||
className="py-3 px-4 border border-dashed border-[#1e2a3a] text-slate-500 hover:text-slate-300 hover:border-accent flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<Layers size={16} /> Add from Template
|
||||
</button>
|
||||
{showTemplates && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowTemplates(false)} />
|
||||
<div className="absolute right-0 top-full mt-2 z-50 w-80 bg-[#1a2332] border border-[#2a3a4a] rounded-lg shadow-xl overflow-hidden">
|
||||
<div className="absolute right-0 top-full mt-2 z-50 w-80 bg-[#1a2332] border border-[#2a3a4a] shadow-xl overflow-hidden">
|
||||
<div className="p-2 border-b border-[#2a3a4a] text-xs text-slate-500 uppercase">Rule Templates</div>
|
||||
{RULE_TEMPLATES.map((t) => (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default function TownAnchors() {
|
|||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5 text-cyan-400" />
|
||||
<MapPin className="w-5 h-5 text-accent" />
|
||||
<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>
|
||||
<button onClick={beginAdd}
|
||||
|
|
@ -82,7 +82,7 @@ export default function TownAnchors() {
|
|||
|
||||
{adding && <RowEditor draft={draft} setDraft={setDraft} onSave={save} onCancel={cancel} adding />}
|
||||
|
||||
<div className="bg-slate-800/60 border border-slate-700 rounded-lg overflow-x-auto">
|
||||
<div className="bg-slate-800/60 border border-slate-700 overflow-x-auto">
|
||||
<table className="w-full text-sm text-slate-200">
|
||||
<thead className="bg-slate-900 text-xs text-slate-400 uppercase">
|
||||
<tr>
|
||||
|
|
@ -107,7 +107,7 @@ export default function TownAnchors() {
|
|||
<td className="px-3 py-2 text-center text-xs">{r.state || '-'}</td>
|
||||
<td className="px-3 py-2 text-center">{r.enabled ? <Check className="w-4 h-4 text-emerald-400 inline" /> : <X className="w-4 h-4 text-slate-500 inline" />}</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<button onClick={() => beginEdit(r)} className="text-cyan-400 hover:text-cyan-300 text-xs mr-3">Edit</button>
|
||||
<button onClick={() => beginEdit(r)} className="text-accent hover:text-accent text-xs mr-3">Edit</button>
|
||||
<button onClick={() => remove(r.anchor_id)} className="text-red-400 hover:text-red-300"><Trash2 className="w-4 h-4 inline" /></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -136,7 +136,7 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding }: {
|
|||
value={draft.state ?? ''} onChange={e => upd('state', e.target.value)} />
|
||||
</label>
|
||||
<label className="text-xs text-slate-400 flex items-center gap-2">
|
||||
<input type="checkbox" checked={draft.enabled} onChange={e => upd('enabled', e.target.checked)} className="accent-cyan-500 mt-4" />
|
||||
<input type="checkbox" checked={draft.enabled} onChange={e => upd('enabled', e.target.checked)} className="accent-[#f59e0b] mt-4" />
|
||||
Enabled
|
||||
</label>
|
||||
<label className="text-xs text-slate-400">Lat
|
||||
|
|
|
|||
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-B6zGwxmY.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DqWqopB2.css">
|
||||
<script type="module" crossorigin src="/assets/index-7P5nbdAV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-EzV2LMjq.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue