@enigmax/dashboard 0.1.43 → 0.1.45
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 +220 -19
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -354,6 +354,19 @@
|
|
|
354
354
|
/* Usage view provider/account pickers. */
|
|
355
355
|
.useg-pick { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
|
|
356
356
|
.useg-pick select { width: auto; min-width: 170px; }
|
|
357
|
+
/* Icon dropdown: a native <select> can't render icons in its <option>s, so the provider
|
|
358
|
+
picker is a custom listbox - the logo shows on the button AND inside every option. */
|
|
359
|
+
.dd { position: relative; }
|
|
360
|
+
.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; }
|
|
361
|
+
.dd-btn:hover { border-color: #2f3744; }
|
|
362
|
+
.dd-btn .dd-label { flex: 1; text-align: left; }
|
|
363
|
+
.dd-btn .dd-caret { width: 13px; height: 13px; flex: 0 0 auto; color: var(--muted); }
|
|
364
|
+
.dd-ic { display: inline-flex; align-items: center; }
|
|
365
|
+
.dd-ic svg { width: 14px; height: 14px; display: block; }
|
|
366
|
+
.dd-menu { position: absolute; top: calc(100% + 4px); left: 0; min-width: 100%; margin: 0; padding: 4px; list-style: none; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow-2); z-index: 50; }
|
|
367
|
+
.dd-opt { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; font-size: 12px; color: var(--text); white-space: nowrap; cursor: pointer; }
|
|
368
|
+
.dd-opt:hover, .dd-opt:focus { background: var(--surface2); outline: none; }
|
|
369
|
+
.dd-opt.sel { color: var(--accent); }
|
|
357
370
|
/* Icon picker: clickable app cards (a native <select> can't render SVG in options). */
|
|
358
371
|
.modal-picker { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
|
|
359
372
|
.pick-card {
|
|
@@ -384,6 +397,7 @@
|
|
|
384
397
|
<a href="#/usage" data-view="usage" class="tab">Usage</a>
|
|
385
398
|
<a href="#/accounts" data-view="accounts" class="tab">Accounts</a>
|
|
386
399
|
<a href="#/skills" data-view="skills" class="tab">Skills</a>
|
|
400
|
+
<a href="#/projects" data-view="projects" class="tab">Projects</a>
|
|
387
401
|
<a href="#/settings" data-view="settings" class="tab">Settings</a>
|
|
388
402
|
<a href="#/resources" data-view="resources" class="tab">Resources</a>
|
|
389
403
|
</nav>
|
|
@@ -507,7 +521,12 @@
|
|
|
507
521
|
</div>
|
|
508
522
|
</div>
|
|
509
523
|
<div class="useg-controls" style="display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-bottom:16px">
|
|
510
|
-
<
|
|
524
|
+
<div class="useg-pick"><span>Provider</span>
|
|
525
|
+
<div class="dd" id="uProviderDD">
|
|
526
|
+
<button type="button" class="dd-btn" aria-haspopup="listbox" aria-expanded="false"><span class="dd-ic"></span><span class="dd-label"></span><svg class="dd-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg></button>
|
|
527
|
+
<ul class="dd-menu" role="listbox" aria-label="Provider" hidden></ul>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
511
530
|
<label class="useg-pick" id="uAccountWrap"><span>Account</span><select id="uAccount" class="modal-input"></select></label>
|
|
512
531
|
</div>
|
|
513
532
|
<div class="panel" id="usagePanel" style="display:none">
|
|
@@ -645,6 +664,23 @@
|
|
|
645
664
|
<div class="panel"><div class="set-cat-h">Processes <small>by memory</small></div><div id="resProcs"><div class="empty" style="padding:12px 0">Loading...</div></div></div>
|
|
646
665
|
</section>
|
|
647
666
|
|
|
667
|
+
<section id="view-projects" style="display:none">
|
|
668
|
+
<div class="page-head" style="display:flex;align-items:flex-start;gap:16px">
|
|
669
|
+
<div style="flex:1">
|
|
670
|
+
<h1>Projects</h1>
|
|
671
|
+
<p>Manage enigma per project folder: its deployed skills, project-local config, git hooks and the AI quality gate. Add a project by path; changes apply only to that project. Global config stays in <a href="#/settings">Settings</a>.</p>
|
|
672
|
+
</div>
|
|
673
|
+
<button id="projRefresh" type="button" class="iconbtn" title="Refresh" aria-label="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
|
|
674
|
+
</div>
|
|
675
|
+
<div id="projNote" class="set-note"></div>
|
|
676
|
+
<div class="panel">
|
|
677
|
+
<div class="set-cat-h">Add a project</div>
|
|
678
|
+
<div class="ladd"><input id="projPath" class="linput" type="text" placeholder="absolute path to a project folder"><input id="projLabel" class="linput" type="text" style="flex:none;width:160px" placeholder="label (optional)"><button id="projAdd" type="button" class="toggle on">Add</button></div>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="panel"><div class="set-cat-h">Projects</div><div id="projList"><div class="empty" style="padding:12px 0">Loading...</div></div></div>
|
|
681
|
+
<div id="projDetail" class="panel" style="display:none"></div>
|
|
682
|
+
</section>
|
|
683
|
+
|
|
648
684
|
<footer>
|
|
649
685
|
Polling pauses automatically when this tab is hidden, and the server caches each snapshot - so an open dashboard costs almost nothing.
|
|
650
686
|
Data source: <code>~/.enigma/ccr/stats.json</code> + <code>history.jsonl</code> - reset it with <code>enigma compress --clear</code>. Enigma <span id="ver"></span>
|
|
@@ -1886,12 +1922,44 @@ export {
|
|
|
1886
1922
|
try { await fetch("/api/plan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: Number(value) }) }); refreshAll(true); }
|
|
1887
1923
|
catch { /* leave as-is */ }
|
|
1888
1924
|
}
|
|
1925
|
+
// Accessible icon dropdown (a native <select> can't render icons in its <option>s). The
|
|
1926
|
+
// button shows the selected entry's icon + label; the listbox shows every entry with its
|
|
1927
|
+
// icon. entries: [{ value, label, icon }]; getVal()/onPick(value) bind it to state.
|
|
1928
|
+
function iconDropdown(rootId, entries, getVal, onPick) {
|
|
1929
|
+
const root = $(rootId), btn = root.querySelector(".dd-btn"), menu = root.querySelector(".dd-menu");
|
|
1930
|
+
const btnIc = btn.querySelector(".dd-ic"), btnLabel = btn.querySelector(".dd-label");
|
|
1931
|
+
const opts = () => [...menu.querySelectorAll(".dd-opt")];
|
|
1932
|
+
const render = () => {
|
|
1933
|
+
const cur = getVal(), e = entries.find((x) => x.value === cur) || entries[0];
|
|
1934
|
+
btnIc.innerHTML = e.icon || ""; btnLabel.textContent = e.label;
|
|
1935
|
+
menu.innerHTML = entries.map((x) => '<li class="dd-opt' + (x.value === cur ? " sel" : "")
|
|
1936
|
+
+ '" role="option" tabindex="-1" data-val="' + esc(x.value) + '" aria-selected="' + (x.value === cur)
|
|
1937
|
+
+ '"><span class="dd-ic">' + (x.icon || "") + "</span><span>" + esc(x.label) + "</span></li>").join("");
|
|
1938
|
+
};
|
|
1939
|
+
const close = () => { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); };
|
|
1940
|
+
const open = () => { menu.hidden = false; btn.setAttribute("aria-expanded", "true"); (menu.querySelector(".sel") || opts()[0]) && (menu.querySelector(".sel") || opts()[0]).focus(); };
|
|
1941
|
+
const pick = (li) => { close(); btn.focus(); if (li && li.dataset.val) onPick(li.dataset.val); };
|
|
1942
|
+
btn.addEventListener("click", (e) => { e.stopPropagation(); menu.hidden ? open() : close(); });
|
|
1943
|
+
btn.addEventListener("keydown", (e) => { if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); } });
|
|
1944
|
+
menu.addEventListener("click", (e) => pick(e.target.closest(".dd-opt")));
|
|
1945
|
+
menu.addEventListener("keydown", (e) => {
|
|
1946
|
+
const list = opts(), i = list.indexOf(document.activeElement);
|
|
1947
|
+
if (e.key === "Escape") { e.preventDefault(); close(); btn.focus(); }
|
|
1948
|
+
else if (e.key === "ArrowDown") { e.preventDefault(); (list[i + 1] || list[0]).focus(); }
|
|
1949
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); (list[i - 1] || list[list.length - 1]).focus(); }
|
|
1950
|
+
else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(document.activeElement); }
|
|
1951
|
+
});
|
|
1952
|
+
document.addEventListener("click", close);
|
|
1953
|
+
render();
|
|
1954
|
+
return { render };
|
|
1955
|
+
}
|
|
1956
|
+
let providerDD = null;
|
|
1889
1957
|
function wireUsage() {
|
|
1890
|
-
//
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1958
|
+
// The provider picker is a custom icon dropdown; the account one stays a native select.
|
|
1959
|
+
providerDD = iconDropdown("uProviderDD",
|
|
1960
|
+
Object.entries(USAGE_PROVIDERS).map(([k, v]) => ({ value: k, label: v.label, icon: agentSvg(k) })),
|
|
1961
|
+
() => usageProvider,
|
|
1962
|
+
(val) => { if (val === usageProvider) return; usageProvider = val; usageAccount = "__all__"; applyUsageView(); loadProviderStatus(); });
|
|
1895
1963
|
$("uAccount").addEventListener("change", () => { usageAccount = $("uAccount").value; applyUsageView(); });
|
|
1896
1964
|
const el = $("uWindows");
|
|
1897
1965
|
el.addEventListener("click", (e) => {
|
|
@@ -2002,7 +2070,7 @@ export {
|
|
|
2002
2070
|
// measurable local store; the others show an honest "no local usage" panel instead.
|
|
2003
2071
|
function applyUsageView() {
|
|
2004
2072
|
const p = USAGE_PROVIDERS[usageProvider] || USAGE_PROVIDERS.claude;
|
|
2005
|
-
|
|
2073
|
+
if (providerDD) providerDD.render();
|
|
2006
2074
|
$("usageTitle").textContent = p.label + " Usage";
|
|
2007
2075
|
const link = $("usageViewLink");
|
|
2008
2076
|
link.href = p.viewHref; link.textContent = p.viewLabel + " ->";
|
|
@@ -2695,8 +2763,12 @@ export {
|
|
|
2695
2763
|
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>',
|
|
2696
2764
|
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>',
|
|
2697
2765
|
};
|
|
2698
|
-
// App label -> internal name
|
|
2699
|
-
|
|
2766
|
+
// App label -> internal name. Normalized (substring match) so every label variant maps
|
|
2767
|
+
// to an icon: "Claude Code", "Codex"/"OpenAI Codex", "opencode"/"OpenCode".
|
|
2768
|
+
const agentName = (label) => {
|
|
2769
|
+
const s = String(label || "").toLowerCase();
|
|
2770
|
+
return s.includes("claude") ? "claude" : s.includes("opencode") ? "opencode" : s.includes("codex") ? "codex" : "";
|
|
2771
|
+
};
|
|
2700
2772
|
const agentSvg = (name) => AGENT_ICONS[name] || "";
|
|
2701
2773
|
|
|
2702
2774
|
// Compression sources -> a brand icon. Reuses the app icons; the CLI source is enigma itself
|
|
@@ -2712,14 +2784,18 @@ export {
|
|
|
2712
2784
|
// Last-edited date (from skill.json), shown relative via the vendored <relative-time>.
|
|
2713
2785
|
const edited = s.updated ? '<span class="tag" title="Last edited ' + esc(new Date(s.updated).toLocaleString()) + '">edited '
|
|
2714
2786
|
+ rtime(new Date(s.updated).getTime(), 'format="relative"', new Date(s.updated).toLocaleDateString()) + "</span>" : "";
|
|
2715
|
-
//
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2787
|
+
// Per-app deployment is shown by the on/off chips below (one per CLI) for an enabled
|
|
2788
|
+
// enigma skill, so the static "where deployed" icons would be redundant there - only
|
|
2789
|
+
// show them when there are no chips (external or disabled skills).
|
|
2790
|
+
const hasChips = s.source === "enigma" && !s.discarded && s.agentStates && s.agentStates.length;
|
|
2791
|
+
const where = hasChips ? ""
|
|
2792
|
+
: s.agents && s.agents.length
|
|
2793
|
+
? '<span class="aicons">' + s.agents.map((l) => {
|
|
2794
|
+
const ic = agentSvg(agentName(l));
|
|
2795
|
+
return ic ? '<span class="aicon" title="' + esc(l) + '" aria-label="' + esc(l) + '">' + ic + "</span>"
|
|
2796
|
+
: '<span class="tag">' + esc(l) + "</span>";
|
|
2797
|
+
}).join("") + "</span>"
|
|
2798
|
+
: (s.discarded ? '<span class="tag">disabled</span>' : '<span class="tag">not deployed</span>');
|
|
2723
2799
|
const ext = s.source === "external" ? '<span class="tag ext">external</span>' : "";
|
|
2724
2800
|
const upd = s.source !== "enigma" ? ""
|
|
2725
2801
|
: s.update === "update" ? '<span class="tag warn">update available</span>'
|
|
@@ -2739,7 +2815,7 @@ export {
|
|
|
2739
2815
|
// Per-app on/off chips (Claude Code, Codex, OpenCode...). Only for an enabled enigma
|
|
2740
2816
|
// skill: when the whole skill is disabled the per-agent state is moot. data-off is the
|
|
2741
2817
|
// value to SET on click (turn off if currently on, on if currently off).
|
|
2742
|
-
const agentChips =
|
|
2818
|
+
const agentChips = hasChips
|
|
2743
2819
|
? '<div class="set-agents">' + s.agentStates.map((a) =>
|
|
2744
2820
|
'<button type="button" class="achip' + (a.deployed ? " on" : "") + '" data-action="agent-toggle" data-name="' + esc(s.name)
|
|
2745
2821
|
+ '" data-agent="' + esc(a.name) + '" data-off="' + (a.deployed ? 1 : 0) + '" aria-label="' + esc(a.label) + '" title="'
|
|
@@ -3025,7 +3101,7 @@ export {
|
|
|
3025
3101
|
|
|
3026
3102
|
// --- hash routing between the subpages ---
|
|
3027
3103
|
// Single source of truth for the subpage ids (savings is the default/fallback).
|
|
3028
|
-
const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "resources"];
|
|
3104
|
+
const VIEWS = ["savings", "usage", "accounts", "skills", "projects", "settings", "resources"];
|
|
3029
3105
|
function route() {
|
|
3030
3106
|
const h = (location.hash || "").replace(/^#\/?/, "");
|
|
3031
3107
|
const v = VIEWS.includes(h) ? h : "savings";
|
|
@@ -3036,6 +3112,7 @@ export {
|
|
|
3036
3112
|
if (v === "accounts" && !accountsLoaded) { accountsLoaded = true; loadAccounts(); }
|
|
3037
3113
|
if (v === "usage") { loadProviderStatus(); if (!usageAccountsLoaded) { usageAccountsLoaded = true; loadUsageAccounts(); } }
|
|
3038
3114
|
if (v === "resources") loadResources();
|
|
3115
|
+
if (v === "projects") loadProjects();
|
|
3039
3116
|
// The chart was sized while its view may have been hidden; nudge it on return.
|
|
3040
3117
|
if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
|
|
3041
3118
|
}
|
|
@@ -3095,6 +3172,129 @@ export {
|
|
|
3095
3172
|
});
|
|
3096
3173
|
}
|
|
3097
3174
|
|
|
3175
|
+
// --- projects subpage (per-project skills / config / hooks / gate, origin-guarded) ---
|
|
3176
|
+
let projOpen = null;
|
|
3177
|
+
function renderProjectList(projects) {
|
|
3178
|
+
const el = $("projList");
|
|
3179
|
+
if (!projects || !projects.length) { el.innerHTML = '<div class="empty" style="padding:12px 0">No projects yet. Add one above.</div>'; return; }
|
|
3180
|
+
el.innerHTML = projects.map((p) => {
|
|
3181
|
+
const badges = [];
|
|
3182
|
+
if (!p.exists) badges.push('<span class="tag">missing</span>');
|
|
3183
|
+
if (p.isGitRepo) badges.push('<span class="tag">git</span>');
|
|
3184
|
+
badges.push('<span class="tag">' + p.skills + " skill" + (p.skills === 1 ? "" : "s") + "</span>");
|
|
3185
|
+
if (p.hooks) badges.push('<span class="tag">hooks</span>');
|
|
3186
|
+
if (p.gate) badges.push('<span class="tag">gate</span>');
|
|
3187
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(p.label) + " " + badges.join(" ") + '</div><div class="set-hint">' + esc(p.path) + "</div></div>"
|
|
3188
|
+
+ '<div class="set-ctl ladd" style="align-items:center"><button type="button" class="toggle on" data-proj-open="' + esc(p.path) + '">Manage</button>'
|
|
3189
|
+
+ '<button type="button" class="toggle" data-proj-remove="' + esc(p.path) + '">Remove</button></div></div>';
|
|
3190
|
+
}).join("");
|
|
3191
|
+
}
|
|
3192
|
+
function projConfigRow(c) {
|
|
3193
|
+
const asSwitch = !c.choices || (c.choices.length === 2 && c.choices.indexOf(c.offChoice) !== -1);
|
|
3194
|
+
const ctl = !asSwitch
|
|
3195
|
+
? '<select class="choice" data-proj-cfg="' + esc(c.key) + '">' + c.choices.map((x) => '<option value="' + esc(x) + '"' + (x === c.choice ? " selected" : "") + ">" + esc(prettyChoice(x)) + "</option>").join("") + "</select>"
|
|
3196
|
+
: '<button type="button" class="toggle' + (c.value ? " on" : "") + '" data-proj-cfg-bool="' + esc(c.key) + '" data-on="' + (c.value ? 1 : 0) + '">' + (c.value ? "On" : "Off") + "</button>";
|
|
3197
|
+
const reset = c.overridden ? ' <button type="button" class="iconbtn" data-proj-cfg-reset="' + esc(c.key) + '" title="Inherit global">reset</button>' : "";
|
|
3198
|
+
const badge = c.overridden ? ' <span class="tag">project</span>' : ' <span class="tag">inherited</span>';
|
|
3199
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(c.label) + badge + '</div><div class="set-hint">' + esc(c.hint) + "</div></div>"
|
|
3200
|
+
+ '<div class="set-ctl ladd" style="align-items:center">' + ctl + reset + "</div></div>";
|
|
3201
|
+
}
|
|
3202
|
+
function renderProjectDetail(d) {
|
|
3203
|
+
const el = $("projDetail");
|
|
3204
|
+
if (!d || d.error) { el.style.display = "none"; projOpen = null; if (d && d.error) setNote("projNote", d.error, "err"); return; }
|
|
3205
|
+
projOpen = d.path;
|
|
3206
|
+
const deployed = new Set();
|
|
3207
|
+
(d.agents || []).forEach((a) => (a.deployed || []).forEach((s) => deployed.add(s)));
|
|
3208
|
+
const agentsLine = (d.agents || []).map((a) => esc(a.label) + (a.installed ? "" : " (not installed)")).join(", ") || "no agents detected";
|
|
3209
|
+
const cfg = (d.config || []).map(projConfigRow).join("");
|
|
3210
|
+
const skills = (d.available || []).map((s) => {
|
|
3211
|
+
const on = deployed.has(s.name);
|
|
3212
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + '</div><div class="set-hint">' + esc(s.description || "") + "</div></div>"
|
|
3213
|
+
+ '<div class="set-ctl"><button type="button" class="toggle' + (on ? " on" : "") + '" data-proj-skill="' + esc(s.name) + '" data-on="' + (on ? 1 : 0) + '">' + (on ? "On" : "Off") + "</button></div></div>";
|
|
3214
|
+
}).join("");
|
|
3215
|
+
const hooks = d.hooks ? '<span class="tag">installed</span>' : (d.isGitRepo ? '<button type="button" class="toggle on" data-proj-act="hooks">Install git hooks</button>' : '<span class="tag">not a git repo</span>');
|
|
3216
|
+
const gate = d.gate
|
|
3217
|
+
? '<span class="tag">initialized</span> <button type="button" class="toggle" data-proj-act="gate-eject">Eject</button>'
|
|
3218
|
+
: '<button type="button" class="toggle on" data-proj-act="gate-init">Initialize gate</button>';
|
|
3219
|
+
el.style.display = "";
|
|
3220
|
+
el.innerHTML =
|
|
3221
|
+
'<div class="panel-head" style="display:flex;justify-content:space-between;align-items:center"><h2>' + esc(d.label) + '</h2><button type="button" class="iconbtn" data-proj-close title="Close" aria-label="Close">x</button></div>'
|
|
3222
|
+
+ '<div class="set-hint" style="margin-bottom:10px">' + esc(d.path) + " · " + agentsLine + "</div>"
|
|
3223
|
+
+ '<div class="set-cat-h">Setup</div><div class="set-grid">'
|
|
3224
|
+
+ '<div class="set-row"><div class="set-meta"><div class="set-name">Git hooks</div><div class="set-hint">commit secret guard for this repo</div></div><div class="set-ctl">' + hooks + "</div></div>"
|
|
3225
|
+
+ '<div class="set-row"><div class="set-meta"><div class="set-name">AI quality gate</div><div class="set-hint">validate changes through the gate pipeline before push (needs the gate feature enabled)</div></div><div class="set-ctl">' + gate + "</div></div></div>"
|
|
3226
|
+
+ '<div class="set-cat-h" style="margin-top:14px">Project config</div><div class="set-grid">' + (cfg || '<div class="empty" style="padding:8px 0">No project settings.</div>') + "</div>"
|
|
3227
|
+
+ '<div class="set-cat-h" style="margin-top:14px">Skills</div><div class="set-grid">' + (skills || '<div class="empty" style="padding:8px 0">No skills.</div>') + "</div>";
|
|
3228
|
+
}
|
|
3229
|
+
async function loadProjects() {
|
|
3230
|
+
try { renderProjectList(((await (await fetch("/api/projects", { cache: "no-store" })).json()).projects) || []); }
|
|
3231
|
+
catch { $("projList").innerHTML = '<div class="empty" style="padding:12px 0">Projects unavailable.</div>'; }
|
|
3232
|
+
}
|
|
3233
|
+
async function openProject(path) {
|
|
3234
|
+
setNote("projNote", "Loading...");
|
|
3235
|
+
try {
|
|
3236
|
+
const d = await (await fetch("/api/projects/detail?path=" + encodeURIComponent(path), { cache: "no-store" })).json();
|
|
3237
|
+
renderProjectDetail(d);
|
|
3238
|
+
if (!d.error) setNote("projNote", "");
|
|
3239
|
+
} catch { setNote("projNote", "Could not load the project.", "err"); }
|
|
3240
|
+
}
|
|
3241
|
+
async function projAction(payload, confirmMsg) {
|
|
3242
|
+
if (!projOpen) return;
|
|
3243
|
+
if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, true))) return;
|
|
3244
|
+
setNote("projNote", "Working...");
|
|
3245
|
+
try {
|
|
3246
|
+
const out = await (await fetch("/api/projects/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: projOpen, ...payload }) })).json();
|
|
3247
|
+
if (out.detail) renderProjectDetail(out.detail);
|
|
3248
|
+
setNote("projNote", out.note || (out.ok ? "Done." : (out.error || "Action failed.")), out.ok ? "ok" : "err");
|
|
3249
|
+
loadProjects();
|
|
3250
|
+
} catch { setNote("projNote", "Could not reach the server.", "err"); }
|
|
3251
|
+
}
|
|
3252
|
+
async function addProjectUI() {
|
|
3253
|
+
const path = $("projPath").value.trim();
|
|
3254
|
+
if (!path) { setNote("projNote", "Enter a path.", "err"); return; }
|
|
3255
|
+
const label = $("projLabel").value.trim();
|
|
3256
|
+
try {
|
|
3257
|
+
const out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "add", path, label }) })).json();
|
|
3258
|
+
if (out.ok) { $("projPath").value = ""; $("projLabel").value = ""; setNote("projNote", "Added.", "ok"); renderProjectList(out.projects); }
|
|
3259
|
+
else setNote("projNote", out.error || "Could not add.", "err");
|
|
3260
|
+
} catch { setNote("projNote", "Could not reach the server.", "err"); }
|
|
3261
|
+
}
|
|
3262
|
+
function wireProjects() {
|
|
3263
|
+
$("projAdd").addEventListener("click", addProjectUI);
|
|
3264
|
+
$("projRefresh").addEventListener("click", loadProjects);
|
|
3265
|
+
$("projPath").addEventListener("keydown", (e) => { if (e.key === "Enter") addProjectUI(); });
|
|
3266
|
+
$("projList").addEventListener("click", async (e) => {
|
|
3267
|
+
const open = e.target.closest("[data-proj-open]"), rem = e.target.closest("[data-proj-remove]");
|
|
3268
|
+
if (open) { openProject(open.dataset.projOpen); return; }
|
|
3269
|
+
if (rem) {
|
|
3270
|
+
const path = rem.dataset.projRemove;
|
|
3271
|
+
if (!(await askConfirm("Remove project", "Stop managing " + path + "? (its files are not touched)", false))) return;
|
|
3272
|
+
try { const out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "remove", path }) })).json(); renderProjectList(out.projects || []); if (projOpen === path) { $("projDetail").style.display = "none"; projOpen = null; } }
|
|
3273
|
+
catch { setNote("projNote", "Could not reach the server.", "err"); }
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
$("projDetail").addEventListener("click", (e) => {
|
|
3277
|
+
if (e.target.closest("[data-proj-close]")) { $("projDetail").style.display = "none"; projOpen = null; return; }
|
|
3278
|
+
const skill = e.target.closest("[data-proj-skill]");
|
|
3279
|
+
if (skill) { projAction({ op: "skill", name: skill.dataset.projSkill, on: skill.dataset.on !== "1" }); return; }
|
|
3280
|
+
const cfgBool = e.target.closest("[data-proj-cfg-bool]");
|
|
3281
|
+
if (cfgBool) { projAction({ op: "config-set", key: cfgBool.dataset.projCfgBool, value: cfgBool.dataset.on !== "1" }); return; }
|
|
3282
|
+
const reset = e.target.closest("[data-proj-cfg-reset]");
|
|
3283
|
+
if (reset) { projAction({ op: "config-unset", key: reset.dataset.projCfgReset }); return; }
|
|
3284
|
+
const act = e.target.closest("[data-proj-act]");
|
|
3285
|
+
if (act) {
|
|
3286
|
+
const a = act.dataset.projAct;
|
|
3287
|
+
if (a === "hooks") projAction({ op: "hooks" }, "Install enigma git hooks in this repo?");
|
|
3288
|
+
else if (a === "gate-init") projAction({ op: "gate", gateOp: "init" }, "Initialize the AI quality gate for this project?");
|
|
3289
|
+
else if (a === "gate-eject") projAction({ op: "gate", gateOp: "eject" }, "Eject the gate from this project?");
|
|
3290
|
+
}
|
|
3291
|
+
});
|
|
3292
|
+
$("projDetail").addEventListener("change", (e) => {
|
|
3293
|
+
const sel = e.target.closest("select[data-proj-cfg]");
|
|
3294
|
+
if (sel) projAction({ op: "config-set", key: sel.dataset.projCfg, value: sel.value });
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3098
3298
|
initChart();
|
|
3099
3299
|
poll(true);
|
|
3100
3300
|
loadProviderStatus();
|
|
@@ -3108,6 +3308,7 @@ export {
|
|
|
3108
3308
|
wireAccounts();
|
|
3109
3309
|
wireUsage();
|
|
3110
3310
|
wireResources();
|
|
3311
|
+
wireProjects();
|
|
3111
3312
|
$("cfgExport").addEventListener("click", exportConfig);
|
|
3112
3313
|
$("cfgImport").addEventListener("click", importConfig);
|
|
3113
3314
|
$("updateBtn").addEventListener("click", runUpdate);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.45",
|
|
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": [
|