claude-code-templates 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -0
- package/package.json +26 -12
- package/src/analytics/core/ConversationAnalyzer.js +754 -0
- package/src/analytics/core/FileWatcher.js +285 -0
- package/src/analytics/core/ProcessDetector.js +242 -0
- package/src/analytics/core/SessionAnalyzer.js +597 -0
- package/src/analytics/core/StateCalculator.js +190 -0
- package/src/analytics/data/DataCache.js +550 -0
- package/src/analytics/notifications/NotificationManager.js +448 -0
- package/src/analytics/notifications/WebSocketServer.js +526 -0
- package/src/analytics/utils/PerformanceMonitor.js +455 -0
- package/src/analytics-web/assets/js/main.js +312 -0
- package/src/analytics-web/components/Charts.js +114 -0
- package/src/analytics-web/components/ConversationTable.js +437 -0
- package/src/analytics-web/components/Dashboard.js +573 -0
- package/src/analytics-web/components/SessionTimer.js +596 -0
- package/src/analytics-web/index.html +882 -49
- package/src/analytics-web/index.html.original +1939 -0
- package/src/analytics-web/services/DataService.js +357 -0
- package/src/analytics-web/services/StateService.js +276 -0
- package/src/analytics-web/services/WebSocketService.js +523 -0
- package/src/analytics.js +626 -2317
- package/src/analytics.log +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionTimer Component
|
|
3
|
+
* Displays current session information, timing, token usage, and Max plan limits
|
|
4
|
+
*/
|
|
5
|
+
class SessionTimer {
|
|
6
|
+
constructor(container, dataService, stateService) {
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.dataService = dataService;
|
|
9
|
+
this.stateService = stateService;
|
|
10
|
+
this.sessionData = null;
|
|
11
|
+
this.updateInterval = null;
|
|
12
|
+
this.isInitialized = false;
|
|
13
|
+
this.refreshInterval = 1000; // 1 second for real-time updates
|
|
14
|
+
this.SESSION_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
|
|
15
|
+
this.isTooltipVisible = false; // Track tooltip state globally
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the session timer component
|
|
20
|
+
*/
|
|
21
|
+
async initialize() {
|
|
22
|
+
if (this.isInitialized) return;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await this.render();
|
|
26
|
+
await this.loadSessionData();
|
|
27
|
+
this.startAutoUpdate();
|
|
28
|
+
this.isInitialized = true;
|
|
29
|
+
|
|
30
|
+
console.log('📊 SessionTimer component initialized');
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error initializing SessionTimer:', error);
|
|
33
|
+
this.showError('Failed to initialize session timer');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render the session timer UI
|
|
39
|
+
*/
|
|
40
|
+
async render() {
|
|
41
|
+
this.container.innerHTML = `
|
|
42
|
+
<div class="session-timer-accordion">
|
|
43
|
+
<div class="session-timer-header" onclick="window.sessionTimer?.toggleAccordion()">
|
|
44
|
+
<div class="session-timer-title-section">
|
|
45
|
+
<span class="session-timer-chevron">▼</span>
|
|
46
|
+
<h3 class="session-timer-title">Current Session</h3>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="session-timer-status-inline">
|
|
49
|
+
<span class="session-timer-status-dot"></span>
|
|
50
|
+
<span class="session-timer-status-text">Loading...</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="session-timer-content" id="session-timer-content">
|
|
55
|
+
<div class="session-loading-state">
|
|
56
|
+
<div class="session-spinner"></div>
|
|
57
|
+
<span>Loading session data...</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="session-display" style="display: none;">
|
|
61
|
+
<!-- Session timer display will be populated here -->
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="session-warnings">
|
|
65
|
+
<!-- Warnings will be displayed here -->
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
// Make component globally accessible for button clicks
|
|
72
|
+
window.sessionTimer = this;
|
|
73
|
+
this.isExpanded = true; // Start expanded
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Load session data from API
|
|
78
|
+
*/
|
|
79
|
+
async loadSessionData() {
|
|
80
|
+
try {
|
|
81
|
+
this.sessionData = await this.dataService.getSessionData();
|
|
82
|
+
this.updateDisplay();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error loading session data:', error);
|
|
85
|
+
this.showError('Failed to load session data');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Refresh session data manually
|
|
91
|
+
*/
|
|
92
|
+
async refreshSessionData() {
|
|
93
|
+
const refreshBtn = this.container.querySelector('.session-refresh-btn button');
|
|
94
|
+
if (refreshBtn) {
|
|
95
|
+
refreshBtn.disabled = true;
|
|
96
|
+
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await this.loadSessionData();
|
|
101
|
+
} finally {
|
|
102
|
+
if (refreshBtn) {
|
|
103
|
+
refreshBtn.disabled = false;
|
|
104
|
+
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i>';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Update the display with current session data
|
|
111
|
+
*/
|
|
112
|
+
updateDisplay() {
|
|
113
|
+
if (!this.sessionData) return;
|
|
114
|
+
|
|
115
|
+
const loadingState = this.container.querySelector('.session-loading-state');
|
|
116
|
+
const sessionDisplay = this.container.querySelector('.session-display');
|
|
117
|
+
const warningsContainer = this.container.querySelector('.session-warnings');
|
|
118
|
+
|
|
119
|
+
// Update title with plan name
|
|
120
|
+
const titleElement = this.container.querySelector('.session-timer-title');
|
|
121
|
+
if (titleElement && this.sessionData.limits) {
|
|
122
|
+
titleElement.textContent = `Current Session - ${this.sessionData.limits.name}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (loadingState) loadingState.style.display = 'none';
|
|
126
|
+
if (sessionDisplay) sessionDisplay.style.display = 'block';
|
|
127
|
+
|
|
128
|
+
// Update session display
|
|
129
|
+
this.renderSessionInfo(sessionDisplay);
|
|
130
|
+
|
|
131
|
+
// Update warnings
|
|
132
|
+
this.renderWarnings(warningsContainer);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Load Claude session information
|
|
137
|
+
*/
|
|
138
|
+
async loadClaudeSessionInfo() {
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch('/api/claude/session');
|
|
141
|
+
if (!response.ok) throw new Error('Failed to fetch session info');
|
|
142
|
+
return await response.json();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error loading Claude session info:', error);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render session information
|
|
151
|
+
*/
|
|
152
|
+
async renderSessionInfo(container) {
|
|
153
|
+
const { timer, userPlan, monthlyUsage, limits } = this.sessionData;
|
|
154
|
+
|
|
155
|
+
// Load Claude session info
|
|
156
|
+
const claudeSessionInfo = await this.loadClaudeSessionInfo();
|
|
157
|
+
|
|
158
|
+
// Update header status
|
|
159
|
+
this.updateHeaderStatus(timer, claudeSessionInfo);
|
|
160
|
+
|
|
161
|
+
if (!timer.hasActiveSession) {
|
|
162
|
+
container.innerHTML = `
|
|
163
|
+
<div class="session-timer-empty">
|
|
164
|
+
<div class="session-timer-empty-text">No active session</div>
|
|
165
|
+
<div class="session-timer-empty-subtext">Start a conversation to begin tracking</div>
|
|
166
|
+
</div>
|
|
167
|
+
`;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Calculate progress colors based on usage
|
|
172
|
+
const progressPercentage = Math.round(timer.sessionProgress);
|
|
173
|
+
const timeProgressPercentage = Math.round(((this.SESSION_DURATION - timer.timeRemaining) / this.SESSION_DURATION) * 100);
|
|
174
|
+
|
|
175
|
+
const getProgressColor = (percentage) => {
|
|
176
|
+
if (percentage < 50) return '#3fb950';
|
|
177
|
+
if (percentage < 80) return '#f97316';
|
|
178
|
+
return '#f85149';
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const messageProgressColor = getProgressColor(progressPercentage);
|
|
182
|
+
const timeProgressColor = getProgressColor(timeProgressPercentage);
|
|
183
|
+
|
|
184
|
+
// Format time remaining with better UX
|
|
185
|
+
const formatTimeRemaining = (ms) => {
|
|
186
|
+
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
187
|
+
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
|
188
|
+
const seconds = Math.floor((ms % (1000 * 60)) / 1000);
|
|
189
|
+
|
|
190
|
+
if (hours > 0) {
|
|
191
|
+
return `${hours}h ${minutes}m`;
|
|
192
|
+
} else if (minutes > 0) {
|
|
193
|
+
return `${minutes}m ${seconds}s`;
|
|
194
|
+
} else {
|
|
195
|
+
return `${seconds}s`;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
container.innerHTML = `
|
|
200
|
+
<div class="session-timer-compact">
|
|
201
|
+
|
|
202
|
+
<div class="session-timer-row">
|
|
203
|
+
<div class="session-timer-time-compact">
|
|
204
|
+
<div class="session-timer-time-value">${claudeSessionInfo && claudeSessionInfo.hasSession ?
|
|
205
|
+
(claudeSessionInfo.estimatedTimeRemaining.isExpired ? 'Expired' : claudeSessionInfo.estimatedTimeRemaining.formatted) :
|
|
206
|
+
formatTimeRemaining(timer.timeRemaining)
|
|
207
|
+
}</div>
|
|
208
|
+
<div class="session-timer-time-label">remaining</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="session-timer-progress-compact">
|
|
212
|
+
<div class="session-timer-progress-item">
|
|
213
|
+
<div class="session-timer-progress-header">
|
|
214
|
+
<span class="session-timer-progress-label">Messages</span>
|
|
215
|
+
<span class="session-timer-progress-value">
|
|
216
|
+
${timer.messagesUsed}/${timer.messagesLimit}
|
|
217
|
+
<span class="session-timer-info-icon" data-tooltip="message-info" title="Message calculation info">
|
|
218
|
+
ℹ️
|
|
219
|
+
</span>
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="session-timer-progress-bar">
|
|
223
|
+
<div class="session-timer-progress-fill"
|
|
224
|
+
style="width: ${progressPercentage}%; background-color: ${messageProgressColor};"></div>
|
|
225
|
+
</div>
|
|
226
|
+
${timer.usageDetails && timer.usageDetails.shortMessages > 0 ? `
|
|
227
|
+
<div class="session-timer-usage-details">
|
|
228
|
+
<small>Short: ${timer.usageDetails.shortMessages}, Long: ${timer.usageDetails.longMessages}</small>
|
|
229
|
+
</div>
|
|
230
|
+
` : ''}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="session-timer-progress-item">
|
|
234
|
+
<div class="session-timer-progress-header">
|
|
235
|
+
<span class="session-timer-progress-label">Session Time</span>
|
|
236
|
+
<span class="session-timer-progress-value">${claudeSessionInfo && claudeSessionInfo.hasSession ?
|
|
237
|
+
`${claudeSessionInfo.sessionDuration.formatted}/${claudeSessionInfo.sessionLimit.formatted}` :
|
|
238
|
+
`${formatTimeRemaining(this.SESSION_DURATION - timer.timeRemaining)}/5h`
|
|
239
|
+
}</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="session-timer-progress-bar">
|
|
242
|
+
<div class="session-timer-progress-fill"
|
|
243
|
+
style="width: ${claudeSessionInfo && claudeSessionInfo.hasSession ?
|
|
244
|
+
Math.min(100, (claudeSessionInfo.sessionDuration.ms / claudeSessionInfo.sessionLimit.ms) * 100) :
|
|
245
|
+
timeProgressPercentage
|
|
246
|
+
}%; background-color: ${claudeSessionInfo && claudeSessionInfo.hasSession ?
|
|
247
|
+
(claudeSessionInfo.estimatedTimeRemaining.isExpired ? '#f85149' :
|
|
248
|
+
claudeSessionInfo.estimatedTimeRemaining.ms < 600000 ? '#f97316' : '#3fb950') :
|
|
249
|
+
timeProgressColor
|
|
250
|
+
};"></div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
</div>
|
|
257
|
+
`;
|
|
258
|
+
|
|
259
|
+
// Add popover to the container
|
|
260
|
+
this.addPopover(container);
|
|
261
|
+
|
|
262
|
+
// Add popover event listeners
|
|
263
|
+
this.setupPopoverEvents(container);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Add popover to the container
|
|
268
|
+
*/
|
|
269
|
+
addPopover(container) {
|
|
270
|
+
// Check if popover already exists
|
|
271
|
+
const existingPopover = document.getElementById('message-info-tooltip');
|
|
272
|
+
if (existingPopover) {
|
|
273
|
+
// Don't recreate if it already exists, just return
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create popover HTML
|
|
278
|
+
const popoverHTML = `
|
|
279
|
+
<div class="session-timer-tooltip" id="message-info-tooltip" style="display: ${this.isTooltipVisible ? 'block' : 'none'};">
|
|
280
|
+
<div class="session-timer-tooltip-content">
|
|
281
|
+
<h4>Message Count Calculation</h4>
|
|
282
|
+
<p>This count includes only user messages (your prompts) within the current 5-hour session window. Assistant responses are not counted toward usage limits.</p>
|
|
283
|
+
<p>The actual limit varies based on message length, conversation context, and current system capacity. The displayed limit is an estimate for typical usage.</p>
|
|
284
|
+
<div class="session-timer-tooltip-link">
|
|
285
|
+
<a href="https://support.anthropic.com/en/articles/9797557-usage-limit-best-practices" target="_blank" rel="noopener noreferrer">
|
|
286
|
+
<i class="fas fa-external-link-alt"></i> Usage Limit Best Practices
|
|
287
|
+
</a>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
// Add popover to document body for better positioning
|
|
294
|
+
document.body.insertAdjacentHTML('beforeend', popoverHTML);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Setup popover event listeners
|
|
299
|
+
*/
|
|
300
|
+
setupPopoverEvents(container) {
|
|
301
|
+
const infoIcon = container.querySelector('.session-timer-info-icon');
|
|
302
|
+
const tooltip = document.getElementById('message-info-tooltip');
|
|
303
|
+
|
|
304
|
+
if (infoIcon && tooltip) {
|
|
305
|
+
// Remove existing listeners to prevent duplicates
|
|
306
|
+
const existingClickHandler = infoIcon.clickHandler;
|
|
307
|
+
if (existingClickHandler) {
|
|
308
|
+
infoIcon.removeEventListener('click', existingClickHandler);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Create new click handler
|
|
312
|
+
const clickHandler = (e) => {
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
if (this.isTooltipVisible) {
|
|
315
|
+
this.hideTooltip(tooltip);
|
|
316
|
+
this.isTooltipVisible = false;
|
|
317
|
+
} else {
|
|
318
|
+
this.showTooltip(tooltip, infoIcon);
|
|
319
|
+
this.isTooltipVisible = true;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Store handler reference for cleanup
|
|
324
|
+
infoIcon.clickHandler = clickHandler;
|
|
325
|
+
|
|
326
|
+
// Add click listener
|
|
327
|
+
infoIcon.addEventListener('click', clickHandler);
|
|
328
|
+
|
|
329
|
+
// Setup document click listener only once
|
|
330
|
+
if (!this.documentClickSetup) {
|
|
331
|
+
document.addEventListener('click', (e) => {
|
|
332
|
+
if (this.isTooltipVisible && !tooltip.contains(e.target) && !infoIcon.contains(e.target)) {
|
|
333
|
+
this.hideTooltip(tooltip);
|
|
334
|
+
this.isTooltipVisible = false;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
this.documentClickSetup = true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Prevent tooltip from closing when clicking inside it
|
|
341
|
+
tooltip.addEventListener('click', (e) => {
|
|
342
|
+
e.stopPropagation();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Show tooltip with positioning
|
|
349
|
+
*/
|
|
350
|
+
showTooltip(tooltip, trigger) {
|
|
351
|
+
const rect = trigger.getBoundingClientRect();
|
|
352
|
+
tooltip.style.display = 'block';
|
|
353
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
354
|
+
|
|
355
|
+
// Position tooltip below the icon
|
|
356
|
+
tooltip.style.left = `${rect.left - tooltipRect.width / 2 + rect.width / 2}px`;
|
|
357
|
+
tooltip.style.top = `${rect.bottom + 10}px`;
|
|
358
|
+
|
|
359
|
+
// Adjust position if tooltip goes off-screen horizontally
|
|
360
|
+
const viewportWidth = window.innerWidth;
|
|
361
|
+
const tooltipLeft = parseInt(tooltip.style.left);
|
|
362
|
+
|
|
363
|
+
if (tooltipLeft < 10) {
|
|
364
|
+
tooltip.style.left = '10px';
|
|
365
|
+
} else if (tooltipLeft + tooltipRect.width > viewportWidth - 10) {
|
|
366
|
+
tooltip.style.left = `${viewportWidth - tooltipRect.width - 10}px`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Adjust position if tooltip goes off-screen vertically
|
|
370
|
+
const viewportHeight = window.innerHeight;
|
|
371
|
+
const tooltipTop = parseInt(tooltip.style.top);
|
|
372
|
+
|
|
373
|
+
if (tooltipTop + tooltipRect.height > viewportHeight - 10) {
|
|
374
|
+
// If it goes off-screen below, position it above the trigger
|
|
375
|
+
tooltip.style.top = `${rect.top - tooltipRect.height - 10}px`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.isTooltipVisible = true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Hide tooltip
|
|
383
|
+
*/
|
|
384
|
+
hideTooltip(tooltip) {
|
|
385
|
+
tooltip.style.display = 'none';
|
|
386
|
+
this.isTooltipVisible = false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Render warnings if any
|
|
391
|
+
*/
|
|
392
|
+
renderWarnings(container) {
|
|
393
|
+
if (!this.sessionData || !this.sessionData.warnings || this.sessionData.warnings.length === 0) {
|
|
394
|
+
container.innerHTML = '';
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const warnings = this.sessionData.warnings
|
|
399
|
+
.filter(warning => warning.type.includes('session') || warning.type.includes('monthly'))
|
|
400
|
+
.slice(0, 3); // Show max 3 warnings
|
|
401
|
+
|
|
402
|
+
if (warnings.length === 0) {
|
|
403
|
+
container.innerHTML = '';
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const warningHtml = warnings.map(warning => `
|
|
408
|
+
<div class="session-warning ${warning.level}">
|
|
409
|
+
<i class="fas ${this.getWarningIcon(warning.level)}"></i>
|
|
410
|
+
<span>${warning.message}</span>
|
|
411
|
+
${warning.timeRemaining ? `<small>Time remaining: ${this.formatTimeRemaining(warning.timeRemaining)}</small>` : ''}
|
|
412
|
+
</div>
|
|
413
|
+
`).join('');
|
|
414
|
+
|
|
415
|
+
container.innerHTML = `<div class="warnings-list">${warningHtml}</div>`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get message limit based on current plan
|
|
420
|
+
*/
|
|
421
|
+
getMessageLimit() {
|
|
422
|
+
if (!this.sessionData || !this.sessionData.limits) return 225;
|
|
423
|
+
return this.sessionData.limits.messagesPerSession;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get plan badge CSS class
|
|
428
|
+
*/
|
|
429
|
+
getPlanBadgeClass(planType) {
|
|
430
|
+
const classes = {
|
|
431
|
+
'premium': 'plan-premium',
|
|
432
|
+
'standard': 'plan-standard',
|
|
433
|
+
'pro': 'plan-pro'
|
|
434
|
+
};
|
|
435
|
+
return classes[planType] || 'plan-standard';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get warning icon based on level
|
|
440
|
+
*/
|
|
441
|
+
getWarningIcon(level) {
|
|
442
|
+
const icons = {
|
|
443
|
+
'error': 'fa-exclamation-triangle',
|
|
444
|
+
'warning': 'fa-exclamation-circle',
|
|
445
|
+
'info': 'fa-info-circle'
|
|
446
|
+
};
|
|
447
|
+
return icons[level] || 'fa-info-circle';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Format time remaining for display
|
|
452
|
+
*/
|
|
453
|
+
formatTimeRemaining(milliseconds) {
|
|
454
|
+
if (milliseconds <= 0) return '0m';
|
|
455
|
+
|
|
456
|
+
const hours = Math.floor(milliseconds / (60 * 60 * 1000));
|
|
457
|
+
const minutes = Math.floor((milliseconds % (60 * 60 * 1000)) / (60 * 1000));
|
|
458
|
+
|
|
459
|
+
if (hours > 0) {
|
|
460
|
+
return `${hours}h ${minutes}m`;
|
|
461
|
+
}
|
|
462
|
+
return `${minutes}m`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Format numbers with appropriate units
|
|
467
|
+
*/
|
|
468
|
+
formatNumber(num) {
|
|
469
|
+
if (num >= 1000000) {
|
|
470
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
471
|
+
}
|
|
472
|
+
if (num >= 1000) {
|
|
473
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
474
|
+
}
|
|
475
|
+
return num.toString();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Show error message
|
|
480
|
+
*/
|
|
481
|
+
showError(message) {
|
|
482
|
+
const loadingState = this.container.querySelector('.session-loading-state');
|
|
483
|
+
const sessionDisplay = this.container.querySelector('.session-display');
|
|
484
|
+
|
|
485
|
+
if (loadingState) loadingState.style.display = 'none';
|
|
486
|
+
if (sessionDisplay) {
|
|
487
|
+
sessionDisplay.style.display = 'block';
|
|
488
|
+
sessionDisplay.innerHTML = `
|
|
489
|
+
<div class="session-timer-error">
|
|
490
|
+
<div class="session-timer-error-text">${message}</div>
|
|
491
|
+
<button class="session-timer-retry-btn" onclick="window.sessionTimer?.refreshSessionData()">Retry</button>
|
|
492
|
+
</div>
|
|
493
|
+
`;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Start automatic updates
|
|
499
|
+
*/
|
|
500
|
+
startAutoUpdate() {
|
|
501
|
+
// Update every 1 second for real-time display
|
|
502
|
+
this.updateInterval = setInterval(() => {
|
|
503
|
+
this.loadSessionData();
|
|
504
|
+
}, this.refreshInterval);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Stop automatic updates
|
|
509
|
+
*/
|
|
510
|
+
stopAutoUpdate() {
|
|
511
|
+
if (this.updateInterval) {
|
|
512
|
+
clearInterval(this.updateInterval);
|
|
513
|
+
this.updateInterval = null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Handle real-time updates
|
|
519
|
+
*/
|
|
520
|
+
handleRealtimeUpdate(data) {
|
|
521
|
+
if (data.type === 'session_update' && data.sessionData) {
|
|
522
|
+
this.sessionData = data.sessionData;
|
|
523
|
+
this.updateDisplay();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Toggle accordion open/closed
|
|
529
|
+
*/
|
|
530
|
+
toggleAccordion() {
|
|
531
|
+
const content = this.container.querySelector('#session-timer-content');
|
|
532
|
+
const chevron = this.container.querySelector('.session-timer-chevron');
|
|
533
|
+
|
|
534
|
+
if (this.isExpanded) {
|
|
535
|
+
content.style.display = 'none';
|
|
536
|
+
chevron.textContent = '▶';
|
|
537
|
+
this.isExpanded = false;
|
|
538
|
+
} else {
|
|
539
|
+
content.style.display = 'block';
|
|
540
|
+
chevron.textContent = '▼';
|
|
541
|
+
this.isExpanded = true;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Update header status display
|
|
547
|
+
*/
|
|
548
|
+
updateHeaderStatus(timer, claudeSessionInfo) {
|
|
549
|
+
const statusDot = this.container.querySelector('.session-timer-status-dot');
|
|
550
|
+
const statusText = this.container.querySelector('.session-timer-status-text');
|
|
551
|
+
|
|
552
|
+
// If we have Claude session info, prioritize that
|
|
553
|
+
if (claudeSessionInfo && claudeSessionInfo.hasSession) {
|
|
554
|
+
if (claudeSessionInfo.estimatedTimeRemaining.isExpired) {
|
|
555
|
+
statusDot.className = 'session-timer-status-dot expired';
|
|
556
|
+
statusText.textContent = 'Session Expired';
|
|
557
|
+
} else if (claudeSessionInfo.estimatedTimeRemaining.ms < 600000) { // < 10 minutes
|
|
558
|
+
statusDot.className = 'session-timer-status-dot warning';
|
|
559
|
+
statusText.textContent = 'Ending Soon';
|
|
560
|
+
} else {
|
|
561
|
+
statusDot.className = 'session-timer-status-dot active';
|
|
562
|
+
statusText.textContent = 'Active';
|
|
563
|
+
}
|
|
564
|
+
} else if (!timer.hasActiveSession) {
|
|
565
|
+
statusDot.className = 'session-timer-status-dot inactive';
|
|
566
|
+
statusText.textContent = 'Inactive';
|
|
567
|
+
} else if (timer.timeRemaining < 600000) {
|
|
568
|
+
statusDot.className = 'session-timer-status-dot warning';
|
|
569
|
+
statusText.textContent = 'Ending Soon';
|
|
570
|
+
} else {
|
|
571
|
+
statusDot.className = 'session-timer-status-dot active';
|
|
572
|
+
statusText.textContent = 'Active';
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Cleanup component
|
|
578
|
+
*/
|
|
579
|
+
destroy() {
|
|
580
|
+
this.stopAutoUpdate();
|
|
581
|
+
if (window.sessionTimer === this) {
|
|
582
|
+
delete window.sessionTimer;
|
|
583
|
+
}
|
|
584
|
+
this.isInitialized = false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Export for module systems
|
|
589
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
590
|
+
module.exports = SessionTimer;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Global registration for browser
|
|
594
|
+
if (typeof window !== 'undefined') {
|
|
595
|
+
window.SessionTimer = SessionTimer;
|
|
596
|
+
}
|