// Modules = reusable building blocks, each a sub-project with its own Forgejo // repo + spec (brief) + roadmap. Agents improve a module the same way they work // any project. This screen: Library (cards) / Graph (deps) / Matrix (project×module). // Reuses globals from board.jsx (Icon, mdToHtml) and pages.jsx (RepoFiles, // RoadmapModal, BriefModal). brief/roadmap/files hit the shared project endpoints. const MOD_CAT_ORDER = ['foundation', 'content', 'support', 'revenue', 'insight']; const MOD_STATUS = { planned: 'var(--c-text-3)', building: 'var(--c-status-running)', done: 'var(--c-status-done)' }; // ---- dependency layering (column = dependency depth) ---- function moduleLayers(modules) { const by = Object.fromEntries(modules.map(m => [m.id, m])); const memo = {}; const depth = (id, seen = new Set()) => { if (id in memo) return memo[id]; if (seen.has(id)) return 0; // cycle guard seen.add(id); const m = by[id]; const deps = (m && m.deps) || []; const d = deps.length ? 1 + Math.max(...deps.map(x => (by[x] ? depth(x, seen) : -1))) : 0; return (memo[id] = d); }; modules.forEach(m => depth(m.id)); const layers = []; modules.forEach(m => { const d = memo[m.id]; (layers[d] = layers[d] || []).push(m); }); return layers; } function ModuleGraph({ modules }) { const layers = moduleLayers(modules); const NW = 160, NH = 48, VGAP = 20, COLW = 230, PADX = 24, PADY = 28; const pos = {}; layers.forEach((col, li) => (col || []).forEach((m, ri) => { pos[m.id] = { x: PADX + li * COLW, y: PADY + ri * (NH + VGAP), m }; })); const W = PADX * 2 + (layers.length - 1) * COLW + NW; const maxRows = Math.max(1, ...layers.map(c => (c || []).length)); const H = PADY * 2 + maxRows * (NH + VGAP) - VGAP; const edges = []; modules.forEach(m => ((m.deps || []).forEach(dep => { if (pos[dep] && pos[m.id]) edges.push({ from: pos[dep], to: pos[m.id], key: `${dep}-${m.id}` }); }))); return (
{edges.map(e => { const x1 = e.from.x + NW, y1 = e.from.y + NH / 2; const x2 = e.to.x, y2 = e.to.y + NH / 2; const mx = (x1 + x2) / 2; return ; })} {Object.values(pos).map(({ x, y, m }) => ( {m.icon || m.name[0]} {m.name} {m.category} ))}
); } function AssemblyMatrix({ data, onToggle, busyCell }) { const { projects, modules, links } = data; const linkOf = (pid, mid) => links.find(l => l.project_id === pid && l.module_id === mid); if (!projects.length) return
no projects yet
; return (
{modules.map(m => ( ))} {projects.map(p => ( {modules.map(m => { const link = linkOf(p.id, m.id); const busy = busyCell === `${p.id}:${m.id}`; return ( ); })} ))}
Project \ Module {m.icon || m.name[0]} {m.name}
{p.name}
{Object.entries(MOD_STATUS).map(([k, c]) => ( {k} ))}
); } function ImproveModal({ module, onSubmit, onClose }) { const [text, setText] = React.useState(''); const [busy, setBusy] = React.useState(false); async function submit() { if (!text.trim()) return; setBusy(true); try { await onSubmit(text.trim()); onClose(); } finally { setBusy(false); } } return (
e.stopPropagation()}>
Improve {module.name}

codex-agent works the module's own repo ({module.repo}). The spec is sent as context automatically.