mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-10 17:04:45 +02:00
feat: dashboard layout — tabbed alerts/feed, tropo in middle row, fix emoji
- Active Alerts panel now has pill-style tab switcher (Active Alerts | Event Feed) - LiveEventFeed supports embedded mode (no card wrapper) for tab use - HepburnTropoCard moved from bottom row to middle row (replaces LiveEventFeed) - Bottom row removed (empty after tropo card move) - Fixed broken unicode escapes in BandConditionsCard (satellite, circle, moon emoji) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3e06aacc3f
commit
af51c51708
4 changed files with 150 additions and 118 deletions
|
|
@ -159,16 +159,16 @@ function StatCard({ icon: Icon, label, value, subvalue }: { icon: typeof Radio;
|
||||||
function BandConditionsCard({ bandConditions }: { bandConditions: BandConditionsStatus | null }) {
|
function BandConditionsCard({ bandConditions }: { bandConditions: BandConditionsStatus | null }) {
|
||||||
const getRatingEmoji = (rating?: string) => {
|
const getRatingEmoji = (rating?: string) => {
|
||||||
switch (rating) {
|
switch (rating) {
|
||||||
case 'Good': return '\ud83d\udfe2' // green circle
|
case 'Good': return '🟢' // green circle
|
||||||
case 'Fair': return '\ud83d\udfe1' // yellow circle
|
case 'Fair': return '🟡' // yellow circle
|
||||||
case 'Poor': return '\ud83d\udd34' // red circle
|
case 'Poor': return '🔴' // red circle
|
||||||
default: return '\u2014'
|
default: return '—'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSlotEmoji = (label?: string) => {
|
const getSlotEmoji = (label?: string) => {
|
||||||
if (!label) return ''
|
if (!label) return ''
|
||||||
return label.includes('Night') ? '\ud83c\udf19' : '\u2600\ufe0f'
|
return label.includes('Night') ? '🌙' : '☀️'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bandConditions?.enabled || !bandConditions?.ratings) {
|
if (!bandConditions?.enabled || !bandConditions?.ratings) {
|
||||||
|
|
@ -204,7 +204,7 @@ function BandConditionsCard({ bandConditions }: { bandConditions: BandConditions
|
||||||
|
|
||||||
{/* Band conditions header */}
|
{/* Band conditions header */}
|
||||||
<div className="text-xs text-slate-500 mb-3 flex items-center gap-1">
|
<div className="text-xs text-slate-500 mb-3 flex items-center gap-1">
|
||||||
<span>\ud83d\udce1</span> Band Conditions:
|
📡 Band Conditions:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Band rows */}
|
{/* Band rows */}
|
||||||
|
|
@ -215,7 +215,7 @@ function BandConditionsCard({ bandConditions }: { bandConditions: BandConditions
|
||||||
<div key={band} className="flex items-center justify-between px-2 py-1.5 rounded bg-bg-hover">
|
<div key={band} className="flex items-center justify-between px-2 py-1.5 rounded bg-bg-hover">
|
||||||
<span className="text-sm font-mono text-slate-300">{band}</span>
|
<span className="text-sm font-mono text-slate-300">{band}</span>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{getRatingEmoji(rating)} <span className="text-slate-300 ml-1">{rating || '\u2014'}</span>
|
{getRatingEmoji(rating)} <span className="text-slate-300 ml-1">{rating || '—'}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -433,7 +433,7 @@ function EventFeedItem({ event, isLocal }: { event: EnvEvent; isLocal?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live Event Feed Card
|
// Live Event Feed Card
|
||||||
function LiveEventFeed({ events, envStatus }: { events: EnvEvent[]; envStatus: EnvStatus | null }) {
|
function LiveEventFeed({ events, envStatus, embedded }: { events: EnvEvent[]; envStatus: EnvStatus | null; embedded?: boolean }) {
|
||||||
// Severity order for sorting
|
// Severity order for sorting
|
||||||
const severityOrder: Record<string, number> = { immediate: 0, priority: 1, routine: 2 }
|
const severityOrder: Record<string, number> = { immediate: 0, priority: 1, routine: 2 }
|
||||||
|
|
||||||
|
|
@ -473,13 +473,8 @@ function LiveEventFeed({ events, envStatus }: { events: EnvEvent[]; envStatus: E
|
||||||
return { total, active, errors, secAgo }
|
return { total, active, errors, secAgo }
|
||||||
}, [envStatus])
|
}, [envStatus])
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<div className="bg-bg-card border border-border rounded-lg p-4 flex flex-col h-full">
|
<>
|
||||||
<h2 className="text-sm font-medium text-slate-400 mb-3 flex items-center gap-2">
|
|
||||||
<Activity size={14} />
|
|
||||||
Live Event Feed
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{sortedEvents.length > 0 ? (
|
{sortedEvents.length > 0 ? (
|
||||||
<div className="flex-1 overflow-y-auto max-h-80 pr-1 -mr-1">
|
<div className="flex-1 overflow-y-auto max-h-80 pr-1 -mr-1">
|
||||||
{sortedEvents.map((event, i) => (
|
{sortedEvents.map((event, i) => (
|
||||||
|
|
@ -510,6 +505,18 @@ function LiveEventFeed({ events, envStatus }: { events: EnvEvent[]; envStatus: E
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (embedded) return <div className="flex flex-col h-full">{content}</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-bg-card border border-border rounded-lg p-4 flex flex-col h-full">
|
||||||
|
<h2 className="text-sm font-medium text-slate-400 mb-3 flex items-center gap-2">
|
||||||
|
<Activity size={14} />
|
||||||
|
Live Event Feed
|
||||||
|
</h2>
|
||||||
|
{content}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -521,6 +528,7 @@ export default function Dashboard() {
|
||||||
const [envStatus, setEnvStatus] = useState<EnvStatus | null>(null)
|
const [envStatus, setEnvStatus] = useState<EnvStatus | null>(null)
|
||||||
const [envEvents, setEnvEvents] = useState<EnvEvent[]>([])
|
const [envEvents, setEnvEvents] = useState<EnvEvent[]>([])
|
||||||
const [bandConditions, setBandConditions] = useState<BandConditionsStatus | null>(null)
|
const [bandConditions, setBandConditions] = useState<BandConditionsStatus | null>(null)
|
||||||
|
const [alertTab, setAlertTab] = useState<'alerts' | 'feed'>('alerts')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
|
@ -610,9 +618,33 @@ export default function Dashboard() {
|
||||||
|
|
||||||
{/* Alerts + Stats */}
|
{/* Alerts + Stats */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
{/* Active Alerts */}
|
{/* Active Alerts / Event Feed — tabbed */}
|
||||||
<div className="bg-bg-card border border-border rounded-lg p-6">
|
<div className="bg-bg-card border border-border rounded-lg p-6">
|
||||||
<h2 className="text-sm font-medium text-slate-400 mb-4">Active Alerts</h2>
|
<div className="flex items-center gap-1 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setAlertTab('alerts')}
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||||
|
alertTab === 'alerts'
|
||||||
|
? 'bg-slate-600 text-slate-100'
|
||||||
|
: 'text-slate-400 hover:text-slate-300 hover:bg-bg-hover'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Active Alerts
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setAlertTab('feed')}
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||||
|
alertTab === 'feed'
|
||||||
|
? 'bg-slate-600 text-slate-100'
|
||||||
|
: 'text-slate-400 hover:text-slate-300 hover:bg-bg-hover'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Event Feed
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{alertTab === 'alerts' ? (
|
||||||
|
<>
|
||||||
{alerts.length > 0 ? (
|
{alerts.length > 0 ? (
|
||||||
<div className="space-y-3 max-h-48 overflow-y-auto">
|
<div className="space-y-3 max-h-48 overflow-y-auto">
|
||||||
{alerts.map((alert, i) => (
|
{alerts.map((alert, i) => (
|
||||||
|
|
@ -661,6 +693,10 @@ export default function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<LiveEventFeed events={envEvents} envStatus={envStatus} embedded />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Stats */}
|
{/* Quick Stats */}
|
||||||
|
|
@ -692,14 +728,10 @@ export default function Dashboard() {
|
||||||
{/* RF Propagation */}
|
{/* RF Propagation */}
|
||||||
<BandConditionsCard bandConditions={bandConditions} />
|
<BandConditionsCard bandConditions={bandConditions} />
|
||||||
|
|
||||||
{/* Live Event Feed */}
|
{/* Tropo Forecast */}
|
||||||
<LiveEventFeed events={envEvents} envStatus={envStatus} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom row: Tropo Forecast */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<HepburnTropoCard />
|
<HepburnTropoCard />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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.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-DVlb83LX.js"></script>
|
<script type="module" crossorigin src="/assets/index-BUzOKXu1.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-kJaaQ570.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-LGjCLdSa.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue