@enigmax/dashboard 0.1.5 → 0.1.7
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 +326 -43
- package/assets/lib/chart.min.js +1 -7
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -118,6 +118,8 @@
|
|
|
118
118
|
}
|
|
119
119
|
.tag.ext { color: var(--accent2); }
|
|
120
120
|
.tag.warn { color: var(--accent); border-color: var(--accent); }
|
|
121
|
+
.tag.exp { color: var(--accent); border-color: var(--accent); background: rgba(224,164,88,0.10); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
|
122
|
+
.tag.meas { color: var(--good); border-color: var(--good); background: rgba(111,207,151,0.10); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
|
121
123
|
.editor {
|
|
122
124
|
width: 100%; min-height: 360px; resize: vertical; background: var(--surface2); color: var(--text);
|
|
123
125
|
border: 1px solid var(--border); border-radius: 8px; padding: 12px;
|
|
@@ -128,6 +130,17 @@
|
|
|
128
130
|
border-radius: 7px; padding: 5px 14px; font-size: 12px; cursor: pointer; font-family: inherit;
|
|
129
131
|
}
|
|
130
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); }
|
|
131
144
|
.sys-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0 24px; }
|
|
132
145
|
.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; }
|
|
133
146
|
.sys-item .k { color: var(--muted); }
|
|
@@ -146,6 +159,8 @@
|
|
|
146
159
|
</div>
|
|
147
160
|
<nav class="tabs">
|
|
148
161
|
<a href="#/" data-view="savings" class="tab active">Savings</a>
|
|
162
|
+
<a href="#/usage" data-view="usage" class="tab">Usage</a>
|
|
163
|
+
<a href="#/accounts" data-view="accounts" class="tab">Accounts</a>
|
|
149
164
|
<a href="#/skills" data-view="skills" class="tab">Skills</a>
|
|
150
165
|
<a href="#/settings" data-view="settings" class="tab">Settings</a>
|
|
151
166
|
</nav>
|
|
@@ -213,25 +228,6 @@
|
|
|
213
228
|
<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>
|
|
214
229
|
</div>
|
|
215
230
|
|
|
216
|
-
<div class="panel" id="usagePanel" style="display:none">
|
|
217
|
-
<h2 style="margin-bottom:4px">Real Tool Usage <small id="usageSub">Claude Code transcripts</small></h2>
|
|
218
|
-
<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>
|
|
219
|
-
<div class="grid" style="margin-bottom:16px">
|
|
220
|
-
<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>
|
|
221
|
-
<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>
|
|
222
|
-
<div class="card"><div class="label">Input Tokens</div><div id="uInput" class="value">-</div><div class="foot">Consumed across sessions</div></div>
|
|
223
|
-
<div class="card"><div class="label">Output Tokens</div><div id="uOutput" class="value">-</div><div class="foot">Generated by the agent</div></div>
|
|
224
|
-
<div class="card"><div class="label">Sessions</div><div id="uSessions" class="value">-</div><div id="uSessionsFoot" class="foot">Transcripts scanned</div></div>
|
|
225
|
-
</div>
|
|
226
|
-
<h2 style="margin:0 0 10px">By Model</h2>
|
|
227
|
-
<div id="uByModel"></div>
|
|
228
|
-
</div>
|
|
229
|
-
|
|
230
|
-
<div class="panel" id="usageHint" style="display:none">
|
|
231
|
-
<h2 style="margin-bottom:8px">Real Tool Usage <small>opt-in</small></h2>
|
|
232
|
-
<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>
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
231
|
<div class="panel">
|
|
236
232
|
<h2 style="margin-bottom:14px">Savings By Source <small>which app/CLI compressed</small></h2>
|
|
237
233
|
<div id="sources"></div>
|
|
@@ -274,6 +270,61 @@
|
|
|
274
270
|
</div>
|
|
275
271
|
</main>
|
|
276
272
|
|
|
273
|
+
<section id="view-usage" style="display:none">
|
|
274
|
+
<div class="page-head">
|
|
275
|
+
<h1>Claude Usage</h1>
|
|
276
|
+
<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>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="panel" id="usagePanel" style="display:none">
|
|
279
|
+
<div class="grid" style="margin-bottom:16px">
|
|
280
|
+
<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>
|
|
281
|
+
<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>
|
|
282
|
+
<div class="card"><div class="label">Input Tokens</div><div id="uInput" class="value">-</div><div class="foot">Consumed across sessions</div></div>
|
|
283
|
+
<div class="card"><div class="label">Output Tokens</div><div id="uOutput" class="value">-</div><div class="foot">Generated by the agent</div></div>
|
|
284
|
+
<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>
|
|
285
|
+
<div class="card"><div class="label">Sessions</div><div id="uSessions" class="value">-</div><div id="uSessionsFoot" class="foot">Transcripts scanned</div></div>
|
|
286
|
+
</div>
|
|
287
|
+
<div id="uBlockWrap" style="display:none;margin-bottom:6px">
|
|
288
|
+
<h2 style="margin-bottom:10px">Current 5-hour block <small id="uBlockState"></small></h2>
|
|
289
|
+
<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>
|
|
290
|
+
<div class="stat-line" style="margin:12px 0 0">
|
|
291
|
+
<div>Tokens <b id="uBlockTokens">-</b></div>
|
|
292
|
+
<div>Cost <b id="uBlockCost">-</b></div>
|
|
293
|
+
<div>Burn rate <b id="uBlockBurn">-</b></div>
|
|
294
|
+
<div>Projected <b id="uBlockProj">-</b></div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
<h2 style="margin:18px 0 10px">By Model</h2>
|
|
298
|
+
<div id="uByModel"></div>
|
|
299
|
+
<h2 style="margin:18px 0 10px">By Project</h2>
|
|
300
|
+
<div id="uByProject"></div>
|
|
301
|
+
<h2 style="margin:18px 0 10px">Recent Sessions</h2>
|
|
302
|
+
<div class="tbl-wrap"><div id="uRecent"></div></div>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="panel" id="usageHint" style="display:none">
|
|
305
|
+
<h2 style="margin-bottom:8px">Real tool usage <small>opt-in</small></h2>
|
|
306
|
+
<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>
|
|
307
|
+
</div>
|
|
308
|
+
</section>
|
|
309
|
+
|
|
310
|
+
<section id="view-accounts" style="display:none">
|
|
311
|
+
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
312
|
+
<div style="flex:1">
|
|
313
|
+
<h1>Accounts & Profiles</h1>
|
|
314
|
+
<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>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="panel">
|
|
318
|
+
<div class="panel-head"><h2>Accounts</h2><button id="accAdd" type="button" class="toggle on">Add account</button></div>
|
|
319
|
+
<div id="accountsBody"><div class="empty" style="padding:24px 0">Loading accounts...</div></div>
|
|
320
|
+
</div>
|
|
321
|
+
<div class="panel">
|
|
322
|
+
<div class="panel-head"><h2>Profiles</h2><button id="profAdd" type="button" class="toggle on">Add profile</button></div>
|
|
323
|
+
<div id="profilesBody"><div class="empty" style="padding:24px 0">Loading profiles...</div></div>
|
|
324
|
+
</div>
|
|
325
|
+
<div id="accountsNote" class="set-note"></div>
|
|
326
|
+
</section>
|
|
327
|
+
|
|
277
328
|
<section id="view-skills" style="display:none">
|
|
278
329
|
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
279
330
|
<div style="flex:1">
|
|
@@ -297,11 +348,17 @@
|
|
|
297
348
|
</section>
|
|
298
349
|
|
|
299
350
|
<section id="view-settings" style="display:none">
|
|
300
|
-
<div class="page-head">
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
|
|
351
|
+
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
352
|
+
<div style="flex:1">
|
|
353
|
+
<h1>Settings</h1>
|
|
354
|
+
<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>
|
|
355
|
+
</div>
|
|
356
|
+
<div style="display:flex;gap:8px;white-space:nowrap">
|
|
357
|
+
<button id="cfgExport" type="button" class="toggle">Export config</button>
|
|
358
|
+
<button id="cfgImport" type="button" class="toggle">Import config</button>
|
|
359
|
+
</div>
|
|
304
360
|
</div>
|
|
361
|
+
<div id="settingsNote" class="set-note"></div>
|
|
305
362
|
<div id="settingsBody"><div class="empty" style="padding:24px 0">Loading settings...</div></div>
|
|
306
363
|
</section>
|
|
307
364
|
|
|
@@ -456,28 +513,58 @@
|
|
|
456
513
|
}
|
|
457
514
|
|
|
458
515
|
// Real tool-usage panel (Claude Code transcripts). null = the opt-in flag is off.
|
|
516
|
+
function shortProject(p) { p = String(p || ""); return p.length > 44 ? "..." + p.slice(-41) : p; }
|
|
517
|
+
|
|
518
|
+
// One token/cost breakdown table (By Model / By Project), sorted by cost then tokens.
|
|
519
|
+
function renderUsageTable(el, map, pending, label) {
|
|
520
|
+
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)));
|
|
521
|
+
if (!entries.length) { el.innerHTML = '<div class="empty" style="padding:16px 0">' + (pending ? "Scanning..." : "No usage recorded yet.") + "</div>"; return; }
|
|
522
|
+
el.innerHTML = '<table class="tbl"><thead><tr><th>' + label + '</th><th class="r">Input</th><th class="r">Output</th>'
|
|
523
|
+
+ '<th class="r">Cache reads</th><th class="r">Msgs</th><th class="r">Cost</th></tr></thead><tbody>'
|
|
524
|
+
+ entries.map(([k, v]) => '<tr><td>' + esc(label === "Project" ? shortProject(k) : k) + '</td><td class="r">' + fmt(v.input) + '</td><td class="r">'
|
|
525
|
+
+ 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("")
|
|
526
|
+
+ "</tbody></table>";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function renderRecentSessions(el, rows, pending) {
|
|
530
|
+
rows = rows || [];
|
|
531
|
+
if (!rows.length) { el.innerHTML = '<div class="empty" style="padding:16px 0">' + (pending ? "Scanning..." : "No sessions yet.") + "</div>"; return; }
|
|
532
|
+
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>'
|
|
533
|
+
+ rows.map((s) => '<tr><td>' + (s.lastActive ? new Date(s.lastActive).toLocaleString() : "-") + '</td><td>' + esc(shortProject(s.project)) + '</td><td>' + esc(s.model || "-")
|
|
534
|
+
+ '</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("")
|
|
535
|
+
+ "</tbody></table>";
|
|
536
|
+
}
|
|
537
|
+
|
|
459
538
|
function renderUsage(u) {
|
|
460
539
|
const panel = $("usagePanel"), hint = $("usageHint");
|
|
461
540
|
if (!u) { panel.style.display = "none"; hint.style.display = "block"; return; }
|
|
462
541
|
hint.style.display = "none";
|
|
463
542
|
panel.style.display = "block";
|
|
543
|
+
$("uCost").textContent = fmtUsd(u.cost || 0);
|
|
464
544
|
$("uCacheMoney").textContent = fmtUsd(cacheMoney(u.cacheRead || 0));
|
|
465
545
|
$("uCacheRead").textContent = fmt(u.cacheRead || 0);
|
|
466
546
|
$("uInput").textContent = fmt(u.input || 0);
|
|
467
547
|
$("uOutput").textContent = fmt(u.output || 0);
|
|
468
548
|
$("uSessions").textContent = fmt(u.sessions || 0);
|
|
469
549
|
$("uSessionsFoot").textContent = u.pending ? "scanning sessions..." : fmt(u.scannedFiles || 0) + " files scanned";
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
550
|
+
// Current 5-hour block (computed locally from transcript timestamps).
|
|
551
|
+
const b = u.block, bw = $("uBlockWrap");
|
|
552
|
+
if (b && b.startedAt) {
|
|
553
|
+
bw.style.display = "block";
|
|
554
|
+
const now = Date.now(), total = 5 * 3600 * 1000;
|
|
555
|
+
const pct = Math.max(0, Math.min(100, Math.round((now - b.startedAt) / total * 100)));
|
|
556
|
+
$("uBlockBar").style.width = pct + "%";
|
|
557
|
+
$("uBlockState").textContent = b.active ? "active" : "window elapsed";
|
|
558
|
+
const remMin = Math.max(0, Math.round((b.endsAt - now) / 60000));
|
|
559
|
+
$("uBlockTime").textContent = b.active ? (remMin + " min left") : "ended";
|
|
560
|
+
$("uBlockTokens").textContent = fmt(b.tokens || 0);
|
|
561
|
+
$("uBlockCost").textContent = fmtUsd(b.cost || 0);
|
|
562
|
+
$("uBlockBurn").textContent = fmt(Math.round(b.burnRatePerMin || 0)) + " tok/min";
|
|
563
|
+
$("uBlockProj").textContent = fmt(b.projectedTokens || 0) + " · " + fmtUsd(b.projectedCost || 0);
|
|
564
|
+
} else { bw.style.display = "none"; }
|
|
565
|
+
renderUsageTable($("uByModel"), u.byModel, u.pending, "Model");
|
|
566
|
+
renderUsageTable($("uByProject"), u.byProject, u.pending, "Project");
|
|
567
|
+
renderRecentSessions($("uRecent"), u.recentSessions, u.pending);
|
|
481
568
|
}
|
|
482
569
|
|
|
483
570
|
function renderCache(c) {
|
|
@@ -669,14 +756,36 @@
|
|
|
669
756
|
// --- settings subpage (mirrors the TUI registry over /api/settings) ---
|
|
670
757
|
function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); }
|
|
671
758
|
|
|
759
|
+
// Turn "(experimental)" and " · measured/experimental" label suffixes into visual badges.
|
|
760
|
+
function labelWithBadges(text) {
|
|
761
|
+
let name = String(text);
|
|
762
|
+
const badges = [];
|
|
763
|
+
if (/\(experimental\)/i.test(name)) { name = name.replace(/\s*\(experimental\)/i, ""); badges.push(["experimental", "exp"]); }
|
|
764
|
+
name = name.replace(/\s*·\s*(measured|experimental)\b/gi, (_, w) => { badges.push([w.toLowerCase(), w.toLowerCase() === "experimental" ? "exp" : "meas"]); return ""; });
|
|
765
|
+
return esc(name) + badges.map(([t, c]) => ' <span class="tag ' + c + '">' + esc(t) + "</span>").join("");
|
|
766
|
+
}
|
|
767
|
+
|
|
672
768
|
function settingRow(s) {
|
|
769
|
+
if (s.kind === "list") {
|
|
770
|
+
const items = s.items || [];
|
|
771
|
+
const chips = items.length
|
|
772
|
+
? '<div class="lchips">' + items.map((it) => '<span class="lchip">' + esc(it)
|
|
773
|
+
+ '<button type="button" class="lx" title="remove" data-skey="' + esc(s.key) + '" data-item="' + esc(it) + '">x</button></span>').join("") + "</div>"
|
|
774
|
+
: '<div class="lnone">none - using the built-in defaults</div>';
|
|
775
|
+
const adder = '<div class="ladd"><input type="text" class="linput" data-skey="' + esc(s.key)
|
|
776
|
+
+ '" placeholder="' + esc(s.itemHint || "add an entry...") + '">'
|
|
777
|
+
+ '<button type="button" class="laddb" data-skey="' + esc(s.key) + '">Add</button></div>';
|
|
778
|
+
return '<div class="set-row list"><div class="set-meta"><div class="set-name">' + labelWithBadges(s.label) + "</div>"
|
|
779
|
+
+ '<div class="set-hint">' + esc(s.hint) + (s.globalOnly ? " · global" : "") + "</div>"
|
|
780
|
+
+ chips + adder + "</div></div>";
|
|
781
|
+
}
|
|
673
782
|
const ctl = s.choices
|
|
674
783
|
? '<select class="choice" data-skey="' + esc(s.key) + '">'
|
|
675
784
|
+ s.choices.map((c) => '<option value="' + esc(c) + '"' + (c === s.choice ? " selected" : "") + ">" + esc(c) + "</option>").join("")
|
|
676
785
|
+ "</select>"
|
|
677
786
|
: '<button type="button" class="toggle' + (s.value ? " on" : "") + '" data-skey="' + esc(s.key)
|
|
678
787
|
+ '" data-kind="bool" data-on="' + (s.value ? 1 : 0) + '">' + (s.value ? "On" : "Off") + "</button>";
|
|
679
|
-
return '<div class="set-row"><div class="set-meta"><div class="set-name">' +
|
|
788
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + labelWithBadges(s.label) + "</div>"
|
|
680
789
|
+ '<div class="set-hint">' + esc(s.hint) + (s.globalOnly ? " · global" : "") + "</div></div>"
|
|
681
790
|
+ '<div class="set-ctl">' + ctl + "</div></div>";
|
|
682
791
|
}
|
|
@@ -704,16 +813,42 @@
|
|
|
704
813
|
} catch { $("settingsNote").textContent = "Could not reach the server to change " + key + "."; }
|
|
705
814
|
}
|
|
706
815
|
|
|
816
|
+
// List settings: add/remove one entry, then reload so the chips reflect disk truth.
|
|
817
|
+
async function postListOp(key, op, item) {
|
|
818
|
+
item = (item || "").trim();
|
|
819
|
+
if (!item) return;
|
|
820
|
+
$("settingsNote").textContent = (op === "add" ? "Adding to " : "Removing from ") + key + "...";
|
|
821
|
+
try {
|
|
822
|
+
const res = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: { op, item } }) });
|
|
823
|
+
const out = await res.json();
|
|
824
|
+
if (!out.ok) { $("settingsNote").textContent = "Could not change " + key + ": " + (out.error || "error"); return; }
|
|
825
|
+
$("settingsNote").textContent = "Saved " + key + ".";
|
|
826
|
+
await loadSettings();
|
|
827
|
+
} catch { $("settingsNote").textContent = "Could not reach the server to change " + key + "."; }
|
|
828
|
+
}
|
|
829
|
+
|
|
707
830
|
function wireSettings() {
|
|
708
831
|
const el = $("settingsBody");
|
|
709
832
|
el.addEventListener("click", (e) => {
|
|
710
833
|
const b = e.target.closest("button.toggle");
|
|
711
|
-
if (b) postSetting(b.dataset.skey, b.dataset.on !== "1", b);
|
|
834
|
+
if (b) { postSetting(b.dataset.skey, b.dataset.on !== "1", b); return; }
|
|
835
|
+
const rm = e.target.closest("button.lx");
|
|
836
|
+
if (rm) { postListOp(rm.dataset.skey, "remove", rm.dataset.item); return; }
|
|
837
|
+
const add = e.target.closest("button.laddb");
|
|
838
|
+
if (add) {
|
|
839
|
+
const inp = el.querySelector('input.linput[data-skey="' + (window.CSS && CSS.escape ? CSS.escape(add.dataset.skey) : add.dataset.skey) + '"]');
|
|
840
|
+
if (inp) { postListOp(add.dataset.skey, "add", inp.value); inp.value = ""; }
|
|
841
|
+
}
|
|
712
842
|
});
|
|
713
843
|
el.addEventListener("change", (e) => {
|
|
714
844
|
const sel = e.target.closest("select.choice");
|
|
715
845
|
if (sel) postSetting(sel.dataset.skey, sel.value, sel);
|
|
716
846
|
});
|
|
847
|
+
el.addEventListener("keydown", (e) => {
|
|
848
|
+
if (e.key !== "Enter") return;
|
|
849
|
+
const inp = e.target.closest("input.linput");
|
|
850
|
+
if (inp) { e.preventDefault(); postListOp(inp.dataset.skey, "add", inp.value); inp.value = ""; }
|
|
851
|
+
});
|
|
717
852
|
}
|
|
718
853
|
|
|
719
854
|
let settingsLoaded = false;
|
|
@@ -732,7 +867,7 @@
|
|
|
732
867
|
function renderSystems(s) {
|
|
733
868
|
const el = $("systems");
|
|
734
869
|
if (!s) { el.innerHTML = '<div class="empty" style="padding:16px 0">Status unavailable.</div>'; return; }
|
|
735
|
-
const item = (k, v) => '<div class="sys-item"><span class="k">' + k + '</span><span class="v">' + v + "</span></div>";
|
|
870
|
+
const item = (k, v) => '<div class="sys-item"><span class="k">' + labelWithBadges(k) + '</span><span class="v">' + v + "</span></div>";
|
|
736
871
|
const sk = s.skills || {};
|
|
737
872
|
const skillTags = '<span class="tag">' + (sk.enigma || 0) + " enigma</span>"
|
|
738
873
|
+ '<span class="tag ext">' + (sk.external || 0) + " external</span>"
|
|
@@ -743,12 +878,30 @@
|
|
|
743
878
|
? item("Proxy measured · real", '<span class="tag">' + fmt(ps.calls) + " calls</span><span class=\"tag\">"
|
|
744
879
|
+ fmt(ps.input) + " in</span><span class=\"tag\">" + fmt(ps.output) + " out</span><span class=\"tag\">" + fmt(ps.cacheRead) + " cache</span>")
|
|
745
880
|
: "";
|
|
881
|
+
const proxyLast = (ps.lastRequestAt || 0) > 0
|
|
882
|
+
? item("Proxy last request · real", '<span class="tag">' + esc(fmtDuration((Date.now() - ps.lastRequestAt) / 1000) + " ago")
|
|
883
|
+
+ '</span><span class="tag">' + esc(new Date(ps.lastRequestAt).toLocaleString()) + "</span>")
|
|
884
|
+
: "";
|
|
885
|
+
// Prompt secret guard: configured state + what it has actually blocked (measured).
|
|
886
|
+
const guardState = s.promptSecretGuard
|
|
887
|
+
? '<span class="pill on">on</span><span class="tag">' + esc(s.promptSecretMode || "redact") + "</span>"
|
|
888
|
+
: pill("off", "off");
|
|
889
|
+
const promptGuard = item("Prompt secret guard · experimental", guardState);
|
|
890
|
+
const blockedTotal = (ps.redacted || 0) + (ps.rejected || 0);
|
|
891
|
+
const promptBlocked = blockedTotal > 0
|
|
892
|
+
? item("Prompt secrets blocked · real", '<span class="tag">' + fmt(ps.redacted || 0) + " redacted</span><span class=\"tag\">"
|
|
893
|
+
+ fmt(ps.rejected || 0) + " rejected</span>"
|
|
894
|
+
+ ((ps.lastBlockedAt || 0) > 0 ? '<span class="tag">' + esc(fmtDuration((Date.now() - ps.lastBlockedAt) / 1000) + " ago") + "</span>" : ""))
|
|
895
|
+
: "";
|
|
746
896
|
const guard = (sec.guardProtects || []).map((p) => '<span class="tag">' + esc(p) + "</span>").join("");
|
|
747
897
|
el.innerHTML =
|
|
748
898
|
item("Context compression (MCP) · measured", boolPill(s.compress))
|
|
749
899
|
+ item("Real tool-usage stats · measured", boolPill(s.usageStats))
|
|
750
|
-
+ item("Claude Code
|
|
900
|
+
+ item("Claude Code proxy · experimental", boolPill(s.proxy))
|
|
751
901
|
+ proxyMeasured
|
|
902
|
+
+ proxyLast
|
|
903
|
+
+ promptGuard
|
|
904
|
+
+ promptBlocked
|
|
752
905
|
+ item("Token-efficient output", levelPill(s.outputStyle))
|
|
753
906
|
+ item("Minimal code", levelPill(s.minimalCode))
|
|
754
907
|
+ item("Parallel sub-agents", boolPill(s.parallelSubagents))
|
|
@@ -765,9 +918,11 @@
|
|
|
765
918
|
try {
|
|
766
919
|
const res = await fetch("/api/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
|
|
767
920
|
const out = await res.json();
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
921
|
+
if (!out.ok) { $("updateNote").textContent = "Update failed: " + (out.error || "error"); return; }
|
|
922
|
+
// The UI bundle may have been refreshed server-side; reload so the browser
|
|
923
|
+
// picks up the new dashboard and fresh data automatically.
|
|
924
|
+
$("updateNote").textContent = (out.note || "Updated.") + " Refreshing the dashboard...";
|
|
925
|
+
setTimeout(() => location.reload(), 1200);
|
|
771
926
|
} catch { $("updateNote").textContent = "Could not reach the server to update."; }
|
|
772
927
|
}
|
|
773
928
|
|
|
@@ -879,16 +1034,141 @@
|
|
|
879
1034
|
} catch { $("skillsBody").innerHTML = '<div class="empty" style="padding:24px 0">Skills unavailable.</div>'; }
|
|
880
1035
|
}
|
|
881
1036
|
|
|
882
|
-
// ---
|
|
1037
|
+
// --- accounts & profiles subpage (mirrors the TUI hub over /api/accounts) ---
|
|
1038
|
+
let accountsLoaded = false;
|
|
1039
|
+
let accTools = [];
|
|
1040
|
+
async function loadAccounts() {
|
|
1041
|
+
try { const r = await fetch("/api/accounts", { cache: "no-store" }); renderAccounts(await r.json()); }
|
|
1042
|
+
catch { $("accountsBody").innerHTML = '<div class="empty" style="padding:24px 0">Accounts unavailable.</div>'; }
|
|
1043
|
+
}
|
|
1044
|
+
function accountRow(a) {
|
|
1045
|
+
const badges = (a.active ? '<span class="tag meas">active</span>' : "")
|
|
1046
|
+
+ (a.loggedIn ? '<span class="tag">' + esc(a.email || "logged in") + "</span>" : '<span class="tag warn">not logged in</span>');
|
|
1047
|
+
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>";
|
|
1048
|
+
const acts = '<div class="set-ctl" style="display:flex;gap:6px;flex-wrap:wrap">'
|
|
1049
|
+
+ (a.active ? "" : btn("activate", "Use"))
|
|
1050
|
+
+ (a.loggedIn ? "" : btn("login", "Log in..."))
|
|
1051
|
+
+ (a.removable ? btn("rename", "Rename") + btn("remove", "Remove", "btn-danger") : "")
|
|
1052
|
+
+ "</div>";
|
|
1053
|
+
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
|
|
1054
|
+
+ '</div><div class="set-hint">' + esc(a.dir) + "</div></div>" + acts + "</div>";
|
|
1055
|
+
}
|
|
1056
|
+
function profileRow(p, tools, accounts) {
|
|
1057
|
+
const maps = tools.map((t) => {
|
|
1058
|
+
const cur = p.accounts[t.name] || "";
|
|
1059
|
+
const opts = ['<option value="">(none)</option>'].concat(accounts.filter((a) => a.tool === t.name)
|
|
1060
|
+
.map((a) => '<option value="' + esc(a.name) + '"' + (a.name === cur ? " selected" : "") + ">" + esc(a.name) + "</option>")).join("");
|
|
1061
|
+
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)
|
|
1062
|
+
+ ' <select class="choice" data-prof-tool="' + esc(t.name) + '" data-prof="' + esc(p.name) + '">' + opts + "</select></label>";
|
|
1063
|
+
}).join("");
|
|
1064
|
+
const acts = '<div class="set-ctl" style="display:flex;gap:6px">'
|
|
1065
|
+
+ (p.active ? "" : '<button type="button" class="toggle" data-prof-act="activate" data-name="' + esc(p.name) + '">Use</button>')
|
|
1066
|
+
+ '<button type="button" class="toggle" data-prof-act="rename" data-name="' + esc(p.name) + '">Rename</button>'
|
|
1067
|
+
+ '<button type="button" class="btn-danger" data-prof-act="remove" data-name="' + esc(p.name) + '">Remove</button></div>';
|
|
1068
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(p.name) + (p.active ? ' <span class="tag meas">active</span>' : "")
|
|
1069
|
+
+ '</div><div class="set-hint" style="margin-top:6px">' + maps + "</div></div>" + acts + "</div>";
|
|
1070
|
+
}
|
|
1071
|
+
function renderAccounts(d) {
|
|
1072
|
+
d = d || {};
|
|
1073
|
+
const accounts = d.accounts || [], profiles = d.profiles || [], tools = d.tools || [];
|
|
1074
|
+
accTools = tools;
|
|
1075
|
+
$("accountsBody").innerHTML = accounts.length ? accounts.map(accountRow).join("") : '<div class="empty" style="padding:20px 0">No accounts.</div>';
|
|
1076
|
+
$("profilesBody").innerHTML = profiles.length ? profiles.map((p) => profileRow(p, tools, accounts)).join("")
|
|
1077
|
+
: '<div class="empty" style="padding:20px 0">No profiles yet. Add one to pin an account per tool.</div>';
|
|
1078
|
+
}
|
|
1079
|
+
async function accAction(op, payload) {
|
|
1080
|
+
$("accountsNote").textContent = "Working...";
|
|
1081
|
+
try {
|
|
1082
|
+
const r = await fetch("/api/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(Object.assign({ op }, payload)) });
|
|
1083
|
+
const out = await r.json();
|
|
1084
|
+
$("accountsNote").textContent = out.ok ? "Saved." : ("Error: " + (out.error || "failed"));
|
|
1085
|
+
if (out.data) renderAccounts(out.data);
|
|
1086
|
+
} catch { $("accountsNote").textContent = "Could not reach the server."; }
|
|
1087
|
+
}
|
|
1088
|
+
function wireAccounts() {
|
|
1089
|
+
$("accAdd").addEventListener("click", () => {
|
|
1090
|
+
const tool = accTools.length === 1 ? accTools[0].name : prompt("Which tool? (" + accTools.map((t) => t.name).join(" / ") + ")", accTools[0] ? accTools[0].name : "");
|
|
1091
|
+
if (!tool) return;
|
|
1092
|
+
const name = prompt("New account name (e.g. work):", "");
|
|
1093
|
+
if (name) accAction("account.add", { tool, name });
|
|
1094
|
+
});
|
|
1095
|
+
$("profAdd").addEventListener("click", () => { const name = prompt("New profile name:", ""); if (name) accAction("profile.add", { name }); });
|
|
1096
|
+
$("accountsBody").addEventListener("click", (e) => {
|
|
1097
|
+
const b = e.target.closest("button[data-acc]"); if (!b) return;
|
|
1098
|
+
const op = b.dataset.acc, tool = b.dataset.tool, name = b.dataset.name;
|
|
1099
|
+
if (op === "activate") accAction("account.activate", { tool, name });
|
|
1100
|
+
else if (op === "remove") { if (confirm("Remove account '" + name + "' and delete its local config dir?")) accAction("account.remove", { tool, name }); }
|
|
1101
|
+
else if (op === "rename") { const nn = prompt("Rename '" + name + "' to:", name); if (nn && nn !== name) accAction("account.rename", { tool, name, newName: nn }); }
|
|
1102
|
+
else if (op === "login") { $("accountsNote").textContent = "To log in, run this in a terminal: enigma " + tool + " " + name; }
|
|
1103
|
+
});
|
|
1104
|
+
$("profilesBody").addEventListener("click", (e) => {
|
|
1105
|
+
const b = e.target.closest("button[data-prof-act]"); if (!b) return;
|
|
1106
|
+
const op = b.dataset.profAct, name = b.dataset.name;
|
|
1107
|
+
if (op === "activate") accAction("profile.activate", { name });
|
|
1108
|
+
else if (op === "remove") { if (confirm("Remove profile '" + name + "'? (its accounts are kept)")) accAction("profile.remove", { name }); }
|
|
1109
|
+
else if (op === "rename") { const nn = prompt("Rename profile '" + name + "' to:", name); if (nn && nn !== name) accAction("profile.rename", { name, newName: nn }); }
|
|
1110
|
+
});
|
|
1111
|
+
$("profilesBody").addEventListener("change", (e) => {
|
|
1112
|
+
const sel = e.target.closest("select[data-prof-tool]"); if (!sel) return;
|
|
1113
|
+
accAction("profile.setAccount", { profile: sel.dataset.prof, tool: sel.dataset.profTool, account: sel.value || null });
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// --- config import / export (the browser file picker is what "asks where") ---
|
|
1118
|
+
async function exportConfig() {
|
|
1119
|
+
$("settingsNote").textContent = "Exporting...";
|
|
1120
|
+
let text;
|
|
1121
|
+
try { text = await (await fetch("/api/config-export", { cache: "no-store" })).text(); }
|
|
1122
|
+
catch { $("settingsNote").textContent = "Export failed."; return; }
|
|
1123
|
+
const name = "enigma-config-" + new Date().toISOString().slice(0, 10) + ".json";
|
|
1124
|
+
if (window.showSaveFilePicker) {
|
|
1125
|
+
try {
|
|
1126
|
+
const h = await window.showSaveFilePicker({ suggestedName: name, types: [{ description: "JSON", accept: { "application/json": [".json"] } }] });
|
|
1127
|
+
const w = await h.createWritable(); await w.write(text); await w.close();
|
|
1128
|
+
$("settingsNote").textContent = "Exported (no secrets included)."; return;
|
|
1129
|
+
} catch (e) { if (e && e.name === "AbortError") { $("settingsNote").textContent = "Export cancelled."; return; } }
|
|
1130
|
+
}
|
|
1131
|
+
const url = URL.createObjectURL(new Blob([text], { type: "application/json" }));
|
|
1132
|
+
const a = document.createElement("a"); a.href = url; a.download = name; a.click(); URL.revokeObjectURL(url);
|
|
1133
|
+
$("settingsNote").textContent = "Exported to your downloads (no secrets included).";
|
|
1134
|
+
}
|
|
1135
|
+
async function importConfig() {
|
|
1136
|
+
let text = null;
|
|
1137
|
+
if (window.showOpenFilePicker) {
|
|
1138
|
+
try { const [h] = await window.showOpenFilePicker({ types: [{ description: "JSON", accept: { "application/json": [".json"] } }] }); text = await (await h.getFile()).text(); }
|
|
1139
|
+
catch (e) { if (e && e.name === "AbortError") return; }
|
|
1140
|
+
}
|
|
1141
|
+
if (text === null) {
|
|
1142
|
+
text = await new Promise((res) => {
|
|
1143
|
+
const inp = document.createElement("input"); inp.type = "file"; inp.accept = "application/json,.json";
|
|
1144
|
+
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); };
|
|
1145
|
+
inp.click();
|
|
1146
|
+
});
|
|
1147
|
+
if (text === null) return;
|
|
1148
|
+
}
|
|
1149
|
+
let body; try { body = JSON.parse(text); } catch { $("settingsNote").textContent = "That file is not valid JSON."; return; }
|
|
1150
|
+
$("settingsNote").textContent = "Importing...";
|
|
1151
|
+
try {
|
|
1152
|
+
const out = await (await fetch("/api/config-import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })).json();
|
|
1153
|
+
if (!out.ok) { $("settingsNote").textContent = "Import failed: " + (out.error || "error"); return; }
|
|
1154
|
+
$("settingsNote").textContent = "Imported: " + (out.applied || []).join(", ") + ((out.skipped && out.skipped.length) ? (" · skipped: " + out.skipped.join(", ")) : "");
|
|
1155
|
+
settingsLoaded = false; loadSettings(); accountsLoaded = false;
|
|
1156
|
+
} catch { $("settingsNote").textContent = "Could not reach the server to import."; }
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// --- hash routing between the subpages ---
|
|
883
1160
|
function route() {
|
|
884
1161
|
const h = (location.hash || "").replace(/^#\/?/, "");
|
|
885
|
-
const v =
|
|
1162
|
+
const v = ["usage", "accounts", "settings", "skills"].includes(h) ? h : "savings";
|
|
886
1163
|
$("view-savings").style.display = v === "savings" ? "" : "none";
|
|
1164
|
+
$("view-usage").style.display = v === "usage" ? "" : "none";
|
|
1165
|
+
$("view-accounts").style.display = v === "accounts" ? "" : "none";
|
|
887
1166
|
$("view-skills").style.display = v === "skills" ? "" : "none";
|
|
888
1167
|
$("view-settings").style.display = v === "settings" ? "" : "none";
|
|
889
1168
|
document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.view === v));
|
|
890
1169
|
if (v === "settings" && !settingsLoaded) { settingsLoaded = true; loadSettings(); }
|
|
891
1170
|
if (v === "skills" && !skillsLoaded) { skillsLoaded = true; loadSkills(); }
|
|
1171
|
+
if (v === "accounts" && !accountsLoaded) { accountsLoaded = true; loadAccounts(); }
|
|
892
1172
|
// The chart was sized while its view may have been hidden; nudge it on return.
|
|
893
1173
|
if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
|
|
894
1174
|
}
|
|
@@ -899,6 +1179,9 @@
|
|
|
899
1179
|
document.addEventListener("visibilitychange", () => { if (!document.hidden) refreshAll(true); });
|
|
900
1180
|
wireSettings();
|
|
901
1181
|
wireSkills();
|
|
1182
|
+
wireAccounts();
|
|
1183
|
+
$("cfgExport").addEventListener("click", exportConfig);
|
|
1184
|
+
$("cfgImport").addEventListener("click", importConfig);
|
|
902
1185
|
$("updateBtn").addEventListener("click", runUpdate);
|
|
903
1186
|
$("refreshBtn").addEventListener("click", () => refreshAll(true));
|
|
904
1187
|
window.addEventListener("hashchange", route);
|