diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ffc5877..5f2e78c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -5,7 +5,7 @@ {"id":"bd-195","title":"ALT-007: SQLite direct query fallback","description":"For HUMAN bead bd-3sh. Query beads.db directly using sqlite3 or Node.js better-sqlite3. Bypasses br CLI entirely. Requires sqlite3 CLI or npm package. Fastest access but tight coupling to schema.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T08:39:58.775979286Z","created_by":"coder","updated_at":"2026-03-03T10:33:32.997760049Z","closed_at":"2026-03-03T10:33:31.799597115Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["alternative","br","resilience","worker"],"comments":[{"id":32,"issue_id":"bd-195","author":"Jed Arden","text":"No longer needed - br v0.1.20 fixes the schema bug natively.","created_at":"2026-03-03T10:33:32Z"}]} {"id":"bd-1a2","title":"P2: Add unit tests for parser.ts","description":"Add comprehensive unit tests for src/parser.ts covering: JSON parsing, formatEvent function, edge cases (malformed JSON, missing fields), and colorization options. Follow vitest patterns from tailer.test.ts.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T07:50:12.670624516Z","created_by":"coder","updated_at":"2026-03-03T10:45:42.655993370Z","closed_at":"2026-03-03T10:45:42.654557737Z","close_reason":"Parser tests already implemented in bd-5eh (36 tests covering parseLogLine, parseLogLines, formatEvent)","source_repo":".","compaction_level":0,"original_size":0,"labels":["parser","testing","unit-test"]} {"id":"bd-1c6","title":"TEST-002: Add store integration tests","description":"Test Coverage: Add integration tests for EventStore - event indexing, LRU eviction, worker tracking, query performance.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-03-03T07:53:40.409846186Z","created_by":"coder","updated_at":"2026-03-03T07:53:40.409846186Z","closed_at":"2026-03-03T07:53:40.409846186Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["store","testing"]} -{"id":"bd-1cc","title":"Port FileHeatmap component to web dashboard","description":"Port the TUI FileHeatmap component (src/tui/components/FileHeatmap.ts) to React for the web dashboard. Add corresponding API endpoint if needed.","status":"in_progress","priority":3,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:27:43.759301428Z","created_by":"coder","updated_at":"2026-03-03T14:58:30.513770688Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-4","web"]} +{"id":"bd-1cc","title":"Port FileHeatmap component to web dashboard","description":"Port the TUI FileHeatmap component (src/tui/components/FileHeatmap.ts) to React for the web dashboard. Add corresponding API endpoint if needed.","status":"closed","priority":3,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:27:43.759301428Z","created_by":"coder","updated_at":"2026-03-03T15:08:13.822190937Z","closed_at":"2026-03-03T15:08:13.792085291Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-4","web"]} {"id":"bd-1e1","title":"P3-001: Setup Express HTTP server with static file serving","description":"Phase 3 Web Dashboard: Create Express server in src/web/server.ts that serves static files and handles WebSocket upgrade. Foundation for web dashboard.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-03T07:52:09.228852666Z","created_by":"coder","updated_at":"2026-03-03T10:05:21.171663977Z","closed_at":"2026-03-03T10:05:21.171457522Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["phase-3","server","web"]} {"id":"bd-1fe","title":"Add RecoveryPanel component to web frontend","description":"Port TUI RecoveryPanel.ts to React web frontend. Create src/web/frontend/src/components/RecoveryPanel.tsx showing recovery suggestions for stuck workers.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T14:26:22.464306236Z","created_by":"coder","updated_at":"2026-03-03T14:26:22.464306236Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]} {"id":"bd-1fk","title":"P1: Add Express HTTP server for web dashboard","description":"Set up Express.js HTTP server with basic routing for web dashboard. Should serve static files and provide API endpoints for worker data. Part of Phase 3 Web Dashboard.","status":"closed","priority":1,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-03-03T07:50:12.280655428Z","created_by":"coder","updated_at":"2026-03-03T09:37:50.271173474Z","closed_at":"2026-03-03T08:51:00.693337164Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["http","phase-3","server","web"],"comments":[{"id":10,"issue_id":"bd-1fk","author":"Jed Arden","text":"Express server implemented in src/web/server.ts","created_at":"2026-03-03T08:52:43Z"}]} @@ -18,7 +18,7 @@ {"id":"bd-1k7","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 23231s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","assignee":"coder","created_at":"2026-03-03T10:50:43.655296543Z","created_by":"coder","updated_at":"2026-03-03T10:54:08.648495859Z","closed_at":"2026-03-03T10:54:08.646738418Z","close_reason":"FALSE POSITIVE: Worker starvation alert triggered incorrectly. Ready queue has 22 beads available (ready-queue.json). The br ready command shows epic bd-2kf and other work exists. Worker discovery logic should check ready-queue.json directly before creating starvation alerts. This follows the established pattern for false-positive starvation alerts.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-1l5","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 30662s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","assignee":"coder","created_at":"2026-03-03T12:54:35.115007190Z","created_by":"coder","updated_at":"2026-03-03T12:55:34.048020006Z","closed_at":"2026-03-03T12:55:34.034977802Z","close_reason":"FALSE POSITIVE: Ready queue has 22 available beads. Worker did not check ready-queue.json before escalating starvation alert. See MEMORY.md pattern.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-1lc","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 19477s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:48:10.053171230Z","created_by":"coder","updated_at":"2026-03-03T09:55:17.989495128Z","closed_at":"2026-03-03T09:55:07.746952209Z","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":20,"issue_id":"bd-1lc","author":"Jed Arden","text":"FALSE POSITIVE: ready-queue.json contains 22 available beads. Worker discovery failed to check ready queue before escalating. Closed per Worker Starvation Resolution pattern.","created_at":"2026-03-03T09:55:17Z"}]} -{"id":"bd-1mh","title":"Add DependencyDag component to web frontend","description":"Port TUI DependencyDag.ts to React web frontend. Create src/web/frontend/src/components/DependencyDag.tsx with interactive task dependency graph visualization.","status":"open","priority":2,"issue_type":"task","created_at":"2026-03-03T14:26:17.941694993Z","created_by":"coder","updated_at":"2026-03-03T14:26:17.941694993Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]} +{"id":"bd-1mh","title":"Add DependencyDag component to web frontend","description":"Port TUI DependencyDag.ts to React web frontend. Create src/web/frontend/src/components/DependencyDag.tsx with interactive task dependency graph visualization.","status":"in_progress","priority":2,"issue_type":"task","assignee":"coder","created_at":"2026-03-03T14:26:17.941694993Z","created_by":"coder","updated_at":"2026-03-03T15:00:57.556080545Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["frontend","phase-3","web"]} {"id":"bd-1mq","title":"ALT-003: Use br list JSON instead of br ready","description":"For HUMAN bd-1sw. Create a wrapper script that uses br list --all --format json (which works) to find available work instead of br ready (which has schema bug). Script created at scripts/br-ready-jsonl.sh. Drop-in replacement for br ready command.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-03-03T08:30:23.972858496Z","created_by":"coder","updated_at":"2026-03-03T09:59:56.375702385Z","closed_at":"2026-03-03T09:59:56.375449151Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["alternative","discovery","worker"],"dependencies":[{"issue_id":"bd-1mq","depends_on_id":"bd-1sw","type":"blocks","created_at":"2026-03-03T08:30:48.335086821Z","created_by":"coder","metadata":"{}","thread_id":""}],"comments":[{"id":23,"issue_id":"bd-1mq","author":"Jed Arden","text":"Implementation verified working. Script outputs 17 available beads correctly in both JSON and table formats.","created_at":"2026-03-03T09:59:56Z"}]} {"id":"bd-1ms","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 19113s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:42:05.483000034Z","created_by":"coder","updated_at":"2026-03-03T09:44:49.799179920Z","closed_at":"2026-03-03T09:44:49.798973717Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-1of","title":"ALERT: Worker claude-code-glm-5-bravo has no work available","description":"# Worker Starvation Alert\n\nWorker **claude-code-glm-5-bravo** has exhausted all priorities and found zero work.\n\nThis is considered an error state - there should always be more work.\n\n## Worker State\n\n- **Executor:** claude-code-glm-5\n- **Model:** glm-5\n- **Workspace:** /home/coder/FABRIC\n- **Root Boundary:** /home/coder/FABRIC\n- **Last completion:** \n- **Beads completed:** 0\n- **Claim success rate:** %\n- **Uptime:** 17909s (h)\n- **Consecutive empty iterations:** 5\n\n## Priorities Exhausted\n\n1. ✗ Local workspace (bottoms-up): No beads in /home/coder/FABRIC or subfolders\n2. ✗ Parent exploration: No suitable workspaces found\n3. ✓ Maintenance: Completed (cleaned orphaned claims/locks)\n4. ✗ Gap analysis: false - No gaps found or created\n5. ✗ HUMAN alternatives: true - No HUMAN beads found to unblock\n\n## Discovered Workspaces\n\nTotal: 1\n\n- /home/coder/FABRIC\n\n## Required Actions\n\n1. Review discovery roots: Are all project folders being scanned?\n2. Check if projects need new features/tasks\n3. Review ROADMAP.md files across projects\n4. Enable gap analysis if disabled: `--enable-gap-analysis`\n5. Enable HUMAN alternatives if disabled\n6. Create manual beads to bootstrap work\n\n---\n*This alert was created automatically by Priority 6*","status":"closed","priority":0,"issue_type":"human","created_at":"2026-03-03T09:22:01.376456909Z","created_by":"coder","updated_at":"2026-03-03T09:23:18.070431328Z","closed_at":"2026-03-03T09:23:09.029612467Z","source_repo":".","compaction_level":0,"original_size":0,"comments":[{"id":18,"issue_id":"bd-1of","author":"Jed Arden","text":"FALSE POSITIVE - ready-queue.json has 22 available beads. Worker discovery is not checking ready-queue.json properly (known issue tracked in bd-b02). Available work: bd-2zt, bd-2ed, bd-1fk, bd-1sk, bd-2qr and 17 more.","created_at":"2026-03-03T09:23:18Z"}]} diff --git a/src/web/frontend/src/components/DependencyDag.tsx b/src/web/frontend/src/components/DependencyDag.tsx new file mode 100644 index 0000000..2052534 --- /dev/null +++ b/src/web/frontend/src/components/DependencyDag.tsx @@ -0,0 +1,648 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { + DependencyGraph, + DagStats, + BeadNode, + BeadStatus, + DagOptions, + DagViewMode, +} from '../types'; + +interface DependencyDagProps { + visible: boolean; + onClose: () => void; +} + +// Status icons and colors +const getStatusIcon = (status: BeadStatus): string => { + switch (status) { + case 'open': return '○'; + case 'in_progress': return '◐'; + case 'blocked': return '⛔'; + case 'completed': return '●'; + case 'closed': return '✓'; + case 'deferred': return '⏸'; + default: return '?'; + } +}; + +const getStatusColor = (status: BeadStatus): string => { + switch (status) { + case 'open': return 'var(--text-secondary)'; + case 'in_progress': return '#00bcd4'; + case 'blocked': return 'var(--error)'; + case 'completed': return 'var(--success)'; + case 'closed': return 'var(--success)'; + case 'deferred': return 'var(--warning)'; + default: return 'var(--text-secondary)'; + } +}; + +const getStatusClassName = (status: BeadStatus): string => { + switch (status) { + case 'open': return 'status-open'; + case 'in_progress': return 'status-progress'; + case 'blocked': return 'status-blocked'; + case 'completed': return 'status-completed'; + case 'closed': return 'status-closed'; + case 'deferred': return 'status-deferred'; + default: return 'status-unknown'; + } +}; + +const getPriorityLabel = (priority: number): string => { + switch (priority) { + case 0: return 'P0'; + case 1: return 'P1'; + case 2: return 'P2'; + case 3: return 'P3'; + case 4: return 'P4'; + default: return 'P?'; + } +}; + +const getPriorityClassName = (priority: number): string => { + switch (priority) { + case 0: return 'priority-critical'; + case 1: return 'priority-high'; + case 2: return 'priority-normal'; + case 3: return 'priority-low'; + case 4: return 'priority-backlog'; + default: return 'priority-unknown'; + } +}; + +// Helper functions for graph analysis +const getTopBlockers = (graph: DependencyGraph | null, limit: number = 15): BeadNode[] => { + if (!graph) return []; + const allNodes: BeadNode[] = []; + for (const component of graph.components) { + allNodes.push(...component.nodes); + } + allNodes.sort((a, b) => b.dependentCount - a.dependentCount); + return allNodes.filter(n => n.dependentCount > 0).slice(0, limit); +}; + +const getReadyBeads = (graph: DependencyGraph | null): BeadNode[] => { + if (!graph) return []; + const ready: BeadNode[] = []; + for (const component of graph.components) { + for (const node of component.nodes) { + if (node.status === 'open' && node.dependencyCount === 0) { + ready.push(node); + } + } + } + ready.sort((a, b) => a.priority - b.priority); + return ready; +}; + +const DependencyDag: React.FC = ({ visible, onClose }) => { + const [graph, setGraph] = useState(null); + const [stats, setStats] = useState(null); + const [viewMode, setViewMode] = useState('tree'); + const [selectedIndex, setSelectedIndex] = useState(0); + const [filterOptions, setFilterOptions] = useState({}); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selectedBead, setSelectedBead] = useState(null); + + // Fetch dependency graph from API + const fetchGraph = useCallback(async () => { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams(); + if (filterOptions.status && filterOptions.status !== 'all') { + params.set('status', filterOptions.status); + } + if (filterOptions.criticalOnly) { + params.set('criticalOnly', 'true'); + } + if (filterOptions.maxDepth !== undefined) { + params.set('maxDepth', filterOptions.maxDepth.toString()); + } + if (filterOptions.includeClosed) { + params.set('includeClosed', 'true'); + } + + const response = await fetch(`/api/dag?${params.toString()}`); + if (!response.ok) { + throw new Error(`Failed to fetch DAG: ${response.statusText}`); + } + const data = await response.json(); + setGraph(data.graph); + setStats(data.stats); + setSelectedIndex(0); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }, [filterOptions]); + + useEffect(() => { + if (visible) { + fetchGraph(); + } + }, [visible, fetchGraph]); + + // Get items based on view mode + const currentItems = useMemo(() => { + if (!graph) return []; + switch (viewMode) { + case 'blockers': + return getTopBlockers(graph); + case 'ready': + return getReadyBeads(graph); + default: + const allNodes: BeadNode[] = []; + for (const component of graph.components) { + allNodes.push(...component.nodes); + } + return allNodes; + } + }, [graph, viewMode]); + + // Cycle filter options + const cycleFilter = useCallback(() => { + const filters: Array<{ key: keyof DagOptions; value: any }> = [ + { key: 'status', value: undefined }, + { key: 'status', value: 'blocked' as BeadStatus }, + { key: 'status', value: 'in_progress' as BeadStatus }, + { key: 'criticalOnly', value: true }, + { key: 'criticalOnly', value: false }, + ]; + + const currentIdx = filters.findIndex( + (f) => + (f.key === 'status' && filterOptions.status === f.value) || + (f.key === 'criticalOnly' && filterOptions.criticalOnly === f.value) + ); + + const nextIdx = (currentIdx + 1) % filters.length; + const nextFilter = filters[nextIdx]; + + setFilterOptions({ ...filterOptions, [nextFilter.key]: nextFilter.value }); + }, [filterOptions]); + + // Get filter description + const getFilterDescription = (): string => { + const parts: string[] = []; + if (filterOptions.status) { + parts.push(`status=${filterOptions.status}`); + } + if (filterOptions.criticalOnly) { + parts.push('critical-only'); + } + if (filterOptions.maxDepth !== undefined) { + parts.push(`depth≤${filterOptions.maxDepth}`); + } + return parts.length > 0 ? ` [${parts.join(', ')}]` : ''; + }; + + // Render tree node recursively + const renderTreeNode = ( + node: BeadNode, + componentIndex: number, + depth: number, + isLast: boolean, + visited: Set + ): React.ReactNode => { + if (depth > 5) return null; + if (visited.has(node.id)) { + return ( +
+ ↩ {node.id} (cycle) +
+ ); + } + + const component = graph?.components[componentIndex]; + if (!component) return null; + + const children = component.edges + .filter(e => e.from === node.id) + .map(e => component.nodes.find(n => n.id === e.to)) + .filter((n): n is BeadNode => n !== undefined); + + return ( +
+
setSelectedBead(node)} + > + {isLast ? '└─' : '├─'} + + {getStatusIcon(node.status)} + + + {node.id} + + + [{getPriorityLabel(node.priority)}] + + {node.isCriticalPath && } +
+ {children.map((child, i) => + renderTreeNode( + child, + componentIndex, + depth + 1, + i === children.length - 1, + new Set([...visited, node.id]) + ) + )} +
+ ); + }; + + // Render tree view + const renderTreeView = (): React.ReactNode => { + if (!graph) return null; + + if (graph.components.length === 0) { + return ( +
+

No dependencies found

+

Tasks with dependencies will appear here.

+
+ ); + } + + return ( +
+ {graph.components.map((component, componentIndex) => ( +
+ {component.hasCycle && ( +
+ ⚠ Cycle detected in this component! +
+ )} + {component.criticalPath.length > 0 && ( +
+ ⚡ Critical path: {component.criticalPath.map((id, i) => ( + + {id} + {i < component.criticalPath.length - 1 && } + + ))} +
+ )} +
+ {component.roots.length > 0 + ? component.roots.map((rootId, i) => { + const rootNode = component.nodes.find(n => n.id === rootId); + if (!rootNode) return null; + return ( + + {renderTreeNode( + rootNode, + componentIndex, + 0, + i === component.roots.length - 1, + new Set() + )} + + ); + }) + : component.nodes.map((node, i) => + renderTreeNode(node, componentIndex, 0, i === component.nodes.length - 1, new Set()) + )} +
+
+ ))} +
+ ); + }; + + // Render blockers view + const renderBlockersView = (): React.ReactNode => { + const blockers = getTopBlockers(graph, 15); + + if (blockers.length === 0) { + return ( +
+

No blockers found!

+

All tasks are unblocked.

+
+ ); + } + + return ( +
+

Tasks blocking the most other tasks:

+
+ {blockers.map((node, i) => ( +
{ + setSelectedIndex(i); + setSelectedBead(node); + }} + > + + {getStatusIcon(node.status)} + + + {node.id} + + + [{getPriorityLabel(node.priority)}] + + + {node.dependentCount} blocked + + {node.isCriticalPath && } +
{node.title.slice(0, 50)}
+
+ ))} +
+
+ ); + }; + + // Render ready view + const renderReadyView = (): React.ReactNode => { + const ready = getReadyBeads(graph); + + if (ready.length === 0) { + return ( +
+

No ready tasks found.

+

All open tasks have blocking dependencies.

+

Complete blockers to unlock new work.

+
+ ); + } + + return ( +
+

{ready.length} tasks ready to work on:

+
+ {ready.map((node, i) => ( +
{ + setSelectedIndex(i); + setSelectedBead(node); + }} + > + + {getStatusIcon(node.status)} + + + {node.id} + + + [{getPriorityLabel(node.priority)}] + + {node.isCriticalPath && } +
{node.title.slice(0, 50)}
+
+ ))} +
+
+ ); + }; + + // Render stats view + const renderStatsView = (): React.ReactNode => { + if (!stats || !graph) return null; + + return ( +
+
+

Overview

+
+ Total Beads: + {stats.totalBeads} +
+
+ Components: + {graph.totalComponents} +
+
+ Total Edges: + {graph.totalEdges} +
+
+ +
+

Status Breakdown

+
+ Ready: + {stats.readyCount} +
+
+ Blocked: + {stats.blockedCount} +
+
+ +
+

Graph Depth

+
+ Maximum: + {stats.maxDepth} +
+
+ +
+

Critical Path

+
+ Length: + {stats.criticalPathLength} +
+
+ Beads on path: + {stats.criticalPathBeads} +
+ {graph.globalCriticalPath.length > 0 && ( +
+ Path: +
+ {graph.globalCriticalPath.slice(0, 5).map((id, i) => ( + + → {id} + + ))} + {graph.globalCriticalPath.length > 5 && ( + + ... and {graph.globalCriticalPath.length - 5} more + + )} +
+
+ )} +
+ +
+

Averages

+
+ Dependencies: + {stats.avgDependencies.toFixed(1)} +
+
+ Dependents: + {stats.avgDependents.toFixed(1)} +
+
+ + {stats.cycleCount > 0 && ( +
+ ⚠ {stats.cycleCount} cycle(s) detected! +

Circular dependencies prevent proper execution.

+
+ )} +
+ ); + }; + + if (!visible) return null; + + return ( +
+
+

+ 🔗 + Task Dependency DAG + {graph && {graph.totalNodes}} +

+
+ + + +
+
+ +
+ + + + +
+ +
+ {loading && !graph && ( +
Loading dependency graph...
+ )} + + {error && ( +
+

Error loading dependency graph

+

{error}

+ +
+ )} + + {!loading && !error && ( + <> +
+ {viewMode.charAt(0).toUpperCase() + viewMode.slice(1)} View{getFilterDescription()} +
+
+ {viewMode === 'tree' && renderTreeView()} + {viewMode === 'blockers' && renderBlockersView()} + {viewMode === 'ready' && renderReadyView()} + {viewMode === 'stats' && renderStatsView()} +
+ + )} +
+ + {selectedBead && ( +
+
+

{selectedBead.id}

+ +
+
+
+ Title: + {selectedBead.title} +
+
+ Status: + + {getStatusIcon(selectedBead.status)} {selectedBead.status} + +
+
+ Priority: + + {getPriorityLabel(selectedBead.priority)} + +
+
+ Depth: + {selectedBead.depth} +
+
+ Dependencies: + {selectedBead.dependencyCount} +
+
+ Blocking: + {selectedBead.dependentCount} +
+ {selectedBead.isCriticalPath && ( +
+ ⚡ On critical path +
+ )} +
+
+ )} +
+ ); +}; + +export default DependencyDag; diff --git a/src/web/frontend/src/index.css b/src/web/frontend/src/index.css index c9e8526..68900bb 100644 --- a/src/web/frontend/src/index.css +++ b/src/web/frontend/src/index.css @@ -1005,3 +1005,873 @@ body { flex-direction: column; } } + +/* ============================================ + File Heatmap Component Styles + ============================================ */ + +.file-heatmap-panel { + display: flex; + flex-direction: column; + background: var(--bg-secondary); + border: 1px solid var(--bg-tertiary); + border-radius: 6px; + overflow: hidden; + min-width: 350px; + max-width: 450px; + max-height: 500px; +} + +.file-heatmap-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--bg-primary); +} + +.file-heatmap-header h2 { + font-size: 0.875rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; +} + +.file-heatmap-close { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + transition: all 0.2s; +} + +.file-heatmap-close:hover { + background: var(--bg-primary); + color: var(--text-primary); +} + +.file-heatmap-controls { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: var(--bg-primary); + border-bottom: 1px solid var(--bg-tertiary); +} + +.file-heatmap-filter { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.file-heatmap-filter input { + background: var(--bg-tertiary); + border: 1px solid var(--bg-secondary); + border-radius: 4px; + color: var(--text-primary); + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + min-width: 150px; +} + +.file-heatmap-filter input:focus { + outline: none; + border-color: var(--accent); +} + +.file-heatmap-toggle { + display: flex; + align-items: center; + gap: 0.25rem; + background: var(--bg-tertiary); + border: 1px solid var(--bg-secondary); + color: var(--text-secondary); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.file-heatmap-toggle:hover { + background: var(--bg-primary); +} + +.file-heatmap-toggle.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.file-heatmap-stats { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.75rem; + border-bottom: 1px solid var(--bg-tertiary); +} + +.file-heatmap-stat { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.file-heatmap-stat-label { + color: var(--text-secondary); + font-size: 0.7rem; +} + +.file-heatmap-stat-value { + font-weight: 600; + font-size: 0.875rem; +} + +.file-heatmap-stat-value.critical { + color: var(--error); +} + +.file-heatmap-stat-value.warning { + color: var(--warning); +} + +.file-heatmap-content { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.file-heatmap-list { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.file-heatmap-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + color: var(--text-secondary); + gap: 0.5rem; +} + +.file-heatmap-empty-icon { + font-size: 2rem; + opacity: 0.5; +} + +.file-heatmap-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.5rem; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.file-heatmap-item:hover { + background: var(--bg-tertiary); +} + +.file-heatmap-item.selected { + background: var(--bg-tertiary); + border: 1px solid var(--accent); +} + +.file-heatmap-item.collision { + border-left: 3px solid var(--warning); +} + +.file-heatmap-icon { + min-width: 20px; + text-align: center; + font-size: 1rem; +} + +.file-heatmap-bar-container { + display: flex; + align-items: center; + gap: 0.25rem; + flex: 1; +} + +.file-heatmap-bar { + height: 8px; + background: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; + flex: 1; +} + +.file-heatmap-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.2s ease; +} + +.file-heatmap-bar-fill.cold { background: #4fc3f7; } +.file-heatmap-bar-fill.warm { background: #ffb74d; } +.file-heatmap-bar-fill.hot { background: #f06292; } +.file-heatmap-bar-fill.critical { background: #e53935; } + +.file-heatmap-path { + font-family: 'SF Mono', Monaco, monospace; + font-size: 0.75rem; + color: var(--text-primary); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-heatmap-mod-count { + color: var(--text-secondary); + font-size: 0.75rem; + min-width: 30px; + text-align: right; +} + +.file-heatmap-workers { + color: #00bcd4; + font-size: 0.75rem; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; +} + +.file-heatmap-collision-indicator { + min-width: 20px; + text-align: center; +} + +.file-heatmap-collision-indicator.has-collision { + color: var(--warning); + animation: pulse-warning 2s infinite; +} + +.file-heatmap-collision-indicator.multiple-workers { + color: var(--warning); +} + +.file-heatmap-detail { + padding: 0.75rem; + border-top: 1px solid var(--bg-tertiary); + margin-top: 0.5rem; +} + +.file-heatmap-detail-header { + font-weight: 600; + font-size: 0.875rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.file-heatmap-detail-path { + font-family: 'SF Mono', Monaco, monospace; + font-size: 0.8125rem; + color: var(--text-primary); + word-break: break-all; +} + +.file-heatmap-detail-row { + display: flex; + justify-content: space-between; + padding: 0.25rem 0; + font-size: 0.8125rem; +} + +.file-heatmap-detail-label { + color: var(--text-secondary); +} + +.file-heatmap-detail-value { + font-weight: 500; +} + +.file-heatmap-detail-workers { + margin-top: 0.5rem; +} + +.file-heatmap-worker-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0; + font-size: 0.75rem; +} + +.file-heatmap-worker-bar { + height: 4px; + background: var(--bg-tertiary); + border-radius: 2px; + width: 60px; + overflow: hidden; +} + +.file-heatmap-worker-bar-fill { + height: 100%; + background: var(--accent); +} + +.file-heatmap-footer { + display: flex; + justify-content: space-between; + padding: 0.5rem 1rem; + background: var(--bg-primary); + border-top: 1px solid var(--bg-tertiary); + font-size: 0.75rem; + color: var(--text-secondary); +} + +.file-heatmap-help { + display: flex; + gap: 1rem; +} + +.file-heatmap-help span { + opacity: 0.7; +} + +/* Responsive adjustments for */ +@media (max-width: 768px) { + .file-heatmap-panel { + max-width: 100%; + max-height: 80vh; + } + + .file-heatmap-stats { + grid-template-columns: repeat(3, 1fr); + } +} + +/* ============================================ + Dependency DAG Panel Styles + ============================================ */ + +.dag-panel { + display: flex; + flex-direction: column; + background: var(--bg-secondary); + border: 1px solid var(--bg-tertiary); + border-radius: 6px; + overflow: hidden; + min-width: 400px; + max-width: 600px; + max-height: 80vh; +} + +.dag-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--bg-primary); +} + +.dag-header h2 { + font-size: 0.875rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + color: var(--text-primary); +} + +.dag-header-icon { + font-size: 1rem; +} + +.dag-count { + background: var(--accent); + color: #fff; + font-size: 0.7rem; + padding: 0.125rem 0.375rem; + border-radius: 10px; + margin-left: 0.25rem; +} + +.dag-header-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.dag-btn { + background: var(--bg-tertiary); + border: none; + color: var(--text-secondary); + padding: 0.375rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.dag-btn:hover:not(:disabled) { + background: var(--bg-primary); + color: var(--accent); +} + +.dag-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.dag-btn-secondary { + background: var(--bg-primary); +} + +.dag-btn-close { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +.dag-view-modes { + display: flex; + gap: 0.25rem; + padding: 0.5rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--bg-tertiary); +} + +.dag-mode-btn { + flex: 1; + background: var(--bg-tertiary); + border: none; + color: var(--text-secondary); + padding: 0.375rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.dag-mode-btn:hover { + background: var(--bg-primary); + color: var(--text-primary); +} + +.dag-mode-btn.active { + background: var(--accent); + color: #fff; +} + +.dag-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.dag-filter-info { + padding: 0.5rem 0.75rem; + font-size: 0.75rem; + color: var(--text-secondary); + background: var(--bg-tertiary); + border-bottom: 1px solid var(--bg-primary); +} + +.dag-scroll-content { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.dag-loading { + display: flex; + align-items: center; + justify-content: center; + height: 200px; + color: var(--text-secondary); +} + +.dag-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + color: var(--error); + gap: 0.5rem; +} + +.dag-error-message { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.dag-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + color: var(--text-secondary); + text-align: center; +} + +.dag-empty-hint { + font-size: 0.75rem; + margin-top: 0.5rem; +} + +.dag-success-text { + color: var(--success); +} + +.dag-warning-text { + color: var(--warning); +} + +/* Tree View Styles */ +.dag-tree-container { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.dag-component { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.dag-cycle-warning { + padding: 0.5rem; + background: rgba(244, 67, 54, 0.1); + border: 1px solid rgba(244, 67, 54, 0.3); + border-radius: 4px; + color: var(--error); + font-size: 0.8125rem; +} + +.dag-critical-path { + padding: 0.5rem; + background: rgba(255, 193, 7, 0.1); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: 4px; + font-size: 0.8125rem; + color: var(--warning); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem; +} + +.dag-critical-node { + font-weight: 600; + color: var(--warning); +} + +.dag-arrow { + color: var(--text-secondary); +} + +.dag-tree { + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; + font-size: 0.8125rem; +} + +.dag-tree-node-wrapper { + display: flex; + flex-direction: column; +} + +.dag-tree-node { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.375rem; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; +} + +.dag-tree-node:hover { + background: var(--bg-tertiary); +} + +.dag-tree-node.selected { + background: var(--bg-tertiary); + border: 1px solid var(--accent); +} + +.dag-tree-node.cycle { + color: var(--error); + font-style: italic; +} + +.dag-tree-connector { + color: var(--text-secondary); + min-width: 16px; +} + +.dag-status-icon { + font-size: 0.875rem; + min-width: 16px; + text-align: center; +} + +.dag-status-icon.status-open { color: var(--text-secondary); } +.dag-status-icon.status-progress { color: #00bcd4; } +.dag-status-icon.status-blocked { color: var(--error); } +.dag-status-icon.status-completed { color: var(--success); } +.dag-status-icon.status-closed { color: var(--success); } +.dag-status-icon.status-deferred { color: var(--warning); } + +.dag-node-id { + font-weight: 500; +} + +.dag-priority { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.dag-priority.priority-critical { color: var(--error); font-weight: 600; } +.dag-priority.priority-high { color: var(--warning); } +.dag-priority.priority-normal { color: var(--text-primary); } +.dag-priority.priority-low { color: var(--text-secondary); } +.dag-priority.priority-backlog { color: var(--text-secondary); } + +.dag-critical-icon { + color: var(--warning); + font-size: 0.875rem; +} + +/* List View Styles */ +.dag-list-container { + display: flex; + flex-direction: column; +} + +.dag-list-header { + font-size: 0.8125rem; + font-weight: 600; + padding: 0.5rem; + color: var(--text-primary); +} + +.dag-list { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.dag-list-item { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.375rem; + padding: 0.5rem; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.dag-list-item:hover { + background: var(--bg-tertiary); +} + +.dag-list-item.selected { + background: var(--bg-tertiary); + border: 1px solid var(--accent); +} + +.dag-blocked-count { + font-size: 0.75rem; + color: var(--error); + margin-left: auto; +} + +.dag-item-title { + width: 100%; + font-size: 0.75rem; + color: var(--text-secondary); + padding-left: 1.5rem; +} + +/* Stats View Styles */ +.dag-stats-container { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 0.5rem; +} + +.dag-stats-section { + background: var(--bg-tertiary); + border-radius: 4px; + padding: 0.75rem; +} + +.dag-stats-section h3 { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin: 0 0 0.5rem 0; +} + +.dag-stats-row { + display: flex; + justify-content: space-between; + padding: 0.25rem 0; + font-size: 0.8125rem; +} + +.dag-stats-label { + color: var(--text-secondary); +} + +.dag-stats-label.status-ready { color: var(--success); } +.dag-stats-label.status-blocked { color: var(--error); } + +.dag-stats-value { + font-weight: 500; + color: var(--text-primary); +} + +.dag-critical-path-preview { + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--bg-primary); +} + +.dag-path-nodes { + display: flex; + flex-direction: column; + gap: 0.125rem; + margin-top: 0.25rem; +} + +.dag-path-node { + font-size: 0.75rem; + color: #9c27b0; + font-family: 'SF Mono', Monaco, monospace; +} + +.dag-path-more { + font-size: 0.7rem; + color: var(--text-secondary); + font-style: italic; +} + +.dag-cycle-warning-section { + padding: 0.75rem; + background: rgba(244, 67, 54, 0.1); + border: 1px solid rgba(244, 67, 54, 0.3); + border-radius: 4px; + color: var(--error); + font-size: 0.8125rem; +} + +.dag-cycle-warning-section p { + font-size: 0.75rem; + margin-top: 0.25rem; + color: var(--text-secondary); +} + +/* Detail Panel Styles */ +.dag-detail-panel { + border-top: 1px solid var(--bg-tertiary); + background: var(--bg-primary); +} + +.dag-detail-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--bg-tertiary); +} + +.dag-detail-header h3 { + font-size: 0.875rem; + font-weight: 600; + margin: 0; + color: var(--accent); +} + +.dag-detail-close { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + font-size: 0.875rem; + transition: all 0.2s; +} + +.dag-detail-close:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.dag-detail-content { + padding: 0.5rem 0.75rem; +} + +.dag-detail-row { + display: flex; + justify-content: space-between; + padding: 0.25rem 0; + font-size: 0.8125rem; + border-bottom: 1px solid var(--bg-tertiary); +} + +.dag-detail-row:last-of-type { + border-bottom: none; +} + +.dag-detail-label { + color: var(--text-secondary); +} + +.dag-detail-value { + font-weight: 500; + color: var(--text-primary); +} + +.dag-detail-critical { + margin-top: 0.5rem; + padding: 0.5rem; + background: rgba(255, 193, 7, 0.1); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: 4px; + color: var(--warning); + font-size: 0.8125rem; + text-align: center; +} + +/* Responsive adjustments for DAG */ +@media (max-width: 768px) { + .dag-panel { + min-width: 100%; + max-width: 100%; + } + + .dag-view-modes { + flex-wrap: wrap; + } + + .dag-mode-btn { + flex: 1 1 45%; + } +} diff --git a/src/web/frontend/src/types.ts b/src/web/frontend/src/types.ts index 9bf1b48..7aa913e 100644 --- a/src/web/frontend/src/types.ts +++ b/src/web/frontend/src/types.ts @@ -174,3 +174,66 @@ export interface FileHeatmapStats { } export type HeatmapSortMode = 'modifications' | 'recent' | 'workers' | 'collisions'; + +// Dependency DAG Types +export type BeadStatus = 'open' | 'in_progress' | 'blocked' | 'completed' | 'closed' | 'deferred'; + +export interface BeadNode { + id: string; + title: string; + status: BeadStatus; + priority: number; + depth: number; + dependentCount: number; + dependencyCount: number; + isCriticalPath: boolean; + estimatedEffort?: number; +} + +export interface DependencyEdge { + from: string; + to: string; + isCritical: boolean; +} + +export interface DagComponent { + nodes: BeadNode[]; + edges: DependencyEdge[]; + roots: string[]; + hasCycle: boolean; + criticalPath: string[]; + maxDepth: number; +} + +export interface DependencyGraph { + components: DagComponent[]; + totalNodes: number; + totalEdges: number; + totalComponents: number; + globalCriticalPath: string[]; + generatedAt: number; +} + +export interface DagStats { + totalBeads: number; + blockedCount: number; + readyCount: number; + avgDependencies: number; + avgDependents: number; + maxDepth: number; + cycleCount: number; + criticalPathLength: number; + criticalPathBeads: number; +} + +export interface DagOptions { + status?: BeadStatus | 'all'; + minPriority?: number; + maxPriority?: number; + criticalOnly?: boolean; + maxDepth?: number; + sortBy?: 'priority' | 'depth' | 'dependents'; + includeClosed?: boolean; +} + +export type DagViewMode = 'tree' | 'blockers' | 'ready' | 'stats'; diff --git a/src/web/frontend/test/DependencyDag.test.tsx b/src/web/frontend/test/DependencyDag.test.tsx new file mode 100644 index 0000000..2a83b26 --- /dev/null +++ b/src/web/frontend/test/DependencyDag.test.tsx @@ -0,0 +1,257 @@ +/** + * Tests for DependencyDag component + * @vitest-environment jsdom + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import DependencyDag from '../src/components/DependencyDag'; + +// Mock fetch globally +const mockFetch = vi.fn(); +global.fetch = mockFetch; + +// Mock graph data +const mockGraph = { + components: [ + { + nodes: [ + { id: 'bd-abc', title: 'Test bead', status: 'open', priority: 1, depth: 0, dependentCount: 2, dependencyCount: 0, isCriticalPath: true }, + { id: 'bd-def', title: 'Dependent bead', status: 'blocked', priority: 2, depth: 1, dependentCount: 0, dependencyCount: 1, isCriticalPath: false }, + ], + edges: [{ from: 'bd-def', to: 'bd-abc', isCritical: true }], + roots: ['bd-abc'], + hasCycle: false, + criticalPath: ['bd-abc'], + maxDepth: 1, + }, + ], + totalNodes: 2, + totalEdges: 1, + totalComponents: 1, + globalCriticalPath: ['bd-abc'], + generatedAt: Date.now(), +}; + +const mockStats = { + totalBeads: 2, + blockedCount: 1, + readyCount: 1, + avgDependencies: 0.5, + avgDependents: 1, + maxDepth: 1, + cycleCount: 0, + criticalPathLength: 1, + criticalPathBeads: 1, +}; + +// Helper to set up successful mock +const setupSuccessMock = (times: number = 1) => { + for (let i = 0; i < times; i++) { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ graph: mockGraph, stats: mockStats }), + }); + } +}; + +// Helper to set up error mock +const setupErrorMock = () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + statusText: 'Internal Server Error', + }); +}; + +describe('DependencyDag Component', () => { + beforeEach(() => { + mockFetch.mockReset(); + }); + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + describe('Visibility', () => { + it('should not render when visible is false', () => { + render( {}} />); + expect(screen.queryByText('Task Dependency DAG')).not.toBeInTheDocument(); + }); + + it('should render when visible is true', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByText('Task Dependency DAG')).toBeInTheDocument(); + }); + }); + }); + + describe('View Modes', () => { + it('should show tree view by default', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByText(/Tree View/)).toBeInTheDocument(); + }); + }); + + it('should switch to blockers view when blockers button clicked', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /blockers/i })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /blockers/i })); + + await waitFor(() => { + expect(screen.getByText(/Blockers View/)).toBeInTheDocument(); + }); + }); + + it('should switch to ready view when ready button clicked', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /ready/i })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /ready/i })); + + await waitFor(() => { + expect(screen.getByText(/Ready View/)).toBeInTheDocument(); + }); + }); + + it('should switch to stats view when stats button clicked', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /stats/i })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /stats/i })); + + await waitFor(() => { + expect(screen.getByText(/Stats View/)).toBeInTheDocument(); + }); + }); + }); + + describe('Stats View', () => { + it('should display stats correctly', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /stats/i })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /stats/i })); + + await waitFor(() => { + expect(screen.getByText('Overview')).toBeInTheDocument(); + }); + }); + }); + + describe('Close Button', () => { + it('should call onClose when close button clicked', async () => { + setupSuccessMock(); + const onClose = vi.fn(); + render(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /✕/i })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /✕/i })); + expect(onClose).toHaveBeenCalled(); + }); + }); + + describe('Refresh', () => { + it('should call API on mount when visible', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith('/api/dag?'); + }); + }); + + it('should refresh when refresh button clicked', async () => { + setupSuccessMock(2); // Need 2 calls - initial + refresh + render( {}} />); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /refresh/i })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /refresh/i })); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('Error Handling', () => { + it('should show error message when API fails', async () => { + setupErrorMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByText(/Error loading dependency graph/i)).toBeInTheDocument(); + }); + }); + }); + + describe('Tree View', () => { + it('should display critical path', async () => { + setupSuccessMock(); + render( {}} />); + + await waitFor(() => { + expect(screen.getByText(/Critical path:/i)).toBeInTheDocument(); + }); + }); + + it('should display bead nodes', async () => { + setupSuccessMock(); + render( {}} />); + + // Look for any element containing the bead ID text + await waitFor(() => { + const beadElements = screen.getAllByText((content, element) => { + return element?.classList?.contains('dag-node-id') && content.includes('bd-abc'); + }); + expect(beadElements.length).toBeGreaterThan(0); + }); + }); + }); + + describe('Filter Options', () => { + it('should cycle filters when filter button clicked', async () => { + setupSuccessMock(2); // Need 2 calls - initial + filter refresh + render( {}} />); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /filter/i })); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + }); + }); +}); diff --git a/src/web/server.ts b/src/web/server.ts index c96dd1e..06f618f 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -10,9 +10,10 @@ import { EventEmitter } from 'events'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { WebSocketServer, WebSocket } from 'ws'; -import { LogEvent, EventFilter, CrossReferenceEntityType, CrossReferenceRelationship } from '../types.js'; +import { LogEvent, EventFilter, CrossReferenceEntityType, CrossReferenceRelationship, DagOptions, BeadStatus } from '../types.js'; import { InMemoryEventStore } from '../store.js'; import { CrossReferenceManager, getCrossReferenceManager } from '../crossReferenceManager.js'; +import { refreshDependencyGraph, getDagStats } from '../tui/dagUtils.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -125,6 +126,72 @@ export function createWebServer(options: WebServerOptions): WebServer { res.json(collisions); }); + // ============================================ + // File Heatmap API Endpoints + // ============================================ + + // Get file heatmap entries + app.get('/api/heatmap', (req: Request, res: Response) => { + const sortBy = req.query.sortBy as 'modifications' | 'recent' | 'workers' | 'collisions' || undefined; + const maxEntries = req.query.maxEntries ? parseInt(req.query.maxEntries as string) : 100; + const collisionsOnly = req.query.collisionsOnly === 'true'; + const directoryFilter = req.query.directoryFilter as string | undefined; + + const entries = store.getFileHeatmap({ + sortBy, + maxEntries, + collisionsOnly, + directoryFilter, + }); + + res.json(entries); + }); + + // Get file heatmap statistics + app.get('/api/heatmap/stats', (_req: Request, res: Response) => { + const stats = store.getFileHeatmapStats(); + res.json(stats); + }); + + // ============================================ + // Dependency DAG API Endpoints + // ============================================ + + // Get dependency graph + app.get('/api/dag', (req: Request, res: Response) => { + try { + const status = req.query.status as BeadStatus | 'all' | undefined; + const criticalOnly = req.query.criticalOnly === 'true'; + const maxDepth = req.query.maxDepth ? parseInt(req.query.maxDepth as string) : undefined; + const includeClosed = req.query.includeClosed === 'true'; + + const options: DagOptions = {}; + if (status && status !== 'all') { + options.status = status as BeadStatus; + } + if (criticalOnly) { + options.criticalOnly = true; + } + if (maxDepth !== undefined) { + options.maxDepth = maxDepth; + } + if (includeClosed) { + options.includeClosed = true; + } + + const graph = refreshDependencyGraph(options); + const stats = getDagStats(graph); + + res.json({ graph, stats }); + } catch (error) { + console.error('Error generating dependency graph:', error); + res.status(500).json({ + error: 'Failed to generate dependency graph', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } + }); + // ============================================ // Cross-Reference API Endpoints // ============================================