@enigmax/dashboard 0.1.6 → 0.1.8
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/assets/index.html +379 -38
- package/assets/lib/chart.min.js +1 -7
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -130,6 +130,26 @@
|
|
|
130
130
|
border-radius: 7px; padding: 5px 14px; font-size: 12px; cursor: pointer; font-family: inherit;
|
|
131
131
|
}
|
|
132
132
|
.btn-danger:hover { border-color: var(--accent2); }
|
|
133
|
+
/* List settings (guard block/allow globs, custom secret patterns): chips + add input. A list row spans the full grid width. */
|
|
134
|
+
.set-row.list { grid-column: 1 / -1; align-items: stretch; flex-direction: column; }
|
|
135
|
+
.lchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
136
|
+
.lchip { display: inline-flex; align-items: center; gap: 4px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 3px 4px 3px 9px; font-size: 12px; color: var(--text); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
137
|
+
.lchip .lx { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 13px; line-height: 1; padding: 0 4px; font-family: inherit; }
|
|
138
|
+
.lchip .lx:hover { color: var(--accent2); }
|
|
139
|
+
.lnone { color: var(--muted); font-size: 12px; margin-top: 8px; font-style: italic; }
|
|
140
|
+
.ladd { display: flex; gap: 8px; margin-top: 8px; }
|
|
141
|
+
.linput { flex: 1; min-width: 0; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 7px; padding: 5px 10px; font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
142
|
+
.laddb { background: var(--surface2); color: var(--accent); border: 1px solid var(--border); border-radius: 7px; padding: 5px 14px; font-size: 12px; cursor: pointer; font-family: inherit; }
|
|
143
|
+
.laddb:hover { border-color: var(--accent); }
|
|
144
|
+
/* Claude-style usage window gauges (current session / weekly all / weekly Sonnet). */
|
|
145
|
+
.wgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 18px; }
|
|
146
|
+
.wcard { background: var(--surface2); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
|
|
147
|
+
.wtitle { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
148
|
+
.wreset { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
|
149
|
+
.wval { font-size: 22px; font-weight: 600; color: var(--accent); margin-top: 12px; }
|
|
150
|
+
.wval.muted { font-size: 14px; font-weight: 500; color: var(--muted); }
|
|
151
|
+
.wset { display: flex; gap: 6px; margin-top: 10px; }
|
|
152
|
+
.wlimit { flex: 1; min-width: 0; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 7px; padding: 4px 8px; font-size: 12px; }
|
|
133
153
|
.sys-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0 24px; }
|
|
134
154
|
.sys-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--surface2); font-size: 13px; }
|
|
135
155
|
.sys-item .k { color: var(--muted); }
|
|
@@ -148,6 +168,8 @@
|
|
|
148
168
|
</div>
|
|
149
169
|
<nav class="tabs">
|
|
150
170
|
<a href="#/" data-view="savings" class="tab active">Savings</a>
|
|
171
|
+
<a href="#/usage" data-view="usage" class="tab">Usage</a>
|
|
172
|
+
<a href="#/accounts" data-view="accounts" class="tab">Accounts</a>
|
|
151
173
|
<a href="#/skills" data-view="skills" class="tab">Skills</a>
|
|
152
174
|
<a href="#/settings" data-view="settings" class="tab">Settings</a>
|
|
153
175
|
</nav>
|
|
@@ -215,25 +237,6 @@
|
|
|
215
237
|
<div id="chartEmpty" class="empty" style="display:none">No compression activity recorded yet. Run <code>enigma compress <file></code> or enable the compression MCP (<code>enigma config compress on</code>).</div>
|
|
216
238
|
</div>
|
|
217
239
|
|
|
218
|
-
<div class="panel" id="usagePanel" style="display:none">
|
|
219
|
-
<h2 style="margin-bottom:4px">Real Tool Usage <small id="usageSub">Claude Code transcripts</small></h2>
|
|
220
|
-
<div class="sub" style="margin-bottom:14px">Measured token consumption and real prompt-cache savings, read-only from your own session logs. Deliberately not attributed to skills or output-style: a transcript has no counterfactual baseline, so any such number would be invented.</div>
|
|
221
|
-
<div class="grid" style="margin-bottom:16px">
|
|
222
|
-
<div class="card"><div class="label">Cache Saved</div><div id="uCacheMoney" class="value good">-</div><div class="foot">Est. from prompt caching</div></div>
|
|
223
|
-
<div class="card"><div class="label">Cache Reads</div><div id="uCacheRead" class="value accent">-</div><div class="foot">Tokens served from cache</div></div>
|
|
224
|
-
<div class="card"><div class="label">Input Tokens</div><div id="uInput" class="value">-</div><div class="foot">Consumed across sessions</div></div>
|
|
225
|
-
<div class="card"><div class="label">Output Tokens</div><div id="uOutput" class="value">-</div><div class="foot">Generated by the agent</div></div>
|
|
226
|
-
<div class="card"><div class="label">Sessions</div><div id="uSessions" class="value">-</div><div id="uSessionsFoot" class="foot">Transcripts scanned</div></div>
|
|
227
|
-
</div>
|
|
228
|
-
<h2 style="margin:0 0 10px">By Model</h2>
|
|
229
|
-
<div id="uByModel"></div>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
<div class="panel" id="usageHint" style="display:none">
|
|
233
|
-
<h2 style="margin-bottom:8px">Real Tool Usage <small>opt-in</small></h2>
|
|
234
|
-
<div class="sub">Show real token consumption and prompt-cache savings from your Claude Code sessions in <a href="#/settings">Settings</a> (Real tool-usage stats). enigma reads only your local session transcripts; nothing is sent anywhere.</div>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
240
|
<div class="panel">
|
|
238
241
|
<h2 style="margin-bottom:14px">Savings By Source <small>which app/CLI compressed</small></h2>
|
|
239
242
|
<div id="sources"></div>
|
|
@@ -276,6 +279,64 @@
|
|
|
276
279
|
</div>
|
|
277
280
|
</main>
|
|
278
281
|
|
|
282
|
+
<section id="view-usage" style="display:none">
|
|
283
|
+
<div class="page-head">
|
|
284
|
+
<h1>Claude Usage</h1>
|
|
285
|
+
<p>Measured token consumption and estimated cost, read-only from your own Claude Code session transcripts (<code>~/.claude/projects</code>). Cost is an estimate from a per-model price table; real spend is billed by Anthropic. Nothing is sent anywhere.</p>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="panel" id="usagePanel" style="display:none">
|
|
288
|
+
<h2 style="margin:0 0 4px">Usage limits <small>same windows Claude Code shows</small></h2>
|
|
289
|
+
<div class="sub" style="margin-bottom:12px">Reset times and tokens are measured from your transcripts; the <b>% used</b> needs your plan limit (set it on a card, or with <code>enigma config plan-weekly-limit <tokens></code>). The weekly reset day/time is <code>enigma config plan-weekly-reset "mon 11:00"</code>.</div>
|
|
290
|
+
<div id="uWindows"></div>
|
|
291
|
+
<div class="grid" style="margin-bottom:16px">
|
|
292
|
+
<div class="card"><div class="label">Est. Cost</div><div id="uCost" class="value accent">-</div><div class="foot">All sessions, per-model pricing</div></div>
|
|
293
|
+
<div class="card"><div class="label">Cache Saved</div><div id="uCacheMoney" class="value good">-</div><div class="foot">Est. from prompt caching</div></div>
|
|
294
|
+
<div class="card"><div class="label">Input Tokens</div><div id="uInput" class="value">-</div><div class="foot">Consumed across sessions</div></div>
|
|
295
|
+
<div class="card"><div class="label">Output Tokens</div><div id="uOutput" class="value">-</div><div class="foot">Generated by the agent</div></div>
|
|
296
|
+
<div class="card"><div class="label">Cache Reads</div><div id="uCacheRead" class="value accent">-</div><div class="foot">Tokens served from cache</div></div>
|
|
297
|
+
<div class="card"><div class="label">Sessions</div><div id="uSessions" class="value">-</div><div id="uSessionsFoot" class="foot">Transcripts scanned</div></div>
|
|
298
|
+
</div>
|
|
299
|
+
<div id="uBlockWrap" style="display:none;margin-bottom:6px">
|
|
300
|
+
<h2 style="margin-bottom:10px">Current 5-hour block <small id="uBlockState"></small></h2>
|
|
301
|
+
<div class="bar-row"><span class="name" style="width:120px">Window</span><div class="bar-track"><div id="uBlockBar" class="bar-fill" style="background:var(--accent)"></div></div><span id="uBlockTime" class="num">-</span></div>
|
|
302
|
+
<div class="stat-line" style="margin:12px 0 0">
|
|
303
|
+
<div>Tokens <b id="uBlockTokens">-</b></div>
|
|
304
|
+
<div>Cost <b id="uBlockCost">-</b></div>
|
|
305
|
+
<div>Burn rate <b id="uBlockBurn">-</b></div>
|
|
306
|
+
<div>Projected <b id="uBlockProj">-</b></div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
<h2 style="margin:18px 0 10px">By Model</h2>
|
|
310
|
+
<div id="uByModel"></div>
|
|
311
|
+
<h2 style="margin:18px 0 10px">By Project</h2>
|
|
312
|
+
<div id="uByProject"></div>
|
|
313
|
+
<h2 style="margin:18px 0 10px">Recent Sessions</h2>
|
|
314
|
+
<div class="tbl-wrap"><div id="uRecent"></div></div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="panel" id="usageHint" style="display:none">
|
|
317
|
+
<h2 style="margin-bottom:8px">Real tool usage <small>opt-in</small></h2>
|
|
318
|
+
<div class="sub">Turn on <b>Real tool-usage stats</b> in <a href="#/settings">Settings</a> to read your Claude Code sessions for token consumption and cost. enigma reads only your local session transcripts; nothing is sent anywhere.</div>
|
|
319
|
+
</div>
|
|
320
|
+
</section>
|
|
321
|
+
|
|
322
|
+
<section id="view-accounts" style="display:none">
|
|
323
|
+
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
324
|
+
<div style="flex:1">
|
|
325
|
+
<h1>Accounts & Profiles</h1>
|
|
326
|
+
<p>Manage per-tool logins and profiles (a profile pins one account per tool and drives launches), the same as the terminal UI. Logging an account IN happens in a terminal - the browser shows the command to run.</p>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="panel">
|
|
330
|
+
<div class="panel-head"><h2>Accounts</h2><button id="accAdd" type="button" class="toggle on">Add account</button></div>
|
|
331
|
+
<div id="accountsBody"><div class="empty" style="padding:24px 0">Loading accounts...</div></div>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="panel">
|
|
334
|
+
<div class="panel-head"><h2>Profiles</h2><button id="profAdd" type="button" class="toggle on">Add profile</button></div>
|
|
335
|
+
<div id="profilesBody"><div class="empty" style="padding:24px 0">Loading profiles...</div></div>
|
|
336
|
+
</div>
|
|
337
|
+
<div id="accountsNote" class="set-note"></div>
|
|
338
|
+
</section>
|
|
339
|
+
|
|
279
340
|
<section id="view-skills" style="display:none">
|
|
280
341
|
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
281
342
|
<div style="flex:1">
|
|
@@ -299,11 +360,17 @@
|
|
|
299
360
|
</section>
|
|
300
361
|
|
|
301
362
|
<section id="view-settings" style="display:none">
|
|
302
|
-
<div class="page-head">
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
363
|
+
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
364
|
+
<div style="flex:1">
|
|
365
|
+
<h1>Settings</h1>
|
|
366
|
+
<p>Everything you can configure with <code>enigma config</code> or the terminal UI, editable here. Changes apply at global scope and take effect immediately; toggles that change agent memory need an agent restart.</p>
|
|
367
|
+
</div>
|
|
368
|
+
<div style="display:flex;gap:8px;white-space:nowrap">
|
|
369
|
+
<button id="cfgExport" type="button" class="toggle">Export config</button>
|
|
370
|
+
<button id="cfgImport" type="button" class="toggle">Import config</button>
|
|
371
|
+
</div>
|
|
306
372
|
</div>
|
|
373
|
+
<div id="settingsNote" class="set-note"></div>
|
|
307
374
|
<div id="settingsBody"><div class="empty" style="padding:24px 0">Loading settings...</div></div>
|
|
308
375
|
</section>
|
|
309
376
|
|
|
@@ -458,28 +525,116 @@
|
|
|
458
525
|
}
|
|
459
526
|
|
|
460
527
|
// Real tool-usage panel (Claude Code transcripts). null = the opt-in flag is off.
|
|
528
|
+
function shortProject(p) { p = String(p || ""); return p.length > 44 ? "..." + p.slice(-41) : p; }
|
|
529
|
+
|
|
530
|
+
// --- Claude-style usage windows (current session / weekly all / weekly Sonnet) ---
|
|
531
|
+
function fmtResetIn(ms) {
|
|
532
|
+
const d = ms - Date.now();
|
|
533
|
+
if (d <= 0) return "now";
|
|
534
|
+
const h = Math.floor(d / 3600000), m = Math.round((d % 3600000) / 60000);
|
|
535
|
+
return h ? ("in " + h + " hr " + m + " min") : ("in " + m + " min");
|
|
536
|
+
}
|
|
537
|
+
function fmtResetAt(ms) {
|
|
538
|
+
const dt = new Date(ms);
|
|
539
|
+
return "Resets " + dt.toLocaleDateString([], { weekday: "short" }) + " " + dt.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
540
|
+
}
|
|
541
|
+
function windowCard(w, sub) {
|
|
542
|
+
const reset = w.kind === "session"
|
|
543
|
+
? (w.resetsAt ? "Resets " + fmtResetIn(w.resetsAt) : "No active session")
|
|
544
|
+
: (w.resetsAt ? fmtResetAt(w.resetsAt) : "");
|
|
545
|
+
let body;
|
|
546
|
+
if (sub === "weeklySonnet" && (w.used || 0) === 0) {
|
|
547
|
+
body = '<div class="wval muted">You haven\'t used Sonnet yet</div>';
|
|
548
|
+
} else if (w.pct != null) {
|
|
549
|
+
const p = Math.round(w.pct);
|
|
550
|
+
const col = w.pct >= 90 ? "var(--accent2)" : "var(--accent)";
|
|
551
|
+
body = '<div class="wval">' + p + '% used</div>'
|
|
552
|
+
+ '<div class="bar-track" style="margin-top:8px"><div class="bar-fill" style="width:' + Math.min(100, w.pct) + '%;background:' + col + '"></div></div>';
|
|
553
|
+
} else {
|
|
554
|
+
body = '<div class="wval">' + fmt(w.used || 0) + ' tok</div>'
|
|
555
|
+
+ '<div class="wset"><input type="number" min="0" class="wlimit" data-plan="' + sub + '" placeholder="set limit (tokens)">'
|
|
556
|
+
+ '<button type="button" class="laddb" data-plan="' + sub + '">Set</button></div>';
|
|
557
|
+
}
|
|
558
|
+
return '<div class="wcard"><div class="wtitle">' + esc(w.label) + '</div><div class="wreset">' + esc(reset) + '</div>' + body + '</div>';
|
|
559
|
+
}
|
|
560
|
+
function renderWindows(u) {
|
|
561
|
+
const w = u && u.windows;
|
|
562
|
+
if (!w) { $("uWindows").innerHTML = ""; return; }
|
|
563
|
+
$("uWindows").innerHTML = '<div class="wgrid">'
|
|
564
|
+
+ windowCard(w.session, "session")
|
|
565
|
+
+ windowCard(w.weeklyAll, "weekly")
|
|
566
|
+
+ windowCard(w.weeklySonnet, "weeklySonnet")
|
|
567
|
+
+ "</div>";
|
|
568
|
+
}
|
|
569
|
+
async function planSet(key, value) {
|
|
570
|
+
try { await fetch("/api/plan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: Number(value) }) }); refreshAll(true); }
|
|
571
|
+
catch { /* leave as-is */ }
|
|
572
|
+
}
|
|
573
|
+
function wireUsage() {
|
|
574
|
+
const el = $("uWindows");
|
|
575
|
+
el.addEventListener("click", (e) => {
|
|
576
|
+
const b = e.target.closest("button[data-plan]"); if (!b) return;
|
|
577
|
+
const inp = el.querySelector('input.wlimit[data-plan="' + b.dataset.plan + '"]');
|
|
578
|
+
if (inp && inp.value) planSet(b.dataset.plan, inp.value);
|
|
579
|
+
});
|
|
580
|
+
el.addEventListener("keydown", (e) => {
|
|
581
|
+
if (e.key !== "Enter") return;
|
|
582
|
+
const inp = e.target.closest("input.wlimit");
|
|
583
|
+
if (inp && inp.value) planSet(inp.dataset.plan, inp.value);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// One token/cost breakdown table (By Model / By Project), sorted by cost then tokens.
|
|
588
|
+
function renderUsageTable(el, map, pending, label) {
|
|
589
|
+
const entries = Object.entries(map || {}).sort((a, b) => ((b[1].cost || 0) - (a[1].cost || 0)) || ((b[1].input + b[1].output) - (a[1].input + a[1].output)));
|
|
590
|
+
if (!entries.length) { el.innerHTML = '<div class="empty" style="padding:16px 0">' + (pending ? "Scanning..." : "No usage recorded yet.") + "</div>"; return; }
|
|
591
|
+
el.innerHTML = '<table class="tbl"><thead><tr><th>' + label + '</th><th class="r">Input</th><th class="r">Output</th>'
|
|
592
|
+
+ '<th class="r">Cache reads</th><th class="r">Msgs</th><th class="r">Cost</th></tr></thead><tbody>'
|
|
593
|
+
+ entries.map(([k, v]) => '<tr><td>' + esc(label === "Project" ? shortProject(k) : k) + '</td><td class="r">' + fmt(v.input) + '</td><td class="r">'
|
|
594
|
+
+ fmt(v.output) + '</td><td class="r good">' + fmt(v.cacheRead) + '</td><td class="r">' + fmt(v.messages) + '</td><td class="r">' + fmtUsd(v.cost || 0) + "</td></tr>").join("")
|
|
595
|
+
+ "</tbody></table>";
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function renderRecentSessions(el, rows, pending) {
|
|
599
|
+
rows = rows || [];
|
|
600
|
+
if (!rows.length) { el.innerHTML = '<div class="empty" style="padding:16px 0">' + (pending ? "Scanning..." : "No sessions yet.") + "</div>"; return; }
|
|
601
|
+
el.innerHTML = '<table class="tbl"><thead><tr><th>Last active</th><th>Project</th><th>Model</th><th class="r">Input</th><th class="r">Output</th><th class="r">Cost</th></tr></thead><tbody>'
|
|
602
|
+
+ rows.map((s) => '<tr><td>' + (s.lastActive ? new Date(s.lastActive).toLocaleString() : "-") + '</td><td>' + esc(shortProject(s.project)) + '</td><td>' + esc(s.model || "-")
|
|
603
|
+
+ '</td><td class="r">' + fmt(s.input) + '</td><td class="r">' + fmt(s.output) + '</td><td class="r">' + fmtUsd(s.cost || 0) + "</td></tr>").join("")
|
|
604
|
+
+ "</tbody></table>";
|
|
605
|
+
}
|
|
606
|
+
|
|
461
607
|
function renderUsage(u) {
|
|
462
608
|
const panel = $("usagePanel"), hint = $("usageHint");
|
|
463
609
|
if (!u) { panel.style.display = "none"; hint.style.display = "block"; return; }
|
|
464
610
|
hint.style.display = "none";
|
|
465
611
|
panel.style.display = "block";
|
|
612
|
+
renderWindows(u);
|
|
613
|
+
$("uCost").textContent = fmtUsd(u.cost || 0);
|
|
466
614
|
$("uCacheMoney").textContent = fmtUsd(cacheMoney(u.cacheRead || 0));
|
|
467
615
|
$("uCacheRead").textContent = fmt(u.cacheRead || 0);
|
|
468
616
|
$("uInput").textContent = fmt(u.input || 0);
|
|
469
617
|
$("uOutput").textContent = fmt(u.output || 0);
|
|
470
618
|
$("uSessions").textContent = fmt(u.sessions || 0);
|
|
471
619
|
$("uSessionsFoot").textContent = u.pending ? "scanning sessions..." : fmt(u.scannedFiles || 0) + " files scanned";
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
620
|
+
// Current 5-hour block (computed locally from transcript timestamps).
|
|
621
|
+
const b = u.block, bw = $("uBlockWrap");
|
|
622
|
+
if (b && b.startedAt) {
|
|
623
|
+
bw.style.display = "block";
|
|
624
|
+
const now = Date.now(), total = 5 * 3600 * 1000;
|
|
625
|
+
const pct = Math.max(0, Math.min(100, Math.round((now - b.startedAt) / total * 100)));
|
|
626
|
+
$("uBlockBar").style.width = pct + "%";
|
|
627
|
+
$("uBlockState").textContent = b.active ? "active" : "window elapsed";
|
|
628
|
+
const remMin = Math.max(0, Math.round((b.endsAt - now) / 60000));
|
|
629
|
+
$("uBlockTime").textContent = b.active ? (remMin + " min left") : "ended";
|
|
630
|
+
$("uBlockTokens").textContent = fmt(b.tokens || 0);
|
|
631
|
+
$("uBlockCost").textContent = fmtUsd(b.cost || 0);
|
|
632
|
+
$("uBlockBurn").textContent = fmt(Math.round(b.burnRatePerMin || 0)) + " tok/min";
|
|
633
|
+
$("uBlockProj").textContent = fmt(b.projectedTokens || 0) + " · " + fmtUsd(b.projectedCost || 0);
|
|
634
|
+
} else { bw.style.display = "none"; }
|
|
635
|
+
renderUsageTable($("uByModel"), u.byModel, u.pending, "Model");
|
|
636
|
+
renderUsageTable($("uByProject"), u.byProject, u.pending, "Project");
|
|
637
|
+
renderRecentSessions($("uRecent"), u.recentSessions, u.pending);
|
|
483
638
|
}
|
|
484
639
|
|
|
485
640
|
function renderCache(c) {
|
|
@@ -681,6 +836,19 @@
|
|
|
681
836
|
}
|
|
682
837
|
|
|
683
838
|
function settingRow(s) {
|
|
839
|
+
if (s.kind === "list") {
|
|
840
|
+
const items = s.items || [];
|
|
841
|
+
const chips = items.length
|
|
842
|
+
? '<div class="lchips">' + items.map((it) => '<span class="lchip">' + esc(it)
|
|
843
|
+
+ '<button type="button" class="lx" title="remove" data-skey="' + esc(s.key) + '" data-item="' + esc(it) + '">x</button></span>').join("") + "</div>"
|
|
844
|
+
: '<div class="lnone">none - using the built-in defaults</div>';
|
|
845
|
+
const adder = '<div class="ladd"><input type="text" class="linput" data-skey="' + esc(s.key)
|
|
846
|
+
+ '" placeholder="' + esc(s.itemHint || "add an entry...") + '">'
|
|
847
|
+
+ '<button type="button" class="laddb" data-skey="' + esc(s.key) + '">Add</button></div>';
|
|
848
|
+
return '<div class="set-row list"><div class="set-meta"><div class="set-name">' + labelWithBadges(s.label) + "</div>"
|
|
849
|
+
+ '<div class="set-hint">' + esc(s.hint) + (s.globalOnly ? " · global" : "") + "</div>"
|
|
850
|
+
+ chips + adder + "</div></div>";
|
|
851
|
+
}
|
|
684
852
|
const ctl = s.choices
|
|
685
853
|
? '<select class="choice" data-skey="' + esc(s.key) + '">'
|
|
686
854
|
+ s.choices.map((c) => '<option value="' + esc(c) + '"' + (c === s.choice ? " selected" : "") + ">" + esc(c) + "</option>").join("")
|
|
@@ -715,16 +883,42 @@
|
|
|
715
883
|
} catch { $("settingsNote").textContent = "Could not reach the server to change " + key + "."; }
|
|
716
884
|
}
|
|
717
885
|
|
|
886
|
+
// List settings: add/remove one entry, then reload so the chips reflect disk truth.
|
|
887
|
+
async function postListOp(key, op, item) {
|
|
888
|
+
item = (item || "").trim();
|
|
889
|
+
if (!item) return;
|
|
890
|
+
$("settingsNote").textContent = (op === "add" ? "Adding to " : "Removing from ") + key + "...";
|
|
891
|
+
try {
|
|
892
|
+
const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: { op, item } }) });
|
|
893
|
+
const out = await res.json();
|
|
894
|
+
if (!out.ok) { $("settingsNote").textContent = "Could not change " + key + ": " + (out.error || "error"); return; }
|
|
895
|
+
$("settingsNote").textContent = "Saved " + key + ".";
|
|
896
|
+
await loadSettings();
|
|
897
|
+
} catch { $("settingsNote").textContent = "Could not reach the server to change " + key + "."; }
|
|
898
|
+
}
|
|
899
|
+
|
|
718
900
|
function wireSettings() {
|
|
719
901
|
const el = $("settingsBody");
|
|
720
902
|
el.addEventListener("click", (e) => {
|
|
721
903
|
const b = e.target.closest("button.toggle");
|
|
722
|
-
if (b) postSetting(b.dataset.skey, b.dataset.on !== "1", b);
|
|
904
|
+
if (b) { postSetting(b.dataset.skey, b.dataset.on !== "1", b); return; }
|
|
905
|
+
const rm = e.target.closest("button.lx");
|
|
906
|
+
if (rm) { postListOp(rm.dataset.skey, "remove", rm.dataset.item); return; }
|
|
907
|
+
const add = e.target.closest("button.laddb");
|
|
908
|
+
if (add) {
|
|
909
|
+
const inp = el.querySelector('input.linput[data-skey="' + (window.CSS && CSS.escape ? CSS.escape(add.dataset.skey) : add.dataset.skey) + '"]');
|
|
910
|
+
if (inp) { postListOp(add.dataset.skey, "add", inp.value); inp.value = ""; }
|
|
911
|
+
}
|
|
723
912
|
});
|
|
724
913
|
el.addEventListener("change", (e) => {
|
|
725
914
|
const sel = e.target.closest("select.choice");
|
|
726
915
|
if (sel) postSetting(sel.dataset.skey, sel.value, sel);
|
|
727
916
|
});
|
|
917
|
+
el.addEventListener("keydown", (e) => {
|
|
918
|
+
if (e.key !== "Enter") return;
|
|
919
|
+
const inp = e.target.closest("input.linput");
|
|
920
|
+
if (inp) { e.preventDefault(); postListOp(inp.dataset.skey, "add", inp.value); inp.value = ""; }
|
|
921
|
+
});
|
|
728
922
|
}
|
|
729
923
|
|
|
730
924
|
let settingsLoaded = false;
|
|
@@ -754,12 +948,30 @@
|
|
|
754
948
|
? item("Proxy measured · real", '<span class="tag">' + fmt(ps.calls) + " calls</span><span class=\"tag\">"
|
|
755
949
|
+ fmt(ps.input) + " in</span><span class=\"tag\">" + fmt(ps.output) + " out</span><span class=\"tag\">" + fmt(ps.cacheRead) + " cache</span>")
|
|
756
950
|
: "";
|
|
951
|
+
const proxyLast = (ps.lastRequestAt || 0) > 0
|
|
952
|
+
? item("Proxy last request · real", '<span class="tag">' + esc(fmtDuration((Date.now() - ps.lastRequestAt) / 1000) + " ago")
|
|
953
|
+
+ '</span><span class="tag">' + esc(new Date(ps.lastRequestAt).toLocaleString()) + "</span>")
|
|
954
|
+
: "";
|
|
955
|
+
// Prompt secret guard: configured state + what it has actually blocked (measured).
|
|
956
|
+
const guardState = s.promptSecretGuard
|
|
957
|
+
? '<span class="pill on">on</span><span class="tag">' + esc(s.promptSecretMode || "redact") + "</span>"
|
|
958
|
+
: pill("off", "off");
|
|
959
|
+
const promptGuard = item("Prompt secret guard · experimental", guardState);
|
|
960
|
+
const blockedTotal = (ps.redacted || 0) + (ps.rejected || 0);
|
|
961
|
+
const promptBlocked = blockedTotal > 0
|
|
962
|
+
? item("Prompt secrets blocked · real", '<span class="tag">' + fmt(ps.redacted || 0) + " redacted</span><span class=\"tag\">"
|
|
963
|
+
+ fmt(ps.rejected || 0) + " rejected</span>"
|
|
964
|
+
+ ((ps.lastBlockedAt || 0) > 0 ? '<span class="tag">' + esc(fmtDuration((Date.now() - ps.lastBlockedAt) / 1000) + " ago") + "</span>" : ""))
|
|
965
|
+
: "";
|
|
757
966
|
const guard = (sec.guardProtects || []).map((p) => '<span class="tag">' + esc(p) + "</span>").join("");
|
|
758
967
|
el.innerHTML =
|
|
759
968
|
item("Context compression (MCP) · measured", boolPill(s.compress))
|
|
760
969
|
+ item("Real tool-usage stats · measured", boolPill(s.usageStats))
|
|
761
|
-
+ item("Claude Code
|
|
970
|
+
+ item("Claude Code proxy · experimental", boolPill(s.proxy))
|
|
762
971
|
+ proxyMeasured
|
|
972
|
+
+ proxyLast
|
|
973
|
+
+ promptGuard
|
|
974
|
+
+ promptBlocked
|
|
763
975
|
+ item("Token-efficient output", levelPill(s.outputStyle))
|
|
764
976
|
+ item("Minimal code", levelPill(s.minimalCode))
|
|
765
977
|
+ item("Parallel sub-agents", boolPill(s.parallelSubagents))
|
|
@@ -892,16 +1104,141 @@
|
|
|
892
1104
|
} catch { $("skillsBody").innerHTML = '<div class="empty" style="padding:24px 0">Skills unavailable.</div>'; }
|
|
893
1105
|
}
|
|
894
1106
|
|
|
895
|
-
// ---
|
|
1107
|
+
// --- accounts & profiles subpage (mirrors the TUI hub over /api/accounts) ---
|
|
1108
|
+
let accountsLoaded = false;
|
|
1109
|
+
let accTools = [];
|
|
1110
|
+
async function loadAccounts() {
|
|
1111
|
+
try { const r = await fetch("/api/accounts", { cache: "no-store" }); renderAccounts(await r.json()); }
|
|
1112
|
+
catch { $("accountsBody").innerHTML = '<div class="empty" style="padding:24px 0">Accounts unavailable.</div>'; }
|
|
1113
|
+
}
|
|
1114
|
+
function accountRow(a) {
|
|
1115
|
+
const badges = (a.active ? '<span class="tag meas">active</span>' : "")
|
|
1116
|
+
+ (a.loggedIn ? '<span class="tag">' + esc(a.email || "logged in") + "</span>" : '<span class="tag warn">not logged in</span>');
|
|
1117
|
+
const btn = (act, label, cls) => '<button type="button" class="' + (cls || "toggle") + '" data-acc="' + act + '" data-tool="' + esc(a.tool) + '" data-name="' + esc(a.name) + '">' + label + "</button>";
|
|
1118
|
+
const acts = '<div class="set-ctl" style="display:flex;gap:6px;flex-wrap:wrap">'
|
|
1119
|
+
+ (a.active ? "" : btn("activate", "Use"))
|
|
1120
|
+
+ (a.loggedIn ? "" : btn("login", "Log in..."))
|
|
1121
|
+
+ (a.removable ? btn("rename", "Rename") + btn("remove", "Remove", "btn-danger") : "")
|
|
1122
|
+
+ "</div>";
|
|
1123
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(a.name) + ' <span class="tag ext">' + esc(a.toolLabel) + "</span> " + badges
|
|
1124
|
+
+ '</div><div class="set-hint">' + esc(a.dir) + "</div></div>" + acts + "</div>";
|
|
1125
|
+
}
|
|
1126
|
+
function profileRow(p, tools, accounts) {
|
|
1127
|
+
const maps = tools.map((t) => {
|
|
1128
|
+
const cur = p.accounts[t.name] || "";
|
|
1129
|
+
const opts = ['<option value="">(none)</option>'].concat(accounts.filter((a) => a.tool === t.name)
|
|
1130
|
+
.map((a) => '<option value="' + esc(a.name) + '"' + (a.name === cur ? " selected" : "") + ">" + esc(a.name) + "</option>")).join("");
|
|
1131
|
+
return '<label style="font-size:12px;color:var(--muted);display:inline-flex;align-items:center;gap:6px;margin:0 12px 6px 0">' + esc(t.label)
|
|
1132
|
+
+ ' <select class="choice" data-prof-tool="' + esc(t.name) + '" data-prof="' + esc(p.name) + '">' + opts + "</select></label>";
|
|
1133
|
+
}).join("");
|
|
1134
|
+
const acts = '<div class="set-ctl" style="display:flex;gap:6px">'
|
|
1135
|
+
+ (p.active ? "" : '<button type="button" class="toggle" data-prof-act="activate" data-name="' + esc(p.name) + '">Use</button>')
|
|
1136
|
+
+ '<button type="button" class="toggle" data-prof-act="rename" data-name="' + esc(p.name) + '">Rename</button>'
|
|
1137
|
+
+ '<button type="button" class="btn-danger" data-prof-act="remove" data-name="' + esc(p.name) + '">Remove</button></div>';
|
|
1138
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(p.name) + (p.active ? ' <span class="tag meas">active</span>' : "")
|
|
1139
|
+
+ '</div><div class="set-hint" style="margin-top:6px">' + maps + "</div></div>" + acts + "</div>";
|
|
1140
|
+
}
|
|
1141
|
+
function renderAccounts(d) {
|
|
1142
|
+
d = d || {};
|
|
1143
|
+
const accounts = d.accounts || [], profiles = d.profiles || [], tools = d.tools || [];
|
|
1144
|
+
accTools = tools;
|
|
1145
|
+
$("accountsBody").innerHTML = accounts.length ? accounts.map(accountRow).join("") : '<div class="empty" style="padding:20px 0">No accounts.</div>';
|
|
1146
|
+
$("profilesBody").innerHTML = profiles.length ? profiles.map((p) => profileRow(p, tools, accounts)).join("")
|
|
1147
|
+
: '<div class="empty" style="padding:20px 0">No profiles yet. Add one to pin an account per tool.</div>';
|
|
1148
|
+
}
|
|
1149
|
+
async function accAction(op, payload) {
|
|
1150
|
+
$("accountsNote").textContent = "Working...";
|
|
1151
|
+
try {
|
|
1152
|
+
const r = await fetch("/api/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(Object.assign({ op }, payload)) });
|
|
1153
|
+
const out = await r.json();
|
|
1154
|
+
$("accountsNote").textContent = out.ok ? "Saved." : ("Error: " + (out.error || "failed"));
|
|
1155
|
+
if (out.data) renderAccounts(out.data);
|
|
1156
|
+
} catch { $("accountsNote").textContent = "Could not reach the server."; }
|
|
1157
|
+
}
|
|
1158
|
+
function wireAccounts() {
|
|
1159
|
+
$("accAdd").addEventListener("click", () => {
|
|
1160
|
+
const tool = accTools.length === 1 ? accTools[0].name : prompt("Which tool? (" + accTools.map((t) => t.name).join(" / ") + ")", accTools[0] ? accTools[0].name : "");
|
|
1161
|
+
if (!tool) return;
|
|
1162
|
+
const name = prompt("New account name (e.g. work):", "");
|
|
1163
|
+
if (name) accAction("account.add", { tool, name });
|
|
1164
|
+
});
|
|
1165
|
+
$("profAdd").addEventListener("click", () => { const name = prompt("New profile name:", ""); if (name) accAction("profile.add", { name }); });
|
|
1166
|
+
$("accountsBody").addEventListener("click", (e) => {
|
|
1167
|
+
const b = e.target.closest("button[data-acc]"); if (!b) return;
|
|
1168
|
+
const op = b.dataset.acc, tool = b.dataset.tool, name = b.dataset.name;
|
|
1169
|
+
if (op === "activate") accAction("account.activate", { tool, name });
|
|
1170
|
+
else if (op === "remove") { if (confirm("Remove account '" + name + "' and delete its local config dir?")) accAction("account.remove", { tool, name }); }
|
|
1171
|
+
else if (op === "rename") { const nn = prompt("Rename '" + name + "' to:", name); if (nn && nn !== name) accAction("account.rename", { tool, name, newName: nn }); }
|
|
1172
|
+
else if (op === "login") { $("accountsNote").textContent = "To log in, run this in a terminal: enigma " + tool + " " + name; }
|
|
1173
|
+
});
|
|
1174
|
+
$("profilesBody").addEventListener("click", (e) => {
|
|
1175
|
+
const b = e.target.closest("button[data-prof-act]"); if (!b) return;
|
|
1176
|
+
const op = b.dataset.profAct, name = b.dataset.name;
|
|
1177
|
+
if (op === "activate") accAction("profile.activate", { name });
|
|
1178
|
+
else if (op === "remove") { if (confirm("Remove profile '" + name + "'? (its accounts are kept)")) accAction("profile.remove", { name }); }
|
|
1179
|
+
else if (op === "rename") { const nn = prompt("Rename profile '" + name + "' to:", name); if (nn && nn !== name) accAction("profile.rename", { name, newName: nn }); }
|
|
1180
|
+
});
|
|
1181
|
+
$("profilesBody").addEventListener("change", (e) => {
|
|
1182
|
+
const sel = e.target.closest("select[data-prof-tool]"); if (!sel) return;
|
|
1183
|
+
accAction("profile.setAccount", { profile: sel.dataset.prof, tool: sel.dataset.profTool, account: sel.value || null });
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// --- config import / export (the browser file picker is what "asks where") ---
|
|
1188
|
+
async function exportConfig() {
|
|
1189
|
+
$("settingsNote").textContent = "Exporting...";
|
|
1190
|
+
let text;
|
|
1191
|
+
try { text = await (await fetch("/api/config-export", { cache: "no-store" })).text(); }
|
|
1192
|
+
catch { $("settingsNote").textContent = "Export failed."; return; }
|
|
1193
|
+
const name = "enigma-config-" + new Date().toISOString().slice(0, 10) + ".json";
|
|
1194
|
+
if (window.showSaveFilePicker) {
|
|
1195
|
+
try {
|
|
1196
|
+
const h = await window.showSaveFilePicker({ suggestedName: name, types: [{ description: "JSON", accept: { "application/json": [".json"] } }] });
|
|
1197
|
+
const w = await h.createWritable(); await w.write(text); await w.close();
|
|
1198
|
+
$("settingsNote").textContent = "Exported (no secrets included)."; return;
|
|
1199
|
+
} catch (e) { if (e && e.name === "AbortError") { $("settingsNote").textContent = "Export cancelled."; return; } }
|
|
1200
|
+
}
|
|
1201
|
+
const url = URL.createObjectURL(new Blob([text], { type: "application/json" }));
|
|
1202
|
+
const a = document.createElement("a"); a.href = url; a.download = name; a.click(); URL.revokeObjectURL(url);
|
|
1203
|
+
$("settingsNote").textContent = "Exported to your downloads (no secrets included).";
|
|
1204
|
+
}
|
|
1205
|
+
async function importConfig() {
|
|
1206
|
+
let text = null;
|
|
1207
|
+
if (window.showOpenFilePicker) {
|
|
1208
|
+
try { const [h] = await window.showOpenFilePicker({ types: [{ description: "JSON", accept: { "application/json": [".json"] } }] }); text = await (await h.getFile()).text(); }
|
|
1209
|
+
catch (e) { if (e && e.name === "AbortError") return; }
|
|
1210
|
+
}
|
|
1211
|
+
if (text === null) {
|
|
1212
|
+
text = await new Promise((res) => {
|
|
1213
|
+
const inp = document.createElement("input"); inp.type = "file"; inp.accept = "application/json,.json";
|
|
1214
|
+
inp.onchange = () => { const f = inp.files[0]; if (!f) { res(null); return; } const rd = new FileReader(); rd.onload = () => res(String(rd.result)); rd.readAsText(f); };
|
|
1215
|
+
inp.click();
|
|
1216
|
+
});
|
|
1217
|
+
if (text === null) return;
|
|
1218
|
+
}
|
|
1219
|
+
let body; try { body = JSON.parse(text); } catch { $("settingsNote").textContent = "That file is not valid JSON."; return; }
|
|
1220
|
+
$("settingsNote").textContent = "Importing...";
|
|
1221
|
+
try {
|
|
1222
|
+
const out = await (await fetch("/api/config-import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })).json();
|
|
1223
|
+
if (!out.ok) { $("settingsNote").textContent = "Import failed: " + (out.error || "error"); return; }
|
|
1224
|
+
$("settingsNote").textContent = "Imported: " + (out.applied || []).join(", ") + ((out.skipped && out.skipped.length) ? (" · skipped: " + out.skipped.join(", ")) : "");
|
|
1225
|
+
settingsLoaded = false; loadSettings(); accountsLoaded = false;
|
|
1226
|
+
} catch { $("settingsNote").textContent = "Could not reach the server to import."; }
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// --- hash routing between the subpages ---
|
|
896
1230
|
function route() {
|
|
897
1231
|
const h = (location.hash || "").replace(/^#\/?/, "");
|
|
898
|
-
const v =
|
|
1232
|
+
const v = ["usage", "accounts", "settings", "skills"].includes(h) ? h : "savings";
|
|
899
1233
|
$("view-savings").style.display = v === "savings" ? "" : "none";
|
|
1234
|
+
$("view-usage").style.display = v === "usage" ? "" : "none";
|
|
1235
|
+
$("view-accounts").style.display = v === "accounts" ? "" : "none";
|
|
900
1236
|
$("view-skills").style.display = v === "skills" ? "" : "none";
|
|
901
1237
|
$("view-settings").style.display = v === "settings" ? "" : "none";
|
|
902
1238
|
document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.view === v));
|
|
903
1239
|
if (v === "settings" && !settingsLoaded) { settingsLoaded = true; loadSettings(); }
|
|
904
1240
|
if (v === "skills" && !skillsLoaded) { skillsLoaded = true; loadSkills(); }
|
|
1241
|
+
if (v === "accounts" && !accountsLoaded) { accountsLoaded = true; loadAccounts(); }
|
|
905
1242
|
// The chart was sized while its view may have been hidden; nudge it on return.
|
|
906
1243
|
if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
|
|
907
1244
|
}
|
|
@@ -912,6 +1249,10 @@
|
|
|
912
1249
|
document.addEventListener("visibilitychange", () => { if (!document.hidden) refreshAll(true); });
|
|
913
1250
|
wireSettings();
|
|
914
1251
|
wireSkills();
|
|
1252
|
+
wireAccounts();
|
|
1253
|
+
wireUsage();
|
|
1254
|
+
$("cfgExport").addEventListener("click", exportConfig);
|
|
1255
|
+
$("cfgImport").addEventListener("click", importConfig);
|
|
915
1256
|
$("updateBtn").addEventListener("click", runUpdate);
|
|
916
1257
|
$("refreshBtn").addEventListener("click", () => refreshAll(true));
|
|
917
1258
|
window.addEventListener("hashchange", route);
|