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.
@@ -0,0 +1,273 @@
1
+ /**
2
+ * History Store Module
3
+ *
4
+ * Stores historical usage snapshots to a local JSON file for trend analysis
5
+ * and pace evaluation. Automatically prunes records older than the configured
6
+ * retention period.
7
+ */
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const os = require('node:os');
12
+
13
+ // Default configuration
14
+ const DEFAULT_RETENTION_DAYS = 14;
15
+ const DEFAULT_HISTORY_DIR = path.join(os.homedir(), '.agent-tank');
16
+ const DEFAULT_HISTORY_FILE = 'history.json';
17
+
18
+ /**
19
+ * HistoryStore manages persistent storage of usage snapshots.
20
+ *
21
+ * Usage snapshots are stored with timestamps and automatically pruned
22
+ * based on the configured retention period.
23
+ */
24
+ class HistoryStore {
25
+ /**
26
+ * Create a HistoryStore instance.
27
+ *
28
+ * @param {Object} options - Configuration options
29
+ * @param {number} [options.retentionDays=14] - Number of days to retain history
30
+ * @param {string} [options.historyDir] - Directory for history file (default: ~/.agent-tank)
31
+ * @param {string} [options.historyFile] - History filename (default: history.json)
32
+ */
33
+ constructor(options = {}) {
34
+ this.retentionDays = options.retentionDays ?? DEFAULT_RETENTION_DAYS;
35
+ this.historyDir = options.historyDir ?? DEFAULT_HISTORY_DIR;
36
+ this.historyFile = options.historyFile ?? DEFAULT_HISTORY_FILE;
37
+ this.historyPath = path.join(this.historyDir, this.historyFile);
38
+
39
+ // In-memory cache of history data
40
+ this._history = null;
41
+ this._initialized = false;
42
+ }
43
+
44
+ /**
45
+ * Ensure the history directory exists.
46
+ * @private
47
+ */
48
+ _ensureDirectory() {
49
+ if (!fs.existsSync(this.historyDir)) {
50
+ fs.mkdirSync(this.historyDir, { recursive: true });
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Load history from disk into memory.
56
+ * @private
57
+ */
58
+ _loadHistory() {
59
+ if (this._initialized) {
60
+ return this._history;
61
+ }
62
+
63
+ try {
64
+ this._ensureDirectory();
65
+
66
+ if (fs.existsSync(this.historyPath)) {
67
+ const data = fs.readFileSync(this.historyPath, 'utf8');
68
+ this._history = JSON.parse(data);
69
+
70
+ // Validate structure
71
+ if (!Array.isArray(this._history)) {
72
+ console.warn('[HistoryStore] Invalid history format, resetting to empty array');
73
+ this._history = [];
74
+ }
75
+ } else {
76
+ this._history = [];
77
+ }
78
+ } catch (err) {
79
+ console.error('[HistoryStore] Error loading history:', err.message);
80
+ this._history = [];
81
+ }
82
+
83
+ this._initialized = true;
84
+ return this._history;
85
+ }
86
+
87
+ /**
88
+ * Save history from memory to disk.
89
+ * @private
90
+ */
91
+ _saveHistory() {
92
+ try {
93
+ this._ensureDirectory();
94
+ fs.writeFileSync(this.historyPath, JSON.stringify(this._history, null, 2));
95
+ } catch (err) {
96
+ console.error('[HistoryStore] Error saving history:', err.message);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Calculate the cutoff timestamp for pruning.
102
+ * @private
103
+ * @returns {number} Unix timestamp in milliseconds
104
+ */
105
+ _getCutoffTimestamp() {
106
+ const now = Date.now();
107
+ const retentionMs = this.retentionDays * 24 * 60 * 60 * 1000;
108
+ return now - retentionMs;
109
+ }
110
+
111
+ /**
112
+ * Prune records older than the retention period.
113
+ * @private
114
+ */
115
+ _pruneOldRecords() {
116
+ const cutoff = this._getCutoffTimestamp();
117
+ const originalLength = this._history.length;
118
+
119
+ this._history = this._history.filter(record => {
120
+ const recordTime = new Date(record.timestamp).getTime();
121
+ return recordTime >= cutoff;
122
+ });
123
+
124
+ const prunedCount = originalLength - this._history.length;
125
+ if (prunedCount > 0) {
126
+ console.log(`[HistoryStore] Pruned ${prunedCount} old records (>${this.retentionDays} days)`);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Add a usage snapshot to history.
132
+ *
133
+ * @param {string} agentName - Name of the agent (e.g., 'claude', 'gemini', 'codex')
134
+ * @param {Object} usageData - The usage data to store
135
+ * @param {string} [timestamp] - ISO timestamp (defaults to current time)
136
+ * @returns {Object} The stored record
137
+ */
138
+ addSnapshot(agentName, usageData, timestamp = null) {
139
+ if (!agentName || typeof agentName !== 'string') {
140
+ throw new Error('agentName is required and must be a string');
141
+ }
142
+
143
+ if (!usageData || typeof usageData !== 'object') {
144
+ throw new Error('usageData is required and must be an object');
145
+ }
146
+
147
+ this._loadHistory();
148
+
149
+ const record = {
150
+ timestamp: timestamp || new Date().toISOString(),
151
+ agent: agentName,
152
+ usage: usageData
153
+ };
154
+
155
+ this._history.push(record);
156
+ this._pruneOldRecords();
157
+ this._saveHistory();
158
+
159
+ return record;
160
+ }
161
+
162
+ /**
163
+ * Get all history records, optionally filtered by agent.
164
+ *
165
+ * @param {string} [agentName] - Filter by agent name (optional)
166
+ * @returns {Array} Array of history records
167
+ */
168
+ getHistory(agentName = null) {
169
+ this._loadHistory();
170
+ this._pruneOldRecords();
171
+
172
+ if (agentName) {
173
+ return this._history.filter(record => record.agent === agentName);
174
+ }
175
+
176
+ return [...this._history];
177
+ }
178
+
179
+ /**
180
+ * Get the most recent snapshot for an agent.
181
+ *
182
+ * @param {string} agentName - Name of the agent
183
+ * @returns {Object|null} The most recent record or null
184
+ */
185
+ getLatestSnapshot(agentName) {
186
+ const agentHistory = this.getHistory(agentName);
187
+
188
+ if (agentHistory.length === 0) {
189
+ return null;
190
+ }
191
+
192
+ // Sort by timestamp descending and return the first
193
+ return agentHistory.sort((a, b) =>
194
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
195
+ )[0];
196
+ }
197
+
198
+ /**
199
+ * Get snapshots within a specific time window.
200
+ *
201
+ * @param {string} agentName - Name of the agent
202
+ * @param {number} windowSeconds - Time window in seconds from now
203
+ * @returns {Array} Array of records within the window
204
+ */
205
+ getSnapshotsInWindow(agentName, windowSeconds) {
206
+ const agentHistory = this.getHistory(agentName);
207
+ const cutoff = Date.now() - (windowSeconds * 1000);
208
+
209
+ return agentHistory.filter(record =>
210
+ new Date(record.timestamp).getTime() >= cutoff
211
+ ).sort((a, b) =>
212
+ new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Clear all history records.
218
+ *
219
+ * @param {string} [agentName] - Clear only records for this agent (optional)
220
+ */
221
+ clearHistory(agentName = null) {
222
+ this._loadHistory();
223
+
224
+ if (agentName) {
225
+ this._history = this._history.filter(record => record.agent !== agentName);
226
+ } else {
227
+ this._history = [];
228
+ }
229
+
230
+ this._saveHistory();
231
+ }
232
+
233
+ /**
234
+ * Get statistics about stored history.
235
+ *
236
+ * @returns {Object} Statistics object
237
+ */
238
+ getStats() {
239
+ this._loadHistory();
240
+
241
+ const stats = {
242
+ totalRecords: this._history.length,
243
+ retentionDays: this.retentionDays,
244
+ historyPath: this.historyPath,
245
+ agents: {}
246
+ };
247
+
248
+ for (const record of this._history) {
249
+ if (!stats.agents[record.agent]) {
250
+ stats.agents[record.agent] = {
251
+ count: 0,
252
+ oldest: null,
253
+ newest: null
254
+ };
255
+ }
256
+
257
+ const agentStats = stats.agents[record.agent];
258
+ agentStats.count++;
259
+
260
+ const recordTime = new Date(record.timestamp).getTime();
261
+ if (!agentStats.oldest || recordTime < new Date(agentStats.oldest).getTime()) {
262
+ agentStats.oldest = record.timestamp;
263
+ }
264
+ if (!agentStats.newest || recordTime > new Date(agentStats.newest).getTime()) {
265
+ agentStats.newest = record.timestamp;
266
+ }
267
+ }
268
+
269
+ return stats;
270
+ }
271
+ }
272
+
273
+ module.exports = { HistoryStore, DEFAULT_RETENTION_DAYS };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * HTTP request handler utilities for AgentTank server.
3
+ */
4
+
5
+ const { statusPage } = require('./status-page.js');
6
+
7
+ /**
8
+ * Handle GET / - Status HTML page
9
+ */
10
+ function handleStatusPage(res, tank) {
11
+ res.setHeader('Content-Type', 'text/html');
12
+ res.writeHead(200);
13
+ res.end(statusPage(tank.getStatus()));
14
+ }
15
+
16
+ /**
17
+ * Handle GET /status - JSON status for all agents
18
+ */
19
+ function handleStatusJson(res, tank) {
20
+ const status = tank.getStatus();
21
+ console.log(`[HTTP] GET /status - returning status for ${Object.keys(status).length} agents`);
22
+ res.setHeader('Content-Type', 'application/json');
23
+ res.writeHead(200);
24
+ res.end(JSON.stringify(status, null, 2));
25
+ }
26
+
27
+ /**
28
+ * Handle GET /config - Server configuration
29
+ */
30
+ function handleConfig(res, tank) {
31
+ const config = {
32
+ autoRefresh: {
33
+ enabled: tank.autoRefresh.enabled && tank.autoRefresh.interval > 0,
34
+ interval: tank.autoRefresh.interval,
35
+ },
36
+ history: {
37
+ retentionDays: tank.historyRetentionDays,
38
+ },
39
+ lastRefreshedAt: tank.lastRefreshedAt,
40
+ };
41
+ res.setHeader('Content-Type', 'application/json');
42
+ res.writeHead(200);
43
+ res.end(JSON.stringify(config, null, 2));
44
+ }
45
+
46
+ /**
47
+ * Handle GET /history - History statistics
48
+ */
49
+ function handleHistoryStats(res, tank) {
50
+ const stats = tank.getHistoryStats();
51
+ res.setHeader('Content-Type', 'application/json');
52
+ res.writeHead(200);
53
+ res.end(JSON.stringify(stats, null, 2));
54
+ }
55
+
56
+ /**
57
+ * Handle GET /history/:agent - Agent-specific history
58
+ */
59
+ function handleAgentHistory(res, tank, agentName) {
60
+ const history = tank.getHistory(agentName);
61
+ res.setHeader('Content-Type', 'application/json');
62
+ res.writeHead(200);
63
+ res.end(JSON.stringify(history, null, 2));
64
+ }
65
+
66
+ /**
67
+ * Handle GET /status/:agent - Agent-specific status
68
+ */
69
+ function handleAgentStatus(res, tank, agentName) {
70
+ const status = tank.getAgentStatus(agentName);
71
+ if (!status) {
72
+ res.writeHead(404);
73
+ res.end(JSON.stringify({ error: 'Agent not found' }));
74
+ return;
75
+ }
76
+ res.setHeader('Content-Type', 'application/json');
77
+ res.writeHead(200);
78
+ res.end(JSON.stringify(status, null, 2));
79
+ }
80
+
81
+ /**
82
+ * Handle POST /refresh - Refresh all agents
83
+ */
84
+ async function handleRefreshAll(res, tank) {
85
+ await tank.refreshAll();
86
+ res.setHeader('Content-Type', 'application/json');
87
+ res.writeHead(200);
88
+ res.end(JSON.stringify({ success: true, status: tank.getStatus() }));
89
+ }
90
+
91
+ /**
92
+ * Handle POST /refresh/:agent - Refresh specific agent
93
+ */
94
+ async function handleRefreshAgent(res, tank, agentName) {
95
+ try {
96
+ await tank.refreshAgent(agentName);
97
+ res.setHeader('Content-Type', 'application/json');
98
+ res.writeHead(200);
99
+ res.end(JSON.stringify({ success: true, status: tank.getAgentStatus(agentName) }));
100
+ } catch (err) {
101
+ res.writeHead(404);
102
+ res.end(JSON.stringify({ error: err.message }));
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Handle 404 - Not Found
108
+ */
109
+ function handleNotFound(res) {
110
+ res.writeHead(404);
111
+ res.end(JSON.stringify({ error: 'Not found' }));
112
+ }
113
+
114
+ /**
115
+ * Route and handle HTTP requests.
116
+ * @param {Object} req - HTTP request
117
+ * @param {Object} res - HTTP response
118
+ * @param {Object} tank - AgentTank instance
119
+ */
120
+ async function handleRequest(req, res, tank) {
121
+ const url = new URL(req.url, `http://localhost:${tank.port}`);
122
+ const path = url.pathname;
123
+
124
+ // CORS headers
125
+ res.setHeader('Access-Control-Allow-Origin', '*');
126
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
127
+
128
+ if (req.method === 'OPTIONS') {
129
+ res.writeHead(204);
130
+ res.end();
131
+ return;
132
+ }
133
+
134
+ if (!tank.authenticate(req, url)) {
135
+ res.setHeader('WWW-Authenticate', 'Basic realm="LLM Limit Watcher"');
136
+ res.writeHead(401);
137
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
138
+ return;
139
+ }
140
+
141
+ // Route requests
142
+ if (req.method === 'GET') {
143
+ if (path === '/') {
144
+ handleStatusPage(res, tank);
145
+ } else if (path === '/status') {
146
+ handleStatusJson(res, tank);
147
+ } else if (path === '/config') {
148
+ handleConfig(res, tank);
149
+ } else if (path === '/history') {
150
+ handleHistoryStats(res, tank);
151
+ } else if (path.startsWith('/history/')) {
152
+ handleAgentHistory(res, tank, path.slice(9));
153
+ } else if (path.startsWith('/status/')) {
154
+ handleAgentStatus(res, tank, path.slice(8));
155
+ } else {
156
+ handleNotFound(res);
157
+ }
158
+ } else if (req.method === 'POST') {
159
+ if (path === '/refresh') {
160
+ await handleRefreshAll(res, tank);
161
+ } else if (path.startsWith('/refresh/')) {
162
+ await handleRefreshAgent(res, tank, path.slice(9));
163
+ } else {
164
+ handleNotFound(res);
165
+ }
166
+ } else {
167
+ handleNotFound(res);
168
+ }
169
+ }
170
+
171
+ module.exports = {
172
+ handleRequest,
173
+ handleStatusPage,
174
+ handleStatusJson,
175
+ handleConfig,
176
+ handleHistoryStats,
177
+ handleAgentHistory,
178
+ handleAgentStatus,
179
+ handleRefreshAll,
180
+ handleRefreshAgent,
181
+ handleNotFound
182
+ };
package/src/icons.js ADDED
@@ -0,0 +1,67 @@
1
+ // Brand icons for each agent - inline SVG for performance and styling flexibility
2
+
3
+ // Tank logo icon for Agent Tank branding - minimalist U-shaped vertical tank
4
+ const tankIcon = `<svg class="tank-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
5
+ <path d="M6 4v12a4 4 0 0 0 4 4h4a4 4 0 0 0 4-4V4"/>
6
+ <line x1="6" y1="4" x2="18" y2="4"/>
7
+ <line x1="8" y1="8" x2="16" y2="8" opacity="0.3"/>
8
+ <line x1="8" y1="12" x2="16" y2="12" opacity="0.3"/>
9
+ </svg>`;
10
+
11
+ const agentIcons = {
12
+ claude: `<svg class="agent-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
13
+ <path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />
14
+ </svg>`,
15
+ gemini: `<svg class="agent-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
16
+ <path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" />
17
+ </svg>`,
18
+ codex: `<svg class="agent-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
19
+ <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
20
+ </svg>`
21
+ };
22
+
23
+ // Inline SVG refresh icon for card headers
24
+ const refreshIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
25
+ <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
26
+ <path d="M21 3v5h-5"/>
27
+ </svg>`;
28
+
29
+ // Crosshair/target icon for tracking metrics
30
+ const trackIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+ <circle cx="12" cy="12" r="10"/>
32
+ <circle cx="12" cy="12" r="6"/>
33
+ <circle cx="12" cy="12" r="2"/>
34
+ <line x1="12" y1="2" x2="12" y2="6"/>
35
+ <line x1="12" y1="18" x2="12" y2="22"/>
36
+ <line x1="2" y1="12" x2="6" y2="12"/>
37
+ <line x1="18" y1="12" x2="22" y2="12"/>
38
+ </svg>`;
39
+
40
+ // Monitor icon for top nav toggle
41
+ const monitorIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
42
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
43
+ <line x1="8" y1="21" x2="16" y2="21"/>
44
+ <line x1="12" y1="17" x2="12" y2="21"/>
45
+ </svg>`;
46
+
47
+ // Sync icon (circular arrows) for Refresh All button
48
+ const syncIcon = `<svg class="sync-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
49
+ <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
50
+ <path d="M21 3v5h-5"/>
51
+ </svg>`;
52
+
53
+ // Copy icon for copying API endpoint
54
+ const copyIcon = `<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
55
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
56
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
57
+ </svg>`;
58
+
59
+ module.exports = {
60
+ agentIcons,
61
+ refreshIcon,
62
+ trackIcon,
63
+ monitorIcon,
64
+ tankIcon,
65
+ syncIcon,
66
+ copyIcon
67
+ };