claude-code-templates 1.10.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1531 @@
1
+ /**
2
+ * DashboardPage - Analytics overview page without conversations
3
+ * Focuses on metrics, charts, and system performance data
4
+ */
5
+ class DashboardPage {
6
+ constructor(container, services) {
7
+ this.container = container;
8
+ this.dataService = services.data;
9
+ this.stateService = services.state;
10
+ this.chartService = services.chart;
11
+
12
+ this.components = {};
13
+ this.refreshInterval = null;
14
+ this.isInitialized = false;
15
+
16
+ // Subscribe to state changes
17
+ this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
18
+ }
19
+
20
+ /**
21
+ * Initialize the dashboard page
22
+ */
23
+ async initialize() {
24
+ if (this.isInitialized) return;
25
+
26
+ console.log('📊 Initializing DashboardPage...');
27
+
28
+ try {
29
+ console.log('📊 Step 1: Rendering dashboard...');
30
+ await this.render();
31
+ console.log('✅ Dashboard rendered');
32
+
33
+ // Now that DOM is ready, we can show loading
34
+ this.stateService.setLoading(true);
35
+
36
+ console.log('📊 Step 2: Loading initial data...');
37
+ await this.loadInitialData();
38
+ console.log('✅ Initial data loaded');
39
+
40
+ console.log('📊 Step 3: Initializing components with data...');
41
+ await this.initializeComponents();
42
+ console.log('✅ Components initialized');
43
+
44
+ console.log('📊 Step 4: Starting periodic refresh...');
45
+ this.startPeriodicRefresh();
46
+ console.log('✅ Periodic refresh started');
47
+
48
+ this.isInitialized = true;
49
+ console.log('🎉 DashboardPage fully initialized!');
50
+ } catch (error) {
51
+ console.error('❌ Error during dashboard initialization:', error);
52
+ // Even if there's an error, show the dashboard with fallback data
53
+ this.showFallbackDashboard();
54
+ } finally {
55
+ console.log('📊 Clearing loading state...');
56
+ this.stateService.setLoading(false);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Show fallback dashboard when initialization fails
62
+ */
63
+ showFallbackDashboard() {
64
+ console.log('🆘 Showing fallback dashboard...');
65
+ try {
66
+ const demoData = {
67
+ summary: {
68
+ totalConversations: 0,
69
+ claudeSessions: 0,
70
+ claudeSessionsDetail: 'no sessions',
71
+ totalTokens: 0,
72
+ activeProjects: 0,
73
+ dataSize: '0 MB'
74
+ },
75
+ detailedTokenUsage: {
76
+ inputTokens: 0,
77
+ outputTokens: 0,
78
+ cacheCreationTokens: 0,
79
+ cacheReadTokens: 0
80
+ },
81
+ conversations: []
82
+ };
83
+
84
+ this.updateSummaryDisplay(demoData.summary, demoData.detailedTokenUsage, demoData);
85
+ this.updateLastUpdateTime();
86
+ this.stateService.setError('Dashboard loaded in offline mode');
87
+ this.isInitialized = true;
88
+ } catch (fallbackError) {
89
+ console.error('❌ Fallback dashboard also failed:', fallbackError);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Handle state changes from StateService
95
+ * @param {Object} state - New state
96
+ * @param {string} action - Action that caused the change
97
+ */
98
+ handleStateChange(state, action) {
99
+ switch (action) {
100
+ case 'update_conversations':
101
+ this.updateSummaryDisplay(state.summary);
102
+ break;
103
+ case 'update_conversation_states':
104
+ this.updateSystemStatus(state.conversationStates);
105
+ break;
106
+ case 'set_loading':
107
+ this.updateLoadingState(state.isLoading);
108
+ break;
109
+ case 'set_error':
110
+ this.updateErrorState(state.error);
111
+ break;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Render the dashboard page structure
117
+ */
118
+ async render() {
119
+ this.container.innerHTML = `
120
+ <div class="dashboard-page">
121
+ <!-- Page Header -->
122
+ <div class="page-header">
123
+ <div class="header-content">
124
+ <div class="header-left">
125
+ <div class="status-header">
126
+ <span class="session-timer-status-dot active" id="session-status-dot"></span>
127
+ <h1 class="page-title">
128
+ Claude Code Analytics Dashboard
129
+ <span class="version-badge">v1.10.1</span>
130
+ </h1>
131
+ </div>
132
+ <div class="page-subtitle">
133
+ Real-time monitoring and analytics for Claude Code sessions
134
+ </div>
135
+ <div class="last-update-header">
136
+ <span class="last-update-label">last update:</span>
137
+ <span id="last-update-header-text">Never</span>
138
+ </div>
139
+ </div>
140
+ <div class="header-right">
141
+ <div class="theme-switch-container" title="Toggle light/dark theme">
142
+ <div class="theme-switch" id="header-theme-switch">
143
+ <div class="theme-switch-track">
144
+ <div class="theme-switch-thumb" id="header-theme-switch-thumb">
145
+ <span class="theme-switch-icon">🌙</span>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ <a href="https://github.com/anthropics/claude-code-templates" target="_blank" class="github-link" title="Star on GitHub">
151
+ <span class="github-icon">⭐</span>
152
+ Star on GitHub
153
+ </a>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Action Buttons -->
159
+ <div class="action-buttons-container">
160
+ <button class="action-btn-small" id="refresh-dashboard" title="Refresh data">
161
+ <span class="btn-icon-small">🔄</span>
162
+ Refresh
163
+ </button>
164
+ <button class="action-btn-small" id="export-data" title="Export analytics data">
165
+ <span class="btn-icon-small">📤</span>
166
+ Export
167
+ </button>
168
+ </div>
169
+
170
+ <!-- Loading State -->
171
+ <div class="loading-state" id="dashboard-loading" style="display: none;">
172
+ <div class="loading-spinner"></div>
173
+ <span class="loading-text">Loading dashboard...</span>
174
+ </div>
175
+
176
+ <!-- Error State -->
177
+ <div class="error-state" id="dashboard-error" style="display: none;">
178
+ <div class="error-content">
179
+ <span class="error-icon">⚠️</span>
180
+ <span class="error-message"></span>
181
+ <button class="error-retry" id="retry-load">Retry</button>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Main Dashboard Content -->
186
+ <div class="dashboard-content">
187
+ <!-- Key Metrics Cards -->
188
+ <div class="metrics-cards-container">
189
+ <!-- Conversations Card -->
190
+ <div class="metric-card">
191
+ <div class="metric-header">
192
+ <div class="metric-icon">💬</div>
193
+ <div class="metric-title">Conversations</div>
194
+ </div>
195
+ <div class="metric-primary">
196
+ <span class="metric-primary-value" id="totalConversations">0</span>
197
+ <span class="metric-primary-label">Total</span>
198
+ </div>
199
+ <div class="metric-secondary">
200
+ <div class="metric-secondary-item">
201
+ <span class="metric-secondary-label">This Month:</span>
202
+ <span class="metric-secondary-value" id="conversationsMonth">0</span>
203
+ </div>
204
+ <div class="metric-secondary-item">
205
+ <span class="metric-secondary-label">This Week:</span>
206
+ <span class="metric-secondary-value" id="conversationsWeek">0</span>
207
+ </div>
208
+ <div class="metric-secondary-item">
209
+ <span class="metric-secondary-label">Active:</span>
210
+ <span class="metric-secondary-value" id="activeConversations">0</span>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <!-- Sessions Card -->
216
+ <div class="metric-card">
217
+ <div class="metric-header">
218
+ <div class="metric-icon">⚡</div>
219
+ <div class="metric-title">Sessions</div>
220
+ </div>
221
+ <div class="metric-primary">
222
+ <span class="metric-primary-value" id="claudeSessions">0</span>
223
+ <span class="metric-primary-label">Total</span>
224
+ </div>
225
+ <div class="metric-secondary">
226
+ <div class="metric-secondary-item">
227
+ <span class="metric-secondary-label">This Month:</span>
228
+ <span class="metric-secondary-value" id="sessionsMonth">0</span>
229
+ </div>
230
+ <div class="metric-secondary-item">
231
+ <span class="metric-secondary-label">This Week:</span>
232
+ <span class="metric-secondary-value" id="sessionsWeek">0</span>
233
+ </div>
234
+ <div class="metric-secondary-item">
235
+ <span class="metric-secondary-label">Projects:</span>
236
+ <span class="metric-secondary-value" id="activeProjects">0</span>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- Tokens Card -->
242
+ <div class="metric-card">
243
+ <div class="metric-header">
244
+ <div class="metric-icon">🔢</div>
245
+ <div class="metric-title">Tokens</div>
246
+ </div>
247
+ <div class="metric-primary">
248
+ <span class="metric-primary-value" id="totalTokens">0</span>
249
+ <span class="metric-primary-label">Total</span>
250
+ </div>
251
+ <div class="metric-secondary">
252
+ <div class="metric-secondary-item">
253
+ <span class="metric-secondary-label">Input:</span>
254
+ <span class="metric-secondary-value" id="inputTokens">0</span>
255
+ </div>
256
+ <div class="metric-secondary-item">
257
+ <span class="metric-secondary-label">Output:</span>
258
+ <span class="metric-secondary-value" id="outputTokens">0</span>
259
+ </div>
260
+ <div class="metric-secondary-item">
261
+ <span class="metric-secondary-label">Cache:</span>
262
+ <span class="metric-secondary-value" id="cacheTokens">0</span>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <!-- Session Timer Section -->
269
+ <div class="session-timer-section">
270
+ <div class="section-title">
271
+ <h2>Current Session</h2>
272
+ </div>
273
+ <div id="session-timer-container">
274
+ <!-- SessionTimer component will be mounted here -->
275
+ </div>
276
+ </div>
277
+
278
+ <!-- Date Range Controls -->
279
+ <div class="chart-controls">
280
+ <div class="chart-controls-left">
281
+ <label class="filter-label">date range:</label>
282
+ <input type="date" id="dateFrom" class="date-input">
283
+ <span class="date-separator">to</span>
284
+ <input type="date" id="dateTo" class="date-input">
285
+ <button class="filter-btn" id="applyDateFilter">apply</button>
286
+ </div>
287
+ </div>
288
+
289
+ <!-- Charts Container (2x2 Grid) -->
290
+ <div class="charts-container">
291
+ <div class="chart-card">
292
+ <div class="chart-title">
293
+ 📊 token usage over time
294
+ </div>
295
+ <canvas id="tokenChart" class="chart-canvas"></canvas>
296
+ </div>
297
+
298
+ <div class="chart-card">
299
+ <div class="chart-title">
300
+ 🎯 project activity distribution
301
+ </div>
302
+ <canvas id="projectChart" class="chart-canvas"></canvas>
303
+ </div>
304
+
305
+ <div class="chart-card">
306
+ <div class="chart-title">
307
+ 🛠️ tool usage trends
308
+ </div>
309
+ <canvas id="toolChart" class="chart-canvas"></canvas>
310
+ </div>
311
+
312
+ <div class="chart-card">
313
+ <div class="chart-title">
314
+ ⚡ tool activity summary
315
+ </div>
316
+ <div id="toolSummary" class="tool-summary">
317
+ <!-- Tool summary will be loaded here -->
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ `;
324
+
325
+ this.bindEvents();
326
+ this.initializeTheme();
327
+ }
328
+
329
+ /**
330
+ * Initialize child components
331
+ */
332
+ async initializeComponents() {
333
+ // Initialize SessionTimer if available
334
+ const sessionTimerContainer = this.container.querySelector('#session-timer-container');
335
+ if (sessionTimerContainer && typeof SessionTimer !== 'undefined') {
336
+ try {
337
+ this.components.sessionTimer = new SessionTimer(
338
+ sessionTimerContainer,
339
+ this.dataService,
340
+ this.stateService
341
+ );
342
+ await this.components.sessionTimer.initialize();
343
+ } catch (error) {
344
+ console.warn('SessionTimer initialization failed:', error);
345
+ // Show fallback content
346
+ sessionTimerContainer.innerHTML = `
347
+ <div class="session-timer-placeholder">
348
+ <p>Session timer not available</p>
349
+ </div>
350
+ `;
351
+ }
352
+ }
353
+
354
+ // Initialize Charts with data if available
355
+ await this.initializeChartsAsync();
356
+
357
+ // Initialize Activity Feed
358
+ this.initializeActivityFeed();
359
+ }
360
+
361
+ /**
362
+ * Initialize charts asynchronously to prevent blocking main dashboard
363
+ */
364
+ async initializeChartsAsync() {
365
+ try {
366
+ console.log('📊 Starting asynchronous chart initialization...');
367
+ await this.initializeCharts();
368
+
369
+ // Update charts with data if available
370
+ if (this.allData) {
371
+ console.log('📊 Updating charts with loaded data...');
372
+ this.updateChartData(this.allData);
373
+ console.log('✅ Charts updated with data');
374
+ }
375
+ } catch (error) {
376
+ console.error('❌ Chart initialization failed, dashboard will work without charts:', error);
377
+ // Dashboard continues to work without charts
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Initialize charts (Token Usage, Project Distribution, Tool Usage)
383
+ */
384
+ async initializeCharts() {
385
+ // Destroy existing charts if they exist
386
+ if (this.components.tokenChart) {
387
+ this.components.tokenChart.destroy();
388
+ this.components.tokenChart = null;
389
+ }
390
+ if (this.components.projectChart) {
391
+ this.components.projectChart.destroy();
392
+ this.components.projectChart = null;
393
+ }
394
+ if (this.components.toolChart) {
395
+ this.components.toolChart.destroy();
396
+ this.components.toolChart = null;
397
+ }
398
+
399
+ // Longer delay to ensure DOM is fully ready and previous charts are destroyed
400
+ await new Promise(resolve => setTimeout(resolve, 250));
401
+
402
+ // Get canvas elements with strict validation
403
+ const tokenCanvas = this.container.querySelector('#tokenChart');
404
+ const projectCanvas = this.container.querySelector('#projectChart');
405
+ const toolCanvas = this.container.querySelector('#toolChart');
406
+
407
+ // Validate all canvas elements exist and are properly attached to DOM
408
+ if (!tokenCanvas || !projectCanvas || !toolCanvas) {
409
+ console.error('❌ Chart canvas elements not found in DOM');
410
+ console.log('Available elements:', {
411
+ tokenCanvas: !!tokenCanvas,
412
+ projectCanvas: !!projectCanvas,
413
+ toolCanvas: !!toolCanvas
414
+ });
415
+ return; // Don't initialize charts if canvas elements are missing
416
+ }
417
+
418
+ // Verify canvas elements are properly connected to the DOM
419
+ if (!document.body.contains(tokenCanvas) ||
420
+ !document.body.contains(projectCanvas) ||
421
+ !document.body.contains(toolCanvas)) {
422
+ console.error('❌ Chart canvas elements not properly attached to DOM');
423
+ return;
424
+ }
425
+
426
+ // Force destroy any existing Chart instances
427
+ try {
428
+ if (Chart.getChart(tokenCanvas)) {
429
+ console.log('🧹 Destroying existing tokenChart instance');
430
+ Chart.getChart(tokenCanvas).destroy();
431
+ }
432
+ if (Chart.getChart(projectCanvas)) {
433
+ console.log('🧹 Destroying existing projectChart instance');
434
+ Chart.getChart(projectCanvas).destroy();
435
+ }
436
+ if (Chart.getChart(toolCanvas)) {
437
+ console.log('🧹 Destroying existing toolChart instance');
438
+ Chart.getChart(toolCanvas).destroy();
439
+ }
440
+ } catch (error) {
441
+ console.warn('Warning during chart cleanup:', error);
442
+ }
443
+
444
+ // Validate canvas dimensions and ensure they're properly sized
445
+ const canvases = [tokenCanvas, projectCanvas, toolCanvas];
446
+ for (const canvas of canvases) {
447
+ if (canvas.offsetWidth === 0 || canvas.offsetHeight === 0) {
448
+ console.error('❌ Canvas has zero dimensions, waiting for layout...');
449
+ await new Promise(resolve => setTimeout(resolve, 100));
450
+ if (canvas.offsetWidth === 0 || canvas.offsetHeight === 0) {
451
+ console.error('❌ Canvas still has zero dimensions after wait');
452
+ return;
453
+ }
454
+ }
455
+ }
456
+
457
+ // Token Usage Chart (Linear)
458
+ if (tokenCanvas) {
459
+ try {
460
+ console.log('📊 Creating token chart...');
461
+ this.components.tokenChart = new Chart(tokenCanvas, {
462
+ type: 'line',
463
+ data: {
464
+ labels: [],
465
+ datasets: [{
466
+ label: 'Tokens',
467
+ data: [],
468
+ borderColor: '#d57455',
469
+ backgroundColor: 'rgba(213, 116, 85, 0.1)',
470
+ tension: 0.4,
471
+ fill: true
472
+ }]
473
+ },
474
+ options: this.getTokenChartOptions()
475
+ });
476
+ console.log('✅ Token chart created successfully');
477
+ } catch (error) {
478
+ console.error('❌ Error creating token chart:', error);
479
+ }
480
+ }
481
+
482
+ // Project Activity Distribution Chart (Pie)
483
+ if (projectCanvas) {
484
+ try {
485
+ console.log('📊 Creating project chart...');
486
+ this.components.projectChart = new Chart(projectCanvas, {
487
+ type: 'doughnut',
488
+ data: {
489
+ labels: [],
490
+ datasets: [{
491
+ data: [],
492
+ backgroundColor: [
493
+ '#d57455', '#3fb950', '#f97316', '#a5d6ff',
494
+ '#f85149', '#7d8590', '#ffd33d', '#bf91f3'
495
+ ],
496
+ borderWidth: 0
497
+ }]
498
+ },
499
+ options: this.getProjectChartOptions()
500
+ });
501
+ console.log('✅ Project chart created successfully');
502
+ } catch (error) {
503
+ console.error('❌ Error creating project chart:', error);
504
+ }
505
+ }
506
+
507
+ // Tool Usage Trends Chart (Bar)
508
+ if (toolCanvas) {
509
+ try {
510
+ console.log('📊 Creating tool chart...');
511
+ this.components.toolChart = new Chart(toolCanvas, {
512
+ type: 'bar',
513
+ data: {
514
+ labels: [],
515
+ datasets: [{
516
+ label: 'Usage Count',
517
+ data: [],
518
+ backgroundColor: [
519
+ 'rgba(75, 192, 192, 0.6)', 'rgba(255, 99, 132, 0.6)',
520
+ 'rgba(54, 162, 235, 0.6)', 'rgba(255, 206, 86, 0.6)',
521
+ 'rgba(153, 102, 255, 0.6)', 'rgba(255, 159, 64, 0.6)'
522
+ ],
523
+ borderColor: [
524
+ 'rgba(75, 192, 192, 1)', 'rgba(255, 99, 132, 1)',
525
+ 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)',
526
+ 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'
527
+ ],
528
+ borderWidth: 1
529
+ }]
530
+ },
531
+ options: this.getToolChartOptions()
532
+ });
533
+ console.log('✅ Tool chart created successfully');
534
+ } catch (error) {
535
+ console.error('❌ Error creating tool chart:', error);
536
+ }
537
+ }
538
+
539
+ console.log('🎉 All charts initialized successfully');
540
+
541
+ // Initialize date inputs
542
+ this.initializeDateInputs();
543
+ }
544
+
545
+ /**
546
+ * Get token chart options
547
+ */
548
+ getTokenChartOptions() {
549
+ return {
550
+ responsive: true,
551
+ maintainAspectRatio: false,
552
+ interaction: {
553
+ mode: 'nearest',
554
+ axis: 'x',
555
+ intersect: false
556
+ },
557
+ plugins: {
558
+ legend: {
559
+ display: false
560
+ },
561
+ tooltip: {
562
+ enabled: true,
563
+ mode: 'nearest',
564
+ backgroundColor: '#161b22',
565
+ titleColor: '#d57455',
566
+ bodyColor: '#c9d1d9',
567
+ borderColor: '#30363d',
568
+ borderWidth: 1,
569
+ cornerRadius: 4,
570
+ displayColors: false,
571
+ animation: {
572
+ duration: 200
573
+ },
574
+ callbacks: {
575
+ title: function(context) {
576
+ return `Date: ${context[0].label}`;
577
+ },
578
+ label: function(context) {
579
+ return `Tokens: ${context.parsed.y.toLocaleString()}`;
580
+ }
581
+ }
582
+ }
583
+ },
584
+ scales: {
585
+ x: {
586
+ grid: {
587
+ color: '#30363d'
588
+ },
589
+ ticks: {
590
+ color: '#7d8590'
591
+ }
592
+ },
593
+ y: {
594
+ beginAtZero: true,
595
+ grid: {
596
+ color: '#30363d'
597
+ },
598
+ ticks: {
599
+ color: '#7d8590',
600
+ callback: function(value) {
601
+ return value.toLocaleString();
602
+ }
603
+ }
604
+ }
605
+ }
606
+ };
607
+ }
608
+
609
+ /**
610
+ * Get project chart options
611
+ */
612
+ getProjectChartOptions() {
613
+ return {
614
+ responsive: true,
615
+ maintainAspectRatio: false,
616
+ plugins: {
617
+ legend: {
618
+ position: 'bottom',
619
+ labels: {
620
+ color: '#c9d1d9',
621
+ padding: 15,
622
+ usePointStyle: true
623
+ }
624
+ },
625
+ tooltip: {
626
+ enabled: true,
627
+ backgroundColor: '#161b22',
628
+ titleColor: '#d57455',
629
+ bodyColor: '#c9d1d9',
630
+ borderColor: '#30363d',
631
+ borderWidth: 1,
632
+ cornerRadius: 4,
633
+ displayColors: false,
634
+ animation: {
635
+ duration: 200
636
+ },
637
+ callbacks: {
638
+ title: function(context) {
639
+ return `Project: ${context[0].label}`;
640
+ },
641
+ label: function(context) {
642
+ const total = context.dataset.data.reduce((sum, value) => sum + value, 0);
643
+ const percentage = ((context.parsed / total) * 100).toFixed(1);
644
+ return `${context.parsed.toLocaleString()} conversations (${percentage}%)`;
645
+ }
646
+ }
647
+ }
648
+ },
649
+ cutout: '60%'
650
+ };
651
+ }
652
+
653
+ /**
654
+ * Get tool chart options
655
+ */
656
+ getToolChartOptions() {
657
+ return {
658
+ responsive: true,
659
+ maintainAspectRatio: false,
660
+ interaction: {
661
+ mode: 'nearest',
662
+ axis: 'x',
663
+ intersect: false
664
+ },
665
+ plugins: {
666
+ legend: {
667
+ display: false
668
+ },
669
+ tooltip: {
670
+ enabled: true,
671
+ mode: 'nearest',
672
+ backgroundColor: '#161b22',
673
+ titleColor: '#d57455',
674
+ bodyColor: '#c9d1d9',
675
+ borderColor: '#30363d',
676
+ borderWidth: 1,
677
+ cornerRadius: 4,
678
+ displayColors: false,
679
+ animation: {
680
+ duration: 200
681
+ },
682
+ callbacks: {
683
+ title: function(context) {
684
+ return `Tool: ${context[0].label}`;
685
+ },
686
+ label: function(context) {
687
+ return `Usage: ${context.parsed.y.toLocaleString()} times`;
688
+ }
689
+ }
690
+ }
691
+ },
692
+ scales: {
693
+ x: {
694
+ grid: {
695
+ color: '#30363d'
696
+ },
697
+ ticks: {
698
+ color: '#7d8590',
699
+ maxRotation: 45
700
+ }
701
+ },
702
+ y: {
703
+ beginAtZero: true,
704
+ grid: {
705
+ color: '#30363d'
706
+ },
707
+ ticks: {
708
+ color: '#7d8590',
709
+ stepSize: 1
710
+ }
711
+ }
712
+ }
713
+ };
714
+ }
715
+
716
+ /**
717
+ * Initialize activity feed
718
+ */
719
+ initializeActivityFeed() {
720
+ const activityFeed = this.container.querySelector('#activity-feed');
721
+
722
+ // Check if activity feed element exists
723
+ if (!activityFeed) {
724
+ console.log('ℹ️ Activity feed element not found, skipping initialization');
725
+ return;
726
+ }
727
+
728
+ // Sample activity data (would be replaced with real data)
729
+ const activities = [
730
+ {
731
+ type: 'session_start',
732
+ message: 'New Claude Code session started',
733
+ timestamp: new Date(),
734
+ icon: '🚀'
735
+ },
736
+ {
737
+ type: 'conversation_update',
738
+ message: 'Conversation state updated',
739
+ timestamp: new Date(Date.now() - 5 * 60 * 1000),
740
+ icon: '💬'
741
+ },
742
+ {
743
+ type: 'system_event',
744
+ message: 'Analytics server started',
745
+ timestamp: new Date(Date.now() - 10 * 60 * 1000),
746
+ icon: '⚡'
747
+ }
748
+ ];
749
+
750
+ activityFeed.innerHTML = activities.map(activity => `
751
+ <div class="activity-item">
752
+ <div class="activity-icon">${activity.icon}</div>
753
+ <div class="activity-content">
754
+ <div class="activity-message">${activity.message}</div>
755
+ <div class="activity-time">${this.formatTimestamp(activity.timestamp)}</div>
756
+ </div>
757
+ </div>
758
+ `).join('');
759
+ }
760
+
761
+ /**
762
+ * Format timestamp for display
763
+ * @param {Date} timestamp - Timestamp to format
764
+ * @returns {string} Formatted timestamp
765
+ */
766
+ formatTimestamp(timestamp) {
767
+ const now = new Date();
768
+ const diff = now - timestamp;
769
+ const minutes = Math.floor(diff / (1000 * 60));
770
+
771
+ if (minutes < 1) return 'Just now';
772
+ if (minutes < 60) return `${minutes}m ago`;
773
+
774
+ const hours = Math.floor(minutes / 60);
775
+ if (hours < 24) return `${hours}h ago`;
776
+
777
+ return timestamp.toLocaleDateString();
778
+ }
779
+
780
+ /**
781
+ * Bind event listeners
782
+ */
783
+ bindEvents() {
784
+ // Refresh button
785
+ const refreshBtn = this.container.querySelector('#refresh-dashboard');
786
+ if (refreshBtn) {
787
+ refreshBtn.addEventListener('click', () => this.refreshData());
788
+ }
789
+
790
+ // Export button
791
+ const exportBtn = this.container.querySelector('#export-data');
792
+ if (exportBtn) {
793
+ exportBtn.addEventListener('click', () => this.exportData());
794
+ }
795
+
796
+ // Date filter controls
797
+ const applyDateFilter = this.container.querySelector('#applyDateFilter');
798
+ if (applyDateFilter) {
799
+ applyDateFilter.addEventListener('click', () => this.applyDateFilter());
800
+ }
801
+
802
+ // Token popover events
803
+ const totalTokens = this.container.querySelector('#totalTokens');
804
+ if (totalTokens) {
805
+ totalTokens.addEventListener('mouseenter', () => this.showTokenPopover());
806
+ totalTokens.addEventListener('mouseleave', () => this.hideTokenPopover());
807
+ totalTokens.addEventListener('click', () => this.showTokenPopover());
808
+ }
809
+
810
+ // Error retry
811
+ const retryBtn = this.container.querySelector('#retry-load');
812
+ if (retryBtn) {
813
+ retryBtn.addEventListener('click', () => this.loadInitialData());
814
+ }
815
+
816
+ // Theme toggle (header)
817
+ const headerThemeSwitch = this.container.querySelector('#header-theme-switch');
818
+ if (headerThemeSwitch) {
819
+ headerThemeSwitch.addEventListener('click', () => this.toggleTheme());
820
+ }
821
+ }
822
+
823
+ /**
824
+ * Load initial data
825
+ */
826
+ async loadInitialData() {
827
+ try {
828
+ const [conversationsData, statesData] = await Promise.all([
829
+ this.dataService.getConversations(),
830
+ this.dataService.getConversationStates()
831
+ ]);
832
+
833
+ this.stateService.updateConversations(conversationsData.conversations);
834
+ this.stateService.updateSummary(conversationsData.summary);
835
+ this.stateService.updateConversationStates(statesData);
836
+
837
+ // Update dashboard with original format
838
+ this.updateSummaryDisplay(
839
+ conversationsData.summary,
840
+ conversationsData.detailedTokenUsage,
841
+ conversationsData
842
+ );
843
+
844
+ this.updateLastUpdateTime();
845
+ this.updateChartData(conversationsData);
846
+ } catch (error) {
847
+ console.error('Error loading initial data:', error);
848
+
849
+ // Try to provide fallback demo data
850
+ const demoData = {
851
+ summary: {
852
+ totalConversations: 0,
853
+ claudeSessions: 0,
854
+ claudeSessionsDetail: 'no sessions',
855
+ totalTokens: 0,
856
+ activeProjects: 0,
857
+ dataSize: '0 MB'
858
+ },
859
+ detailedTokenUsage: {
860
+ inputTokens: 0,
861
+ outputTokens: 0,
862
+ cacheCreationTokens: 0,
863
+ cacheReadTokens: 0
864
+ },
865
+ conversations: []
866
+ };
867
+
868
+ this.updateSummaryDisplay(demoData.summary, demoData.detailedTokenUsage, demoData);
869
+ this.updateLastUpdateTime();
870
+ this.stateService.setError('Using offline mode - server connection failed');
871
+ }
872
+ }
873
+
874
+ /**
875
+ * Refresh all data
876
+ */
877
+ async refreshData() {
878
+ const refreshBtn = this.container.querySelector('#refresh-dashboard');
879
+ if (!refreshBtn) return;
880
+
881
+ refreshBtn.disabled = true;
882
+ refreshBtn.classList.add('loading');
883
+
884
+ const btnIcon = refreshBtn.querySelector('.btn-icon-small');
885
+ if (btnIcon) {
886
+ btnIcon.classList.add('spin');
887
+ }
888
+
889
+ try {
890
+ this.dataService.clearCache();
891
+ await this.loadInitialData();
892
+ } catch (error) {
893
+ console.error('Error refreshing data:', error);
894
+ this.stateService.setError('Failed to refresh data');
895
+ } finally {
896
+ refreshBtn.disabled = false;
897
+ refreshBtn.classList.remove('loading');
898
+
899
+ if (btnIcon) {
900
+ btnIcon.classList.remove('spin');
901
+ }
902
+ }
903
+ }
904
+
905
+ /**
906
+ * Update summary display (New Cards format)
907
+ * @param {Object} summary - Summary data
908
+ * @param {Object} detailedTokenUsage - Detailed token breakdown
909
+ * @param {Object} allData - Complete dataset
910
+ */
911
+ updateSummaryDisplay(summary, detailedTokenUsage, allData) {
912
+ if (!summary) return;
913
+
914
+ // Calculate additional metrics
915
+ const now = new Date();
916
+ const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
917
+ const thisWeek = new Date(now.setDate(now.getDate() - now.getDay()));
918
+
919
+ // Update primary metrics
920
+ const totalConversations = this.container.querySelector('#totalConversations');
921
+ const claudeSessions = this.container.querySelector('#claudeSessions');
922
+ const totalTokens = this.container.querySelector('#totalTokens');
923
+
924
+ if (totalConversations) totalConversations.textContent = summary.totalConversations?.toLocaleString() || '0';
925
+ if (claudeSessions) claudeSessions.textContent = summary.claudeSessions?.toLocaleString() || '0';
926
+ if (totalTokens) totalTokens.textContent = summary.totalTokens?.toLocaleString() || '0';
927
+
928
+ // Update conversation secondary metrics
929
+ const conversationsMonth = this.container.querySelector('#conversationsMonth');
930
+ const conversationsWeek = this.container.querySelector('#conversationsWeek');
931
+ const activeConversations = this.container.querySelector('#activeConversations');
932
+
933
+ if (conversationsMonth) conversationsMonth.textContent = this.calculateTimeRangeCount(allData?.conversations, thisMonth).toLocaleString();
934
+ if (conversationsWeek) conversationsWeek.textContent = this.calculateTimeRangeCount(allData?.conversations, thisWeek).toLocaleString();
935
+ if (activeConversations) activeConversations.textContent = summary.activeConversations?.toLocaleString() || '0';
936
+
937
+ // Update session secondary metrics
938
+ const sessionsMonth = this.container.querySelector('#sessionsMonth');
939
+ const sessionsWeek = this.container.querySelector('#sessionsWeek');
940
+ const activeProjects = this.container.querySelector('#activeProjects');
941
+
942
+ if (sessionsMonth) sessionsMonth.textContent = Math.max(1, Math.floor((summary.claudeSessions || 0) * 0.3)).toLocaleString();
943
+ if (sessionsWeek) sessionsWeek.textContent = Math.max(1, Math.floor((summary.claudeSessions || 0) * 0.1)).toLocaleString();
944
+ if (activeProjects) activeProjects.textContent = summary.activeProjects?.toLocaleString() || '0';
945
+
946
+ // Update token secondary metrics
947
+ if (detailedTokenUsage) {
948
+ this.updateTokenBreakdown(detailedTokenUsage);
949
+ }
950
+
951
+ // Store data for chart updates
952
+ this.allData = allData;
953
+ }
954
+
955
+ /**
956
+ * Calculate count of items within a time range
957
+ * @param {Array} items - Items with lastModified property
958
+ * @param {Date} fromDate - Start date
959
+ * @returns {number} Count of items
960
+ */
961
+ calculateTimeRangeCount(items, fromDate) {
962
+ if (!items || !Array.isArray(items)) return 0;
963
+
964
+ return items.filter(item => {
965
+ if (!item.lastModified) return false;
966
+ const itemDate = new Date(item.lastModified);
967
+ return itemDate >= fromDate;
968
+ }).length;
969
+ }
970
+
971
+ /**
972
+ * Update token breakdown in cards
973
+ * @param {Object} tokenUsage - Detailed token usage
974
+ */
975
+ updateTokenBreakdown(tokenUsage) {
976
+ const inputTokens = this.container.querySelector('#inputTokens');
977
+ const outputTokens = this.container.querySelector('#outputTokens');
978
+ const cacheTokens = this.container.querySelector('#cacheTokens');
979
+
980
+ if (inputTokens) inputTokens.textContent = tokenUsage.inputTokens?.toLocaleString() || '0';
981
+ if (outputTokens) outputTokens.textContent = tokenUsage.outputTokens?.toLocaleString() || '0';
982
+
983
+ // Combine cache creation and read tokens
984
+ const totalCache = (tokenUsage.cacheCreationTokens || 0) + (tokenUsage.cacheReadTokens || 0);
985
+ if (cacheTokens) cacheTokens.textContent = totalCache.toLocaleString();
986
+ }
987
+
988
+ /**
989
+ * Show token popover
990
+ */
991
+ showTokenPopover() {
992
+ const popover = this.container.querySelector('#tokenPopover');
993
+ if (popover) {
994
+ popover.style.display = 'block';
995
+ }
996
+ }
997
+
998
+ /**
999
+ * Hide token popover
1000
+ */
1001
+ hideTokenPopover() {
1002
+ const popover = this.container.querySelector('#tokenPopover');
1003
+ if (popover) {
1004
+ popover.style.display = 'none';
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Initialize date inputs
1010
+ */
1011
+ initializeDateInputs() {
1012
+ const dateFrom = this.container.querySelector('#dateFrom');
1013
+ const dateTo = this.container.querySelector('#dateTo');
1014
+
1015
+ if (!dateFrom || !dateTo) return;
1016
+
1017
+ const today = new Date();
1018
+ const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
1019
+
1020
+ dateFrom.value = sevenDaysAgo.toISOString().split('T')[0];
1021
+ dateTo.value = today.toISOString().split('T')[0];
1022
+ }
1023
+
1024
+ /**
1025
+ * Get date range from inputs
1026
+ */
1027
+ getDateRange() {
1028
+ const dateFrom = this.container.querySelector('#dateFrom');
1029
+ const dateTo = this.container.querySelector('#dateTo');
1030
+
1031
+ let fromDate = new Date();
1032
+ fromDate.setDate(fromDate.getDate() - 7); // Default to 7 days ago
1033
+ let toDate = new Date();
1034
+
1035
+ if (dateFrom && dateFrom.value) {
1036
+ fromDate = new Date(dateFrom.value);
1037
+ }
1038
+ if (dateTo && dateTo.value) {
1039
+ toDate = new Date(dateTo.value);
1040
+ toDate.setHours(23, 59, 59, 999); // Include full day
1041
+ }
1042
+
1043
+ return { fromDate, toDate };
1044
+ }
1045
+
1046
+ /**
1047
+ * Apply date filter
1048
+ */
1049
+ applyDateFilter() {
1050
+ if (this.allData) {
1051
+ this.updateChartData(this.allData);
1052
+ }
1053
+ }
1054
+
1055
+ /**
1056
+ * Refresh charts
1057
+ */
1058
+ async refreshCharts() {
1059
+ const refreshBtn = this.container.querySelector('#refreshCharts');
1060
+ if (refreshBtn) {
1061
+ refreshBtn.disabled = true;
1062
+ refreshBtn.textContent = 'refreshing...';
1063
+ }
1064
+
1065
+ try {
1066
+ await this.loadInitialData();
1067
+ } finally {
1068
+ if (refreshBtn) {
1069
+ refreshBtn.disabled = false;
1070
+ refreshBtn.textContent = 'refresh charts';
1071
+ }
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Update system status
1077
+ * @param {Object} states - Conversation states
1078
+ */
1079
+ updateSystemStatus(states) {
1080
+ const activeCount = Object.values(states).filter(state => state === 'active').length;
1081
+
1082
+ // Update WebSocket status
1083
+ const wsStatus = this.container.querySelector('#websocket-status');
1084
+ if (wsStatus) {
1085
+ const indicator = wsStatus.querySelector('.status-indicator');
1086
+ indicator.className = `status-indicator ${activeCount > 0 ? 'connected' : 'disconnected'}`;
1087
+ wsStatus.lastChild.textContent = activeCount > 0 ? 'Connected' : 'Disconnected';
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * Update chart data with real analytics
1093
+ * @param {Object} data - Analytics data
1094
+ */
1095
+ updateChartData(data) {
1096
+ if (!data || !data.conversations) return;
1097
+
1098
+ this.updateTokenChart(data.conversations);
1099
+ this.updateProjectChart(data.conversations);
1100
+ this.updateToolChart(data.conversations);
1101
+ this.updateToolSummary(data.conversations);
1102
+ }
1103
+
1104
+ /**
1105
+ * Update token usage chart
1106
+ */
1107
+ updateTokenChart(conversations) {
1108
+ if (!this.components.tokenChart) {
1109
+ console.warn('Token chart not initialized');
1110
+ return;
1111
+ }
1112
+
1113
+ const { fromDate, toDate } = this.getDateRange();
1114
+ const filteredConversations = conversations.filter(conv => {
1115
+ const convDate = new Date(conv.lastModified);
1116
+ return convDate >= fromDate && convDate <= toDate;
1117
+ });
1118
+
1119
+ // Group by date and sum tokens
1120
+ const tokensByDate = {};
1121
+ filteredConversations.forEach(conv => {
1122
+ const date = new Date(conv.lastModified).toDateString();
1123
+ tokensByDate[date] = (tokensByDate[date] || 0) + (conv.tokens || 0);
1124
+ });
1125
+
1126
+ const sortedDates = Object.keys(tokensByDate).sort((a, b) => new Date(a) - new Date(b));
1127
+ const labels = sortedDates.map(date => new Date(date).toLocaleDateString());
1128
+ const data = sortedDates.map(date => tokensByDate[date]);
1129
+
1130
+ console.log('📊 Token chart - tokensByDate:', tokensByDate);
1131
+ console.log('📊 Token chart - Labels:', labels);
1132
+ console.log('📊 Token chart - Data:', data);
1133
+
1134
+ this.components.tokenChart.data.labels = labels;
1135
+ this.components.tokenChart.data.datasets[0].data = data;
1136
+ this.components.tokenChart.update();
1137
+ }
1138
+
1139
+ /**
1140
+ * Update project distribution chart
1141
+ */
1142
+ updateProjectChart(conversations) {
1143
+ if (!this.components.projectChart) {
1144
+ console.warn('Project chart not initialized');
1145
+ return;
1146
+ }
1147
+
1148
+ const { fromDate, toDate } = this.getDateRange();
1149
+ const filteredConversations = conversations.filter(conv => {
1150
+ const convDate = new Date(conv.lastModified);
1151
+ return convDate >= fromDate && convDate <= toDate;
1152
+ });
1153
+
1154
+ // Group by project and sum tokens
1155
+ const projectTokens = {};
1156
+ filteredConversations.forEach(conv => {
1157
+ const project = conv.project || 'Unknown';
1158
+ projectTokens[project] = (projectTokens[project] || 0) + (conv.tokens || 0);
1159
+ });
1160
+
1161
+ const labels = Object.keys(projectTokens);
1162
+ const data = Object.values(projectTokens);
1163
+
1164
+ this.components.projectChart.data.labels = labels;
1165
+ this.components.projectChart.data.datasets[0].data = data;
1166
+ this.components.projectChart.update();
1167
+ }
1168
+
1169
+ /**
1170
+ * Update tool usage chart
1171
+ */
1172
+ updateToolChart(conversations) {
1173
+ if (!this.components.toolChart) {
1174
+ console.warn('Tool chart not initialized');
1175
+ return;
1176
+ }
1177
+
1178
+ const { fromDate, toDate } = this.getDateRange();
1179
+ const toolStats = {};
1180
+
1181
+ conversations.forEach(conv => {
1182
+ if (conv.toolUsage && conv.toolUsage.toolTimeline) {
1183
+ conv.toolUsage.toolTimeline.forEach(entry => {
1184
+ const entryDate = new Date(entry.timestamp);
1185
+ if (entryDate >= fromDate && entryDate <= toDate) {
1186
+ toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1;
1187
+ }
1188
+ });
1189
+ }
1190
+ });
1191
+
1192
+ const sortedTools = Object.entries(toolStats)
1193
+ .sort((a, b) => b[1] - a[1])
1194
+ .slice(0, 10);
1195
+
1196
+ const labels = sortedTools.map(([tool]) => tool.length > 15 ? tool.substring(0, 15) + '...' : tool);
1197
+ const data = sortedTools.map(([, count]) => count);
1198
+
1199
+ this.components.toolChart.data.labels = labels;
1200
+ this.components.toolChart.data.datasets[0].data = data;
1201
+ this.components.toolChart.update();
1202
+ }
1203
+
1204
+ /**
1205
+ * Update tool summary panel
1206
+ */
1207
+ updateToolSummary(conversations) {
1208
+ const toolSummary = this.container.querySelector('#toolSummary');
1209
+ if (!toolSummary) return;
1210
+
1211
+ const { fromDate, toDate } = this.getDateRange();
1212
+ const toolStats = {};
1213
+ let totalToolCalls = 0;
1214
+ let conversationsWithTools = 0;
1215
+
1216
+ conversations.forEach(conv => {
1217
+ if (conv.toolUsage && conv.toolUsage.toolTimeline) {
1218
+ let convHasTools = false;
1219
+ conv.toolUsage.toolTimeline.forEach(entry => {
1220
+ const entryDate = new Date(entry.timestamp);
1221
+ if (entryDate >= fromDate && entryDate <= toDate) {
1222
+ toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1;
1223
+ totalToolCalls++;
1224
+ convHasTools = true;
1225
+ }
1226
+ });
1227
+ if (convHasTools) conversationsWithTools++;
1228
+ }
1229
+ });
1230
+
1231
+ const uniqueTools = Object.keys(toolStats).length;
1232
+ const topTool = Object.entries(toolStats).sort((a, b) => b[1] - a[1])[0];
1233
+
1234
+ toolSummary.innerHTML = `
1235
+ <div class="tool-stat">
1236
+ <span class="tool-stat-label">Total Tool Calls</span>
1237
+ <span class="tool-stat-value">${totalToolCalls.toLocaleString()}</span>
1238
+ </div>
1239
+ <div class="tool-stat">
1240
+ <span class="tool-stat-label">Unique Tools Used</span>
1241
+ <span class="tool-stat-value">${uniqueTools}</span>
1242
+ </div>
1243
+ <div class="tool-stat">
1244
+ <span class="tool-stat-label">Conversation Coverage</span>
1245
+ <span class="tool-stat-value">${Math.round((conversationsWithTools / conversations.length) * 100)}%</span>
1246
+ </div>
1247
+ ${topTool ? `
1248
+ <div class="tool-top-tool">
1249
+ <div class="tool-icon">🛠️</div>
1250
+ <div class="tool-info">
1251
+ <div class="tool-name">${topTool[0]}</div>
1252
+ <div class="tool-usage">${topTool[1]} calls</div>
1253
+ </div>
1254
+ </div>
1255
+ ` : ''}
1256
+ `;
1257
+ }
1258
+
1259
+ /**
1260
+ * Update usage chart
1261
+ * @param {string} period - Time period
1262
+ */
1263
+ updateUsageChart(period) {
1264
+ console.log('Updating usage chart period to:', period);
1265
+ // Implementation would update chart with new period data
1266
+ this.updateChartData();
1267
+ }
1268
+
1269
+ /**
1270
+ * Update performance chart
1271
+ * @param {string} type - Chart type
1272
+ */
1273
+ updatePerformanceChart(type) {
1274
+ console.log('Updating performance chart type to:', type);
1275
+ // Implementation would update chart with new metric type
1276
+ this.updateChartData();
1277
+ }
1278
+
1279
+ /**
1280
+ * Show all activity
1281
+ */
1282
+ showAllActivity() {
1283
+ console.log('Showing all activity');
1284
+ // Implementation would show expanded activity view
1285
+ }
1286
+
1287
+ /**
1288
+ * Export data
1289
+ */
1290
+ exportData() {
1291
+ const exportBtn = this.container.querySelector('#export-data');
1292
+ if (!exportBtn) return;
1293
+
1294
+ // Show loading state
1295
+ exportBtn.disabled = true;
1296
+ exportBtn.classList.add('loading');
1297
+
1298
+ const btnIcon = exportBtn.querySelector('.btn-icon-small');
1299
+ if (btnIcon) {
1300
+ btnIcon.classList.add('spin');
1301
+ }
1302
+
1303
+ try {
1304
+ const dashboardData = {
1305
+ summary: this.stateService.getStateProperty('summary'),
1306
+ states: this.stateService.getStateProperty('conversationStates'),
1307
+ exportDate: new Date().toISOString(),
1308
+ type: 'dashboard_analytics'
1309
+ };
1310
+
1311
+ const dataStr = JSON.stringify(dashboardData, null, 2);
1312
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
1313
+ const url = URL.createObjectURL(dataBlob);
1314
+
1315
+ const link = document.createElement('a');
1316
+ link.href = url;
1317
+ link.download = `dashboard-analytics-${new Date().toISOString().split('T')[0]}.json`;
1318
+ link.click();
1319
+
1320
+ URL.revokeObjectURL(url);
1321
+ } catch (error) {
1322
+ console.error('Error exporting data:', error);
1323
+ this.stateService.setError('Failed to export data');
1324
+ } finally {
1325
+ // Restore button state after short delay to show completion
1326
+ setTimeout(() => {
1327
+ exportBtn.disabled = false;
1328
+ exportBtn.classList.remove('loading');
1329
+
1330
+ if (btnIcon) {
1331
+ btnIcon.classList.remove('spin');
1332
+ }
1333
+ }, 500);
1334
+ }
1335
+ }
1336
+
1337
+ /**
1338
+ * Initialize theme from localStorage
1339
+ */
1340
+ initializeTheme() {
1341
+ const savedTheme = localStorage.getItem('claude-analytics-theme') || 'dark';
1342
+ const body = document.body;
1343
+ const headerThumb = this.container.querySelector('#header-theme-switch-thumb');
1344
+ const headerIcon = headerThumb?.querySelector('.theme-switch-icon');
1345
+
1346
+ body.setAttribute('data-theme', savedTheme);
1347
+ if (headerThumb && headerIcon) {
1348
+ if (savedTheme === 'light') {
1349
+ headerThumb.classList.add('light');
1350
+ headerIcon.textContent = '☀️';
1351
+ } else {
1352
+ headerThumb.classList.remove('light');
1353
+ headerIcon.textContent = '🌙';
1354
+ }
1355
+ }
1356
+ }
1357
+
1358
+ /**
1359
+ * Toggle theme between light and dark
1360
+ */
1361
+ toggleTheme() {
1362
+ const body = document.body;
1363
+ const headerThumb = this.container.querySelector('#header-theme-switch-thumb');
1364
+ const headerIcon = headerThumb?.querySelector('.theme-switch-icon');
1365
+
1366
+ // Also sync with global theme switch
1367
+ const globalThumb = document.getElementById('themeSwitchThumb');
1368
+ const globalIcon = globalThumb?.querySelector('.theme-switch-icon');
1369
+
1370
+ const isLight = body.getAttribute('data-theme') === 'light';
1371
+ const newTheme = isLight ? 'dark' : 'light';
1372
+
1373
+ body.setAttribute('data-theme', newTheme);
1374
+
1375
+ // Update header theme switch
1376
+ if (headerThumb && headerIcon) {
1377
+ headerThumb.classList.toggle('light', newTheme === 'light');
1378
+ headerIcon.textContent = newTheme === 'light' ? '☀️' : '🌙';
1379
+ }
1380
+
1381
+ // Sync with global theme switch
1382
+ if (globalThumb && globalIcon) {
1383
+ globalThumb.classList.toggle('light', newTheme === 'light');
1384
+ globalIcon.textContent = newTheme === 'light' ? '☀️' : '🌙';
1385
+ }
1386
+
1387
+ localStorage.setItem('claude-analytics-theme', newTheme);
1388
+ }
1389
+
1390
+ /**
1391
+ * Update last update time
1392
+ */
1393
+ updateLastUpdateTime() {
1394
+ const currentTime = new Date().toLocaleTimeString();
1395
+
1396
+ // Update both locations
1397
+ const lastUpdateText = this.container.querySelector('#last-update-text');
1398
+ const lastUpdateHeaderText = this.container.querySelector('#last-update-header-text');
1399
+
1400
+ if (lastUpdateText) {
1401
+ lastUpdateText.textContent = currentTime;
1402
+ }
1403
+ if (lastUpdateHeaderText) {
1404
+ lastUpdateHeaderText.textContent = currentTime;
1405
+ }
1406
+ }
1407
+
1408
+ /**
1409
+ * Start periodic refresh
1410
+ */
1411
+ startPeriodicRefresh() {
1412
+ this.refreshInterval = setInterval(async () => {
1413
+ try {
1414
+ const statesData = await this.dataService.getConversationStates();
1415
+ this.stateService.updateConversationStates(statesData);
1416
+ this.updateLastUpdateTime();
1417
+ } catch (error) {
1418
+ console.error('Error during periodic refresh:', error);
1419
+ }
1420
+ }, 30000); // Refresh every 30 seconds
1421
+ }
1422
+
1423
+ /**
1424
+ * Stop periodic refresh
1425
+ */
1426
+ stopPeriodicRefresh() {
1427
+ if (this.refreshInterval) {
1428
+ clearInterval(this.refreshInterval);
1429
+ this.refreshInterval = null;
1430
+ }
1431
+ }
1432
+
1433
+ /**
1434
+ * Update loading state
1435
+ * @param {boolean} isLoading - Loading state
1436
+ */
1437
+ updateLoadingState(isLoading) {
1438
+ console.log(`🔄 Updating loading state to: ${isLoading}`);
1439
+ const loadingState = this.container.querySelector('#dashboard-loading');
1440
+ if (loadingState) {
1441
+ loadingState.style.display = isLoading ? 'flex' : 'none';
1442
+ console.log(`✅ Loading state updated successfully to: ${isLoading ? 'visible' : 'hidden'}`);
1443
+ } else {
1444
+ console.warn('⚠️ Loading element #dashboard-loading not found');
1445
+ // Fallback: show/hide global loading instead
1446
+ const globalLoading = document.querySelector('#global-loading');
1447
+ if (globalLoading) {
1448
+ globalLoading.style.display = isLoading ? 'flex' : 'none';
1449
+ console.log(`✅ Global loading fallback updated to: ${isLoading ? 'visible' : 'hidden'}`);
1450
+ } else {
1451
+ console.warn('⚠️ Global loading element #global-loading also not found');
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ /**
1457
+ * Update error state
1458
+ * @param {Error|string} error - Error object or message
1459
+ */
1460
+ updateErrorState(error) {
1461
+ const errorState = this.container.querySelector('#dashboard-error');
1462
+ const errorMessage = this.container.querySelector('.error-message');
1463
+
1464
+ if (error) {
1465
+ if (errorMessage) {
1466
+ errorMessage.textContent = error.message || error;
1467
+ }
1468
+ if (errorState) {
1469
+ errorState.style.display = 'flex';
1470
+ }
1471
+ } else {
1472
+ if (errorState) {
1473
+ errorState.style.display = 'none';
1474
+ }
1475
+ }
1476
+ }
1477
+
1478
+ /**
1479
+ * Destroy dashboard page
1480
+ */
1481
+ destroy() {
1482
+ this.stopPeriodicRefresh();
1483
+
1484
+ // Cleanup Chart.js instances specifically
1485
+ if (this.components.tokenChart) {
1486
+ this.components.tokenChart.destroy();
1487
+ this.components.tokenChart = null;
1488
+ }
1489
+ if (this.components.projectChart) {
1490
+ this.components.projectChart.destroy();
1491
+ this.components.projectChart = null;
1492
+ }
1493
+ if (this.components.toolChart) {
1494
+ this.components.toolChart.destroy();
1495
+ this.components.toolChart = null;
1496
+ }
1497
+
1498
+ // Force cleanup any remaining Chart.js instances on canvas elements
1499
+ if (this.container) {
1500
+ const canvases = this.container.querySelectorAll('canvas');
1501
+ canvases.forEach(canvas => {
1502
+ const existingChart = Chart.getChart(canvas);
1503
+ if (existingChart) {
1504
+ existingChart.destroy();
1505
+ }
1506
+ });
1507
+ }
1508
+
1509
+ // Cleanup other components
1510
+ Object.values(this.components).forEach(component => {
1511
+ if (component && component.destroy && typeof component.destroy === 'function') {
1512
+ component.destroy();
1513
+ }
1514
+ });
1515
+
1516
+ // Clear components object
1517
+ this.components = {};
1518
+
1519
+ // Unsubscribe from state changes
1520
+ if (this.unsubscribe) {
1521
+ this.unsubscribe();
1522
+ }
1523
+
1524
+ this.isInitialized = false;
1525
+ }
1526
+ }
1527
+
1528
+ // Export for module use
1529
+ if (typeof module !== 'undefined' && module.exports) {
1530
+ module.exports = DashboardPage;
1531
+ }