'use client'; import React from 'react'; import styled from 'styled-components'; // ─── Types ─────────────────────────────────────────────────────────────────── export interface ServerEntry { id: string; label: string; type: 'sister' | 'dev' | 'docker'; status: 'online' | 'offline' | 'working' | 'unknown'; detail?: string | null; source: 'live' | 'snapshot' | 'fallback'; } interface ServerHealthPanelProps { servers: ServerEntry[]; dataMode: 'live' | 'snapshot' | 'fallback'; generatedAt: string; } // ─── Styled Components ──────────────────────────────────────────────────────── const Panel = styled.section` border: 1px solid var(--border-color); background: var(--bg-surface); padding: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-md); `; const PanelHeader = styled.div` display: flex; align-items: center; gap: var(--space-md); justify-content: space-between; `; const Eyebrow = styled.div` font-family: var(--font-mono); font-size: 10px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.08em; `; const WsDot = styled.span<{ $connected: boolean }>` display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: ${({ $connected }) => ($connected ? '#00FF00' : '#555')}; margin-right: 4px; vertical-align: middle; `; const FreshnessNote = styled.div` font-family: var(--font-mono); font-size: 10px; color: var(--text-secondary); opacity: 0.5; text-transform: uppercase; letter-spacing: 0.06em; `; const Grid = styled.div` display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--space-sm); @media (max-width: 767px) { grid-template-columns: repeat(2, 1fr); } @media (max-width: 359px) { grid-template-columns: 1fr; } `; const statusColors: Record = { online: '#00FF00', offline: '#FF1744', working: '#2979FF', unknown: '#555555', }; const ServerCard = styled.div<{ $status: ServerEntry['status'] }>` border: 1px solid var(--border-color); border-left: 2px solid ${({ $status }) => statusColors[$status]}; padding: var(--space-sm) var(--space-md); display: flex; flex-direction: column; gap: 4px; `; const CardLabel = styled.div` font-size: 12px; font-weight: 600; color: var(--text-primary); display: flex; align-items: center; gap: 6px; `; const StatusDot = styled.span<{ $status: ServerEntry['status'] }>` width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; background: ${({ $status }) => statusColors[$status]}; `; const CardStatus = styled.div<{ $status: ServerEntry['status'] }>` font-family: var(--font-mono); font-size: 10px; color: ${({ $status }) => statusColors[$status]}; text-transform: uppercase; letter-spacing: 0.06em; `; const CardDetail = styled.div` font-size: 11px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; const SourceBadge = styled.span` font-family: var(--font-mono); font-size: 9px; color: var(--text-secondary); opacity: 0.5; text-transform: uppercase; letter-spacing: 0.06em; `; // ─── Component ──────────────────────────────────────────────────────────────── function formatGeneratedAt(ts: string): string { try { const date = new Date(ts); const diff = Date.now() - date.getTime(); const sec = Math.floor(diff / 1000); if (sec < 60) return `${sec}s ago`; const min = Math.floor(sec / 60); if (min < 60) return `${min}m ago`; return `${Math.floor(min / 60)}h ago`; } catch { return ts; } } export default function ServerHealthPanel({ servers, dataMode, generatedAt, }: ServerHealthPanelProps) { const onlineCount = servers.filter((s) => s.status === 'online' || s.status === 'working').length; return ( server health · {dataMode} {' · '}{onlineCount}/{servers.length} online refreshed {formatGeneratedAt(generatedAt)} {servers.map((server) => ( {server.label} {server.status} {server.detail && {server.detail}} {server.source} ))} ); }