Repository Public
1import { A, Meter, PageHead, Panel, Stat, bytes, usd, when } from "@/components/studio/ui";2import { requireRole } from "@/lib/auth";3import { costSummary } from "@/lib/studio-ops";4import { IMAGE_USD, PRICES_RETRIEVED, TTS_USD_PER_CHAR, VIDEO_USD_PER_S } from "@/providers/pricing";56export const metadata = { title: "Costs" };7export default async function Costs() {8 await requireRole("viewer");9 const c = await costSummary();10 const max = Math.max(0.01, ...c.byDay.map((d) => d.usd));11 const cad = (n: number) => (c.fx ? ` · ${(n * c.fx.rate).toFixed(2)} CAD` : "");12 return (13 <>14 <PageHead title="Costs" sub="Actual spend comes from the provider's own usage figures (cost in USD ticks). Estimates are the pre-flight figures the budget gate used." />15 <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">16 <Stat label="Today" value={usd(c.totals.day)} sub={`cap ${usd(c.policy.per_day_usd)}${cad(c.totals.day)}`} />17 <Stat label="This month" value={usd(c.totals.month)} sub={`cap ${usd(c.policy.per_month_usd)}${cad(c.totals.month)}`} />18 <Stat label="All time" value={usd(c.totals.all_time)} sub={cad(c.totals.all_time).slice(3) || undefined} />19 <Stat label="Failed generations" value={usd(c.totals.failed)} sub="billed by the provider, no usable output" tone={c.totals.failed > 0 ? "warn" : undefined} />20 </div>21 <p className="mt-2 text-xs text-[var(--color-faint)]">{c.fx ? `CAD at ${c.fx.rate} USD→CAD, rate entered ${when(c.fx.at)}.` : "No USD→CAD rate set (Settings): amounts are shown in USD only."}</p>22 <div className="mt-5 grid gap-5 lg:grid-cols-2">23 <Panel title="Ceilings"><div className="space-y-4"><Meter label="Today" spent={c.totals.day} cap={Number(c.policy.per_day_usd)} /><Meter label="This month" spent={c.totals.month} cap={Number(c.policy.per_month_usd)} /></div>24 <p className="mt-4 text-xs text-[var(--color-faint)]">Per shot {usd(c.policy.per_shot_usd)} · per film {usd(c.policy.per_film_usd)} · {c.policy.max_regenerations} regenerations per shot before owner approval. <A href="/studio/settings">Edit</A></p></Panel>25 <Panel title="Last 30 days">26 {c.byDay.length ? <div className="flex h-32 items-end gap-1" role="img" aria-label="Daily spend, last 30 days">{c.byDay.map((d) => <div key={d.day} className="group relative flex-1"><div className="rounded-t bg-[var(--color-lamp)]" style={{ height: `${Math.max(2, (d.usd / max) * 120)}px` }} title={`${d.day}: ${usd(d.usd)}`} /></div>)}</div> : <p className="text-sm text-[var(--color-faint)]">No spend yet.</p>}27 {c.byDay.length > 0 && <table className="studio-table mt-3"><tbody>{c.byDay.slice(-5).reverse().map((d) => <tr key={d.day}><td>{d.day}</td><td className="text-right tabular-nums">{usd(d.usd, 3)}</td></tr>)}</tbody></table>}28 </Panel>29 </div>30 <div className="mt-5 grid gap-5 lg:grid-cols-2">31 <Panel title="Per model — estimated vs actual">32 <table className="studio-table"><thead><tr><th>Kind · model</th><th>Calls</th><th>Estimated</th><th>Actual</th></tr></thead><tbody>33 {c.byModel.map((m) => <tr key={m.kind + m.model}><td className="mono">{m.kind} · {m.model}</td><td className="tabular-nums">{m.calls}</td><td className="tabular-nums">{usd(m.est, 3)}</td><td className="tabular-nums">{usd(m.usd, 3)}</td></tr>)}34 {!c.byModel.length && <tr><td colSpan={4} className="text-[var(--color-faint)]">No provider call yet.</td></tr>}35 </tbody></table>36 </Panel>37 <Panel title="Per film — estimated vs actual">38 <table className="studio-table"><thead><tr><th>Project</th><th>Status</th><th>Estimated</th><th>Actual</th></tr></thead><tbody>39 {c.byProject.filter((p) => p.usd > 0).map((p) => <tr key={p.id}><td><A href={`/studio/projects/${p.id}`}>{p.title}</A></td><td className="text-[var(--color-muted)]">{p.status.replace(/_/g, " ")}</td><td className="tabular-nums">{usd(p.est, 3)}</td><td className="tabular-nums">{usd(p.usd, 3)}</td></tr>)}40 {!c.byProject.some((p) => p.usd > 0) && <tr><td colSpan={4} className="text-[var(--color-faint)]">No spend yet.</td></tr>}41 </tbody></table>42 </Panel>43 </div>44 <div className="mt-5 grid gap-5 lg:grid-cols-2">45 <Panel title={`Price table used for estimates (retrieved ${PRICES_RETRIEVED})`}>46 <table className="studio-table"><tbody>47 {Object.entries(VIDEO_USD_PER_S).map(([k, v]) => <tr key={k}><td className="mono">{k}</td><td className="tabular-nums">${v.usd}/s</td><td className="text-[var(--color-muted)]">{v.basis}</td></tr>)}48 {Object.entries(IMAGE_USD).flatMap(([m, t]) => Object.entries(t).slice(0, 1).map(([k, v]) => <tr key={m + k}><td className="mono">{m} ({k})</td><td className="tabular-nums">${v}/image</td><td className="text-[var(--color-muted)]">list</td></tr>))}49 <tr><td className="mono">text to speech</td><td className="tabular-nums">${(TTS_USD_PER_CHAR * 1e6).toFixed(0)}/1M chars</td><td className="text-[var(--color-muted)]">list · computed (no usage returned)</td></tr>50 </tbody></table>51 <p className="mt-3 text-xs text-[var(--color-faint)]">“measured” = observed on a real call; “assumed” = no official per-resolution price was found, replaced by the observed mean as soon as a real job exists. Unknown prices block autonomous execution.</p>52 </Panel>53 <Panel title="Storage"><p className="display text-4xl tabular-nums">{bytes(c.storage.bytes)}</p><p className="mt-1 text-sm text-[var(--color-faint)]">{c.storage.files} distinct objects on the node's disk (content-addressed). No provider storage is used, so there is no storage charge; sources, intermediates and masters are all kept.</p></Panel>54 </div>55 </>56 );57}58