- New /api/dashboard/funnel (5-stage stack health, 30s TTL cache) and /api/dashboard/summary (containers, daily uptime jsonl, ops activity) - Token system (tokens.css + Tailwind sp-* aliases); legacy bg/card/accent remapped onto the tokens; Schibsted Grotesk bundled via fontsource - TopNav pill navigation + AppShell replace the sidebar layout (off-canvas drawer below 1024px); central display-weight page titles - Dashboard redesign: FunnelChart (gradient/hatch SVG waterfall), container count card with per-host bars + Insights chip, UptimeChart, OpsGrid, AiPromptBar; 30/7-day range selector; host sections retained below - Stacks page honours ?q= / ?filter= deep links + new status-filter select Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
47 lines
1.3 KiB
TypeScript
47 lines
1.3 KiB
TypeScript
/** Contribution-grid style dot matrix: one column per day, cells light up
|
||
* bottom-to-top with the day's normalised activity. Fully deterministic —
|
||
* derived only from `data`, no randomness. */
|
||
export function OpsGrid({
|
||
data,
|
||
cols = 30,
|
||
rows = 5,
|
||
}: {
|
||
data: number[]; // normalised 0–1 per day
|
||
cols?: number;
|
||
rows?: number;
|
||
}) {
|
||
const days = data.slice(-cols);
|
||
const cell = 10;
|
||
const gap = 3;
|
||
const W = cols * (cell + gap) - gap;
|
||
const H = rows * (cell + gap) - gap;
|
||
|
||
// 4 opacity tiers, like a contribution graph.
|
||
const tier = (v: number) => (v <= 0 ? 0.08 : v < 0.34 ? 0.28 : v < 0.67 ? 0.58 : 1);
|
||
|
||
return (
|
||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" role="img" aria-label="Operations activity grid">
|
||
{days.map((v, day) => {
|
||
const lit = Math.min(rows, Math.ceil(Math.max(0, Math.min(v, 1)) * rows));
|
||
const x = day * (cell + gap);
|
||
return Array.from({ length: rows }, (_, r) => {
|
||
const y = (rows - 1 - r) * (cell + gap);
|
||
const on = r < lit;
|
||
return (
|
||
<rect
|
||
key={`${day}-${r}`}
|
||
x={x}
|
||
y={y}
|
||
width={cell}
|
||
height={cell}
|
||
rx={2.5}
|
||
fill="var(--sp-blue)"
|
||
fillOpacity={on ? tier(v) : 0.08}
|
||
/>
|
||
);
|
||
});
|
||
})}
|
||
</svg>
|
||
);
|
||
}
|