claude-code-templates 1.5.17 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.5.17",
3
+ "version": "1.6.0",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Claude Code Analytics - Terminal</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
7
8
  <style>
8
9
  * {
9
10
  margin: 0;
@@ -120,6 +121,111 @@
120
121
  margin-top: 2px;
121
122
  }
122
123
 
124
+ .chart-controls {
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: space-between;
128
+ gap: 16px;
129
+ margin: 20px 0;
130
+ padding: 12px 0;
131
+ border-top: 1px solid #21262d;
132
+ border-bottom: 1px solid #21262d;
133
+ }
134
+
135
+ .chart-controls-left {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 16px;
139
+ }
140
+
141
+ .chart-controls-right {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 12px;
145
+ }
146
+
147
+ .date-control {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 8px;
151
+ }
152
+
153
+ .date-label {
154
+ color: #7d8590;
155
+ font-size: 0.875rem;
156
+ }
157
+
158
+ .date-input {
159
+ background: #21262d;
160
+ border: 1px solid #30363d;
161
+ color: #c9d1d9;
162
+ padding: 6px 12px;
163
+ border-radius: 4px;
164
+ font-family: inherit;
165
+ font-size: 0.875rem;
166
+ cursor: pointer;
167
+ }
168
+
169
+ .date-input:focus {
170
+ outline: none;
171
+ border-color: #d57455;
172
+ }
173
+
174
+ .refresh-btn {
175
+ background: none;
176
+ border: 1px solid #30363d;
177
+ color: #7d8590;
178
+ padding: 6px 12px;
179
+ border-radius: 4px;
180
+ cursor: pointer;
181
+ font-family: inherit;
182
+ font-size: 0.875rem;
183
+ transition: all 0.2s ease;
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 6px;
187
+ }
188
+
189
+ .refresh-btn:hover {
190
+ border-color: #d57455;
191
+ color: #d57455;
192
+ }
193
+
194
+ .refresh-btn.loading {
195
+ opacity: 0.6;
196
+ cursor: not-allowed;
197
+ }
198
+
199
+ .charts-container {
200
+ display: grid;
201
+ grid-template-columns: 2fr 1fr;
202
+ gap: 30px;
203
+ margin: 20px 0 30px 0;
204
+ }
205
+
206
+ .chart-card {
207
+ background: #161b22;
208
+ border: 1px solid #30363d;
209
+ border-radius: 6px;
210
+ padding: 20px;
211
+ position: relative;
212
+ }
213
+
214
+ .chart-title {
215
+ color: #d57455;
216
+ font-size: 0.875rem;
217
+ text-transform: uppercase;
218
+ margin-bottom: 16px;
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 8px;
222
+ }
223
+
224
+ .chart-canvas {
225
+ width: 100% !important;
226
+ height: 200px !important;
227
+ }
228
+
123
229
  .filter-bar {
124
230
  display: flex;
125
231
  align-items: center;
@@ -190,6 +296,23 @@
190
296
  .session-id {
191
297
  color: #d57455;
192
298
  font-family: monospace;
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 6px;
302
+ }
303
+
304
+ .process-indicator {
305
+ display: inline-block;
306
+ width: 6px;
307
+ height: 6px;
308
+ background: #3fb950;
309
+ border-radius: 50%;
310
+ animation: pulse 2s infinite;
311
+ cursor: help;
312
+ }
313
+
314
+ .process-indicator.orphan {
315
+ background: #f85149;
193
316
  }
194
317
 
195
318
  .session-id-container {
@@ -587,6 +710,35 @@
587
710
  gap: 20px;
588
711
  }
589
712
 
713
+ .chart-controls {
714
+ flex-direction: column;
715
+ gap: 12px;
716
+ align-items: stretch;
717
+ }
718
+
719
+ .chart-controls-left {
720
+ flex-direction: column;
721
+ gap: 12px;
722
+ }
723
+
724
+ .chart-controls-right {
725
+ justify-content: center;
726
+ }
727
+
728
+ .charts-container {
729
+ grid-template-columns: 1fr;
730
+ gap: 20px;
731
+ margin: 20px 0;
732
+ }
733
+
734
+ .chart-card {
735
+ padding: 16px;
736
+ }
737
+
738
+ .chart-canvas {
739
+ height: 180px !important;
740
+ }
741
+
590
742
  .filter-bar {
591
743
  flex-direction: column;
592
744
  align-items: flex-start;
@@ -664,6 +816,40 @@
664
816
  </div>
665
817
  </div>
666
818
 
819
+ <div class="chart-controls">
820
+ <div class="chart-controls-left">
821
+ <div class="date-control">
822
+ <span class="date-label">from:</span>
823
+ <input type="date" id="dateFrom" class="date-input">
824
+ </div>
825
+ <div class="date-control">
826
+ <span class="date-label">to:</span>
827
+ <input type="date" id="dateTo" class="date-input">
828
+ </div>
829
+ </div>
830
+ <div class="chart-controls-right">
831
+ <button class="refresh-btn" onclick="refreshCharts()" id="refreshBtn">
832
+ 🔄 refresh charts
833
+ </button>
834
+ </div>
835
+ </div>
836
+
837
+ <div class="charts-container">
838
+ <div class="chart-card">
839
+ <div class="chart-title">
840
+ 📊 token usage over time
841
+ </div>
842
+ <canvas id="tokenChart" class="chart-canvas"></canvas>
843
+ </div>
844
+
845
+ <div class="chart-card">
846
+ <div class="chart-title">
847
+ 🎯 project activity distribution
848
+ </div>
849
+ <canvas id="projectChart" class="chart-canvas"></canvas>
850
+ </div>
851
+ </div>
852
+
667
853
  <div class="filter-bar">
668
854
  <span class="filter-label">filter conversations:</span>
669
855
  <div class="filter-buttons">
@@ -730,6 +916,9 @@
730
916
  let allConversations = [];
731
917
  let currentFilter = 'active';
732
918
  let currentSession = null;
919
+ let tokenChart = null;
920
+ let projectChart = null;
921
+ let allData = null;
733
922
 
734
923
  async function loadData() {
735
924
  try {
@@ -746,6 +935,15 @@
746
935
 
747
936
  updateStats(data.summary);
748
937
  allConversations = data.conversations;
938
+ allData = data; // Store data globally for access
939
+ window.allData = data; // Keep for backward compatibility
940
+
941
+ // Initialize date inputs on first load
942
+ if (!document.getElementById('dateFrom').value) {
943
+ initializeDateInputs();
944
+ }
945
+
946
+ updateCharts(data);
749
947
  updateSessionsTable();
750
948
 
751
949
  } catch (error) {
@@ -769,6 +967,296 @@
769
967
  }
770
968
  }
771
969
 
970
+ function initializeDateInputs() {
971
+ const today = new Date();
972
+ const sevenDaysAgo = new Date(today);
973
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
974
+
975
+ document.getElementById('dateFrom').value = sevenDaysAgo.toISOString().split('T')[0];
976
+ document.getElementById('dateTo').value = today.toISOString().split('T')[0];
977
+ }
978
+
979
+ function getDateRange() {
980
+ const fromDate = new Date(document.getElementById('dateFrom').value);
981
+ const toDate = new Date(document.getElementById('dateTo').value);
982
+ toDate.setHours(23, 59, 59, 999); // Include the entire end date
983
+
984
+ return { fromDate, toDate };
985
+ }
986
+
987
+ function filterConversationsByDate(conversations) {
988
+ const { fromDate, toDate } = getDateRange();
989
+
990
+ return conversations.filter(conv => {
991
+ const convDate = new Date(conv.lastModified);
992
+ return convDate >= fromDate && convDate <= toDate;
993
+ });
994
+ }
995
+
996
+ function updateCharts(data) {
997
+ // Wait for Chart.js to load before creating charts
998
+ if (typeof Chart === 'undefined') {
999
+ console.log('Chart.js not loaded yet, retrying in 100ms...');
1000
+ setTimeout(() => updateCharts(data), 100);
1001
+ return;
1002
+ }
1003
+
1004
+ // Use ALL conversations but filter chart display by date range
1005
+ // This maintains the original behavior
1006
+
1007
+ // Update Token Usage Over Time Chart
1008
+ updateTokenChart(data.conversations);
1009
+
1010
+ // Update Project Activity Distribution Chart
1011
+ updateProjectChart(data.conversations);
1012
+ }
1013
+
1014
+ async function refreshCharts() {
1015
+ const refreshBtn = document.getElementById('refreshBtn');
1016
+ refreshBtn.classList.add('loading');
1017
+ refreshBtn.textContent = '🔄 refreshing...';
1018
+
1019
+ try {
1020
+ // Use existing data but re-filter and update charts
1021
+ if (allData) {
1022
+ updateCharts(allData);
1023
+ }
1024
+ } catch (error) {
1025
+ console.error('Error refreshing charts:', error);
1026
+ } finally {
1027
+ refreshBtn.classList.remove('loading');
1028
+ refreshBtn.textContent = '🔄 refresh charts';
1029
+ }
1030
+ }
1031
+
1032
+ function updateTokenChart(conversations) {
1033
+ // Check if Chart.js is available
1034
+ if (typeof Chart === 'undefined') {
1035
+ console.warn('Chart.js not available for updateTokenChart');
1036
+ return;
1037
+ }
1038
+
1039
+ // Prepare data for selected date range
1040
+ const { fromDate, toDate } = getDateRange();
1041
+ const dateRange = [];
1042
+
1043
+ const currentDate = new Date(fromDate);
1044
+ while (currentDate <= toDate) {
1045
+ dateRange.push({
1046
+ date: currentDate.toISOString().split('T')[0],
1047
+ label: currentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
1048
+ tokens: 0
1049
+ });
1050
+ currentDate.setDate(currentDate.getDate() + 1);
1051
+ }
1052
+
1053
+ // Aggregate tokens by day
1054
+ conversations.forEach(conv => {
1055
+ const convDate = new Date(conv.lastModified).toISOString().split('T')[0];
1056
+ const dayData = dateRange.find(day => day.date === convDate);
1057
+ if (dayData) {
1058
+ dayData.tokens += conv.tokens;
1059
+ }
1060
+ });
1061
+
1062
+ const ctx = document.getElementById('tokenChart').getContext('2d');
1063
+
1064
+ if (tokenChart) {
1065
+ tokenChart.destroy();
1066
+ }
1067
+
1068
+ tokenChart = new Chart(ctx, {
1069
+ type: 'line',
1070
+ data: {
1071
+ labels: dateRange.map(day => day.label),
1072
+ datasets: [{
1073
+ label: 'Tokens',
1074
+ data: dateRange.map(day => day.tokens),
1075
+ borderColor: '#d57455',
1076
+ backgroundColor: 'rgba(213, 116, 85, 0.1)',
1077
+ borderWidth: 2,
1078
+ pointBackgroundColor: '#d57455',
1079
+ pointBorderColor: '#d57455',
1080
+ pointRadius: 4,
1081
+ pointHoverRadius: 6,
1082
+ fill: true,
1083
+ tension: 0.3
1084
+ }]
1085
+ },
1086
+ options: {
1087
+ responsive: true,
1088
+ maintainAspectRatio: false,
1089
+ plugins: {
1090
+ legend: {
1091
+ display: false
1092
+ },
1093
+ tooltip: {
1094
+ backgroundColor: '#161b22',
1095
+ titleColor: '#d57455',
1096
+ bodyColor: '#c9d1d9',
1097
+ borderColor: '#30363d',
1098
+ borderWidth: 1,
1099
+ titleFont: {
1100
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1101
+ size: 12
1102
+ },
1103
+ bodyFont: {
1104
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1105
+ size: 11
1106
+ },
1107
+ callbacks: {
1108
+ title: function(context) {
1109
+ return context[0].label;
1110
+ },
1111
+ label: function(context) {
1112
+ return `Tokens: ${context.parsed.y.toLocaleString()}`;
1113
+ }
1114
+ }
1115
+ }
1116
+ },
1117
+ interaction: {
1118
+ intersect: false,
1119
+ mode: 'index'
1120
+ },
1121
+ hover: {
1122
+ animationDuration: 200
1123
+ },
1124
+ scales: {
1125
+ x: {
1126
+ grid: {
1127
+ color: '#30363d',
1128
+ borderColor: '#30363d'
1129
+ },
1130
+ ticks: {
1131
+ color: '#7d8590',
1132
+ font: {
1133
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1134
+ size: 11
1135
+ }
1136
+ }
1137
+ },
1138
+ y: {
1139
+ grid: {
1140
+ color: '#30363d',
1141
+ borderColor: '#30363d'
1142
+ },
1143
+ ticks: {
1144
+ color: '#7d8590',
1145
+ font: {
1146
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1147
+ size: 11
1148
+ },
1149
+ callback: function(value) {
1150
+ return value.toLocaleString();
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ });
1157
+ }
1158
+
1159
+ function updateProjectChart(conversations) {
1160
+ // Check if Chart.js is available
1161
+ if (typeof Chart === 'undefined') {
1162
+ console.warn('Chart.js not available for updateProjectChart');
1163
+ return;
1164
+ }
1165
+
1166
+ // Aggregate data by project
1167
+ const projectData = {};
1168
+
1169
+ conversations.forEach(conv => {
1170
+ if (!projectData[conv.project]) {
1171
+ projectData[conv.project] = 0;
1172
+ }
1173
+ projectData[conv.project] += conv.tokens;
1174
+ });
1175
+
1176
+ // Get top 5 projects and group others
1177
+ const sortedProjects = Object.entries(projectData)
1178
+ .sort(([,a], [,b]) => b - a)
1179
+ .slice(0, 5);
1180
+
1181
+ const othersTotal = Object.entries(projectData)
1182
+ .slice(5)
1183
+ .reduce((sum, [,tokens]) => sum + tokens, 0);
1184
+
1185
+ if (othersTotal > 0) {
1186
+ sortedProjects.push(['others', othersTotal]);
1187
+ }
1188
+
1189
+ // Terminal-style colors
1190
+ const colors = [
1191
+ '#d57455', // Orange
1192
+ '#3fb950', // Green
1193
+ '#a5d6ff', // Blue
1194
+ '#f97316', // Orange variant
1195
+ '#c9d1d9', // Light gray
1196
+ '#7d8590' // Gray
1197
+ ];
1198
+
1199
+ const ctx = document.getElementById('projectChart').getContext('2d');
1200
+
1201
+ if (projectChart) {
1202
+ projectChart.destroy();
1203
+ }
1204
+
1205
+ projectChart = new Chart(ctx, {
1206
+ type: 'doughnut',
1207
+ data: {
1208
+ labels: sortedProjects.map(([project]) => project),
1209
+ datasets: [{
1210
+ data: sortedProjects.map(([,tokens]) => tokens),
1211
+ backgroundColor: colors.slice(0, sortedProjects.length),
1212
+ borderColor: '#161b22',
1213
+ borderWidth: 2,
1214
+ hoverBorderWidth: 3
1215
+ }]
1216
+ },
1217
+ options: {
1218
+ responsive: true,
1219
+ maintainAspectRatio: false,
1220
+ plugins: {
1221
+ legend: {
1222
+ position: 'bottom',
1223
+ labels: {
1224
+ color: '#7d8590',
1225
+ font: {
1226
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1227
+ size: 10
1228
+ },
1229
+ padding: 15,
1230
+ usePointStyle: true,
1231
+ pointStyle: 'circle'
1232
+ }
1233
+ },
1234
+ tooltip: {
1235
+ backgroundColor: '#161b22',
1236
+ titleColor: '#d57455',
1237
+ bodyColor: '#c9d1d9',
1238
+ borderColor: '#30363d',
1239
+ borderWidth: 1,
1240
+ titleFont: {
1241
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace'
1242
+ },
1243
+ bodyFont: {
1244
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace'
1245
+ },
1246
+ callbacks: {
1247
+ label: function(context) {
1248
+ const total = context.dataset.data.reduce((sum, value) => sum + value, 0);
1249
+ const percentage = ((context.parsed / total) * 100).toFixed(1);
1250
+ return `${context.label}: ${context.parsed.toLocaleString()} tokens (${percentage}%)`;
1251
+ }
1252
+ }
1253
+ }
1254
+ },
1255
+ cutout: '60%'
1256
+ }
1257
+ });
1258
+ }
1259
+
772
1260
  function updateSessionsTable() {
773
1261
  const tableBody = document.getElementById('sessionsTable');
774
1262
  const noSessionsDiv = document.getElementById('noSessions');
@@ -791,7 +1279,10 @@
791
1279
  <tr onclick="showSessionDetail('${conv.id}')" style="cursor: pointer;">
792
1280
  <td>
793
1281
  <div class="session-id-container">
794
- <div class="session-id">${conv.id.substring(0, 8)}...</div>
1282
+ <div class="session-id">
1283
+ ${conv.id.substring(0, 8)}...
1284
+ ${conv.runningProcess ? `<span class="process-indicator" title="Active claude process (PID: ${conv.runningProcess.pid})"></span>` : ''}
1285
+ </div>
795
1286
  <div class="status-squares">
796
1287
  ${generateStatusSquaresHTML(conv.statusSquares || [])}
797
1288
  </div>
@@ -806,6 +1297,33 @@
806
1297
  <td class="status-${conv.status}">${conv.status}</td>
807
1298
  </tr>
808
1299
  `).join('');
1300
+
1301
+ // NEW: Add orphan processes (active claude commands without conversation)
1302
+ if (window.allData && window.allData.orphanProcesses && window.allData.orphanProcesses.length > 0) {
1303
+ const orphanRows = window.allData.orphanProcesses.map(process => `
1304
+ <tr style="background: rgba(248, 81, 73, 0.1); cursor: default;">
1305
+ <td>
1306
+ <div class="session-id-container">
1307
+ <div class="session-id">
1308
+ orphan-${process.pid}
1309
+ <span class="process-indicator orphan" title="Orphan claude process (PID: ${process.pid})"></span>
1310
+ </div>
1311
+ </div>
1312
+ </td>
1313
+ <td class="session-project">${process.workingDir}</td>
1314
+ <td class="session-model">Unknown</td>
1315
+ <td class="session-messages">-</td>
1316
+ <td class="session-tokens">-</td>
1317
+ <td class="session-time">Running</td>
1318
+ <td class="conversation-state">Active process</td>
1319
+ <td class="status-active">orphan</td>
1320
+ </tr>
1321
+ `).join('');
1322
+
1323
+ if (currentFilter === 'active' || currentFilter === 'all') {
1324
+ tableBody.innerHTML += orphanRows;
1325
+ }
1326
+ }
809
1327
  }
810
1328
 
811
1329
  function formatTime(date) {
@@ -1172,11 +1690,26 @@
1172
1690
  }
1173
1691
  }
1174
1692
 
1175
- // Load initial data
1176
- loadData();
1177
-
1178
- // Refresh data every 5 seconds
1179
- setInterval(loadData, 5000);
1693
+ // Wait for DOM and Chart.js to load
1694
+ document.addEventListener('DOMContentLoaded', function() {
1695
+ // Check if Chart.js is loaded
1696
+ function initWhenReady() {
1697
+ if (typeof Chart !== 'undefined') {
1698
+ console.log('Chart.js loaded successfully');
1699
+ loadData();
1700
+ // No automatic refresh - manual only
1701
+ } else {
1702
+ console.log('Waiting for Chart.js to load...');
1703
+ setTimeout(initWhenReady, 100);
1704
+ }
1705
+ }
1706
+
1707
+ initWhenReady();
1708
+
1709
+ // Add event listeners for date inputs
1710
+ document.getElementById('dateFrom').addEventListener('change', refreshCharts);
1711
+ document.getElementById('dateTo').addEventListener('change', refreshCharts);
1712
+ });
1180
1713
 
1181
1714
  // Add keyboard shortcut for refresh (F5 or Ctrl+R)
1182
1715
  document.addEventListener('keydown', function(e) {
package/src/analytics.js CHANGED
@@ -51,6 +51,9 @@ class ClaudeAnalytics {
51
51
  const projects = await this.loadActiveProjects();
52
52
  this.data.activeProjects = projects;
53
53
 
54
+ // NEW: Detect active Claude processes and enrich data
55
+ await this.enrichWithRunningProcesses();
56
+
54
57
  // Calculate summary statistics
55
58
  this.data.summary = this.calculateSummary(conversations, projects);
56
59
 
@@ -285,6 +288,94 @@ class ClaudeAnalytics {
285
288
  return null;
286
289
  }
287
290
 
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
+
337
+ // NEW: Enrich existing conversations with process information
338
+ async enrichWithRunningProcesses() {
339
+ try {
340
+ const runningProcesses = await this.detectRunningClaudeProcesses();
341
+
342
+ // Add active process information to each conversation
343
+ this.data.conversations.forEach(conversation => {
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
+ } else {
365
+ conversation.runningProcess = null;
366
+ }
367
+ });
368
+
369
+ // Disable orphan process detection to reduce noise
370
+ this.data.orphanProcesses = [];
371
+
372
+ console.log(chalk.blue(`🔍 Found ${runningProcesses.length} running Claude processes`));
373
+
374
+ } catch (error) {
375
+ console.warn(chalk.yellow('Warning: Could not detect running processes'), error.message);
376
+ }
377
+ }
378
+
288
379
  extractProjectFromConversation(messages) {
289
380
  // Try to extract project information from conversation
290
381
  for (const message of messages.slice(0, 5)) {
@@ -313,16 +404,16 @@ class ClaudeAnalytics {
313
404
  const lastMessageTime = new Date(lastMessage.timestamp);
314
405
  const lastMessageMinutesAgo = (now - lastMessageTime) / (1000 * 60);
315
406
 
316
- // Simplified status logic - typing is now part of active
407
+ // More balanced logic - active conversations and recent activity
317
408
  if (lastMessage.role === 'user' && lastMessageMinutesAgo < 3) {
318
409
  return 'active';
319
410
  } else if (lastMessage.role === 'assistant' && lastMessageMinutesAgo < 5) {
320
411
  return 'active';
321
412
  }
322
413
 
323
- // Fallback to file modification time for edge cases
414
+ // Use file modification time for recent activity
324
415
  if (minutesAgo < 5) return 'active';
325
- if (minutesAgo < 60) return 'recent';
416
+ if (minutesAgo < 30) return 'recent';
326
417
  return 'inactive';
327
418
  }
328
419
 
@@ -636,6 +727,11 @@ class ClaudeAnalytics {
636
727
  console.log(chalk.blue('⏱️ Periodic data refresh...'));
637
728
  await this.loadInitialData();
638
729
  }, 30000); // Every 30 seconds
730
+
731
+ // NEW: More frequent updates for active processes (every 10 seconds)
732
+ setInterval(async () => {
733
+ await this.enrichWithRunningProcesses();
734
+ }, 10000);
639
735
  }
640
736
 
641
737
  setupWebServer() {
@@ -822,6 +918,7 @@ async function createWebDashboard() {
822
918
  <meta charset="UTF-8">
823
919
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
824
920
  <title>Claude Code Analytics - Terminal</title>
921
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
825
922
  <style>
826
923
  * {
827
924
  margin: 0;
@@ -938,6 +1035,111 @@ async function createWebDashboard() {
938
1035
  margin-top: 2px;
939
1036
  }
940
1037
 
1038
+ .chart-controls {
1039
+ display: flex;
1040
+ align-items: center;
1041
+ justify-content: space-between;
1042
+ gap: 16px;
1043
+ margin: 20px 0;
1044
+ padding: 12px 0;
1045
+ border-top: 1px solid #21262d;
1046
+ border-bottom: 1px solid #21262d;
1047
+ }
1048
+
1049
+ .chart-controls-left {
1050
+ display: flex;
1051
+ align-items: center;
1052
+ gap: 16px;
1053
+ }
1054
+
1055
+ .chart-controls-right {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ gap: 12px;
1059
+ }
1060
+
1061
+ .date-control {
1062
+ display: flex;
1063
+ align-items: center;
1064
+ gap: 8px;
1065
+ }
1066
+
1067
+ .date-label {
1068
+ color: #7d8590;
1069
+ font-size: 0.875rem;
1070
+ }
1071
+
1072
+ .date-input {
1073
+ background: #21262d;
1074
+ border: 1px solid #30363d;
1075
+ color: #c9d1d9;
1076
+ padding: 6px 12px;
1077
+ border-radius: 4px;
1078
+ font-family: inherit;
1079
+ font-size: 0.875rem;
1080
+ cursor: pointer;
1081
+ }
1082
+
1083
+ .date-input:focus {
1084
+ outline: none;
1085
+ border-color: #d57455;
1086
+ }
1087
+
1088
+ .refresh-btn {
1089
+ background: none;
1090
+ border: 1px solid #30363d;
1091
+ color: #7d8590;
1092
+ padding: 6px 12px;
1093
+ border-radius: 4px;
1094
+ cursor: pointer;
1095
+ font-family: inherit;
1096
+ font-size: 0.875rem;
1097
+ transition: all 0.2s ease;
1098
+ display: flex;
1099
+ align-items: center;
1100
+ gap: 6px;
1101
+ }
1102
+
1103
+ .refresh-btn:hover {
1104
+ border-color: #d57455;
1105
+ color: #d57455;
1106
+ }
1107
+
1108
+ .refresh-btn.loading {
1109
+ opacity: 0.6;
1110
+ cursor: not-allowed;
1111
+ }
1112
+
1113
+ .charts-container {
1114
+ display: grid;
1115
+ grid-template-columns: 2fr 1fr;
1116
+ gap: 30px;
1117
+ margin: 20px 0 30px 0;
1118
+ }
1119
+
1120
+ .chart-card {
1121
+ background: #161b22;
1122
+ border: 1px solid #30363d;
1123
+ border-radius: 6px;
1124
+ padding: 20px;
1125
+ position: relative;
1126
+ }
1127
+
1128
+ .chart-title {
1129
+ color: #d57455;
1130
+ font-size: 0.875rem;
1131
+ text-transform: uppercase;
1132
+ margin-bottom: 16px;
1133
+ display: flex;
1134
+ align-items: center;
1135
+ gap: 8px;
1136
+ }
1137
+
1138
+ .chart-canvas {
1139
+ width: 100% !important;
1140
+ height: 200px !important;
1141
+ }
1142
+
941
1143
  .filter-bar {
942
1144
  display: flex;
943
1145
  align-items: center;
@@ -1008,6 +1210,23 @@ async function createWebDashboard() {
1008
1210
  .session-id {
1009
1211
  color: #d57455;
1010
1212
  font-family: monospace;
1213
+ display: flex;
1214
+ align-items: center;
1215
+ gap: 6px;
1216
+ }
1217
+
1218
+ .process-indicator {
1219
+ display: inline-block;
1220
+ width: 6px;
1221
+ height: 6px;
1222
+ background: #3fb950;
1223
+ border-radius: 50%;
1224
+ animation: pulse 2s infinite;
1225
+ cursor: help;
1226
+ }
1227
+
1228
+ .process-indicator.orphan {
1229
+ background: #f85149;
1011
1230
  }
1012
1231
 
1013
1232
  .session-id-container {
@@ -1405,6 +1624,35 @@ async function createWebDashboard() {
1405
1624
  gap: 20px;
1406
1625
  }
1407
1626
 
1627
+ .chart-controls {
1628
+ flex-direction: column;
1629
+ gap: 12px;
1630
+ align-items: stretch;
1631
+ }
1632
+
1633
+ .chart-controls-left {
1634
+ flex-direction: column;
1635
+ gap: 12px;
1636
+ }
1637
+
1638
+ .chart-controls-right {
1639
+ justify-content: center;
1640
+ }
1641
+
1642
+ .charts-container {
1643
+ grid-template-columns: 1fr;
1644
+ gap: 20px;
1645
+ margin: 20px 0;
1646
+ }
1647
+
1648
+ .chart-card {
1649
+ padding: 16px;
1650
+ }
1651
+
1652
+ .chart-canvas {
1653
+ height: 180px !important;
1654
+ }
1655
+
1408
1656
  .filter-bar {
1409
1657
  flex-direction: column;
1410
1658
  align-items: flex-start;
@@ -1482,6 +1730,40 @@ async function createWebDashboard() {
1482
1730
  </div>
1483
1731
  </div>
1484
1732
 
1733
+ <div class="chart-controls">
1734
+ <div class="chart-controls-left">
1735
+ <div class="date-control">
1736
+ <span class="date-label">from:</span>
1737
+ <input type="date" id="dateFrom" class="date-input">
1738
+ </div>
1739
+ <div class="date-control">
1740
+ <span class="date-label">to:</span>
1741
+ <input type="date" id="dateTo" class="date-input">
1742
+ </div>
1743
+ </div>
1744
+ <div class="chart-controls-right">
1745
+ <button class="refresh-btn" onclick="refreshCharts()" id="refreshBtn">
1746
+ refresh charts
1747
+ </button>
1748
+ </div>
1749
+ </div>
1750
+
1751
+ <div class="charts-container">
1752
+ <div class="chart-card">
1753
+ <div class="chart-title">
1754
+ 📊 token usage over time
1755
+ </div>
1756
+ <canvas id="tokenChart" class="chart-canvas"></canvas>
1757
+ </div>
1758
+
1759
+ <div class="chart-card">
1760
+ <div class="chart-title">
1761
+ 🎯 project activity distribution
1762
+ </div>
1763
+ <canvas id="projectChart" class="chart-canvas"></canvas>
1764
+ </div>
1765
+ </div>
1766
+
1485
1767
  <div class="filter-bar">
1486
1768
  <span class="filter-label">filter conversations:</span>
1487
1769
  <div class="filter-buttons">
@@ -1548,6 +1830,9 @@ async function createWebDashboard() {
1548
1830
  let allConversations = [];
1549
1831
  let currentFilter = 'active';
1550
1832
  let currentSession = null;
1833
+ let tokenChart = null;
1834
+ let projectChart = null;
1835
+ let allData = null;
1551
1836
 
1552
1837
  async function loadData() {
1553
1838
  try {
@@ -1564,6 +1849,15 @@ async function createWebDashboard() {
1564
1849
 
1565
1850
  updateStats(data.summary);
1566
1851
  allConversations = data.conversations;
1852
+ allData = data; // Store data globally for access
1853
+ window.allData = data; // Keep for backward compatibility
1854
+
1855
+ // Initialize date inputs on first load
1856
+ if (!document.getElementById('dateFrom').value) {
1857
+ initializeDateInputs();
1858
+ }
1859
+
1860
+ updateCharts(data);
1567
1861
  updateSessionsTable();
1568
1862
 
1569
1863
  } catch (error) {
@@ -1587,6 +1881,296 @@ async function createWebDashboard() {
1587
1881
  }
1588
1882
  }
1589
1883
 
1884
+ function initializeDateInputs() {
1885
+ const today = new Date();
1886
+ const sevenDaysAgo = new Date(today);
1887
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
1888
+
1889
+ document.getElementById('dateFrom').value = sevenDaysAgo.toISOString().split('T')[0];
1890
+ document.getElementById('dateTo').value = today.toISOString().split('T')[0];
1891
+ }
1892
+
1893
+ function getDateRange() {
1894
+ const fromDate = new Date(document.getElementById('dateFrom').value);
1895
+ const toDate = new Date(document.getElementById('dateTo').value);
1896
+ toDate.setHours(23, 59, 59, 999); // Include the entire end date
1897
+
1898
+ return { fromDate, toDate };
1899
+ }
1900
+
1901
+ function filterConversationsByDate(conversations) {
1902
+ const { fromDate, toDate } = getDateRange();
1903
+
1904
+ return conversations.filter(conv => {
1905
+ const convDate = new Date(conv.lastModified);
1906
+ return convDate >= fromDate && convDate <= toDate;
1907
+ });
1908
+ }
1909
+
1910
+ function updateCharts(data) {
1911
+ // Wait for Chart.js to load before creating charts
1912
+ if (typeof Chart === 'undefined') {
1913
+ console.log('Chart.js not loaded yet, retrying in 100ms...');
1914
+ setTimeout(() => updateCharts(data), 100);
1915
+ return;
1916
+ }
1917
+
1918
+ // Use ALL conversations but filter chart display by date range
1919
+ // This maintains the original behavior
1920
+
1921
+ // Update Token Usage Over Time Chart
1922
+ updateTokenChart(data.conversations);
1923
+
1924
+ // Update Project Activity Distribution Chart
1925
+ updateProjectChart(data.conversations);
1926
+ }
1927
+
1928
+ async function refreshCharts() {
1929
+ const refreshBtn = document.getElementById('refreshBtn');
1930
+ refreshBtn.classList.add('loading');
1931
+ refreshBtn.textContent = '🔄 refreshing...';
1932
+
1933
+ try {
1934
+ // Use existing data but re-filter and update charts
1935
+ if (allData) {
1936
+ updateCharts(allData);
1937
+ }
1938
+ } catch (error) {
1939
+ console.error('Error refreshing charts:', error);
1940
+ } finally {
1941
+ refreshBtn.classList.remove('loading');
1942
+ refreshBtn.textContent = '🔄 refresh charts';
1943
+ }
1944
+ }
1945
+
1946
+ function updateTokenChart(conversations) {
1947
+ // Check if Chart.js is available
1948
+ if (typeof Chart === 'undefined') {
1949
+ console.warn('Chart.js not available for updateTokenChart');
1950
+ return;
1951
+ }
1952
+
1953
+ // Prepare data for selected date range
1954
+ const { fromDate, toDate } = getDateRange();
1955
+ const dateRange = [];
1956
+
1957
+ const currentDate = new Date(fromDate);
1958
+ while (currentDate <= toDate) {
1959
+ dateRange.push({
1960
+ date: currentDate.toISOString().split('T')[0],
1961
+ label: currentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
1962
+ tokens: 0
1963
+ });
1964
+ currentDate.setDate(currentDate.getDate() + 1);
1965
+ }
1966
+
1967
+ // Aggregate tokens by day
1968
+ conversations.forEach(conv => {
1969
+ const convDate = new Date(conv.lastModified).toISOString().split('T')[0];
1970
+ const dayData = dateRange.find(day => day.date === convDate);
1971
+ if (dayData) {
1972
+ dayData.tokens += conv.tokens;
1973
+ }
1974
+ });
1975
+
1976
+ const ctx = document.getElementById('tokenChart').getContext('2d');
1977
+
1978
+ if (tokenChart) {
1979
+ tokenChart.destroy();
1980
+ }
1981
+
1982
+ tokenChart = new Chart(ctx, {
1983
+ type: 'line',
1984
+ data: {
1985
+ labels: dateRange.map(day => day.label),
1986
+ datasets: [{
1987
+ label: 'Tokens',
1988
+ data: dateRange.map(day => day.tokens),
1989
+ borderColor: '#d57455',
1990
+ backgroundColor: 'rgba(213, 116, 85, 0.1)',
1991
+ borderWidth: 2,
1992
+ pointBackgroundColor: '#d57455',
1993
+ pointBorderColor: '#d57455',
1994
+ pointRadius: 4,
1995
+ pointHoverRadius: 6,
1996
+ fill: true,
1997
+ tension: 0.3
1998
+ }]
1999
+ },
2000
+ options: {
2001
+ responsive: true,
2002
+ maintainAspectRatio: false,
2003
+ plugins: {
2004
+ legend: {
2005
+ display: false
2006
+ },
2007
+ tooltip: {
2008
+ backgroundColor: '#161b22',
2009
+ titleColor: '#d57455',
2010
+ bodyColor: '#c9d1d9',
2011
+ borderColor: '#30363d',
2012
+ borderWidth: 1,
2013
+ titleFont: {
2014
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
2015
+ size: 12
2016
+ },
2017
+ bodyFont: {
2018
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
2019
+ size: 11
2020
+ },
2021
+ callbacks: {
2022
+ title: function(context) {
2023
+ return context[0].label;
2024
+ },
2025
+ label: function(context) {
2026
+ return \`Tokens: \${context.parsed.y.toLocaleString()}\`;
2027
+ }
2028
+ }
2029
+ }
2030
+ },
2031
+ interaction: {
2032
+ intersect: false,
2033
+ mode: 'index'
2034
+ },
2035
+ hover: {
2036
+ animationDuration: 200
2037
+ },
2038
+ scales: {
2039
+ x: {
2040
+ grid: {
2041
+ color: '#30363d',
2042
+ borderColor: '#30363d'
2043
+ },
2044
+ ticks: {
2045
+ color: '#7d8590',
2046
+ font: {
2047
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
2048
+ size: 11
2049
+ }
2050
+ }
2051
+ },
2052
+ y: {
2053
+ grid: {
2054
+ color: '#30363d',
2055
+ borderColor: '#30363d'
2056
+ },
2057
+ ticks: {
2058
+ color: '#7d8590',
2059
+ font: {
2060
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
2061
+ size: 11
2062
+ },
2063
+ callback: function(value) {
2064
+ return value.toLocaleString();
2065
+ }
2066
+ }
2067
+ }
2068
+ }
2069
+ }
2070
+ });
2071
+ }
2072
+
2073
+ function updateProjectChart(conversations) {
2074
+ // Check if Chart.js is available
2075
+ if (typeof Chart === 'undefined') {
2076
+ console.warn('Chart.js not available for updateProjectChart');
2077
+ return;
2078
+ }
2079
+
2080
+ // Aggregate data by project
2081
+ const projectData = {};
2082
+
2083
+ conversations.forEach(conv => {
2084
+ if (!projectData[conv.project]) {
2085
+ projectData[conv.project] = 0;
2086
+ }
2087
+ projectData[conv.project] += conv.tokens;
2088
+ });
2089
+
2090
+ // Get top 5 projects and group others
2091
+ const sortedProjects = Object.entries(projectData)
2092
+ .sort(([,a], [,b]) => b - a)
2093
+ .slice(0, 5);
2094
+
2095
+ const othersTotal = Object.entries(projectData)
2096
+ .slice(5)
2097
+ .reduce((sum, [,tokens]) => sum + tokens, 0);
2098
+
2099
+ if (othersTotal > 0) {
2100
+ sortedProjects.push(['others', othersTotal]);
2101
+ }
2102
+
2103
+ // Terminal-style colors
2104
+ const colors = [
2105
+ '#d57455', // Orange
2106
+ '#3fb950', // Green
2107
+ '#a5d6ff', // Blue
2108
+ '#f97316', // Orange variant
2109
+ '#c9d1d9', // Light gray
2110
+ '#7d8590' // Gray
2111
+ ];
2112
+
2113
+ const ctx = document.getElementById('projectChart').getContext('2d');
2114
+
2115
+ if (projectChart) {
2116
+ projectChart.destroy();
2117
+ }
2118
+
2119
+ projectChart = new Chart(ctx, {
2120
+ type: 'doughnut',
2121
+ data: {
2122
+ labels: sortedProjects.map(([project]) => project),
2123
+ datasets: [{
2124
+ data: sortedProjects.map(([,tokens]) => tokens),
2125
+ backgroundColor: colors.slice(0, sortedProjects.length),
2126
+ borderColor: '#161b22',
2127
+ borderWidth: 2,
2128
+ hoverBorderWidth: 3
2129
+ }]
2130
+ },
2131
+ options: {
2132
+ responsive: true,
2133
+ maintainAspectRatio: false,
2134
+ plugins: {
2135
+ legend: {
2136
+ position: 'bottom',
2137
+ labels: {
2138
+ color: '#7d8590',
2139
+ font: {
2140
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
2141
+ size: 10
2142
+ },
2143
+ padding: 15,
2144
+ usePointStyle: true,
2145
+ pointStyle: 'circle'
2146
+ }
2147
+ },
2148
+ tooltip: {
2149
+ backgroundColor: '#161b22',
2150
+ titleColor: '#d57455',
2151
+ bodyColor: '#c9d1d9',
2152
+ borderColor: '#30363d',
2153
+ borderWidth: 1,
2154
+ titleFont: {
2155
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace'
2156
+ },
2157
+ bodyFont: {
2158
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace'
2159
+ },
2160
+ callbacks: {
2161
+ label: function(context) {
2162
+ const total = context.dataset.data.reduce((sum, value) => sum + value, 0);
2163
+ const percentage = ((context.parsed / total) * 100).toFixed(1);
2164
+ return \`\${context.label}: \${context.parsed.toLocaleString()} tokens (\${percentage}%)\`;
2165
+ }
2166
+ }
2167
+ }
2168
+ },
2169
+ cutout: '60%'
2170
+ }
2171
+ });
2172
+ }
2173
+
1590
2174
  function updateSessionsTable() {
1591
2175
  const tableBody = document.getElementById('sessionsTable');
1592
2176
  const noSessionsDiv = document.getElementById('noSessions');
@@ -1609,7 +2193,10 @@ async function createWebDashboard() {
1609
2193
  <tr onclick="showSessionDetail('\${conv.id}')" style="cursor: pointer;">
1610
2194
  <td>
1611
2195
  <div class="session-id-container">
1612
- <div class="session-id">\${conv.id.substring(0, 8)}...</div>
2196
+ <div class="session-id">
2197
+ \${conv.id.substring(0, 8)}...
2198
+ \${conv.runningProcess ? \`<span class="process-indicator" title="Active claude process (PID: \${conv.runningProcess.pid})"></span>\` : ''}
2199
+ </div>
1613
2200
  <div class="status-squares">
1614
2201
  \${generateStatusSquaresHTML(conv.statusSquares || [])}
1615
2202
  </div>
@@ -1624,6 +2211,33 @@ async function createWebDashboard() {
1624
2211
  <td class="status-\${conv.status}">\${conv.status}</td>
1625
2212
  </tr>
1626
2213
  \`).join('');
2214
+
2215
+ // NEW: Add orphan processes (active claude commands without conversation)
2216
+ if (window.allData && window.allData.orphanProcesses && window.allData.orphanProcesses.length > 0) {
2217
+ const orphanRows = window.allData.orphanProcesses.map(process => \`
2218
+ <tr style="background: rgba(248, 81, 73, 0.1); cursor: default;">
2219
+ <td>
2220
+ <div class="session-id-container">
2221
+ <div class="session-id">
2222
+ orphan-\${process.pid}
2223
+ <span class="process-indicator orphan" title="Orphan claude process (PID: \${process.pid})"></span>
2224
+ </div>
2225
+ </div>
2226
+ </td>
2227
+ <td class="session-project">\${process.workingDir}</td>
2228
+ <td class="session-model">Unknown</td>
2229
+ <td class="session-messages">-</td>
2230
+ <td class="session-tokens">-</td>
2231
+ <td class="session-time">Running</td>
2232
+ <td class="conversation-state">Active process</td>
2233
+ <td class="status-active">orphan</td>
2234
+ </tr>
2235
+ \`).join('');
2236
+
2237
+ if (currentFilter === 'active' || currentFilter === 'all') {
2238
+ tableBody.innerHTML += orphanRows;
2239
+ }
2240
+ }
1627
2241
  }
1628
2242
 
1629
2243
  function formatTime(date) {
@@ -1990,11 +2604,26 @@ async function createWebDashboard() {
1990
2604
  }
1991
2605
  }
1992
2606
 
1993
- // Load initial data
1994
- loadData();
1995
-
1996
- // Refresh data every 5 seconds
1997
- setInterval(loadData, 5000);
2607
+ // Wait for DOM and Chart.js to load
2608
+ document.addEventListener('DOMContentLoaded', function() {
2609
+ // Check if Chart.js is loaded
2610
+ function initWhenReady() {
2611
+ if (typeof Chart !== 'undefined') {
2612
+ console.log('Chart.js loaded successfully');
2613
+ loadData();
2614
+ // No automatic refresh - manual only
2615
+ } else {
2616
+ console.log('Waiting for Chart.js to load...');
2617
+ setTimeout(initWhenReady, 100);
2618
+ }
2619
+ }
2620
+
2621
+ initWhenReady();
2622
+
2623
+ // Add event listeners for date inputs
2624
+ document.getElementById('dateFrom').addEventListener('change', refreshCharts);
2625
+ document.getElementById('dateTo').addEventListener('change', refreshCharts);
2626
+ });
1998
2627
 
1999
2628
  // Add keyboard shortcut for refresh (F5 or Ctrl+R)
2000
2629
  document.addEventListener('keydown', function(e) {