claude-code-templates 1.8.0 → 1.8.1
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.
- package/README.md +246 -0
- package/package.json +26 -12
- package/src/analytics/core/ConversationAnalyzer.js +754 -0
- package/src/analytics/core/FileWatcher.js +285 -0
- package/src/analytics/core/ProcessDetector.js +242 -0
- package/src/analytics/core/SessionAnalyzer.js +597 -0
- package/src/analytics/core/StateCalculator.js +190 -0
- package/src/analytics/data/DataCache.js +550 -0
- package/src/analytics/notifications/NotificationManager.js +448 -0
- package/src/analytics/notifications/WebSocketServer.js +526 -0
- package/src/analytics/utils/PerformanceMonitor.js +455 -0
- package/src/analytics-web/assets/js/main.js +312 -0
- package/src/analytics-web/components/Charts.js +114 -0
- package/src/analytics-web/components/ConversationTable.js +437 -0
- package/src/analytics-web/components/Dashboard.js +573 -0
- package/src/analytics-web/components/SessionTimer.js +596 -0
- package/src/analytics-web/index.html +882 -49
- package/src/analytics-web/index.html.original +1939 -0
- package/src/analytics-web/services/DataService.js +357 -0
- package/src/analytics-web/services/StateService.js +276 -0
- package/src/analytics-web/services/WebSocketService.js +523 -0
- package/src/analytics.js +626 -2317
- package/src/analytics.log +0 -0
package/src/analytics.js
CHANGED
|
@@ -2,14 +2,36 @@ const chalk = require('chalk');
|
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const express = require('express');
|
|
5
|
-
const chokidar = require('chokidar');
|
|
6
5
|
const open = require('open');
|
|
7
6
|
const os = require('os');
|
|
7
|
+
const packageJson = require('../package.json');
|
|
8
|
+
const StateCalculator = require('./analytics/core/StateCalculator');
|
|
9
|
+
const ProcessDetector = require('./analytics/core/ProcessDetector');
|
|
10
|
+
const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
|
|
11
|
+
const FileWatcher = require('./analytics/core/FileWatcher');
|
|
12
|
+
const SessionAnalyzer = require('./analytics/core/SessionAnalyzer');
|
|
13
|
+
const DataCache = require('./analytics/data/DataCache');
|
|
14
|
+
const WebSocketServer = require('./analytics/notifications/WebSocketServer');
|
|
15
|
+
const NotificationManager = require('./analytics/notifications/NotificationManager');
|
|
16
|
+
const PerformanceMonitor = require('./analytics/utils/PerformanceMonitor');
|
|
8
17
|
|
|
9
18
|
class ClaudeAnalytics {
|
|
10
19
|
constructor() {
|
|
11
20
|
this.app = express();
|
|
12
21
|
this.port = 3333;
|
|
22
|
+
this.stateCalculator = new StateCalculator();
|
|
23
|
+
this.processDetector = new ProcessDetector();
|
|
24
|
+
this.fileWatcher = new FileWatcher();
|
|
25
|
+
this.sessionAnalyzer = new SessionAnalyzer();
|
|
26
|
+
this.dataCache = new DataCache();
|
|
27
|
+
this.performanceMonitor = new PerformanceMonitor({
|
|
28
|
+
enabled: true,
|
|
29
|
+
logInterval: 60000,
|
|
30
|
+
memoryThreshold: 300 * 1024 * 1024 // 300MB - more realistic for analytics dashboard
|
|
31
|
+
});
|
|
32
|
+
this.webSocketServer = null;
|
|
33
|
+
this.notificationManager = null;
|
|
34
|
+
this.httpServer = null;
|
|
13
35
|
this.data = {
|
|
14
36
|
conversations: [],
|
|
15
37
|
summary: {},
|
|
@@ -21,47 +43,54 @@ class ClaudeAnalytics {
|
|
|
21
43
|
lastActivity: null,
|
|
22
44
|
},
|
|
23
45
|
};
|
|
24
|
-
this.watchers = [];
|
|
25
46
|
}
|
|
26
47
|
|
|
27
48
|
async initialize() {
|
|
28
49
|
const homeDir = os.homedir();
|
|
29
50
|
this.claudeDir = path.join(homeDir, '.claude');
|
|
30
51
|
this.claudeDesktopDir = path.join(homeDir, 'Library', 'Application Support', 'Claude');
|
|
52
|
+
this.claudeStatsigDir = path.join(this.claudeDir, 'statsig');
|
|
31
53
|
|
|
32
54
|
// Check if Claude directories exist
|
|
33
55
|
if (!(await fs.pathExists(this.claudeDir))) {
|
|
34
56
|
throw new Error(`Claude Code directory not found at ${this.claudeDir}`);
|
|
35
57
|
}
|
|
36
58
|
|
|
59
|
+
// Initialize conversation analyzer with Claude directory and cache
|
|
60
|
+
this.conversationAnalyzer = new ConversationAnalyzer(this.claudeDir, this.dataCache);
|
|
61
|
+
|
|
37
62
|
await this.loadInitialData();
|
|
38
63
|
this.setupFileWatchers();
|
|
39
64
|
this.setupWebServer();
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
async loadInitialData() {
|
|
43
|
-
console.log(chalk.yellow('📊 Analyzing Claude Code data...'));
|
|
44
|
-
|
|
45
68
|
try {
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
// Store previous data for comparison
|
|
70
|
+
const previousData = this.data;
|
|
71
|
+
|
|
72
|
+
// Use ConversationAnalyzer to load and analyze all data
|
|
73
|
+
const analyzedData = await this.conversationAnalyzer.loadInitialData(
|
|
74
|
+
this.stateCalculator,
|
|
75
|
+
this.processDetector
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Update our data structure with analyzed data
|
|
79
|
+
this.data = analyzedData;
|
|
80
|
+
|
|
81
|
+
// Get Claude session information
|
|
82
|
+
const claudeSessionInfo = await this.getClaudeSessionInfo();
|
|
83
|
+
|
|
84
|
+
// Analyze session data for Max plan usage tracking with real Claude session info
|
|
85
|
+
this.data.sessionData = this.sessionAnalyzer.analyzeSessionData(this.data.conversations, claudeSessionInfo);
|
|
86
|
+
|
|
87
|
+
// Send real-time notifications if WebSocket is available
|
|
88
|
+
if (this.notificationManager) {
|
|
89
|
+
this.notificationManager.notifyDataRefresh(this.data, 'data_refresh');
|
|
90
|
+
|
|
91
|
+
// Check for conversation state changes
|
|
92
|
+
this.detectAndNotifyStateChanges(previousData, this.data);
|
|
93
|
+
}
|
|
65
94
|
|
|
66
95
|
} catch (error) {
|
|
67
96
|
console.error(chalk.red('Error loading Claude data:'), error.message);
|
|
@@ -69,102 +98,6 @@ class ClaudeAnalytics {
|
|
|
69
98
|
}
|
|
70
99
|
}
|
|
71
100
|
|
|
72
|
-
async loadConversations() {
|
|
73
|
-
const conversations = [];
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
// Search for .jsonl files recursively in all subdirectories
|
|
77
|
-
const findJsonlFiles = async (dir) => {
|
|
78
|
-
const files = [];
|
|
79
|
-
const items = await fs.readdir(dir);
|
|
80
|
-
|
|
81
|
-
for (const item of items) {
|
|
82
|
-
const itemPath = path.join(dir, item);
|
|
83
|
-
const stats = await fs.stat(itemPath);
|
|
84
|
-
|
|
85
|
-
if (stats.isDirectory()) {
|
|
86
|
-
// Recursively search subdirectories
|
|
87
|
-
const subFiles = await findJsonlFiles(itemPath);
|
|
88
|
-
files.push(...subFiles);
|
|
89
|
-
} else if (item.endsWith('.jsonl')) {
|
|
90
|
-
files.push(itemPath);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return files;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const jsonlFiles = await findJsonlFiles(this.claudeDir);
|
|
98
|
-
console.log(chalk.blue(`Found ${jsonlFiles.length} conversation files`));
|
|
99
|
-
|
|
100
|
-
for (const filePath of jsonlFiles) {
|
|
101
|
-
const stats = await fs.stat(filePath);
|
|
102
|
-
const filename = path.basename(filePath);
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
106
|
-
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
107
|
-
const messages = lines.map(line => {
|
|
108
|
-
try {
|
|
109
|
-
return JSON.parse(line);
|
|
110
|
-
} catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}).filter(Boolean);
|
|
114
|
-
|
|
115
|
-
// Extract project name from path
|
|
116
|
-
const projectFromPath = this.extractProjectFromPath(filePath);
|
|
117
|
-
|
|
118
|
-
// Parse messages to get their content for status determination
|
|
119
|
-
const parsedMessages = lines.map(line => {
|
|
120
|
-
try {
|
|
121
|
-
const item = JSON.parse(line);
|
|
122
|
-
if (item.message && item.message.role) {
|
|
123
|
-
return {
|
|
124
|
-
role: item.message.role,
|
|
125
|
-
timestamp: new Date(item.timestamp),
|
|
126
|
-
content: item.message.content,
|
|
127
|
-
model: item.message.model || null,
|
|
128
|
-
usage: item.message.usage || null,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
} catch {}
|
|
132
|
-
return null;
|
|
133
|
-
}).filter(Boolean);
|
|
134
|
-
|
|
135
|
-
// Calculate real token usage and extract model info
|
|
136
|
-
const tokenUsage = this.calculateRealTokenUsage(parsedMessages);
|
|
137
|
-
const modelInfo = this.extractModelInfo(parsedMessages);
|
|
138
|
-
|
|
139
|
-
const conversation = {
|
|
140
|
-
id: filename.replace('.jsonl', ''),
|
|
141
|
-
filename: filename,
|
|
142
|
-
filePath: filePath,
|
|
143
|
-
messageCount: parsedMessages.length,
|
|
144
|
-
fileSize: stats.size,
|
|
145
|
-
lastModified: stats.mtime,
|
|
146
|
-
created: stats.birthtime,
|
|
147
|
-
tokens: tokenUsage.total > 0 ? tokenUsage.total : this.estimateTokens(content),
|
|
148
|
-
tokenUsage: tokenUsage,
|
|
149
|
-
modelInfo: modelInfo,
|
|
150
|
-
project: projectFromPath || this.extractProjectFromConversation(parsedMessages),
|
|
151
|
-
status: this.determineConversationStatus(parsedMessages, stats.mtime),
|
|
152
|
-
conversationState: this.determineConversationState(parsedMessages, stats.mtime),
|
|
153
|
-
statusSquares: this.generateStatusSquares(parsedMessages),
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
conversations.push(conversation);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.warn(chalk.yellow(`Warning: Could not parse ${filename}:`, error.message));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return conversations.sort((a, b) => b.lastModified - a.lastModified);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
console.error(chalk.red('Error loading conversations:'), error.message);
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
101
|
|
|
169
102
|
async loadActiveProjects() {
|
|
170
103
|
const projects = [];
|
|
@@ -231,7 +164,7 @@ class ClaudeAnalytics {
|
|
|
231
164
|
});
|
|
232
165
|
|
|
233
166
|
return {
|
|
234
|
-
total: totalInputTokens + totalOutputTokens,
|
|
167
|
+
total: totalInputTokens + totalOutputTokens + totalCacheCreationTokens + totalCacheReadTokens,
|
|
235
168
|
inputTokens: totalInputTokens,
|
|
236
169
|
outputTokens: totalOutputTokens,
|
|
237
170
|
cacheCreationTokens: totalCacheCreationTokens,
|
|
@@ -241,6 +174,42 @@ class ClaudeAnalytics {
|
|
|
241
174
|
};
|
|
242
175
|
}
|
|
243
176
|
|
|
177
|
+
calculateDetailedTokenUsage() {
|
|
178
|
+
if (!this.data || !this.data.conversations) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let totalInputTokens = 0;
|
|
183
|
+
let totalOutputTokens = 0;
|
|
184
|
+
let totalCacheCreationTokens = 0;
|
|
185
|
+
let totalCacheReadTokens = 0;
|
|
186
|
+
let totalMessages = 0;
|
|
187
|
+
let messagesWithUsage = 0;
|
|
188
|
+
|
|
189
|
+
this.data.conversations.forEach(conversation => {
|
|
190
|
+
if (conversation.tokenUsage) {
|
|
191
|
+
totalInputTokens += conversation.tokenUsage.inputTokens || 0;
|
|
192
|
+
totalOutputTokens += conversation.tokenUsage.outputTokens || 0;
|
|
193
|
+
totalCacheCreationTokens += conversation.tokenUsage.cacheCreationTokens || 0;
|
|
194
|
+
totalCacheReadTokens += conversation.tokenUsage.cacheReadTokens || 0;
|
|
195
|
+
messagesWithUsage += conversation.tokenUsage.messagesWithUsage || 0;
|
|
196
|
+
totalMessages += conversation.tokenUsage.totalMessages || 0;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const total = totalInputTokens + totalOutputTokens + totalCacheCreationTokens + totalCacheReadTokens;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
total,
|
|
204
|
+
inputTokens: totalInputTokens,
|
|
205
|
+
outputTokens: totalOutputTokens,
|
|
206
|
+
cacheCreationTokens: totalCacheCreationTokens,
|
|
207
|
+
cacheReadTokens: totalCacheReadTokens,
|
|
208
|
+
messagesWithUsage,
|
|
209
|
+
totalMessages
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
244
213
|
extractModelInfo(parsedMessages) {
|
|
245
214
|
const models = new Set();
|
|
246
215
|
const serviceTiers = new Set();
|
|
@@ -288,111 +257,8 @@ class ClaudeAnalytics {
|
|
|
288
257
|
return null;
|
|
289
258
|
}
|
|
290
259
|
|
|
291
|
-
// NEW: Function to detect active Claude processes
|
|
292
|
-
async detectRunningClaudeProcesses() {
|
|
293
|
-
const { exec } = require('child_process');
|
|
294
|
-
|
|
295
|
-
return new Promise((resolve) => {
|
|
296
|
-
// Search for processes containing 'claude' but exclude our own analytics process and system processes
|
|
297
|
-
exec('ps aux | grep -i claude | grep -v grep | grep -v analytics | grep -v "/Applications/Claude.app" | grep -v "npm start"', (error, stdout) => {
|
|
298
|
-
if (error) {
|
|
299
|
-
resolve([]);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const processes = stdout.split('\n')
|
|
304
|
-
.filter(line => line.trim())
|
|
305
|
-
.filter(line => {
|
|
306
|
-
// Only include actual Claude CLI processes, not system processes
|
|
307
|
-
const fullCommand = line.split(/\s+/).slice(10).join(' ');
|
|
308
|
-
return fullCommand.includes('claude') &&
|
|
309
|
-
!fullCommand.includes('chrome_crashpad_handler') &&
|
|
310
|
-
!fullCommand.includes('create-claude-config') &&
|
|
311
|
-
!fullCommand.includes('node bin/') &&
|
|
312
|
-
fullCommand.trim() === 'claude'; // Only the basic claude command
|
|
313
|
-
})
|
|
314
|
-
.map(line => {
|
|
315
|
-
const parts = line.split(/\s+/);
|
|
316
|
-
const fullCommand = parts.slice(10).join(' ');
|
|
317
|
-
|
|
318
|
-
// Extract useful information from command
|
|
319
|
-
const cwdMatch = fullCommand.match(/--cwd[=\s]+([^\s]+)/);
|
|
320
|
-
const workingDir = cwdMatch ? cwdMatch[1] : 'unknown';
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
pid: parts[1],
|
|
324
|
-
command: fullCommand,
|
|
325
|
-
workingDir: workingDir,
|
|
326
|
-
startTime: new Date(), // For now we use current time
|
|
327
|
-
status: 'running',
|
|
328
|
-
user: parts[0]
|
|
329
|
-
};
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
resolve(processes);
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
260
|
|
|
337
|
-
|
|
338
|
-
async enrichWithRunningProcesses() {
|
|
339
|
-
try {
|
|
340
|
-
const runningProcesses = await this.detectRunningClaudeProcesses();
|
|
341
|
-
|
|
342
|
-
// Add active process information to each conversation
|
|
343
|
-
for (const conversation of this.data.conversations) {
|
|
344
|
-
// Look for active process for this project
|
|
345
|
-
const matchingProcess = runningProcesses.find(process =>
|
|
346
|
-
process.workingDir.includes(conversation.project) ||
|
|
347
|
-
process.command.includes(conversation.project)
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
if (matchingProcess) {
|
|
351
|
-
// ENRICH without changing existing logic
|
|
352
|
-
conversation.runningProcess = {
|
|
353
|
-
pid: matchingProcess.pid,
|
|
354
|
-
startTime: matchingProcess.startTime,
|
|
355
|
-
workingDir: matchingProcess.workingDir,
|
|
356
|
-
hasActiveCommand: true
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
// Only change status if not already marked as active by existing logic
|
|
360
|
-
if (conversation.status !== 'active') {
|
|
361
|
-
conversation.status = 'active';
|
|
362
|
-
conversation.statusReason = 'running_process';
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Recalculate conversation state with process information
|
|
366
|
-
const conversationFile = path.join(this.claudeDir, conversation.fileName);
|
|
367
|
-
try {
|
|
368
|
-
const content = await fs.readFile(conversationFile, 'utf8');
|
|
369
|
-
const parsedMessages = content.split('\n')
|
|
370
|
-
.filter(line => line.trim())
|
|
371
|
-
.map(line => JSON.parse(line));
|
|
372
|
-
|
|
373
|
-
const stats = await fs.stat(conversationFile);
|
|
374
|
-
conversation.conversationState = this.determineConversationState(
|
|
375
|
-
parsedMessages,
|
|
376
|
-
stats.mtime,
|
|
377
|
-
conversation.runningProcess
|
|
378
|
-
);
|
|
379
|
-
} catch (error) {
|
|
380
|
-
// If we can't read the file, keep the existing state
|
|
381
|
-
}
|
|
382
|
-
} else {
|
|
383
|
-
conversation.runningProcess = null;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Disable orphan process detection to reduce noise
|
|
388
|
-
this.data.orphanProcesses = [];
|
|
389
|
-
|
|
390
|
-
console.log(chalk.blue(`🔍 Found ${runningProcesses.length} running Claude processes`));
|
|
391
|
-
|
|
392
|
-
} catch (error) {
|
|
393
|
-
console.warn(chalk.yellow('Warning: Could not detect running processes'), error.message);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
261
|
+
|
|
396
262
|
|
|
397
263
|
extractProjectFromConversation(messages) {
|
|
398
264
|
// Try to extract project information from conversation
|
|
@@ -407,100 +273,7 @@ class ClaudeAnalytics {
|
|
|
407
273
|
return 'Unknown';
|
|
408
274
|
}
|
|
409
275
|
|
|
410
|
-
determineConversationStatus(messages, lastModified) {
|
|
411
|
-
const now = new Date();
|
|
412
|
-
const timeDiff = now - lastModified;
|
|
413
|
-
const minutesAgo = timeDiff / (1000 * 60);
|
|
414
|
-
|
|
415
|
-
if (messages.length === 0) {
|
|
416
|
-
return minutesAgo < 5 ? 'active' : 'inactive';
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Sort messages by timestamp to get the actual conversation flow
|
|
420
|
-
const sortedMessages = messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
421
|
-
const lastMessage = sortedMessages[sortedMessages.length - 1];
|
|
422
|
-
const lastMessageTime = new Date(lastMessage.timestamp);
|
|
423
|
-
const lastMessageMinutesAgo = (now - lastMessageTime) / (1000 * 60);
|
|
424
|
-
|
|
425
|
-
// More balanced logic - active conversations and recent activity
|
|
426
|
-
if (lastMessage.role === 'user' && lastMessageMinutesAgo < 3) {
|
|
427
|
-
return 'active';
|
|
428
|
-
} else if (lastMessage.role === 'assistant' && lastMessageMinutesAgo < 5) {
|
|
429
|
-
return 'active';
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Use file modification time for recent activity
|
|
433
|
-
if (minutesAgo < 5) return 'active';
|
|
434
|
-
if (minutesAgo < 30) return 'recent';
|
|
435
|
-
return 'inactive';
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
determineConversationState(messages, lastModified, runningProcess = null) {
|
|
439
|
-
const now = new Date();
|
|
440
|
-
const timeDiff = now - lastModified;
|
|
441
|
-
const minutesAgo = timeDiff / (1000 * 60);
|
|
442
|
-
|
|
443
|
-
// If there's an active process, use simpler and more responsive logic
|
|
444
|
-
if (runningProcess && runningProcess.hasActiveCommand) {
|
|
445
|
-
const fileTimeDiff = (now - lastModified) / 1000; // seconds
|
|
446
|
-
|
|
447
|
-
// Very recent file activity = Claude working
|
|
448
|
-
if (fileTimeDiff < 10) {
|
|
449
|
-
return 'Claude Code working...';
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Check conversation flow if we have messages
|
|
453
|
-
if (messages.length > 0) {
|
|
454
|
-
const sortedMessages = messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
455
|
-
const lastMessage = sortedMessages[sortedMessages.length - 1];
|
|
456
|
-
|
|
457
|
-
if (lastMessage.role === 'assistant') {
|
|
458
|
-
// Claude responded, user should be typing or thinking
|
|
459
|
-
return 'User typing...';
|
|
460
|
-
} else if (lastMessage.role === 'user') {
|
|
461
|
-
// User sent message, Claude should be working
|
|
462
|
-
return 'Claude Code working...';
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Fallback for active process
|
|
467
|
-
return 'Awaiting user input...';
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (messages.length === 0) {
|
|
471
|
-
return minutesAgo < 5 ? 'Waiting for input...' : 'Idle';
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Sort messages by timestamp to get the actual conversation flow
|
|
475
|
-
const sortedMessages = messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
476
|
-
const lastMessage = sortedMessages[sortedMessages.length - 1];
|
|
477
|
-
const lastMessageTime = new Date(lastMessage.timestamp);
|
|
478
|
-
const lastMessageMinutesAgo = (now - lastMessageTime) / (1000 * 60);
|
|
479
|
-
|
|
480
|
-
// Detailed conversation state logic
|
|
481
|
-
if (lastMessage.role === 'user') {
|
|
482
|
-
// User sent last message
|
|
483
|
-
if (lastMessageMinutesAgo < 0.5) {
|
|
484
|
-
return 'Claude Code working...';
|
|
485
|
-
} else if (lastMessageMinutesAgo < 3) {
|
|
486
|
-
return 'Awaiting response...';
|
|
487
|
-
} else {
|
|
488
|
-
return 'User typing...';
|
|
489
|
-
}
|
|
490
|
-
} else if (lastMessage.role === 'assistant') {
|
|
491
|
-
// Assistant sent last message
|
|
492
|
-
if (lastMessageMinutesAgo < 2) {
|
|
493
|
-
return 'Awaiting user input...';
|
|
494
|
-
} else if (lastMessageMinutesAgo < 5) {
|
|
495
|
-
return 'User may be typing...';
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
276
|
|
|
499
|
-
// Fallback states
|
|
500
|
-
if (minutesAgo < 5) return 'Recently active';
|
|
501
|
-
if (minutesAgo < 60) return 'Idle';
|
|
502
|
-
return 'Inactive';
|
|
503
|
-
}
|
|
504
277
|
|
|
505
278
|
generateStatusSquares(messages) {
|
|
506
279
|
if (!messages || messages.length === 0) {
|
|
@@ -722,76 +495,58 @@ class ClaudeAnalytics {
|
|
|
722
495
|
}
|
|
723
496
|
|
|
724
497
|
setupFileWatchers() {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
this.watchers.push(conversationWatcher);
|
|
748
|
-
|
|
749
|
-
// Watch project directories
|
|
750
|
-
const projectWatcher = chokidar.watch(this.claudeDir, {
|
|
751
|
-
persistent: true,
|
|
752
|
-
ignoreInitial: true,
|
|
753
|
-
depth: 2, // Increased depth to catch subdirectories
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
projectWatcher.on('addDir', async () => {
|
|
757
|
-
console.log(chalk.yellow('📁 New project directory detected...'));
|
|
758
|
-
await this.loadInitialData();
|
|
759
|
-
console.log(chalk.green('✅ Data updated'));
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
projectWatcher.on('change', async () => {
|
|
763
|
-
console.log(chalk.yellow('📁 Project directory changed...'));
|
|
764
|
-
await this.loadInitialData();
|
|
765
|
-
console.log(chalk.green('✅ Data updated'));
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
this.watchers.push(projectWatcher);
|
|
769
|
-
|
|
770
|
-
// Also set up periodic refresh to catch any missed changes
|
|
771
|
-
setInterval(async () => {
|
|
772
|
-
console.log(chalk.blue('⏱️ Periodic data refresh...'));
|
|
773
|
-
await this.loadInitialData();
|
|
774
|
-
}, 30000); // Every 30 seconds
|
|
775
|
-
|
|
776
|
-
// NEW: More frequent updates for active processes (every 10 seconds)
|
|
777
|
-
setInterval(async () => {
|
|
778
|
-
await this.enrichWithRunningProcesses();
|
|
779
|
-
}, 10000);
|
|
498
|
+
// Setup file watchers using the FileWatcher module
|
|
499
|
+
this.fileWatcher.setupFileWatchers(
|
|
500
|
+
this.claudeDir,
|
|
501
|
+
// Data refresh callback
|
|
502
|
+
async () => {
|
|
503
|
+
await this.loadInitialData();
|
|
504
|
+
},
|
|
505
|
+
// Process refresh callback
|
|
506
|
+
async () => {
|
|
507
|
+
const enrichmentResult = await this.processDetector.enrichWithRunningProcesses(
|
|
508
|
+
this.data.conversations,
|
|
509
|
+
this.claudeDir,
|
|
510
|
+
this.stateCalculator
|
|
511
|
+
);
|
|
512
|
+
this.data.conversations = enrichmentResult.conversations;
|
|
513
|
+
this.data.orphanProcesses = enrichmentResult.orphanProcesses;
|
|
514
|
+
},
|
|
515
|
+
// DataCache for cache invalidation
|
|
516
|
+
this.dataCache
|
|
517
|
+
);
|
|
780
518
|
}
|
|
781
519
|
|
|
782
520
|
setupWebServer() {
|
|
521
|
+
// Add performance monitoring middleware
|
|
522
|
+
this.app.use(this.performanceMonitor.createExpressMiddleware());
|
|
523
|
+
|
|
783
524
|
// Serve static files (we'll create the dashboard HTML)
|
|
784
525
|
this.app.use(express.static(path.join(__dirname, 'analytics-web')));
|
|
785
526
|
|
|
786
527
|
// API endpoints
|
|
787
528
|
this.app.get('/api/data', async (req, res) => {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
529
|
+
try {
|
|
530
|
+
// Calculate detailed token usage
|
|
531
|
+
const detailedTokenUsage = this.calculateDetailedTokenUsage();
|
|
532
|
+
|
|
533
|
+
// Add timestamp to verify data freshness
|
|
534
|
+
const dataWithTimestamp = {
|
|
535
|
+
...this.data,
|
|
536
|
+
detailedTokenUsage,
|
|
537
|
+
timestamp: new Date().toISOString(),
|
|
538
|
+
lastUpdate: new Date().toLocaleString(),
|
|
539
|
+
};
|
|
540
|
+
res.json(dataWithTimestamp);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error('Error calculating detailed token usage:', error);
|
|
543
|
+
res.json({
|
|
544
|
+
...this.data,
|
|
545
|
+
detailedTokenUsage: null,
|
|
546
|
+
timestamp: new Date().toISOString(),
|
|
547
|
+
lastUpdate: new Date().toLocaleString(),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
795
550
|
});
|
|
796
551
|
|
|
797
552
|
this.app.get('/api/realtime', async (req, res) => {
|
|
@@ -814,25 +569,98 @@ class ClaudeAnalytics {
|
|
|
814
569
|
});
|
|
815
570
|
});
|
|
816
571
|
|
|
817
|
-
//
|
|
818
|
-
this.app.get('/api/
|
|
819
|
-
|
|
572
|
+
// NEW: Ultra-fast endpoint ONLY for conversation states
|
|
573
|
+
this.app.get('/api/conversation-state', async (req, res) => {
|
|
574
|
+
try {
|
|
575
|
+
// Only detect processes and calculate states - no file reading
|
|
576
|
+
const runningProcesses = await this.processDetector.detectRunningClaudeProcesses();
|
|
577
|
+
const activeStates = [];
|
|
578
|
+
|
|
579
|
+
// Quick state calculation for active conversations only
|
|
580
|
+
for (const conversation of this.data.conversations) {
|
|
581
|
+
if (conversation.runningProcess) {
|
|
582
|
+
// Use existing state calculation but faster
|
|
583
|
+
const state = this.stateCalculator.quickStateCalculation(conversation, runningProcesses);
|
|
584
|
+
if (state) {
|
|
585
|
+
activeStates.push({
|
|
586
|
+
id: conversation.id,
|
|
587
|
+
project: conversation.project,
|
|
588
|
+
state: state,
|
|
589
|
+
timestamp: Date.now()
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
res.json({ activeStates, timestamp: Date.now() });
|
|
596
|
+
} catch (error) {
|
|
597
|
+
res.status(500).json({ error: 'Failed to get conversation states' });
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Session data endpoint for Max plan usage tracking
|
|
602
|
+
this.app.get('/api/session/data', async (req, res) => {
|
|
603
|
+
try {
|
|
604
|
+
// Get real-time Claude session information
|
|
605
|
+
const claudeSessionInfo = await this.getClaudeSessionInfo();
|
|
606
|
+
|
|
607
|
+
if (!this.data.sessionData) {
|
|
608
|
+
// Generate session data if not available
|
|
609
|
+
this.data.sessionData = this.sessionAnalyzer.analyzeSessionData(this.data.conversations, claudeSessionInfo);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const timerData = this.sessionAnalyzer.getSessionTimerData(this.data.sessionData);
|
|
613
|
+
|
|
614
|
+
res.json({
|
|
615
|
+
...this.data.sessionData,
|
|
616
|
+
timer: timerData,
|
|
617
|
+
claudeSessionInfo: claudeSessionInfo,
|
|
618
|
+
timestamp: Date.now()
|
|
619
|
+
});
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.error('Session data error:', error);
|
|
622
|
+
res.status(500).json({
|
|
623
|
+
error: 'Failed to get session data',
|
|
624
|
+
timestamp: Date.now()
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
});
|
|
820
628
|
|
|
629
|
+
// Get specific conversation history
|
|
630
|
+
this.app.get('/api/session/:id', async (req, res) => {
|
|
821
631
|
try {
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
632
|
+
const conversationId = req.params.id;
|
|
633
|
+
|
|
634
|
+
// Find the conversation
|
|
635
|
+
const conversation = this.data.conversations.find(conv => conv.id === conversationId);
|
|
636
|
+
if (!conversation) {
|
|
637
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Read the conversation file to get full message history
|
|
641
|
+
const conversationFile = conversation.filePath;
|
|
642
|
+
|
|
643
|
+
if (!conversationFile) {
|
|
644
|
+
return res.status(404).json({
|
|
645
|
+
error: 'Conversation file path not found',
|
|
646
|
+
conversationId: conversationId,
|
|
647
|
+
conversationKeys: Object.keys(conversation),
|
|
648
|
+
hasFilePath: !!conversation.filePath,
|
|
649
|
+
hasFileName: !!conversation.filename
|
|
826
650
|
});
|
|
827
651
|
}
|
|
652
|
+
|
|
653
|
+
if (!await fs.pathExists(conversationFile)) {
|
|
654
|
+
return res.status(404).json({ error: 'Conversation file not found', path: conversationFile });
|
|
655
|
+
}
|
|
828
656
|
|
|
829
|
-
|
|
830
|
-
const content = await fs.readFile(session.filePath, 'utf8');
|
|
657
|
+
const content = await fs.readFile(conversationFile, 'utf8');
|
|
831
658
|
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
832
659
|
const rawMessages = lines.map(line => {
|
|
833
660
|
try {
|
|
834
661
|
return JSON.parse(line);
|
|
835
|
-
} catch {
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.warn('Error parsing message line:', error);
|
|
836
664
|
return null;
|
|
837
665
|
}
|
|
838
666
|
}).filter(Boolean);
|
|
@@ -853,7 +681,7 @@ class ClaudeAnalytics {
|
|
|
853
681
|
return block.content || '';
|
|
854
682
|
})
|
|
855
683
|
.join('\n');
|
|
856
|
-
} else if (item.message.content && item.message.content.length) {
|
|
684
|
+
} else if (item.message.content && typeof item.message.content === 'object' && item.message.content.length) {
|
|
857
685
|
content = item.message.content[0].text || '';
|
|
858
686
|
}
|
|
859
687
|
|
|
@@ -862,61 +690,432 @@ class ClaudeAnalytics {
|
|
|
862
690
|
content: content || 'No content',
|
|
863
691
|
timestamp: item.timestamp,
|
|
864
692
|
type: item.type,
|
|
693
|
+
stop_reason: item.message.stop_reason || null,
|
|
694
|
+
message_id: item.message.id || null,
|
|
695
|
+
model: item.message.model || null,
|
|
696
|
+
usage: item.message.usage || null,
|
|
697
|
+
hasToolUse: item.message.content && Array.isArray(item.message.content) &&
|
|
698
|
+
item.message.content.some(block => block.type === 'tool_use'),
|
|
699
|
+
hasToolResult: item.message.content && Array.isArray(item.message.content) &&
|
|
700
|
+
item.message.content.some(block => block.type === 'tool_result'),
|
|
701
|
+
contentBlocks: item.message.content && Array.isArray(item.message.content) ?
|
|
702
|
+
item.message.content.map(block => ({ type: block.type, name: block.name || null })) : [],
|
|
703
|
+
rawContent: item.message.content || null,
|
|
704
|
+
parentUuid: item.parentUuid || null,
|
|
705
|
+
uuid: item.uuid || null,
|
|
706
|
+
sessionId: item.sessionId || null,
|
|
707
|
+
userType: item.userType || null,
|
|
708
|
+
cwd: item.cwd || null,
|
|
709
|
+
version: item.version || null,
|
|
710
|
+
isCompactSummary: item.isCompactSummary || false,
|
|
711
|
+
isSidechain: item.isSidechain || false
|
|
865
712
|
};
|
|
866
713
|
}
|
|
867
714
|
return null;
|
|
868
715
|
}).filter(Boolean);
|
|
869
716
|
|
|
717
|
+
|
|
870
718
|
res.json({
|
|
871
|
-
|
|
719
|
+
conversation: {
|
|
720
|
+
id: conversation.id,
|
|
721
|
+
project: conversation.project,
|
|
722
|
+
messageCount: conversation.messageCount,
|
|
723
|
+
tokens: conversation.tokens,
|
|
724
|
+
created: conversation.created,
|
|
725
|
+
lastModified: conversation.lastModified,
|
|
726
|
+
status: conversation.status
|
|
727
|
+
},
|
|
872
728
|
messages: messages,
|
|
873
|
-
timestamp:
|
|
729
|
+
timestamp: Date.now()
|
|
874
730
|
});
|
|
875
731
|
|
|
876
732
|
} catch (error) {
|
|
877
|
-
console.error(
|
|
878
|
-
|
|
879
|
-
|
|
733
|
+
console.error('Error getting conversation history:', error);
|
|
734
|
+
console.error('Error stack:', error.stack);
|
|
735
|
+
res.status(500).json({
|
|
736
|
+
error: 'Failed to load conversation history',
|
|
737
|
+
details: error.message,
|
|
738
|
+
stack: error.stack
|
|
880
739
|
});
|
|
881
740
|
}
|
|
882
741
|
});
|
|
883
742
|
|
|
884
|
-
//
|
|
885
|
-
this.app.get('/', (req, res) => {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
743
|
+
// Fast state update endpoint - only updates conversation states without full reload
|
|
744
|
+
this.app.get('/api/fast-update', async (req, res) => {
|
|
745
|
+
try {
|
|
746
|
+
// Update process information and conversation states
|
|
747
|
+
const enrichmentResult = await this.processDetector.enrichWithRunningProcesses(
|
|
748
|
+
this.data.conversations,
|
|
749
|
+
this.claudeDir,
|
|
750
|
+
this.stateCalculator
|
|
751
|
+
);
|
|
752
|
+
this.data.conversations = enrichmentResult.conversations;
|
|
753
|
+
this.data.orphanProcesses = enrichmentResult.orphanProcesses;
|
|
754
|
+
|
|
755
|
+
// For active conversations, re-read the files to get latest messages
|
|
756
|
+
const activeConversations = this.data.conversations.filter(c => c.runningProcess);
|
|
757
|
+
|
|
758
|
+
for (const conv of activeConversations) {
|
|
759
|
+
try {
|
|
760
|
+
const conversationFile = path.join(this.claudeDir, conv.fileName);
|
|
761
|
+
const content = await fs.readFile(conversationFile, 'utf8');
|
|
762
|
+
const parsedMessages = content.split('\n')
|
|
763
|
+
.filter(line => line.trim())
|
|
764
|
+
.map(line => JSON.parse(line));
|
|
765
|
+
|
|
766
|
+
const stats = await fs.stat(conversationFile);
|
|
767
|
+
conv.conversationState = this.stateCalculator.determineConversationState(
|
|
768
|
+
parsedMessages,
|
|
769
|
+
stats.mtime,
|
|
770
|
+
conv.runningProcess
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
} catch (error) {
|
|
774
|
+
// If we can't read the file, keep the existing state
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Only log when there are actually active conversations (reduce noise)
|
|
779
|
+
const activeConvs = this.data.conversations.filter(c => c.runningProcess);
|
|
780
|
+
if (activeConvs.length > 0) {
|
|
781
|
+
// Only log every 10th update to reduce spam, or when states change
|
|
782
|
+
if (!this.lastLoggedStates) this.lastLoggedStates = new Map();
|
|
783
|
+
|
|
784
|
+
let hasChanges = false;
|
|
785
|
+
activeConvs.forEach(conv => {
|
|
786
|
+
const lastState = this.lastLoggedStates.get(conv.id);
|
|
787
|
+
if (lastState !== conv.conversationState) {
|
|
788
|
+
hasChanges = true;
|
|
789
|
+
this.lastLoggedStates.set(conv.id, conv.conversationState);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
if (hasChanges) {
|
|
794
|
+
console.log(chalk.gray(`⚡ State update: ${activeConvs.length} active conversations`));
|
|
795
|
+
activeConvs.forEach(conv => {
|
|
796
|
+
console.log(chalk.gray(` 📊 ${conv.project}: ${conv.conversationState}`));
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const dataWithTimestamp = {
|
|
802
|
+
conversations: this.data.conversations,
|
|
803
|
+
summary: this.data.summary,
|
|
804
|
+
timestamp: new Date().toISOString(),
|
|
805
|
+
lastUpdate: new Date().toLocaleString(),
|
|
806
|
+
};
|
|
807
|
+
res.json(dataWithTimestamp);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
console.error('Fast update error:', error);
|
|
810
|
+
res.status(500).json({ error: 'Failed to update states' });
|
|
811
|
+
}
|
|
896
812
|
});
|
|
897
|
-
}
|
|
898
813
|
|
|
899
|
-
|
|
900
|
-
try {
|
|
901
|
-
await open(`http://localhost:${this.port}`);
|
|
902
|
-
console.log(chalk.blue('🌐 Opening browser...'));
|
|
903
|
-
} catch (error) {
|
|
904
|
-
console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
|
|
905
|
-
console.log(chalk.cyan(`http://localhost:${this.port}`));
|
|
906
|
-
}
|
|
907
|
-
}
|
|
814
|
+
// Remove duplicate endpoint - this conflicts with the correct one above
|
|
908
815
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
816
|
+
// System health endpoint
|
|
817
|
+
this.app.get('/api/system/health', (req, res) => {
|
|
818
|
+
try {
|
|
819
|
+
const stats = this.performanceMonitor.getStats();
|
|
820
|
+
const systemHealth = {
|
|
821
|
+
status: 'healthy',
|
|
822
|
+
uptime: stats.uptime,
|
|
823
|
+
memory: stats.memory,
|
|
824
|
+
requests: stats.requests,
|
|
825
|
+
cache: {
|
|
826
|
+
...stats.cache,
|
|
827
|
+
dataCache: this.dataCache.getStats()
|
|
828
|
+
},
|
|
829
|
+
errors: stats.errors,
|
|
830
|
+
counters: stats.counters,
|
|
831
|
+
timestamp: Date.now()
|
|
832
|
+
};
|
|
912
833
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
834
|
+
// Determine overall health status
|
|
835
|
+
if (stats.errors.total > 10) {
|
|
836
|
+
systemHealth.status = 'degraded';
|
|
837
|
+
}
|
|
838
|
+
if (stats.memory.current && stats.memory.current.heapUsed > this.performanceMonitor.options.memoryThreshold) {
|
|
839
|
+
systemHealth.status = 'warning';
|
|
840
|
+
}
|
|
917
841
|
|
|
918
|
-
|
|
919
|
-
|
|
842
|
+
res.json(systemHealth);
|
|
843
|
+
} catch (error) {
|
|
844
|
+
res.status(500).json({
|
|
845
|
+
status: 'error',
|
|
846
|
+
message: 'Failed to get system health',
|
|
847
|
+
timestamp: Date.now()
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Version endpoint
|
|
853
|
+
this.app.get('/api/version', (req, res) => {
|
|
854
|
+
res.json({
|
|
855
|
+
version: packageJson.version,
|
|
856
|
+
name: packageJson.name,
|
|
857
|
+
description: packageJson.description,
|
|
858
|
+
timestamp: Date.now()
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// Claude session information endpoint
|
|
863
|
+
this.app.get('/api/claude/session', async (req, res) => {
|
|
864
|
+
try {
|
|
865
|
+
const sessionInfo = await this.getClaudeSessionInfo();
|
|
866
|
+
res.json({
|
|
867
|
+
...sessionInfo,
|
|
868
|
+
timestamp: Date.now()
|
|
869
|
+
});
|
|
870
|
+
} catch (error) {
|
|
871
|
+
console.error('Error getting Claude session info:', error);
|
|
872
|
+
res.status(500).json({
|
|
873
|
+
error: 'Failed to get Claude session info',
|
|
874
|
+
timestamp: Date.now()
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Performance metrics endpoint
|
|
880
|
+
this.app.get('/api/system/metrics', (req, res) => {
|
|
881
|
+
try {
|
|
882
|
+
const timeframe = parseInt(req.query.timeframe) || 300000; // 5 minutes default
|
|
883
|
+
const stats = this.performanceMonitor.getStats(timeframe);
|
|
884
|
+
res.json({
|
|
885
|
+
...stats,
|
|
886
|
+
dataCache: this.dataCache.getStats(),
|
|
887
|
+
timestamp: Date.now()
|
|
888
|
+
});
|
|
889
|
+
} catch (error) {
|
|
890
|
+
res.status(500).json({
|
|
891
|
+
error: 'Failed to get performance metrics',
|
|
892
|
+
timestamp: Date.now()
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Main dashboard route
|
|
898
|
+
this.app.get('/', (req, res) => {
|
|
899
|
+
res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async startServer() {
|
|
904
|
+
return new Promise(async (resolve) => {
|
|
905
|
+
this.httpServer = this.app.listen(this.port, async () => {
|
|
906
|
+
console.log(chalk.green(`🚀 Analytics dashboard started at http://localhost:${this.port}`));
|
|
907
|
+
|
|
908
|
+
// Initialize WebSocket server
|
|
909
|
+
await this.initializeWebSocket();
|
|
910
|
+
|
|
911
|
+
resolve();
|
|
912
|
+
});
|
|
913
|
+
// Keep reference for compatibility
|
|
914
|
+
this.server = this.httpServer;
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async openBrowser() {
|
|
919
|
+
try {
|
|
920
|
+
await open(`http://localhost:${this.port}`);
|
|
921
|
+
console.log(chalk.blue('🌐 Opening browser...'));
|
|
922
|
+
} catch (error) {
|
|
923
|
+
console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
|
|
924
|
+
console.log(chalk.cyan(`http://localhost:${this.port}`));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Initialize WebSocket server and notification manager
|
|
930
|
+
*/
|
|
931
|
+
async initializeWebSocket() {
|
|
932
|
+
try {
|
|
933
|
+
// Initialize WebSocket server with performance monitoring
|
|
934
|
+
this.webSocketServer = new WebSocketServer(this.httpServer, {
|
|
935
|
+
path: '/ws',
|
|
936
|
+
heartbeatInterval: 30000
|
|
937
|
+
}, this.performanceMonitor);
|
|
938
|
+
await this.webSocketServer.initialize();
|
|
939
|
+
|
|
940
|
+
// Initialize notification manager
|
|
941
|
+
this.notificationManager = new NotificationManager(this.webSocketServer);
|
|
942
|
+
await this.notificationManager.initialize();
|
|
943
|
+
|
|
944
|
+
// Setup notification subscriptions
|
|
945
|
+
this.setupNotificationSubscriptions();
|
|
946
|
+
|
|
947
|
+
console.log(chalk.green('✅ WebSocket and notifications initialized'));
|
|
948
|
+
} catch (error) {
|
|
949
|
+
console.error(chalk.red('❌ Failed to initialize WebSocket:'), error);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Setup notification subscriptions
|
|
955
|
+
*/
|
|
956
|
+
setupNotificationSubscriptions() {
|
|
957
|
+
// Subscribe to refresh requests from WebSocket clients
|
|
958
|
+
this.notificationManager.subscribe('refresh_requested', async (notification) => {
|
|
959
|
+
console.log(chalk.blue('🔄 Refresh requested via WebSocket'));
|
|
960
|
+
await this.loadInitialData();
|
|
961
|
+
|
|
962
|
+
// Notify clients of the refreshed data
|
|
963
|
+
this.notificationManager.notifyDataRefresh(this.data, 'websocket_request');
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Detect and notify conversation state changes
|
|
969
|
+
* @param {Object} previousData - Previous data state
|
|
970
|
+
* @param {Object} currentData - Current data state
|
|
971
|
+
*/
|
|
972
|
+
detectAndNotifyStateChanges(previousData, currentData) {
|
|
973
|
+
if (!previousData || !previousData.conversations || !currentData || !currentData.conversations) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Create maps for easier lookup
|
|
978
|
+
const previousConversations = new Map();
|
|
979
|
+
previousData.conversations.forEach(conv => {
|
|
980
|
+
previousConversations.set(conv.id, conv);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// Check for state changes
|
|
984
|
+
currentData.conversations.forEach(currentConv => {
|
|
985
|
+
const previousConv = previousConversations.get(currentConv.id);
|
|
986
|
+
|
|
987
|
+
if (previousConv && previousConv.status !== currentConv.status) {
|
|
988
|
+
// State changed - notify clients
|
|
989
|
+
this.notificationManager.notifyConversationStateChange(
|
|
990
|
+
currentConv.id,
|
|
991
|
+
previousConv.status,
|
|
992
|
+
currentConv.status,
|
|
993
|
+
{
|
|
994
|
+
project: currentConv.project,
|
|
995
|
+
tokens: currentConv.tokens,
|
|
996
|
+
lastModified: currentConv.lastModified
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Get Claude session information from statsig files
|
|
1005
|
+
*/
|
|
1006
|
+
async getClaudeSessionInfo() {
|
|
1007
|
+
try {
|
|
1008
|
+
if (!await fs.pathExists(this.claudeStatsigDir)) {
|
|
1009
|
+
return {
|
|
1010
|
+
hasSession: false,
|
|
1011
|
+
error: 'Claude statsig directory not found'
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const files = await fs.readdir(this.claudeStatsigDir);
|
|
1016
|
+
const sessionFile = files.find(file => file.startsWith('statsig.session_id.'));
|
|
1017
|
+
|
|
1018
|
+
if (!sessionFile) {
|
|
1019
|
+
return {
|
|
1020
|
+
hasSession: false,
|
|
1021
|
+
error: 'No session file found'
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const sessionFilePath = path.join(this.claudeStatsigDir, sessionFile);
|
|
1026
|
+
const sessionData = await fs.readFile(sessionFilePath, 'utf8');
|
|
1027
|
+
const sessionInfo = JSON.parse(sessionData);
|
|
1028
|
+
|
|
1029
|
+
const now = Date.now();
|
|
1030
|
+
const startTime = sessionInfo.startTime;
|
|
1031
|
+
const lastUpdate = sessionInfo.lastUpdate;
|
|
1032
|
+
|
|
1033
|
+
// Calculate session duration
|
|
1034
|
+
const sessionDuration = now - startTime;
|
|
1035
|
+
const sessionDurationMinutes = Math.floor(sessionDuration / (1000 * 60));
|
|
1036
|
+
const sessionDurationHours = Math.floor(sessionDurationMinutes / 60);
|
|
1037
|
+
const remainingMinutes = sessionDurationMinutes % 60;
|
|
1038
|
+
|
|
1039
|
+
// Calculate time since last update
|
|
1040
|
+
const timeSinceLastUpdate = now - lastUpdate;
|
|
1041
|
+
const timeSinceLastUpdateMinutes = Math.floor(timeSinceLastUpdate / (1000 * 60));
|
|
1042
|
+
|
|
1043
|
+
// Based on observed pattern: ~2 hours and 21 minutes session limit
|
|
1044
|
+
const sessionLimitMs = 2 * 60 * 60 * 1000 + 21 * 60 * 1000; // 2h 21m
|
|
1045
|
+
const timeRemaining = sessionLimitMs - sessionDuration;
|
|
1046
|
+
const timeRemainingMinutes = Math.floor(timeRemaining / (1000 * 60));
|
|
1047
|
+
const timeRemainingHours = Math.floor(timeRemainingMinutes / 60);
|
|
1048
|
+
const remainingMinutesDisplay = timeRemainingMinutes % 60;
|
|
1049
|
+
|
|
1050
|
+
return {
|
|
1051
|
+
hasSession: true,
|
|
1052
|
+
sessionId: sessionInfo.sessionID,
|
|
1053
|
+
startTime: startTime,
|
|
1054
|
+
lastUpdate: lastUpdate,
|
|
1055
|
+
sessionDuration: {
|
|
1056
|
+
ms: sessionDuration,
|
|
1057
|
+
minutes: sessionDurationMinutes,
|
|
1058
|
+
hours: sessionDurationHours,
|
|
1059
|
+
remainingMinutes: remainingMinutes,
|
|
1060
|
+
formatted: `${sessionDurationHours}h ${remainingMinutes}m`
|
|
1061
|
+
},
|
|
1062
|
+
timeSinceLastUpdate: {
|
|
1063
|
+
ms: timeSinceLastUpdate,
|
|
1064
|
+
minutes: timeSinceLastUpdateMinutes,
|
|
1065
|
+
formatted: `${timeSinceLastUpdateMinutes}m`
|
|
1066
|
+
},
|
|
1067
|
+
estimatedTimeRemaining: {
|
|
1068
|
+
ms: timeRemaining,
|
|
1069
|
+
minutes: timeRemainingMinutes,
|
|
1070
|
+
hours: timeRemainingHours,
|
|
1071
|
+
remainingMinutes: remainingMinutesDisplay,
|
|
1072
|
+
formatted: timeRemaining > 0 ? `${timeRemainingHours}h ${remainingMinutesDisplay}m` : 'Session expired',
|
|
1073
|
+
isExpired: timeRemaining <= 0
|
|
1074
|
+
},
|
|
1075
|
+
sessionLimit: {
|
|
1076
|
+
ms: sessionLimitMs,
|
|
1077
|
+
hours: 2,
|
|
1078
|
+
minutes: 21,
|
|
1079
|
+
formatted: '2h 21m'
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
return {
|
|
1084
|
+
hasSession: false,
|
|
1085
|
+
error: `Failed to read session info: ${error.message}`
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
stop() {
|
|
1091
|
+
// Stop file watchers
|
|
1092
|
+
this.fileWatcher.stop();
|
|
1093
|
+
|
|
1094
|
+
// Stop server
|
|
1095
|
+
// Close WebSocket server
|
|
1096
|
+
if (this.webSocketServer) {
|
|
1097
|
+
this.webSocketServer.close();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Shutdown notification manager
|
|
1101
|
+
if (this.notificationManager) {
|
|
1102
|
+
this.notificationManager.shutdown();
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (this.httpServer) {
|
|
1106
|
+
this.httpServer.close();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Keep compatibility
|
|
1110
|
+
if (this.server) {
|
|
1111
|
+
this.server.close();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Log cache statistics before stopping
|
|
1115
|
+
this.dataCache.logStats();
|
|
1116
|
+
|
|
1117
|
+
console.log(chalk.yellow('Analytics dashboard stopped'));
|
|
1118
|
+
}
|
|
920
1119
|
}
|
|
921
1120
|
|
|
922
1121
|
async function runAnalytics(options = {}) {
|
|
@@ -928,7 +1127,7 @@ async function runAnalytics(options = {}) {
|
|
|
928
1127
|
await analytics.initialize();
|
|
929
1128
|
|
|
930
1129
|
// Create web dashboard files
|
|
931
|
-
|
|
1130
|
+
// Web dashboard files are now static in analytics-web directory
|
|
932
1131
|
|
|
933
1132
|
await analytics.startServer();
|
|
934
1133
|
await analytics.openBrowser();
|
|
@@ -952,1896 +1151,6 @@ async function runAnalytics(options = {}) {
|
|
|
952
1151
|
}
|
|
953
1152
|
}
|
|
954
1153
|
|
|
955
|
-
async function createWebDashboard() {
|
|
956
|
-
const webDir = path.join(__dirname, 'analytics-web');
|
|
957
|
-
await fs.ensureDir(webDir);
|
|
958
|
-
|
|
959
|
-
// Create the HTML dashboard
|
|
960
|
-
const htmlContent = `<!DOCTYPE html>
|
|
961
|
-
<html lang="en">
|
|
962
|
-
<head>
|
|
963
|
-
<meta charset="UTF-8">
|
|
964
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
965
|
-
<title>Claude Code Analytics - Terminal</title>
|
|
966
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
|
967
|
-
<style>
|
|
968
|
-
* {
|
|
969
|
-
margin: 0;
|
|
970
|
-
padding: 0;
|
|
971
|
-
box-sizing: border-box;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
body {
|
|
975
|
-
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
976
|
-
background: #0d1117;
|
|
977
|
-
color: #c9d1d9;
|
|
978
|
-
min-height: 100vh;
|
|
979
|
-
line-height: 1.4;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
.terminal {
|
|
983
|
-
max-width: 1400px;
|
|
984
|
-
margin: 0 auto;
|
|
985
|
-
padding: 20px;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
.terminal-header {
|
|
989
|
-
border-bottom: 1px solid #30363d;
|
|
990
|
-
padding-bottom: 20px;
|
|
991
|
-
margin-bottom: 20px;
|
|
992
|
-
position: relative;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
.terminal-title {
|
|
996
|
-
color: #d57455;
|
|
997
|
-
font-size: 1.25rem;
|
|
998
|
-
font-weight: normal;
|
|
999
|
-
display: flex;
|
|
1000
|
-
align-items: center;
|
|
1001
|
-
gap: 8px;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
.status-dot {
|
|
1005
|
-
width: 8px;
|
|
1006
|
-
height: 8px;
|
|
1007
|
-
border-radius: 50%;
|
|
1008
|
-
background: #3fb950;
|
|
1009
|
-
animation: pulse 2s infinite;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
@keyframes pulse {
|
|
1013
|
-
0%, 100% { opacity: 1; }
|
|
1014
|
-
50% { opacity: 0.6; }
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
.terminal-subtitle {
|
|
1018
|
-
color: #7d8590;
|
|
1019
|
-
font-size: 0.875rem;
|
|
1020
|
-
margin-top: 4px;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
.github-star-btn {
|
|
1024
|
-
position: absolute;
|
|
1025
|
-
top: 0;
|
|
1026
|
-
right: 0;
|
|
1027
|
-
background: #21262d;
|
|
1028
|
-
border: 1px solid #30363d;
|
|
1029
|
-
color: #c9d1d9;
|
|
1030
|
-
padding: 8px 12px;
|
|
1031
|
-
border-radius: 6px;
|
|
1032
|
-
text-decoration: none;
|
|
1033
|
-
font-family: inherit;
|
|
1034
|
-
font-size: 0.875rem;
|
|
1035
|
-
display: flex;
|
|
1036
|
-
align-items: center;
|
|
1037
|
-
gap: 6px;
|
|
1038
|
-
transition: all 0.2s ease;
|
|
1039
|
-
cursor: pointer;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
.github-star-btn:hover {
|
|
1043
|
-
border-color: #d57455;
|
|
1044
|
-
background: #30363d;
|
|
1045
|
-
color: #d57455;
|
|
1046
|
-
text-decoration: none;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
.github-star-btn .star-icon {
|
|
1050
|
-
font-size: 0.75rem;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
.stats-bar {
|
|
1054
|
-
display: flex;
|
|
1055
|
-
gap: 40px;
|
|
1056
|
-
margin: 20px 0;
|
|
1057
|
-
flex-wrap: wrap;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
.stat {
|
|
1061
|
-
display: flex;
|
|
1062
|
-
align-items: center;
|
|
1063
|
-
gap: 8px;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
.stat-label {
|
|
1067
|
-
color: #7d8590;
|
|
1068
|
-
font-size: 0.875rem;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
.stat-value {
|
|
1072
|
-
color: #d57455;
|
|
1073
|
-
font-weight: bold;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
.stat-sublabel {
|
|
1077
|
-
color: #7d8590;
|
|
1078
|
-
font-size: 0.75rem;
|
|
1079
|
-
display: block;
|
|
1080
|
-
margin-top: 2px;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
.chart-controls {
|
|
1084
|
-
display: flex;
|
|
1085
|
-
align-items: center;
|
|
1086
|
-
justify-content: space-between;
|
|
1087
|
-
gap: 16px;
|
|
1088
|
-
margin: 20px 0;
|
|
1089
|
-
padding: 12px 0;
|
|
1090
|
-
border-top: 1px solid #21262d;
|
|
1091
|
-
border-bottom: 1px solid #21262d;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
.chart-controls-left {
|
|
1095
|
-
display: flex;
|
|
1096
|
-
align-items: center;
|
|
1097
|
-
gap: 16px;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
.chart-controls-right {
|
|
1101
|
-
display: flex;
|
|
1102
|
-
align-items: center;
|
|
1103
|
-
gap: 12px;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
.date-control {
|
|
1107
|
-
display: flex;
|
|
1108
|
-
align-items: center;
|
|
1109
|
-
gap: 8px;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
.date-label {
|
|
1113
|
-
color: #7d8590;
|
|
1114
|
-
font-size: 0.875rem;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
.date-input {
|
|
1118
|
-
background: #21262d;
|
|
1119
|
-
border: 1px solid #30363d;
|
|
1120
|
-
color: #c9d1d9;
|
|
1121
|
-
padding: 6px 12px;
|
|
1122
|
-
border-radius: 4px;
|
|
1123
|
-
font-family: inherit;
|
|
1124
|
-
font-size: 0.875rem;
|
|
1125
|
-
cursor: pointer;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
.date-input:focus {
|
|
1129
|
-
outline: none;
|
|
1130
|
-
border-color: #d57455;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
.refresh-btn {
|
|
1134
|
-
background: none;
|
|
1135
|
-
border: 1px solid #30363d;
|
|
1136
|
-
color: #7d8590;
|
|
1137
|
-
padding: 6px 12px;
|
|
1138
|
-
border-radius: 4px;
|
|
1139
|
-
cursor: pointer;
|
|
1140
|
-
font-family: inherit;
|
|
1141
|
-
font-size: 0.875rem;
|
|
1142
|
-
transition: all 0.2s ease;
|
|
1143
|
-
display: flex;
|
|
1144
|
-
align-items: center;
|
|
1145
|
-
gap: 6px;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
.refresh-btn:hover {
|
|
1149
|
-
border-color: #d57455;
|
|
1150
|
-
color: #d57455;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
.refresh-btn.loading {
|
|
1154
|
-
opacity: 0.6;
|
|
1155
|
-
cursor: not-allowed;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
.charts-container {
|
|
1159
|
-
display: grid;
|
|
1160
|
-
grid-template-columns: 2fr 1fr;
|
|
1161
|
-
gap: 30px;
|
|
1162
|
-
margin: 20px 0 30px 0;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
.chart-card {
|
|
1166
|
-
background: #161b22;
|
|
1167
|
-
border: 1px solid #30363d;
|
|
1168
|
-
border-radius: 6px;
|
|
1169
|
-
padding: 20px;
|
|
1170
|
-
position: relative;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
.chart-title {
|
|
1174
|
-
color: #d57455;
|
|
1175
|
-
font-size: 0.875rem;
|
|
1176
|
-
text-transform: uppercase;
|
|
1177
|
-
margin-bottom: 16px;
|
|
1178
|
-
display: flex;
|
|
1179
|
-
align-items: center;
|
|
1180
|
-
gap: 8px;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
.chart-canvas {
|
|
1184
|
-
width: 100% !important;
|
|
1185
|
-
height: 200px !important;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
.filter-bar {
|
|
1189
|
-
display: flex;
|
|
1190
|
-
align-items: center;
|
|
1191
|
-
gap: 16px;
|
|
1192
|
-
margin: 20px 0;
|
|
1193
|
-
padding: 12px 0;
|
|
1194
|
-
border-top: 1px solid #21262d;
|
|
1195
|
-
border-bottom: 1px solid #21262d;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
.filter-label {
|
|
1199
|
-
color: #7d8590;
|
|
1200
|
-
font-size: 0.875rem;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
.filter-buttons {
|
|
1204
|
-
display: flex;
|
|
1205
|
-
gap: 8px;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
.filter-btn {
|
|
1209
|
-
background: none;
|
|
1210
|
-
border: 1px solid #30363d;
|
|
1211
|
-
color: #7d8590;
|
|
1212
|
-
padding: 4px 12px;
|
|
1213
|
-
border-radius: 4px;
|
|
1214
|
-
cursor: pointer;
|
|
1215
|
-
font-family: inherit;
|
|
1216
|
-
font-size: 0.875rem;
|
|
1217
|
-
transition: all 0.2s ease;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
.filter-btn:hover {
|
|
1221
|
-
border-color: #d57455;
|
|
1222
|
-
color: #d57455;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
.filter-btn.active {
|
|
1226
|
-
background: #d57455;
|
|
1227
|
-
border-color: #d57455;
|
|
1228
|
-
color: #0d1117;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
.sessions-table {
|
|
1232
|
-
width: 100%;
|
|
1233
|
-
border-collapse: collapse;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
.sessions-table th {
|
|
1237
|
-
text-align: left;
|
|
1238
|
-
padding: 8px 12px;
|
|
1239
|
-
color: #7d8590;
|
|
1240
|
-
font-size: 0.875rem;
|
|
1241
|
-
font-weight: normal;
|
|
1242
|
-
border-bottom: 1px solid #30363d;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
.sessions-table td {
|
|
1246
|
-
padding: 8px 12px;
|
|
1247
|
-
font-size: 0.875rem;
|
|
1248
|
-
border-bottom: 1px solid #21262d;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
.sessions-table tr:hover {
|
|
1252
|
-
background: #161b22;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
.session-id {
|
|
1256
|
-
color: #d57455;
|
|
1257
|
-
font-family: monospace;
|
|
1258
|
-
display: flex;
|
|
1259
|
-
align-items: center;
|
|
1260
|
-
gap: 6px;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
.process-indicator {
|
|
1264
|
-
display: inline-block;
|
|
1265
|
-
width: 6px;
|
|
1266
|
-
height: 6px;
|
|
1267
|
-
background: #3fb950;
|
|
1268
|
-
border-radius: 50%;
|
|
1269
|
-
animation: pulse 2s infinite;
|
|
1270
|
-
cursor: help;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
.process-indicator.orphan {
|
|
1274
|
-
background: #f85149;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
.session-id-container {
|
|
1278
|
-
display: flex;
|
|
1279
|
-
flex-direction: column;
|
|
1280
|
-
gap: 4px;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
.session-project {
|
|
1284
|
-
color: #c9d1d9;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
.session-model {
|
|
1288
|
-
color: #a5d6ff;
|
|
1289
|
-
font-size: 0.8rem;
|
|
1290
|
-
max-width: 150px;
|
|
1291
|
-
white-space: nowrap;
|
|
1292
|
-
overflow: hidden;
|
|
1293
|
-
text-overflow: ellipsis;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
.session-messages {
|
|
1297
|
-
color: #7d8590;
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
.session-tokens {
|
|
1301
|
-
color: #f85149;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
.session-time {
|
|
1305
|
-
color: #7d8590;
|
|
1306
|
-
font-size: 0.8rem;
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
.status-active {
|
|
1310
|
-
color: #3fb950;
|
|
1311
|
-
font-weight: bold;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
.status-recent {
|
|
1315
|
-
color: #d29922;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
.status-inactive {
|
|
1319
|
-
color: #7d8590;
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
.conversation-state {
|
|
1323
|
-
color: #d57455;
|
|
1324
|
-
font-style: italic;
|
|
1325
|
-
font-size: 0.8rem;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
.conversation-state.working {
|
|
1329
|
-
animation: working-pulse 1.5s infinite;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
.conversation-state.typing {
|
|
1333
|
-
animation: typing-pulse 1.5s infinite;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
@keyframes working-pulse {
|
|
1337
|
-
0%, 100% { opacity: 1; }
|
|
1338
|
-
50% { opacity: 0.7; }
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
@keyframes typing-pulse {
|
|
1342
|
-
0%, 100% { opacity: 1; }
|
|
1343
|
-
50% { opacity: 0.6; }
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
.status-squares {
|
|
1347
|
-
display: flex;
|
|
1348
|
-
gap: 2px;
|
|
1349
|
-
align-items: center;
|
|
1350
|
-
flex-wrap: wrap;
|
|
1351
|
-
margin: 0;
|
|
1352
|
-
padding: 0;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
.status-square {
|
|
1356
|
-
width: 10px !important;
|
|
1357
|
-
height: 10px !important;
|
|
1358
|
-
min-width: 10px !important;
|
|
1359
|
-
min-height: 10px !important;
|
|
1360
|
-
max-width: 10px !important;
|
|
1361
|
-
max-height: 10px !important;
|
|
1362
|
-
border-radius: 2px;
|
|
1363
|
-
cursor: help;
|
|
1364
|
-
position: relative;
|
|
1365
|
-
flex-shrink: 0;
|
|
1366
|
-
box-sizing: border-box;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
.status-square.success {
|
|
1370
|
-
background: #d57455;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
.status-square.tool {
|
|
1374
|
-
background: #f97316;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
.status-square.error {
|
|
1378
|
-
background: #dc2626;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
.status-square.pending {
|
|
1382
|
-
background: #6b7280;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
/* Additional specificity to override any table styling */
|
|
1386
|
-
.sessions-table .status-squares .status-square {
|
|
1387
|
-
width: 10px !important;
|
|
1388
|
-
height: 10px !important;
|
|
1389
|
-
min-width: 10px !important;
|
|
1390
|
-
min-height: 10px !important;
|
|
1391
|
-
max-width: 10px !important;
|
|
1392
|
-
max-height: 10px !important;
|
|
1393
|
-
display: inline-block !important;
|
|
1394
|
-
font-size: 0 !important;
|
|
1395
|
-
line-height: 0 !important;
|
|
1396
|
-
border: none !important;
|
|
1397
|
-
outline: none !important;
|
|
1398
|
-
vertical-align: top !important;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
.status-square:hover::after {
|
|
1402
|
-
content: attr(data-tooltip);
|
|
1403
|
-
position: absolute;
|
|
1404
|
-
bottom: 100%;
|
|
1405
|
-
left: 50%;
|
|
1406
|
-
transform: translateX(-50%);
|
|
1407
|
-
background: #1c1c1c;
|
|
1408
|
-
color: #fff;
|
|
1409
|
-
padding: 4px 8px;
|
|
1410
|
-
border-radius: 4px;
|
|
1411
|
-
font-size: 0.75rem;
|
|
1412
|
-
white-space: nowrap;
|
|
1413
|
-
z-index: 1000;
|
|
1414
|
-
margin-bottom: 4px;
|
|
1415
|
-
border: 1px solid #30363d;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
.status-square:hover::before {
|
|
1419
|
-
content: '';
|
|
1420
|
-
position: absolute;
|
|
1421
|
-
bottom: 100%;
|
|
1422
|
-
left: 50%;
|
|
1423
|
-
transform: translateX(-50%);
|
|
1424
|
-
border: 4px solid transparent;
|
|
1425
|
-
border-top-color: #30363d;
|
|
1426
|
-
z-index: 1000;
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
.loading, #error {
|
|
1430
|
-
text-align: center;
|
|
1431
|
-
padding: 40px;
|
|
1432
|
-
color: #7d8590;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
#error {
|
|
1436
|
-
color: #f85149;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
.no-sessions {
|
|
1440
|
-
text-align: center;
|
|
1441
|
-
padding: 40px;
|
|
1442
|
-
color: #7d8590;
|
|
1443
|
-
font-style: italic;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
.session-detail {
|
|
1447
|
-
display: none;
|
|
1448
|
-
margin-top: 20px;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
.session-detail.active {
|
|
1452
|
-
display: block;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
.detail-header {
|
|
1456
|
-
display: flex;
|
|
1457
|
-
justify-content: space-between;
|
|
1458
|
-
align-items: center;
|
|
1459
|
-
padding: 16px 0;
|
|
1460
|
-
border-bottom: 1px solid #30363d;
|
|
1461
|
-
margin-bottom: 20px;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
.detail-title {
|
|
1465
|
-
color: #d57455;
|
|
1466
|
-
font-size: 1.1rem;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
.detail-actions {
|
|
1470
|
-
display: flex;
|
|
1471
|
-
gap: 12px;
|
|
1472
|
-
align-items: center;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
.export-format-select {
|
|
1476
|
-
background: #21262d;
|
|
1477
|
-
border: 1px solid #30363d;
|
|
1478
|
-
color: #c9d1d9;
|
|
1479
|
-
padding: 6px 12px;
|
|
1480
|
-
border-radius: 4px;
|
|
1481
|
-
font-family: inherit;
|
|
1482
|
-
font-size: 0.875rem;
|
|
1483
|
-
cursor: pointer;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
.export-format-select:focus {
|
|
1487
|
-
outline: none;
|
|
1488
|
-
border-color: #d57455;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
.export-format-select option {
|
|
1492
|
-
background: #21262d;
|
|
1493
|
-
color: #c9d1d9;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
.btn {
|
|
1497
|
-
background: none;
|
|
1498
|
-
border: 1px solid #30363d;
|
|
1499
|
-
color: #7d8590;
|
|
1500
|
-
padding: 6px 12px;
|
|
1501
|
-
border-radius: 4px;
|
|
1502
|
-
cursor: pointer;
|
|
1503
|
-
font-family: inherit;
|
|
1504
|
-
font-size: 0.875rem;
|
|
1505
|
-
transition: all 0.2s ease;
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
.btn:hover {
|
|
1509
|
-
border-color: #d57455;
|
|
1510
|
-
color: #d57455;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
.btn-primary {
|
|
1514
|
-
background: #d57455;
|
|
1515
|
-
border-color: #d57455;
|
|
1516
|
-
color: #0d1117;
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
.btn-primary:hover {
|
|
1520
|
-
background: #e8956f;
|
|
1521
|
-
border-color: #e8956f;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
.session-info {
|
|
1525
|
-
display: grid;
|
|
1526
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1527
|
-
gap: 20px;
|
|
1528
|
-
margin-bottom: 30px;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
.info-item {
|
|
1532
|
-
display: flex;
|
|
1533
|
-
flex-direction: column;
|
|
1534
|
-
gap: 4px;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
.info-label {
|
|
1538
|
-
color: #7d8590;
|
|
1539
|
-
font-size: 0.75rem;
|
|
1540
|
-
text-transform: uppercase;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
.info-value {
|
|
1544
|
-
color: #c9d1d9;
|
|
1545
|
-
font-size: 0.875rem;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
.info-value.model {
|
|
1549
|
-
color: #a5d6ff;
|
|
1550
|
-
font-weight: bold;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
.search-input {
|
|
1554
|
-
width: 100%;
|
|
1555
|
-
background: #21262d;
|
|
1556
|
-
border: 1px solid #30363d;
|
|
1557
|
-
color: #c9d1d9;
|
|
1558
|
-
padding: 8px 12px;
|
|
1559
|
-
border-radius: 4px;
|
|
1560
|
-
font-family: inherit;
|
|
1561
|
-
font-size: 0.875rem;
|
|
1562
|
-
margin-bottom: 16px;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
.search-input:focus {
|
|
1566
|
-
outline: none;
|
|
1567
|
-
border-color: #d57455;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
.conversation-history {
|
|
1571
|
-
border: 1px solid #30363d;
|
|
1572
|
-
border-radius: 6px;
|
|
1573
|
-
max-height: 600px;
|
|
1574
|
-
overflow-y: auto;
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
.message {
|
|
1578
|
-
padding: 16px;
|
|
1579
|
-
border-bottom: 1px solid #21262d;
|
|
1580
|
-
position: relative;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
.message:last-child {
|
|
1584
|
-
border-bottom: none;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
.message-header {
|
|
1588
|
-
display: flex;
|
|
1589
|
-
justify-content: space-between;
|
|
1590
|
-
align-items: center;
|
|
1591
|
-
margin-bottom: 8px;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
.message-role {
|
|
1595
|
-
color: #58a6ff;
|
|
1596
|
-
font-size: 0.875rem;
|
|
1597
|
-
font-weight: bold;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
.message-role.user {
|
|
1601
|
-
color: #3fb950;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
.message-role.assistant {
|
|
1605
|
-
color: #d57455;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
.message-time {
|
|
1609
|
-
color: #7d8590;
|
|
1610
|
-
font-size: 0.75rem;
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
.message-content {
|
|
1614
|
-
color: #c9d1d9;
|
|
1615
|
-
font-size: 0.875rem;
|
|
1616
|
-
line-height: 1.5;
|
|
1617
|
-
white-space: pre-wrap;
|
|
1618
|
-
word-wrap: break-word;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
.message-type-indicator {
|
|
1622
|
-
position: absolute;
|
|
1623
|
-
top: 8px;
|
|
1624
|
-
right: 8px;
|
|
1625
|
-
width: 8px;
|
|
1626
|
-
height: 8px;
|
|
1627
|
-
border-radius: 2px;
|
|
1628
|
-
cursor: help;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
.message-type-indicator.success {
|
|
1632
|
-
background: #d57455;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
.message-type-indicator.tool {
|
|
1636
|
-
background: #f97316;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
.message-type-indicator.error {
|
|
1640
|
-
background: #dc2626;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
.message-type-indicator.pending {
|
|
1644
|
-
background: #6b7280;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
.message-type-indicator:hover::after {
|
|
1648
|
-
content: attr(data-tooltip);
|
|
1649
|
-
position: absolute;
|
|
1650
|
-
top: 100%;
|
|
1651
|
-
right: 0;
|
|
1652
|
-
background: #1c1c1c;
|
|
1653
|
-
color: #fff;
|
|
1654
|
-
padding: 4px 8px;
|
|
1655
|
-
border-radius: 4px;
|
|
1656
|
-
font-size: 0.75rem;
|
|
1657
|
-
white-space: nowrap;
|
|
1658
|
-
z-index: 1000;
|
|
1659
|
-
margin-top: 4px;
|
|
1660
|
-
border: 1px solid #30363d;
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
.back-btn {
|
|
1664
|
-
margin-bottom: 20px;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
@media (max-width: 768px) {
|
|
1668
|
-
.stats-bar {
|
|
1669
|
-
gap: 20px;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
.chart-controls {
|
|
1673
|
-
flex-direction: column;
|
|
1674
|
-
gap: 12px;
|
|
1675
|
-
align-items: stretch;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
.chart-controls-left {
|
|
1679
|
-
flex-direction: column;
|
|
1680
|
-
gap: 12px;
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
.chart-controls-right {
|
|
1684
|
-
justify-content: center;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
.charts-container {
|
|
1688
|
-
grid-template-columns: 1fr;
|
|
1689
|
-
gap: 20px;
|
|
1690
|
-
margin: 20px 0;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
.chart-card {
|
|
1694
|
-
padding: 16px;
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
.chart-canvas {
|
|
1698
|
-
height: 180px !important;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
.filter-bar {
|
|
1702
|
-
flex-direction: column;
|
|
1703
|
-
align-items: flex-start;
|
|
1704
|
-
gap: 8px;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
.sessions-table {
|
|
1708
|
-
font-size: 0.8rem;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
.sessions-table th,
|
|
1712
|
-
.sessions-table td {
|
|
1713
|
-
padding: 6px 8px;
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
.github-star-btn {
|
|
1717
|
-
position: relative;
|
|
1718
|
-
margin-top: 12px;
|
|
1719
|
-
align-self: flex-start;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
.terminal-header {
|
|
1723
|
-
display: flex;
|
|
1724
|
-
flex-direction: column;
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
</style>
|
|
1728
|
-
</head>
|
|
1729
|
-
<body>
|
|
1730
|
-
<div class="terminal">
|
|
1731
|
-
<div class="terminal-header">
|
|
1732
|
-
<div class="terminal-title">
|
|
1733
|
-
<span class="status-dot"></span>
|
|
1734
|
-
claude-code-analytics
|
|
1735
|
-
</div>
|
|
1736
|
-
<div class="terminal-subtitle">real-time monitoring dashboard</div>
|
|
1737
|
-
<div class="terminal-subtitle" id="lastUpdate"></div>
|
|
1738
|
-
|
|
1739
|
-
<a href="https://github.com/davila7/claude-code-templates" target="_blank" class="github-star-btn" title="Give us a star on GitHub to support the project!">
|
|
1740
|
-
<span class="star-icon">⭐</span>
|
|
1741
|
-
<span>Star on GitHub</span>
|
|
1742
|
-
</a>
|
|
1743
|
-
</div>
|
|
1744
|
-
|
|
1745
|
-
<div id="loading" class="loading">
|
|
1746
|
-
loading claude code data...
|
|
1747
|
-
</div>
|
|
1748
|
-
|
|
1749
|
-
<div id="error" class="error" style="display: none;">
|
|
1750
|
-
error: failed to load claude code data
|
|
1751
|
-
</div>
|
|
1752
|
-
|
|
1753
|
-
<div id="dashboard" style="display: none;">
|
|
1754
|
-
<div class="stats-bar">
|
|
1755
|
-
<div class="stat">
|
|
1756
|
-
<span class="stat-label">conversations:</span>
|
|
1757
|
-
<span class="stat-value" id="totalConversations">0</span>
|
|
1758
|
-
</div>
|
|
1759
|
-
<div class="stat">
|
|
1760
|
-
<span class="stat-label">claude sessions:</span>
|
|
1761
|
-
<span class="stat-value" id="claudeSessions">0</span>
|
|
1762
|
-
<span class="stat-sublabel" id="claudeSessionsDetail"></span>
|
|
1763
|
-
</div>
|
|
1764
|
-
<div class="stat">
|
|
1765
|
-
<span class="stat-label">tokens:</span>
|
|
1766
|
-
<span class="stat-value" id="totalTokens">0</span>
|
|
1767
|
-
</div>
|
|
1768
|
-
<div class="stat">
|
|
1769
|
-
<span class="stat-label">projects:</span>
|
|
1770
|
-
<span class="stat-value" id="activeProjects">0</span>
|
|
1771
|
-
</div>
|
|
1772
|
-
<div class="stat">
|
|
1773
|
-
<span class="stat-label">storage:</span>
|
|
1774
|
-
<span class="stat-value" id="dataSize">0</span>
|
|
1775
|
-
</div>
|
|
1776
|
-
</div>
|
|
1777
|
-
|
|
1778
|
-
<div class="chart-controls">
|
|
1779
|
-
<div class="chart-controls-left">
|
|
1780
|
-
<div class="date-control">
|
|
1781
|
-
<span class="date-label">from:</span>
|
|
1782
|
-
<input type="date" id="dateFrom" class="date-input">
|
|
1783
|
-
</div>
|
|
1784
|
-
<div class="date-control">
|
|
1785
|
-
<span class="date-label">to:</span>
|
|
1786
|
-
<input type="date" id="dateTo" class="date-input">
|
|
1787
|
-
</div>
|
|
1788
|
-
</div>
|
|
1789
|
-
<div class="chart-controls-right">
|
|
1790
|
-
<button class="refresh-btn" onclick="toggleNotifications()" id="notificationBtn">
|
|
1791
|
-
enable notifications
|
|
1792
|
-
</button>
|
|
1793
|
-
<button class="refresh-btn" onclick="refreshCharts()" id="refreshBtn">
|
|
1794
|
-
refresh charts
|
|
1795
|
-
</button>
|
|
1796
|
-
</div>
|
|
1797
|
-
</div>
|
|
1798
|
-
|
|
1799
|
-
<div class="charts-container">
|
|
1800
|
-
<div class="chart-card">
|
|
1801
|
-
<div class="chart-title">
|
|
1802
|
-
📊 token usage over time
|
|
1803
|
-
</div>
|
|
1804
|
-
<canvas id="tokenChart" class="chart-canvas"></canvas>
|
|
1805
|
-
</div>
|
|
1806
|
-
|
|
1807
|
-
<div class="chart-card">
|
|
1808
|
-
<div class="chart-title">
|
|
1809
|
-
🎯 project activity distribution
|
|
1810
|
-
</div>
|
|
1811
|
-
<canvas id="projectChart" class="chart-canvas"></canvas>
|
|
1812
|
-
</div>
|
|
1813
|
-
</div>
|
|
1814
|
-
|
|
1815
|
-
<div class="filter-bar">
|
|
1816
|
-
<span class="filter-label">filter conversations:</span>
|
|
1817
|
-
<div class="filter-buttons">
|
|
1818
|
-
<button class="filter-btn active" data-filter="active">active</button>
|
|
1819
|
-
<button class="filter-btn" data-filter="recent">recent</button>
|
|
1820
|
-
<button class="filter-btn" data-filter="inactive">inactive</button>
|
|
1821
|
-
<button class="filter-btn" data-filter="all">all</button>
|
|
1822
|
-
</div>
|
|
1823
|
-
</div>
|
|
1824
|
-
|
|
1825
|
-
<table class="sessions-table">
|
|
1826
|
-
<thead>
|
|
1827
|
-
<tr>
|
|
1828
|
-
<th>conversation id</th>
|
|
1829
|
-
<th>project</th>
|
|
1830
|
-
<th>model</th>
|
|
1831
|
-
<th>messages</th>
|
|
1832
|
-
<th>tokens</th>
|
|
1833
|
-
<th>last activity</th>
|
|
1834
|
-
<th>conversation state</th>
|
|
1835
|
-
<th>status</th>
|
|
1836
|
-
</tr>
|
|
1837
|
-
</thead>
|
|
1838
|
-
<tbody id="sessionsTable">
|
|
1839
|
-
<!-- Sessions will be loaded here -->
|
|
1840
|
-
</tbody>
|
|
1841
|
-
</table>
|
|
1842
|
-
|
|
1843
|
-
<div id="noSessions" class="no-sessions" style="display: none;">
|
|
1844
|
-
no conversations found for current filter
|
|
1845
|
-
</div>
|
|
1846
|
-
|
|
1847
|
-
<div id="sessionDetail" class="session-detail">
|
|
1848
|
-
<button class="btn back-btn" onclick="showSessionsList()">← back to conversations</button>
|
|
1849
|
-
|
|
1850
|
-
<div class="detail-header">
|
|
1851
|
-
<div class="detail-title" id="detailTitle">conversation details</div>
|
|
1852
|
-
<div class="detail-actions">
|
|
1853
|
-
<select id="exportFormat" class="export-format-select">
|
|
1854
|
-
<option value="csv">CSV</option>
|
|
1855
|
-
<option value="json">JSON</option>
|
|
1856
|
-
</select>
|
|
1857
|
-
<button class="btn" onclick="exportSession()">export</button>
|
|
1858
|
-
<button class="btn btn-primary" onclick="refreshSessionDetail()">refresh</button>
|
|
1859
|
-
</div>
|
|
1860
|
-
</div>
|
|
1861
|
-
|
|
1862
|
-
<div class="session-info" id="sessionInfo">
|
|
1863
|
-
<!-- Session info will be loaded here -->
|
|
1864
|
-
</div>
|
|
1865
|
-
|
|
1866
|
-
<div>
|
|
1867
|
-
<h3 style="color: #7d8590; margin-bottom: 16px; font-size: 0.875rem; text-transform: uppercase;">conversation history</h3>
|
|
1868
|
-
<input type="text" id="conversationSearch" class="search-input" placeholder="Search messages...">
|
|
1869
|
-
<div class="conversation-history" id="conversationHistory">
|
|
1870
|
-
<!-- Conversation history will be loaded here -->
|
|
1871
|
-
</div>
|
|
1872
|
-
</div>
|
|
1873
|
-
</div>
|
|
1874
|
-
</div>
|
|
1875
|
-
</div>
|
|
1876
|
-
|
|
1877
|
-
<script>
|
|
1878
|
-
let allConversations = [];
|
|
1879
|
-
let currentFilter = 'active';
|
|
1880
|
-
let currentSession = null;
|
|
1881
|
-
let tokenChart = null;
|
|
1882
|
-
let projectChart = null;
|
|
1883
|
-
let allData = null;
|
|
1884
|
-
let notificationsEnabled = false;
|
|
1885
|
-
let previousConversationStates = new Map();
|
|
1886
|
-
|
|
1887
|
-
async function loadData() {
|
|
1888
|
-
try {
|
|
1889
|
-
const response = await fetch('/api/data');
|
|
1890
|
-
const data = await response.json();
|
|
1891
|
-
|
|
1892
|
-
console.log('Data loaded:', data.timestamp);
|
|
1893
|
-
|
|
1894
|
-
document.getElementById('loading').style.display = 'none';
|
|
1895
|
-
document.getElementById('dashboard').style.display = 'block';
|
|
1896
|
-
|
|
1897
|
-
// Update timestamp
|
|
1898
|
-
document.getElementById('lastUpdate').textContent = \`last update: \${data.lastUpdate}\`;
|
|
1899
|
-
|
|
1900
|
-
updateStats(data.summary);
|
|
1901
|
-
allConversations = data.conversations;
|
|
1902
|
-
allData = data; // Store data globally for access
|
|
1903
|
-
window.allData = data; // Keep for backward compatibility
|
|
1904
|
-
|
|
1905
|
-
// Initialize date inputs on first load
|
|
1906
|
-
if (!document.getElementById('dateFrom').value) {
|
|
1907
|
-
initializeDateInputs();
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
updateCharts(data);
|
|
1911
|
-
updateSessionsTable();
|
|
1912
|
-
|
|
1913
|
-
// Check for conversation state changes and send notifications
|
|
1914
|
-
checkForNotifications(data.conversations);
|
|
1915
|
-
|
|
1916
|
-
} catch (error) {
|
|
1917
|
-
document.getElementById('loading').style.display = 'none';
|
|
1918
|
-
document.getElementById('error').style.display = 'block';
|
|
1919
|
-
console.error('Failed to load data:', error);
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
// Function to only update conversation data without refreshing charts
|
|
1924
|
-
async function loadConversationData() {
|
|
1925
|
-
try {
|
|
1926
|
-
const response = await fetch('/api/data');
|
|
1927
|
-
const data = await response.json();
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
// Update timestamp
|
|
1931
|
-
document.getElementById('lastUpdate').textContent = \`last update: \${data.lastUpdate}\`;
|
|
1932
|
-
|
|
1933
|
-
updateStats(data.summary);
|
|
1934
|
-
allConversations = data.conversations;
|
|
1935
|
-
allData = data; // Store data globally for access
|
|
1936
|
-
window.allData = data; // Keep for backward compatibility
|
|
1937
|
-
|
|
1938
|
-
// Only update sessions table, not charts
|
|
1939
|
-
updateSessionsTable();
|
|
1940
|
-
|
|
1941
|
-
// Check for conversation state changes and send notifications
|
|
1942
|
-
checkForNotifications(data.conversations);
|
|
1943
|
-
|
|
1944
|
-
} catch (error) {
|
|
1945
|
-
console.error('Failed to refresh conversation data:', error);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// Notification functions
|
|
1950
|
-
async function requestNotificationPermission() {
|
|
1951
|
-
if (!('Notification' in window)) {
|
|
1952
|
-
console.log('This browser does not support notifications');
|
|
1953
|
-
return false;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
if (Notification.permission === 'granted') {
|
|
1957
|
-
notificationsEnabled = true;
|
|
1958
|
-
return true;
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
if (Notification.permission !== 'denied') {
|
|
1962
|
-
const permission = await Notification.requestPermission();
|
|
1963
|
-
notificationsEnabled = permission === 'granted';
|
|
1964
|
-
return notificationsEnabled;
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
return false;
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
function sendNotification(title, body, conversationId) {
|
|
1971
|
-
if (!notificationsEnabled) return;
|
|
1972
|
-
|
|
1973
|
-
const notification = new Notification(title, {
|
|
1974
|
-
body: body,
|
|
1975
|
-
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzIxMjYyZCIvPgo8cGF0aCBkPSJNOCA4aDE2djE2SDh6IiBmaWxsPSIjZDU3NDU1Ii8+CjwvZGJnPgo=',
|
|
1976
|
-
badge: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iOCIgY3k9IjgiIHI9IjgiIGZpbGw9IiNkNTc0NTUiLz4KPC9zdmc+',
|
|
1977
|
-
tag: conversationId,
|
|
1978
|
-
requireInteraction: true
|
|
1979
|
-
});
|
|
1980
|
-
|
|
1981
|
-
notification.onclick = function() {
|
|
1982
|
-
window.focus();
|
|
1983
|
-
this.close();
|
|
1984
|
-
// Focus on the conversation if possible
|
|
1985
|
-
if (conversationId) {
|
|
1986
|
-
showSessionDetail(conversationId);
|
|
1987
|
-
}
|
|
1988
|
-
};
|
|
1989
|
-
|
|
1990
|
-
// Auto close after 10 seconds
|
|
1991
|
-
setTimeout(() => {
|
|
1992
|
-
notification.close();
|
|
1993
|
-
}, 10000);
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
function checkForNotifications(conversations) {
|
|
1997
|
-
if (!notificationsEnabled) return;
|
|
1998
|
-
|
|
1999
|
-
conversations.forEach(conv => {
|
|
2000
|
-
const currentState = conv.conversationState;
|
|
2001
|
-
const prevState = previousConversationStates.get(conv.id);
|
|
2002
|
-
|
|
2003
|
-
// Check if conversation state changed to "Awaiting user input..."
|
|
2004
|
-
if (prevState && prevState !== currentState) {
|
|
2005
|
-
if (currentState === 'Awaiting user input...' ||
|
|
2006
|
-
currentState === 'User may be typing...' ||
|
|
2007
|
-
currentState === 'Awaiting response...') {
|
|
2008
|
-
|
|
2009
|
-
const title = 'Claude is waiting for you!';
|
|
2010
|
-
const body = \`Project: \${conv.project} - Claude needs your input\`;
|
|
2011
|
-
|
|
2012
|
-
sendNotification(title, body, conv.id);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
// Update previous state
|
|
2017
|
-
previousConversationStates.set(conv.id, currentState);
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
async function toggleNotifications() {
|
|
2022
|
-
const btn = document.getElementById('notificationBtn');
|
|
2023
|
-
|
|
2024
|
-
if (!notificationsEnabled) {
|
|
2025
|
-
const granted = await requestNotificationPermission();
|
|
2026
|
-
if (granted) {
|
|
2027
|
-
btn.textContent = 'notifications on';
|
|
2028
|
-
btn.style.borderColor = '#3fb950';
|
|
2029
|
-
btn.style.color = '#3fb950';
|
|
2030
|
-
|
|
2031
|
-
// Send a test notification
|
|
2032
|
-
sendNotification(
|
|
2033
|
-
'Notifications enabled!',
|
|
2034
|
-
'You will now receive alerts when Claude is waiting for your input.',
|
|
2035
|
-
null
|
|
2036
|
-
);
|
|
2037
|
-
} else {
|
|
2038
|
-
btn.textContent = 'notifications denied';
|
|
2039
|
-
btn.style.borderColor = '#f85149';
|
|
2040
|
-
btn.style.color = '#f85149';
|
|
2041
|
-
}
|
|
2042
|
-
} else {
|
|
2043
|
-
// Disable notifications
|
|
2044
|
-
notificationsEnabled = false;
|
|
2045
|
-
btn.textContent = 'enable notifications';
|
|
2046
|
-
btn.style.borderColor = '#30363d';
|
|
2047
|
-
btn.style.color = '#7d8590';
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
function updateStats(summary) {
|
|
2052
|
-
document.getElementById('totalConversations').textContent = summary.totalConversations.toLocaleString();
|
|
2053
|
-
document.getElementById('totalTokens').textContent = summary.totalTokens.toLocaleString();
|
|
2054
|
-
document.getElementById('activeProjects').textContent = summary.activeProjects;
|
|
2055
|
-
document.getElementById('dataSize').textContent = summary.totalFileSize;
|
|
2056
|
-
|
|
2057
|
-
// Update Claude sessions
|
|
2058
|
-
if (summary.claudeSessions) {
|
|
2059
|
-
document.getElementById('claudeSessions').textContent = summary.claudeSessions.total.toLocaleString();
|
|
2060
|
-
document.getElementById('claudeSessionsDetail').textContent =
|
|
2061
|
-
\`this month: \${summary.claudeSessions.currentMonth} • this week: \${summary.claudeSessions.thisWeek}\`;
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
function initializeDateInputs() {
|
|
2066
|
-
const today = new Date();
|
|
2067
|
-
const sevenDaysAgo = new Date(today);
|
|
2068
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
2069
|
-
|
|
2070
|
-
document.getElementById('dateFrom').value = sevenDaysAgo.toISOString().split('T')[0];
|
|
2071
|
-
document.getElementById('dateTo').value = today.toISOString().split('T')[0];
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
function getDateRange() {
|
|
2075
|
-
const fromDate = new Date(document.getElementById('dateFrom').value);
|
|
2076
|
-
const toDate = new Date(document.getElementById('dateTo').value);
|
|
2077
|
-
toDate.setHours(23, 59, 59, 999); // Include the entire end date
|
|
2078
|
-
|
|
2079
|
-
return { fromDate, toDate };
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
function filterConversationsByDate(conversations) {
|
|
2083
|
-
const { fromDate, toDate } = getDateRange();
|
|
2084
|
-
|
|
2085
|
-
return conversations.filter(conv => {
|
|
2086
|
-
const convDate = new Date(conv.lastModified);
|
|
2087
|
-
return convDate >= fromDate && convDate <= toDate;
|
|
2088
|
-
});
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
function updateCharts(data) {
|
|
2092
|
-
// Wait for Chart.js to load before creating charts
|
|
2093
|
-
if (typeof Chart === 'undefined') {
|
|
2094
|
-
console.log('Chart.js not loaded yet, retrying in 100ms...');
|
|
2095
|
-
setTimeout(() => updateCharts(data), 100);
|
|
2096
|
-
return;
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// Use ALL conversations but filter chart display by date range
|
|
2100
|
-
// This maintains the original behavior
|
|
2101
|
-
|
|
2102
|
-
// Update Token Usage Over Time Chart
|
|
2103
|
-
updateTokenChart(data.conversations);
|
|
2104
|
-
|
|
2105
|
-
// Update Project Activity Distribution Chart
|
|
2106
|
-
updateProjectChart(data.conversations);
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
async function refreshCharts() {
|
|
2110
|
-
const refreshBtn = document.getElementById('refreshBtn');
|
|
2111
|
-
refreshBtn.classList.add('loading');
|
|
2112
|
-
refreshBtn.textContent = '🔄 refreshing...';
|
|
2113
|
-
|
|
2114
|
-
try {
|
|
2115
|
-
// Use existing data but re-filter and update charts
|
|
2116
|
-
if (allData) {
|
|
2117
|
-
updateCharts(allData);
|
|
2118
|
-
}
|
|
2119
|
-
} catch (error) {
|
|
2120
|
-
console.error('Error refreshing charts:', error);
|
|
2121
|
-
} finally {
|
|
2122
|
-
refreshBtn.classList.remove('loading');
|
|
2123
|
-
refreshBtn.textContent = 'refresh charts';
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
function updateTokenChart(conversations) {
|
|
2128
|
-
// Check if Chart.js is available
|
|
2129
|
-
if (typeof Chart === 'undefined') {
|
|
2130
|
-
console.warn('Chart.js not available for updateTokenChart');
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
// Prepare data for selected date range
|
|
2135
|
-
const { fromDate, toDate } = getDateRange();
|
|
2136
|
-
const dateRange = [];
|
|
2137
|
-
|
|
2138
|
-
const currentDate = new Date(fromDate);
|
|
2139
|
-
while (currentDate <= toDate) {
|
|
2140
|
-
dateRange.push({
|
|
2141
|
-
date: currentDate.toISOString().split('T')[0],
|
|
2142
|
-
label: currentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
|
2143
|
-
tokens: 0
|
|
2144
|
-
});
|
|
2145
|
-
currentDate.setDate(currentDate.getDate() + 1);
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
// Aggregate tokens by day
|
|
2149
|
-
conversations.forEach(conv => {
|
|
2150
|
-
const convDate = new Date(conv.lastModified).toISOString().split('T')[0];
|
|
2151
|
-
const dayData = dateRange.find(day => day.date === convDate);
|
|
2152
|
-
if (dayData) {
|
|
2153
|
-
dayData.tokens += conv.tokens;
|
|
2154
|
-
}
|
|
2155
|
-
});
|
|
2156
|
-
|
|
2157
|
-
const ctx = document.getElementById('tokenChart').getContext('2d');
|
|
2158
|
-
|
|
2159
|
-
if (tokenChart) {
|
|
2160
|
-
tokenChart.destroy();
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
tokenChart = new Chart(ctx, {
|
|
2164
|
-
type: 'line',
|
|
2165
|
-
data: {
|
|
2166
|
-
labels: dateRange.map(day => day.label),
|
|
2167
|
-
datasets: [{
|
|
2168
|
-
label: 'Tokens',
|
|
2169
|
-
data: dateRange.map(day => day.tokens),
|
|
2170
|
-
borderColor: '#d57455',
|
|
2171
|
-
backgroundColor: 'rgba(213, 116, 85, 0.1)',
|
|
2172
|
-
borderWidth: 2,
|
|
2173
|
-
pointBackgroundColor: '#d57455',
|
|
2174
|
-
pointBorderColor: '#d57455',
|
|
2175
|
-
pointRadius: 4,
|
|
2176
|
-
pointHoverRadius: 6,
|
|
2177
|
-
fill: true,
|
|
2178
|
-
tension: 0.3
|
|
2179
|
-
}]
|
|
2180
|
-
},
|
|
2181
|
-
options: {
|
|
2182
|
-
responsive: true,
|
|
2183
|
-
maintainAspectRatio: false,
|
|
2184
|
-
plugins: {
|
|
2185
|
-
legend: {
|
|
2186
|
-
display: false
|
|
2187
|
-
},
|
|
2188
|
-
tooltip: {
|
|
2189
|
-
backgroundColor: '#161b22',
|
|
2190
|
-
titleColor: '#d57455',
|
|
2191
|
-
bodyColor: '#c9d1d9',
|
|
2192
|
-
borderColor: '#30363d',
|
|
2193
|
-
borderWidth: 1,
|
|
2194
|
-
titleFont: {
|
|
2195
|
-
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
2196
|
-
size: 12
|
|
2197
|
-
},
|
|
2198
|
-
bodyFont: {
|
|
2199
|
-
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
2200
|
-
size: 11
|
|
2201
|
-
},
|
|
2202
|
-
callbacks: {
|
|
2203
|
-
title: function(context) {
|
|
2204
|
-
return context[0].label;
|
|
2205
|
-
},
|
|
2206
|
-
label: function(context) {
|
|
2207
|
-
return \`Tokens: \${context.parsed.y.toLocaleString()}\`;
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
},
|
|
2212
|
-
interaction: {
|
|
2213
|
-
intersect: false,
|
|
2214
|
-
mode: 'index'
|
|
2215
|
-
},
|
|
2216
|
-
hover: {
|
|
2217
|
-
animationDuration: 200
|
|
2218
|
-
},
|
|
2219
|
-
scales: {
|
|
2220
|
-
x: {
|
|
2221
|
-
grid: {
|
|
2222
|
-
color: '#30363d',
|
|
2223
|
-
borderColor: '#30363d'
|
|
2224
|
-
},
|
|
2225
|
-
ticks: {
|
|
2226
|
-
color: '#7d8590',
|
|
2227
|
-
font: {
|
|
2228
|
-
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
2229
|
-
size: 11
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
},
|
|
2233
|
-
y: {
|
|
2234
|
-
grid: {
|
|
2235
|
-
color: '#30363d',
|
|
2236
|
-
borderColor: '#30363d'
|
|
2237
|
-
},
|
|
2238
|
-
ticks: {
|
|
2239
|
-
color: '#7d8590',
|
|
2240
|
-
font: {
|
|
2241
|
-
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
2242
|
-
size: 11
|
|
2243
|
-
},
|
|
2244
|
-
callback: function(value) {
|
|
2245
|
-
return value.toLocaleString();
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
});
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
function updateProjectChart(conversations) {
|
|
2255
|
-
// Check if Chart.js is available
|
|
2256
|
-
if (typeof Chart === 'undefined') {
|
|
2257
|
-
console.warn('Chart.js not available for updateProjectChart');
|
|
2258
|
-
return;
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
// Aggregate data by project
|
|
2262
|
-
const projectData = {};
|
|
2263
|
-
|
|
2264
|
-
conversations.forEach(conv => {
|
|
2265
|
-
if (!projectData[conv.project]) {
|
|
2266
|
-
projectData[conv.project] = 0;
|
|
2267
|
-
}
|
|
2268
|
-
projectData[conv.project] += conv.tokens;
|
|
2269
|
-
});
|
|
2270
|
-
|
|
2271
|
-
// Get top 5 projects and group others
|
|
2272
|
-
const sortedProjects = Object.entries(projectData)
|
|
2273
|
-
.sort(([,a], [,b]) => b - a)
|
|
2274
|
-
.slice(0, 5);
|
|
2275
|
-
|
|
2276
|
-
const othersTotal = Object.entries(projectData)
|
|
2277
|
-
.slice(5)
|
|
2278
|
-
.reduce((sum, [,tokens]) => sum + tokens, 0);
|
|
2279
|
-
|
|
2280
|
-
if (othersTotal > 0) {
|
|
2281
|
-
sortedProjects.push(['others', othersTotal]);
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
// Terminal-style colors
|
|
2285
|
-
const colors = [
|
|
2286
|
-
'#d57455', // Orange
|
|
2287
|
-
'#3fb950', // Green
|
|
2288
|
-
'#a5d6ff', // Blue
|
|
2289
|
-
'#f97316', // Orange variant
|
|
2290
|
-
'#c9d1d9', // Light gray
|
|
2291
|
-
'#7d8590' // Gray
|
|
2292
|
-
];
|
|
2293
|
-
|
|
2294
|
-
const ctx = document.getElementById('projectChart').getContext('2d');
|
|
2295
|
-
|
|
2296
|
-
if (projectChart) {
|
|
2297
|
-
projectChart.destroy();
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
projectChart = new Chart(ctx, {
|
|
2301
|
-
type: 'doughnut',
|
|
2302
|
-
data: {
|
|
2303
|
-
labels: sortedProjects.map(([project]) => project),
|
|
2304
|
-
datasets: [{
|
|
2305
|
-
data: sortedProjects.map(([,tokens]) => tokens),
|
|
2306
|
-
backgroundColor: colors.slice(0, sortedProjects.length),
|
|
2307
|
-
borderColor: '#161b22',
|
|
2308
|
-
borderWidth: 2,
|
|
2309
|
-
hoverBorderWidth: 3
|
|
2310
|
-
}]
|
|
2311
|
-
},
|
|
2312
|
-
options: {
|
|
2313
|
-
responsive: true,
|
|
2314
|
-
maintainAspectRatio: false,
|
|
2315
|
-
plugins: {
|
|
2316
|
-
legend: {
|
|
2317
|
-
position: 'bottom',
|
|
2318
|
-
labels: {
|
|
2319
|
-
color: '#7d8590',
|
|
2320
|
-
font: {
|
|
2321
|
-
family: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
2322
|
-
size: 10
|
|
2323
|
-
},
|
|
2324
|
-
padding: 15,
|
|
2325
|
-
usePointStyle: true,
|
|
2326
|
-
pointStyle: 'circle'
|
|
2327
|
-
}
|
|
2328
|
-
},
|
|
2329
|
-
tooltip: {
|
|
2330
|
-
backgroundColor: '#161b22',
|
|
2331
|
-
titleColor: '#d57455',
|
|
2332
|
-
bodyColor: '#c9d1d9',
|
|
2333
|
-
borderColor: '#30363d',
|
|
2334
|
-
borderWidth: 1,
|
|
2335
|
-
titleFont: {
|
|
2336
|
-
family: 'Monaco, Menlo, Ubuntu Mono, monospace'
|
|
2337
|
-
},
|
|
2338
|
-
bodyFont: {
|
|
2339
|
-
family: 'Monaco, Menlo, Ubuntu Mono, monospace'
|
|
2340
|
-
},
|
|
2341
|
-
callbacks: {
|
|
2342
|
-
label: function(context) {
|
|
2343
|
-
const total = context.dataset.data.reduce((sum, value) => sum + value, 0);
|
|
2344
|
-
const percentage = ((context.parsed / total) * 100).toFixed(1);
|
|
2345
|
-
return \`\${context.label}: \${context.parsed.toLocaleString()} tokens (\${percentage}%)\`;
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
},
|
|
2350
|
-
cutout: '60%'
|
|
2351
|
-
}
|
|
2352
|
-
});
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
function updateSessionsTable() {
|
|
2356
|
-
const tableBody = document.getElementById('sessionsTable');
|
|
2357
|
-
const noSessionsDiv = document.getElementById('noSessions');
|
|
2358
|
-
|
|
2359
|
-
// Filter conversations based on current filter
|
|
2360
|
-
let filteredConversations = allConversations;
|
|
2361
|
-
if (currentFilter !== 'all') {
|
|
2362
|
-
filteredConversations = allConversations.filter(conv => conv.status === currentFilter);
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
if (filteredConversations.length === 0) {
|
|
2366
|
-
tableBody.innerHTML = '';
|
|
2367
|
-
noSessionsDiv.style.display = 'block';
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
noSessionsDiv.style.display = 'none';
|
|
2372
|
-
|
|
2373
|
-
tableBody.innerHTML = filteredConversations.map(conv => \`
|
|
2374
|
-
<tr onclick="showSessionDetail('\${conv.id}')" style="cursor: pointer;">
|
|
2375
|
-
<td>
|
|
2376
|
-
<div class="session-id-container">
|
|
2377
|
-
<div class="session-id">
|
|
2378
|
-
\${conv.id.substring(0, 8)}...
|
|
2379
|
-
\${conv.runningProcess ? \`<span class="process-indicator" title="Active claude process (PID: \${conv.runningProcess.pid})"></span>\` : ''}
|
|
2380
|
-
</div>
|
|
2381
|
-
<div class="status-squares">
|
|
2382
|
-
\${generateStatusSquaresHTML(conv.statusSquares || [])}
|
|
2383
|
-
</div>
|
|
2384
|
-
</div>
|
|
2385
|
-
</td>
|
|
2386
|
-
<td class="session-project">\${conv.project}</td>
|
|
2387
|
-
<td class="session-model" title="\${conv.modelInfo ? conv.modelInfo.primaryModel + ' (' + conv.modelInfo.currentServiceTier + ')' : 'N/A'}">\${conv.modelInfo ? conv.modelInfo.primaryModel : 'N/A'}</td>
|
|
2388
|
-
<td class="session-messages">\${conv.messageCount}</td>
|
|
2389
|
-
<td class="session-tokens">\${conv.tokens.toLocaleString()}</td>
|
|
2390
|
-
<td class="session-time">\${formatTime(conv.lastModified)}</td>
|
|
2391
|
-
<td class="conversation-state \${getStateClass(conv.conversationState)}">\${conv.conversationState}</td>
|
|
2392
|
-
<td class="status-\${conv.status}">\${conv.status}</td>
|
|
2393
|
-
</tr>
|
|
2394
|
-
\`).join('');
|
|
2395
|
-
|
|
2396
|
-
// NEW: Add orphan processes (active claude commands without conversation)
|
|
2397
|
-
if (window.allData && window.allData.orphanProcesses && window.allData.orphanProcesses.length > 0) {
|
|
2398
|
-
const orphanRows = window.allData.orphanProcesses.map(process => \`
|
|
2399
|
-
<tr style="background: rgba(248, 81, 73, 0.1); cursor: default;">
|
|
2400
|
-
<td>
|
|
2401
|
-
<div class="session-id-container">
|
|
2402
|
-
<div class="session-id">
|
|
2403
|
-
orphan-\${process.pid}
|
|
2404
|
-
<span class="process-indicator orphan" title="Orphan claude process (PID: \${process.pid})"></span>
|
|
2405
|
-
</div>
|
|
2406
|
-
</div>
|
|
2407
|
-
</td>
|
|
2408
|
-
<td class="session-project">\${process.workingDir}</td>
|
|
2409
|
-
<td class="session-model">Unknown</td>
|
|
2410
|
-
<td class="session-messages">-</td>
|
|
2411
|
-
<td class="session-tokens">-</td>
|
|
2412
|
-
<td class="session-time">Running</td>
|
|
2413
|
-
<td class="conversation-state">Active process</td>
|
|
2414
|
-
<td class="status-active">orphan</td>
|
|
2415
|
-
</tr>
|
|
2416
|
-
\`).join('');
|
|
2417
|
-
|
|
2418
|
-
if (currentFilter === 'active' || currentFilter === 'all') {
|
|
2419
|
-
tableBody.innerHTML += orphanRows;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
function formatTime(date) {
|
|
2425
|
-
const now = new Date();
|
|
2426
|
-
const diff = now - new Date(date);
|
|
2427
|
-
const minutes = Math.floor(diff / (1000 * 60));
|
|
2428
|
-
const hours = Math.floor(minutes / 60);
|
|
2429
|
-
const days = Math.floor(hours / 24);
|
|
2430
|
-
|
|
2431
|
-
if (minutes < 1) return 'now';
|
|
2432
|
-
if (minutes < 60) return \`\${minutes}m ago\`;
|
|
2433
|
-
if (hours < 24) return \`\${hours}h ago\`;
|
|
2434
|
-
return \`\${days}d ago\`;
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
function formatMessageTime(timestamp) {
|
|
2438
|
-
const date = new Date(timestamp);
|
|
2439
|
-
return date.toLocaleTimeString('en-US', {
|
|
2440
|
-
hour12: false,
|
|
2441
|
-
hour: '2-digit',
|
|
2442
|
-
minute: '2-digit',
|
|
2443
|
-
second: '2-digit'
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
function getStateClass(conversationState) {
|
|
2448
|
-
if (conversationState.includes('working') || conversationState.includes('Working')) {
|
|
2449
|
-
return 'working';
|
|
2450
|
-
}
|
|
2451
|
-
if (conversationState.includes('typing') || conversationState.includes('Typing')) {
|
|
2452
|
-
return 'typing';
|
|
2453
|
-
}
|
|
2454
|
-
return '';
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
function generateStatusSquaresHTML(statusSquares) {
|
|
2458
|
-
if (!statusSquares || statusSquares.length === 0) {
|
|
2459
|
-
return '';
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
return statusSquares.map(square =>
|
|
2463
|
-
\`<div class="status-square \${square.type}" data-tooltip="\${square.tooltip}"></div>\`
|
|
2464
|
-
).join('');
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
function getMessageType(message) {
|
|
2468
|
-
if (message.role === 'user') {
|
|
2469
|
-
return {
|
|
2470
|
-
type: 'pending',
|
|
2471
|
-
tooltip: 'User input'
|
|
2472
|
-
};
|
|
2473
|
-
} else if (message.role === 'assistant') {
|
|
2474
|
-
const content = message.content || '';
|
|
2475
|
-
|
|
2476
|
-
if (typeof content === 'string') {
|
|
2477
|
-
if (content.includes('[Tool:') || content.includes('tool_use')) {
|
|
2478
|
-
return {
|
|
2479
|
-
type: 'tool',
|
|
2480
|
-
tooltip: 'Tool execution'
|
|
2481
|
-
};
|
|
2482
|
-
} else if (content.includes('error') || content.includes('Error') || content.includes('failed')) {
|
|
2483
|
-
return {
|
|
2484
|
-
type: 'error',
|
|
2485
|
-
tooltip: 'Error in response'
|
|
2486
|
-
};
|
|
2487
|
-
} else {
|
|
2488
|
-
return {
|
|
2489
|
-
type: 'success',
|
|
2490
|
-
tooltip: 'Successful response'
|
|
2491
|
-
};
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
return {
|
|
2497
|
-
type: 'success',
|
|
2498
|
-
tooltip: 'Message'
|
|
2499
|
-
};
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
// Filter button handlers
|
|
2503
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
2504
|
-
const filterButtons = document.querySelectorAll('.filter-btn');
|
|
2505
|
-
|
|
2506
|
-
filterButtons.forEach(button => {
|
|
2507
|
-
button.addEventListener('click', function() {
|
|
2508
|
-
// Remove active class from all buttons
|
|
2509
|
-
filterButtons.forEach(btn => btn.classList.remove('active'));
|
|
2510
|
-
|
|
2511
|
-
// Add active class to clicked button
|
|
2512
|
-
this.classList.add('active');
|
|
2513
|
-
|
|
2514
|
-
// Update current filter
|
|
2515
|
-
currentFilter = this.dataset.filter;
|
|
2516
|
-
|
|
2517
|
-
// Update table
|
|
2518
|
-
updateSessionsTable();
|
|
2519
|
-
});
|
|
2520
|
-
});
|
|
2521
|
-
});
|
|
2522
|
-
|
|
2523
|
-
// Session detail functions
|
|
2524
|
-
async function showSessionDetail(sessionId) {
|
|
2525
|
-
currentSession = allConversations.find(conv => conv.id === sessionId);
|
|
2526
|
-
if (!currentSession) return;
|
|
2527
|
-
|
|
2528
|
-
// Hide sessions list and show detail
|
|
2529
|
-
document.querySelector('.filter-bar').style.display = 'none';
|
|
2530
|
-
document.querySelector('.sessions-table').style.display = 'none';
|
|
2531
|
-
document.getElementById('noSessions').style.display = 'none';
|
|
2532
|
-
document.getElementById('sessionDetail').classList.add('active');
|
|
2533
|
-
|
|
2534
|
-
// Update title
|
|
2535
|
-
document.getElementById('detailTitle').textContent = \`conversation: \${sessionId.substring(0, 8)}...\`;
|
|
2536
|
-
|
|
2537
|
-
// Load session info
|
|
2538
|
-
updateSessionInfo(currentSession);
|
|
2539
|
-
|
|
2540
|
-
// Load conversation history
|
|
2541
|
-
await loadConversationHistory(currentSession);
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
function showSessionsList() {
|
|
2545
|
-
document.getElementById('sessionDetail').classList.remove('active');
|
|
2546
|
-
document.querySelector('.filter-bar').style.display = 'flex';
|
|
2547
|
-
document.querySelector('.sessions-table').style.display = 'table';
|
|
2548
|
-
updateSessionsTable();
|
|
2549
|
-
currentSession = null;
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
function updateSessionInfo(session) {
|
|
2553
|
-
const container = document.getElementById('sessionInfo');
|
|
2554
|
-
|
|
2555
|
-
container.innerHTML = \`
|
|
2556
|
-
<div class="info-item">
|
|
2557
|
-
<div class="info-label">conversation id</div>
|
|
2558
|
-
<div class="info-value">\${session.id}</div>
|
|
2559
|
-
</div>
|
|
2560
|
-
<div class="info-item">
|
|
2561
|
-
<div class="info-label">project</div>
|
|
2562
|
-
<div class="info-value">\${session.project}</div>
|
|
2563
|
-
</div>
|
|
2564
|
-
<div class="info-item">
|
|
2565
|
-
<div class="info-label">messages</div>
|
|
2566
|
-
<div class="info-value">\${session.messageCount}</div>
|
|
2567
|
-
</div>
|
|
2568
|
-
<div class="info-item">
|
|
2569
|
-
<div class="info-label">total tokens</div>
|
|
2570
|
-
<div class="info-value">\${session.tokens.toLocaleString()}</div>
|
|
2571
|
-
</div>
|
|
2572
|
-
<div class="info-item">
|
|
2573
|
-
<div class="info-label">model</div>
|
|
2574
|
-
<div class="info-value model">\${session.modelInfo ? session.modelInfo.primaryModel : 'N/A'}</div>
|
|
2575
|
-
</div>
|
|
2576
|
-
<div class="info-item">
|
|
2577
|
-
<div class="info-label">service tier</div>
|
|
2578
|
-
<div class="info-value">\${session.modelInfo ? session.modelInfo.currentServiceTier : 'N/A'}</div>
|
|
2579
|
-
</div>
|
|
2580
|
-
<div class="info-item">
|
|
2581
|
-
<div class="info-label">token details</div>
|
|
2582
|
-
<div class="info-value">\${session.tokenUsage ? session.tokenUsage.inputTokens.toLocaleString() + ' in / ' + session.tokenUsage.outputTokens.toLocaleString() + ' out' : 'N/A'}</div>
|
|
2583
|
-
</div>
|
|
2584
|
-
\${session.tokenUsage && (session.tokenUsage.cacheCreationTokens > 0 || session.tokenUsage.cacheReadTokens > 0) ?
|
|
2585
|
-
\`<div class="info-item">
|
|
2586
|
-
<div class="info-label">cache tokens</div>
|
|
2587
|
-
<div class="info-value">\${session.tokenUsage.cacheCreationTokens.toLocaleString()} created / \${session.tokenUsage.cacheReadTokens.toLocaleString()} read</div>
|
|
2588
|
-
</div>\` : ''
|
|
2589
|
-
}
|
|
2590
|
-
<div class="info-item">
|
|
2591
|
-
<div class="info-label">file size</div>
|
|
2592
|
-
<div class="info-value">\${formatBytes(session.fileSize)}</div>
|
|
2593
|
-
</div>
|
|
2594
|
-
<div class="info-item">
|
|
2595
|
-
<div class="info-label">created</div>
|
|
2596
|
-
<div class="info-value">\${new Date(session.created).toLocaleString()}</div>
|
|
2597
|
-
</div>
|
|
2598
|
-
<div class="info-item">
|
|
2599
|
-
<div class="info-label">last modified</div>
|
|
2600
|
-
<div class="info-value">\${new Date(session.lastModified).toLocaleString()}</div>
|
|
2601
|
-
</div>
|
|
2602
|
-
<div class="info-item">
|
|
2603
|
-
<div class="info-label">conversation state</div>
|
|
2604
|
-
<div class="info-value conversation-state \${getStateClass(session.conversationState)}">\${session.conversationState}</div>
|
|
2605
|
-
</div>
|
|
2606
|
-
<div class="info-item">
|
|
2607
|
-
<div class="info-label">status</div>
|
|
2608
|
-
<div class="info-value status-\${session.status}">\${session.status}</div>
|
|
2609
|
-
</div>
|
|
2610
|
-
\`;
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
async function loadConversationHistory(session) {
|
|
2614
|
-
try {
|
|
2615
|
-
const response = await fetch(\`/api/session/\${session.id}\`);
|
|
2616
|
-
const sessionData = await response.json();
|
|
2617
|
-
|
|
2618
|
-
const container = document.getElementById('conversationHistory');
|
|
2619
|
-
const searchInput = document.getElementById('conversationSearch');
|
|
2620
|
-
|
|
2621
|
-
if (!sessionData.messages || sessionData.messages.length === 0) {
|
|
2622
|
-
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #7d8590;">no messages found</div>';
|
|
2623
|
-
searchInput.style.display = 'none';
|
|
2624
|
-
return;
|
|
2625
|
-
}
|
|
2626
|
-
searchInput.style.display = 'block';
|
|
2627
|
-
|
|
2628
|
-
const renderMessages = (filter = '') => {
|
|
2629
|
-
const lowerCaseFilter = filter.toLowerCase();
|
|
2630
|
-
const filteredMessages = sessionData.messages.filter(m =>
|
|
2631
|
-
(m.content || '').toLowerCase().includes(lowerCaseFilter)
|
|
2632
|
-
);
|
|
2633
|
-
|
|
2634
|
-
if (filteredMessages.length === 0) {
|
|
2635
|
-
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #7d8590;">No messages match your search.</div>';
|
|
2636
|
-
return;
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
const reversedMessages = filteredMessages.slice().reverse();
|
|
2640
|
-
|
|
2641
|
-
container.innerHTML = reversedMessages.map((message, index) => {
|
|
2642
|
-
const messageType = getMessageType(message);
|
|
2643
|
-
const messageNum = filteredMessages.length - index;
|
|
2644
|
-
return \`
|
|
2645
|
-
<div class="message">
|
|
2646
|
-
<div class="message-type-indicator \${messageType.type}" data-tooltip="\${messageType.tooltip}"></div>
|
|
2647
|
-
<div class="message-header">
|
|
2648
|
-
<div class="message-role \${message.role}">\${message.role}</div>
|
|
2649
|
-
<div class="message-time">
|
|
2650
|
-
#\${messageNum} • \${message.timestamp ? formatMessageTime(message.timestamp) : 'unknown time'}
|
|
2651
|
-
</div>
|
|
2652
|
-
</div>
|
|
2653
|
-
<div class="message-content">\${truncateContent(message.content || 'no content')}</div>
|
|
2654
|
-
</div>
|
|
2655
|
-
\`;
|
|
2656
|
-
}).join('');
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
renderMessages();
|
|
2660
|
-
|
|
2661
|
-
searchInput.addEventListener('input', () => {
|
|
2662
|
-
renderMessages(searchInput.value);
|
|
2663
|
-
});
|
|
2664
|
-
|
|
2665
|
-
} catch (error) {
|
|
2666
|
-
document.getElementById('conversationHistory').innerHTML =
|
|
2667
|
-
'<div style="padding: 20px; text-align: center; color: #f85149;">error loading conversation history</div>';
|
|
2668
|
-
console.error('Failed to load conversation history:', error);
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
function truncateContent(content, maxLength = 1000) {
|
|
2673
|
-
if (typeof content !== 'string') return 'no content';
|
|
2674
|
-
if (!content.trim()) return 'empty message';
|
|
2675
|
-
if (content.length <= maxLength) return content;
|
|
2676
|
-
return content.substring(0, maxLength) + '\\n\\n[... message truncated ...]';
|
|
2677
|
-
}
|
|
2678
|
-
|
|
2679
|
-
function formatBytes(bytes) {
|
|
2680
|
-
if (bytes === 0) return '0 B';
|
|
2681
|
-
const k = 1024;
|
|
2682
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
2683
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
2684
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
function exportSession() {
|
|
2688
|
-
if (!currentSession) return;
|
|
2689
|
-
|
|
2690
|
-
const format = document.getElementById('exportFormat').value;
|
|
2691
|
-
|
|
2692
|
-
// Fetch conversation history and export
|
|
2693
|
-
fetch(\`/api/session/\${currentSession.id}\`)
|
|
2694
|
-
.then(response => response.json())
|
|
2695
|
-
.then(sessionData => {
|
|
2696
|
-
if (format === 'csv') {
|
|
2697
|
-
exportSessionAsCSV(sessionData);
|
|
2698
|
-
} else if (format === 'json') {
|
|
2699
|
-
exportSessionAsJSON(sessionData);
|
|
2700
|
-
}
|
|
2701
|
-
})
|
|
2702
|
-
.catch(error => {
|
|
2703
|
-
console.error(\`Failed to export \${format.toUpperCase()}:\`, error);
|
|
2704
|
-
alert(\`Failed to export \${format.toUpperCase()}. Please try again.\`);
|
|
2705
|
-
});
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
function exportSessionAsCSV(sessionData) {
|
|
2709
|
-
// Create CSV content
|
|
2710
|
-
let csvContent = 'Conversation ID,Project,Message Count,Tokens,File Size,Created,Last Modified,Conversation State,Status\\n';
|
|
2711
|
-
csvContent += \`"\${currentSession.id}","\${currentSession.project}",\${currentSession.messageCount},\${currentSession.tokens},\${currentSession.fileSize},"\${new Date(currentSession.created).toISOString()}","\${new Date(currentSession.lastModified).toISOString()}","\${currentSession.conversationState}","\${currentSession.status}"\\n\\n\`;
|
|
2712
|
-
|
|
2713
|
-
csvContent += 'Message #,Role,Timestamp,Content\\n';
|
|
2714
|
-
|
|
2715
|
-
// Add conversation history
|
|
2716
|
-
if (sessionData.messages) {
|
|
2717
|
-
sessionData.messages.forEach((message, index) => {
|
|
2718
|
-
const content = (message.content || 'no content').replace(/"/g, '""');
|
|
2719
|
-
const timestamp = message.timestamp ? new Date(message.timestamp).toISOString() : 'unknown';
|
|
2720
|
-
csvContent += \`\${index + 1},"\${message.role}","\${timestamp}","\${content}"\\n\`;
|
|
2721
|
-
});
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
// Download CSV
|
|
2725
|
-
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
2726
|
-
downloadFile(blob, \`claude-conversation-\${currentSession.id.substring(0, 8)}.csv\`);
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
function exportSessionAsJSON(sessionData) {
|
|
2730
|
-
// Create comprehensive JSON export
|
|
2731
|
-
const exportData = {
|
|
2732
|
-
conversation: {
|
|
2733
|
-
id: currentSession.id,
|
|
2734
|
-
filename: currentSession.filename,
|
|
2735
|
-
project: currentSession.project,
|
|
2736
|
-
messageCount: currentSession.messageCount,
|
|
2737
|
-
tokens: currentSession.tokens,
|
|
2738
|
-
fileSize: currentSession.fileSize,
|
|
2739
|
-
created: currentSession.created,
|
|
2740
|
-
lastModified: currentSession.lastModified,
|
|
2741
|
-
conversationState: currentSession.conversationState,
|
|
2742
|
-
status: currentSession.status
|
|
2743
|
-
},
|
|
2744
|
-
messages: sessionData.messages || [],
|
|
2745
|
-
metadata: {
|
|
2746
|
-
exportedAt: new Date().toISOString(),
|
|
2747
|
-
exportFormat: 'json',
|
|
2748
|
-
toolVersion: '1.5.7'
|
|
2749
|
-
}
|
|
2750
|
-
};
|
|
2751
|
-
|
|
2752
|
-
// Download JSON
|
|
2753
|
-
const jsonString = JSON.stringify(exportData, null, 2);
|
|
2754
|
-
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
|
|
2755
|
-
downloadFile(blob, \`claude-conversation-\${currentSession.id.substring(0, 8)}.json\`);
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
function downloadFile(blob, filename) {
|
|
2759
|
-
const link = document.createElement('a');
|
|
2760
|
-
const url = URL.createObjectURL(blob);
|
|
2761
|
-
link.setAttribute('href', url);
|
|
2762
|
-
link.setAttribute('download', filename);
|
|
2763
|
-
link.style.visibility = 'hidden';
|
|
2764
|
-
document.body.appendChild(link);
|
|
2765
|
-
link.click();
|
|
2766
|
-
document.body.removeChild(link);
|
|
2767
|
-
URL.revokeObjectURL(url);
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
function refreshSessionDetail() {
|
|
2771
|
-
if (currentSession) {
|
|
2772
|
-
loadConversationHistory(currentSession);
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
// Manual refresh function
|
|
2777
|
-
async function forceRefresh() {
|
|
2778
|
-
try {
|
|
2779
|
-
const response = await fetch('/api/refresh');
|
|
2780
|
-
const result = await response.json();
|
|
2781
|
-
console.log('Manual refresh:', result);
|
|
2782
|
-
await loadData();
|
|
2783
|
-
} catch (error) {
|
|
2784
|
-
console.error('Failed to refresh:', error);
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
// Wait for DOM and Chart.js to load
|
|
2789
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
2790
|
-
// Check if Chart.js is loaded
|
|
2791
|
-
function initWhenReady() {
|
|
2792
|
-
if (typeof Chart !== 'undefined') {
|
|
2793
|
-
console.log('Chart.js loaded successfully');
|
|
2794
|
-
loadData();
|
|
2795
|
-
|
|
2796
|
-
// Automatic refresh for conversation data every 1 second for real-time updates
|
|
2797
|
-
setInterval(() => {
|
|
2798
|
-
loadConversationData();
|
|
2799
|
-
}, 1000);
|
|
2800
|
-
} else {
|
|
2801
|
-
console.log('Waiting for Chart.js to load...');
|
|
2802
|
-
setTimeout(initWhenReady, 100);
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
initWhenReady();
|
|
2807
|
-
|
|
2808
|
-
// Add event listeners for date inputs
|
|
2809
|
-
document.getElementById('dateFrom').addEventListener('change', refreshCharts);
|
|
2810
|
-
document.getElementById('dateTo').addEventListener('change', refreshCharts);
|
|
2811
|
-
|
|
2812
|
-
// Initialize notification button state
|
|
2813
|
-
updateNotificationButtonState();
|
|
2814
|
-
});
|
|
2815
|
-
|
|
2816
|
-
function updateNotificationButtonState() {
|
|
2817
|
-
const btn = document.getElementById('notificationBtn');
|
|
2818
|
-
if (!btn) return;
|
|
2819
|
-
|
|
2820
|
-
if (Notification.permission === 'granted') {
|
|
2821
|
-
notificationsEnabled = true;
|
|
2822
|
-
btn.textContent = 'notifications on';
|
|
2823
|
-
btn.style.borderColor = '#3fb950';
|
|
2824
|
-
btn.style.color = '#3fb950';
|
|
2825
|
-
} else if (Notification.permission === 'denied') {
|
|
2826
|
-
btn.textContent = 'notifications denied';
|
|
2827
|
-
btn.style.borderColor = '#f85149';
|
|
2828
|
-
btn.style.color = '#f85149';
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
// Add keyboard shortcut for refresh (F5 or Ctrl+R)
|
|
2833
|
-
document.addEventListener('keydown', function(e) {
|
|
2834
|
-
if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
|
|
2835
|
-
e.preventDefault();
|
|
2836
|
-
forceRefresh();
|
|
2837
|
-
}
|
|
2838
|
-
});
|
|
2839
|
-
</script>
|
|
2840
|
-
</body>
|
|
2841
|
-
</html>`;
|
|
2842
|
-
|
|
2843
|
-
await fs.writeFile(path.join(webDir, 'index.html'), htmlContent);
|
|
2844
|
-
}
|
|
2845
1154
|
|
|
2846
1155
|
module.exports = {
|
|
2847
1156
|
runAnalytics
|