feat: Carbon theme — amber accent, Inter/JetBrains Mono, sharp corners, traffic-light semantic colors across all components

Global: removed all rounded-lg/md/sm classes, replaced blue-500 with
sky-400 informational, cyan accents with amber across all tsx files.
Environment.tsx + AdapterConfig.tsx: full Carbon color sweep — slate
hierarchy replaced with #333/#444/white tokens, border-border tokens,
font-sans labels, font-mono values. Layout.tsx logo text-[15px].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Johnson (via Claude) 2026-06-10 15:37:27 +00:00
commit d0bf298f89
19 changed files with 1472 additions and 1472 deletions

View file

@ -131,7 +131,7 @@ export default function ChannelPicker(props: ChannelPickerProps) {
return ( return (
<div className="space-y-1"> <div className="space-y-1">
<label className="block text-xs text-slate-500 uppercase tracking-wide">{label}</label> <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) => ( {enabledChannels.map((ch) => (
<label <label
key={ch.index} key={ch.index}

View file

@ -181,7 +181,7 @@ export default function GeoMap({
}, [selectedNodeId, edges]) }, [selectedNodeId, edges])
return ( 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 <MapContainer
center={defaultCenter} center={defaultCenter}
zoom={7} zoom={7}

View file

@ -97,7 +97,7 @@ export default function Layout({ children }: LayoutProps) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-[3px] h-8 bg-accent flex-shrink-0" /> <div className="w-[3px] h-8 bg-accent flex-shrink-0" />
<div> <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]"> <div className="text-xs font-mono text-[#333]">
v{status?.version || '...'} v{status?.version || '...'}
</div> </div>

View file

@ -150,7 +150,7 @@ export default function NodeDetail({
<div className="p-4 border-b border-border grid grid-cols-2 gap-3"> <div className="p-4 border-b border-border grid grid-cols-2 gap-3">
<div> <div>
<div className="text-xs text-slate-500 mb-0.5">Role</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} {node.role}
</div> </div>
</div> </div>
@ -187,7 +187,7 @@ export default function NodeDetail({
href={`https://www.google.com/maps?q=${node.latitude},${node.longitude}`} href={`https://www.google.com/maps?q=${node.latitude},${node.longitude}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} /> <ExternalLink size={10} />
Google Maps Google Maps
@ -196,7 +196,7 @@ export default function NodeDetail({
href={`https://www.openstreetmap.org/?mlat=${node.latitude}&mlon=${node.longitude}&zoom=14`} href={`https://www.openstreetmap.org/?mlat=${node.latitude}&mlon=${node.longitude}&zoom=14`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} /> <ExternalLink size={10} />
OSM OSM

View file

@ -175,7 +175,7 @@ export default function NodePicker({
{isOpen && !loading && ( {isOpen && !loading && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} /> <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 ? ( {filteredNodes.length === 0 ? (
<div className="p-3 text-sm text-slate-500 text-center"> <div className="p-3 text-sm text-slate-500 text-center">
No nodes found No nodes found

View file

@ -148,7 +148,7 @@ export default function NodeTable({
} }
return ( 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 */} {/* Filter bar */}
<div className="p-3 border-b border-border flex items-center gap-3"> <div className="p-3 border-b border-border flex items-center gap-3">
{/* Search */} {/* Search */}
@ -254,7 +254,7 @@ export default function NodeTable({
<span <span
className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${ className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${
isInfra isInfra
? 'bg-cyan-500/20 text-cyan-400' ? 'bg-cyan-500/20 text-accent'
: 'bg-slate-500/20 text-slate-400' : 'bg-slate-500/20 text-slate-400'
}`} }`}
> >

View file

@ -42,10 +42,10 @@ function getSeverityStyles(severity: string) {
} }
default: default:
return { return {
bg: 'bg-blue-500/10', bg: 'bg-sky-400/10',
border: 'border-blue-500', border: 'border-sky-400',
icon: Info, icon: Info,
iconColor: 'text-blue-500', iconColor: 'text-sky-400',
} }
} }
} }
@ -70,7 +70,7 @@ function ToastItem({
return ( return (
<div <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} onClick={onNavigate}
role="alert" role="alert"
> >

View file

@ -242,7 +242,7 @@ export default function TopologyGraph({
}, [option]) }, [option])
return ( 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 <ReactECharts
ref={chartRef} ref={chartRef}
option={option} 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="text-xs text-slate-400 font-medium mb-2">Node Type</div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-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> <span className="text-xs text-slate-500">Infrastructure</span>
</div> </div>
<div className="flex items-center gap-2"> <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> <span className="text-xs text-slate-500">Client</span>
</div> </div>
</div> </div>

View file

@ -156,7 +156,7 @@ export default function AdapterConfig() {
if (loading) { if (loading) {
return ( 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 <Loader2 className="w-5 h-5 animate-spin" /> Loading adapter config
</div> </div>
) )
@ -179,14 +179,14 @@ export default function AdapterConfig() {
return ( return (
<div className="p-6 space-y-4"> <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" /> <Sliders className="w-5 h-5" />
<h1 className="text-xl font-semibold">Adapter Config</h1> <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 {Object.values(config).reduce((n, l) => n + l.length, 0)} settings across {allAdapters.length} adapters
</span> </span>
</div> </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). Per-adapter tunables (thresholds, freshness windows, toggles, curation lists).
Changes take effect on the next handler call -- no container restart needed. 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 &amp; 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. 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 &amp; 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 metaId = `meta:${adapter}`
const metaStatus = saveStatus[metaId] || 'idle' const metaStatus = saveStatus[metaId] || 'idle'
return ( 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 */} {/* Card header */}
<div className="p-4 flex items-start gap-4"> <div className="p-4 flex items-start gap-4">
<button <button
onClick={() => setExpanded((e) => ({ ...e, [adapter]: !e[adapter] }))} onClick={() => setExpanded((e) => ({ ...e, [adapter]: !e[adapter] }))}
className="text-slate-400 hover:text-white" className="text-[#444] hover:text-white"
aria-label="toggle expand" aria-label="toggle expand"
> >
{isExpanded {isExpanded
@ -217,27 +217,27 @@ export default function AdapterConfig() {
</button> </button>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="text-base font-semibold text-slate-100">{m.display_name}</h2> <h2 className="text-base font-semibold text-white">{m.display_name}</h2>
<code className="text-xs text-slate-500">{adapter}</code> <code className="text-xs text-[#333]">{adapter}</code>
{rows.length > 0 && ( {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 && ( {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> </div>
{m.description && ( {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> </div>
{/* include_in_llm_context toggle */} {/* 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 <input
type="checkbox" type="checkbox"
checked={m.include_in_llm_context} checked={m.include_in_llm_context}
onChange={(e) => putMeta(adapter, { include_in_llm_context: e.target.checked })} 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 LLM context
<SaveBadge status={metaStatus} error={saveError[metaId]} /> <SaveBadge status={metaStatus} error={saveError[metaId]} />
@ -246,7 +246,7 @@ export default function AdapterConfig() {
{/* Expanded body */} {/* Expanded body */}
{isExpanded && rows.length > 0 && ( {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) => ( {rows.map((row) => (
<KeyRow <KeyRow
key={row.key} 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="px-6 py-3 flex items-start gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="text-sm font-mono text-cyan-300">{row.key}</code> <code className="text-sm font-mono text-accent">{row.key}</code>
<span className="text-xs text-slate-500">[{row.type}]</span> <span className="text-xs text-[#333]">[{row.type}]</span>
{!isDefault && ( {!isDefault && (
<span className="text-xs text-amber-400">edited</span> <span className="text-xs text-accent">edited</span>
)} )}
</div> </div>
{row.description && ( {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> </div>
@ -319,11 +319,11 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
type="checkbox" type="checkbox"
checked={row.value === true} checked={row.value === true}
onChange={(e) => onCommit(e.target.checked)} onChange={(e) => onCommit(e.target.checked)}
className="w-5 h-5 accent-cyan-500" className="w-5 h-5 accent-[#f59e0b]"
/> />
) : row.type === 'json' ? ( ) : row.type === 'json' ? (
<textarea <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} value={draft}
onChange={(e) => setDraft(e.target.value)} onChange={(e) => setDraft(e.target.value)}
onBlur={commit} onBlur={commit}
@ -332,7 +332,7 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
<input <input
type={row.type === 'int' || row.type === 'float' ? 'number' : 'text'} type={row.type === 'int' || row.type === 'float' ? 'number' : 'text'}
step={row.type === 'float' ? 'any' : '1'} 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} value={draft}
onChange={(e) => setDraft(e.target.value)} onChange={(e) => setDraft(e.target.value)}
onBlur={commit} onBlur={commit}
@ -345,7 +345,7 @@ function KeyRow({ row, status, error, onCommit, onReset }: KeyRowProps) {
<button <button
onClick={onReset} onClick={onReset}
disabled={isDefault} 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" title="Reset to default"
> >
<RotateCcw className="w-4 h-4" /> <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 }) { 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 === 'saving') return <Loader2 className="w-4 h-4 text-accent animate-spin" />
if (status === 'saved') return <Check className="w-4 h-4 text-emerald-400" /> if (status === 'saved') return <Check className="w-4 h-4 text-green-500" />
if (status === 'error') return ( if (status === 'error') return (
<span title={error} className="text-red-400 cursor-help"> <span title={error} className="text-red-400 cursor-help">
<AlertCircle className="w-4 h-4" /> <AlertCircle className="w-4 h-4" />
</span> </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" /> return <span className="w-4 h-4" />
} }

View file

@ -76,10 +76,10 @@ function getSeverityStyles(severity: string) {
case 'routine': case 'routine':
default: default:
return { return {
bg: 'bg-blue-500/10', bg: 'bg-sky-400/10',
border: 'border-blue-500', border: 'border-sky-400',
badge: 'bg-blue-500/20 text-blue-400', badge: 'bg-sky-400/20 text-sky-400',
iconColor: 'text-blue-500', iconColor: 'text-sky-400',
} }
} }
} }
@ -129,7 +129,7 @@ function ActiveAlertCard({
const Icon = getAlertIcon(alert.type) const Icon = getAlertIcon(alert.type)
return ( 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"> <div className="flex items-start gap-3">
<Icon size={20} className={styles.iconColor} /> <Icon size={20} className={styles.iconColor} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@ -197,7 +197,7 @@ function AlertHistoryTable({
const severities = ["all", "immediate", "priority", "routine"] const severities = ["all", "immediate", "priority", "routine"]
return ( return (
<div className="bg-bg-card border border-border rounded-lg"> <div className="bg-bg-card border border-border">
{/* Filters */} {/* Filters */}
<div className="p-4 border-b border-border flex items-center gap-4"> <div className="p-4 border-b border-border flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -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-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) => ( {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-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) => ( {severities.map((s) => (
<option key={s} value={s}> <option key={s} value={s}>
@ -352,10 +352,10 @@ function SubscriptionCard({ subscription, nodes }: { subscription: Subscription;
const Icon = getTypeIcon() const Icon = getTypeIcon()
return ( 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="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center"> <div className="w-10 h-10 bg-sky-400/10 flex items-center justify-center">
<Icon size={18} className="text-blue-400" /> <Icon size={18} className="text-sky-400" />
</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">
@ -490,7 +490,7 @@ export default function Alerts() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Active Alerts */} {/* 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"> <h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
<AlertTriangle size={14} /> <AlertTriangle size={14} />
Active Alerts ({visibleAlerts.length}) Active Alerts ({visibleAlerts.length})
@ -538,7 +538,7 @@ export default function Alerts() {
</div> </div>
{/* Subscriptions */} {/* 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"> <h2 className="text-sm font-medium text-slate-400 mb-4 flex items-center gap-2">
<Users size={14} /> <Users size={14} />
Mesh Subscriptions ({subscriptions.length}) Mesh Subscriptions ({subscriptions.length})
@ -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-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> </p>
</div> </div>
)} )}

View file

@ -340,7 +340,7 @@ function InfoButton({ info, link, linkText = 'Learn more' }: { info: string; lin
? ?
</button> </button>
{open && ( {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 <button
type="button" type="button"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
@ -628,7 +628,7 @@ function AlertRuleToggle({ label, description, checked, onChange, threshold, onT
thresholdSuffix?: string thresholdSuffix?: string
}) { }) {
return ( 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 items-center justify-between">
<div className="flex-1"> <div className="flex-1">
<span className="text-sm text-slate-300">{label}</span> <span className="text-sm text-slate-300">{label}</span>
@ -1299,7 +1299,7 @@ function MeshSourceCard({ source, onChange, onDelete }: {
} }
return ( return (
<div className="border border-[#1e2a3a] rounded-lg overflow-hidden"> <div className="border border-[#1e2a3a] overflow-hidden">
<div <div
className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer" className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer"
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
@ -1402,7 +1402,7 @@ function MeshSourcesSection({ data, onChange }: { data: MeshSourceConfig[]; onCh
))} ))}
<button <button
onClick={addSource} 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 <Plus size={16} /> Add Source
</button> </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." /> <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> </label>
{data.regions.map((region, i) => ( {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 <div
className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer" className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer"
onClick={() => setExpandedRegion(expandedRegion === i ? null : i)} onClick={() => setExpandedRegion(expandedRegion === i ? null : i)}
@ -1580,7 +1580,7 @@ function MeshIntelligenceSection({ data, onChange }: { data: MeshIntelligenceCon
onChange({ ...data, regions: [...data.regions, newRegion] }) onChange({ ...data, regions: [...data.regions, newRegion] })
setExpandedRegion(data.regions.length) 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 <Plus size={16} /> Add Region
</button> </button>
@ -1974,7 +1974,7 @@ export default function Config() {
</div> </div>
{restartRequired && ( {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"> <div className="flex items-center gap-2 text-amber-400">
<AlertTriangle size={16} /> <AlertTriangle size={16} />
<span className="text-sm">Restart required for changes to take effect</span> <span className="text-sm">Restart required for changes to take effect</span>
@ -1989,21 +1989,21 @@ export default function Config() {
)} )}
{error && ( {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} /> <X size={16} />
<span className="text-sm">{error}</span> <span className="text-sm">{error}</span>
</div> </div>
)} )}
{success && ( {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} /> <Check size={16} />
<span className="text-sm">{success}</span> <span className="text-sm">{success}</span>
</div> </div>
)} )}
<div className="flex-1 overflow-y-auto pr-2"> <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()} {renderSection()}
</div> </div>
</div> </div>

View file

@ -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 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' const lastFetch = feed.last_fetch ? new Date(feed.last_fetch * 1000).toLocaleTimeString() : 'Never'
return ( 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 justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${color}`} /> <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> </div>
<span className="text-xs text-slate-400">{text}</span> <span className="text-xs text-[#444]">{text}</span>
</div> </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>Events: {feed.event_count}</div>
<div>Last fetch: {lastFetch}</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>
</div> </div>
) )
@ -116,19 +116,19 @@ function EventCard({ event }: { event: EnvEvent }) {
const styles = (sev === 'extreme' || sev === 'severe' || sev === 'immediate') const styles = (sev === 'extreme' || sev === 'severe' || sev === 'immediate')
? { bg: 'bg-red-500/10', border: 'border-red-500', Icon: AlertCircle, color: 'text-red-500' } ? { bg: 'bg-red-500/10', border: 'border-red-500', Icon: AlertCircle, color: 'text-red-500' }
: (sev === 'moderate' || sev === 'warning' || sev === 'priority') : (sev === 'moderate' || sev === 'warning' || sev === 'priority')
? { bg: 'bg-amber-500/10', border: 'border-amber-500', Icon: AlertTriangle, color: 'text-amber-500' } ? { bg: 'bg-accent/10', border: 'border-amber-500', Icon: AlertTriangle, color: 'text-accent' }
: { bg: 'bg-blue-500/10', border: 'border-blue-500', Icon: Info, color: 'text-blue-500' } : { bg: 'bg-sky-400/10', border: 'border-sky-400', Icon: Info, color: 'text-sky-400' }
const Icon = styles.Icon const Icon = styles.Icon
return ( 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"> <div className="flex items-start gap-3">
<Icon size={16} className={styles.color} /> <Icon size={16} className={styles.color} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <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-sm font-medium text-white">{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-xs px-1.5 py-0.5 ${styles.bg} ${styles.color}`}>{event.severity}</span>
</div> </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> </div>
</div> </div>
@ -141,19 +141,19 @@ function FeedSourceToggle({ value, onChange, disabled, centralDisabled }: {
}) { }) {
const base = 'px-2 py-1 text-xs transition-colors' const base = 'px-2 py-1 text-xs transition-colors'
return ( 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 <button
type="button" type="button"
disabled={disabled} disabled={disabled}
onClick={() => onChange('native')} 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> >native</button>
<button <button
type="button" type="button"
disabled={disabled || centralDisabled} disabled={disabled || centralDisabled}
title={centralDisabled ? 'Central not available for this adapter' : ''} title={centralDisabled ? 'Central not available for this adapter' : ''}
onClick={() => { if (!centralDisabled) onChange('central') }} 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> >central</button>
</div> </div>
) )
@ -169,35 +169,35 @@ function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedS
}) { }) {
const centralDisabled = nativeOnly || !hasCentral const centralDisabled = nativeOnly || !hasCentral
return ( 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 className="flex items-center justify-between">
<div> <div>
<span className="text-sm font-medium text-slate-300">{title}</span> <span className="text-sm font-medium text-[#e0e0e0]">{title}</span>
{subtitle && <p className="text-xs text-slate-600">{subtitle}</p>} {subtitle && <p className="text-xs text-[#333]">{subtitle}</p>}
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-1"> <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} /> <FeedSourceToggle value={feedSource} onChange={onFeedSource} disabled={!enabled} centralDisabled={centralDisabled} />
</div> </div>
<Toggle label="" checked={enabled} onChange={onEnabled} /> <Toggle label="" checked={enabled} onChange={onEnabled} />
</div> </div>
</div> </div>
{!hasKey && ( {!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 API key not configured contact admin
</div> </div>
)} )}
{nativeOnly && ( {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'}> <div className={enabled ? 'space-y-3' : 'space-y-3 opacity-40 pointer-events-none select-none'}>
{children} {children}
</div> </div>
{(health || (events && events.length > 0)) && ( {(health || (events && events.length > 0)) && (
<div className="pt-2 border-t border-[#1e2a3a] space-y-3"> <div className="pt-2 border-t border-border space-y-3">
<div className="text-[10px] uppercase tracking-wide text-slate-600">Live status</div> <div className="text-[10px] uppercase tracking-wide text-[#333]">Live status</div>
{health ? <FeedStatusCard feed={health} /> : <div className="text-xs text-slate-600">No status reported.</div>} {health ? <FeedStatusCard feed={health} /> : <div className="text-xs text-[#333]">No status reported.</div>}
{events && events.length > 0 && ( {events && events.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{events.slice(0, 5).map((e, i) => <EventCard key={i} event={e} />)} {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 }) 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> 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 => const healthFor = (key: AdapterKey): FeedHealth | undefined =>
@ -663,10 +663,10 @@ const save = async () => {
</> </>
)} )}
{env.nws.feed_source === 'central' && ( {env.nws.feed_source === 'central' && (
<div className="border-t border-slate-700/50 pt-4 mt-4"> <div className="border-t border-border pt-4 mt-4">
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Filters</div> <div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#333] mb-3">Broadcast Filters</div>
<div className="mb-3"> <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"> <div className="flex gap-6">
{['Extreme', 'Severe', 'Moderate', 'Minor'].map((sev) => ( {['Extreme', 'Severe', 'Moderate', 'Minor'].map((sev) => (
<label key={sev} className="flex items-center gap-2 cursor-pointer"> <label key={sev} className="flex items-center gap-2 cursor-pointer">
@ -675,8 +675,8 @@ const save = async () => {
const cur = nwsConfig.broadcast_severities const cur = nwsConfig.broadcast_severities
setNwsConfig({ ...nwsConfig, broadcast_severities: e.target.checked ? [...cur, sev] : cur.filter(s => s !== sev) }) setNwsConfig({ ...nwsConfig, broadcast_severities: e.target.checked ? [...cur, sev] : cur.filter(s => s !== sev) })
}} }}
className="w-4 h-4 rounded accent-blue-500" /> className="w-4 h-4 accent-[#f59e0b]" />
<span className="text-sm text-slate-300">{sev}</span> <span className="text-sm font-sans text-[#e0e0e0]">{sev}</span>
</label> </label>
))} ))}
</div> </div>
@ -690,7 +690,7 @@ const save = async () => {
case 'swpc': return ( case 'swpc': return (
<div className="space-y-6"> <div className="space-y-6">
<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">
Broadcast Thresholds Broadcast Thresholds
</div> </div>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
@ -741,28 +741,28 @@ const save = async () => {
</div> </div>
)} )}
<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"> <div className="flex gap-6">
{[['WF', 'Wildfire'], ['RX', 'Prescribed Burn'], ['OTHER', 'Other']].map(([val, label]) => ( {[['WF', 'Wildfire'], ['RX', 'Prescribed Burn'], ['OTHER', 'Other']].map(([val, label]) => (
<label key={val} className="flex items-center gap-2 cursor-pointer"> <label key={val} className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={wfigsConfig.allowed_incident_types?.includes(val) ?? val === 'WF'} <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) }) }} 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" /> className="w-4 h-4 accent-[#f59e0b]" />
<span className="text-sm text-slate-300">{label}</span> <span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
</label> </label>
))} ))}
</div> </div>
</div> </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"> <div className="space-y-2">
<label className="flex items-center justify-between"> <label className="flex items-center justify-between">
<span className="text-sm text-slate-300">Broadcast on acres increase</span> <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 rounded accent-blue-500" /> <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>
<label className="flex items-center justify-between"> <label className="flex items-center justify-between">
<span className="text-sm text-slate-300">Broadcast on containment increase</span> <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 rounded accent-blue-500" /> <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> </label>
</div> </div>
</div> </div>
@ -788,7 +788,7 @@ const save = async () => {
onChange={(v) => up({ avalanche: { ...env.avalanche, center_ids: v } })} onChange={(v) => up({ avalanche: { ...env.avalanche, center_ids: v } })}
helper="e.g., SNFAC" infoLink="https://avalanche.org/avalanche-centers/" /> helper="e.g., SNFAC" infoLink="https://avalanche.org/avalanche-centers/" />
<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">
Broadcast Settings Broadcast Settings
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@ -820,7 +820,7 @@ const save = async () => {
</div> </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">
Magnitude Thresholds Magnitude Thresholds
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@ -839,10 +839,10 @@ const save = async () => {
</div> </div>
</div> </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 PAGER Alert Levels
</div> </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 Broadcast at any magnitude when USGS PAGER alert reaches these levels
</div> </div>
<div className="flex gap-6"> <div className="flex gap-6">
@ -857,8 +857,8 @@ const save = async () => {
? [...cur, level] ? [...cur, level]
: cur.filter((l) => l !== level) }}) : cur.filter((l) => l !== level) }})
}} }}
className="w-4 h-4 rounded accent-blue-500" /> className="w-4 h-4 accent-[#f59e0b]" />
<span className="text-sm text-slate-300 capitalize">{level}</span> <span className="text-sm font-sans text-[#e0e0e0] capitalize">{level}</span>
</label> </label>
))} ))}
</div> </div>
@ -868,42 +868,42 @@ const save = async () => {
case 'traffic': return (<> 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" /> <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} /> <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) => ( {(env.traffic.corridors || []).map((c, i) => (
<div key={i} className="grid grid-cols-4 gap-2 items-end"> <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 } }) }} /> <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="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} /> <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> </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> <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="border-t border-border pt-4 mt-4">
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Filters</div> <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 className="grid grid-cols-2 gap-4">
<div> <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 <select
value={tomtomConfig.min_magnitude} value={tomtomConfig.min_magnitude}
onChange={(e) => setTomtomConfig({...tomtomConfig, min_magnitude: parseInt(e.target.value)})} 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={1}>1 Minor (all)</option>
<option value={2}>2 Moderate (yellow+)</option> <option value={2}>2 Moderate (yellow+)</option>
<option value={3}>3 Major (orange+)</option> <option value={3}>3 Major (orange+)</option>
<option value={4}>4 Severe (red only)</option> <option value={4}>4 Severe (red only)</option>
</select> </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> </div>
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
<label className="flex items-center justify-between"> <label className="flex items-center justify-between">
<span className="text-sm text-slate-300">Drop non-present time validity</span> <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 rounded accent-blue-500" /> <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>
<label className="flex items-center justify-between"> <label className="flex items-center justify-between">
<span className="text-sm text-slate-300">Drop zero-magnitude events</span> <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 rounded accent-blue-500" /> <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> </label>
</div> </div>
</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} /> <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>
<div className="border-t border-slate-700/50 pt-4 mt-4"> <div className="border-t border-border pt-4 mt-4">
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Filters</div> <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 className="grid grid-cols-2 gap-4">
<div> <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 <select
value={roads511Config.min_severity} value={roads511Config.min_severity}
onChange={(e) => setRoads511Config({...roads511Config, min_severity: e.target.value})} 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="None">None (all)</option>
<option value="Minor">Minor+</option> <option value="Minor">Minor+</option>
<option value="Major">Major only</option> <option value="Major">Major only</option>
</select> </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> </div>
<div className="mt-4"> <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"> <div className="flex gap-6">
{([['incident', 'Incident'], ['closure', 'Closure'], ['special_event', 'Special Event']] as const).map(([val, label]) => ( {([['incident', 'Incident'], ['closure', 'Closure'], ['special_event', 'Special Event']] as const).map(([val, label]) => (
<label key={val} className="flex items-center gap-2 cursor-pointer"> <label key={val} className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={roads511Config.enabled_categories.includes(val)} <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)}) }} 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" /> className="w-4 h-4 accent-[#f59e0b]" />
<span className="text-sm text-slate-300">{label}</span> <span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
</label> </label>
))} ))}
</div> </div>
</div> </div>
<div className="mt-4"> <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"> <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]) => ( {([['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"> <label key={val} className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={roads511Config.enabled_sub_types.includes(val)} <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)}) }} 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" /> className="w-4 h-4 accent-[#f59e0b]" />
<span className="text-sm text-slate-300">{label}</span> <span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
</label> </label>
))} ))}
</div> </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} /> <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>
<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="border-t border-border pt-4 mt-4">
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Settings</div> <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"> <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} <input type="checkbox" checked={wzdxConfig.broadcast}
onChange={(e) => setWzdxConfig({...wzdxConfig, broadcast: e.target.checked})} 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> </label>
{wzdxConfig.broadcast ? ( {wzdxConfig.broadcast ? (
<div className="space-y-3 mt-3"> <div className="space-y-3 mt-3">
<div> <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 <select
value={wzdxConfig.min_severity} value={wzdxConfig.min_severity}
onChange={(e) => setWzdxConfig({...wzdxConfig, min_severity: e.target.value})} 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="None">None (all)</option>
<option value="Minor">Minor+</option> <option value="Minor">Minor+</option>
@ -1001,21 +1001,21 @@ const save = async () => {
</select> </select>
</div> </div>
<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"> <div className="flex gap-6">
{([['road_works', 'Road Works'], ['lane_closed', 'Lane Closure'], ['road_closed', 'Road Closed']] as const).map(([val, label]) => ( {([['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"> <label key={val} className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={wzdxConfig.sub_types.includes(val)} <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)}) }} 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" /> className="w-4 h-4 accent-[#f59e0b]" />
<span className="text-sm text-slate-300">{label}</span> <span className="text-sm font-sans text-[#e0e0e0]">{label}</span>
</label> </label>
))} ))}
</div> </div>
</div> </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> </div>
</>) </>)
@ -1047,15 +1047,15 @@ const save = async () => {
<div className="space-y-6"> <div className="space-y-6">
{/* Header + master enable + save bar */} {/* Header + master enable + save bar */}
<div className="flex items-center justify-between"> <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"> <div className="flex items-center gap-3">
<Toggle label="Feeds Enabled" checked={env.enabled} onChange={(v) => up({ enabled: v })} /> <Toggle label="Feeds Enabled" checked={env.enabled} onChange={(v) => up({ enabled: v })} />
{hasChanges && ( {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 <RotateCcw size={14} /> Discard
</button> </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'} <Save size={14} /> {saving ? 'Saving…' : 'Save'}
</button> </button>
</> </>
@ -1063,12 +1063,12 @@ const save = async () => {
</div> </div>
</div> </div>
{error && <div className="text-sm text-red-400 bg-red-500/10 rounded p-3">{error}</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 rounded p-3">{success}</div>} {success && <div className="text-sm text-green-400 bg-green-500/10 p-3">{success}</div>}
{restartRequired && ( {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> <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> </div>
)} )}
@ -1077,7 +1077,7 @@ const save = async () => {
<div className="flex gap-1 border-b border-border overflow-x-auto"> <div className="flex gap-1 border-b border-border overflow-x-auto">
{FAMILIES.map(({ key, label, icon: Icon }) => ( {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) }} <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} <Icon size={15} /> {label}
</button> </button>
))} ))}
@ -1085,11 +1085,11 @@ const save = async () => {
{/* Central Connection tab */} {/* Central Connection tab */}
{family === 'central' && env.central && ( {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 className="flex items-center justify-between">
<div> <div>
<span className="text-sm font-medium text-slate-300">Central Connection</span> <span className="text-sm font-medium text-[#e0e0e0]">Central Connection</span>
<p className="text-xs text-slate-600">NATS JetStream source for any adapter set to "central"</p> <p className="text-xs text-[#333]">NATS JetStream source for any adapter set to "central"</p>
</div> </div>
<Toggle label="" checked={!!env.central.enabled} <Toggle label="" checked={!!env.central.enabled}
onChange={(v) => up({ central: { ...env.central!, enabled: v } })} /> onChange={(v) => up({ central: { ...env.central!, enabled: v } })} />
@ -1113,25 +1113,25 @@ const save = async () => {
{/* Tracking placeholder */} {/* Tracking placeholder */}
{family === 'tracking' && ( {family === 'tracking' && (
<div className="flex flex-col items-center justify-center h-[40vh] text-center"> <div className="flex flex-col items-center justify-center h-[40vh] text-center">
<Satellite size={32} className="text-slate-600 mb-4" /> <Satellite size={32} className="text-[#333] mb-4" />
<p className="text-slate-500 max-w-md">No adapters yet. ADS-B / AIS / satellite passes are planned for v0.5.</p> <p className="text-[#333] max-w-md">No adapters yet. ADS-B / AIS / satellite passes are planned for v0.5.</p>
</div> </div>
)} )}
{/* Mesh Health (no env adapters; central greyed for future migration) */} {/* Mesh Health (no env adapters; central greyed for future migration) */}
{family === 'mesh' && ( {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 className="flex items-center justify-between">
<div> <div>
<span className="text-sm font-medium text-slate-300">Mesh Health</span> <span className="text-sm font-medium text-[#e0e0e0]">Mesh Health</span>
<p className="text-xs text-slate-600">Node/infra telemetry sourced from the mesh, not an environmental feed.</p> <p className="text-xs text-[#333]">Node/infra telemetry sourced from the mesh, not an environmental feed.</p>
</div> </div>
<div className="flex items-center gap-1"> <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} /> <FeedSourceToggle value="native" onChange={() => {}} disabled={false} centralDisabled={true} />
</div> </div>
</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> </div>
)} )}
@ -1142,7 +1142,7 @@ const save = async () => {
<div className="flex gap-1"> <div className="flex gap-1">
{fam.adapters.map((k) => ( {fam.adapters.map((k) => (
<button key={k} onClick={() => setAdapter(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} {META[k].label}
</button> </button>
))} ))}

View file

@ -92,7 +92,7 @@ export default function GaugeSites() {
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div className="flex items-center gap-2"> <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> <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}
@ -106,7 +106,7 @@ 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 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"> <table className="w-full text-sm text-slate-200">
<thead className="bg-slate-900 text-xs text-slate-400 uppercase"> <thead className="bg-slate-900 text-xs text-slate-400 uppercase">
<tr> <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-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-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"> <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> <button onClick={() => remove(r.site_id)} className="text-red-400 hover:text-red-300"><Trash2 className="w-4 h-4 inline" /></button>
</td> </td>
</tr> </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))} /> 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">
<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 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">

View file

@ -76,7 +76,7 @@ export default function Mesh() {
</div> </div>
{/* View toggle */} {/* 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 <button
onClick={() => setViewMode('topo')} onClick={() => setViewMode('topo')}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${ className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${

View file

@ -349,7 +349,7 @@ function InfoButton({ info }: { info: string }) {
{open && ( {open && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} /> <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} {info}
</div> </div>
</> </>
@ -584,7 +584,7 @@ function SeveritySelector({ value, onChange }: {
{isOpen && ( {isOpen && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} /> <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) => ( {SEVERITY_OPTIONS.map((opt) => (
<button <button
key={opt.value} key={opt.value}
@ -915,7 +915,7 @@ function NotificationRuleCard({
} }
return ( 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 */} {/* Header */}
<div <div
className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer" className="flex items-center justify-between p-3 bg-[#0a0e17] cursor-pointer"
@ -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-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" /> <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-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" title="Test rule"
> >
<Send size={14} /> <Send size={14} />
@ -1034,7 +1034,7 @@ function NotificationRuleCard({
<button <button
type="button" type="button"
onClick={() => onChange({ ...rule, trigger_type: 'condition' })} 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' rule.trigger_type !== 'schedule'
? 'bg-accent/10 border-accent text-accent' ? 'bg-accent/10 border-accent text-accent'
: 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200' : 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200'
@ -1046,7 +1046,7 @@ function NotificationRuleCard({
<button <button
type="button" type="button"
onClick={() => onChange({ ...rule, trigger_type: 'schedule' })} 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' rule.trigger_type === 'schedule'
? 'bg-accent/10 border-accent text-accent' ? 'bg-accent/10 border-accent text-accent'
: 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200' : 'bg-[#0a0e17] border-[#1e2a3a] text-slate-400 hover:text-slate-200'
@ -1065,7 +1065,7 @@ function NotificationRuleCard({
{/* WHEN section - Condition trigger */} {/* WHEN section - Condition trigger */}
{rule.trigger_type !== 'schedule' && ( {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"> <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
<AlertTriangle size={14} /> <AlertTriangle size={14} />
WHEN (Condition) WHEN (Condition)
@ -1106,7 +1106,7 @@ function NotificationRuleCard({
{/* WHEN section - Schedule trigger */} {/* WHEN section - Schedule trigger */}
{rule.trigger_type === 'schedule' && ( {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"> <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
<Calendar size={14} /> <Calendar size={14} />
WHEN (Schedule) WHEN (Schedule)
@ -1197,7 +1197,7 @@ function NotificationRuleCard({
)} )}
{/* REGIONS section — scope rule to specific regions; empty = all regions */} {/* 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"> <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
<MapPin size={14} /> <MapPin size={14} />
REGIONS REGIONS
@ -1235,7 +1235,7 @@ function NotificationRuleCard({
</div> </div>
{/* SEND VIA section */} {/* 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"> <div className="flex items-center gap-2 text-sm font-medium text-slate-300">
<Send size={14} /> <Send size={14} />
SEND VIA SEND VIA
@ -1262,7 +1262,7 @@ function NotificationRuleCard({
{/* No delivery warning */} {/* No delivery warning */}
{!rule.delivery_type && ( {!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" /> <AlertCircle size={16} className="text-amber-400 mt-0.5 flex-shrink-0" />
<div className="text-sm text-amber-300"> <div className="text-sm text-amber-300">
Rule will log matches but not deliver until a delivery method is configured. Rule will log matches but not deliver until a delivery method is configured.
@ -1402,7 +1402,7 @@ function NotificationRuleCard({
{rule.trigger_type !== 'schedule' && ( {rule.trigger_type !== 'schedule' && (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs text-slate-500 uppercase tracking-wide">Example Message</label> <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> <p className="text-sm text-slate-300 font-mono">{getExampleMessage()}</p>
</div> </div>
<p className="text-xs text-slate-600">This is an example of what this rule would send.</p> <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 ( 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) || []))} {TOGGLE_FAMILY_META.map(f => renderGroup(f.key, f.label, f.Icon, byFamily.get(f.key) || []))}
{renderGroup('other', 'Other', null, other)} {renderGroup('other', 'Other', null, other)}
</div> </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 chanCount = Object.values(t.severity_channels || {}).reduce((n, arr) => n + ((arr as string[])?.length || 0), 0)
const regionCount = (t.regions || []).length const regionCount = (t.regions || []).length
return ( 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"> <div className="flex items-center justify-between">
<button type="button" onClick={() => setExpanded(isOpen ? null : key)} <button type="button" onClick={() => setExpanded(isOpen ? null : key)}
className="flex items-center gap-2 text-sm text-slate-200"> className="flex items-center gap-2 text-sm text-slate-200">
@ -1826,7 +1826,7 @@ export default function Notifications() {
{/* Test Dialog */} {/* Test Dialog */}
{testDialog.open && ( {testDialog.open && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <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]"> <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> <h3 className="text-lg font-semibold">Test Notification Rule</h3>
<button onClick={closeTestDialog} className="text-slate-500 hover:text-slate-300"> <button onClick={closeTestDialog} className="text-slate-500 hover:text-slate-300">
@ -2033,19 +2033,19 @@ export default function Notifications() {
{/* Status messages */} {/* Status messages */}
{error && ( {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} {error}
</div> </div>
)} )}
{success && ( {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" /> <Check size={14} className="inline mr-2" />
{success} {success}
</div> </div>
)} )}
{/* Main content */} {/* 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 <Toggle
label="Enable Notifications" label="Enable Notifications"
checked={config.enabled} checked={config.enabled}
@ -2056,7 +2056,7 @@ export default function Notifications() {
{config.enabled && ( {config.enabled && (
<> {/* Cold-start grace -- v0.5.8b */} <> {/* 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"> <div className="flex items-center gap-2">
<label className="text-xs text-slate-500 uppercase tracking-wide">Cold-start grace</label> <label className="text-xs text-slate-500 uppercase tracking-wide">Cold-start grace</label>
</div> </div>
@ -2072,7 +2072,7 @@ export default function Notifications() {
</div> </div>
{/* Band Conditions -- v0.5.11 */} {/* 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"> <div className="flex items-center gap-2">
<label className="text-xs text-slate-500 uppercase tracking-wide">Band Conditions (HF propagation)</label> <label className="text-xs text-slate-500 uppercase tracking-wide">Band Conditions (HF propagation)</label>
</div> </div>
@ -2164,21 +2164,21 @@ export default function Notifications() {
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={addRule} 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 <Plus size={16} /> Add Rule
</button> </button>
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowTemplates(!showTemplates)} 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 <Layers size={16} /> Add from Template
</button> </button>
{showTemplates && ( {showTemplates && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => setShowTemplates(false)} /> <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> <div className="p-2 border-b border-[#2a3a4a] text-xs text-slate-500 uppercase">Rule Templates</div>
{RULE_TEMPLATES.map((t) => ( {RULE_TEMPLATES.map((t) => (
<button <button

View file

@ -64,7 +64,7 @@ export default function TownAnchors() {
return ( return (
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div className="flex items-center gap-2"> <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> <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}
@ -82,7 +82,7 @@ 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 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"> <table className="w-full text-sm text-slate-200">
<thead className="bg-slate-900 text-xs text-slate-400 uppercase"> <thead className="bg-slate-900 text-xs text-slate-400 uppercase">
<tr> <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 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-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"> <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> <button onClick={() => remove(r.anchor_id)} className="text-red-400 hover:text-red-300"><Trash2 className="w-4 h-4 inline" /></button>
</td> </td>
</tr> </tr>
@ -136,7 +136,7 @@ function RowEditor({ draft, setDraft, onSave, onCancel, adding }: {
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">
<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 Enabled
</label> </label>
<label className="text-xs text-slate-400">Lat <label className="text-xs text-slate-400">Lat

View file

@ -8,8 +8,8 @@
<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-B6zGwxmY.js"></script> <script type="module" crossorigin src="/assets/index-7P5nbdAV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DqWqopB2.css"> <link rel="stylesheet" crossorigin href="/assets/index-EzV2LMjq.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>