Recursive deploy ladder: Company + Org tiers, mesh mark, two-tier rail

Completes the scale ladder (single → team → company → org). Every tier is a
topology whose nodes are the tier below; running a parent recursively runs each
child's sub-topology down to the leaf claws.

Backend:
- migration 0011: companies/company_teams, orgs/org_companies, topology_runs.tier
- cm-db repos for companies + orgs (mirror teams)
- TurnRequest.attrs (forwarded from node.attrs) for child-id binding
- SubTopologyExecutor (recursive_exec.rs): a parent "turn" runs the child's
  sub-topology; durability via parent updated_at keepalive + cancel propagation
  + depth cap; boxed future breaks the org→company recursion
- topology_worker selects executor by job.tier
- routes: /api/companies, /api/orgs (create/list/get/run) + unified
  /api/structure/{level}/{id} for the zoom canvas

Frontend:
- MeshMark: node-mesh brand glyph (replaces the claw PNG), tier variants
- TopologyGraphView: optional onNodeClick/nodeMeta + dark-token theming
- StructureCanvas + Breadcrumb: one recursive zoom view for every tier
  (drill down on node click, breadcrumb up); TeamRunPanel extracted + shared
- two-tier Discord-style rail: StructureRail (mesh mark + org/company/team
  glyphs + tools popover + deploy + user) | RosterColumn (selected group's
  children, or your claws); SecondaryNav for cross-cutting tools
- ComposeWizard (company/org) wired into DeployWizard; /companies + /orgs pages

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-18 14:25:06 -07:00
co-authored by Claude Opus 4.8
parent bba18a4687
commit 3eca4ed70c
58 changed files with 3221 additions and 229 deletions
@@ -1,4 +1,4 @@
import type { TopologyGraph } from "@/lib/api/topology";
import type { TopologyGraph, TopologyNode } from "@/lib/api/topology";
const ROW_KINDS = new Set(["pipeline", "ring"]);
const STAR_KINDS = new Set(["hierarchical", "hub_spoke", "star_moe", "market"]);
@@ -6,6 +6,13 @@ const STAR_KINDS = new Set(["hierarchical", "hub_spoke", "star_moe", "market"]);
const W = 640;
const H = 340;
/** Per-node display hints for the recursive zoom canvas: a human label and
* whether clicking it drills into a child level. */
export interface NodeMeta {
label?: string;
drillable?: boolean;
}
/** Position nodes by topology kind: a row for pipeline/ring, a center+row star
* for delegation kinds, a circle otherwise. */
function layout(kind: string, n: number): { x: number; y: number }[] {
@@ -33,15 +40,29 @@ function layout(kind: string, n: number): { x: number; y: number }[] {
});
}
/** Renders a topology graph as a lightweight SVG (no external deps). */
export function TopologyGraphView({ graph }: { graph: TopologyGraph }) {
interface TopologyGraphViewProps {
graph: TopologyGraph;
/** When set, nodes become interactive (drill down a level). */
onNodeClick?: (node: TopologyNode) => void;
/** Optional per-node label / drillability overrides (keyed by node id). */
nodeMeta?: Record<string, NodeMeta>;
}
/** Renders a topology graph as a lightweight SVG (no external deps), themed for
* the dark app shell. With `onNodeClick`, nodes are keyboard-accessible drill
* targets (the recursive zoom canvas uses this to descend org→company→team→claw). */
export function TopologyGraphView({
graph,
onNodeClick,
nodeMeta,
}: TopologyGraphViewProps) {
const index = new Map(graph.nodes.map((node, i) => [node.id, i]));
const pos = layout(graph.kind, graph.nodes.length);
return (
<svg
viewBox={`0 0 ${W} ${H}`}
className="h-auto w-full"
className="h-auto w-full text-muted-foreground"
role="img"
aria-label={`${graph.kind} topology with ${graph.nodes.length} nodes`}
>
@@ -56,20 +77,65 @@ export function TopologyGraphView({ graph }: { graph: TopologyGraph }) {
y1={a.y}
x2={b.x}
y2={b.y}
stroke="#64748b"
stroke="currentColor"
strokeOpacity={0.35}
strokeWidth={1.5}
/>
);
})}
{graph.nodes.map((node, i) => {
const p = pos[i];
const meta = nodeMeta?.[node.id];
const drillable = !!onNodeClick && meta?.drillable !== false;
const label = meta?.label ?? node.id;
return (
<g key={node.id}>
<circle cx={p.x} cy={p.y} r={24} fill="#e2e8f0" stroke="#94a3b8" />
<text x={p.x} y={p.y + 4} textAnchor="middle" fontSize={11} fontWeight={600} fill="#0b1220">
{node.id}
<g
key={node.id}
className={
drillable
? "cursor-pointer outline-none [&:hover_circle]:stroke-coral [&:focus_circle]:stroke-coral"
: undefined
}
role={drillable ? "button" : undefined}
tabIndex={drillable ? 0 : undefined}
aria-label={drillable ? `Open ${label}` : undefined}
onClick={drillable ? () => onNodeClick?.(node) : undefined}
onKeyDown={
drillable
? (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onNodeClick?.(node);
}
}
: undefined
}
>
<circle
cx={p.x}
cy={p.y}
r={24}
fill="var(--color-surface-warm-muted)"
stroke="var(--color-border)"
strokeWidth={1.5}
/>
<text
x={p.x}
y={p.y + 4}
textAnchor="middle"
fontSize={11}
fontWeight={600}
fill="var(--color-foreground)"
>
{label.length > 8 ? `${label.slice(0, 7)}…` : label}
</text>
<text x={p.x} y={p.y + 42} textAnchor="middle" fontSize={11} fill="#94a3b8">
<text
x={p.x}
y={p.y + 42}
textAnchor="middle"
fontSize={11}
fill="currentColor"
>
{node.role}
</text>
</g>