@enigmax/dashboard 0.1.102 → 0.1.104
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 +201 -74
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -517,6 +517,24 @@
|
|
|
517
517
|
full grid width while it is showing and its controls wrap instead of squeezing the label. */
|
|
518
518
|
#gateModelRow.wide { grid-column: 1 / -1; }
|
|
519
519
|
#gateModelRow .set-ctl { flex-wrap: wrap; justify-content: flex-end; }
|
|
520
|
+
/* A row whose control and hint both need the full width of the settings grid. */
|
|
521
|
+
.set-row.span { grid-column: 1 / -1; }
|
|
522
|
+
/* What a run is doing right now: the step in flight and the tail of its log. */
|
|
523
|
+
.gate-act { margin-top: 10px; border-left: 2px solid var(--accent); padding-left: 11px; }
|
|
524
|
+
.gate-act-h { display: flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 12px; color: var(--text); }
|
|
525
|
+
.gate-act-dot {
|
|
526
|
+
width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
|
|
527
|
+
flex: none; animation: gate-pulse 1.4s ease-in-out infinite;
|
|
528
|
+
}
|
|
529
|
+
@keyframes gate-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
|
|
530
|
+
@media (prefers-reduced-motion: reduce) { .gate-act-dot { animation: none; } }
|
|
531
|
+
.gate-act-log {
|
|
532
|
+
margin: 6px 0 0; padding: 8px 10px; max-height: 168px; overflow: auto; overscroll-behavior: contain;
|
|
533
|
+
background: var(--surface2); border-radius: 7px;
|
|
534
|
+
font-family: var(--mono); font-size: 11.5px; line-height: 1.5; color: var(--muted);
|
|
535
|
+
white-space: pre-wrap; overflow-wrap: anywhere;
|
|
536
|
+
}
|
|
537
|
+
.gate-act-empty { margin-top: 6px; font-size: 12px; color: var(--muted); }
|
|
520
538
|
.gate-run { padding: 13px 0; border-top: 1px solid var(--border-soft); }
|
|
521
539
|
.gate-run:first-child { border-top: 0; padding-top: 4px; }
|
|
522
540
|
.gate-run-h { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
@@ -1126,7 +1144,7 @@
|
|
|
1126
1144
|
<div class="set-grid">
|
|
1127
1145
|
<div class="set-row">
|
|
1128
1146
|
<div class="set-meta"><div class="set-name">Agent</div><div class="set-hint" id="gateAgentHint">which coding agent the pipeline drives</div></div>
|
|
1129
|
-
<div class="set-ctl ladd" style="align-items:center"><select id="gateAgent" class="linput"><option value="auto">
|
|
1147
|
+
<div class="set-ctl ladd" style="align-items:center"><select id="gateAgent" class="linput" data-dd-icons="agent"><option value="auto">Auto</option><option value="claude">Claude Code</option><option value="codex">Codex</option><option value="opencode">OpenCode</option><option value="rovodev">Rovo Dev</option><option value="pi">Pi</option></select></div>
|
|
1130
1148
|
</div>
|
|
1131
1149
|
<div class="set-row" id="gateModelRow">
|
|
1132
1150
|
<div class="set-meta"><div class="set-name">Model</div><div class="set-hint" id="gateModelHint">the model every step runs on; a smaller one cuts run time and tokens</div></div>
|
|
@@ -1145,21 +1163,19 @@
|
|
|
1145
1163
|
<div class="set-ctl ladd" style="align-items:center"><button id="gateIntent" type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" aria-label="Read intent from transcripts"></button></div>
|
|
1146
1164
|
</div>
|
|
1147
1165
|
</div>
|
|
1166
|
+
<div class="set-cat-h" style="margin-top:18px">Findings</div>
|
|
1167
|
+
<div class="sub" style="margin-bottom:10px">Who decides what happens to something the review turns up.</div>
|
|
1168
|
+
<div class="set-grid">
|
|
1169
|
+
<div class="set-row span">
|
|
1170
|
+
<div class="set-meta"><div class="set-name">When the gate finds something</div><div class="set-hint" id="gateFixHint"></div></div>
|
|
1171
|
+
<div class="set-ctl ladd" style="align-items:center"><select id="gateFixPolicy" class="linput" style="flex:none;width:300px"></select></div>
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1148
1174
|
<div class="set-cat-h" style="margin-top:18px">Auto-fix attempts</div>
|
|
1149
1175
|
<div class="sub" style="margin-bottom:10px">Follow-up rounds a step gets before it pauses for you. Every round is another full agent pass over the diff, so this is the setting that multiplies a run's cost. 0 pauses immediately.</div>
|
|
1150
1176
|
<div class="set-grid" id="gateAutoFix"></div>
|
|
1151
1177
|
</div>
|
|
1152
1178
|
|
|
1153
|
-
<div class="panel">
|
|
1154
|
-
<div class="set-cat-h">Global configuration</div>
|
|
1155
|
-
<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>
|
|
1156
|
-
<textarea id="gateCfg" class="linput gate-yaml" spellcheck="false" autocapitalize="none" autocorrect="off" aria-label="Global gate configuration"></textarea>
|
|
1157
|
-
<div class="ladd" style="gap:8px;align-items:center">
|
|
1158
|
-
<button id="gateCfgSave" type="button" class="toggle on" disabled title="No changes to save">Save</button>
|
|
1159
|
-
<span id="gateCfgNote" class="set-hint"></span>
|
|
1160
|
-
</div>
|
|
1161
|
-
</div>
|
|
1162
|
-
|
|
1163
1179
|
<div class="panel" id="gateRepoPanel" style="display:none">
|
|
1164
1180
|
<div class="set-cat-h">Repository configuration</div>
|
|
1165
1181
|
<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>
|
|
@@ -3821,7 +3837,7 @@ export {
|
|
|
3821
3837
|
+ item("Minimal code", levelPill(s.minimalCode))
|
|
3822
3838
|
+ item("Parallel sub-agents", boolPill(s.parallelSubagents))
|
|
3823
3839
|
+ item("Auto-lint on edit", boolPill(s.autoLint))
|
|
3824
|
-
+ item("Convention guardrails", s.guardrails ? boolPill(s.guardrails.on) + (s.guardrails.on ? ' <span class="tag">' + s.guardrails.rules + " rules</span>" : "") : boolPill(false))
|
|
3840
|
+
+ item("Convention guardrails", s.guardrails ? boolPill(s.guardrails.on) + (s.guardrails.on ? ' <span class="tag">' + s.guardrails.rules + " rules</span>" + (s.guardrails.findings ? ' <span class="tag" title="Findings the agent ran into over the last 7 days. See: enigma guardrails stats">' + s.guardrails.findings + " findings, 7d</span>" : "") : "") : boolPill(false))
|
|
3825
3841
|
+ item("Trim blank line at end of file", boolPill(s.trim))
|
|
3826
3842
|
+ item("Verify completion claims", s.verify ? boolPill(s.verify.on) + (s.verify.on && s.verify.command ? ' <span class="tag">' + esc(s.verify.command) + "</span>" : "") : boolPill(false))
|
|
3827
3843
|
+ item("Commit emoji", boolPill(s.commitEmoji))
|
|
@@ -3925,14 +3941,34 @@ export {
|
|
|
3925
3941
|
claude: '<svg viewBox="0 0 100 100" fill="currentColor" aria-hidden="true"><path d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"/></svg>',
|
|
3926
3942
|
codex: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>',
|
|
3927
3943
|
opencode: '<svg viewBox="0 0 240 300" fill="currentColor" aria-hidden="true"><path d="M180 240H60V120H180V240Z" opacity=".55"/><path fill-rule="evenodd" d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z"/></svg>',
|
|
3944
|
+
// Rovo Dev is Atlassian's agent (it is driven through `acli rovodev`), so it carries
|
|
3945
|
+
// the Atlassian mark - there is no separate Rovo Dev glyph to use.
|
|
3946
|
+
rovodev: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.12 11.084a.683.683 0 00-1.16.126L.075 22.974a.703.703 0 00.63 1.018h8.19a.678.678 0 00.63-.39c1.767-3.65.696-9.203-2.406-12.52zM11.434.386a15.515 15.515 0 00-.906 15.317l3.95 7.9a.703.703 0 00.628.388h8.19a.703.703 0 00.63-1.017L12.63.38a.664.664 0 00-1.196.006z"/></svg>',
|
|
3947
|
+
// Pi's own mark (pi.dev), already monochrome upstream - it ships a light/dark pair,
|
|
3948
|
+
// which is exactly what currentColor gives us here.
|
|
3949
|
+
pi: '<svg viewBox="0 0 800 800" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/><path d="M517.36 400H634.72V634.72H517.36Z"/></svg>',
|
|
3950
|
+
};
|
|
3951
|
+
// Agent id -> the name a person would write. A selector must never show the raw id: the
|
|
3952
|
+
// config stores `opencode` and `rovodev`, the reader sees OpenCode and Rovo Dev.
|
|
3953
|
+
const AGENT_LABELS = {
|
|
3954
|
+
auto: "Auto", claude: "Claude Code", codex: "Codex",
|
|
3955
|
+
opencode: "OpenCode", rovodev: "Rovo Dev", pi: "Pi"
|
|
3928
3956
|
};
|
|
3929
|
-
// App label -> internal name. Normalized
|
|
3930
|
-
//
|
|
3957
|
+
// App label -> internal name. Normalized so every label variant maps to an icon:
|
|
3958
|
+
// "Claude Code", "Codex"/"OpenAI Codex", "opencode"/"OpenCode", "Rovo Dev", "Pi".
|
|
3959
|
+
// Ordered longest-distinctive-first, and `pi` is matched exactly rather than as a
|
|
3960
|
+
// substring, which would otherwise claim anything containing those two letters.
|
|
3931
3961
|
const agentName = (label) => {
|
|
3932
|
-
const s = String(label || "").toLowerCase();
|
|
3933
|
-
|
|
3962
|
+
const s = String(label || "").trim().toLowerCase();
|
|
3963
|
+
if (s === "pi") return "pi";
|
|
3964
|
+
return s.includes("claude") ? "claude"
|
|
3965
|
+
: s.includes("opencode") ? "opencode"
|
|
3966
|
+
: s.includes("codex") ? "codex"
|
|
3967
|
+
: s.includes("rovo") ? "rovodev" : "";
|
|
3934
3968
|
};
|
|
3935
3969
|
const agentSvg = (name) => AGENT_ICONS[name] || "";
|
|
3970
|
+
/** The display name for an agent id, falling back to the id when it is not one we know. */
|
|
3971
|
+
const agentLabel = (name) => AGENT_LABELS[agentName(name) || name] || String(name || "");
|
|
3936
3972
|
|
|
3937
3973
|
// Compression sources -> a brand icon. Reuses the app icons; the CLI source is enigma itself
|
|
3938
3974
|
// (the monochrome cube mark, recoloured to currentColor so it sits uniformly in the tables).
|
|
@@ -4389,10 +4425,17 @@ export {
|
|
|
4389
4425
|
}
|
|
4390
4426
|
|
|
4391
4427
|
// --- quality gate subpage ---------------------------------------------------
|
|
4392
|
-
// Two halves with independent availability: the
|
|
4393
|
-
//
|
|
4394
|
-
//
|
|
4395
|
-
|
|
4428
|
+
// Two halves with independent availability: the config is a plain file and always
|
|
4429
|
+
// renders, while the run history needs enigma's own binary (the gate database is
|
|
4430
|
+
// Bun-only), so an unavailable history reports itself instead of blanking the page.
|
|
4431
|
+
//
|
|
4432
|
+
// The GLOBAL config has no raw YAML editor: every setting it holds has a control in
|
|
4433
|
+
// the panel above, and a textarea of YAML is not a setting anyone should have to edit
|
|
4434
|
+
// to change how the gate behaves. The keys with no control (agent path overrides, ACP
|
|
4435
|
+
// targets, evidence) stay in the file, whose path the info dialog gives you. The
|
|
4436
|
+
// REPOSITORY config keeps its editor - it is committed with the repo, carries keys the
|
|
4437
|
+
// global one does not, and has no structured equivalent yet.
|
|
4438
|
+
let gateRepoSaved = "";
|
|
4396
4439
|
let gateWritable = true;
|
|
4397
4440
|
// Where the gate keeps its files, kept for the info dialog instead of printed in the headers.
|
|
4398
4441
|
let gateWhere = { global: "", repo: "", root: "", note: "" };
|
|
@@ -4401,7 +4444,7 @@ export {
|
|
|
4401
4444
|
function openGateInfo() {
|
|
4402
4445
|
openInfoModal({
|
|
4403
4446
|
title: "Gate configuration",
|
|
4404
|
-
intro: "Where the gate reads its settings and keeps its run history.",
|
|
4447
|
+
intro: "Where the gate reads its settings and keeps its run history. Everything on this page writes to the global config; edit the file directly only for the advanced keys with no control here.",
|
|
4405
4448
|
rows: [
|
|
4406
4449
|
{ label: "Global config", value: gateWhere.global, hint: "applies to every repository" },
|
|
4407
4450
|
{ label: "Repository config", value: gateWhere.repo, hint: "committed with the repository, merged over the global config" },
|
|
@@ -4414,15 +4457,9 @@ export {
|
|
|
4414
4457
|
],
|
|
4415
4458
|
});
|
|
4416
4459
|
}
|
|
4417
|
-
function gateEls(which) {
|
|
4418
|
-
return which === "global"
|
|
4419
|
-
? { area: $("gateCfg"), btn: $("gateCfgSave"), note: "gateCfgNote" }
|
|
4420
|
-
: { area: $("gateRepoCfg"), btn: $("gateRepoCfgSave"), note: "gateRepoCfgNote" };
|
|
4421
|
-
}
|
|
4422
4460
|
// Dirty means different from what was loaded, so an edit typed and undone disables Save again.
|
|
4423
|
-
function syncGateSave(
|
|
4424
|
-
const
|
|
4425
|
-
const dirty = area.value !== gateSaved[which];
|
|
4461
|
+
function syncGateSave() {
|
|
4462
|
+
const btn = $("gateRepoCfgSave"), dirty = $("gateRepoCfg").value !== gateRepoSaved;
|
|
4426
4463
|
btn.disabled = !dirty || !gateWritable;
|
|
4427
4464
|
btn.title = !gateWritable ? "Read-only in this runtime" : dirty ? "Save this configuration" : "No changes to save";
|
|
4428
4465
|
}
|
|
@@ -4452,6 +4489,29 @@ export {
|
|
|
4452
4489
|
+ (s.findings ? " · " + s.findings : "") + "</span>";
|
|
4453
4490
|
}).join("");
|
|
4454
4491
|
}
|
|
4492
|
+
// What the run is doing, in the words the status codes stand for. A step name alone
|
|
4493
|
+
// does not say whether the pipeline is working or waiting, and that is the difference
|
|
4494
|
+
// between "leave it alone" and "it needs you".
|
|
4495
|
+
const GATE_ACT_VERB = {
|
|
4496
|
+
running: "Running", fixing: "Fixing findings in",
|
|
4497
|
+
awaiting_approval: "Waiting for a decision on", fix_review: "Waiting on the fix review of"
|
|
4498
|
+
};
|
|
4499
|
+
/** The live header of a run's activity: what it is on and for how long. */
|
|
4500
|
+
function gateActHead(a) {
|
|
4501
|
+
const el = gateStepDur({ status: a.status, startedAt: a.startedAt, durationMs: null });
|
|
4502
|
+
return '<span class="gate-act-dot"></span>' + esc((GATE_ACT_VERB[a.status] || "Running") + " " + a.step)
|
|
4503
|
+
+ (el ? " · " + esc(el) : "");
|
|
4504
|
+
}
|
|
4505
|
+
// The tail of the step's log, which is the only place a run says what it is actually
|
|
4506
|
+
// doing. Refreshed by the poll, not the 1s tick: the file only changes server-side.
|
|
4507
|
+
function gateActivityHtml(a) {
|
|
4508
|
+
if (!a) return "";
|
|
4509
|
+
const tail = (a.tail || []).length
|
|
4510
|
+
? '<pre class="gate-act-log">' + esc((a.tail || []).join("\n")) + "</pre>"
|
|
4511
|
+
: '<div class="gate-act-empty">No output from this step yet.</div>';
|
|
4512
|
+
return '<div class="gate-act"><div class="gate-act-h">' + gateActHead(a) + "</div>" + tail + "</div>";
|
|
4513
|
+
}
|
|
4514
|
+
const STOP_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="6" y="6" width="12" height="12" rx="2"/></svg>';
|
|
4455
4515
|
function gateRunRow(r) {
|
|
4456
4516
|
const pr = r.prUrl ? ' <a href="' + esc(r.prUrl) + '" target="_blank" rel="noopener noreferrer">PR</a>' : "";
|
|
4457
4517
|
const steps = gateStepsHtml(r.steps);
|
|
@@ -4461,11 +4521,19 @@ export {
|
|
|
4461
4521
|
+ (f.file ? " · " + esc(f.file) : "") + "<br>" + esc(f.description) + "</div>").join("") + "</div>"
|
|
4462
4522
|
: "";
|
|
4463
4523
|
const err = r.error ? '<div class="gate-find-row" style="color:var(--bad)">' + esc(r.error) + "</div>" : "";
|
|
4524
|
+
// Only a run that can still be stopped gets the control; a finished one would
|
|
4525
|
+
// offer an action that does nothing.
|
|
4526
|
+
const stop = r.live
|
|
4527
|
+
? '<button type="button" class="iconbtn" data-gate-stop="' + esc(r.id) + '" data-gate-branch="' + esc(r.branch) + '"'
|
|
4528
|
+
+ ' title="Stop this run" aria-label="Stop the run on ' + esc(r.branch) + '">' + STOP_ICON + "</button>"
|
|
4529
|
+
: "";
|
|
4464
4530
|
return '<div class="gate-run"><div class="gate-run-h">'
|
|
4465
4531
|
+ '<span class="gate-run-b">' + esc(r.branch) + "</span>"
|
|
4466
4532
|
+ '<span class="cstatus ' + (r.status === "completed" ? "ok" : r.status === "running" ? "orange" : r.status === "failed" ? "bad" : "") + '"><span class="cdot"></span>' + esc(r.status) + (r.awaitingAgent ? " · parked" : "") + "</span>"
|
|
4467
4533
|
+ '<span class="gate-run-meta">' + esc(r.headSha) + pr + "</span>"
|
|
4468
|
-
+
|
|
4534
|
+
+ '<span style="flex:1"></span>' + stop
|
|
4535
|
+
+ "</div>" + '<div class="gate-steps">' + steps + "</div>"
|
|
4536
|
+
+ gateActivityHtml(r.activity) + findings + err + "</div>";
|
|
4469
4537
|
}
|
|
4470
4538
|
// The two on/off switches at the top of the page. Both are optimistic: the switch flips
|
|
4471
4539
|
// first and rolls back if the server refuses, like every other control here.
|
|
@@ -4504,6 +4572,7 @@ export {
|
|
|
4504
4572
|
function gateCtlFor(key) {
|
|
4505
4573
|
if (key.indexOf("auto_fix.") === 0) return document.querySelector('[data-gate-af="' + key.slice("auto_fix.".length) + '"]');
|
|
4506
4574
|
if (key === "agent") return $("gateAgent");
|
|
4575
|
+
if (key === "fix_policy") return $("gateFixPolicy");
|
|
4507
4576
|
if (key === "log_level") return $("gateLogLevel");
|
|
4508
4577
|
if (key === "ci_timeout") return $("gateCiTimeout");
|
|
4509
4578
|
if (key === "intent.enabled") return $("gateIntent");
|
|
@@ -4594,12 +4663,36 @@ export {
|
|
|
4594
4663
|
btn.title = dirty ? "" : "No changes to save";
|
|
4595
4664
|
}
|
|
4596
4665
|
|
|
4666
|
+
// Who settles a finding the pipeline parks on. The middle option is the default and the
|
|
4667
|
+
// one worth reading: it is what makes the run stop only for a decision a person has to
|
|
4668
|
+
// make, instead of for every mechanical nit.
|
|
4669
|
+
const GATE_FIX_POLICIES = [
|
|
4670
|
+
["auto", "Fix without asking", "the agent fixes everything the review turns up and never checks back"],
|
|
4671
|
+
["assisted", "Fix, and ask about the rest", "the agent fixes what the review marked mechanical and asks you only about a finding that needs a judgment call"],
|
|
4672
|
+
["ask", "Always ask", "nothing is fixed until you say so - every finding comes back to you first"]
|
|
4673
|
+
];
|
|
4674
|
+
/** The hint follows the selection: each policy hands the decision to someone different. */
|
|
4675
|
+
function syncGateFixHint() {
|
|
4676
|
+
const cur = $("gateFixPolicy").value;
|
|
4677
|
+
const row = GATE_FIX_POLICIES.find(([v]) => v === cur);
|
|
4678
|
+
const def = gateDefaults && gateDefaults.fixPolicy;
|
|
4679
|
+
const suffix = def && def === cur ? ' <span class="set-def">default</span>' : "";
|
|
4680
|
+
$("gateFixHint").innerHTML = esc(row ? row[2] : "") + suffix;
|
|
4681
|
+
}
|
|
4682
|
+
function renderGateFixPolicy(value) {
|
|
4683
|
+
const sel = $("gateFixPolicy");
|
|
4684
|
+
sel.innerHTML = GATE_FIX_POLICIES.map(([v, label]) => '<option value="' + esc(v) + '">' + esc(label) + "</option>").join("");
|
|
4685
|
+
sel.value = GATE_FIX_POLICIES.some(([v]) => v === value) ? value : "assisted";
|
|
4686
|
+
syncDD(sel);
|
|
4687
|
+
syncGateFixHint();
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4597
4690
|
function renderGateSettings(s) {
|
|
4598
4691
|
const panel = $("gateSetPanel");
|
|
4599
4692
|
if (!s) { panel.style.display = "none"; return; }
|
|
4600
4693
|
panel.style.display = "";
|
|
4601
|
-
// An `acp:<target>` agent set
|
|
4602
|
-
// carry it as one so the select shows the truth instead of going blank.
|
|
4694
|
+
// An `acp:<target>` agent set in the config file by hand is not one of the fixed
|
|
4695
|
+
// options; carry it as one so the select shows the truth instead of going blank.
|
|
4603
4696
|
const agentSel = $("gateAgent");
|
|
4604
4697
|
if (!Array.prototype.some.call(agentSel.options, (o) => o.value === s.agent)) {
|
|
4605
4698
|
agentSel.add(new Option(s.agent, s.agent));
|
|
@@ -4616,6 +4709,7 @@ export {
|
|
|
4616
4709
|
syncDD($("gateLogLevel"));
|
|
4617
4710
|
gateBool($("gateIntent"), s.intentEnabled);
|
|
4618
4711
|
gateDefaults = s.defaults || null;
|
|
4712
|
+
renderGateFixPolicy(s.fixPolicy || "assisted"); // after gateDefaults: the hint reads it
|
|
4619
4713
|
$("gateAutoFix").innerHTML = GATE_AUTOFIX.map(([step, hint]) => {
|
|
4620
4714
|
const def = gateDefaults && gateDefaults.autoFix ? Number(gateDefaults.autoFix[step] || 0) : null;
|
|
4621
4715
|
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(step) + '</div><div class="set-hint">' + esc(hint)
|
|
@@ -4627,6 +4721,7 @@ export {
|
|
|
4627
4721
|
// because only the payload knows what each default actually is.
|
|
4628
4722
|
if (gateDefaults) {
|
|
4629
4723
|
mountGateReset("agent", "the agent", gateDefaults.agent);
|
|
4724
|
+
mountGateReset("fix_policy", "the findings policy", gateDefaults.fixPolicy);
|
|
4630
4725
|
mountGateReset("log_level", "the log level", gateDefaults.logLevel);
|
|
4631
4726
|
mountGateReset("ci_timeout", "the CI timeout", gateDefaults.ciTimeout);
|
|
4632
4727
|
mountGateReset("intent.enabled", "intent extraction", String(gateDefaults.intentEnabled));
|
|
@@ -4667,31 +4762,27 @@ export {
|
|
|
4667
4762
|
root: g.root || "",
|
|
4668
4763
|
note: g.canWrite === false ? (g.writeNote || "") : "",
|
|
4669
4764
|
};
|
|
4670
|
-
gateSaved.global = (g.globalConfig || {}).text || "";
|
|
4671
|
-
$("gateCfg").value = gateSaved.global;
|
|
4672
4765
|
// A runtime that cannot parse a gate config must not write one: read-only, with the reason.
|
|
4673
4766
|
gateWritable = g.canWrite !== false;
|
|
4674
|
-
$("gateCfg").readOnly = !gateWritable;
|
|
4675
4767
|
$("gateRepoCfg").readOnly = !gateWritable;
|
|
4676
|
-
if (!gateWritable)
|
|
4677
|
-
syncGateSave("global");
|
|
4768
|
+
if (!gateWritable) setNote("gateNote", g.writeNote || "Read-only here.", "err");
|
|
4678
4769
|
renderGateSettings(g.settings);
|
|
4679
4770
|
$("gateRepoPanel").style.display = repo ? "" : "none";
|
|
4680
4771
|
if (repo) {
|
|
4681
|
-
|
|
4682
|
-
$("gateRepoCfg").value =
|
|
4683
|
-
syncGateSave(
|
|
4772
|
+
gateRepoSaved = repo.text || "";
|
|
4773
|
+
$("gateRepoCfg").value = gateRepoSaved;
|
|
4774
|
+
syncGateSave();
|
|
4684
4775
|
}
|
|
4685
4776
|
}
|
|
4686
4777
|
|
|
4687
4778
|
// --- keeping the run strip live -------------------------------------------------
|
|
4688
4779
|
// A gate run takes tens of minutes and the page has no push channel, so without this the
|
|
4689
|
-
//
|
|
4780
|
+
// view shows whatever was true when the page was opened. Two loops: a 3s refetch while
|
|
4690
4781
|
// something is in flight (20s when idle) and a 1s repaint so the active step's elapsed
|
|
4691
4782
|
// counts up between fetches. Both stop when the tab is hidden or the view is not open.
|
|
4692
4783
|
let gateRuns = [], gateFetchTimer = null, gateTickTimer = null;
|
|
4693
4784
|
function gateBusy() {
|
|
4694
|
-
return gateRuns.some((r) => r.
|
|
4785
|
+
return gateRuns.some((r) => r.live);
|
|
4695
4786
|
}
|
|
4696
4787
|
function gateStopPoll() {
|
|
4697
4788
|
clearTimeout(gateFetchTimer); gateFetchTimer = null;
|
|
@@ -4703,13 +4794,20 @@ export {
|
|
|
4703
4794
|
if (activeView !== "gate" || document.hidden) return;
|
|
4704
4795
|
const busy = gateBusy();
|
|
4705
4796
|
$("gateLive").textContent = busy ? "live" : "";
|
|
4706
|
-
gateFetchTimer = setTimeout(() => loadGate(true), busy ?
|
|
4797
|
+
gateFetchTimer = setTimeout(() => loadGate(true), busy ? 3000 : 20000);
|
|
4707
4798
|
if (!busy) return;
|
|
4708
|
-
// Repaint only the
|
|
4709
|
-
// selection inside
|
|
4799
|
+
// Repaint only the elapsed counters: replacing whole rows every second would drop a
|
|
4800
|
+
// text selection inside a parked run's findings or a scroll position in its log.
|
|
4710
4801
|
gateTickTimer = setInterval(() => {
|
|
4711
|
-
const
|
|
4712
|
-
|
|
4802
|
+
const rows = $("gateRuns").querySelectorAll(".gate-run");
|
|
4803
|
+
rows.forEach((row, i) => {
|
|
4804
|
+
const r = gateRuns[i];
|
|
4805
|
+
if (!r) return;
|
|
4806
|
+
const strip = row.querySelector(".gate-steps");
|
|
4807
|
+
if (strip) strip.innerHTML = gateStepsHtml(r.steps);
|
|
4808
|
+
const head = row.querySelector(".gate-act-h");
|
|
4809
|
+
if (head && r.activity) head.innerHTML = gateActHead(r.activity);
|
|
4810
|
+
});
|
|
4713
4811
|
}, 1000);
|
|
4714
4812
|
}
|
|
4715
4813
|
async function loadGate(live) {
|
|
@@ -4725,56 +4823,80 @@ export {
|
|
|
4725
4823
|
} catch { note("Could not reach the server."); }
|
|
4726
4824
|
}
|
|
4727
4825
|
document.addEventListener("visibilitychange", () => { if (activeView === "gate") { if (document.hidden) gateStopPoll(); else loadGate(true); } });
|
|
4728
|
-
async function
|
|
4729
|
-
const
|
|
4730
|
-
if (area.value ===
|
|
4731
|
-
setNote(
|
|
4732
|
-
const payload = { scope: which === "global" ? "global" : "repo", text: area.value };
|
|
4733
|
-
if (which === "repo") payload.path = currentScope;
|
|
4826
|
+
async function saveGateRepoCfg() {
|
|
4827
|
+
const area = $("gateRepoCfg");
|
|
4828
|
+
if (area.value === gateRepoSaved) return; // no-op: nothing changed
|
|
4829
|
+
setNote("gateRepoCfgNote", "Saving...");
|
|
4734
4830
|
try {
|
|
4735
|
-
const
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4831
|
+
const body = JSON.stringify({ scope: "repo", text: area.value, path: currentScope });
|
|
4832
|
+
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body })).json();
|
|
4833
|
+
setNote("gateRepoCfgNote", out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
|
|
4834
|
+
if (out.ok) { gateRepoSaved = area.value; syncGateSave(); }
|
|
4835
|
+
} catch { setNote("gateRepoCfgNote", "Could not reach the server.", "err"); }
|
|
4836
|
+
}
|
|
4837
|
+
/**
|
|
4838
|
+
* Stops an in-flight run. Destructive and not undoable - the pipeline drops whatever it
|
|
4839
|
+
* was doing - so it confirms first, naming the branch rather than the run id.
|
|
4840
|
+
*/
|
|
4841
|
+
async function stopGateRun(runId, branch) {
|
|
4842
|
+
const ok = await openModal({
|
|
4843
|
+
title: "Stop this run?",
|
|
4844
|
+
message: "The gate run on " + branch + " stops where it is. Nothing it already pushed is undone, and you can start a new run afterwards.",
|
|
4845
|
+
danger: true,
|
|
4846
|
+
okLabel: "Stop run"
|
|
4847
|
+
}).then(Boolean);
|
|
4848
|
+
if (!ok) return;
|
|
4849
|
+
setNote("gateNote", "Stopping the run...");
|
|
4850
|
+
try {
|
|
4851
|
+
const body = JSON.stringify({ action: "abort", run: runId });
|
|
4852
|
+
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body })).json();
|
|
4853
|
+
setNote("gateNote", out.message || (out.ok ? "Run stopped." : "Not stopped."), out.ok ? "ok" : "err");
|
|
4854
|
+
} catch { setNote("gateNote", "Could not reach the server.", "err"); }
|
|
4855
|
+
await loadGate(true);
|
|
4739
4856
|
}
|
|
4740
4857
|
/** One form control -> one key of the global config. Reloads so the file stays the truth. */
|
|
4741
4858
|
async function saveGateSetting(key, value, label) {
|
|
4742
|
-
setNote("
|
|
4859
|
+
setNote("gateNote", "Saving " + label + "...");
|
|
4743
4860
|
try {
|
|
4744
4861
|
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "setting", key, value }) })).json();
|
|
4745
|
-
setNote("
|
|
4862
|
+
setNote("gateNote", out.message || (out.ok ? "Saved." : "Not saved."), out.ok ? "ok" : "err");
|
|
4746
4863
|
await loadGate();
|
|
4747
|
-
} catch { setNote("
|
|
4864
|
+
} catch { setNote("gateNote", "Could not reach the server.", "err"); await loadGate(); }
|
|
4748
4865
|
}
|
|
4749
4866
|
async function gateDaemonPost(on) {
|
|
4750
4867
|
gateBool($("gateDaemonBtn"), on); // optimistic; the reload below is the truth
|
|
4751
|
-
setNote("
|
|
4868
|
+
setNote("gateNote", on ? "Starting the daemon..." : "Stopping the daemon...");
|
|
4752
4869
|
try {
|
|
4753
4870
|
const out = await (await fetch("/api/gate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "daemon", on }) })).json();
|
|
4754
|
-
setNote("
|
|
4755
|
-
} catch { setNote("
|
|
4871
|
+
setNote("gateNote", out.message || (out.ok ? "Done." : "Failed."), out.ok ? "ok" : "err");
|
|
4872
|
+
} catch { setNote("gateNote", "Could not reach the server.", "err"); }
|
|
4756
4873
|
await loadGate();
|
|
4757
4874
|
}
|
|
4758
4875
|
// The gate on/off flag is a normal enigma setting, so it goes through the settings route
|
|
4759
4876
|
// the Settings page uses rather than growing a second writer for the same key.
|
|
4760
4877
|
async function gateOnPost(on) {
|
|
4761
4878
|
gateBool($("gateOnBtn"), on);
|
|
4762
|
-
setNote("
|
|
4879
|
+
setNote("gateNote", on ? "Turning the gate on..." : "Turning the gate off...");
|
|
4763
4880
|
try {
|
|
4764
4881
|
const out = await (await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "gate", value: on }) })).json();
|
|
4765
|
-
setNote("
|
|
4766
|
-
} catch { setNote("
|
|
4882
|
+
setNote("gateNote", out.ok ? (out.restartNote || "Saved. Restart your agent to apply.") : ("Could not change the gate: " + (out.error || "error")), out.ok ? "warn" : "err");
|
|
4883
|
+
} catch { setNote("gateNote", "Could not reach the server.", "err"); }
|
|
4767
4884
|
await loadGate();
|
|
4768
4885
|
}
|
|
4769
4886
|
$("gateRefresh").addEventListener("click", () => loadGate());
|
|
4770
4887
|
$("gateInfo").addEventListener("click", openGateInfo);
|
|
4771
|
-
$("
|
|
4772
|
-
$("
|
|
4773
|
-
$("gateCfgSave").addEventListener("click", () => saveGateCfg("global"));
|
|
4774
|
-
$("gateRepoCfgSave").addEventListener("click", () => saveGateCfg("repo"));
|
|
4888
|
+
$("gateRepoCfg").addEventListener("input", syncGateSave);
|
|
4889
|
+
$("gateRepoCfgSave").addEventListener("click", saveGateRepoCfg);
|
|
4775
4890
|
$("gateOnBtn").addEventListener("click", () => gateOnPost($("gateOnBtn").dataset.on !== "1"));
|
|
4776
4891
|
$("gateDaemonBtn").addEventListener("click", () => gateDaemonPost($("gateDaemonBtn").dataset.on !== "1"));
|
|
4892
|
+
// Delegated: the run rows are rebuilt on every poll, so a per-button listener would be
|
|
4893
|
+
// rebound (or lost) three times a minute.
|
|
4894
|
+
$("gateRuns").addEventListener("click", (e) => {
|
|
4895
|
+
const b = e.target.closest("[data-gate-stop]");
|
|
4896
|
+
if (b) stopGateRun(b.dataset.gateStop, b.dataset.gateBranch);
|
|
4897
|
+
});
|
|
4777
4898
|
$("gateAgent").addEventListener("change", (e) => saveGateSetting("agent", e.target.value, "the agent"));
|
|
4899
|
+
$("gateFixPolicy").addEventListener("change", (e) => { syncGateFixHint(); saveGateSetting("fix_policy", e.target.value, "the findings policy"); });
|
|
4778
4900
|
$("gateLogLevel").addEventListener("change", (e) => saveGateSetting("log_level", e.target.value, "the log level"));
|
|
4779
4901
|
$("gateIntent").addEventListener("click", () => saveGateSetting("intent.enabled", $("gateIntent").dataset.on !== "1", "intent extraction"));
|
|
4780
4902
|
// A listed model commits on pick, like the agent and log-level selects; "Custom" reveals
|
|
@@ -4806,6 +4928,7 @@ export {
|
|
|
4806
4928
|
if (el) {
|
|
4807
4929
|
if (key === "intent.enabled") gateBool(el, def === "true");
|
|
4808
4930
|
else { el.value = def; if (el.tagName === "SELECT") syncDD(el); }
|
|
4931
|
+
if (key === "fix_policy") syncGateFixHint(); // the hint follows the selection
|
|
4809
4932
|
syncGateResets();
|
|
4810
4933
|
}
|
|
4811
4934
|
const value = key.indexOf("auto_fix.") === 0 ? Number(def) : (key === "intent.enabled" ? def === "true" : def);
|
|
@@ -5485,16 +5608,20 @@ export {
|
|
|
5485
5608
|
async function loadPlayground() {
|
|
5486
5609
|
let info = { agents: [], models: [], apiPort: 8000 };
|
|
5487
5610
|
try { info = await (await fetch("/api/playground", { cache: "no-store" })).json(); } catch { /* keep defaults */ }
|
|
5488
|
-
// Populate the backend/model dropdown from the installed agents' model lists.
|
|
5611
|
+
// Populate the backend/model dropdown from the installed agents' model lists. The
|
|
5612
|
+
// option VALUE is the model id, so the agent logo has to come from the owning tool
|
|
5613
|
+
// via data-icon - `data-dd-icons="agent"` alone would look the id up and find nothing.
|
|
5489
5614
|
const sel = $("pgModel");
|
|
5490
5615
|
const opts = [];
|
|
5491
|
-
(info.models || []).forEach((m) => (m.models || []).forEach((id) => opts.push(
|
|
5492
|
-
|
|
5616
|
+
(info.models || []).forEach((m) => (m.models || []).forEach((id) => opts.push(
|
|
5617
|
+
'<option value="' + esc(id) + '" data-icon="' + esc(agentName(m.tool)) + '">' + esc(id) + " (" + esc(agentLabel(m.tool)) + ")</option>")));
|
|
5618
|
+
if (!opts.length) opts.push('<option value="claude" data-icon="claude">Claude Code (default)</option>');
|
|
5493
5619
|
sel.innerHTML = opts.join("");
|
|
5494
5620
|
syncDD(sel);
|
|
5495
5621
|
// Context selectors: account (labeled by tool), profile, pack.
|
|
5496
5622
|
const acc = $("pgAccount");
|
|
5497
|
-
acc.innerHTML = '<option value="">(active)</option>' + (info.accounts || []).map((a) =>
|
|
5623
|
+
acc.innerHTML = '<option value="">(active)</option>' + (info.accounts || []).map((a) =>
|
|
5624
|
+
'<option value="' + esc(a.name) + '" data-icon="' + esc(agentName(a.tool)) + '">' + esc(a.name) + " (" + esc(agentLabel(a.tool)) + ")</option>").join("");
|
|
5498
5625
|
syncDD(acc);
|
|
5499
5626
|
const prof = $("pgProfile");
|
|
5500
5627
|
prof.innerHTML = '<option value="">(none)</option>' + (info.profiles || []).map((n) => '<option value="' + esc(n) + '">' + esc(n) + "</option>").join("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.104",
|
|
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": [
|