@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.
- package/assets/index.html +108 -13
- 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
|
|
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
|
|
1132
|
-
<div class="sub" style="margin-bottom:10px">The
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
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.
|
|
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": [
|