Files
so-many-crows/ui/src/api.ts
T
SmokeyandClaude Opus 5.5 2d85cab716 Nests: team rooms with core crows and borrowing
- Server: nests API, persistence, core-first picking, borrowed flag
  (built by the backend agent against EVENTS.md; tests pass with -race)
- UI: nests in the sidebar, one feed per nest, header shows the core
  team, New nest dialog, borrowed crows labeled, per-nest waiting count
- start.command: one-click local server in controlled mode (mock crows,
  localhost only, state in .crows/)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
2026-10-27 13:45:04 -06:00

78 lines
3.2 KiB
TypeScript

// Thin client for the HTTP API in EVENTS.md.
export type PostListResult =
| { ok: true; listId: string }
| { ok: false; pranked: true; message: string; cooldownS: number }
| { ok: false; pranked: false; message: string };
export async function postList(text: string, requester: string, nestId: string): Promise<PostListResult> {
const res = await fetch("/api/lists", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, requester, nest_id: nestId }),
});
const body = await res.json().catch(() => ({}));
if (res.ok) return { ok: true, listId: String(body.list_id) };
if (res.status === 429) return { ok: false, pranked: true, message: String(body.message ?? "Caw."), cooldownS: Number(body.cooldown_s ?? 30) };
return { ok: false, pranked: false, message: String(body.message ?? `The flock said no (${res.status}).`) };
}
export async function postVerdict(itemId: string, target: "triage" | "output", approve: boolean): Promise<void> {
const res = await fetch("/api/verdicts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item_id: itemId, target, approve }),
});
if (!res.ok) throw new Error(res.status === 404 ? "That question already has an answer." : `Verdict failed (${res.status}).`);
}
export type TrustLevels = Map<string, number>; // "Name|task_type" -> level
export async function getTrust(): Promise<TrustLevels> {
const out: TrustLevels = new Map();
try {
const res = await fetch("/api/trust");
const body = await res.json();
for (const [key, rec] of Object.entries<{ level?: number }>(body.crows ?? {})) out.set(key, rec.level ?? 0);
} catch {
// Badges start at zero if the ledger can't be read.
}
return out;
}
export function listEvents(listId: string): EventSource {
return new EventSource(`/api/events?list_id=${encodeURIComponent(listId)}`);
}
// ---------- nests ----------
export type Nest = { nestId: string; name: string; crows: string[] };
export type Nests = { roster: string[]; nests: Nest[] };
const DEFAULT_ROSTER = ["Hugin", "Munin", "Corvina", "Soot", "Pepper", "Inky", "Edgar", "Jet"];
export async function getNests(): Promise<Nests> {
try {
const res = await fetch("/api/nests");
if (!res.ok) throw new Error(String(res.status));
const body = await res.json();
return {
roster: (body.roster ?? DEFAULT_ROSTER).map(String),
nests: (body.nests ?? []).map((n: any) => ({ nestId: String(n.nest_id), name: String(n.name), crows: (n.crows ?? []).map(String) })),
};
} catch {
// A server without nests still has the default room.
return { roster: DEFAULT_ROSTER, nests: [{ nestId: "todo", name: "todo", crows: [] }] };
}
}
export async function createNest(name: string, crows: string[]): Promise<Nest> {
const res = await fetch("/api/nests", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, crows }),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(String(body.message ?? `Couldn't build that nest (${res.status}).`));
return { nestId: String(body.nest_id), name: String(body.name), crows: (body.crows ?? []).map(String) };
}