aisessions 1.0.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/src/ui/js.js ADDED
@@ -0,0 +1,528 @@
1
+ export const PAGE_JS = `
2
+ (function () {
3
+ 'use strict';
4
+
5
+ // ── state ─────────────────────────────────────────────────────────────────────
6
+ var ALL = [];
7
+ var filtered = [];
8
+ var AGENTS_MAP = {}; // id → {label, color}
9
+ var selected = new Set();
10
+ var agentFilter = 'all';
11
+ var activeTab = 'sessions';
12
+ var grouped = true;
13
+ var collapsed = new Set();
14
+ var PAGE = 0;
15
+ var PER_PAGE = 100;
16
+ var usageCache = {}; // keyed by agentFilter
17
+
18
+ // ── utils ─────────────────────────────────────────────────────────────────────
19
+ function $(id) { return document.getElementById(id); }
20
+ function esc(s) {
21
+ return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
23
+ }
24
+ function fmtNum(n) { return (n || 0).toLocaleString(); }
25
+ function fmtCost(n) {
26
+ n = n || 0;
27
+ if (n >= 1000) return '$' + (n / 1000).toFixed(2) + 'K';
28
+ if (n >= 1) return '$' + n.toFixed(2);
29
+ return '$' + n.toFixed(4);
30
+ }
31
+ function fmtTok(n) {
32
+ n = n || 0;
33
+ if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
34
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
35
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
36
+ return String(n);
37
+ }
38
+ async function post(url, body) {
39
+ var r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
40
+ return r.json();
41
+ }
42
+ var toastTimer;
43
+ function toast(msg, type, dur) {
44
+ var el = $('toast');
45
+ el.textContent = msg;
46
+ el.className = 'show' + (type ? ' ' + type : '');
47
+ clearTimeout(toastTimer);
48
+ toastTimer = setTimeout(function () { el.className = ''; }, dur || 2800);
49
+ }
50
+
51
+ function agentLabel(id) {
52
+ return id === 'all' ? 'ALL AGENTS' : ((AGENTS_MAP[id] && AGENTS_MAP[id].label) || id).toUpperCase();
53
+ }
54
+
55
+ // ── global event delegation ───────────────────────────────────────────────────
56
+ document.addEventListener('click', function (e) {
57
+ var btn = e.target.closest('[data-action]');
58
+ if (btn) {
59
+ e.stopPropagation();
60
+ var a = btn.dataset.action;
61
+ var p = btn.dataset.path || '';
62
+ var meta= btn.dataset.meta || '';
63
+ var ag = btn.dataset.agent || '';
64
+ var nav = btn.dataset.nav || '';
65
+ if (a === 'filter-agent') { setAgentFilter(ag); return; }
66
+ if (a === 'switch-tab') { switchTab(nav); return; }
67
+ if (a === 'agent-tab') { switchTab(nav); return; }
68
+ if (a === 'switch-tab-global') { setAgentFilter('all'); switchTab(nav); return; }
69
+ if (a === 'trash-one') { trashOne(p); return; }
70
+ if (a === 'backup-one') { backupOne(p); return; }
71
+ if (a === 'restore-trash') { restoreTrash(meta); return; }
72
+ if (a === 'purge-trash') { purgeTrash(meta); return; }
73
+ if (a === 'restore-backup') { restoreBackup(meta); return; }
74
+ if (a === 'delete-backup') { deleteBackup(meta); return; }
75
+ return;
76
+ }
77
+ var row = e.target.closest('.tbl-row');
78
+ if (row && e.target.tagName !== 'INPUT') {
79
+ var path = row.dataset.path; if (!path) return;
80
+ selected.has(path) ? selected.delete(path) : selected.add(path);
81
+ row.classList.toggle('selected', selected.has(path));
82
+ var cb = row.querySelector('input[type=checkbox]');
83
+ if (cb) cb.checked = selected.has(path);
84
+ updateSelInfo();
85
+ return;
86
+ }
87
+ if (e.target.tagName === 'INPUT' && e.target.type === 'checkbox') {
88
+ var r2 = e.target.closest('.tbl-row'); if (!r2) return;
89
+ var p2 = r2.dataset.path;
90
+ e.target.checked ? selected.add(p2) : selected.delete(p2);
91
+ r2.classList.toggle('selected', selected.has(p2));
92
+ updateSelInfo(); return;
93
+ }
94
+ var gh = e.target.closest('.group-hdr');
95
+ if (gh) { var k = gh.dataset.gkey; collapsed.has(k) ? collapsed.delete(k) : collapsed.add(k); renderSessions(); }
96
+ });
97
+
98
+ // ── INIT ──────────────────────────────────────────────────────────────────────
99
+ async function init() {
100
+ var tblBody = $('tbl-body');
101
+ if (!tblBody) return;
102
+ tblBody.innerHTML = '<div class="loading"><span class="loading-dots">LOADING SESSIONS</span></div>';
103
+ try {
104
+ var agents = await fetch('/api/agents').then(function(r) { return r.json(); });
105
+ var sessions = await fetch('/api/sessions').then(function(r) { return r.json(); });
106
+ agents.forEach(function(a) { AGENTS_MAP[a.id] = a; });
107
+ ALL = sessions;
108
+ buildSidebar(agents, sessions);
109
+ applyFilter();
110
+ var fi = $('foot-info');
111
+ if (fi) fi.textContent = sessions.length + ' SESSIONS / ' + agents.length + ' AGENT' + (agents.length !== 1 ? 'S' : '');
112
+ } catch (e) {
113
+ console.error('[AM]', e);
114
+ if (tblBody) tblBody.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>LOAD FAILED: ' + esc(e.message) + '</div></div>';
115
+ }
116
+ }
117
+
118
+ // ── SIDEBAR ───────────────────────────────────────────────────────────────────
119
+ function buildSidebar(agents, sessions) {
120
+ var counts = {};
121
+ sessions.forEach(function(s) { counts[s.agent] = (counts[s.agent] || 0) + 1; });
122
+ var c = $('sb-all-count'); if (c) c.textContent = sessions.length;
123
+ var sb = $('sb-agents'); if (!sb) return;
124
+ if (!agents.length) { sb.innerHTML = '<div class="sb-label" style="color:var(--muted)">NO AGENTS DETECTED</div>'; return; }
125
+ sb.innerHTML = agents.map(function(a) {
126
+ var aid = esc(a.id);
127
+ return '<button class="sb-item" data-action="filter-agent" data-agent="' + aid + '">' +
128
+ '<span class="agent-dot" style="background:' + esc(a.color) + '"></span>' +
129
+ esc(a.label) + '<span class="sb-count">' + (counts[a.id] || 0) + '</span></button>' +
130
+ '<div class="sb-sub" id="sub-' + aid + '">' +
131
+ '<button class="sb-sub-item" data-action="agent-tab" data-nav="sessions">[=] SESSIONS</button>' +
132
+ '<button class="sb-sub-item" data-action="agent-tab" data-nav="usage">[$] USAGE</button>' +
133
+ '<button class="sb-sub-item" data-action="agent-tab" data-nav="trash">[T] TRASH</button>' +
134
+ '<button class="sb-sub-item" data-action="agent-tab" data-nav="backups">[B] BACKUPS</button>' +
135
+ '</div>';
136
+ }).join('');
137
+ }
138
+
139
+ function setSidebarActive(id) {
140
+ document.querySelectorAll('.sb-item[data-action="filter-agent"]').forEach(function(el) {
141
+ el.classList.toggle('active', el.dataset.agent === id);
142
+ });
143
+ document.querySelectorAll('.sb-item[data-agent="all"]').forEach(function(el) {
144
+ el.classList.toggle('active', id === 'all');
145
+ });
146
+ }
147
+
148
+ function setAgentFilter(id) {
149
+ agentFilter = id; PAGE = 0;
150
+ setSidebarActive(id);
151
+ updateContextLabel();
152
+
153
+ // Show sub-items for selected agent, hide all others
154
+ document.querySelectorAll('.sb-sub').forEach(function(el) {
155
+ el.classList.toggle('show', id !== 'all' && el.id === 'sub-' + id);
156
+ });
157
+
158
+ // Always land on sessions when switching agent
159
+ switchTab('sessions');
160
+ }
161
+
162
+ // ── CONTEXT LABEL (shows current filter scope in each panel) ──────────────────
163
+ function updateContextLabel() {
164
+ var label = agentFilter === 'all' ? 'ALL AGENTS' : agentLabel(agentFilter);
165
+ document.querySelectorAll('.ctx-label').forEach(function(el) { el.textContent = label; });
166
+ }
167
+
168
+ // ── SEARCH / FILTER ───────────────────────────────────────────────────────────
169
+ var searchEl = $('search');
170
+ if (searchEl) searchEl.addEventListener('input', applyFilter);
171
+
172
+ function applyFilter() {
173
+ var q = (($('search') || {}).value || '').toLowerCase().trim();
174
+ filtered = ALL.filter(function(s) {
175
+ if (agentFilter !== 'all' && s.agent !== agentFilter) return false;
176
+ if (!q) return true;
177
+ return q.split(' ').every(function(w) {
178
+ return (s.title + ' ' + s.project + ' ' + s.agentLabel + ' ' + s.agent).toLowerCase().includes(w);
179
+ });
180
+ });
181
+ selected.clear(); PAGE = 0;
182
+ renderSessions();
183
+ }
184
+
185
+ // ── SESSIONS RENDER ───────────────────────────────────────────────────────────
186
+ function updateSelInfo() {
187
+ var el = $('sel-info');
188
+ if (el) el.textContent = selected.size ? selected.size + ' SELECTED' : filtered.length + ' SESSIONS';
189
+ }
190
+
191
+ function renderSessions() {
192
+ updateSelInfo();
193
+ var wrap = $('tbl-body'); if (!wrap) return;
194
+ if (!filtered.length) {
195
+ wrap.innerHTML = '<div class="empty"><div class="e-icon">[_]</div><div>NO SESSIONS FOUND</div></div>';
196
+ var pg = $('pagination'); if (pg) pg.style.display = 'none';
197
+ return;
198
+ }
199
+ if (grouped) renderGrouped(wrap);
200
+ else renderFlat(wrap);
201
+ }
202
+
203
+ function renderGrouped(wrap) {
204
+ var groups = new Map();
205
+ filtered.forEach(function(s) {
206
+ var k = s.agent + '::' + s.project;
207
+ if (!groups.has(k)) groups.set(k, { agent: s.agent, agentLabel: s.agentLabel, project: s.project, items: [] });
208
+ groups.get(k).items.push(s);
209
+ });
210
+ var html = '';
211
+ groups.forEach(function(g, k) {
212
+ var col = collapsed.has(k);
213
+ var shown = col ? [] : g.items.slice(0, 50);
214
+ var more = col ? 0 : Math.max(0, g.items.length - 50);
215
+ html += '<div class="group-hdr' + (col ? ' collapsed' : '') + '" data-gkey="' + esc(k) + '">' +
216
+ '<span class="g-proj">' + esc(g.project) + '</span>' +
217
+ '<span class="agent-badge">' + esc(g.agentLabel) + '</span>' +
218
+ '<span class="g-count">' + g.items.length + ' session' + (g.items.length !== 1 ? 's' : '') + '</span>' +
219
+ '<span class="g-chev">&#9660;</span></div>';
220
+ shown.forEach(function(s) { html += rowHtml(s); });
221
+ if (more) html += '<div style="padding:8px 16px;font-size:16px;color:var(--muted)">...' + more + ' MORE (search to filter)</div>';
222
+ });
223
+ wrap.innerHTML = html;
224
+ var pg = $('pagination'); if (pg) pg.style.display = 'none';
225
+ }
226
+
227
+ function renderFlat(wrap) {
228
+ var total = filtered.length, pages = Math.ceil(total / PER_PAGE);
229
+ if (PAGE >= pages) PAGE = Math.max(0, pages - 1);
230
+ wrap.innerHTML = filtered.slice(PAGE * PER_PAGE, (PAGE + 1) * PER_PAGE).map(rowHtml).join('');
231
+ var pag = $('pagination'); if (!pag) return;
232
+ if (pages <= 1) { pag.style.display = 'none'; return; }
233
+ pag.style.display = 'flex';
234
+ pag.innerHTML =
235
+ '<button class="btn" id="pg-prev"' + (PAGE === 0 ? ' disabled' : '') + '>PREV</button>' +
236
+ '<span>PAGE ' + (PAGE + 1) + ' / ' + pages + ' &nbsp;&middot;&nbsp; ' + total + ' SESSIONS</span>' +
237
+ '<button class="btn" id="pg-next"' + (PAGE >= pages - 1 ? ' disabled' : '') + '>NEXT</button>';
238
+ var prev = $('pg-prev'), next = $('pg-next');
239
+ if (prev) prev.addEventListener('click', function() { PAGE--; renderSessions(); });
240
+ if (next) next.addEventListener('click', function() { PAGE++; renderSessions(); });
241
+ }
242
+
243
+ function rowHtml(s) {
244
+ var sel = selected.has(s.path) ? ' selected' : '';
245
+ var chk = selected.has(s.path) ? ' checked' : '';
246
+ var p = esc(s.path);
247
+ var agt = esc((s.agent || '?').toUpperCase().slice(0, 6));
248
+ var agtL = esc(s.agentLabel || s.agent || '');
249
+ var ttl = esc(s.title || 'Untitled');
250
+ var msgs = (s.msgCount ? s.msgCount + ' msg' : '?');
251
+ return '<div class="tbl-row' + sel + '" data-path="' + p + '">' +
252
+ '<input type="checkbox"' + chk + '>' +
253
+ '<span class="agent-badge" title="' + agtL + '">' + agt + '</span>' +
254
+ '<span class="col-title" title="' + ttl + '">' + ttl + '</span>' +
255
+ '<span class="col-msgs">' + msgs + '</span>' +
256
+ '<span class="col-size">' + esc(s.sizeLabel || '') + '</span>' +
257
+ '<span class="col-date">' + esc(s.dateLabel || '') + '</span>' +
258
+ '<span class="row-actions">' +
259
+ '<button class="act-btn" data-action="backup-one" data-path="' + p + '" title="Backup">BAK</button>' +
260
+ '<button class="act-btn del" data-action="trash-one" data-path="' + p + '" title="Trash">DEL</button>' +
261
+ '</span></div>';
262
+ }
263
+
264
+ // ── TOOLBAR ───────────────────────────────────────────────────────────────────
265
+ function wireBtn(id, fn) { var el = $(id); if (el) el.addEventListener('click', fn); }
266
+
267
+ wireBtn('btn-sel-all', function() { filtered.forEach(function(s) { selected.add(s.path); }); renderSessions(); });
268
+ wireBtn('btn-sel-none', function() { selected.clear(); renderSessions(); });
269
+ wireBtn('btn-group', function() {
270
+ grouped = !grouped;
271
+ var el = $('btn-group');
272
+ if (el) { el.textContent = grouped ? 'FLAT VIEW' : 'GROUP VIEW'; el.classList.toggle('active-btn', !grouped); }
273
+ PAGE = 0; renderSessions();
274
+ });
275
+
276
+ wireBtn('btn-trash', async function() {
277
+ if (!selected.size) { toast('SELECT SESSIONS FIRST', 'err'); return; }
278
+ if (!confirm('Move ' + selected.size + ' session(s) to trash?')) return;
279
+ var items = buildItems(Array.from(selected));
280
+ var res = await post('/api/trash/move', { items: items });
281
+ var ok = res.filter(function(r) { return r.ok; }).map(function(r) { return r.path; });
282
+ ok.forEach(function(p) { ALL = ALL.filter(function(s) { return s.path !== p; }); selected.delete(p); });
283
+ applyFilter(); toast(ok.length + ' MOVED TO TRASH', 'ok');
284
+ });
285
+ wireBtn('btn-backup', async function() {
286
+ if (!selected.size) { toast('SELECT SESSIONS FIRST', 'err'); return; }
287
+ var items = buildItems(Array.from(selected));
288
+ var res = await post('/api/backup/create', { items: items, note: '' });
289
+ toast(res.filter(function(r) { return r.ok; }).length + ' BACKUP(S) CREATED', 'ok');
290
+ });
291
+
292
+ // Enrich paths with session metadata for storage
293
+ function buildItems(paths) {
294
+ return paths.map(function(p) {
295
+ var s = ALL.find(function(s) { return s.path === p; }) || {};
296
+ return { path: p, agent: s.agent || '', agentLabel: s.agentLabel || '', project: s.project || '', title: s.title || '' };
297
+ });
298
+ }
299
+
300
+ // ── TAB SWITCHING ─────────────────────────────────────────────────────────────
301
+ function switchTab(tab) {
302
+ activeTab = tab;
303
+ // Show correct panel
304
+ document.querySelectorAll('.panel').forEach(function(p) { p.classList.toggle('active', p.id === tab + '-panel'); });
305
+ // Mark active sub-item (agent-specific nav)
306
+ document.querySelectorAll('.sb-sub-item').forEach(function(el) {
307
+ el.classList.toggle('active', el.dataset.nav === tab);
308
+ });
309
+ // Mark global tools active only when agentFilter is all
310
+ document.querySelectorAll('.sb-item[data-action="switch-tab-global"]').forEach(function(el) {
311
+ el.classList.toggle('active', el.dataset.nav === tab && agentFilter === 'all');
312
+ });
313
+ if (tab === 'usage') loadUsage(false);
314
+ if (tab === 'trash') loadTrash(false);
315
+ if (tab === 'backups') loadBackups(false);
316
+ }
317
+ document.querySelectorAll('.tab').forEach(function(t) {
318
+ t.addEventListener('click', function() { switchTab(t.dataset.tab); });
319
+ });
320
+
321
+ // ── ROW ACTIONS ───────────────────────────────────────────────────────────────
322
+ async function trashOne(path) {
323
+ if (!path || !confirm('Move to trash?')) return;
324
+ var items = buildItems([path]);
325
+ var res = await post('/api/trash/move', { items: items });
326
+ if (res[0] && res[0].ok) {
327
+ ALL = ALL.filter(function(s) { return s.path !== path; }); selected.delete(path);
328
+ applyFilter(); toast('MOVED TO TRASH', 'ok');
329
+ } else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
330
+ }
331
+ async function backupOne(path) {
332
+ if (!path) return;
333
+ var items = buildItems([path]);
334
+ var res = await post('/api/backup/create', { items: items, note: '' });
335
+ if (res[0] && res[0].ok) toast('BACKUP CREATED', 'ok');
336
+ else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
337
+ }
338
+
339
+ // ── USAGE TAB ─────────────────────────────────────────────────────────────────
340
+ async function loadUsage(force) {
341
+ var key = agentFilter;
342
+ var panel = $('usage-panel'); if (!panel) return;
343
+ if (!force && usageCache[key]) { panel.innerHTML = usageCache[key]; return; }
344
+ panel.innerHTML = '<div class="loading"><span class="loading-dots">LOADING ' + agentLabel(key) + ' USAGE</span></div>';
345
+ try {
346
+ var url = '/api/usage' + (key !== 'all' ? '?agent=' + encodeURIComponent(key) : '');
347
+ var data = await fetch(url).then(function(r) { return r.json(); });
348
+ var html = renderUsagePanel(data, key);
349
+ usageCache[key] = html;
350
+ panel.innerHTML = html;
351
+ } catch (e) {
352
+ panel.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(e.message) + '</div></div>';
353
+ }
354
+ }
355
+
356
+ function renderUsagePanel(data, key) {
357
+ if (!data.available) {
358
+ return '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(data.error || 'ccusage unavailable') + '</div></div>';
359
+ }
360
+ var t = data.totals || {};
361
+ var days = data.daily || [];
362
+
363
+ var ctxLine = '<div class="ctx-banner">SCOPE: <span class="ctx-label">' + agentLabel(key) + '</span> &nbsp;|&nbsp; ' + days.length + ' DAYS ON RECORD</div>';
364
+
365
+ // ── stat cards ────────────────────────────────────────────────────────────
366
+ var stats = '<div class="stat-grid">' +
367
+ sc('TOTAL COST', fmtCost(t.totalCost), '') +
368
+ sc('TOTAL TOKENS', fmtTok(t.totalTokens), '') +
369
+ sc('INPUT', fmtTok(t.inputTokens), '') +
370
+ sc('OUTPUT', fmtTok(t.outputTokens), '') +
371
+ sc('CACHE READ', fmtTok(t.cacheReadTokens), '') +
372
+ sc('CACHE WRITE', fmtTok(t.cacheCreationTokens), '') +
373
+ (t.reasoningTokens ? sc('REASONING', fmtTok(t.reasoningTokens), '') : '') +
374
+ '</div>';
375
+
376
+ // ── aggregate models ──────────────────────────────────────────────────────
377
+ var modelMap = {};
378
+ days.forEach(function(d) {
379
+ (d.models || []).forEach(function(m) {
380
+ if (!modelMap[m.name]) modelMap[m.name] = { cost: 0, input: 0, output: 0, cacheRead: 0, reasoning: 0 };
381
+ var mm = modelMap[m.name];
382
+ mm.cost += m.cost || 0;
383
+ mm.input += m.input || 0;
384
+ mm.output += m.output || 0;
385
+ mm.cacheRead += m.cacheRead || 0;
386
+ mm.reasoning += m.reasoning || 0;
387
+ });
388
+ });
389
+
390
+ var modelSection = '';
391
+ var modelKeys = Object.keys(modelMap);
392
+ if (modelKeys.length) {
393
+ var mRows = modelKeys
394
+ .sort(function(a, b) { return modelMap[b].cost - modelMap[a].cost; })
395
+ .map(function(name) {
396
+ var m = modelMap[name];
397
+ return '<tr><td>' + esc(name) + '</td><td>' + fmtCost(m.cost) + '</td>' +
398
+ '<td>' + fmtTok(m.input) + '</td><td>' + fmtTok(m.output) + '</td>' +
399
+ '<td>' + fmtTok(m.cacheRead) + '</td>' +
400
+ '<td>' + (m.reasoning ? fmtTok(m.reasoning) : '—') + '</td></tr>';
401
+ }).join('');
402
+ modelSection = '<div class="u-section"><h3>BY MODEL (' + modelKeys.length + ')</h3>' +
403
+ '<table class="u-table"><thead><tr>' +
404
+ '<th>MODEL</th><th>COST</th><th>INPUT</th><th>OUTPUT</th><th>CACHE READ</th><th>REASONING</th>' +
405
+ '</tr></thead><tbody>' + mRows + '</tbody></table></div>';
406
+ }
407
+
408
+ // ── daily table (shown first for immediate visibility) ────────────────────
409
+ var dailySection = '';
410
+ if (days.length) {
411
+ var dRows = days.slice().reverse().map(function(d) {
412
+ var modelNames = (d.models || []).map(function(m) { return m.name; }).join(', ');
413
+ return '<tr>' +
414
+ '<td>' + esc(d.date || '') + '</td>' +
415
+ '<td>' + fmtCost(d.cost) + '</td>' +
416
+ '<td>' + fmtTok(d.input) + '</td>' +
417
+ '<td>' + fmtTok(d.output) + '</td>' +
418
+ '<td>' + fmtTok(d.cacheRead) + '</td>' +
419
+ (d.reasoning ? '<td>' + fmtTok(d.reasoning) + '</td>' : '<td>—</td>') +
420
+ '<td style="font-size:14px;color:var(--muted)">' + esc(modelNames) + '</td>' +
421
+ '</tr>';
422
+ }).join('');
423
+ dailySection = '<div class="u-section"><h3>BY DATE (' + days.length + ' DAYS)</h3>' +
424
+ '<table class="u-table"><thead><tr>' +
425
+ '<th>DATE</th><th>COST</th><th>INPUT</th><th>OUTPUT</th><th>CACHE</th><th>REASONING</th><th>MODELS</th>' +
426
+ '</tr></thead><tbody>' + dRows + '</tbody></table></div>';
427
+ }
428
+
429
+ // Order: stats → daily (most useful) → by model
430
+ return ctxLine + stats + dailySection + modelSection;
431
+ }
432
+
433
+ function sc(label, value, sub) {
434
+ return '<div class="stat-card"><div class="s-label">' + esc(label) + '</div>' +
435
+ '<div class="s-value">' + esc(value) + '</div>' +
436
+ (sub ? '<div class="s-sub">' + esc(sub) + '</div>' : '') + '</div>';
437
+ }
438
+
439
+ // ── TRASH TAB ─────────────────────────────────────────────────────────────────
440
+ async function loadTrash(force) {
441
+ var panel = $('trash-panel'); if (!panel) return;
442
+ panel.innerHTML = '<div class="loading"><span class="loading-dots">LOADING</span></div>';
443
+ try {
444
+ var url = '/api/trash' + (agentFilter !== 'all' ? '?agent=' + encodeURIComponent(agentFilter) : '');
445
+ var items = await fetch(url).then(function(r) { return r.json(); });
446
+ var ctx = '<div class="ctx-banner">SCOPE: <span class="ctx-label">' + agentLabel(agentFilter) + '</span></div>';
447
+ if (!items.length) {
448
+ panel.innerHTML = ctx + '<div class="empty"><div class="e-icon">[_]</div><div>TRASH IS EMPTY</div></div>';
449
+ return;
450
+ }
451
+ panel.innerHTML = ctx + '<div class="list-panel-wrap">' + items.map(function(meta) {
452
+ var mp = esc(meta.metaPath);
453
+ var subL = [meta.agentLabel, meta.project, 'DELETED ' + (meta.deletedAt || '')].filter(Boolean).join(' / ');
454
+ return '<div class="meta-card">' +
455
+ '<div class="meta-body">' +
456
+ '<div class="meta-title">' + esc(meta.title || meta.originalPath || 'Unknown') + '</div>' +
457
+ '<div class="meta-sub">' + esc(subL) + '</div>' +
458
+ '</div>' +
459
+ '<div class="meta-actions">' +
460
+ '<button class="btn success" data-action="restore-trash" data-meta="' + mp + '">RESTORE</button>' +
461
+ '<button class="btn danger" data-action="purge-trash" data-meta="' + mp + '">PURGE</button>' +
462
+ '</div></div>';
463
+ }).join('') + '</div>';
464
+ } catch (e) {
465
+ panel.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(e.message) + '</div></div>';
466
+ }
467
+ }
468
+
469
+ async function restoreTrash(mp) {
470
+ var res = await post('/api/trash/restore', { metaPath: mp });
471
+ if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); loadTrash(true); }
472
+ else toast('ERROR: ' + (res.error || '?'), 'err');
473
+ }
474
+ async function purgeTrash(mp) {
475
+ if (!confirm('Permanently delete? Cannot be undone.')) return;
476
+ var res = await post('/api/trash/purge', { metaPaths: [mp] });
477
+ if (res[0] && res[0].ok) { toast('PERMANENTLY DELETED', 'ok'); loadTrash(true); }
478
+ else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
479
+ }
480
+
481
+ // ── BACKUPS TAB ───────────────────────────────────────────────────────────────
482
+ async function loadBackups(force) {
483
+ var panel = $('backups-panel'); if (!panel) return;
484
+ panel.innerHTML = '<div class="loading"><span class="loading-dots">LOADING</span></div>';
485
+ try {
486
+ var url = '/api/backups' + (agentFilter !== 'all' ? '?agent=' + encodeURIComponent(agentFilter) : '');
487
+ var items = await fetch(url).then(function(r) { return r.json(); });
488
+ var ctx = '<div class="ctx-banner">SCOPE: <span class="ctx-label">' + agentLabel(agentFilter) + '</span></div>';
489
+ if (!items.length) {
490
+ panel.innerHTML = ctx + '<div class="empty"><div class="e-icon">[_]</div><div>NO BACKUPS YET</div></div>';
491
+ return;
492
+ }
493
+ panel.innerHTML = ctx + '<div class="list-panel-wrap">' + items.map(function(meta) {
494
+ var mp = esc(meta.metaPath);
495
+ var note = meta.note ? ' / ' + esc(meta.note) : '';
496
+ var subL = [meta.agentLabel, meta.project, 'BACKED UP ' + (meta.backedUpAt || '')].filter(Boolean).join(' / ') + note;
497
+ return '<div class="meta-card">' +
498
+ '<div class="meta-body">' +
499
+ '<div class="meta-title">' + esc(meta.title || meta.originalPath || meta.filename || 'Unknown') + '</div>' +
500
+ '<div class="meta-sub">' + esc(subL) + '</div>' +
501
+ '</div>' +
502
+ '<div class="meta-actions">' +
503
+ '<button class="btn success" data-action="restore-backup" data-meta="' + mp + '">RESTORE</button>' +
504
+ '<button class="btn danger" data-action="delete-backup" data-meta="' + mp + '">DELETE</button>' +
505
+ '</div></div>';
506
+ }).join('') + '</div>';
507
+ } catch (e) {
508
+ panel.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(e.message) + '</div></div>';
509
+ }
510
+ }
511
+
512
+ async function restoreBackup(mp) {
513
+ var res = await post('/api/backup/restore', { metaPath: mp });
514
+ if (res.ok) toast('RESTORED TO ' + res.restoredTo, 'ok');
515
+ else toast('ERROR: ' + (res.error || '?'), 'err');
516
+ }
517
+ async function deleteBackup(mp) {
518
+ if (!confirm('Delete this backup?')) return;
519
+ var res = await post('/api/backup/delete', { metaPaths: [mp] });
520
+ if (res[0] && res[0].ok) { toast('BACKUP DELETED', 'ok'); loadBackups(true); }
521
+ else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
522
+ }
523
+
524
+ // ── BOOT ──────────────────────────────────────────────────────────────────────
525
+ init().catch(function(e) { console.error('[AM] fatal:', e); });
526
+
527
+ })();
528
+ `
@@ -0,0 +1,138 @@
1
+ import { PAGE_CSS } from './css.js'
2
+ import { PAGE_JS } from './js.js'
3
+
4
+ export function renderPage() {
5
+ return `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width,initial-scale=1">
10
+ <title>aisessions</title>
11
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link href="https://fonts.googleapis.com/css2?family=VT323&family=Press+Start+2P&display=swap" rel="stylesheet">
14
+ <style>${PAGE_CSS}</style>
15
+ </head>
16
+ <body>
17
+ <div id="shell">
18
+
19
+ <!-- HEADER -->
20
+ <header id="hdr">
21
+ <div id="logo">AI<br>SESSIONS</div>
22
+ <span id="hdr-sub">// AI SESSION MANAGER</span>
23
+ <div id="hdr-right">
24
+ <input id="search" type="search" placeholder="SEARCH SESSIONS...">
25
+ </div>
26
+ </header>
27
+
28
+ <div id="body">
29
+
30
+ <!-- SIDEBAR -->
31
+ <nav id="sidebar">
32
+ <div class="sb-section">
33
+ <div class="sb-label">NAVIGATE</div>
34
+ <button class="sb-item active" data-agent="all" data-action="filter-agent">
35
+ [*] ALL AGENTS
36
+ <span class="sb-count" id="sb-all-count">...</span>
37
+ </button>
38
+ </div>
39
+ <div class="sb-section">
40
+ <div class="sb-label">AGENTS</div>
41
+ <div id="sb-agents">
42
+ <div class="sb-item" style="color:var(--dim)">LOADING...</div>
43
+ </div>
44
+ </div>
45
+ <div class="sb-section">
46
+ <div class="sb-label">TOOLS (GLOBAL)</div>
47
+ <button class="sb-item" data-action="switch-tab-global" data-nav="usage">[$] USAGE</button>
48
+ <button class="sb-item" data-action="switch-tab-global" data-nav="trash">[T] TRASH</button>
49
+ <button class="sb-item" data-action="switch-tab-global" data-nav="backups">[B] BACKUPS</button>
50
+ </div>
51
+
52
+ <div id="sb-credit">
53
+ <div class="credit-kicker">CRAFTED WITH &lt;3</div>
54
+ <div class="credit-frame">
55
+ <div class="credit-handle">@s41r4j</div>
56
+ </div>
57
+ <div class="credit-links">
58
+ <a class="credit-link" href="https://github.com/s41r4j" target="_blank" rel="noopener">
59
+ <span class="credit-link-icon">GH</span>
60
+ <span>GitHub</span>
61
+ </a>
62
+ <a class="credit-link" href="https://x.com/s41r4j" target="_blank" rel="noopener">
63
+ <span class="credit-link-icon">X</span>
64
+ <span>X.com</span>
65
+ </a>
66
+ </div>
67
+ <div class="credit-pkg">aisessions v1.0</div>
68
+ </div>
69
+ </nav>
70
+
71
+ <!-- MAIN -->
72
+ <div id="main">
73
+
74
+ <!-- TAB BAR -->
75
+ <div id="tab-bar">
76
+ <div class="tab active" data-tab="sessions">[ SESSIONS ]</div>
77
+ <div class="tab" data-tab="usage">[ USAGE ]</div>
78
+ <div class="tab" data-tab="trash">[ TRASH ]</div>
79
+ <div class="tab" data-tab="backups">[ BACKUPS ]</div>
80
+ </div>
81
+
82
+ <!-- CONTENT -->
83
+ <div id="content">
84
+
85
+ <!-- ── Sessions ── -->
86
+ <div id="sessions-panel" class="panel active">
87
+ <div id="toolbar">
88
+ <button class="btn" id="btn-sel-all">SELECT ALL</button>
89
+ <button class="btn" id="btn-sel-none">DESELECT</button>
90
+ <button class="btn" id="btn-group">FLAT VIEW</button>
91
+ <span class="spacer"></span>
92
+ <span id="sel-info">...</span>
93
+ <span class="spacer"></span>
94
+ <button class="btn success" id="btn-backup">BACKUP SEL</button>
95
+ <button class="btn danger" id="btn-trash">TRASH SEL</button>
96
+ </div>
97
+ <div id="tbl-wrap">
98
+ <div class="tbl-hdr">
99
+ <span></span>
100
+ <span>AGENT</span>
101
+ <span>TITLE</span>
102
+ <span>MSGS</span>
103
+ <span>SIZE</span>
104
+ <span>DATE</span>
105
+ <span>ACTIONS</span>
106
+ </div>
107
+ <div id="tbl-body"></div>
108
+ </div>
109
+ <div id="pagination" style="display:none"></div>
110
+ </div>
111
+
112
+ <!-- ── Usage ── -->
113
+ <div id="usage-panel" class="panel"></div>
114
+
115
+ <!-- ── Trash ── -->
116
+ <div id="trash-panel" class="panel"></div>
117
+
118
+ <!-- ── Backups ── -->
119
+ <div id="backups-panel" class="panel"></div>
120
+
121
+ </div><!-- /content -->
122
+ </div><!-- /main -->
123
+ </div><!-- /body -->
124
+
125
+ <!-- FOOTER -->
126
+ <footer id="foot">
127
+ <span>AISESSIONS v1.0</span>
128
+ <span>&nbsp;|&nbsp;</span>
129
+ <span id="foot-info">LOADING...</span>
130
+ </footer>
131
+
132
+ </div><!-- /shell -->
133
+
134
+ <div id="toast"></div>
135
+ <script>${PAGE_JS}</script>
136
+ </body>
137
+ </html>`
138
+ }