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 +1 -1
- package/src/analytics-web/index.html +539 -6
- package/src/analytics.js +638 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-templates",
|
|
3
|
-
"version": "1.
|
|
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"
|
|
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
|
-
//
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
414
|
+
// Use file modification time for recent activity
|
|
324
415
|
if (minutesAgo < 5) return 'active';
|
|
325
|
-
if (minutesAgo <
|
|
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"
|
|
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
|
-
//
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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) {
|