"use client"; // The integrated post-login dashboard (design comp: Clawmates Dashboard.html). // One screen: top bar (breadcrumb) · ORG/CO/TEAM/CLAW tier rail · context list · // topology canvas · agent "computer" slide-out · status bar. Self-contained // in-memory state machine, wired to the existing API. import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import { Plus } from "lucide-react"; import { MeshMark } from "@/components/brand/MeshMark"; import { ComputerPanel } from "@/components/computer/ComputerPanel"; import type { Agent } from "@/lib/api/schemas"; import type { GroupSummary, StructureNode, StructureStats } from "@/lib/api/structure"; import { TopologyCanvas, modeForKind, type CanvasNode, type TopologyMode, } from "./TopologyCanvas"; type Tier = "org" | "company" | "team" | "claw"; async function getJSON(url: string, fallback: T): Promise { try { const r = await fetch(url); return r.ok ? ((await r.json()) as T) : fallback; } catch { return fallback; } } const railIcon: Record = { org: ( ), company: ( ), team: ( ), claw: ( ), }; const TIERS: { key: Tier; label: string }[] = [ { key: "org", label: "ORG" }, { key: "company", label: "CO" }, { key: "team", label: "TEAM" }, { key: "claw", label: "CLAW" }, ]; export function Dashboard({ user }: { user: { display_name?: string; email?: string } }) { const [tier, setTier] = useState("team"); const [orgId, setOrgId] = useState(null); const [companyId, setCompanyId] = useState(null); const [teamId, setTeamId] = useState(null); const [clawId, setClawId] = useState(null); const [mode, setMode] = useState("hub-spoke"); const [orgs, setOrgs] = useState([]); const [companies, setCompanies] = useState([]); const [teams, setTeams] = useState([]); const [roster, setRoster] = useState([]); const [stats, setStats] = useState(null); const [node, setNode] = useState(null); const [loaded, setLoaded] = useState(false); const modeKey = useRef(""); // Load the workspace + default to the most-recent team. useEffect(() => { let live = true; void (async () => { const [t, c, o, r, s] = await Promise.all([ getJSON("/api/teams", []), getJSON("/api/companies", []), getJSON("/api/orgs", []), getJSON("/api/team/claws", []), getJSON("/api/structure/stats", null), ]); if (!live) return; setTeams(t); setCompanies(c); setOrgs(o); setRoster(r); setStats(s); if (t[0]) { setTier("team"); setTeamId(t[0].id); } else if (c[0]) { setTier("company"); setCompanyId(c[0].id); } else if (o[0]) { setTier("org"); setOrgId(o[0].id); } setLoaded(true); })(); return () => { live = false; }; }, []); // The canvas always renders a group's children; a claw selection keeps the // team canvas and opens the slide-out. const canvasLevel: "org" | "company" | "team" | null = tier === "org" ? "org" : tier === "company" ? "company" : "team"; const canvasId = tier === "org" ? orgId : tier === "company" ? companyId : teamId; // Clear the canvas when its target changes (render-phase reset — avoids a // synchronous setState inside the effect). const ckey = `${canvasLevel ?? ""}:${canvasId ?? ""}`; const [seenCanvas, setSeenCanvas] = useState(ckey); if (seenCanvas !== ckey) { setSeenCanvas(ckey); setNode(null); } useEffect(() => { if (!canvasLevel || !canvasId) return; let live = true; void (async () => { const n = await getJSON( `/api/structure/${canvasLevel}/${canvasId}`, null, ); if (!live) return; setNode(n); // Reset the view-mode to the group's real kind only when the group changes. const key = `${canvasLevel}:${canvasId}`; if (n && modeKey.current !== key) { modeKey.current = key; setMode(modeForKind(n.kind)); } })(); return () => { live = false; }; }, [canvasLevel, canvasId]); const rosterMap = new Map(roster.map((a) => [a.id, a])); const nodes: CanvasNode[] = (node?.children ?? []).map((c) => ({ id: c.child_id, label: c.child_name || rosterMap.get(c.child_id)?.name || c.role, role: c.role, level: c.child_level, status: rosterMap.get(c.child_id)?.status, })); function onNodeClick(n: CanvasNode) { if (n.level === "claw") { setClawId(n.id); setTier("claw"); } else if (n.level === "team") { setTeamId(n.id); setClawId(null); setTier("team"); } else { setCompanyId(n.id); setTeamId(null); setClawId(null); setTier("company"); } } function gotoTier(t: Tier) { if (t === "org" && !orgId && orgs[0]) setOrgId(orgs[0].id); if (t === "company" && !companyId && companies[0]) setCompanyId(companies[0].id); if (t === "team" && !teamId && teams[0]) setTeamId(teams[0].id); if (t === "claw" && !clawId) return; // nothing selected yet setTier(t); } const teamName = teams.find((x) => x.id === teamId)?.name; const companyName = companies.find((x) => x.id === companyId)?.name; const orgName = orgs.find((x) => x.id === orgId)?.name; const clawName = clawId ? (rosterMap.get(clawId)?.name ?? "Claw") : null; const crumbs = [orgName, companyName, teamName, tier === "claw" ? clawName : null].filter( Boolean, ) as string[]; const initial = (user.display_name || user.email || "?").trim().charAt(0).toUpperCase(); const running = stats?.running_now ?? 0; const clawCount = stats?.claw_count ?? roster.length; const empty = loaded && teams.length === 0 && companies.length === 0 && orgs.length === 0; return (
{/* TOP BAR */}
Clawmates
0 ? "cm-blink" : ""}`} /> {clawCount} claws{running > 0 ? " · running" : ""}
+ Deploy {initial}
{/* BODY */}
{/* TIER RAIL */}
{TIERS.map((t) => { const active = tier === t.key; return ( ); })}
{/* CONTEXT LIST */}
{tier === "org" || tier === "company" || tier === "team" ? `${tier.toUpperCase()} · ${nodes.length}` : "CLAW"}
{node?.name ?? "…"}
{nodes.length === 0 ? (

{loaded ? "Empty" : "Loading…"}

) : (
    {nodes.map((n) => { const sel = n.level === "claw" && n.id === clawId; const grad = rosterMap.get(n.id)?.accent; return (
  • ); })}
)}
{/* CANVAS */}
{empty ? (

No teams yet

Deploy a team to populate your dashboard.

Deploy a team
) : ( )}
{/* COMPUTER SLIDE-OUT */} {clawId ? (
COMPUTER
) : null}
{/* STATUS BAR */}
); }