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:
Matt Johnson (via Claude) 2026-06-10 06:53:48 +00:00
commit af51c51708
4 changed files with 150 additions and 118 deletions

View file

@ -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>
) )
} }

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-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>