claude-session-viewer 0.3.5 → 0.3.7

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.
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Claude Session Viewer</title>
8
- <script type="module" crossorigin src="/assets/index-BREABlIc.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-nqJgfpl7.css">
8
+ <script type="module" crossorigin src="/assets/index-Bo1qXVSw.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BKZOwcdy.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -30,7 +30,7 @@ export function getProjectPath(projectsDir, projectName) {
30
30
  * Remove user's home directory prefix from project directory name
31
31
  * e.g., "-Users-hanyeol-Projects-foo" → "Projects-foo"
32
32
  */
33
- export function getProjectDisplayName(projectDirName) {
33
+ export function getProjectName(projectDirName) {
34
34
  const userHomePath = homedir().split('/').filter(Boolean).join('-');
35
35
  const prefix = `-${userHomePath}-`;
36
36
  if (projectDirName.startsWith(prefix)) {
@@ -1,6 +1,6 @@
1
1
  import { stat } from 'fs/promises';
2
2
  import { join } from 'path';
3
- import { listProjects, getProjectDisplayName } from './repository.js';
3
+ import { listProjects, getProjectName } from './repository.js';
4
4
  import { getProjectSessions, sortSessionsByTimestamp } from '../sessions/service.js';
5
5
  /**
6
6
  * Project service
@@ -25,10 +25,10 @@ export async function getAllProjectsWithSessions(projectsDir) {
25
25
  const sessions = await getProjectSessions(projectPath);
26
26
  if (sessions.length > 0) {
27
27
  sortSessionsByTimestamp(sessions);
28
- const displayName = getProjectDisplayName(project);
28
+ const name = getProjectName(project);
29
29
  projectGroups.push({
30
- name: project,
31
- displayName,
30
+ id: project,
31
+ name,
32
32
  sessionCount: sessions.length,
33
33
  lastActivity: sessions[0].timestamp,
34
34
  sessions
@@ -3,7 +3,7 @@ import { listSessionFiles, readSessionFile } from './repository.js';
3
3
  import { shouldSkipSession, isAgentSession, isEmptyFile } from './filters.js';
4
4
  import { extractSessionTitle } from './title.js';
5
5
  import { collectAgentDescriptions, attachAgentSessionsToParent } from './agents.js';
6
- import { getProjectDisplayName } from '../projects/repository.js';
6
+ import { getProjectName } from '../projects/repository.js';
7
7
  /**
8
8
  * Session service
9
9
  * Orchestrates session loading, filtering, and organization
@@ -21,7 +21,8 @@ export async function getProjectSessions(projectPath) {
21
21
  const sessionFiles = await listSessionFiles(projectPath);
22
22
  const allSessions = [];
23
23
  const agentSessionsMap = new Map();
24
- const projectName = getProjectDisplayName(projectPath.split('/').pop() || 'unknown');
24
+ const projectId = projectPath.split('/').pop() || 'unknown';
25
+ const projectName = getProjectName(projectId);
25
26
  // First pass: load all sessions
26
27
  for (const { filename, size, path } of sessionFiles) {
27
28
  if (isEmptyFile(size))
@@ -33,7 +34,8 @@ export async function getProjectSessions(projectPath) {
33
34
  continue;
34
35
  const session = {
35
36
  id: sessionId,
36
- project: projectName,
37
+ projectId,
38
+ projectName,
37
39
  timestamp,
38
40
  messages,
39
41
  messageCount: messages.length,
@@ -61,15 +63,17 @@ export async function getProjectSessions(projectPath) {
61
63
  /**
62
64
  * Load agent sessions from files
63
65
  */
64
- export async function loadAgentSessionsFromFiles(projectPath, projectName, agentDescriptions) {
66
+ export async function loadAgentSessionsFromFiles(projectPath, projectId, agentDescriptions) {
65
67
  const agentSessions = [];
68
+ const projectName = getProjectName(projectId);
66
69
  for (const [agentSessionId, description] of agentDescriptions) {
67
70
  const agentFile = join(projectPath, `${agentSessionId}.jsonl`);
68
71
  try {
69
72
  const { messages, timestamp } = await readSessionFile(agentFile);
70
73
  agentSessions.push({
71
74
  id: agentSessionId,
72
- project: projectName,
75
+ projectId,
76
+ projectName,
73
77
  timestamp,
74
78
  messages,
75
79
  messageCount: messages.length,
@@ -0,0 +1,127 @@
1
+ import { readdir, stat } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { CLAUDE_DIR } from '../constants.js';
4
+ import { parseJsonl } from '../utils/jsonl.js';
5
+ import { isAgentSession, isEmptyFile, shouldSkipSession } from '../claude/sessions/filters.js';
6
+ import { calculateSessionWindows, getActiveWindow, loadPlanConfig } from '../utils/sessionWindows.js';
7
+ import { PLAN_LIMITS } from '../claude/config.js';
8
+ /**
9
+ * Session window routes
10
+ */
11
+ export async function registerSessionWindowRoutes(server) {
12
+ /**
13
+ * GET /api/session-windows
14
+ * Returns all 5-hour session windows with token usage
15
+ */
16
+ server.get('/api/session-windows', async (request, reply) => {
17
+ try {
18
+ const projectsDir = join(CLAUDE_DIR, 'projects');
19
+ const projects = await readdir(projectsDir);
20
+ // Parse days parameter
21
+ const daysParam = request.query.days || '8'; // Default 8 days for P90 calculation
22
+ const cutoffDate = new Date();
23
+ if (daysParam !== 'all') {
24
+ const days = parseInt(daysParam, 10);
25
+ cutoffDate.setDate(cutoffDate.getDate() - days);
26
+ cutoffDate.setHours(0, 0, 0, 0);
27
+ }
28
+ else {
29
+ cutoffDate.setFullYear(2000, 0, 1);
30
+ }
31
+ // Collect all session data
32
+ const sessionsData = [];
33
+ for (const project of projects) {
34
+ const projectPath = join(projectsDir, project);
35
+ const projectStat = await stat(projectPath);
36
+ if (!projectStat.isDirectory())
37
+ continue;
38
+ const files = await readdir(projectPath);
39
+ for (const file of files) {
40
+ if (!file.endsWith('.jsonl'))
41
+ continue;
42
+ const filePath = join(projectPath, file);
43
+ const fileStat = await stat(filePath);
44
+ if (isEmptyFile(fileStat.size))
45
+ continue;
46
+ const sessionId = file.replace('.jsonl', '');
47
+ if (isAgentSession(sessionId))
48
+ continue;
49
+ try {
50
+ const messages = await parseJsonl(filePath);
51
+ if (shouldSkipSession(messages))
52
+ continue;
53
+ // Filter messages by date
54
+ const filteredMessages = messages.filter((msg) => {
55
+ if (!msg.timestamp)
56
+ return false;
57
+ const msgDate = new Date(msg.timestamp);
58
+ return msgDate >= cutoffDate;
59
+ });
60
+ if (filteredMessages.length > 0) {
61
+ sessionsData.push({
62
+ sessionId,
63
+ messages: filteredMessages
64
+ });
65
+ }
66
+ }
67
+ catch (error) {
68
+ console.error(`Error processing ${file}:`, error);
69
+ }
70
+ }
71
+ }
72
+ // Calculate initial windows with default plan for detection
73
+ const initialWindows = calculateSessionWindows(sessionsData, {
74
+ name: 'Custom',
75
+ limits: PLAN_LIMITS.Custom,
76
+ autoDetected: false
77
+ });
78
+ // Auto-detect plan based on windows
79
+ const planConfig = loadPlanConfig(initialWindows);
80
+ // Recalculate windows with detected plan
81
+ const windows = calculateSessionWindows(sessionsData, planConfig);
82
+ // Get active window
83
+ const activeWindow = getActiveWindow(windows);
84
+ const response = {
85
+ windows,
86
+ activeWindow,
87
+ planConfig
88
+ };
89
+ return response;
90
+ }
91
+ catch (error) {
92
+ console.error('Error calculating session windows:', error);
93
+ return reply.code(500).send({ error: 'Internal server error' });
94
+ }
95
+ });
96
+ /**
97
+ * GET /api/session-windows/active
98
+ * Returns only the currently active session window
99
+ */
100
+ server.get('/api/session-windows/active', async (request, reply) => {
101
+ try {
102
+ // Reuse the main endpoint logic but return only active window
103
+ const fullResponse = await server.inject({
104
+ method: 'GET',
105
+ url: '/api/session-windows?days=1' // Only check last day for performance
106
+ });
107
+ if (fullResponse.statusCode !== 200) {
108
+ return reply.code(fullResponse.statusCode).send(fullResponse.json());
109
+ }
110
+ const data = fullResponse.json();
111
+ if (!data.activeWindow) {
112
+ return {
113
+ activeWindow: null,
114
+ planConfig: data.planConfig
115
+ };
116
+ }
117
+ return {
118
+ activeWindow: data.activeWindow,
119
+ planConfig: data.planConfig
120
+ };
121
+ }
122
+ catch (error) {
123
+ console.error('Error getting active session window:', error);
124
+ return reply.code(500).send({ error: 'Internal server error' });
125
+ }
126
+ });
127
+ }
@@ -3,7 +3,7 @@ import { join } from 'path';
3
3
  import { CLAUDE_DIR } from '../constants.js';
4
4
  import { parseJsonl } from '../utils/jsonl.js';
5
5
  import { getAllProjectsWithSessions } from '../claude/projects/service.js';
6
- import { getProjectDisplayName } from '../claude/projects/repository.js';
6
+ import { getProjectName } from '../claude/projects/repository.js';
7
7
  import { loadAgentSessionsFromFiles } from '../claude/sessions/service.js';
8
8
  import { collectAgentDescriptions, injectAgentIdsIntoMessages, findAgentTitleFromParentMessages } from '../claude/sessions/agents.js';
9
9
  import { isAgentSession } from '../claude/sessions/filters.js';
@@ -37,7 +37,7 @@ export async function registerSessionRoutes(server) {
37
37
  try {
38
38
  const messages = await parseJsonl(sessionFile);
39
39
  const fileStat = await stat(sessionFile);
40
- const projectName = getProjectDisplayName(project);
40
+ const projectName = project;
41
41
  let title = extractSessionTitle(messages);
42
42
  // For agent sessions, find description from parent
43
43
  if (isAgent) {
@@ -64,14 +64,15 @@ export async function registerSessionRoutes(server) {
64
64
  if (!isAgent) {
65
65
  const agentDescriptions = collectAgentDescriptions(messages);
66
66
  if (agentDescriptions.size > 0) {
67
- agentSessions = await loadAgentSessionsFromFiles(projectPath, projectName, agentDescriptions);
67
+ agentSessions = await loadAgentSessionsFromFiles(projectPath, project, agentDescriptions);
68
68
  }
69
69
  }
70
70
  const messagesWithAgentIds = injectAgentIdsIntoMessages(messages);
71
71
  return {
72
72
  session: {
73
73
  id,
74
- project: projectName,
74
+ projectId: projectName,
75
+ projectName: getProjectName(projectName),
75
76
  timestamp: fileStat.mtime.toISOString(),
76
77
  messages: messagesWithAgentIds,
77
78
  messageCount: messages.length,
@@ -1,327 +1,51 @@
1
- import { readdir, stat } from 'fs/promises';
1
+ import { stat } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { CLAUDE_DIR } from '../constants.js';
4
- import { PRICING, DEFAULT_PRICING_MODEL, WEEKDAY_NAMES } from '../claude/config.js';
5
- import { parseJsonl } from '../utils/jsonl.js';
6
- import { extractTokenUsage, aggregateTokenUsage, calculateCost, createEmptyTokenUsage } from '../utils/tokenStats.js';
7
- import { getProjectDisplayName } from '../claude/projects/repository.js';
8
- import { collectAgentDescriptions } from '../claude/sessions/agents.js';
9
- import { shouldSkipSession, isAgentSession, isEmptyFile } from '../claude/sessions/filters.js';
4
+ import { getOverallStatistics, getProjectStatistics } from '../statistics/service.js';
10
5
  /**
11
6
  * Statistics routes
12
7
  */
13
8
  export async function registerStatisticsRoutes(server) {
14
- server.get('/api/statistics/tokens', async (request, reply) => {
9
+ /**
10
+ * GET /api/statistics/overall
11
+ * Get overall usage statistics across all projects
12
+ */
13
+ server.get('/api/statistics/overall', async (request, reply) => {
15
14
  try {
15
+ const days = request.query.days || '7';
16
+ const statistics = await getOverallStatistics(days);
17
+ return statistics;
18
+ }
19
+ catch (error) {
20
+ console.error('Error calculating overall statistics:', error);
21
+ return reply.code(500).send({ error: 'Internal server error' });
22
+ }
23
+ });
24
+ /**
25
+ * GET /api/statistics/projects/:projectId
26
+ * Get usage statistics for a specific project
27
+ */
28
+ server.get('/api/statistics/projects/:projectId', async (request, reply) => {
29
+ try {
30
+ const { projectId } = request.params;
16
31
  const projectsDir = join(CLAUDE_DIR, 'projects');
17
- const projects = await readdir(projectsDir);
18
- const daysParam = request.query.days || '30';
19
- const cutoffDate = new Date();
20
- if (daysParam !== 'all') {
21
- const days = parseInt(daysParam, 10);
22
- cutoffDate.setDate(cutoffDate.getDate() - days);
23
- cutoffDate.setHours(0, 0, 0, 0);
24
- }
25
- else {
26
- cutoffDate.setFullYear(2000, 0, 1);
27
- }
28
- const dailyMap = new Map();
29
- const projectMap = new Map();
30
- const modelMap = new Map();
31
- let totalMessages = 0;
32
- let totalSessions = 0;
33
- let minDate = null;
34
- let maxDate = null;
35
- let totalCacheCreation = 0;
36
- let totalCacheRead = 0;
37
- let ephemeral5mTokens = 0;
38
- let ephemeral1hTokens = 0;
39
- const allUsages = [];
40
- const allCosts = [];
41
- const toolUsageMap = new Map();
42
- let totalAgentSessions = 0;
43
- const hourlyMap = new Map();
44
- const weekdayMap = new Map();
45
- for (let hour = 0; hour < 24; hour++) {
46
- hourlyMap.set(hour, {
47
- usage: createEmptyTokenUsage(),
48
- sessionIds: new Set(),
49
- messageCount: 0
50
- });
51
- }
52
- for (let weekday = 0; weekday < 7; weekday++) {
53
- weekdayMap.set(weekday, {
54
- usage: createEmptyTokenUsage(),
55
- sessionIds: new Set(),
56
- messageCount: 0
57
- });
58
- }
59
- for (const project of projects) {
60
- const projectPath = join(projectsDir, project);
32
+ const projectPath = join(projectsDir, projectId);
33
+ // Check if project exists
34
+ try {
61
35
  const projectStat = await stat(projectPath);
62
- if (!projectStat.isDirectory())
63
- continue;
64
- const files = await readdir(projectPath);
65
- const displayName = getProjectDisplayName(project);
66
- if (!projectMap.has(project)) {
67
- projectMap.set(project, {
68
- usage: createEmptyTokenUsage(),
69
- sessionIds: new Set(),
70
- displayName
71
- });
72
- }
73
- for (const file of files) {
74
- if (!file.endsWith('.jsonl'))
75
- continue;
76
- const filePath = join(projectPath, file);
77
- const fileStat = await stat(filePath);
78
- if (isEmptyFile(fileStat.size))
79
- continue;
80
- const sessionId = file.replace('.jsonl', '');
81
- if (isAgentSession(sessionId))
82
- continue;
83
- try {
84
- const messages = await parseJsonl(filePath);
85
- if (shouldSkipSession(messages))
86
- continue;
87
- totalSessions++;
88
- const projectData = projectMap.get(project);
89
- projectData.sessionIds.add(sessionId);
90
- const agentDescriptions = collectAgentDescriptions(messages);
91
- if (agentDescriptions.size > 0) {
92
- totalAgentSessions++;
93
- }
94
- for (const message of messages) {
95
- const tokenData = extractTokenUsage(message);
96
- if (!tokenData)
97
- continue;
98
- const messageDate = new Date(message.timestamp);
99
- if (messageDate < cutoffDate)
100
- continue;
101
- totalMessages++;
102
- const { usage, model } = tokenData;
103
- const pricing = PRICING[model] || PRICING[DEFAULT_PRICING_MODEL];
104
- allUsages.push(usage);
105
- allCosts.push(calculateCost(usage, pricing));
106
- const dateKey = messageDate.toISOString().split('T')[0];
107
- if (!minDate || messageDate < minDate)
108
- minDate = messageDate;
109
- if (!maxDate || messageDate > maxDate)
110
- maxDate = messageDate;
111
- if (!dailyMap.has(dateKey)) {
112
- dailyMap.set(dateKey, {
113
- usage: createEmptyTokenUsage(),
114
- sessionIds: new Set()
115
- });
116
- }
117
- const dailyData = dailyMap.get(dateKey);
118
- dailyData.usage.inputTokens += usage.inputTokens;
119
- dailyData.usage.cacheCreationTokens += usage.cacheCreationTokens;
120
- dailyData.usage.cacheReadTokens += usage.cacheReadTokens;
121
- dailyData.usage.outputTokens += usage.outputTokens;
122
- dailyData.usage.totalTokens += usage.totalTokens;
123
- dailyData.sessionIds.add(sessionId);
124
- const hour = messageDate.getHours();
125
- const hourData = hourlyMap.get(hour);
126
- hourData.usage.inputTokens += usage.inputTokens;
127
- hourData.usage.cacheCreationTokens += usage.cacheCreationTokens;
128
- hourData.usage.cacheReadTokens += usage.cacheReadTokens;
129
- hourData.usage.outputTokens += usage.outputTokens;
130
- hourData.usage.totalTokens += usage.totalTokens;
131
- hourData.sessionIds.add(sessionId);
132
- hourData.messageCount++;
133
- const weekday = messageDate.getDay();
134
- const weekdayData = weekdayMap.get(weekday);
135
- weekdayData.usage.inputTokens += usage.inputTokens;
136
- weekdayData.usage.cacheCreationTokens += usage.cacheCreationTokens;
137
- weekdayData.usage.cacheReadTokens += usage.cacheReadTokens;
138
- weekdayData.usage.outputTokens += usage.outputTokens;
139
- weekdayData.usage.totalTokens += usage.totalTokens;
140
- weekdayData.sessionIds.add(sessionId);
141
- weekdayData.messageCount++;
142
- projectData.usage.inputTokens += usage.inputTokens;
143
- projectData.usage.cacheCreationTokens += usage.cacheCreationTokens;
144
- projectData.usage.cacheReadTokens += usage.cacheReadTokens;
145
- projectData.usage.outputTokens += usage.outputTokens;
146
- projectData.usage.totalTokens += usage.totalTokens;
147
- if (!modelMap.has(model)) {
148
- modelMap.set(model, {
149
- usage: createEmptyTokenUsage(),
150
- messageCount: 0
151
- });
152
- }
153
- const modelData = modelMap.get(model);
154
- modelData.usage.inputTokens += usage.inputTokens;
155
- modelData.usage.cacheCreationTokens += usage.cacheCreationTokens;
156
- modelData.usage.cacheReadTokens += usage.cacheReadTokens;
157
- modelData.usage.outputTokens += usage.outputTokens;
158
- modelData.usage.totalTokens += usage.totalTokens;
159
- modelData.messageCount++;
160
- totalCacheCreation += usage.cacheCreationTokens;
161
- totalCacheRead += usage.cacheReadTokens;
162
- const cacheCreation = message.message?.usage?.cache_creation;
163
- if (cacheCreation) {
164
- ephemeral5mTokens += cacheCreation.ephemeral_5m_input_tokens || 0;
165
- ephemeral1hTokens += cacheCreation.ephemeral_1h_input_tokens || 0;
166
- }
167
- }
168
- const toolUseIds = new Set();
169
- const successfulToolUseIds = new Set();
170
- for (const message of messages) {
171
- const messageDate = new Date(message.timestamp);
172
- if (messageDate < cutoffDate)
173
- continue;
174
- if (message.type === 'assistant' && message.message?.content && Array.isArray(message.message.content)) {
175
- for (const item of message.message.content) {
176
- if (item.type === 'tool_use' && item.name && item.id) {
177
- const toolName = item.name;
178
- if (!toolUsageMap.has(toolName)) {
179
- toolUsageMap.set(toolName, { total: 0, successful: 0 });
180
- }
181
- if (!toolUseIds.has(item.id)) {
182
- toolUseIds.add(item.id);
183
- toolUsageMap.get(toolName).total++;
184
- }
185
- }
186
- }
187
- }
188
- }
189
- for (const message of messages) {
190
- const messageDate = new Date(message.timestamp);
191
- if (messageDate < cutoffDate)
192
- continue;
193
- if (message.message?.content && Array.isArray(message.message.content)) {
194
- for (const item of message.message.content) {
195
- if (item.type === 'tool_result' && !item.is_error && item.tool_use_id) {
196
- if (!successfulToolUseIds.has(item.tool_use_id)) {
197
- successfulToolUseIds.add(item.tool_use_id);
198
- let found = false;
199
- for (const msg of messages) {
200
- if (found)
201
- break;
202
- if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
203
- for (const toolUseItem of msg.message.content) {
204
- if (toolUseItem.type === 'tool_use' && toolUseItem.id === item.tool_use_id && toolUseItem.name) {
205
- const toolStats = toolUsageMap.get(toolUseItem.name);
206
- if (toolStats) {
207
- toolStats.successful++;
208
- found = true;
209
- break;
210
- }
211
- }
212
- }
213
- }
214
- }
215
- }
216
- }
217
- }
218
- }
219
- }
220
- }
221
- catch (error) {
222
- console.error(`Error processing ${file}:`, error);
223
- }
36
+ if (!projectStat.isDirectory()) {
37
+ return reply.code(404).send({ error: 'Project not found' });
224
38
  }
225
39
  }
226
- const totalUsage = aggregateTokenUsage(allUsages);
227
- const totalCost = allCosts.reduce((acc, cost) => ({
228
- inputCost: acc.inputCost + cost.inputCost,
229
- outputCost: acc.outputCost + cost.outputCost,
230
- cacheCreationCost: acc.cacheCreationCost + cost.cacheCreationCost,
231
- cacheReadCost: acc.cacheReadCost + cost.cacheReadCost,
232
- totalCost: acc.totalCost + cost.totalCost
233
- }), { inputCost: 0, outputCost: 0, cacheCreationCost: 0, cacheReadCost: 0, totalCost: 0 });
234
- const totalPotentialInput = totalUsage.inputTokens + totalCacheCreation + totalCacheRead;
235
- const cacheHitRate = totalPotentialInput > 0 ? (totalCacheRead / totalPotentialInput) * 100 : 0;
236
- const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
237
- const savedCost = (totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
238
- const daily = Array.from(dailyMap.entries())
239
- .map(([date, data]) => ({
240
- date,
241
- usage: data.usage,
242
- sessionCount: data.sessionIds.size
243
- }))
244
- .sort((a, b) => a.date.localeCompare(b.date));
245
- const byProject = Array.from(projectMap.entries())
246
- .map(([project, data]) => ({
247
- project,
248
- displayName: data.displayName,
249
- usage: data.usage,
250
- sessionCount: data.sessionIds.size
251
- }))
252
- .sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
253
- const byModel = Array.from(modelMap.entries())
254
- .map(([model, data]) => ({
255
- model,
256
- usage: data.usage,
257
- messageCount: data.messageCount
258
- }))
259
- .sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
260
- const toolUsage = Array.from(toolUsageMap.entries())
261
- .map(([toolName, stats]) => ({
262
- toolName,
263
- totalUses: stats.total,
264
- successfulUses: stats.successful,
265
- successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
266
- }))
267
- .sort((a, b) => b.totalUses - a.totalUses);
268
- const totalToolCalls = Array.from(toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
269
- const agentUsageRate = totalSessions > 0 ? (totalAgentSessions / totalSessions) * 100 : 0;
270
- const byHour = Array.from(hourlyMap.entries())
271
- .map(([hour, data]) => ({
272
- hour,
273
- sessionCount: data.sessionIds.size,
274
- messageCount: data.messageCount,
275
- usage: data.usage
276
- }))
277
- .sort((a, b) => a.hour - b.hour);
278
- const byWeekday = Array.from(weekdayMap.entries())
279
- .map(([weekday, data]) => ({
280
- weekday,
281
- weekdayName: WEEKDAY_NAMES[weekday],
282
- sessionCount: data.sessionIds.size,
283
- messageCount: data.messageCount,
284
- usage: data.usage
285
- }))
286
- .sort((a, b) => a.weekday - b.weekday);
287
- const statistics = {
288
- overview: {
289
- total: totalUsage,
290
- totalSessions,
291
- totalMessages,
292
- dateRange: {
293
- start: minDate?.toISOString() || new Date().toISOString(),
294
- end: maxDate?.toISOString() || new Date().toISOString()
295
- }
296
- },
297
- daily,
298
- byProject,
299
- byModel,
300
- cache: {
301
- totalCacheCreation,
302
- totalCacheRead,
303
- ephemeral5mTokens,
304
- ephemeral1hTokens,
305
- cacheHitRate,
306
- estimatedSavings: savedCost
307
- },
308
- cost: totalCost,
309
- productivity: {
310
- toolUsage,
311
- totalToolCalls,
312
- agentSessions: totalAgentSessions,
313
- totalSessions,
314
- agentUsageRate
315
- },
316
- trends: {
317
- byHour,
318
- byWeekday
319
- }
320
- };
40
+ catch (error) {
41
+ return reply.code(404).send({ error: 'Project not found' });
42
+ }
43
+ const days = request.query.days || '7';
44
+ const statistics = await getProjectStatistics(projectId, days);
321
45
  return statistics;
322
46
  }
323
47
  catch (error) {
324
- console.error('Error calculating token statistics:', error);
48
+ console.error('Error calculating project statistics:', error);
325
49
  return reply.code(500).send({ error: 'Internal server error' });
326
50
  }
327
51
  });