claude-code-templates 1.8.0 → 1.8.2

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,573 @@
1
+ /**
2
+ * Dashboard - Main dashboard component that orchestrates all other components
3
+ * Part of the modular frontend architecture
4
+ */
5
+ class Dashboard {
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
22
+ */
23
+ async initialize() {
24
+ if (this.isInitialized) return;
25
+
26
+ try {
27
+ this.stateService.setLoading(true);
28
+ await this.render();
29
+ await this.initializeComponents();
30
+ await this.loadInitialData();
31
+ this.startPeriodicRefresh();
32
+ this.isInitialized = true;
33
+ } catch (error) {
34
+ console.error('Error initializing dashboard:', error);
35
+ this.stateService.setError(error);
36
+ } finally {
37
+ this.stateService.setLoading(false);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Handle state changes from StateService
43
+ * @param {Object} state - New state
44
+ * @param {string} action - Action that caused the change
45
+ */
46
+ handleStateChange(state, action) {
47
+ switch (action) {
48
+ case 'update_conversations':
49
+ this.updateSummaryDisplay(state.summary);
50
+ break;
51
+ case 'update_conversation_states':
52
+ this.updateConversationStates(state.conversationStates);
53
+ break;
54
+ case 'set_loading':
55
+ this.updateLoadingState(state.isLoading);
56
+ break;
57
+ case 'set_error':
58
+ this.updateErrorState(state.error);
59
+ break;
60
+ case 'conversation_state_change':
61
+ this.handleConversationStateChange(state);
62
+ break;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Render the dashboard structure
68
+ */
69
+ async render() {
70
+ this.container.innerHTML = `
71
+ <div class="dashboard-container">
72
+ <!-- Header -->
73
+ <div class="dashboard-header">
74
+ <div class="terminal-title">
75
+ <span class="status-dot"></span>
76
+ Claude Code Analytics - Terminal
77
+ </div>
78
+ <div class="header-controls">
79
+ <button class="control-btn" id="refresh-btn" title="Refresh data">
80
+ <span class="refresh-icon">🔄</span>
81
+ Refresh
82
+ </button>
83
+ <button class="control-btn" id="notifications-btn" title="Toggle notifications">
84
+ <span class="notification-icon">🔔</span>
85
+ Notifications
86
+ </button>
87
+ <div class="last-update">
88
+ <span id="last-update-text">Never</span>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- Loading State -->
94
+ <div class="loading-overlay" id="loading-overlay" style="display: none;">
95
+ <div class="loading-spinner"></div>
96
+ <span class="loading-text">Loading dashboard...</span>
97
+ </div>
98
+
99
+ <!-- Error State -->
100
+ <div class="error-banner" id="error-banner" style="display: none;">
101
+ <span class="error-icon">⚠️</span>
102
+ <span class="error-text"></span>
103
+ <button class="error-close" id="error-close">×</button>
104
+ </div>
105
+
106
+ <!-- Summary Cards -->
107
+ <div class="summary-section">
108
+ <div class="summary-cards">
109
+ <div class="summary-card">
110
+ <div class="card-header">
111
+ <h3>Total Conversations</h3>
112
+ <span class="card-icon">💬</span>
113
+ </div>
114
+ <div class="card-value" id="total-conversations">0</div>
115
+ <div class="card-change">
116
+ <span class="change-text" id="conversations-change">+0 today</span>
117
+ </div>
118
+ </div>
119
+
120
+ <div class="summary-card">
121
+ <div class="card-header">
122
+ <h3>Active Sessions</h3>
123
+ <span class="card-icon">⚡</span>
124
+ </div>
125
+ <div class="card-value" id="active-sessions">0</div>
126
+ <div class="card-change">
127
+ <span class="change-text" id="sessions-change">0 running</span>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="summary-card">
132
+ <div class="card-header">
133
+ <h3>Total Tokens</h3>
134
+ <span class="card-icon">🔤</span>
135
+ </div>
136
+ <div class="card-value" id="total-tokens">0</div>
137
+ <div class="card-change">
138
+ <span class="change-text" id="tokens-change">0 avg/conv</span>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="summary-card">
143
+ <div class="card-header">
144
+ <h3>Projects</h3>
145
+ <span class="card-icon">📁</span>
146
+ </div>
147
+ <div class="card-value" id="total-projects">0</div>
148
+ <div class="card-change">
149
+ <span class="change-text" id="projects-change">0 active</span>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Session Timer Section -->
156
+ <div class="session-timer-section">
157
+ <div id="session-timer-container">
158
+ <!-- SessionTimer component will be mounted here -->
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Charts Section -->
163
+ <div class="charts-section">
164
+ <div class="chart-container">
165
+ <div class="chart-header">
166
+ <h3>Usage Over Time</h3>
167
+ <div class="chart-controls">
168
+ <select id="chart-period">
169
+ <option value="7d">Last 7 days</option>
170
+ <option value="30d">Last 30 days</option>
171
+ <option value="90d">Last 90 days</option>
172
+ </select>
173
+ </div>
174
+ </div>
175
+ <div class="chart-canvas-container">
176
+ <canvas id="usage-chart"></canvas>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Conversations Table -->
182
+ <div class="conversations-section">
183
+ <div class="section-header">
184
+ <h3>Recent Conversations</h3>
185
+ <div class="section-controls">
186
+ <button class="control-btn" id="export-btn">
187
+ <span class="export-icon">📤</span>
188
+ Export
189
+ </button>
190
+ </div>
191
+ </div>
192
+ <div class="conversations-table-container" id="conversations-table">
193
+ <!-- ConversationTable component will be mounted here -->
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Footer -->
198
+ <div class="dashboard-footer">
199
+ <div class="footer-info">
200
+ <span>Analytics Dashboard v1.0</span>
201
+ <span>•</span>
202
+ <span id="connection-status">Connected</span>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ `;
207
+
208
+ this.bindEvents();
209
+ }
210
+
211
+ /**
212
+ * Initialize child components
213
+ */
214
+ async initializeComponents() {
215
+ // Initialize SessionTimer
216
+ const sessionTimerContainer = this.container.querySelector('#session-timer-container');
217
+ if (sessionTimerContainer && window.SessionTimer) {
218
+ this.components.sessionTimer = new SessionTimer(
219
+ sessionTimerContainer,
220
+ this.dataService,
221
+ this.stateService
222
+ );
223
+ await this.components.sessionTimer.initialize();
224
+ }
225
+
226
+ // Initialize ConversationTable
227
+ const tableContainer = this.container.querySelector('#conversations-table');
228
+ this.components.conversationTable = new ConversationTable(
229
+ tableContainer,
230
+ this.dataService,
231
+ this.stateService
232
+ );
233
+ await this.components.conversationTable.initialize();
234
+
235
+ // Initialize Charts (if available)
236
+ if (this.chartService) {
237
+ await this.initializeCharts();
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Initialize charts
243
+ */
244
+ async initializeCharts() {
245
+ const chartCanvas = this.container.querySelector('#usage-chart');
246
+ if (chartCanvas) {
247
+ this.components.usageChart = new Chart(chartCanvas, {
248
+ type: 'line',
249
+ data: {
250
+ labels: [],
251
+ datasets: [{
252
+ label: 'Conversations',
253
+ data: [],
254
+ borderColor: '#3fb950',
255
+ backgroundColor: 'rgba(63, 185, 80, 0.1)',
256
+ tension: 0.4
257
+ }]
258
+ },
259
+ options: {
260
+ responsive: true,
261
+ maintainAspectRatio: false,
262
+ scales: {
263
+ y: {
264
+ beginAtZero: true,
265
+ grid: {
266
+ color: '#30363d'
267
+ },
268
+ ticks: {
269
+ color: '#c9d1d9'
270
+ }
271
+ },
272
+ x: {
273
+ grid: {
274
+ color: '#30363d'
275
+ },
276
+ ticks: {
277
+ color: '#c9d1d9'
278
+ }
279
+ }
280
+ },
281
+ plugins: {
282
+ legend: {
283
+ labels: {
284
+ color: '#c9d1d9'
285
+ }
286
+ }
287
+ }
288
+ }
289
+ });
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Bind event listeners
295
+ */
296
+ bindEvents() {
297
+ // Refresh button
298
+ const refreshBtn = this.container.querySelector('#refresh-btn');
299
+ refreshBtn.addEventListener('click', () => this.refreshData());
300
+
301
+ // Notifications button
302
+ const notificationsBtn = this.container.querySelector('#notifications-btn');
303
+ notificationsBtn.addEventListener('click', () => this.toggleNotifications());
304
+
305
+ // Export button
306
+ const exportBtn = this.container.querySelector('#export-btn');
307
+ exportBtn.addEventListener('click', () => this.exportData());
308
+
309
+ // Chart period selector
310
+ const chartPeriod = this.container.querySelector('#chart-period');
311
+ chartPeriod.addEventListener('change', (e) => this.updateChartPeriod(e.target.value));
312
+
313
+ // Error banner close
314
+ const errorClose = this.container.querySelector('#error-close');
315
+ errorClose.addEventListener('click', () => this.clearError());
316
+ }
317
+
318
+ /**
319
+ * Load initial data
320
+ */
321
+ async loadInitialData() {
322
+ try {
323
+ const [conversationsData, statesData] = await Promise.all([
324
+ this.dataService.getConversations(),
325
+ this.dataService.getConversationStates()
326
+ ]);
327
+
328
+ this.stateService.updateConversations(conversationsData.conversations);
329
+ this.stateService.updateSummary(conversationsData.summary);
330
+ this.stateService.updateConversationStates(statesData);
331
+
332
+ this.updateLastUpdateTime();
333
+ } catch (error) {
334
+ console.error('Error loading initial data:', error);
335
+ this.stateService.setError('Failed to load dashboard data');
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Refresh all data
341
+ */
342
+ async refreshData() {
343
+ const refreshBtn = this.container.querySelector('#refresh-btn');
344
+ refreshBtn.disabled = true;
345
+ refreshBtn.querySelector('.refresh-icon').style.animation = 'spin 1s linear infinite';
346
+
347
+ try {
348
+ this.dataService.clearCache();
349
+ await this.loadInitialData();
350
+ } catch (error) {
351
+ console.error('Error refreshing data:', error);
352
+ this.stateService.setError('Failed to refresh data');
353
+ } finally {
354
+ refreshBtn.disabled = false;
355
+ refreshBtn.querySelector('.refresh-icon').style.animation = '';
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Update summary display
361
+ * @param {Object} summary - Summary data
362
+ */
363
+ updateSummaryDisplay(summary) {
364
+ if (!summary) return;
365
+
366
+ const totalConvs = this.container.querySelector('#total-conversations');
367
+ const activeSessions = this.container.querySelector('#active-sessions');
368
+ const totalTokens = this.container.querySelector('#total-tokens');
369
+ const totalProjects = this.container.querySelector('#total-projects');
370
+
371
+ if (totalConvs) totalConvs.textContent = summary.totalConversations?.toLocaleString() || '0';
372
+ if (activeSessions) activeSessions.textContent = summary.activeConversations?.toLocaleString() || '0';
373
+ if (totalTokens) totalTokens.textContent = summary.totalTokens?.toLocaleString() || '0';
374
+ if (totalProjects) totalProjects.textContent = summary.totalProjects?.toLocaleString() || '0';
375
+
376
+ // Update change indicators
377
+ const conversationsChange = this.container.querySelector('#conversations-change');
378
+ const sessionsChange = this.container.querySelector('#sessions-change');
379
+ const tokensChange = this.container.querySelector('#tokens-change');
380
+ const projectsChange = this.container.querySelector('#projects-change');
381
+
382
+ if (conversationsChange) conversationsChange.textContent = `+${summary.totalConversations || 0} total`;
383
+ if (sessionsChange) sessionsChange.textContent = `${summary.activeConversations || 0} running`;
384
+ if (tokensChange) tokensChange.textContent = `${summary.avgTokensPerConversation || 0} avg/conv`;
385
+ if (projectsChange) projectsChange.textContent = `${summary.activeProjects || 0} active`;
386
+ }
387
+
388
+ /**
389
+ * Update conversation states
390
+ * @param {Object} states - Conversation states
391
+ */
392
+ updateConversationStates(states) {
393
+ // Update status dot based on active conversations
394
+ const statusDot = this.container.querySelector('.status-dot');
395
+ const activeCount = Object.values(states).filter(state => state === 'active').length;
396
+
397
+ if (activeCount > 0) {
398
+ statusDot.style.background = '#3fb950';
399
+ statusDot.style.animation = 'pulse 2s infinite';
400
+ } else {
401
+ statusDot.style.background = '#f85149';
402
+ statusDot.style.animation = 'none';
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Handle conversation state change
408
+ * @param {Object} state - New state
409
+ */
410
+ handleConversationStateChange(state) {
411
+ // Show notification for state changes
412
+ this.showNotification(`Conversation state changed`, 'info');
413
+ this.updateLastUpdateTime();
414
+ }
415
+
416
+ /**
417
+ * Update loading state
418
+ * @param {boolean} isLoading - Loading state
419
+ */
420
+ updateLoadingState(isLoading) {
421
+ const loadingOverlay = this.container.querySelector('#loading-overlay');
422
+ loadingOverlay.style.display = isLoading ? 'flex' : 'none';
423
+ }
424
+
425
+ /**
426
+ * Update error state
427
+ * @param {Error|string} error - Error object or message
428
+ */
429
+ updateErrorState(error) {
430
+ const errorBanner = this.container.querySelector('#error-banner');
431
+ const errorText = this.container.querySelector('.error-text');
432
+
433
+ if (error) {
434
+ errorText.textContent = error.message || error;
435
+ errorBanner.style.display = 'flex';
436
+ } else {
437
+ errorBanner.style.display = 'none';
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Clear error state
443
+ */
444
+ clearError() {
445
+ this.stateService.clearError();
446
+ }
447
+
448
+ /**
449
+ * Toggle notifications
450
+ */
451
+ toggleNotifications() {
452
+ const notificationsBtn = this.container.querySelector('#notifications-btn');
453
+ const isEnabled = notificationsBtn.classList.toggle('active');
454
+
455
+ if (isEnabled) {
456
+ this.requestNotificationPermission();
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Request notification permission
462
+ */
463
+ async requestNotificationPermission() {
464
+ if ('Notification' in window) {
465
+ const permission = await Notification.requestPermission();
466
+ if (permission === 'granted') {
467
+ this.showNotification('Notifications enabled', 'success');
468
+ }
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Show notification
474
+ * @param {string} message - Notification message
475
+ * @param {string} type - Notification type
476
+ */
477
+ showNotification(message, type = 'info') {
478
+ if ('Notification' in window && Notification.permission === 'granted') {
479
+ new Notification(`Claude Code Analytics`, {
480
+ body: message,
481
+ icon: '/favicon.ico'
482
+ });
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Export data
488
+ */
489
+ exportData() {
490
+ const conversations = this.stateService.getStateProperty('conversations');
491
+ const dataStr = JSON.stringify(conversations, null, 2);
492
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
493
+ const url = URL.createObjectURL(dataBlob);
494
+
495
+ const link = document.createElement('a');
496
+ link.href = url;
497
+ link.download = `claude-analytics-${new Date().toISOString().split('T')[0]}.json`;
498
+ link.click();
499
+
500
+ URL.revokeObjectURL(url);
501
+ }
502
+
503
+ /**
504
+ * Update chart period
505
+ * @param {string} period - New period
506
+ */
507
+ updateChartPeriod(period) {
508
+ // Update chart with new period data
509
+ console.log('Updating chart period to:', period);
510
+ // Implementation would update the chart with filtered data
511
+ }
512
+
513
+ /**
514
+ * Update last update time
515
+ */
516
+ updateLastUpdateTime() {
517
+ const lastUpdateText = this.container.querySelector('#last-update-text');
518
+ if (lastUpdateText) {
519
+ lastUpdateText.textContent = new Date().toLocaleTimeString();
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Start periodic refresh
525
+ */
526
+ startPeriodicRefresh() {
527
+ this.refreshInterval = setInterval(async () => {
528
+ try {
529
+ const statesData = await this.dataService.getConversationStates();
530
+ this.stateService.updateConversationStates(statesData);
531
+ this.updateLastUpdateTime();
532
+ } catch (error) {
533
+ console.error('Error during periodic refresh:', error);
534
+ }
535
+ }, 30000); // Refresh every 30 seconds
536
+ }
537
+
538
+ /**
539
+ * Stop periodic refresh
540
+ */
541
+ stopPeriodicRefresh() {
542
+ if (this.refreshInterval) {
543
+ clearInterval(this.refreshInterval);
544
+ this.refreshInterval = null;
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Cleanup and destroy dashboard
550
+ */
551
+ destroy() {
552
+ this.stopPeriodicRefresh();
553
+
554
+ // Cleanup components
555
+ Object.values(this.components).forEach(component => {
556
+ if (component.destroy) {
557
+ component.destroy();
558
+ }
559
+ });
560
+
561
+ // Unsubscribe from state changes
562
+ if (this.unsubscribe) {
563
+ this.unsubscribe();
564
+ }
565
+
566
+ this.isInitialized = false;
567
+ }
568
+ }
569
+
570
+ // Export for module use
571
+ if (typeof module !== 'undefined' && module.exports) {
572
+ module.exports = Dashboard;
573
+ }