SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
6.5 KB · 147 lines tsx
Raw Blame History
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&rsquo;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