import { Badge, ClipboardText, Empty, Table, Tooltip } from "@cloudflare/kumo"; import brand from "../../../brand.json" with { type: "json" }; import type { ProjectState, Race } from "./contracts.ts"; import { ArchivedBadge } from "./archive.tsx"; import { agentIdentities, Saddlecloth } from "./lane.tsx"; import { duration } from "./receipts.tsx"; import { photoFinish, raceEntrants, raceSplit, raceStart, timestamp } from "./race-model.ts"; /** Only recorded starts/finishes determine order; empty open races follow started ones. */ export function orderedRaces(races: Race[]): Race[] { return races.toSorted((a, b) => { if (a.status !== b.status) return a.status === "open" ? -1 : 1; const at = a.status === "open" ? raceStart(a) : timestamp(a.finishedAt); const bt = b.status === "open" ? raceStart(b) : timestamp(b.finishedAt); return (bt ?? -Infinity) - (at ?? -Infinity) || a.packet.localeCompare(b.packet); }); } export const openRaceCount = (races: Race[]) => races.filter((race) => race.status === "open" && race.entrants.length > 0).length; const postTime = (start: number) => new Date(start).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false, }); /** FH-081 read-only list. Advice is never a promotion or a recorded winner. */ export function Races({ state, owner, openRace }: { state: ProjectState; owner: boolean; openRace(id: string): void; }) { const races = orderedRaces(state.races ?? []); const identities = agentIdentities(state.agents); const runner = (name: string) => {name} ; const command = `${brand.slug} packet create packet.yaml --config "$CLI_CONFIG"`; return Races Races are started by the owner (CLI or owner API). {!races.length ? Add mode: race to the packet file. {command} : undefined} /> : {["Race", "Status", "Entrants", "Winner", "Judge's pick", "Started"].map((heading, index) => {heading})} {races.map((race) => { const packet = state.packets.find((entry) => entry.id === race.packet); const title = packet?.title || race.packet; const start = raceStart(race); const finished = timestamp(race.finishedAt); const pick = race.judge ? race.verdict?.ranking[0] : undefined; const judge = !race.judge ? "—" : pick ? <> {runner(pick)}Judge {race.judge} > : {race.judge} · no advice yet; const started = start === undefined ? "—" : <> }> {postTime(start)} {race.status === "finished" && finished !== undefined && finished >= start && Finished {duration(finished - start)} later} >; return { if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); openRace(race.packet); }}>{title} {race.packet} {packet?.archived && } Judge's pick: {pick ?? (race.judge ? `${race.judge} · no advice yet` : "—")} Started {started} {race.status === "open" && !race.entrants.length ? No entrants yet : {race.status === "finished" ? "Finished" : "Racing"} } {photoFinish(race) >= 2 && photo finish} {raceEntrants(race).slice(0, 3).map((entrant) => {runner(entrant.agent)} {entrant.closedAt && closed} )}{!race.entrants.length && "—"} {race.status !== "finished" ? "—" : race.winner ? <> {runner(race.winner)} {raceSplit(race.finishedAt, start) && won in {raceSplit(race.finishedAt, start)}} > : "No winner recorded"} {judge} {started} ; })} } ; }
Races are started by the owner (CLI or owner API).
Add mode: race to the packet file.
mode: race
{command}
{race.packet}