SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
7.1 KB · 186 lines tsx
Raw Blame History
1import Link from 'next/link';2import { IncidentRow, TYPE_LABEL } from '@/components/incidents/IncidentRow';3import { Empty, PNum, Section } from '@/components/ui/primitives';4import { MONTH_NAMES, fmt, fmtInt } from '@/lib/format';5import { pressureColor } from '@/lib/pressure';6import type { HistoryDay, HistoryMonthRow, HistorySummary, Incident } from '@/lib/types';78/** Calendar heat strip: one cell per day coloured by max pressure. */9export function DayStrip({ days, year, month }: { days: HistoryDay[]; year: number; month: number }) {10  const dim = new Date(Date.UTC(year, month, 0)).getUTCDate();11  const byDate = new Map(days.map((d) => [d.date, d]));12  const firstDow = (new Date(Date.UTC(year, month - 1, 1)).getUTCDay() + 6) % 7; // Monday first13  return (14    <div>15      <div className="grid grid-cols-7 gap-1 text-center text-[10px] text-ink-3">16        {['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((d, i) => (17          <span key={i}>{d}</span>18        ))}19      </div>20      <div className="mt-1 grid grid-cols-7 gap-0.5 sm:gap-1">21        {Array.from({ length: firstDow }).map((_, i) => (22          <span key={`e${i}`} />23        ))}24        {Array.from({ length: dim }, (_, i) => i + 1).map((n) => {25          const date = `${year}-${String(month).padStart(2, '0')}-${String(n).padStart(2, '0')}`;26          const d = byDate.get(date);27          return (28            <div key={date} className="min-h-[44px] min-w-0 overflow-hidden rounded-[3px] border border-line p-1 sm:p-1.5" style={{ background: d ? pressureColor(d.max) + '33' : 'var(--neutral)' }} title={d ? `${date}: min ${fmt(d.min)} · avg ${fmt(d.avg)} · max ${fmt(d.max)} · ${d.events} events` : `${date}: not observed`}>29              <div className="flex items-baseline justify-between">30                <span className="num text-[10.5px] text-ink-2">{n}</span>31                {d && d.events > 0 && <span className="num text-[9.5px] text-high">{d.events}</span>}32              </div>33              {d && (34                <div className="num text-[13px] leading-tight" style={{ color: pressureColor(d.max) }}>35                  {fmt(d.max, 0)}36                </div>37              )}38            </div>39          );40        })}41      </div>42      <p className="mt-2 text-[10.5px] text-ink-3">Cell number = daily max pressure · red count = events opened that day · grey = not observed (observatory started 2026-09)</p>43    </div>44  );45}4647export function MonthStrip({ months }: { months: HistoryMonthRow[] }) {48  if (!months.length) return <Empty>No month observed yet.</Empty>;49  return (50    <ul className="grid grid-cols-2 gap-2 sm:grid-cols-4 lg:grid-cols-6">51      {months.map((m) => {52        const [y, mo] = m.month.split('-');53        return (54          <li key={m.month}>55            <Link href={`/history/${y}/${Number(mo)}`} className="block rounded-[3px] border border-line p-3 hover:border-line-2" style={{ background: pressureColor(m.max) + '22' }}>56              <div className="text-[12px] text-ink">57                {MONTH_NAMES[Number(mo) - 1]} {y}58              </div>59              <div className="num mt-1 text-[22px] leading-none" style={{ color: pressureColor(m.max) }}>60                {fmt(m.max)}61              </div>62              <div className="num mt-1 text-[10.5px] text-ink-2">63                avg {fmt(m.avg)} · min {fmt(m.min)} · {fmtInt(m.events)} events64              </div>65            </Link>66          </li>67        );68      })}69    </ul>70  );71}7273export function LargestGrid({ largest }: { largest: HistorySummary['largest'] }) {74  const cats: [keyof HistorySummary['largest'], string][] = [75    ['pressure', 'Largest by pressure'],76    ['routing', 'Largest routing event'],77    ['dns', 'Largest DNS event'],78    ['latency', 'Largest latency event'],79  ];80  return (81    <ul className="grid grid-cols-[minmax(0,1fr)] gap-px overflow-hidden rounded-[4px] border border-line bg-line sm:grid-cols-2 lg:grid-cols-4">82      {cats.map(([k, label]) => {83        const inc = largest[k];84        return (85          <li key={k} className="bg-panel p-3">86            <div className="label">{label}</div>87            {inc ? (88              <>89                <Link href={`/event/${inc.slug}`} className="mt-1 block text-[13px] text-ink hover:text-accent">90                  {inc.title}91                </Link>92                <div className="num mt-1 flex items-baseline gap-2 text-[11px] text-ink-2">93                  <PNum value={inc.peak_pressure} className="text-[18px]" />94                  <span>{TYPE_LABEL[inc.type]}</span>95                </div>96              </>97            ) : (98              <p className="mt-1 text-[12px] text-ink-3">none in range</p>99            )}100          </li>101        );102      })}103    </ul>104  );105}106107export function TopLists({ s }: { s: HistorySummary }) {108  return (109    <div className="grid grid-cols-[minmax(0,1fr)] gap-x-10 lg:grid-cols-2">110      <Section label="Most affected ASNs" className="min-w-0">111        {s.top_asns.length ? (112          <table className="tbl">113            <thead>114              <tr>115                <th>ASN</th>116                <th className="r">Events</th>117                <th className="r">Max pressure</th>118              </tr>119            </thead>120            <tbody>121              {s.top_asns.map((a) => (122                <tr key={a.asn}>123                  <td>124                    <Link href={`/asn/${a.asn}`} className="text-ink hover:text-accent">125                      <span className="num">AS{a.asn}</span> {a.name}126                    </Link>127                  </td>128                  <td className="num r">{fmtInt(a.events)}</td>129                  <td className="r">130                    <PNum value={a.max_pressure} />131                  </td>132                </tr>133              ))}134            </tbody>135          </table>136        ) : (137          <Empty />138        )}139      </Section>140      <Section label="Most affected regions" className="min-w-0">141        {s.top_regions.length ? (142          <table className="tbl">143            <thead>144              <tr>145                <th>Region</th>146                <th className="r">Events</th>147                <th className="r">Max</th>148                <th className="r">Hours elevated</th>149              </tr>150            </thead>151            <tbody>152              {s.top_regions.map((r) => (153                <tr key={r.id}>154                  <td>155                    <Link href={`/internet/${r.id}`} className="text-ink hover:text-accent">156                      {r.name}157                    </Link>158                  </td>159                  <td className="num r">{fmtInt(r.events)}</td>160                  <td className="r">161                    <PNum value={r.max_pressure} />162                  </td>163                  <td className="num r text-ink-2">{fmt(r.hours_elevated)}</td>164                </tr>165              ))}166            </tbody>167          </table>168        ) : (169          <Empty />170        )}171      </Section>172    </div>173  );174}175176export function TopEvents({ events }: { events: Incident[] }) {177  if (!events.length) return <Empty>No event in this range.</Empty>;178  return (179    <ul className="divide-y divide-line">180      {events.map((e) => (181        <IncidentRow key={e.event_id} inc={e} compact />182      ))}183    </ul>184  );185}186