Files
pig/apps/web/src/pages/Overview.tsx
T
claude 99d165b5e5
CI / verify (push) Successful in 4m57s
CI / publish (push) Has been skipped
Rebuild Piggy's interface, and give the demo book a business to describe
Piggy answered in raw markdown, threw away every tool result it streamed,
and fought the reader's scroll on every token. The three surfaces that
made it worth having — what it read, how it reasoned, what it cost — were
all on the wire and none of them reached the screen.

The transcript is now composed of five parts under components/piggy:
answers render through streamdown, the container sticks to the bottom
without pinning the reader there, tool steps say what they read and link
to the record, and each turn carries its model and token count. Three
lifecycle bugs went with them: Stop left a permanent spinner, a truncated
stream was indistinguishable from thinking, and a failed send destroyed
the message it failed to send.

Underneath, the inference path grew timeouts, jittered retries on 429 and
5xx, tolerance of the malformed frames a 30B model emits, and an
agent_runs row per turn so chat spend is observable. The system prompt now
states that a field ending in Cents is cents — without it nemotron renders
costPerGpuHourCents: 189 as "$189 per GPU-hour", which is a 100x error on
the most scrutinised number in the room.

The demo book was arithmetically incoherent: every deal's value
contradicted its own allocation revenue by up to 3.6x, nothing had ever
closed, no customer had any paper, and the marketplace was empty. Deal
value is now derived from the allocation, the book clears 5.3% across five
blocks with one deliberately underwater, and the renewal, compliance and
agent-provenance machinery finally has rows to act on. A --clear that
deleted every obligation, SLA term and capacity request in the database
regardless of origin is scoped to the demo's own ids.

Around that: accounts have a detail page, ⌘K searches the book, Settings
can mint the API keys it always claimed to, and deploy.sh actually ships
the agent instead of silently skipping its compose profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 00:34:18 -07:00

548 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Overview — the landing view.
*
* Leads with margin and idle capacity rather than deal counts, because those
* are the numbers this business actually turns on. A CRM that opens on
* "23 open opportunities" tells you nothing about whether you are making money.
*
* The one thing that outranks the money is the licence to operate. An export
* authorisation nobody renewed converts lawful business into unlawful business,
* and the Calendar — where every other dated risk lives — can only ever report
* the quarter being read. So a lapse is reported here, in the first screenful,
* however long ago it happened.
*/
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { AlertTriangle, ArrowRight, Plus, Server, ShieldAlert } from 'lucide-react';
import { Link } from 'react-router-dom';
import {
compactNumber,
get,
money,
percent,
relativeTime,
shortDate,
unitPrice,
} from '@/lib/api';
import {
Badge,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
EmptyState,
Skeleton,
Stat,
cn,
} from '@/components/ui';
import { withoutDemoPrefix } from '@/lib/utils';
import { CommitmentSheet } from '@/components/RecordSheets';
import { usePageTitle } from '@/lib/title';
import { usePiggyContext } from '@/lib/piggy-context';
import { useIdentity } from '@/lib/identity';
import { can } from '@/lib/permissions';
interface ComplianceItem {
id: string;
kind: 'authorization' | 'artifact';
/** The specific instrument — "Export licence", "SOC 2" — not the table it came from. */
label: string;
reference: string | null;
accountId: string | null;
accountName: string | null;
expiresAt: string;
lapsed: boolean;
volatile: boolean;
href: string;
}
interface Dashboard {
me: { name: string; teams: { team: string; role: string }[] };
margin: {
revenueCents: number;
costCents: number;
grossMarginCents: number;
grossMarginPct: number | null;
marginPerAllocatedGpuHourCents: number | null;
utilisation: number;
idleGpuHours: number;
committedGpuHours: number;
allocatedGpuHours: number;
};
blocks: number;
openDemandDeals: number;
openDemandAcvCents: number;
openSupplyDeals: number;
compliance: {
horizonDays: number;
lapsedCount: number;
expiringCount: number;
items: ComplianceItem[];
};
idleAlerts: {
commitmentId: string;
name: string;
gpuType: string;
gpuCount: number;
idleCostCents: number;
utilisation: number;
breakEvenPriceCents: number | null;
}[];
recentActivity: {
id: string;
type: string;
subject: string | null;
accountName: string | null;
occurredAt: string;
}[];
}
export function Overview() {
usePageTitle('Overview');
const me = useIdentity();
const [recordingCommitment, setRecordingCommitment] = useState(false);
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['dashboard'],
queryFn: () => get<Dashboard>('/api/dashboard'),
// The book does not change second to second, but it does change while
// someone is looking at it during a pipeline review.
refetchInterval: 60_000,
});
/*
* Published unconditionally — hooks cannot hide behind the loading return
* below — and labelled from the figures once they arrive, so the dock names
* the book the reader is looking at rather than repeating the route.
*/
usePiggyContext({
type: 'page',
route: '/',
...(data
? {
label: `Overview — ${percent(data.margin.grossMarginPct, 1)} margin, ${percent(
data.margin.utilisation,
)} of committed capacity sold`,
}
: {}),
});
if (isLoading) {
return (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-28" />
))}
</div>
);
}
if (error || !data) {
return (
<Card>
<CardContent className="flex flex-col items-center gap-4 pt-6">
<EmptyState
title="Could not load the overview"
description={error instanceof Error ? error.message : 'Unknown error.'}
/>
<Button variant="outline" onClick={() => void refetch()}>Try again</Button>
</CardContent>
</Card>
);
}
const m = data.margin;
const marginTone = m.grossMarginCents >= 0 ? 'positive' : 'danger';
// The seeded book prefixes every name with its demonstration marker, and a
// positional read of that took the label for the person: "Good evening,
// DEMO" was the first line of the screen everyone opens.
const firstName = withoutDemoPrefix(data.me.name).split(' ')[0];
const idleExposureCents = data.idleAlerts.reduce((sum, alert) => sum + alert.idleCostCents, 0);
/*
* Recording what capacity was bought is a supply lead's authority, not
* everyone's. Offering the button to someone the server will refuse turns an
* empty screen into a 403, which is a worse dead end than the one it fixes.
*/
const canRecordCommitment = can(me, 'commitment:write', 'supply');
return (
<div className="space-y-5 pb-[calc(5.5rem+var(--safe-bottom))] md:pb-0">
<header>
<h1 className="text-xl font-semibold tracking-tight sm:text-2xl">
{greeting()}, {firstName}
</h1>
<p className="mt-1 text-sm text-muted">
{data.blocks === 0
? 'No capacity commitments yet — margin appears once you record what you have bought.'
: `${data.blocks} capacity commitment${data.blocks === 1 ? '' : 's'} on the book.`}
</p>
</header>
<section className="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
<Stat
label="Gross margin"
value={money(m.grossMarginCents)}
// "— of $0 revenue" is what a percentage of nothing prints, and it
// reads as a broken figure rather than an empty book.
hint={
m.revenueCents === 0
? 'Nothing sold yet'
: `${percent(m.grossMarginPct, 1)} of ${money(m.revenueCents)} revenue`
}
tone={marginTone}
/>
<Stat
label="Sold ratio"
value={percent(m.utilisation, 1)}
hint={`${compactNumber(m.allocatedGpuHours)} of ${compactNumber(m.committedGpuHours)} GPU-hrs sold`}
// Nothing bought cannot be under-sold; warning on 0% of 0 hours is an
// alarm about a book that does not exist yet.
tone={m.committedGpuHours > 0 && m.utilisation < 0.6 ? 'warning' : 'default'}
/>
<Stat
label="Idle capacity"
value={`${compactNumber(m.idleGpuHours)} hrs`}
hint="Bought and unsold"
tone={m.idleGpuHours > 0 ? 'warning' : 'default'}
/>
{/*
The value of what is open rather than a count of it — a count is the
one figure on this page nobody can act on. The money is demand ACV
alone, because a supply deal carries GPUs and a target cost and never
a contract value; both counts stay in the hint, where they now agree
with the two pipeline boards.
*/}
<Stat
label="Open pipeline"
value={money(data.openDemandAcvCents)}
hint={`${data.openDemandDeals} demand · ${data.openSupplyDeals} supply open`}
/>
</section>
<LicenceToOperate compliance={data.compliance} />
{data.idleAlerts.length > 0 ? (
<Card className="border-warning/30">
<CardHeader className="flex-row items-start justify-between gap-3 space-y-0">
<div className="flex min-w-0 items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-warning" aria-hidden />
<div>
<CardTitle className="text-base">Capacity you are paying for and not selling</CardTitle>
<p className="mt-1 text-xs text-muted">Prioritized by idle cost exposure.</p>
</div>
</div>
<Badge tone="warning" className="nums shrink-0">{money(idleExposureCents)}</Badge>
</CardHeader>
<CardContent className="space-y-2">
{data.idleAlerts.map((alert) => (
<div
key={alert.commitmentId}
className="flex flex-col gap-2 rounded-lg bg-surface-2 p-3 sm:flex-row sm:items-center sm:justify-between"
>
<div className="min-w-0">
<p className="truncate font-medium">{alert.name}</p>
<p className="text-xs text-muted">
{alert.gpuCount}× {alert.gpuType} · {percent(alert.utilisation)} sold
{/*
A zero break-even means the block's cost is already
covered, so any further sale is upside. Printing
"break even above $0.00" is technically true and reads
like a bug, so it is said in words instead.
*/}
{alert.breakEvenPriceCents == null ? null : alert.breakEvenPriceCents > 0 ? (
<>
{' · '}break even above{' '}
<span className="nums">{unitPrice(alert.breakEvenPriceCents)}</span>/GPU-hr
</>
) : (
<>{' · '}cost already covered further sales are upside</>
)}
</p>
</div>
<div className="flex items-center gap-3 sm:justify-end">
<span className="nums whitespace-nowrap text-sm font-semibold text-warning">
{money(alert.idleCostCents)}
</span>
<Link
to="/capacity"
className="tap inline-flex min-h-11 items-center gap-1 rounded-lg px-3 text-sm font-medium text-accent-fg hover:bg-surface"
aria-label={`Match demand to ${alert.name}`}
>
Match
<ArrowRight className="h-3.5 w-3.5" aria-hidden />
</Link>
</div>
</div>
))}
</CardContent>
</Card>
) : null}
{/* `items-start` so a short book does not stretch to the height of a busy
activity feed and open a hole under its last row. */}
<div className="grid items-start gap-4 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-base">The book</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<Row label="Revenue" value={money(m.revenueCents)} />
<Row label="Cost of committed capacity" value={money(m.costCents)} />
<div className="space-y-2 border-t border-border pt-2">
<Row
label="Gross margin"
value={money(m.grossMarginCents)}
emphasis
tone={marginTone}
/>
{/*
The blended rate, which is what a block is actually judged on:
a book can clear millions and still be selling GPU-hours for
pennies over what they cost.
*/}
<Row
label="Margin per GPU-hour sold"
value={unitPrice(m.marginPerAllocatedGpuHourCents)}
/>
</div>
<p className="pt-2 text-xs leading-relaxed text-muted">
Cost is charged against the full commitment, not only the hours that sold
unsold hours are already paid for.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Recent activity</CardTitle>
</CardHeader>
<CardContent>
{data.recentActivity.length === 0 ? (
<p className="py-6 text-center text-sm text-muted">Nothing logged yet.</p>
) : (
<ul className="space-y-2.5">
{data.recentActivity.slice(0, 6).map((activity) => (
<li key={activity.id} className="flex items-start gap-2 text-sm">
<Badge tone="neutral" className="mt-0.5 shrink-0">
{activity.type.replace('_', ' ')}
</Badge>
<span className="min-w-0 flex-1">
<span className="block truncate">{activity.subject ?? '—'}</span>
{activity.accountName ? (
<span className="block truncate text-xs text-muted">
{activity.accountName}
</span>
) : null}
</span>
<span className="shrink-0 text-xs text-muted">
{relativeTime(activity.occurredAt)}
</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
{data.blocks === 0 ? (
<Card>
<CardContent className="pt-5">
<EmptyState
icon={<Server className="h-8 w-8" />}
title="No capacity on the book yet"
description={
canRecordCommitment
? 'Record a capacity commitment — what you bought, at what cost, over what term — and margin, utilisation and idle alerts all follow from it.'
: 'Margin, utilisation and idle alerts all follow from a recorded capacity commitment. Recording one needs supply-lead authority — ask a supply lead to add the first block.'
}
action={
canRecordCommitment ? (
<Button variant="primary" onClick={() => setRecordingCommitment(true)}>
<Plus data-icon="inline-start" aria-hidden />
Record a capacity commitment
</Button>
) : null
}
/>
</CardContent>
</Card>
) : null}
{/*
Mounted outside the empty state it is opened from: recording the first
block makes `blocks` non-zero, and a sheet that unmounts underneath its
own success toast closes with a jump.
*/}
<CommitmentSheet
open={recordingCommitment}
onOpenChange={setRecordingCommitment}
identity={me}
/>
</div>
);
}
/**
* The compliance tile.
*
* Always rendered, never collapsed to nothing when the news is good: a card
* that appears only in trouble teaches the reader that its absence means
* nothing was checked. The tone escalates instead — quiet when clear, warning
* inside the horizon, and danger the moment anything has lapsed.
*/
function LicenceToOperate({ compliance }: { compliance: Dashboard['compliance'] }) {
const lapsed = compliance.lapsedCount > 0;
const expiring = compliance.expiringCount > 0;
/*
* Three rows, however many are dated. The server sorts lapsed first, so the
* rows that survive the cut are never the ones this card exists for; the
* remainder is a queue, and a queue belongs on the Calendar's compliance
* lane rather than on the screen everyone opens first.
*/
const shown = compliance.items.slice(0, COMPLIANCE_ROWS);
const remaining = compliance.lapsedCount + compliance.expiringCount - shown.length;
const hiddenLapsed = compliance.lapsedCount - shown.filter((item) => item.lapsed).length;
return (
<Card className={cn(lapsed && 'border-danger/50', !lapsed && expiring && 'border-warning/30')}>
<CardHeader className="flex-row items-start justify-between gap-3 space-y-0">
<div className="flex min-w-0 items-start gap-2">
<ShieldAlert
className={cn(
'mt-0.5 h-4 w-4 shrink-0',
lapsed ? 'text-danger' : expiring ? 'text-warning' : 'text-muted',
)}
aria-hidden
/>
<div>
<CardTitle className="text-base">Licence to operate</CardTitle>
<p className="mt-1 text-xs text-muted">
{lapsed
? 'An expired export authorisation converts lawful business into unlawful business.'
: `Export authorisations and attestations expiring within ${compliance.horizonDays} days.`}
</p>
</div>
</div>
<Badge
tone={lapsed ? 'danger' : expiring ? 'warning' : 'positive'}
className="nums shrink-0"
>
{lapsed
? `${compliance.lapsedCount} lapsed`
: expiring
? `${compliance.expiringCount} expiring`
: 'Clear'}
</Badge>
</CardHeader>
<CardContent className="space-y-2">
{shown.length === 0 ? (
<p className="text-sm leading-relaxed text-muted">
<span className="text-fg">Nothing on file has lapsed</span>, and nothing expires in the
next {compliance.horizonDays} days. A counterparty with no authorisation recorded at all
is not covered by this check.
</p>
) : (
shown.map((item) => <ComplianceRow key={item.id} item={item} />)
)}
{remaining > 0 ? (
<Link
to="/calendar"
className="tap inline-flex min-h-11 items-center gap-1 text-sm font-medium text-accent-fg"
>
{hiddenLapsed > 0
? `${remaining} more, ${hiddenLapsed} of them lapsed`
: `${remaining} more expiring within ${compliance.horizonDays} days`}
<ArrowRight className="h-3.5 w-3.5" aria-hidden />
</Link>
) : null}
</CardContent>
</Card>
);
}
function ComplianceRow({ item }: { item: ComplianceItem }) {
const days = daysUntil(item.expiresAt);
const detail = [
item.kind === 'authorization' ? 'Export authorisation' : 'Compliance artefact',
item.reference,
// The date on file cannot be trusted for this counterparty; say so where
// the deadline is read, not on a screen nobody opens.
item.volatile ? 'rules in flux, re-verify' : null,
].filter((part): part is string => Boolean(part));
return (
<div className="flex flex-col gap-2 rounded-lg bg-surface-2 p-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<p className="truncate font-medium">
{item.label}
{item.accountName ? <span className="text-muted"> {item.accountName}</span> : null}
</p>
<p className="truncate text-xs text-muted">{detail.join(' · ')}</p>
</div>
<div className="flex items-center gap-3 sm:justify-end">
<span
className={cn(
'nums whitespace-nowrap text-sm font-semibold',
item.lapsed ? 'text-danger' : 'text-warning',
)}
>
{item.lapsed
? `Lapsed ${shortDate(item.expiresAt)} · ${Math.abs(days)}d ago`
: `Expires ${shortDate(item.expiresAt)} · ${days}d`}
</span>
<Link
to={item.href}
className="tap inline-flex min-h-11 items-center gap-1 rounded-lg px-3 text-sm font-medium text-accent-fg hover:bg-surface"
aria-label={`Review ${item.label}${item.accountName ? ` for ${item.accountName}` : ''}`}
>
Review
<ArrowRight className="h-3.5 w-3.5" aria-hidden />
</Link>
</div>
</div>
);
}
const COMPLIANCE_ROWS = 3;
const DAY_MS = 86_400_000;
/** Whole days, negative once the date has passed. Rounded, as the Calendar rounds. */
function daysUntil(value: string): number {
return Math.round((new Date(value).getTime() - Date.now()) / DAY_MS);
}
function Row({
label,
value,
emphasis,
tone,
}: {
label: string;
value: string;
emphasis?: boolean;
tone?: 'positive' | 'danger';
}) {
return (
<div className="flex items-baseline justify-between gap-3">
<span className={emphasis ? 'font-medium' : 'text-muted'}>{label}</span>
<span
className={[
'nums tabular-nums',
emphasis ? 'text-base font-semibold' : '',
tone === 'positive' ? 'text-positive' : tone === 'danger' ? 'text-danger' : '',
].join(' ')}
>
{value}
</span>
</div>
);
}
function greeting(): string {
const hour = new Date().getHours();
if (hour < 12) return 'Good morning';
if (hour < 18) return 'Good afternoon';
return 'Good evening';
}