g1t/apps/sudo/app/components/costs.tsx
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 daily | 1 | /** |
| 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 | */ | |
| 7 | import { axisDollars, barPath, niceCeiling, ticks } from "~/lib/chart"; | |
| 8 | import { type DayFigures, marginPercent, percentLabel } from "~/lib/costs"; | |
| 9 | import { usd } from "~/lib/money"; | |
| 10 | ||
| 11 | const CHARGED = "var(--g1t-merged)"; | |
| 12 | const COST = "#7a7a84"; | |
| 13 | const LOSS = "var(--g1t-danger)"; | |
| 14 | ||
| 15 | const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
| 16 | ||
| 17 | /** `2026-10-05` → `Oct 5`. */ | |
| 18 | export 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 | ||
| 23 | function 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 | ||
| 47 | function 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. */ | |
| 102 | export 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 | } |