g1t/apps/sudo/app/components/costs.tsx

146 lines6,799 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1/**
2 * Costs & margin's chart: a day per slot, what customers were charged
3 * (lavender) beside what it cost on Cloudflare (gray, red on a day that
4 * lost money). Drawn on the server as SVG like sudo's other charts: no
5 * script, no inline style, a <title> per day for hover, and a table.
6 */
7import { axisDollars, barPath, niceCeiling, ticks } from "~/lib/chart";
8import { type DayFigures, marginPercent, percentLabel } from "~/lib/costs";
9import { usd } from "~/lib/money";
10
11const CHARGED = "var(--g1t-merged)";
12const COST = "#7a7a84";
13const LOSS = "var(--g1t-danger)";
14
15const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
16
17/** `2026-10-05` → `Oct 5`. */
18export function dayLabel(day: string): string {
19 const month = MONTHS[Number(day.slice(5, 7)) - 1] ?? day.slice(5, 7);
20 return `${month} ${Number(day.slice(8, 10))}`;
21}
22
23function Legend({ revenueLabel }: { revenueLabel: string }) {
24 const swatch = (fill: string) => (
25 <svg width="10" height="10" aria-hidden="true">
26 <rect width="10" height="10" rx="2" fill={fill} />
27 </svg>
28 );
29 return (
30 <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted">
31 <span className="inline-flex items-center gap-1.5">
32 {swatch(CHARGED)}
33 {revenueLabel}
34 </span>
35 <span className="inline-flex items-center gap-1.5">
36 {swatch(COST)}
37 Cloudflare cost
38 </span>
39 <span className="inline-flex items-center gap-1.5">
40 {swatch(LOSS)}
41 Cost on a day under the floor
42 </span>
43 </div>
44 );
45}
46
47function DaysSvg({ days, width, height, floor, label }: { days: DayFigures[]; width: number; height: number; floor: number; label: string }) {
48 const left = width < 400 ? 40 : 52;
49 const plot = { x: left, y: 10, width: Math.max(1, width - left - 8), height: Math.max(1, height - 10 - 26) };
50 const top = niceCeiling(Math.max(0, ...days.flatMap((d) => [d.revenueMicros, d.costMicros])));
51 const baseline = plot.y + plot.height;
52 const scale = (value: number) => {
53 const clamped = Math.max(0, value);
54 const h = (clamped / top) * plot.height;
55 return clamped > 0 ? Math.max(1.5, h) : 0;
56 };
57 const slot = days.length > 0 ? plot.width / days.length : plot.width;
58 const gap = slot > 8 ? 1 : 0;
59 const bar = Math.max(1, Math.min(14, slot * 0.38));
60 // About six labels across, whatever the range.
61 const every = Math.max(1, Math.ceil(days.length / (width < 400 ? 4 : 8)));
62 return (
63 <svg viewBox={`0 0 ${width} ${height}`} className="block h-auto w-full" role="img" aria-label={label}>
64 {ticks(top, 4).map((value) => {
65 const y = baseline - (value / top) * plot.height;
66 return (
67 <g key={value}>
68 <line x1={plot.x} x2={plot.x + plot.width} y1={y} y2={y} stroke="var(--g1t-line)" strokeWidth="1" strokeDasharray={value === 0 ? undefined : "2 4"} />
69 <text x={plot.x - 8} y={y + 3.5} textAnchor="end" fontSize="11" className="fill-faint tabular">
70 {axisDollars(value)}
71 </text>
72 </g>
73 );
74 })}
75 {days.map((d, index) => {
76 const x = plot.x + index * slot;
77 const center = x + slot / 2;
78 const revenue = scale(d.revenueMicros);
79 const cost = scale(d.costMicros);
80 const margin = marginPercent(d.revenueMicros, d.costMicros);
81 const losing = d.costMicros > 0 && (margin == null || margin < floor);
82 const last = index === days.length - 1;
83 return (
84 <g key={d.day}>
85 <title>{`${dayLabel(d.day)}: charged ${usd(d.revenueMicros)}, Cloudflare cost ${usd(d.costMicros)}, margin ${percentLabel(margin)}`}</title>
86 <rect x={x} y={plot.y} width={slot} height={plot.height} fill="transparent" className="hover:fill-raised/40" />
87 <path d={barPath({ x: center - gap / 2 - bar, y: baseline - revenue, width: bar, height: revenue }, 2)} fill={CHARGED} />
88 <path d={barPath({ x: center + gap / 2, y: baseline - cost, width: bar, height: cost }, 2)} fill={losing ? LOSS : COST} />
89 {(index % every === 0 || last) && (
90 <text x={center} y={baseline + 17} textAnchor="middle" fontSize="11" className={last ? "fill-fg-soft" : "fill-faint"}>
91 {dayLabel(d.day)}
92 </text>
93 )}
94 </g>
95 );
96 })}
97 </svg>
98 );
99}
100
101/** Charged against Cloudflare's cost, a day at a time, drawn for a phone and for wider screens. */
102export function DaysChart({ days, floor, revenueLabel, label }: { days: DayFigures[]; floor: number; revenueLabel: string; label: string }) {
103 if (days.length === 0) return <p className="text-sm text-muted">No days to show yet.</p>;
104 return (
105 <div>
106 <Legend revenueLabel={revenueLabel} />
107 <div className="mt-3 sm:hidden">
108 <DaysSvg days={days} width={340} height={200} floor={floor} label={label} />
109 </div>
110 <div className="mt-3 hidden sm:block">
111 <DaysSvg days={days} width={1080} height={240} floor={floor} label={label} />
112 </div>
113 <details className="group mt-2">
114 <summary className="cursor-pointer list-none text-xs text-faint select-none hover:text-muted [&::-webkit-details-marker]:hidden">
115 <span className="group-open:hidden">Show as a table</span>
116 <span className="hidden group-open:inline">Hide the table</span>
117 </summary>
118 <div className="mt-2 overflow-x-auto">
119 <table className="w-full min-w-[28rem] text-xs">
120 <thead>
121 <tr className="border-b border-line text-left text-muted">
122 <th className="py-1.5 pr-3 font-medium">Day</th>
123 <th className="py-1.5 pr-3 text-right font-medium">{revenueLabel}</th>
124 <th className="py-1.5 pr-3 text-right font-medium">Cloudflare cost</th>
125 <th className="py-1.5 text-right font-medium">Margin</th>
126 </tr>
127 </thead>
128 <tbody>
129 {[...days].reverse().map((d) => {
130 const margin = marginPercent(d.revenueMicros, d.costMicros);
131 return (
132 <tr key={d.day} className="tabular border-b border-line last:border-0">
133 <td className="py-1.5 pr-3 text-fg-soft">{dayLabel(d.day)}</td>
134 <td className="py-1.5 pr-3 text-right">{usd(d.revenueMicros)}</td>
135 <td className="py-1.5 pr-3 text-right text-muted">{usd(d.costMicros)}</td>
136 <td className={`py-1.5 text-right ${margin != null && margin < floor ? "text-danger" : "text-fg-soft"}`}>{percentLabel(margin)}</td>
137 </tr>
138 );
139 })}
140 </tbody>
141 </table>
142 </div>
143 </details>
144 </div>
145 );
146}