@enigmax/dashboard 0.1.113 → 0.1.115

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 +227 -65
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -25,6 +25,9 @@
25
25
  --elev: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0) 42%);
26
26
  --shadow-1: 0 1px 2px rgba(0,0,0,0.4);
27
27
  --shadow-2: 0 18px 40px -22px rgba(0,0,0,0.75);
28
+ /* Hover borders, scrollbars and the text that sits on a filled accent or good pill. */
29
+ --border-strong: #2f3744; --scrollbar: #2b3340;
30
+ --on-accent: #1a1206; --on-good: #06210f;
28
31
  }
29
32
  * { box-sizing: border-box; }
30
33
  /* An icon never gives up its size to the text beside it. Every svg here is an icon, and
@@ -60,6 +63,8 @@
60
63
  otherwise stay glued to the sidebar with all the empty space piled on the right. */
61
64
  .content { flex: 1; min-width: 0; padding: 30px 30px 48px; max-width: 1560px; margin-inline: auto; }
62
65
  .iconbtn.nav-burger { display: none; }
66
+ /* The rail carries the wordmark; once the rail is a drawer the header shows it instead. */
67
+ .wordmark.mob-brand { display: none; }
63
68
  .nav-scrim { position: fixed; inset: 0; z-index: 25; background: rgba(0,0,0,0.6); }
64
69
  a { color: var(--accent2); }
65
70
  /* Quality floor: one visible keyboard-focus ring for every interactive control
@@ -133,7 +138,7 @@
133
138
  }
134
139
  /* A faint top-edge highlight so a card reads as a lit surface, not a flat rectangle. */
135
140
  .card::before { content: ""; position: absolute; inset: 0; background: var(--elev); pointer-events: none; }
136
- .card:hover { border-color: #2f3744; transform: translateY(-2px); box-shadow: var(--shadow-1), var(--shadow-2); }
141
+ .card:hover { border-color: var(--border-strong); transform: translateY(-2px); box-shadow: var(--shadow-1), var(--shadow-2); }
137
142
  /* Hero KPI (Money Saved): spans two columns, accent-lit - the loudest thing on the page. */
138
143
  .card.hero { grid-column: span 2; border-color: rgba(224,164,88,0.35); background: linear-gradient(135deg, var(--accent-weak), transparent 62%), var(--surface); }
139
144
  .card.hero .value { font-size: 42px; }
@@ -171,7 +176,7 @@
171
176
  transition: color .15s, background .15s, border-color .15s;
172
177
  }
173
178
  .ranges button:hover { color: var(--text); }
174
- .ranges button.active { background: var(--accent); color: #1a1206; border-color: var(--accent); font-weight: 600; }
179
+ .ranges button.active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); font-weight: 600; }
175
180
  .chartwrap { position: relative; width: 100%; height: 280px; }
176
181
  /* Suppress the charting library's attribution logo. */
177
182
  #tv-attr-logo { display: none !important; }
@@ -271,8 +276,8 @@
271
276
  .iconbtn.spinning svg { animation: spin .7s linear infinite; }
272
277
  .toggle.with-icon { display: inline-flex; align-items: center; gap: 6px; }
273
278
  .toggle.with-icon svg { width: 14px; height: 14px; display: block; }
274
- .toggle:hover { color: var(--text); border-color: #2f3744; }
275
- .toggle.on { background: var(--good); color: #06210f; border-color: var(--good); font-weight: 600; }
279
+ .toggle:hover { color: var(--text); border-color: var(--border-strong); }
280
+ .toggle.on { background: var(--good); color: var(--on-good); border-color: var(--good); font-weight: 600; }
276
281
  /* Boolean settings render as a sliding switch. Only the bool controls carry
277
282
  data-kind="bool" - action buttons reuse .toggle and stay buttons. The On/Off
278
283
  text is kept for screen readers but pushed off-screen; the knob is drawn with
@@ -288,12 +293,12 @@
288
293
  width: 18px; height: 18px; border-radius: 50%; background: #cdd2da;
289
294
  transition: left .18s, background .18s;
290
295
  }
291
- .toggle[data-kind="bool"]:hover { border-color: #2f3744; }
296
+ .toggle[data-kind="bool"]:hover { border-color: var(--border-strong); }
292
297
  .toggle[data-kind="bool"].on { background: var(--good); border-color: var(--good); }
293
- .toggle[data-kind="bool"].on::after { left: 23px; background: #06210f; }
298
+ .toggle[data-kind="bool"].on::after { left: 23px; background: var(--on-good); }
294
299
  /* Compact icon-only button (e.g. Edit), instead of a text button. */
295
300
  .iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 30px; padding: 0; background: var(--surface2); color: var(--muted); border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: pointer; transition: color .15s, border-color .15s, transform .1s; }
296
- .iconbtn:hover { color: var(--accent); border-color: #2f3744; }
301
+ .iconbtn:hover { color: var(--accent); border-color: var(--border-strong); }
297
302
  .iconbtn svg { width: 15px; height: 15px; display: block; }
298
303
  select.choice {
299
304
  background: var(--surface2); color: var(--text); border: 1px solid var(--border);
@@ -432,8 +437,8 @@
432
437
  /* Per-agent skill toggles: one pill per app (the app's icon), green when the skill is on for it; the app name is the tooltip. */
433
438
  .set-agents { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
434
439
  .achip { display: inline-flex; align-items: center; justify-content: center; padding: 5px 8px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); cursor: pointer; text-decoration: none; transition: color .15s, background .15s, border-color .15s; }
435
- .achip:hover { border-color: #2f3744; color: var(--text); }
436
- .achip.on { background: var(--good); color: #06210f; border-color: var(--good); }
440
+ .achip:hover { border-color: var(--border-strong); color: var(--text); }
441
+ .achip.on { background: var(--good); color: var(--on-good); border-color: var(--good); }
437
442
  .achip:disabled { opacity: .35; cursor: not-allowed; }
438
443
  .achip svg { width: 14px; height: 14px; display: block; }
439
444
  /* Inline app icons in the "where deployed" tag (static, name in tooltip). */
@@ -462,6 +467,15 @@
462
467
  .pg-textarea { min-height: 92px; resize: vertical; line-height: 1.5; }
463
468
  .pg-check { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--text); padding-top: 4px; }
464
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; }
465
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; }
466
480
  /* Claude-style usage window gauges (current session / weekly all / weekly Sonnet). */
467
481
  .wgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 18px; }
@@ -494,10 +508,10 @@
494
508
  /* Both halves hold text of unpredictable length (a setting label, a port, a model id, a
495
509
  command), so the row is built to shrink rather than to push: min-width:0 defeats the
496
510
  default min-width:auto that makes a flex/grid item refuse to go below its content
497
- width, flex-wrap lets the value drop to its own line instead of spilling out of the
498
- card, and overflow-wrap:anywhere lets a long unbroken label break (unlike break-word,
511
+ width, the two grid columns keep the state on the right of every card (a wrapping
512
+ flex row put it under the label in some cards and beside it in others), and overflow-wrap:anywhere lets a long unbroken label break (unlike break-word,
499
513
  it also lowers min-content, which is what stops the grid track being forced wider). */
500
- .sys-item { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; padding: 11px 14px; font-size: 13px; background: var(--surface2); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); }
514
+ .sys-item { display: grid; grid-template-columns: minmax(min(100%, 140px), 1fr) minmax(0, auto); align-items: center; gap: 6px 12px; min-width: 0; padding: 11px 14px; font-size: 13px; background: var(--surface2); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); }
501
515
  .sys-item .k { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
502
516
  .sys-item .v { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; flex: 0 1 auto; min-width: 0; }
503
517
  .sys-item .v .tag { overflow-wrap: anywhere; }
@@ -505,13 +519,13 @@
505
519
  .sys-item .pill.off { background: transparent; }
506
520
  /* Rows whose value is a list of chips span the full width and let the chips wrap with
507
521
  even spacing, instead of cramming many tags into one compact half-column cell. */
508
- .sys-item.wide { grid-column: 1 / -1; flex-direction: column; align-items: stretch; gap: 8px; }
522
+ .sys-item.wide { grid-column: 1 / -1; display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
509
523
  .sys-item.wide .v { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-start; }
510
524
  .sys-item.wide .v .tag { margin-left: 0; }
511
525
  .pill { font-size: 11px; border-radius: 6px; padding: 2px 9px; border: 1px solid var(--border); white-space: nowrap; }
512
- .pill.on { color: #06210f; background: var(--good); border-color: var(--good); font-weight: 600; }
526
+ .pill.on { color: var(--on-good); background: var(--good); border-color: var(--good); font-weight: 600; }
513
527
  .pill.off { color: var(--muted); background: var(--surface2); }
514
- .pill.lvl { color: #1a1206; background: var(--accent); border-color: var(--accent); font-weight: 600; }
528
+ .pill.lvl { color: var(--on-accent); background: var(--accent); border-color: var(--accent); font-weight: 600; }
515
529
  /* Claude operational-status pill (status.claude.com). */
516
530
  .cstatus { display: inline-flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 12px; color: var(--muted); padding: 5px 11px; background: rgba(0,0,0,0.25); border: 1px solid var(--border-soft); border-radius: 999px; white-space: nowrap; }
517
531
  .cdot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: 0 0 auto; }
@@ -577,6 +591,7 @@
577
591
  }
578
592
  .sidebar.open { transform: none; }
579
593
  .iconbtn.side-x, .iconbtn.nav-burger { display: inline-flex; }
594
+ .wordmark.mob-brand { display: inline-flex; font-size: 18px; }
580
595
  .content { padding: 18px; }
581
596
  .tabs .tab { padding: 11px 12px; font-size: 13.5px; }
582
597
  body.nav-open { overflow: hidden; }
@@ -585,7 +600,9 @@
585
600
  .content { padding: 16px; }
586
601
  .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
587
602
  header { gap: 12px; }
588
- .nav { margin-left: auto; flex-wrap: wrap; gap: 10px; }
603
+ /* Burger and wordmark share the first row; the toolbar takes a full row below. */
604
+ .nav { margin-left: 0; flex-basis: 100%; flex-wrap: wrap; gap: 10px; }
605
+ .nav .cmdk { flex: 1 1 auto; }
589
606
  /* A settings row is label + controls side by side, and the control cluster is
590
607
  flex-shrink: 0 so a wide one (account actions, a select plus three buttons)
591
608
  pushes the row past the viewport. Here the row wraps and the cluster is
@@ -600,11 +617,11 @@
600
617
  .set-ctl { flex-shrink: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
601
618
  }
602
619
  /* Pretty scrollbars across the whole dashboard (thin, rounded, themed). */
603
- * { scrollbar-width: thin; scrollbar-color: #2b3340 transparent; }
620
+ * { scrollbar-width: thin; scrollbar-color: var(--scrollbar) transparent; }
604
621
  ::-webkit-scrollbar { width: 10px; height: 10px; }
605
622
  ::-webkit-scrollbar-track { background: transparent; }
606
623
  ::-webkit-scrollbar-corner { background: transparent; }
607
- ::-webkit-scrollbar-thumb { background: #2b3340; border-radius: 999px; }
624
+ ::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 999px; }
608
625
  ::-webkit-scrollbar-thumb:hover { background: #3a4150; }
609
626
  /* Scope selector (global vs a project) in the header. */
610
627
  .scope-wrap { display: inline-flex; align-items: center; gap: 6px; }
@@ -719,7 +736,7 @@
719
736
  picker is a custom listbox - the logo shows on the button AND inside every option. */
720
737
  .dd { position: relative; }
721
738
  .dd-btn { display: inline-flex; align-items: center; gap: 8px; min-width: 170px; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 10px; font-size: 12px; font-family: var(--mono); cursor: pointer; }
722
- .dd-btn:hover { border-color: #2f3744; }
739
+ .dd-btn:hover { border-color: var(--border-strong); }
723
740
  .dd-btn .dd-label { flex: 1; text-align: left; }
724
741
  .dd-btn .dd-caret { width: 13px; height: 13px; flex: 0 0 auto; color: var(--muted); }
725
742
  .dd-ic { display: inline-flex; align-items: center; }
@@ -749,7 +766,7 @@
749
766
  border-radius: var(--radius-sm); padding: 11px 13px; font-size: 13px; font-family: var(--mono);
750
767
  transition: border-color .15s, background .15s, transform .1s;
751
768
  }
752
- .pick-card:hover { border-color: #2f3744; }
769
+ .pick-card:hover { border-color: var(--border-strong); }
753
770
  .pick-card:active { transform: scale(0.98); }
754
771
  .pick-card.sel { border-color: var(--accent); }
755
772
  .pick-card svg { width: 20px; height: 20px; display: block; flex: none; color: var(--muted); }
@@ -828,7 +845,7 @@
828
845
  </a>
829
846
  <a href="#/playground" data-view="playground" class="tab">
830
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>
831
- <span>Playground</span>
848
+ <span>Local API</span>
832
849
  </a>
833
850
  </div>
834
851
  <div class="nav-group" role="group" aria-labelledby="ng-code">
@@ -897,6 +914,7 @@
897
914
  <button type="button" id="navOpen" class="iconbtn nav-burger" title="Menu" aria-label="Open the menu" aria-expanded="false" aria-controls="sidebar">
898
915
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
899
916
  </button>
917
+ <div class="wordmark mob-brand" aria-hidden="true">Enigma<span>.</span></div>
900
918
  <div class="nav">
901
919
  <button type="button" id="palOpen" class="cmdk" aria-label="Search views and projects" aria-haspopup="dialog">
902
920
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
@@ -1418,9 +1436,44 @@
1418
1436
 
1419
1437
  <section id="view-playground" style="display:none">
1420
1438
  <div class="page-head">
1421
- <h1>API Playground</h1>
1422
- <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>
1423
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 pack</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>
1424
1477
  <div class="panel">
1425
1478
  <div class="pg-grid">
1426
1479
  <label class="pg-field"><span>Run</span>
@@ -1445,13 +1498,13 @@
1445
1498
  <input id="pgKey" class="linput" type="password" placeholder="Bearer token / ENIGMA_API_KEY">
1446
1499
  </label>
1447
1500
  <label class="pg-field"><span>Account <small>(optional)</small></span>
1448
- <select id="pgAccount"><option value="">(active)</option></select>
1501
+ <select id="pgAccount"><option value="">(server default)</option></select>
1449
1502
  </label>
1450
1503
  <label class="pg-field"><span>Profile <small>(optional)</small></span>
1451
- <select id="pgProfile"><option value="">(none)</option></select>
1504
+ <select id="pgProfile"><option value="">(server default)</option></select>
1452
1505
  </label>
1453
1506
  <label class="pg-field"><span>Pack <small>(optional)</small></span>
1454
- <select id="pgPack"><option value="">(none)</option></select>
1507
+ <select id="pgPack"><option value="">(server default)</option></select>
1455
1508
  </label>
1456
1509
  <div class="pg-field pg-tools"><span>Agent tools <small>(Read/Write/Bash...)</small></span>
1457
1510
  <button id="pgTools" type="button" class="toggle on" data-kind="bool" role="switch" aria-checked="true" aria-label="Enable agent tools">On</button>
@@ -1460,10 +1513,6 @@
1460
1513
  <input id="pgImage" type="file" accept="image/*" class="linput">
1461
1514
  </label>
1462
1515
  </div>
1463
- <div class="ladd" style="gap:8px;align-items:center">
1464
- <button id="pgSaveDefault" type="button" class="laddb">Save context as <code>enigma api</code> default</button>
1465
- <span id="pgDefaultNote" class="set-hint"></span>
1466
- </div>
1467
1516
  <label class="pg-field"><span>System prompt <small>(optional)</small></span>
1468
1517
  <input id="pgSystem" class="linput" placeholder="e.g. You are a terse assistant.">
1469
1518
  </label>
@@ -3651,8 +3700,9 @@ export {
3651
3700
 
3652
3701
  function renderSettings(cats) {
3653
3702
  const el = $("settingsBody");
3654
- // Skill-related settings live in the Skills tab; drop them here so they aren't shown twice.
3655
- 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);
3656
3706
  if (!cats.length) { el.innerHTML = '<div class="empty" style="padding:24px 0">No settings available.</div>'; return; }
3657
3707
  // One panel per category; settings laid out in a responsive 2-column grid so a
3658
3708
  // long config list stays scannable on a wide screen.
@@ -4050,7 +4100,7 @@ export {
4050
4100
  // config stores `opencode` and `rovodev`, the reader sees OpenCode and Rovo Dev.
4051
4101
  const AGENT_LABELS = {
4052
4102
  auto: "Auto", claude: "Claude Code", codex: "Codex",
4053
- opencode: "OpenCode", rovodev: "Rovo Dev", pi: "Pi"
4103
+ opencode: "OpenCode", kimi: "Kimi Code", rovodev: "Rovo Dev", pi: "Pi"
4054
4104
  };
4055
4105
  // App label -> internal name. Normalized so every label variant maps to an icon:
4056
4106
  // "Claude Code", "Codex"/"OpenAI Codex", "opencode"/"OpenCode", "Rovo Dev", "Pi".
@@ -5081,7 +5131,7 @@ export {
5081
5131
 
5082
5132
  /**
5083
5133
  * The palette's engine, from @enigmax/primitives: subsequence ranking (so "qgate"
5084
- * finds "Quality gate" and "plyg" finds "Playground"), the debounce and the
5134
+ * finds "Quality gate" and "lapi" finds "Local API"), the debounce and the
5085
5135
  * cancellation. The scorer this replaced is the one the package now ships, so the
5086
5136
  * dashboard and every other consumer rank the same way.
5087
5137
  */
@@ -6333,30 +6383,149 @@ export {
6333
6383
  if (!opts.length) opts.push('<option value="claude" data-icon="claude">Claude Code (default)</option>');
6334
6384
  sel.innerHTML = opts.join("");
6335
6385
  syncDD(sel);
6336
- // Context selectors: account (labeled by tool), profile, pack.
6337
- const acc = $("pgAccount");
6338
- acc.innerHTML = '<option value="">(active)</option>' + (info.accounts || []).map((a) =>
6339
- '<option value="' + esc(a.name) + '" data-icon="' + esc(agentName(a.tool)) + '">' + esc(a.name) + " (" + esc(agentLabel(a.tool)) + ")</option>").join("");
6340
- syncDD(acc);
6341
- const prof = $("pgProfile");
6342
- prof.innerHTML = '<option value="">(none)</option>' + (info.profiles || []).map((n) => '<option value="' + esc(n) + '">' + esc(n) + "</option>").join("");
6343
- syncDD(prof);
6344
- const pk = $("pgPack");
6345
- pk.innerHTML = '<option value="">(none)</option>' + (info.packs || []).map((p) => '<option value="' + esc(p.id) + '">' + esc(p.label) + (p.installed ? "" : " - not installed") + "</option>").join("");
6346
- syncDD(pk);
6347
- // Pre-select the persisted server default so the playground matches `enigma api`.
6348
- const def = info.defaults || { account: "", profile: "", pack: "" };
6349
- acc.value = def.account || ""; syncDD(acc);
6350
- prof.value = def.profile || ""; syncDD(prof);
6351
- pk.value = def.pack || ""; syncDD(pk);
6352
- renderPgDefault(def);
6353
- $("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);
6354
6399
  pgSyncMode();
6355
6400
  }
6356
- function renderPgDefault(def) {
6357
- def = def || {};
6358
- const parts = [def.pack && "pack " + def.pack, def.account && "account " + def.account, def.profile && "profile " + def.profile].filter(Boolean);
6359
- $("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));
6360
6529
  }
6361
6530
  function pgSyncMode() {
6362
6531
  const http = $("pgMode").value === "http";
@@ -6408,6 +6577,7 @@ export {
6408
6577
  const meta = [];
6409
6578
  if (out.format) meta.push(out.format === "anthropic" ? "Anthropic" : "OpenAI");
6410
6579
  if (out.tool) meta.push(out.tool);
6580
+ if (out.account) meta.push("account " + out.account);
6411
6581
  if (typeof out.status === "number") meta.push("HTTP " + out.status);
6412
6582
  if (out.usage) meta.push(out.usage.input + " in / " + out.usage.output + " out");
6413
6583
  $("pgMeta").textContent = meta.join(" · ");
@@ -6415,17 +6585,9 @@ export {
6415
6585
  $("pgRaw").textContent = out.response ? (typeof out.response === "string" ? out.response : JSON.stringify(out.response, null, 2)) : "";
6416
6586
  $("pgCurl").textContent = out.curl || "";
6417
6587
  }
6418
- async function pgSaveDefault() {
6419
- const payload = { op: "set-defaults", account: $("pgAccount").value || "", profile: $("pgProfile").value || "", pack: $("pgPack").value || "" };
6420
- try {
6421
- const out = await (await fetch("/api/playground", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) })).json();
6422
- if (out.ok) { renderPgDefault(out.defaults); toast.success("Saved as the enigma api default"); }
6423
- else toast.error(out.error || "Could not save the default");
6424
- } catch { toast.error("Could not reach the dashboard server."); }
6425
- }
6426
6588
  function wirePlayground() {
6589
+ wireApiDefaults();
6427
6590
  $("pgSend").addEventListener("click", pgSend);
6428
- $("pgSaveDefault").addEventListener("click", pgSaveDefault);
6429
6591
  const tog = $("pgTools");
6430
6592
  tog.addEventListener("click", () => paintSwitch(tog, tog.getAttribute("aria-checked") !== "true", "On", "Off"));
6431
6593
  $("pgMode").addEventListener("change", pgSyncMode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
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": [