diff --git a/cmd/core-app/laravel/app/Livewire/Dashboard/ActivityFeed.php b/cmd/core-app/laravel/app/Livewire/Dashboard/ActivityFeed.php new file mode 100644 index 00000000..7af15a05 --- /dev/null +++ b/cmd/core-app/laravel/app/Livewire/Dashboard/ActivityFeed.php @@ -0,0 +1,111 @@ +loadEntries(); + } + + public function loadEntries(): void + { + // Placeholder data — will be replaced with real-time WebSocket feed + $this->entries = [ + [ + 'id' => 'act-001', + 'agent' => 'Athena', + 'type' => 'code_write', + 'message' => 'Created AgentFleet Livewire component', + 'job' => '#96', + 'timestamp' => now()->subMinutes(2)->toIso8601String(), + 'is_question' => false, + ], + [ + 'id' => 'act-002', + 'agent' => 'Athena', + 'type' => 'tool_call', + 'message' => 'Read file: cmd/core-app/laravel/composer.json', + 'job' => '#96', + 'timestamp' => now()->subMinutes(5)->toIso8601String(), + 'is_question' => false, + ], + [ + 'id' => 'act-003', + 'agent' => 'Clotho', + 'type' => 'question', + 'message' => 'Should I apply the fix to both the TCP and Unix socket transports, or just TCP?', + 'job' => '#84', + 'timestamp' => now()->subMinutes(8)->toIso8601String(), + 'is_question' => true, + ], + [ + 'id' => 'act-004', + 'agent' => 'Virgil', + 'type' => 'pr_created', + 'message' => 'Opened PR #89: fix WebSocket reconnection logic', + 'job' => '#89', + 'timestamp' => now()->subMinutes(15)->toIso8601String(), + 'is_question' => false, + ], + [ + 'id' => 'act-005', + 'agent' => 'Virgil', + 'type' => 'test_run', + 'message' => 'All 47 tests passed (0.8s)', + 'job' => '#89', + 'timestamp' => now()->subMinutes(18)->toIso8601String(), + 'is_question' => false, + ], + [ + 'id' => 'act-006', + 'agent' => 'Athena', + 'type' => 'git_push', + 'message' => 'Pushed branch feat/agentic-dashboard', + 'job' => '#96', + 'timestamp' => now()->subMinutes(22)->toIso8601String(), + 'is_question' => false, + ], + [ + 'id' => 'act-007', + 'agent' => 'Clotho', + 'type' => 'code_write', + 'message' => 'Added input validation for MCP file_write paths', + 'job' => '#84', + 'timestamp' => now()->subMinutes(30)->toIso8601String(), + 'is_question' => false, + ], + ]; + } + + public function getFilteredEntriesProperty(): array + { + return array_filter($this->entries, function ($entry) { + if ($this->showOnlyQuestions && !$entry['is_question']) { + return false; + } + if ($this->agentFilter !== 'all' && $entry['agent'] !== $this->agentFilter) { + return false; + } + if ($this->typeFilter !== 'all' && $entry['type'] !== $this->typeFilter) { + return false; + } + return true; + }); + } + + public function render() + { + return view('livewire.dashboard.activity-feed'); + } +} diff --git a/cmd/core-app/laravel/app/Livewire/Dashboard/AgentFleet.php b/cmd/core-app/laravel/app/Livewire/Dashboard/AgentFleet.php new file mode 100644 index 00000000..aec6574f --- /dev/null +++ b/cmd/core-app/laravel/app/Livewire/Dashboard/AgentFleet.php @@ -0,0 +1,85 @@ + */ + public array $agents = []; + + public ?string $selectedAgent = null; + + public function mount(): void + { + $this->loadAgents(); + } + + public function loadAgents(): void + { + // Placeholder data — will be replaced with real API calls to Go backend + $this->agents = [ + [ + 'id' => 'athena', + 'name' => 'Athena', + 'host' => 'studio.snider.dev', + 'model' => 'claude-opus-4-6', + 'status' => 'working', + 'job' => '#96 agentic dashboard', + 'heartbeat' => 'green', + 'uptime' => '4h 23m', + 'tokens_today' => 142_580, + 'jobs_completed' => 3, + ], + [ + 'id' => 'virgil', + 'name' => 'Virgil', + 'host' => 'studio.snider.dev', + 'model' => 'claude-opus-4-6', + 'status' => 'idle', + 'job' => '', + 'heartbeat' => 'green', + 'uptime' => '12h 07m', + 'tokens_today' => 89_230, + 'jobs_completed' => 5, + ], + [ + 'id' => 'clotho', + 'name' => 'Clotho', + 'host' => 'darwin-au', + 'model' => 'claude-sonnet-4-5', + 'status' => 'working', + 'job' => '#84 security audit', + 'heartbeat' => 'yellow', + 'uptime' => '1h 45m', + 'tokens_today' => 34_100, + 'jobs_completed' => 1, + ], + [ + 'id' => 'charon', + 'name' => 'Charon', + 'host' => 'linux.snider.dev', + 'model' => 'claude-haiku-4-5', + 'status' => 'unhealthy', + 'job' => '', + 'heartbeat' => 'red', + 'uptime' => '0m', + 'tokens_today' => 0, + 'jobs_completed' => 0, + ], + ]; + } + + public function selectAgent(string $agentId): void + { + $this->selectedAgent = $this->selectedAgent === $agentId ? null : $agentId; + } + + public function render() + { + return view('livewire.dashboard.agent-fleet'); + } +} diff --git a/cmd/core-app/laravel/app/Livewire/Dashboard/HumanActions.php b/cmd/core-app/laravel/app/Livewire/Dashboard/HumanActions.php new file mode 100644 index 00000000..4d87ee25 --- /dev/null +++ b/cmd/core-app/laravel/app/Livewire/Dashboard/HumanActions.php @@ -0,0 +1,93 @@ +loadPending(); + } + + public function loadPending(): void + { + // Placeholder data — will be replaced with real data from Go backend + $this->pendingQuestions = [ + [ + 'id' => 'q-001', + 'agent' => 'Clotho', + 'job' => '#84', + 'question' => 'Should I apply the fix to both the TCP and Unix socket transports, or just TCP?', + 'asked_at' => now()->subMinutes(8)->toIso8601String(), + 'context' => 'Working on security audit — found unvalidated input in transport layer.', + ], + ]; + + $this->reviewGates = [ + [ + 'id' => 'rg-001', + 'agent' => 'Virgil', + 'job' => '#89', + 'type' => 'pr_review', + 'title' => 'PR #89: fix WebSocket reconnection logic', + 'description' => 'Adds exponential backoff and connection state tracking.', + 'submitted_at' => now()->subMinutes(15)->toIso8601String(), + ], + ]; + } + + public function startAnswer(string $questionId): void + { + $this->answeringId = $questionId; + $this->answerText = ''; + } + + public function submitAnswer(): void + { + if (! $this->answeringId || trim($this->answerText) === '') { + return; + } + + // Remove answered question from list + $this->pendingQuestions = array_values( + array_filter($this->pendingQuestions, fn ($q) => $q['id'] !== $this->answeringId) + ); + + $this->answeringId = null; + $this->answerText = ''; + } + + public function cancelAnswer(): void + { + $this->answeringId = null; + $this->answerText = ''; + } + + public function approveGate(string $gateId): void + { + $this->reviewGates = array_values( + array_filter($this->reviewGates, fn ($g) => $g['id'] !== $gateId) + ); + } + + public function rejectGate(string $gateId): void + { + $this->reviewGates = array_values( + array_filter($this->reviewGates, fn ($g) => $g['id'] !== $gateId) + ); + } + + public function render() + { + return view('livewire.dashboard.human-actions'); + } +} diff --git a/cmd/core-app/laravel/app/Livewire/Dashboard/JobQueue.php b/cmd/core-app/laravel/app/Livewire/Dashboard/JobQueue.php new file mode 100644 index 00000000..75a24190 --- /dev/null +++ b/cmd/core-app/laravel/app/Livewire/Dashboard/JobQueue.php @@ -0,0 +1,125 @@ +loadJobs(); + } + + public function loadJobs(): void + { + // Placeholder data — will be replaced with real API calls to Go backend + $this->jobs = [ + [ + 'id' => 'job-001', + 'issue' => '#96', + 'repo' => 'host-uk/core', + 'title' => 'feat(agentic): real-time dashboard', + 'agent' => 'Athena', + 'status' => 'in_progress', + 'priority' => 1, + 'queued_at' => now()->subMinutes(45)->toIso8601String(), + 'started_at' => now()->subMinutes(30)->toIso8601String(), + ], + [ + 'id' => 'job-002', + 'issue' => '#84', + 'repo' => 'host-uk/core', + 'title' => 'fix: security audit findings', + 'agent' => 'Clotho', + 'status' => 'in_progress', + 'priority' => 2, + 'queued_at' => now()->subHours(2)->toIso8601String(), + 'started_at' => now()->subHours(1)->toIso8601String(), + ], + [ + 'id' => 'job-003', + 'issue' => '#102', + 'repo' => 'host-uk/core', + 'title' => 'feat: add rate limiting to MCP', + 'agent' => null, + 'status' => 'queued', + 'priority' => 3, + 'queued_at' => now()->subMinutes(10)->toIso8601String(), + 'started_at' => null, + ], + [ + 'id' => 'job-004', + 'issue' => '#89', + 'repo' => 'host-uk/core', + 'title' => 'fix: WebSocket reconnection', + 'agent' => 'Virgil', + 'status' => 'review', + 'priority' => 2, + 'queued_at' => now()->subHours(4)->toIso8601String(), + 'started_at' => now()->subHours(3)->toIso8601String(), + ], + [ + 'id' => 'job-005', + 'issue' => '#78', + 'repo' => 'host-uk/core', + 'title' => 'docs: update CLAUDE.md', + 'agent' => 'Virgil', + 'status' => 'completed', + 'priority' => 4, + 'queued_at' => now()->subHours(6)->toIso8601String(), + 'started_at' => now()->subHours(5)->toIso8601String(), + ], + ]; + } + + public function updatedStatusFilter(): void + { + // Livewire auto-updates the view + } + + public function cancelJob(string $jobId): void + { + $this->jobs = array_map(function ($job) use ($jobId) { + if ($job['id'] === $jobId && in_array($job['status'], ['queued', 'in_progress'])) { + $job['status'] = 'cancelled'; + } + return $job; + }, $this->jobs); + } + + public function retryJob(string $jobId): void + { + $this->jobs = array_map(function ($job) use ($jobId) { + if ($job['id'] === $jobId && in_array($job['status'], ['failed', 'cancelled'])) { + $job['status'] = 'queued'; + $job['agent'] = null; + } + return $job; + }, $this->jobs); + } + + public function getFilteredJobsProperty(): array + { + return array_filter($this->jobs, function ($job) { + if ($this->statusFilter !== 'all' && $job['status'] !== $this->statusFilter) { + return false; + } + if ($this->agentFilter !== 'all' && ($job['agent'] ?? '') !== $this->agentFilter) { + return false; + } + return true; + }); + } + + public function render() + { + return view('livewire.dashboard.job-queue'); + } +} diff --git a/cmd/core-app/laravel/app/Livewire/Dashboard/Metrics.php b/cmd/core-app/laravel/app/Livewire/Dashboard/Metrics.php new file mode 100644 index 00000000..301155cc --- /dev/null +++ b/cmd/core-app/laravel/app/Livewire/Dashboard/Metrics.php @@ -0,0 +1,60 @@ +loadMetrics(); + } + + public function loadMetrics(): void + { + // Placeholder data — will be replaced with real metrics from Go backend + $this->stats = [ + 'jobs_completed' => 12, + 'prs_merged' => 8, + 'tokens_used' => 1_245_800, + 'cost_today' => 18.42, + 'active_agents' => 3, + 'queue_depth' => 4, + ]; + + $this->budgetUsed = 18.42; + $this->budgetLimit = 50.00; + + // Hourly throughput for chart + $this->throughputData = [ + ['hour' => '00:00', 'jobs' => 0, 'tokens' => 0], + ['hour' => '02:00', 'jobs' => 0, 'tokens' => 0], + ['hour' => '04:00', 'jobs' => 1, 'tokens' => 45_000], + ['hour' => '06:00', 'jobs' => 2, 'tokens' => 120_000], + ['hour' => '08:00', 'jobs' => 3, 'tokens' => 195_000], + ['hour' => '10:00', 'jobs' => 2, 'tokens' => 280_000], + ['hour' => '12:00', 'jobs' => 1, 'tokens' => 340_000], + ['hour' => '14:00', 'jobs' => 3, 'tokens' => 450_000], + ]; + + $this->costBreakdown = [ + ['model' => 'claude-opus-4-6', 'cost' => 12.80, 'tokens' => 856_000], + ['model' => 'claude-sonnet-4-5', 'cost' => 4.20, 'tokens' => 312_000], + ['model' => 'claude-haiku-4-5', 'cost' => 1.42, 'tokens' => 77_800], + ]; + } + + public function render() + { + return view('livewire.dashboard.metrics'); + } +} diff --git a/cmd/core-app/laravel/resources/views/components/dashboard-layout.blade.php b/cmd/core-app/laravel/resources/views/components/dashboard-layout.blade.php new file mode 100644 index 00000000..5bc44d4c --- /dev/null +++ b/cmd/core-app/laravel/resources/views/components/dashboard-layout.blade.php @@ -0,0 +1,105 @@ + + +
+ + +{{ $q['question'] }}
+ @if (!empty($q['context'])) +{{ $q['context'] }}
+ @endif + + @if ($answeringId === $q['id']) +{{ $gate['title'] }}
+{{ $gate['description'] }}
+No pending actions. All agents are autonomous.
+| Job | +Issue | +Agent | +Status | +Priority | +Queued | +Actions | +
|---|---|---|---|---|---|---|
|
+ {{ $job['id'] }}
+ {{ $job['title'] }}
+ |
+
+ {{ $job['issue'] }}
+ {{ $job['repo'] }}
+ |
+ + {{ $job['agent'] ?? '—' }} + | ++ @php + $statusColors = [ + 'queued' => 'bg-yellow-500/20 text-yellow-400', + 'in_progress' => 'bg-blue-500/20 text-blue-400', + 'review' => 'bg-purple-500/20 text-purple-400', + 'completed' => 'bg-green-500/20 text-green-400', + 'failed' => 'bg-red-500/20 text-red-400', + 'cancelled' => 'bg-gray-500/20 text-gray-400', + ]; + @endphp + + {{ str_replace('_', ' ', $job['status']) }} + + | ++ P{{ $job['priority'] }} + | ++ {{ \Carbon\Carbon::parse($job['queued_at'])->diffForHumans(short: true) }} + | +
+
+ @if (in_array($job['status'], ['queued', 'in_progress']))
+
+ @endif
+ @if (in_array($job['status'], ['failed', 'cancelled']))
+
+ @endif
+
+ |
+
| No jobs match the selected filters. | +||||||