spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import { FreshnessBadge } from '@/components/ui/badges';3import { fmtAgo, fmtDateTime, fmtInt } from '@/lib/format';4import type { ConnectorStatus } from '@/lib/types';56/** "in 2 h" / "overdue 5 min" for scheduled timestamps (fmtAgo only handles the past). */7export function fmtUntil(v: string | null | undefined, now: number = Date.now()): string {8 if (!v) return '—';9 const t = new Date(v).getTime();10 if (Number.isNaN(t)) return '—';11 const s = Math.round((t - now) / 1000);12 if (s <= 0) return s > -60 ? 'due now' : `overdue ${fmtAgo(v, now).replace(' ago', '')}`;13 if (s < 60) return 'in < 1 min';14 const m = Math.round(s / 60);15 if (m < 60) return `in ${m} min`;16 const h = Math.round(m / 60);17 if (h < 48) return `in ${h} h`;18 return `in ${Math.round(h / 24)} d`;19}2021export function fmtDuration(ms: number | null | undefined): string {22 if (ms === null || ms === undefined) return '—';23 if (ms < 1000) return `${fmtInt(ms)} ms`;24 if (ms < 120_000) return `${(ms / 1000).toFixed(1)} s`;25 return `${(ms / 60_000).toFixed(1)} min`;26}2728export function fmtInterval(s: number | null | undefined): string {29 if (!s) return '—';30 if (s % 86400 === 0) return `${s / 86400} d`;31 if (s % 3600 === 0) return `${s / 3600} h`;32 return `${Math.round(s / 60)} min`;33}3435export function RunStatusPill({ status }: { status: string | null | undefined }) {36 const s = status ?? 'never run';37 const color = s === 'success' ? 'var(--active)' : s === 'failed' ? 'var(--danger)' : s === 'running' ? 'var(--accent)' : s === 'unchanged' ? 'var(--ink-2)' : 'var(--inactive)';38 return (39 <span className="mono inline-flex items-center gap-1.5 text-xs" style={{ color }}>40 <span className="dot" aria-hidden /> {s}41 </span>42 );43}4445/**46 * Connector health table (public /status, /status/data, admin overview). Stacks into cards under 768 px.47 * `last_error` is the error of the most recent *failed* run, which may be older than the last success — labelled accordingly.48 */49export function ConnectorsTable({ connectors, now = Date.now(), linkAdmin = false }: { connectors: ConnectorStatus[]; now?: number; linkAdmin?: boolean }) {50 return (51 <div className="overflow-x-auto">52 <table className="data-table stack min-w-0 md:min-w-[960px]">53 <thead>54 <tr>55 <th>Connector</th>56 <th>Freshness</th>57 <th>Last success</th>58 <th>Last status</th>59 <th>Next run</th>60 <th className="num">Duration</th>61 <th className="num">Failures</th>62 <th>Circuit</th>63 <th>Last failure</th>64 </tr>65 </thead>66 <tbody>67 {connectors.map((c) => (68 <tr key={c.name}>69 <td className="primary">70 {linkAdmin ? (71 <Link href={`/admin/connectors/${encodeURIComponent(c.name)}`} className="mono text-sm text-ink hover:text-accent">72 {c.name}73 </Link>74 ) : (75 <span className="mono text-sm text-ink">{c.name}</span>76 )}77 <div className="text-xs text-ink-3">78 {c.source_name} · every {fmtInterval(c.interval_seconds)}79 {!c.enabled && <span className="ml-2 text-warn">disabled</span>}80 </div>81 </td>82 <td data-label="Freshness">83 <FreshnessBadge status={c.freshness} />84 </td>85 <td data-label="Last success" title={fmtDateTime(c.last_success_at)}>86 <span className="text-sm">{fmtAgo(c.last_success_at, now)}</span>87 </td>88 <td data-label="Last status">89 <RunStatusPill status={c.last_status} />90 </td>91 <td data-label="Next run" title={fmtDateTime(c.next_run_at)}>92 <span className="text-sm text-ink-2">{c.enabled ? fmtUntil(c.next_run_at, now) : '—'}</span>93 </td>94 <td data-label="Duration" className="num tnum text-sm text-ink-2">95 {fmtDuration(c.last_duration_ms)}96 </td>97 <td data-label="Failures" className="num tnum text-sm">98 <span className={c.consecutive_failures > 0 ? 'text-warn' : 'text-ink-2'}>{fmtInt(c.consecutive_failures)}</span>99 </td>100 <td data-label="Circuit">101 {c.circuit_open_until && new Date(c.circuit_open_until).getTime() > now ? (102 <span className="text-xs text-danger" title={fmtDateTime(c.circuit_open_until)}>103 open · resets {fmtUntil(c.circuit_open_until, now)}104 </span>105 ) : (106 <span className="text-xs text-ink-3">closed</span>107 )}108 </td>109 <td data-label="Last failure" className="max-w-[280px]">110 {c.last_error ? (111 <span className="mono block truncate text-xs text-ink-3" title={c.last_error}>112 {c.last_error.length > 80 ? `${c.last_error.slice(0, 80)}…` : c.last_error}113 </span>114 ) : (115 <span className="text-xs text-ink-3">none recorded</span>116 )}117 </td>118 </tr>119 ))}120 </tbody>121 </table>122 </div>123 );124}125126/** Freshness thresholds legend: per-connector thresholds scale with the connector interval (misc.py: aging = 3×, stale = 12×). */127export function FreshnessLegend({ connectors }: { connectors?: ConnectorStatus[] }) {128 return (129 <div className="text-sm leading-relaxed text-ink-2">130 <p>131 Connector freshness is relative to each connector’s own schedule: <FreshnessBadge status="fresh" label="fresh" /> when the last successful sync is younger than 3 × the interval,{' '}132 <FreshnessBadge status="aging" label="aging" /> up to 12 × the interval, <FreshnessBadge status="stale" label="stale" /> beyond that, <FreshnessBadge status="unavailable" label="unavailable" /> when it never succeeded and{' '}133 <FreshnessBadge status="not_enabled" label="not enabled" /> for planned connectors.134 </p>135 {connectors && connectors.length > 0 && (136 <ul className="mt-3 grid gap-1 text-xs text-ink-3 sm:grid-cols-2">137 {connectors.map((c) => (138 <li key={c.name} className="mono">139 {c.name}: every {fmtInterval(c.interval_seconds)} → aging after {fmtInterval(c.interval_seconds * 3)}, stale after {fmtInterval(c.interval_seconds * 12)}140 </li>141 ))}142 </ul>143 )}144 </div>145 );146}147