agent-tank 0.9.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/LICENSE +21 -0
- package/README.md +675 -0
- package/bin/agent-tank.js +301 -0
- package/media/www-preview.png +0 -0
- package/package.json +65 -0
- package/src/activity-monitor.js +312 -0
- package/src/agents/api-response-parser.js +255 -0
- package/src/agents/base.js +396 -0
- package/src/agents/claude.js +388 -0
- package/src/agents/codex-pty-helpers.js +226 -0
- package/src/agents/codex-rpc-helpers.js +167 -0
- package/src/agents/codex.js +383 -0
- package/src/agents/gemini.js +302 -0
- package/src/agents/keepalive-helper.js +115 -0
- package/src/agents/oauth-helper.js +210 -0
- package/src/agents/pty-output-parser.js +301 -0
- package/src/auto-refresh-manager.js +246 -0
- package/src/client-auto-refresh-helpers.js +143 -0
- package/src/client-auto-refresh.js +321 -0
- package/src/client-favicon.js +198 -0
- package/src/client-monitor.js +242 -0
- package/src/client-script.js +147 -0
- package/src/discovery.js +128 -0
- package/src/history-store.js +273 -0
- package/src/http-handler.js +182 -0
- package/src/icons.js +67 -0
- package/src/index.js +376 -0
- package/src/json-rpc-client.js +285 -0
- package/src/keepalive-manager.js +163 -0
- package/src/logger.js +213 -0
- package/src/pace-attachment.js +104 -0
- package/src/pace-evaluator.js +269 -0
- package/src/public-status.js +276 -0
- package/src/server.js +144 -0
- package/src/snapshot-metrics.js +79 -0
- package/src/status-page.css +1149 -0
- package/src/status-page.js +128 -0
- package/src/usage-formatters.js +259 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
4
|
+
|
|
5
|
+
const { agentIcons, refreshIcon, tankIcon, syncIcon, copyIcon } = require('./icons');
|
|
6
|
+
const { formatUsage } = require('./usage-formatters');
|
|
7
|
+
const { clientScript } = require('./client-script');
|
|
8
|
+
const { getStatusBadgeClass, getStatusText } = require('./public-status');
|
|
9
|
+
|
|
10
|
+
const styles = fs.readFileSync(path.join(__dirname, 'status-page.css'), 'utf8');
|
|
11
|
+
|
|
12
|
+
function statusPage(status) {
|
|
13
|
+
const agents = Object.entries(status);
|
|
14
|
+
|
|
15
|
+
// Calculate the maximum lastUpdated timestamp across all agents for global display
|
|
16
|
+
const maxLastUpdated = agents.reduce((max, [, data]) => {
|
|
17
|
+
if (data.lastUpdated) {
|
|
18
|
+
const ts = new Date(data.lastUpdated).getTime();
|
|
19
|
+
return ts > max ? ts : max;
|
|
20
|
+
}
|
|
21
|
+
return max;
|
|
22
|
+
}, 0);
|
|
23
|
+
|
|
24
|
+
const globalLastChecked = maxLastUpdated > 0
|
|
25
|
+
? new Date(maxLastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
26
|
+
: null;
|
|
27
|
+
|
|
28
|
+
const agentCards = agents.map(([name, data]) => {
|
|
29
|
+
const usageHtml = formatUsage(name, data.usage);
|
|
30
|
+
const statusClass = data.error ? 'error' : data.isRefreshing ? 'refreshing' : 'ok';
|
|
31
|
+
|
|
32
|
+
const icon = agentIcons[name] || '';
|
|
33
|
+
const displayName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
34
|
+
|
|
35
|
+
// Public API status badge
|
|
36
|
+
const publicStatus = data.publicStatus;
|
|
37
|
+
const badgeClass = publicStatus ? getStatusBadgeClass(publicStatus.status) : 'status-badge-grey';
|
|
38
|
+
const badgeText = publicStatus ? getStatusText(publicStatus.status) : 'Unknown';
|
|
39
|
+
const badgeTitle = publicStatus?.description || 'Unable to fetch status';
|
|
40
|
+
const statusBadgeHtml = `<span class="status-badge ${badgeClass}" title="${badgeTitle}">${badgeText}</span>`;
|
|
41
|
+
|
|
42
|
+
// Version update notice - check usage.version (Codex) or metadata.updateAvailable (Gemini)
|
|
43
|
+
const version = data.usage?.version || data.metadata?.updateAvailable;
|
|
44
|
+
const updateHtml = (version && version.current && version.latest && version.current !== version.latest)
|
|
45
|
+
? `<div class="version-update-notice">Update available: v${version.current} → v${version.latest}</div>`
|
|
46
|
+
: '';
|
|
47
|
+
|
|
48
|
+
// Only show card-footer if there's an error or update notice
|
|
49
|
+
const hasFooterContent = data.error || updateHtml;
|
|
50
|
+
const footerHtml = hasFooterContent
|
|
51
|
+
? `<div class="card-footer">${data.error ? `<span class="error-msg">${data.error}</span>` : ''}${updateHtml}</div>`
|
|
52
|
+
: '';
|
|
53
|
+
|
|
54
|
+
return `
|
|
55
|
+
<div class="agent-card ${statusClass} agent-${name}">
|
|
56
|
+
<h2 class="agent-heading">
|
|
57
|
+
${icon}
|
|
58
|
+
<span>${displayName}</span>
|
|
59
|
+
${statusBadgeHtml}
|
|
60
|
+
<button class="refresh-icon-btn" onclick="refresh('${name}', event)" ${data.isRefreshing ? 'disabled' : ''} title="Refresh ${displayName}">
|
|
61
|
+
${refreshIcon}
|
|
62
|
+
</button>
|
|
63
|
+
</h2>
|
|
64
|
+
<div class="usage">${usageHtml}</div>
|
|
65
|
+
${footerHtml}
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
}).join('');
|
|
69
|
+
|
|
70
|
+
return `<!DOCTYPE html>
|
|
71
|
+
<html>
|
|
72
|
+
<head>
|
|
73
|
+
<meta charset="utf-8">
|
|
74
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
75
|
+
<title>Agent Tank</title>
|
|
76
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
77
|
+
<style>
|
|
78
|
+
${styles}
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<nav class="top-nav">
|
|
83
|
+
<div class="top-nav-left">
|
|
84
|
+
<a href="https://agenttank.io" target="_blank" rel="noopener noreferrer" class="logo-link">
|
|
85
|
+
${tankIcon}
|
|
86
|
+
<h1 class="top-nav-title">AGENT <span class="brand-emphasis">TANK</span></h1>
|
|
87
|
+
</a>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="top-nav-right">
|
|
90
|
+
${globalLastChecked ? `<span class="last-checked">Last checked: ${globalLastChecked}</span>` : ''}
|
|
91
|
+
<button class="refresh-all-btn" onclick="refreshAll(event)">
|
|
92
|
+
${syncIcon}
|
|
93
|
+
<span class="btn-text">Refresh All</span>
|
|
94
|
+
</button>
|
|
95
|
+
<button class="theme-toggle-btn" onclick="toggleTheme()" id="theme-toggle-btn">
|
|
96
|
+
<span id="theme-icon">☀️</span>
|
|
97
|
+
<span id="theme-text">Light</span>
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</nav>
|
|
101
|
+
<div class="container">
|
|
102
|
+
<div class="agents">
|
|
103
|
+
${agentCards || '<p>No agents configured</p>'}
|
|
104
|
+
</div>
|
|
105
|
+
<footer class="footer">
|
|
106
|
+
<div class="footer-left">
|
|
107
|
+
<span>© ${new Date().getFullYear()} <a href="https://propr.dev">Rinalds Uzkalns</a></span>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="footer-right">
|
|
110
|
+
<span class="footer-version"><a href="https://github.com/integry/agent-tank/releases">v${pkg.version}</a></span>
|
|
111
|
+
<span class="footer-separator">•</span>
|
|
112
|
+
<a href="https://github.com/integry/agent-tank/blob/main/CHANGELOG.md">Changelog</a>
|
|
113
|
+
<span class="footer-separator">•</span>
|
|
114
|
+
<a href="https://github.com/integry/agent-tank">GitHub</a>
|
|
115
|
+
<span class="footer-separator">•</span>
|
|
116
|
+
<a href="https://www.reddit.com/r/agenttank">Reddit</a>
|
|
117
|
+
<span class="footer-separator">•</span>
|
|
118
|
+
<a href="/status" target="_blank" class="api-link" id="api-link" title="Open API endpoint">[ API Endpoint ]</a>
|
|
119
|
+
<button class="copy-api-btn" onclick="copyApiEndpoint(event)" title="Copy API URL">${copyIcon}</button>
|
|
120
|
+
</div>
|
|
121
|
+
</footer>
|
|
122
|
+
</div>
|
|
123
|
+
<script>${clientScript}</script>
|
|
124
|
+
</body>
|
|
125
|
+
</html>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { statusPage };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const { trackIcon } = require('./icons');
|
|
2
|
+
const { formatPaceWarning } = require('./pace-evaluator');
|
|
3
|
+
|
|
4
|
+
// Cycle duration constants in seconds
|
|
5
|
+
const CYCLE_DURATIONS = {
|
|
6
|
+
session: 5 * 60 * 60, // 5 hours = 18000 seconds
|
|
7
|
+
sessionGemini: 24 * 60 * 60, // 24 hours = 86400 seconds (Gemini uses 24h sessions)
|
|
8
|
+
weekly: 7 * 24 * 60 * 60, // 7 days = 604800 seconds
|
|
9
|
+
fiveHour: 5 * 60 * 60 // 5 hours = 18000 seconds
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Parse "Resets in" time string to seconds
|
|
13
|
+
function parseResetsInToSeconds(resetsIn) {
|
|
14
|
+
if (!resetsIn || typeof resetsIn !== 'string') return null;
|
|
15
|
+
|
|
16
|
+
let totalSeconds = 0;
|
|
17
|
+
|
|
18
|
+
// Match patterns like "2h 30m", "5 hours", "3d 12h", "45 minutes", etc.
|
|
19
|
+
const dayMatch = resetsIn.match(/(\d+)\s*d(?:ay)?s?/i);
|
|
20
|
+
const hourMatch = resetsIn.match(/(\d+)\s*h(?:our)?s?/i);
|
|
21
|
+
const minMatch = resetsIn.match(/(\d+)\s*m(?:in(?:ute)?)?s?/i);
|
|
22
|
+
const secMatch = resetsIn.match(/(\d+)\s*s(?:ec(?:ond)?)?s?/i);
|
|
23
|
+
|
|
24
|
+
if (dayMatch) totalSeconds += parseInt(dayMatch[1], 10) * 24 * 60 * 60;
|
|
25
|
+
if (hourMatch) totalSeconds += parseInt(hourMatch[1], 10) * 60 * 60;
|
|
26
|
+
if (minMatch) totalSeconds += parseInt(minMatch[1], 10) * 60;
|
|
27
|
+
if (secMatch) totalSeconds += parseInt(secMatch[1], 10);
|
|
28
|
+
|
|
29
|
+
return totalSeconds > 0 ? totalSeconds : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Helper to determine color class based on usage percentage
|
|
33
|
+
function getColorClass(value, isUsed) {
|
|
34
|
+
if (isUsed) {
|
|
35
|
+
if (value < 50) return 'high';
|
|
36
|
+
if (value < 80) return 'medium';
|
|
37
|
+
return 'low';
|
|
38
|
+
}
|
|
39
|
+
if (value > 50) return 'high';
|
|
40
|
+
if (value > 20) return 'medium';
|
|
41
|
+
return 'low';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Helper to determine status dot class based on value
|
|
45
|
+
function getStatusDotClass(value) {
|
|
46
|
+
if (value <= 50) return 'status-green';
|
|
47
|
+
if (value <= 80) return 'status-yellow';
|
|
48
|
+
return 'status-red';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resetInfoItem(resetsIn, originalValue, cycleType, options = {}) {
|
|
52
|
+
const { isZero = false, paceData = null } = options;
|
|
53
|
+
// Always render the wrapper so XHR updates can show/hide it dynamically.
|
|
54
|
+
// Hidden when at 0% (no useful info when at full capacity).
|
|
55
|
+
const hidden = isZero ? ' style="display:none"' : '';
|
|
56
|
+
const tooltip = originalValue ? ` title="${originalValue}"` : '';
|
|
57
|
+
|
|
58
|
+
// Calculate elapsed time progress bar
|
|
59
|
+
let timeProgressHtml = '';
|
|
60
|
+
const resetsInSeconds = parseResetsInToSeconds(resetsIn);
|
|
61
|
+
const cycleDuration = cycleType ? CYCLE_DURATIONS[cycleType] : null;
|
|
62
|
+
|
|
63
|
+
if (resetsInSeconds !== null && cycleDuration) {
|
|
64
|
+
const elapsedSeconds = cycleDuration - resetsInSeconds;
|
|
65
|
+
const elapsedPercent = Math.min(100, Math.max(0, (elapsedSeconds / cycleDuration) * 100));
|
|
66
|
+
|
|
67
|
+
timeProgressHtml = `<div class="time-progress-bar" title="Time elapsed in cycle: ${Math.round(elapsedPercent)}%">
|
|
68
|
+
<div class="time-progress-fill" style="width: ${elapsedPercent}%;"></div>
|
|
69
|
+
</div>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add pace warning if applicable
|
|
73
|
+
const paceWarningHtml = formatPaceWarning(paceData);
|
|
74
|
+
|
|
75
|
+
return `<div class="reset-info-wrapper"${hidden}>
|
|
76
|
+
<div class="usage-item reset-info"${tooltip}><span class="usage-label">↳ Resets in</span><span class="usage-value">${resetsIn || ''}${paceWarningHtml}</span></div>
|
|
77
|
+
${timeProgressHtml}
|
|
78
|
+
</div>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function usageItem(label, value, suffix, options = {}) {
|
|
82
|
+
const { isUsed = true, isZero = false, isModelName = false, isNestedMetric = false, agentName = '', resetsIn = '', metricId = '' } = options;
|
|
83
|
+
const colorClass = getColorClass(value, isUsed);
|
|
84
|
+
const progressPercent = isUsed ? value : (100 - value);
|
|
85
|
+
const progressColor = colorClass === 'high' ? '#48bb78' : colorClass === 'medium' ? '#ecc94b' : '#e53e3e';
|
|
86
|
+
const zeroClass = isZero ? ' zero-usage' : '';
|
|
87
|
+
const nestedClass = isNestedMetric ? ' nested-metric' : '';
|
|
88
|
+
const statusDotClass = getStatusDotClass(value);
|
|
89
|
+
// All usage items now have status dots (LEDs) for unified monitoring station look
|
|
90
|
+
const labelHtml = `<span class="usage-label${isModelName ? ' model-name' : ''}"><span class="status-dot ${statusDotClass}"></span>${label}</span>`;
|
|
91
|
+
|
|
92
|
+
// Generate unique metric ID for tracking
|
|
93
|
+
const trackingId = metricId || `${agentName}-${label.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
94
|
+
|
|
95
|
+
// Track button with data attributes for client-side tracking
|
|
96
|
+
const trackButton = `<button class="track-btn"
|
|
97
|
+
data-metric-id="${trackingId}"
|
|
98
|
+
data-agent="${agentName}"
|
|
99
|
+
data-label="${label}"
|
|
100
|
+
data-percent="${value}"
|
|
101
|
+
data-color="${progressColor}"
|
|
102
|
+
data-resets-in="${resetsIn}"
|
|
103
|
+
onclick="event.stopPropagation(); toggleTracking(this)"
|
|
104
|
+
title="Track this metric in tab">${trackIcon}</button>`;
|
|
105
|
+
|
|
106
|
+
return `
|
|
107
|
+
<div class="usage-item${zeroClass}${nestedClass} trackable"
|
|
108
|
+
data-metric-id="${trackingId}"
|
|
109
|
+
data-agent="${agentName}"
|
|
110
|
+
data-label="${label}"
|
|
111
|
+
data-percent="${value}"
|
|
112
|
+
data-color="${progressColor}"
|
|
113
|
+
data-resets-in="${resetsIn}"
|
|
114
|
+
onclick="toggleTrackingFromRow(this)"
|
|
115
|
+
title="Click to track this metric">
|
|
116
|
+
${labelHtml}
|
|
117
|
+
<span class="usage-value ${colorClass}"><span class="usage-percent">${value}</span><span class="usage-suffix">${suffix}</span>${trackButton}</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="progress-bar${zeroClass}${nestedClass}">
|
|
120
|
+
<div class="progress-fill" style="width: ${progressPercent}%; background: ${progressColor};"></div>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatClaudeUsage(usage) {
|
|
126
|
+
let html = '';
|
|
127
|
+
const sections = [
|
|
128
|
+
{ data: usage.session, label: 'Session', cycle: 'session' },
|
|
129
|
+
{ data: usage.weeklyAll, label: 'Weekly (all)', cycle: 'weekly' },
|
|
130
|
+
{ data: usage.weeklySonnet, label: 'Weekly (Sonnet)', cycle: 'weekly' },
|
|
131
|
+
{ data: usage.weekly, label: 'Weekly', cycle: 'weekly' },
|
|
132
|
+
];
|
|
133
|
+
for (const { data, label, cycle } of sections) {
|
|
134
|
+
if (data) {
|
|
135
|
+
const percent = data.percent ?? 0;
|
|
136
|
+
const isZero = percent === 0;
|
|
137
|
+
const resetsIn = data.resetsIn || '';
|
|
138
|
+
|
|
139
|
+
// Use pre-calculated pace data from the usage object
|
|
140
|
+
const paceData = data.pace || null;
|
|
141
|
+
|
|
142
|
+
html += '<div class="model-container">';
|
|
143
|
+
html += usageItem(label, percent, '% used', { isZero, agentName: 'claude', resetsIn });
|
|
144
|
+
html += resetInfoItem(data.resetsIn, data.resetsAt, cycle, { isZero, paceData });
|
|
145
|
+
html += '</div>';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Extra usage (paid overage budget)
|
|
149
|
+
if (usage.extraUsage) {
|
|
150
|
+
const extra = usage.extraUsage;
|
|
151
|
+
const percent = extra.percent ?? 0;
|
|
152
|
+
const isZero = percent === 0;
|
|
153
|
+
const resetsIn = extra.resetsIn || '';
|
|
154
|
+
|
|
155
|
+
// Use pre-calculated pace data from the usage object
|
|
156
|
+
const paceData = extra.pace || null;
|
|
157
|
+
|
|
158
|
+
const spentLabel = extra.spent != null && extra.budget != null
|
|
159
|
+
? `$${extra.spent.toFixed(2)} / $${extra.budget.toFixed(2)}`
|
|
160
|
+
: '';
|
|
161
|
+
html += '<div class="model-container">';
|
|
162
|
+
html += usageItem('Extra', percent, '% used', { isZero, agentName: 'claude', resetsIn });
|
|
163
|
+
if (spentLabel) {
|
|
164
|
+
html += `<div class="usage-item reset-info"><span class="usage-label">↳ Budget</span><span class="usage-value">${spentLabel}</span></div>`;
|
|
165
|
+
}
|
|
166
|
+
html += resetInfoItem(extra.resetsIn, extra.resetsAt, 'weekly', { isZero, paceData });
|
|
167
|
+
html += '</div>';
|
|
168
|
+
}
|
|
169
|
+
return html;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatGeminiUsage(usage) {
|
|
173
|
+
let html = '';
|
|
174
|
+
if (usage.models && usage.models.length > 0) {
|
|
175
|
+
for (const model of usage.models) {
|
|
176
|
+
const percent = model.percentUsed ?? 0;
|
|
177
|
+
const isZero = percent === 0;
|
|
178
|
+
// Use lowercase model name with model-name styling
|
|
179
|
+
const modelName = model.model.toLowerCase();
|
|
180
|
+
const resetsIn = model.resetsIn || '';
|
|
181
|
+
|
|
182
|
+
// Use pre-calculated pace data from the usage object
|
|
183
|
+
const paceData = model.pace || null;
|
|
184
|
+
|
|
185
|
+
html += '<div class="model-container">';
|
|
186
|
+
html += usageItem(modelName, percent, '% used', { isZero, isModelName: true, agentName: 'gemini', resetsIn });
|
|
187
|
+
html += resetInfoItem(model.resetsIn, null, 'sessionGemini', { isZero, paceData });
|
|
188
|
+
html += '</div>';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return html;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatCodexUsage(usage) {
|
|
195
|
+
let html = '';
|
|
196
|
+
const models = [];
|
|
197
|
+
if (usage.fiveHour || usage.weekly) {
|
|
198
|
+
models.push({ name: usage.model || 'Default', fiveHour: usage.fiveHour, weekly: usage.weekly });
|
|
199
|
+
}
|
|
200
|
+
if (usage.modelLimits) {
|
|
201
|
+
models.push(...usage.modelLimits);
|
|
202
|
+
}
|
|
203
|
+
for (const ml of models) {
|
|
204
|
+
// Use model-subheading class for Codex models to create visual grouping (keeps ALL CAPS)
|
|
205
|
+
// CSS :first-child handles the first item styling automatically
|
|
206
|
+
const modelSlug = ml.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
207
|
+
html += `<div class="usage-item model-subheading"><span class="usage-label">${ml.name.toUpperCase()}</span></div>`;
|
|
208
|
+
if (ml.fiveHour) {
|
|
209
|
+
const fiveHourPercent = ml.fiveHour.percentUsed ?? 0;
|
|
210
|
+
const isZero = fiveHourPercent === 0;
|
|
211
|
+
const resetsIn = ml.fiveHour.resetsIn || '';
|
|
212
|
+
|
|
213
|
+
// Use pre-calculated pace data from the usage object
|
|
214
|
+
const paceData = ml.fiveHour.pace || null;
|
|
215
|
+
|
|
216
|
+
html += '<div class="model-container nested-container">';
|
|
217
|
+
html += usageItem('5h limit', fiveHourPercent, '% used', { isZero, isNestedMetric: true, agentName: 'codex', resetsIn, metricId: `codex-${modelSlug}-5h` });
|
|
218
|
+
html += resetInfoItem(ml.fiveHour.resetsIn, ml.fiveHour.resetsAt, 'fiveHour', { isZero, paceData });
|
|
219
|
+
html += '</div>';
|
|
220
|
+
}
|
|
221
|
+
if (ml.weekly) {
|
|
222
|
+
const weeklyPercent = ml.weekly.percentUsed ?? 0;
|
|
223
|
+
const isZero = weeklyPercent === 0;
|
|
224
|
+
const resetsIn = ml.weekly.resetsIn || '';
|
|
225
|
+
|
|
226
|
+
// Use pre-calculated pace data from the usage object
|
|
227
|
+
const paceData = ml.weekly.pace || null;
|
|
228
|
+
|
|
229
|
+
html += '<div class="model-container nested-container">';
|
|
230
|
+
html += usageItem('Weekly', weeklyPercent, '% used', { isZero, isNestedMetric: true, agentName: 'codex', resetsIn, metricId: `codex-${modelSlug}-weekly` });
|
|
231
|
+
html += resetInfoItem(ml.weekly.resetsIn, ml.weekly.resetsAt, 'weekly', { isZero, paceData });
|
|
232
|
+
html += '</div>';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return html;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatUsage(agentName, usage) {
|
|
239
|
+
if (!usage) {
|
|
240
|
+
return '<p class="usage-item">No data available</p>';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const formatters = { claude: formatClaudeUsage, gemini: formatGeminiUsage, codex: formatCodexUsage };
|
|
244
|
+
const formatter = formatters[agentName];
|
|
245
|
+
const html = formatter ? formatter(usage) : '';
|
|
246
|
+
|
|
247
|
+
return html || '<p class="usage-item">No usage data</p>';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
formatUsage,
|
|
252
|
+
formatClaudeUsage,
|
|
253
|
+
formatGeminiUsage,
|
|
254
|
+
formatCodexUsage,
|
|
255
|
+
usageItem,
|
|
256
|
+
resetInfoItem,
|
|
257
|
+
parseResetsInToSeconds,
|
|
258
|
+
CYCLE_DURATIONS
|
|
259
|
+
};
|