Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.95% covered (success)
95.95%
71 / 74
92.31% covered (success)
92.31%
12 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
DashboardController
95.95% covered (success)
95.95%
71 / 74
92.31% covered (success)
92.31%
12 / 13
26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 index
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 data
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 session
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 mergeSessions
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 sessionActivity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ungroupSession
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 groupSessions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 destroySession
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 errors
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 updateProject
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 resetAll
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDashboardData
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\TelemetrySession;
6use App\Services\DashboardQueryService;
7use App\Services\TelemetryService;
8use Illuminate\Http\JsonResponse;
9use Illuminate\Http\RedirectResponse;
10use Illuminate\Http\Request;
11use Illuminate\View\View;
12
13class DashboardController extends Controller
14{
15    public function __construct(
16        private readonly DashboardQueryService $query,
17        private readonly TelemetryService $telemetry,
18    ) {}
19
20    public function index(): View
21    {
22        return view('dashboard.index', $this->getDashboardData());
23    }
24
25    public function data(): JsonResponse
26    {
27        return response()->json($this->getDashboardData());
28    }
29
30    public function session(string $session): View
31    {
32        $data = $this->query->getSessionDetail($session);
33        $data['otherSessions'] = TelemetrySession::where('session_id', '!=', $session)
34            ->orderByDesc('last_seen_at')
35            ->get();
36
37        $currentSession = $data['session'];
38        $data['groupedSessions'] = $currentSession->session_group_id
39            ? TelemetrySession::where('session_group_id', $currentSession->session_group_id)
40                ->where('session_id', '!=', $session)
41                ->orderBy('first_seen_at')
42                ->get()
43            : collect();
44
45        return view('dashboard.session', $data);
46    }
47
48    public function mergeSessions(Request $request, string $session): RedirectResponse
49    {
50        $request->validate(['merge_into' => 'required|string']);
51        $targetId = $request->input('merge_into');
52
53        if ($targetId === $session) {
54            return redirect()->route('dashboard.session', $session)->with('error', 'Cannot merge a session into itself.');
55        }
56
57        try {
58            $this->telemetry->mergeSessions($session, $targetId);
59        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException) {
60            return redirect()->route('dashboard.session', $session)->with('error', 'Session not found.');
61        }
62
63        return redirect()->route('dashboard.session', $targetId)
64            ->with('success', "Session {$session} merged into this session.");
65    }
66
67    public function sessionActivity(string $session): JsonResponse
68    {
69        return response()->json($this->query->getSessionActivity($session));
70    }
71
72    public function ungroupSession(string $session): RedirectResponse
73    {
74        try {
75            $this->telemetry->ungroupSession($session);
76        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException) {
77            return redirect()->route('dashboard.session', $session)->with('error', 'Session not found.');
78        }
79
80        return redirect()->route('dashboard.session', $session)
81            ->with('success', 'Session removed from group.');
82    }
83
84    public function groupSessions(Request $request, string $session): RedirectResponse
85    {
86        $request->validate(['group_with' => 'required|string']);
87        $groupWith = $request->input('group_with');
88
89        if ($groupWith === $session) {
90            return redirect()->back()->with('error', 'Cannot group a session with itself.');
91        }
92
93        try {
94            $this->telemetry->groupSessions($session, $groupWith);
95        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException) {
96            return redirect()->back()->with('error', 'Session not found.');
97        }
98
99        return redirect()->back()->with('success', 'Sessions grouped together.');
100    }
101
102    public function destroySession(string $session): RedirectResponse
103    {
104        $this->telemetry->deleteSession($session);
105
106        return redirect()->route('dashboard')->with('success', "Session {$session} deleted.");
107    }
108
109    public function errors(): View
110    {
111        return view('dashboard.errors', [
112            'errors' => $this->query->getApiErrors(),
113        ]);
114    }
115
116    public function updateProject(Request $request, string $session): JsonResponse
117    {
118        $validated = $request->validate([
119            'project_name' => 'required|string|max:255',
120            'hostname' => 'nullable|string|max:255',
121        ]);
122
123        $telemetrySession = TelemetrySession::where('session_id', $session)->first();
124
125        if ($telemetrySession) {
126            $update = [];
127            if (! $telemetrySession->project_name || $telemetrySession->project_name === 'background') {
128                $update['project_name'] = $validated['project_name'];
129            }
130            if (! empty($validated['hostname']) && ! $telemetrySession->hostname) {
131                $update['hostname'] = $validated['hostname'];
132            }
133            if ($update) {
134                $telemetrySession->update($update);
135            }
136        } else {
137            $pending = ['project_name' => $validated['project_name']];
138            if (! empty($validated['hostname'])) {
139                $pending['hostname'] = $validated['hostname'];
140            }
141            cache()->put("pending_session_meta:{$session}", $pending, 300);
142        }
143
144        return response()->json(['ok' => true]);
145    }
146
147    public function resetAll(): RedirectResponse
148    {
149        $this->telemetry->resetAll();
150
151        return redirect()->route('dashboard')->with('success', 'All telemetry data has been reset.');
152    }
153
154    private function getDashboardData(): array
155    {
156        return [
157            'summary' => $this->query->getSummary(),
158            'sessions' => $this->query->getSessions(),
159            'tokenBreakdown' => $this->query->getTokenBreakdown(),
160            'locBreakdown' => $this->query->getLinesOfCodeBreakdown(),
161            'costByModel' => $this->query->getCostByModel(),
162            'toolUsage' => $this->query->getToolUsage(),
163            'apiPerformance' => $this->query->getApiPerformance(),
164            'recentEvents' => $this->query->getRecentEvents(),
165            'billingModel' => config('claude-board.billing_model', 'subscription'),
166        ];
167    }
168}