@enigmax/dashboard 0.1.98 → 0.1.99

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 +108 -13
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -200,8 +200,6 @@
200
200
  .set-cat { margin-bottom: 18px; }
201
201
  .set-cat:last-child { margin-bottom: 0; }
202
202
  .set-cat-h { font-family: var(--mono); color: var(--accent); font-size: 12px; text-transform: uppercase; letter-spacing: 0.7px; margin-bottom: 4px; }
203
- /* A path in a header is a literal: uppercasing it prints something that does not exist. */
204
- .set-cat-h code { text-transform: none; letter-spacing: 0; }
205
203
  .set-cat-b { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
206
204
  .set-row { display: flex; align-items: center; gap: 14px; padding: 9px 0; border-bottom: 1px solid var(--surface2); }
207
205
  .set-row:last-child { border-bottom: none; }
@@ -510,6 +508,11 @@
510
508
  width: 100%; min-height: 300px; font-family: var(--mono); font-size: 12.5px; line-height: 1.55;
511
509
  resize: vertical; white-space: pre; overflow-wrap: normal; overflow-x: auto; tab-size: 2;
512
510
  }
511
+ /* The model row grows a text field and a Save when "Custom" is picked. That cluster does
512
+ not fit a grid column beside its label (`.set-ctl` never shrinks), so the row takes the
513
+ full grid width while it is showing and its controls wrap instead of squeezing the label. */
514
+ #gateModelRow.wide { grid-column: 1 / -1; }
515
+ #gateModelRow .set-ctl { flex-wrap: wrap; justify-content: flex-end; }
513
516
  .gate-run { padding: 13px 0; border-top: 1px solid var(--border-soft); }
514
517
  .gate-run:first-child { border-top: 0; padding-top: 4px; }
515
518
  .gate-run-h { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
@@ -594,6 +597,17 @@
594
597
  }
595
598
  .modal-back.show .modal { transform: none; opacity: 1; }
596
599
  .modal.wide { max-width: 900px; }
600
+ /* Info dialog: the paths and links a panel would otherwise print in its header. */
601
+ .modal.info { max-width: 560px; }
602
+ .info-row { padding: 10px 0; border-top: 1px solid var(--surface2); }
603
+ .info-row:first-of-type { border-top: none; }
604
+ .info-k { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
605
+ .info-v { display: flex; align-items: center; gap: 8px; }
606
+ /* A path is a literal that must stay readable: it wraps rather than overflowing the dialog. */
607
+ .info-v code { flex: 1; min-width: 0; overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; }
608
+ .info-links { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 16px; }
609
+ .info-links a { color: var(--accent); font-size: 12px; text-decoration: none; }
610
+ .info-links a:hover { text-decoration: underline; }
597
611
  /* Command palette: the same backdrop and surface as every other dialog, sat in the top
598
612
  third so the result list grows downwards without moving the input under the caret. */
599
613
  /* The palette lives in the markup instead of being created per open, so it needs this:
@@ -1075,6 +1089,7 @@
1075
1089
  <h1>Quality gate</h1>
1076
1090
  <p>The pipeline your agent drives before a change reaches the push target: review, tests, docs, lint, PR and CI. Switch the scope selector to a project to see that repository's runs and its own config.</p>
1077
1091
  </div>
1092
+ <button id="gateInfo" type="button" class="iconbtn" title="Where this is configured" aria-label="Where the gate is configured"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></button>
1078
1093
  <button id="gateRefresh" type="button" class="iconbtn" title="Refresh" aria-label="Refresh the gate view"><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>
1079
1094
  </div>
1080
1095
  <div class="gate-bar">
@@ -1098,7 +1113,7 @@
1098
1113
  </div>
1099
1114
 
1100
1115
  <div class="panel" id="gateSetPanel">
1101
- <div class="set-cat-h">Pipeline settings <code id="gateCfgPath"></code></div>
1116
+ <div class="set-cat-h">Pipeline settings</div>
1102
1117
  <div class="sub" style="margin-bottom:10px">What the daemon runs and how hard it tries. Model and auto-fix limits are what a run's duration and token cost actually hang on.</div>
1103
1118
  <div class="set-grid">
1104
1119
  <div class="set-row">
@@ -1128,8 +1143,8 @@
1128
1143
  </div>
1129
1144
 
1130
1145
  <div class="panel">
1131
- <div class="set-cat-h">Global configuration <code class="gate-cfg-path"></code></div>
1132
- <div class="sub" style="margin-bottom:10px">The same file as raw YAML, for the keys with no control above (agent paths, ACP targets, ignore patterns, evidence). Saved only when it parses, so a typo here never reaches the daemon.</div>
1146
+ <div class="set-cat-h">Global configuration</div>
1147
+ <div class="sub" style="margin-bottom:10px">The global config as raw YAML, for the keys with no control above (agent paths, ACP targets, ignore patterns, evidence). Saved only when it parses, so a typo here never reaches the daemon.</div>
1133
1148
  <textarea id="gateCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Global gate configuration"></textarea>
1134
1149
  <div class="ladd" style="gap:8px;align-items:center">
1135
1150
  <button id="gateCfgSave" type="button" class="toggle on" disabled title="No changes to save">Save</button>
@@ -1138,7 +1153,7 @@
1138
1153
  </div>
1139
1154
 
1140
1155
  <div class="panel" id="gateRepoPanel" style="display:none">
1141
- <div class="set-cat-h">Repository configuration <code id="gateRepoCfgPath"></code></div>
1156
+ <div class="set-cat-h">Repository configuration</div>
1142
1157
  <div class="sub" style="margin-bottom:10px">Committed with the repository and merged over the global config. Only the fields the gate trusts from a branch take effect.</div>
1143
1158
  <textarea id="gateRepoCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Repository gate configuration"></textarea>
1144
1159
  <div class="ladd" style="gap:8px;align-items:center">
@@ -3423,6 +3438,55 @@ export {
3423
3438
  const askIconChoice = (title, iconOptions, value, message) => openModal({ title, message, field: { iconOptions, value } });
3424
3439
  const askConfirm = (title, message, danger) => openModal({ title, message, danger, okLabel: danger ? "Remove" : "OK" }).then(Boolean);
3425
3440
 
3441
+ const COPY_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
3442
+ const CHECK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
3443
+ /** Copies `text`, with the button confirming it; falls back to execCommand where the clipboard API is blocked. */
3444
+ function copyToClipboard(text, btn) {
3445
+ const done = () => {
3446
+ toast.success("Copied");
3447
+ if (!btn) return;
3448
+ const before = btn.innerHTML;
3449
+ btn.innerHTML = CHECK_ICON;
3450
+ setTimeout(() => { btn.innerHTML = before; }, 1200);
3451
+ };
3452
+ (navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).then(done).catch(() => {
3453
+ const ta = document.createElement("textarea");
3454
+ ta.value = text; document.body.appendChild(ta); ta.select();
3455
+ try { document.execCommand("copy"); done(); } catch { toast.error("Copy failed"); }
3456
+ document.body.removeChild(ta);
3457
+ });
3458
+ }
3459
+
3460
+ // Read-only dialog behind an info icon: the paths and links a panel header would otherwise
3461
+ // print inline. Rows are labelled literals with a copy action; a row with no value is dropped.
3462
+ function openInfoModal({ title, intro, rows, note, links }) {
3463
+ const back = document.createElement("div");
3464
+ back.className = "modal-back";
3465
+ const list = (rows || []).filter((r) => r && r.value).map((r) =>
3466
+ '<div class="info-row"><div class="info-k">' + esc(r.label) + "</div>"
3467
+ + '<div class="info-v"><code>' + esc(r.value) + "</code>"
3468
+ + '<button type="button" class="iconbtn" data-copy="' + esc(r.value) + '" title="Copy" aria-label="Copy the ' + esc(r.label.toLowerCase()) + '">' + COPY_ICON + "</button></div>"
3469
+ + (r.hint ? '<div class="set-hint">' + esc(r.hint) + "</div>" : "") + "</div>").join("");
3470
+ back.innerHTML = '<div class="modal info" role="dialog" aria-modal="true">'
3471
+ + '<div class="modal-title">' + esc(title) + "</div>"
3472
+ + (intro ? '<div class="modal-msg">' + esc(intro) + "</div>" : "")
3473
+ + list
3474
+ + (note ? '<div class="set-hint" style="margin-top:12px">' + esc(note) + "</div>" : "")
3475
+ + ((links || []).length
3476
+ ? '<div class="info-links">' + links.map((l) => '<a href="' + esc(l.href) + '" target="_blank" rel="noopener noreferrer">' + esc(l.label) + "</a>").join("") + "</div>"
3477
+ : "")
3478
+ + '<div class="modal-acts"><button type="button" class="toggle on modal-ok">Close</button></div></div>';
3479
+ document.body.appendChild(back);
3480
+ requestAnimationFrame(() => back.classList.add("show"));
3481
+ let done = false;
3482
+ const close = () => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); };
3483
+ back.querySelector(".modal-ok").addEventListener("click", close);
3484
+ back.addEventListener("mousedown", (e) => { if (e.target === back) close(); });
3485
+ back.addEventListener("keydown", (e) => { if (e.key === "Escape" || e.key === "Enter") { e.preventDefault(); close(); } });
3486
+ back.querySelectorAll("[data-copy]").forEach((b) => b.addEventListener("click", () => copyToClipboard(b.dataset.copy, b)));
3487
+ back.querySelector(".modal-ok").focus();
3488
+ }
3489
+
3426
3490
  // Turn "(experimental)" and " · measured/experimental" label suffixes into visual badges.
3427
3491
  function labelWithBadges(text) {
3428
3492
  let name = String(text);
@@ -4322,6 +4386,26 @@ export {
4322
4386
  // is Bun-only), so an unavailable history reports itself instead of blanking the page.
4323
4387
  const gateSaved = { global: "", repo: "" };
4324
4388
  let gateWritable = true;
4389
+ // Where the gate keeps its files, kept for the info dialog instead of printed in the headers.
4390
+ let gateWhere = { global: "", repo: "", root: "", note: "" };
4391
+ const GATE_DOCS_URL = "https://fjrg2007.github.io/enigma/docs/concepts/gate";
4392
+ const GATE_COMMANDS_URL = "https://fjrg2007.github.io/enigma/docs/reference/commands";
4393
+ function openGateInfo() {
4394
+ openInfoModal({
4395
+ title: "Gate configuration",
4396
+ intro: "Where the gate reads its settings and keeps its run history.",
4397
+ rows: [
4398
+ { label: "Global config", value: gateWhere.global, hint: "applies to every repository" },
4399
+ { label: "Repository config", value: gateWhere.repo, hint: "committed with the repository, merged over the global config" },
4400
+ { label: "Gate home", value: gateWhere.root, hint: "run database, logs and the daemon pid file" },
4401
+ ],
4402
+ note: gateWhere.note,
4403
+ links: [
4404
+ { label: "Gate documentation", href: GATE_DOCS_URL },
4405
+ { label: "Command reference", href: GATE_COMMANDS_URL },
4406
+ ],
4407
+ });
4408
+ }
4325
4409
  function gateEls(which) {
4326
4410
  return which === "global"
4327
4411
  ? { area: $("gateCfg"), btn: $("gateCfgSave"), note: "gateCfgNote" }
@@ -4407,7 +4491,12 @@ export {
4407
4491
  const GATE_MODEL_CUSTOM = "__custom__";
4408
4492
  let gateSavedModel = "";
4409
4493
 
4410
- /** Rebuilds the model dropdown for `agent` and selects `model` (empty = the agent default). */
4494
+ /**
4495
+ * Rebuilds the model dropdown for `agent` and selects `model` (empty = the agent default).
4496
+ * `agent` must be the RESOLVED agent: the backend reads and writes the model on whatever
4497
+ * `auto` probed to, so keying the list off the declared name left every stored model under
4498
+ * `agent: auto` falling through to "Custom".
4499
+ */
4411
4500
  function renderGateModel(agent, model) {
4412
4501
  const sel = $("gateModelSel"), inp = $("gateModel");
4413
4502
  const opts = [["", "auto (agent default)"]].concat(GATE_MODELS[agent] || [], [[GATE_MODEL_CUSTOM, "Custom model id..."]]);
@@ -4424,6 +4513,8 @@ export {
4424
4513
  const custom = $("gateModelSel").value === GATE_MODEL_CUSTOM;
4425
4514
  $("gateModel").style.display = custom ? "" : "none";
4426
4515
  $("gateModelSave").style.display = custom ? "" : "none";
4516
+ // Select + field + Save is wider than one settings column; the row takes the full width.
4517
+ $("gateModelRow").classList.toggle("wide", custom);
4427
4518
  syncGateModelSave();
4428
4519
  }
4429
4520
  /** No-op rule: nothing to save while the typed id still equals the stored one. */
@@ -4449,7 +4540,7 @@ export {
4449
4540
  ? "auto: first agent found on PATH - currently " + s.agentResolved
4450
4541
  : "which coding agent the pipeline drives";
4451
4542
  $("gateModelRow").style.display = s.modelSupported ? "" : "none";
4452
- renderGateModel(s.agent, s.model || "");
4543
+ renderGateModel(s.agentResolved || s.agent, s.model || "");
4453
4544
  $("gateModelHint").textContent = "passed to " + s.agentResolved
4454
4545
  + " on every step; a smaller model is the one change that cuts both run time and tokens";
4455
4546
  $("gateCiTimeout").value = s.ciTimeout || "";
@@ -4486,9 +4577,14 @@ export {
4486
4577
  : '<div class="empty" style="padding:12px 0">' + (g.runsAvailable === false ? esc(g.runsNote || "Run history unavailable.") : (repo ? "No runs recorded for this repository yet." : "No run in flight.")) + "</div>";
4487
4578
  gatePoll();
4488
4579
  if (live) return;
4489
- const cfgPath = (g.globalConfig || {}).path || "";
4490
- $("gateCfgPath").textContent = cfgPath;
4491
- for (const el of document.querySelectorAll(".gate-cfg-path")) el.textContent = cfgPath;
4580
+ // The paths are not header decoration: they live behind the info icon, which is the
4581
+ // only place they are worth the room.
4582
+ gateWhere = {
4583
+ global: (g.globalConfig || {}).path || "",
4584
+ repo: repo ? (repo.configPath || "") : "",
4585
+ root: g.root || "",
4586
+ note: g.canWrite === false ? (g.writeNote || "") : "",
4587
+ };
4492
4588
  gateSaved.global = (g.globalConfig || {}).text || "";
4493
4589
  $("gateCfg").value = gateSaved.global;
4494
4590
  // A runtime that cannot parse a gate config must not write one: read-only, with the reason.
@@ -4500,7 +4596,6 @@ export {
4500
4596
  renderGateSettings(g.settings);
4501
4597
  $("gateRepoPanel").style.display = repo ? "" : "none";
4502
4598
  if (repo) {
4503
- $("gateRepoCfgPath").textContent = repo.configPath || "";
4504
4599
  gateSaved.repo = repo.text || "";
4505
4600
  $("gateRepoCfg").value = gateSaved.repo;
4506
4601
  syncGateSave("repo");
@@ -4590,6 +4685,7 @@ export {
4590
4685
  await loadGate();
4591
4686
  }
4592
4687
  $("gateRefresh").addEventListener("click", () => loadGate());
4688
+ $("gateInfo").addEventListener("click", openGateInfo);
4593
4689
  $("gateCfg").addEventListener("input", () => syncGateSave("global"));
4594
4690
  $("gateRepoCfg").addEventListener("input", () => syncGateSave("repo"));
4595
4691
  $("gateCfgSave").addEventListener("click", () => saveGateCfg("global"));
@@ -5394,7 +5490,6 @@ export {
5394
5490
  // The button is an icon, so the toast is not the only confirmation: swap the icon for a
5395
5491
  // check on the control itself, then restore the captured original markup.
5396
5492
  const copyIcon = copyBtn.innerHTML;
5397
- const CHECK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
5398
5493
  let copyTimer = null;
5399
5494
  const copied = () => {
5400
5495
  toast.success("Copied curl");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.98",
3
+ "version": "0.1.99",
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": [