spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import type { Metadata } from "next";2import Link from "next/link";3import { RelativeTime } from "@/components/ui/freshness";4import { Page, PageHeader } from "@/components/ui/section";5import { RightsBadge, StatusBadge } from "@/components/ui/status-badge";6import { api } from "@/lib/api";7import { formatDuration } from "@/lib/format";8import type { ConnectorPublic } from "@/lib/types";910export const metadata: Metadata = { title: "Connectors", description: "Operational status of every Market Atlas connector: type, rights, message rate, latency, coverage and reliability.", alternates: { canonical: "/connectors" } };11export const dynamic = "force-dynamic";1213export default async function ConnectorsPage() {14 const rows = await api<ConnectorPublic[]>("/v1/connectors");15 const healthy = rows.filter((r) => r.status === "HEALTHY").length;16 return (17 <Page wide>18 <PageHeader kicker="Operations" title="Connectors" lead={`${rows.length} connectors, ${healthy} healthy. A connector is one technical integration of a source (WebSocket feed, JSON endpoint, official file, RSS, HTML page…). The framework proves it generalizes across ${new Set(rows.map((r) => r.source_type)).size} transport types.`} actions={<Link href="/data-health" className="text-sm text-accent hover:underline">Data health →</Link>} />19 <div className="overflow-x-auto rounded-md border border-rule bg-surface">20 <table className="table-dense">21 <thead>22 <tr>23 <th>Connector</th>24 <th>Status</th>25 <th>Type</th>26 <th>Rights</th>27 <th className="hidden md:table-cell">Real time</th>28 <th className="text-right">Msg / min</th>29 <th className="text-right">Total</th>30 <th className="hidden text-right lg:table-cell">p50 / p95 latency</th>31 <th className="hidden text-right lg:table-cell">Parse</th>32 <th className="text-right">Instruments</th>33 <th className="text-right">Score</th>34 <th>Last message</th>35 <th className="hidden xl:table-cell">Next poll</th>36 </tr>37 </thead>38 <tbody>39 {rows.map((r) => (40 <tr key={r.id}>41 <td className="max-w-[340px] whitespace-normal">42 <div className="font-medium">{r.name}</div>43 <div className="mono text-[11px] text-ink-3">44 {r.id} · v{r.version} ·{" "}45 <Link href={`/sources#${r.source_id}`} className="hover:underline">46 {r.source_id}47 </Link>48 </div>49 <div className="mt-0.5 text-xs text-ink-2">{r.description}</div>50 </td>51 <td>52 <StatusBadge status={r.status} />53 </td>54 <td className="mono text-xs">{r.source_type}</td>55 <td>56 <RightsBadge status={r.rights_status} />57 </td>58 <td className="hidden text-xs text-ink-2 md:table-cell">{r.realtime_status.replace(/_/g, " ").toLowerCase()}</td>59 <td className="num">{r.messages_1m}</td>60 <td className="num">{r.messages_total.toLocaleString("en-US")}</td>61 <td className="num hidden lg:table-cell">62 {r.median_latency_ms == null ? "—" : formatDuration(r.median_latency_ms)} / {r.p95_latency_ms == null ? "—" : formatDuration(r.p95_latency_ms)}63 </td>64 <td className="num hidden lg:table-cell">{r.parse_success_rate == null ? "—" : `${Math.round(r.parse_success_rate * 100)}%`}</td>65 <td className="num">{r.instruments_covered}</td>66 <td className="num">{r.reliability_score ?? "—"}</td>67 <td className="text-xs text-ink-3">{r.last_message_at ? <RelativeTime value={r.last_message_at} /> : "—"}</td>68 <td className="hidden text-xs text-ink-3 xl:table-cell">{r.next_poll_at ? <RelativeTime value={r.next_poll_at} /> : r.supports_streaming ? "streaming" : "—"}</td>69 </tr>70 ))}71 </tbody>72 </table>73 </div>74 </Page>75 );76}77