@enigmax/dashboard 0.1.114 → 0.1.116

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.
Files changed (2) hide show
  1. package/assets/index.html +209 -56
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -467,6 +467,15 @@
467
467
  .pg-textarea { min-height: 92px; resize: vertical; line-height: 1.5; }
468
468
  .pg-check { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--text); padding-top: 4px; }
469
469
  .pg-check label { color: var(--muted); }
470
+ .ad-pool { display: flex; flex-wrap: wrap; gap: 8px 16px; }
471
+ .ad-pool label { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text); cursor: pointer; min-width: 0; max-width: 100%; }
472
+ .ad-pool label > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
473
+ .ad-pool input { accent-color: var(--good); }
474
+ .ad-pool .aicon { color: var(--muted); }
475
+ .ad-pool.off { opacity: .5; }
476
+ .ad-fixed.off { opacity: .5; }
477
+ .ad-err { color: var(--bad); font-size: 11px; }
478
+ .ad-err:empty { display: none; }
470
479
  .pg-out { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre-wrap; word-break: break-word; overflow-x: auto; max-height: 360px; overflow-y: auto; margin: 0; }
471
480
  /* Claude-style usage window gauges (current session / weekly all / weekly Sonnet). */
472
481
  .wgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 18px; }
@@ -832,11 +841,11 @@
832
841
  </a>
833
842
  <a href="#/packs" data-view="packs" class="tab">
834
843
  <svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
835
- <span>Packs</span>
844
+ <span>Agents</span>
836
845
  </a>
837
846
  <a href="#/playground" data-view="playground" class="tab">
838
847
  <svg class="tab-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 17 6-6-6-6"/><path d="M12 19h8"/></svg>
839
- <span>Playground</span>
848
+ <span>Local API</span>
840
849
  </a>
841
850
  </div>
842
851
  <div class="nav-group" role="group" aria-labelledby="ng-code">
@@ -1257,8 +1266,8 @@
1257
1266
  <section id="view-packs" style="display:none">
1258
1267
  <div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
1259
1268
  <div style="flex:1">
1260
- <h1>Packs</h1>
1261
- <p>Optional harness packs you can add on demand. Each runs in its own <strong>isolated agent context</strong>, so its skills and commands never load into your normal coding agent. Launch a pack from a terminal with <code>enigma &lt;pack&gt;</code>.</p>
1269
+ <h1>Agents</h1>
1270
+ <p>Optional agents you can add on demand. Each runs in its own <strong>isolated agent context</strong>, so its skills, commands and MCP servers never load into your normal coding agent. Launch one from a terminal with <code>enigma &lt;agent&gt;</code>.</p>
1262
1271
  </div>
1263
1272
  <button id="packsRefresh" type="button" class="iconbtn" title="Refresh" aria-label="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
1264
1273
  </div>
@@ -1427,9 +1436,44 @@
1427
1436
 
1428
1437
  <section id="view-playground" style="display:none">
1429
1438
  <div class="page-head">
1430
- <h1>API Playground</h1>
1431
- <p>Test the local agent API without leaving the dashboard. Pick a format (OpenAI or Anthropic), a backend, and send a message. Run it in-process (no server needed) or against a running <code>enigma api</code> with an auth key.</p>
1439
+ <h1>Local API</h1>
1440
+ <p>Set which account answers <code>enigma api</code> requests, then try it below.</p>
1432
1441
  </div>
1442
+ <div class="panel" id="adPanel">
1443
+ <div class="set-cat-h">Server defaults</div>
1444
+ <div class="set-cat-b">What <code>enigma api</code> starts with. A command-line flag overrides them for one run; changes apply the next time the server starts.</div>
1445
+ <div class="pg-grid">
1446
+ <label class="pg-field"><span>Account rotation</span>
1447
+ <select id="adRotation"></select>
1448
+ </label>
1449
+ <label class="pg-field ad-fixed"><span>Default account</span>
1450
+ <select id="adAccount"><option value="">(active account)</option></select>
1451
+ </label>
1452
+ <label class="pg-field ad-fixed"><span>Default profile</span>
1453
+ <select id="adProfile"><option value="">(none)</option></select>
1454
+ </label>
1455
+ <label class="pg-field ad-fixed"><span>Default agent</span>
1456
+ <select id="adPack"><option value="">(none)</option></select>
1457
+ </label>
1458
+ <label class="pg-field"><span>Port</span>
1459
+ <input id="adPort" class="linput" type="number" min="1" max="65535" step="1" inputmode="numeric" aria-describedby="adPortErr">
1460
+ <small id="adPortErr" class="ad-err" role="alert"></small>
1461
+ </label>
1462
+ <div class="pg-field"><span>Callers choose the account</span>
1463
+ <button id="adClient" type="button" class="toggle on" data-kind="bool" role="switch" aria-checked="true" aria-label="Let requests pick their own account, profile or pack">On</button>
1464
+ </div>
1465
+ </div>
1466
+ <div id="adRotationHint" class="set-hint"></div>
1467
+ <div class="pg-field" id="adPoolWrap"><span>Accounts in the rotation <small>(none checked = every account; fill-first uses them in the order you check them)</small></span>
1468
+ <div id="adPool" class="ad-pool"></div>
1469
+ </div>
1470
+ <div class="ladd" style="gap:8px;align-items:center">
1471
+ <button id="adSave" type="button" class="toggle on" disabled>Save defaults</button>
1472
+ <button id="adDiscard" type="button" class="laddb" style="display:none">Discard changes</button>
1473
+ <span id="adNote" class="set-hint"></span>
1474
+ </div>
1475
+ </div>
1476
+ <div class="set-cat-h" style="margin:18px 0 8px">Playground</div>
1433
1477
  <div class="panel">
1434
1478
  <div class="pg-grid">
1435
1479
  <label class="pg-field"><span>Run</span>
@@ -1454,13 +1498,13 @@
1454
1498
  <input id="pgKey" class="linput" type="password" placeholder="Bearer token / ENIGMA_API_KEY">
1455
1499
  </label>
1456
1500
  <label class="pg-field"><span>Account <small>(optional)</small></span>
1457
- <select id="pgAccount"><option value="">(active)</option></select>
1501
+ <select id="pgAccount"><option value="">(server default)</option></select>
1458
1502
  </label>
1459
1503
  <label class="pg-field"><span>Profile <small>(optional)</small></span>
1460
- <select id="pgProfile"><option value="">(none)</option></select>
1504
+ <select id="pgProfile"><option value="">(server default)</option></select>
1461
1505
  </label>
1462
- <label class="pg-field"><span>Pack <small>(optional)</small></span>
1463
- <select id="pgPack"><option value="">(none)</option></select>
1506
+ <label class="pg-field"><span>Agent <small>(optional)</small></span>
1507
+ <select id="pgPack"><option value="">(server default)</option></select>
1464
1508
  </label>
1465
1509
  <div class="pg-field pg-tools"><span>Agent tools <small>(Read/Write/Bash...)</small></span>
1466
1510
  <button id="pgTools" type="button" class="toggle on" data-kind="bool" role="switch" aria-checked="true" aria-label="Enable agent tools">On</button>
@@ -1469,10 +1513,6 @@
1469
1513
  <input id="pgImage" type="file" accept="image/*" class="linput">
1470
1514
  </label>
1471
1515
  </div>
1472
- <div class="ladd" style="gap:8px;align-items:center">
1473
- <button id="pgSaveDefault" type="button" class="laddb">Save context as <code>enigma api</code> default</button>
1474
- <span id="pgDefaultNote" class="set-hint"></span>
1475
- </div>
1476
1516
  <label class="pg-field"><span>System prompt <small>(optional)</small></span>
1477
1517
  <input id="pgSystem" class="linput" placeholder="e.g. You are a terse assistant.">
1478
1518
  </label>
@@ -3660,8 +3700,9 @@ export {
3660
3700
 
3661
3701
  function renderSettings(cats) {
3662
3702
  const el = $("settingsBody");
3663
- // Skill-related settings live in the Skills tab; drop them here so they aren't shown twice.
3664
- cats = (cats || []).map((c) => ({ ...c, settings: (c.settings || []).filter((s) => SKILL_SETTING_KEYS.indexOf(s.key) === -1) })).filter((c) => c.settings.length);
3703
+ // Skill-related settings live in the Skills tab and the API server's in the Local API tab;
3704
+ // drop them here so they aren't shown twice.
3705
+ cats = (cats || []).map((c) => ({ ...c, settings: (c.settings || []).filter((s) => SKILL_SETTING_KEYS.indexOf(s.key) === -1 && !/^api-/.test(s.key)) })).filter((c) => c.settings.length);
3665
3706
  if (!cats.length) { el.innerHTML = '<div class="empty" style="padding:24px 0">No settings available.</div>'; return; }
3666
3707
  // One panel per category; settings laid out in a responsive 2-column grid so a
3667
3708
  // long config list stays scannable on a wide screen.
@@ -4059,7 +4100,7 @@ export {
4059
4100
  // config stores `opencode` and `rovodev`, the reader sees OpenCode and Rovo Dev.
4060
4101
  const AGENT_LABELS = {
4061
4102
  auto: "Auto", claude: "Claude Code", codex: "Codex",
4062
- opencode: "OpenCode", rovodev: "Rovo Dev", pi: "Pi"
4103
+ opencode: "OpenCode", kimi: "Kimi Code", rovodev: "Rovo Dev", pi: "Pi"
4063
4104
  };
4064
4105
  // App label -> internal name. Normalized so every label variant maps to an icon:
4065
4106
  // "Claude Code", "Codex"/"OpenAI Codex", "opencode"/"OpenCode", "Rovo Dev", "Pi".
@@ -5090,7 +5131,7 @@ export {
5090
5131
 
5091
5132
  /**
5092
5133
  * The palette's engine, from @enigmax/primitives: subsequence ranking (so "qgate"
5093
- * finds "Quality gate" and "plyg" finds "Playground"), the debounce and the
5134
+ * finds "Quality gate" and "lapi" finds "Local API"), the debounce and the
5094
5135
  * cancellation. The scorer this replaced is the one the package now ships, so the
5095
5136
  * dashboard and every other consumer rank the same way.
5096
5137
  */
@@ -5341,17 +5382,17 @@ export {
5341
5382
  });
5342
5383
  }
5343
5384
 
5344
- // --- packs subpage (optional isolated harness packs; marketplace) ---
5385
+ // --- agents subpage (optional isolated agents, formerly packs; marketplace) ---
5345
5386
  function renderPacks(list) {
5346
5387
  list = list || [];
5347
- if (!list.length) { $("packsBody").innerHTML = '<div class="empty" style="padding:12px 0">No packs available.</div>'; return; }
5388
+ if (!list.length) { $("packsBody").innerHTML = '<div class="empty" style="padding:12px 0">No agents available.</div>'; return; }
5348
5389
  $("packsBody").innerHTML = list.map((p) => {
5349
5390
  const tags = (p.tags || []).map((t) => '<span class="tag">' + esc(t) + '</span>').join(" ");
5350
5391
  const state = p.installed ? '<span class="tag">' + (p.version ? "installed " + esc(p.version) : "installed") + '</span>' : '<span class="tag">not installed</span>';
5351
5392
  const added = p.enabled ? '<span class="tag" style="color:var(--ok)">added</span>' : "";
5352
5393
  const actions = p.installed
5353
5394
  ? '<button type="button" class="toggle on" data-pack="launch" data-id="' + esc(p.id) + '">Launch command</button>'
5354
- + '<button type="button" class="toggle" data-pack="setup" data-id="' + esc(p.id) + '" title="Register the pack MCP servers (needs Python 3)">Setup MCP</button>'
5395
+ + '<button type="button" class="toggle" data-pack="setup" data-id="' + esc(p.id) + '" title="Register the agent MCP servers">Setup MCP</button>'
5355
5396
  + '<button type="button" class="iconbtn" data-pack="update" data-id="' + esc(p.id) + '" title="Update" aria-label="Update ' + esc(p.label) + '">' + REFRESH_ICON + "</button>"
5356
5397
  + '<button type="button" class="btn-danger icon" data-pack="remove" data-id="' + esc(p.id) + '" title="Remove" aria-label="Remove ' + esc(p.label) + '">' + TRASH_ICON + "</button>"
5357
5398
  : '<button type="button" class="toggle on" data-pack="install" data-id="' + esc(p.id) + '">Install</button>';
@@ -5359,10 +5400,10 @@ export {
5359
5400
  const opts = '<option value="">follow active (' + esc(p.resolvedAccount || "default") + ")" + stTag(p.resolvedState) + "</option>"
5360
5401
  + (p.accounts || []).map((a) => '<option value="' + esc(a.name) + '"' + (p.defaultAccount === a.name ? " selected" : "") + ">" + esc(a.name) + (a.label && a.label !== a.name ? " - " + esc(a.label) : "") + stTag(a.state) + "</option>").join("");
5361
5402
  const warn = (p.installed && !p.contextReady && (p.resolvedState === "expired" || p.resolvedState === "empty"))
5362
- ? '<div class="set-hint" style="margin-top:6px;color:var(--warn,#e0a020)">This login is ' + (p.resolvedState === "expired" ? "expired" : "not signed in") + ' - the pack may ask you to log in. Refresh it (launch that account once) or pick another above.</div>'
5403
+ ? '<div class="set-hint" style="margin-top:6px;color:var(--warn,#e0a020)">This login is ' + (p.resolvedState === "expired" ? "expired" : "not signed in") + ' - the agent may ask you to log in. Refresh it (launch that account once) or pick another above.</div>'
5363
5404
  : (p.installed && p.contextReady ? '<div class="set-hint" style="margin-top:6px;color:var(--ok)">Active session ready - launches without asking to log in.</div>' : "");
5364
5405
  const accountSel = p.installed
5365
- ? '<label class="set-hint" style="display:flex;align-items:center;gap:8px;margin-top:10px">Account that seeds this pack:'
5406
+ ? '<label class="set-hint" style="display:flex;align-items:center;gap:8px;margin-top:10px">Account that seeds this agent:'
5366
5407
  + '<select data-pack-account data-id="' + esc(p.id) + '">' + opts + "</select></label>" + warn
5367
5408
  : "";
5368
5409
  return '<div class="panel"><div class="set-row"><div class="set-meta">'
@@ -5376,10 +5417,10 @@ export {
5376
5417
  async function loadPacks() {
5377
5418
  $("packsNote").textContent = "";
5378
5419
  try { renderPacks((await (await fetch("/api/packs", { cache: "no-store" })).json()).packs); }
5379
- catch { $("packsBody").innerHTML = '<div class="empty" style="padding:12px 0">Packs unavailable.</div>'; }
5420
+ catch { $("packsBody").innerHTML = '<div class="empty" style="padding:12px 0">Agents unavailable.</div>'; }
5380
5421
  }
5381
5422
  async function packAction(id, action, value) {
5382
- const confirmMsg = action === "remove" ? "Remove this pack and its isolated context?" : null;
5423
+ const confirmMsg = action === "remove" ? "Remove this agent and its isolated context?" : null;
5383
5424
  if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, true))) return;
5384
5425
  setNote("packsNote", action === "install" || action === "update" ? "Working..." : "Saving...");
5385
5426
  try {
@@ -6342,30 +6383,149 @@ export {
6342
6383
  if (!opts.length) opts.push('<option value="claude" data-icon="claude">Claude Code (default)</option>');
6343
6384
  sel.innerHTML = opts.join("");
6344
6385
  syncDD(sel);
6345
- // Context selectors: account (labeled by tool), profile, pack.
6346
- const acc = $("pgAccount");
6347
- acc.innerHTML = '<option value="">(active)</option>' + (info.accounts || []).map((a) =>
6348
- '<option value="' + esc(a.name) + '" data-icon="' + esc(agentName(a.tool)) + '">' + esc(a.name) + " (" + esc(agentLabel(a.tool)) + ")</option>").join("");
6349
- syncDD(acc);
6350
- const prof = $("pgProfile");
6351
- prof.innerHTML = '<option value="">(none)</option>' + (info.profiles || []).map((n) => '<option value="' + esc(n) + '">' + esc(n) + "</option>").join("");
6352
- syncDD(prof);
6353
- const pk = $("pgPack");
6354
- pk.innerHTML = '<option value="">(none)</option>' + (info.packs || []).map((p) => '<option value="' + esc(p.id) + '">' + esc(p.label) + (p.installed ? "" : " - not installed") + "</option>").join("");
6355
- syncDD(pk);
6356
- // Pre-select the persisted server default so the playground matches `enigma api`.
6357
- const def = info.defaults || { account: "", profile: "", pack: "" };
6358
- acc.value = def.account || ""; syncDD(acc);
6359
- prof.value = def.profile || ""; syncDD(prof);
6360
- pk.value = def.pack || ""; syncDD(pk);
6361
- renderPgDefault(def);
6362
- $("pgBase").placeholder = "http://127.0.0.1:" + (info.apiPort || 8000);
6386
+ // Context selectors: account (labeled by tool), profile, pack. Blank = whatever the
6387
+ // server defaults below pick (the rotation included), so a test matches `enigma api`.
6388
+ fillContextSelect($("pgAccount"), "(server default)", accountOptions(info.accounts));
6389
+ fillContextSelect($("pgProfile"), "(server default)", (info.profiles || []).map((n) => ({ value: n, label: n })));
6390
+ fillContextSelect($("pgPack"), "(server default)", packOptions(info.packs));
6391
+ fillContextSelect($("adAccount"), "(active account)", accountOptions(info.accounts));
6392
+ fillContextSelect($("adProfile"), "(none)", (info.profiles || []).map((n) => ({ value: n, label: n })));
6393
+ fillContextSelect($("adPack"), "(none)", packOptions(info.packs));
6394
+ const rot = $("adRotation");
6395
+ rot.innerHTML = (info.rotations || ["off"]).map((r) => '<option value="' + esc(r) + '">' + esc(prettyChoice(r)) + "</option>").join("");
6396
+ adInfo = info;
6397
+ adApply(info.defaults);
6398
+ $("pgBase").placeholder = "http://127.0.0.1:" + ((info.defaults && info.defaults.port) || info.apiPort || 8000);
6363
6399
  pgSyncMode();
6364
6400
  }
6365
- function renderPgDefault(def) {
6366
- def = def || {};
6367
- const parts = [def.pack && "pack " + def.pack, def.account && "account " + def.account, def.profile && "profile " + def.profile].filter(Boolean);
6368
- $("pgDefaultNote").textContent = parts.length ? "Current default: " + parts.join(", ") : "Current default: active account";
6401
+ function fillContextSelect(sel, blank, items) {
6402
+ sel.innerHTML = '<option value="">' + esc(blank) + "</option>" + items.map((o) =>
6403
+ '<option value="' + esc(o.value) + '"' + (o.icon ? ' data-icon="' + esc(o.icon) + '"' : "") + ">" + esc(o.label) + "</option>").join("");
6404
+ syncDD(sel);
6405
+ }
6406
+ function accountOptions(accounts) {
6407
+ return (accounts || []).map((a) => ({ value: a.name, label: a.name + " (" + agentLabel(a.tool) + ")", icon: agentName(a.tool) }));
6408
+ }
6409
+ function packOptions(packs) {
6410
+ return (packs || []).map((p) => ({ value: p.id, label: p.label + (p.installed ? "" : " - not installed") }));
6411
+ }
6412
+
6413
+ // --- Server defaults for `enigma api` (account, rotation, pool, caller choice, port) ---
6414
+ let adInfo = null;
6415
+ let adSaved = null;
6416
+ let adPicked = [];
6417
+ const AD_ROTATION_HINT = {
6418
+ "off": "Requests that name no account use the default account, profile or pack below.",
6419
+ "round-robin": "Requests that name no account take turns across the accounts below. One that hits a usage limit is retried on the next.",
6420
+ "least-used": "Requests that name no account go to the one with the least use in its current 5-hour window. One that hits a usage limit is retried on the next.",
6421
+ "fill-first": "Requests use the first account below until it hits a usage limit, then the next one.",
6422
+ "random": "Requests that name no account go to a random account below. One that hits a usage limit is retried on the next.",
6423
+ };
6424
+ // Unique account names (the pool is by name; the same name can exist for several agents), saved pool order first.
6425
+ function adPoolNames(pool) {
6426
+ const all = [];
6427
+ ((adInfo && adInfo.accounts) || []).forEach((a) => { if (all.indexOf(a.name) === -1) all.push(a.name); });
6428
+ return (pool || []).filter((n) => all.indexOf(n) !== -1).concat(all.filter((n) => (pool || []).indexOf(n) === -1));
6429
+ }
6430
+ function adRenderPool(pool) {
6431
+ const el = $("adPool");
6432
+ const names = adPoolNames(pool);
6433
+ if (!names.length) { el.innerHTML = '<span class="lnone">No accounts yet - add one in Accounts.</span>'; return; }
6434
+ el.innerHTML = names.map((n) => {
6435
+ const tools = ((adInfo && adInfo.accounts) || []).filter((a) => a.name === n).map((a) => a.tool);
6436
+ const icon = tools.length === 1 ? '<span class="aicon" aria-hidden="true">' + (agentSvg(agentName(tools[0])) || "") + "</span>" : "";
6437
+ return '<label title="' + esc(n + " (" + tools.map(agentLabel).join(", ") + ")") + '"><input type="checkbox" data-pool="' + esc(n) + '"'
6438
+ + ((pool || []).indexOf(n) !== -1 ? " checked" : "") + ">" + icon + "<span>" + esc(n) + "</span></label>";
6439
+ }).join("");
6440
+ }
6441
+ function adApply(def) {
6442
+ def = def || { account: "", profile: "", pack: "", rotation: "off", pool: [], clientContext: true, port: 8000 };
6443
+ adSaved = { account: def.account || "", profile: def.profile || "", pack: def.pack || "", rotation: def.rotation || "off", pool: (def.pool || []).slice(), clientContext: def.clientContext !== false, port: def.port || 8000 };
6444
+ $("adAccount").value = adSaved.account; syncDD($("adAccount"));
6445
+ $("adProfile").value = adSaved.profile; syncDD($("adProfile"));
6446
+ $("adPack").value = adSaved.pack; syncDD($("adPack"));
6447
+ $("adRotation").value = adSaved.rotation; syncDD($("adRotation"));
6448
+ $("adPort").value = String(adSaved.port);
6449
+ paintSwitch($("adClient"), adSaved.clientContext, "On", "Off");
6450
+ adPicked = adSaved.pool.slice();
6451
+ adRenderPool(adSaved.pool);
6452
+ adSync();
6453
+ }
6454
+ function adRead() {
6455
+ const checked = [...$("adPool").querySelectorAll("input[data-pool]:checked")].map((i) => i.dataset.pool);
6456
+ // Pool order is the order accounts were checked in (fill-first walks it), saved ones first.
6457
+ const pool = adPicked.filter((n) => checked.indexOf(n) !== -1).concat(checked.filter((n) => adPicked.indexOf(n) === -1));
6458
+ return {
6459
+ account: $("adAccount").value, profile: $("adProfile").value, pack: $("adPack").value,
6460
+ rotation: $("adRotation").value, pool,
6461
+ clientContext: $("adClient").getAttribute("aria-checked") === "true",
6462
+ port: $("adPort").value.trim(),
6463
+ };
6464
+ }
6465
+ function adPortError(v) {
6466
+ if (v === "") return "Enter a port.";
6467
+ const n = Number(v);
6468
+ return Number.isInteger(n) && n >= 1 && n <= 65535 ? "" : "Use a whole number from 1 to 65535.";
6469
+ }
6470
+ // Dirty = the form differs from what was loaded, so a value changed and put back leaves Save off.
6471
+ function adDirty(cur) {
6472
+ if (!adSaved) return false;
6473
+ return cur.account !== adSaved.account || cur.profile !== adSaved.profile || cur.pack !== adSaved.pack
6474
+ || cur.rotation !== adSaved.rotation || cur.clientContext !== adSaved.clientContext
6475
+ || Number(cur.port) !== adSaved.port || cur.pool.join("\n") !== adSaved.pool.join("\n");
6476
+ }
6477
+ function adSync() {
6478
+ const cur = adRead();
6479
+ const rotating = cur.rotation !== "off";
6480
+ $("adRotationHint").textContent = AD_ROTATION_HINT[cur.rotation] || "";
6481
+ $("adPool").classList.toggle("off", !rotating);
6482
+ $("adPoolWrap").style.display = rotating ? "" : "none";
6483
+ document.querySelectorAll("#adPanel .ad-fixed").forEach((el) => el.classList.toggle("off", rotating));
6484
+ const portErr = adPortError(cur.port);
6485
+ $("adPortErr").textContent = portErr;
6486
+ $("adPort").setAttribute("aria-invalid", portErr ? "true" : "false");
6487
+ const dirty = adDirty(cur);
6488
+ $("adSave").disabled = !dirty || !!portErr;
6489
+ $("adSave").title = portErr ? portErr : (dirty ? "" : "Nothing changed");
6490
+ $("adDiscard").style.display = dirty ? "" : "none";
6491
+ }
6492
+ async function adSave() {
6493
+ const cur = adRead();
6494
+ if (adPortError(cur.port) || !adDirty(cur)) return;
6495
+ const btn = $("adSave");
6496
+ btn.disabled = true;
6497
+ setNote("adNote", "Saving defaults...");
6498
+ try {
6499
+ const out = await apiJson("/api/playground", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "set-defaults", ...cur, port: Number(cur.port) }) });
6500
+ if (!out.ok) {
6501
+ setNote("adNote", "Could not save: " + (out.error || "error"), "err");
6502
+ const focus = { account: "adAccount", profile: "adProfile", pack: "adPack", rotation: "adRotation", port: "adPort", clientContext: "adClient" }[out.field];
6503
+ if (focus && $(focus)) $(focus).focus();
6504
+ adSync();
6505
+ return;
6506
+ }
6507
+ adApply(out.defaults);
6508
+ setNote("adNote", "Saved. Restart 'enigma api' to use them.", "ok");
6509
+ } catch (e) {
6510
+ setNote("adNote", "Could not save: " + e.message, "err");
6511
+ adSync();
6512
+ }
6513
+ }
6514
+ function wireApiDefaults() {
6515
+ ["adAccount", "adProfile", "adPack", "adRotation"].forEach((id) => $(id).addEventListener("change", adSync));
6516
+ $("adPort").addEventListener("input", adSync);
6517
+ $("adPool").addEventListener("change", (e) => {
6518
+ const box = e.target.closest("input[data-pool]");
6519
+ if (box) {
6520
+ adPicked = adPicked.filter((n) => n !== box.dataset.pool);
6521
+ if (box.checked) adPicked.push(box.dataset.pool);
6522
+ }
6523
+ adSync();
6524
+ });
6525
+ const cl = $("adClient");
6526
+ cl.addEventListener("click", () => { paintSwitch(cl, cl.getAttribute("aria-checked") !== "true", "On", "Off"); adSync(); });
6527
+ $("adSave").addEventListener("click", adSave);
6528
+ $("adDiscard").addEventListener("click", () => adApply(adSaved));
6369
6529
  }
6370
6530
  function pgSyncMode() {
6371
6531
  const http = $("pgMode").value === "http";
@@ -6417,6 +6577,7 @@ export {
6417
6577
  const meta = [];
6418
6578
  if (out.format) meta.push(out.format === "anthropic" ? "Anthropic" : "OpenAI");
6419
6579
  if (out.tool) meta.push(out.tool);
6580
+ if (out.account) meta.push("account " + out.account);
6420
6581
  if (typeof out.status === "number") meta.push("HTTP " + out.status);
6421
6582
  if (out.usage) meta.push(out.usage.input + " in / " + out.usage.output + " out");
6422
6583
  $("pgMeta").textContent = meta.join(" · ");
@@ -6424,17 +6585,9 @@ export {
6424
6585
  $("pgRaw").textContent = out.response ? (typeof out.response === "string" ? out.response : JSON.stringify(out.response, null, 2)) : "";
6425
6586
  $("pgCurl").textContent = out.curl || "";
6426
6587
  }
6427
- async function pgSaveDefault() {
6428
- const payload = { op: "set-defaults", account: $("pgAccount").value || "", profile: $("pgProfile").value || "", pack: $("pgPack").value || "" };
6429
- try {
6430
- const out = await (await fetch("/api/playground", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) })).json();
6431
- if (out.ok) { renderPgDefault(out.defaults); toast.success("Saved as the enigma api default"); }
6432
- else toast.error(out.error || "Could not save the default");
6433
- } catch { toast.error("Could not reach the dashboard server."); }
6434
- }
6435
6588
  function wirePlayground() {
6589
+ wireApiDefaults();
6436
6590
  $("pgSend").addEventListener("click", pgSend);
6437
- $("pgSaveDefault").addEventListener("click", pgSaveDefault);
6438
6591
  const tog = $("pgTools");
6439
6592
  tog.addEventListener("click", () => paintSwitch(tog, tog.getAttribute("aria-checked") !== "true", "On", "Off"));
6440
6593
  $("pgMode").addEventListener("change", pgSyncMode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.114",
3
+ "version": "0.1.116",
4
4
  "description": "Local browser dashboard UI for enigma: the static page and chart assets enigma serves on its loopback dashboard (savings, real tool usage, in-browser settings). Installed on demand by enigma-cli; not a runtime dependency.",
5
5
  "type": "module",
6
6
  "files": [