/**
* Capacity — availability, and the matcher.
*
* The matcher is the screen that justifies the product: given what a customer
* wants, what have we already bought that could serve them, and would selling
* it make money? No generic CRM can answer either half.
*/
import { useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import type { PermissionGrant } from '@pig/core';
import { Search, Server, ShieldCheck, Zap } from 'lucide-react';
import { compactNumber, get, money, percent, post, shortDate } from '@/lib/api';
import { usePageTitle } from '@/lib/title';
import { can } from '@/lib/permissions';
import {
AllocationSheet,
type AvailabilityRow,
type MatchRow,
} from '@/components/AllocationSheet';
import {
Badge,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
EmptyState,
Input,
Skeleton,
} from '@/components/ui';
export function Capacity() {
usePageTitle('Capacity');
const [tab, setTab] = useState<'available' | 'match'>('available');
const [allocation, setAllocation] = useState<{
open: boolean;
preferredCommitmentId?: string;
matches?: MatchRow[];
defaultGpuHours?: number;
}>({ open: false });
const { data: me } = useQuery({
queryKey: ['me'],
queryFn: () => get<{ permissions: PermissionGrant[] }>('/api/me'),
});
const writable = can(me, 'deal:write', 'demand');
return (
Capacity
What we hold, what is sold, and what is still sellable.
{/* A segmented control rather than tabs — it reads correctly at phone
width, where a tab row would either wrap or scroll. */}
{(['available', 'match'] as const).map((value) => (
))}
);
}
function Availability({ writable, onAllocate }: { writable: boolean; onAllocate(id: string): void }) {
const { data, isLoading } = useQuery({
queryKey: ['availability'],
queryFn: () => get('/api/capacity/availability'),
});
if (isLoading) return ;
if (!data || data.length === 0) {
return (
}
title="No live capacity commitments"
description="Once you record what capacity you have committed to buy, this view shows how much of each block is sold, held, and still available."
/>
);
}
return (
);
}
function CapacityCard({ row, writable, onAllocate }: { row: AvailabilityRow; writable: boolean; onAllocate(): void }) {
const soldPct = row.totalGpuHours > 0 ? row.soldGpuHours / row.totalGpuHours : 0;
const heldPct = row.totalGpuHours > 0 ? row.heldGpuHours / row.totalGpuHours : 0;
return (
/*
* `min-w-0` is load-bearing. A grid item defaults to `min-width: auto`,
* which means it refuses to shrink below its content — and `truncate` sets
* `white-space: nowrap`, so a long title becomes unshrinkable content and
* widens the whole track. The result is a page that scrolls sideways on a
* phone. This is the fix, and it is needed on every grid child that
* truncates.
*/
What does the customer need?
{mutation.isSuccess ? (
mutation.data.length === 0 ? (
}
title="Nothing on the book fits"
description="No committed capacity matches. Search provider inventory to find capacity to buy, which would mean opening a supply deal."
/>
) : (