@enigmax/dashboard 0.1.45 → 0.1.47
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 +242 -132
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -331,6 +331,18 @@
|
|
|
331
331
|
::-webkit-scrollbar-corner { background: transparent; }
|
|
332
332
|
::-webkit-scrollbar-thumb { background: #2b3340; border-radius: 999px; }
|
|
333
333
|
::-webkit-scrollbar-thumb:hover { background: #3a4150; }
|
|
334
|
+
/* Scope selector (global vs a project) in the header. */
|
|
335
|
+
.scope-wrap { display: inline-flex; align-items: center; gap: 6px; }
|
|
336
|
+
.scope-lbl { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
337
|
+
.scope-sel {
|
|
338
|
+
font-family: var(--mono); font-size: 12px; color: var(--text); background: var(--surface);
|
|
339
|
+
border: 1px solid var(--border); border-radius: 8px; padding: 5px 8px; cursor: pointer; max-width: 220px;
|
|
340
|
+
}
|
|
341
|
+
.scope-sel:focus { outline: none; border-color: var(--accent); }
|
|
342
|
+
.tab.disabled { opacity: .4; pointer-events: none; }
|
|
343
|
+
.scope-banner { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-bottom: 12px; }
|
|
344
|
+
.scope-banner b { color: var(--text); }
|
|
345
|
+
|
|
334
346
|
/* Animated modal dialog - replaces native alert/prompt/confirm so dialogs keep the design. */
|
|
335
347
|
.modal-back {
|
|
336
348
|
position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center;
|
|
@@ -351,6 +363,9 @@
|
|
|
351
363
|
border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 12px; font-size: 13px; font-family: var(--mono);
|
|
352
364
|
}
|
|
353
365
|
.modal-input:focus { outline: none; border-color: var(--accent); }
|
|
366
|
+
.modal-fl { display: block; margin-top: 14px; margin-bottom: 5px; font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
367
|
+
.modal-fl .modal-opt { text-transform: none; letter-spacing: 0; opacity: .75; }
|
|
368
|
+
.modal-fl + .modal-input { margin-top: 0; }
|
|
354
369
|
/* Usage view provider/account pickers. */
|
|
355
370
|
.useg-pick { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
|
|
356
371
|
.useg-pick select { width: auto; min-width: 170px; }
|
|
@@ -397,11 +412,11 @@
|
|
|
397
412
|
<a href="#/usage" data-view="usage" class="tab">Usage</a>
|
|
398
413
|
<a href="#/accounts" data-view="accounts" class="tab">Accounts</a>
|
|
399
414
|
<a href="#/skills" data-view="skills" class="tab">Skills</a>
|
|
400
|
-
<a href="#/projects" data-view="projects" class="tab">Projects</a>
|
|
401
415
|
<a href="#/settings" data-view="settings" class="tab">Settings</a>
|
|
402
416
|
<a href="#/resources" data-view="resources" class="tab">Resources</a>
|
|
403
417
|
</nav>
|
|
404
418
|
<div class="nav">
|
|
419
|
+
<label class="scope-wrap" title="Scope: global config or a specific project"><span class="scope-lbl">Scope</span><select id="scopeSel" class="scope-sel"><option value="global">Global</option></select></label>
|
|
405
420
|
<span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
|
|
406
421
|
<nav class="links">
|
|
407
422
|
<a href="https://github.com/FJRG2007/enigma" target="_blank" rel="noopener noreferrer" title="GitHub repository" aria-label="GitHub repository">
|
|
@@ -664,23 +679,6 @@
|
|
|
664
679
|
<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>
|
|
665
680
|
</section>
|
|
666
681
|
|
|
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
|
-
|
|
684
682
|
<footer>
|
|
685
683
|
Polling pauses automatically when this tab is hidden, and the server caches each snapshot - so an open dashboard costs almost nothing.
|
|
686
684
|
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>
|
|
@@ -2559,7 +2557,9 @@ export {
|
|
|
2559
2557
|
const el = $("settingsBody");
|
|
2560
2558
|
el.addEventListener("click", (e) => {
|
|
2561
2559
|
const b = e.target.closest("button.toggle");
|
|
2562
|
-
|
|
2560
|
+
// In project scope this body holds project controls (data-proj-*), handled by the
|
|
2561
|
+
// scope delegate; only act on real global settings toggles (they carry data-skey).
|
|
2562
|
+
if (b && b.dataset.skey) { postSetting(b.dataset.skey, b.dataset.on !== "1", b); return; }
|
|
2563
2563
|
const rm = e.target.closest("button.lx");
|
|
2564
2564
|
if (rm) { postListOp(rm.dataset.skey, "remove", rm.dataset.item); return; }
|
|
2565
2565
|
const add = e.target.closest("button.laddb");
|
|
@@ -2569,7 +2569,7 @@ export {
|
|
|
2569
2569
|
}
|
|
2570
2570
|
});
|
|
2571
2571
|
el.addEventListener("change", (e) => {
|
|
2572
|
-
const sel = e.target.closest("select.choice");
|
|
2572
|
+
const sel = e.target.closest("select.choice[data-skey]");
|
|
2573
2573
|
if (sel) postSetting(sel.dataset.skey, sel.value, sel);
|
|
2574
2574
|
});
|
|
2575
2575
|
el.addEventListener("keydown", (e) => {
|
|
@@ -2584,6 +2584,8 @@ export {
|
|
|
2584
2584
|
|
|
2585
2585
|
let settingsLoaded = false;
|
|
2586
2586
|
async function loadSettings() {
|
|
2587
|
+
if (isProjectScope()) return loadProjectSettings();
|
|
2588
|
+
const port = $("dashPortPanel"); if (port) port.style.display = ""; // global-only panel
|
|
2587
2589
|
try {
|
|
2588
2590
|
const data = await (await fetch("/api/settings", { cache: "no-store" })).json();
|
|
2589
2591
|
renderSettings(data.categories || []);
|
|
@@ -2942,6 +2944,7 @@ export {
|
|
|
2942
2944
|
|
|
2943
2945
|
let skillsLoaded = false;
|
|
2944
2946
|
async function loadSkills() {
|
|
2947
|
+
if (isProjectScope()) return loadProjectSkills();
|
|
2945
2948
|
loadSkillSettings();
|
|
2946
2949
|
try {
|
|
2947
2950
|
const res = await fetch("/api/skills", { cache: "no-store" });
|
|
@@ -3101,20 +3104,28 @@ export {
|
|
|
3101
3104
|
|
|
3102
3105
|
// --- hash routing between the subpages ---
|
|
3103
3106
|
// Single source of truth for the subpage ids (savings is the default/fallback).
|
|
3104
|
-
const VIEWS = ["savings", "usage", "accounts", "skills", "
|
|
3107
|
+
const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "resources"];
|
|
3108
|
+
// Views that apply to a single project; the rest are global/machine-level only.
|
|
3109
|
+
const PROJECT_VIEWS = ["settings", "skills"];
|
|
3110
|
+
let activeView = "savings";
|
|
3105
3111
|
function route() {
|
|
3112
|
+
const project = isProjectScope();
|
|
3113
|
+
// Gate the tabs by scope: under a project, only project-applicable views are reachable.
|
|
3114
|
+
document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("disabled", project && PROJECT_VIEWS.indexOf(t.dataset.view) === -1));
|
|
3106
3115
|
const h = (location.hash || "").replace(/^#\/?/, "");
|
|
3107
|
-
|
|
3116
|
+
let v = VIEWS.includes(h) ? h : "savings";
|
|
3117
|
+
if (project && PROJECT_VIEWS.indexOf(v) === -1) v = "settings";
|
|
3118
|
+
activeView = v;
|
|
3108
3119
|
VIEWS.forEach((view) => { $("view-" + view).style.display = view === v ? "" : "none"; });
|
|
3109
3120
|
document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.view === v));
|
|
3110
|
-
|
|
3111
|
-
if (v === "
|
|
3112
|
-
if (v === "
|
|
3113
|
-
if (v === "
|
|
3114
|
-
if (v === "
|
|
3115
|
-
if (v === "
|
|
3121
|
+
// Settings + Skills always reload (they are scope-dependent); the rest keep their once-load.
|
|
3122
|
+
if (v === "settings") loadSettings();
|
|
3123
|
+
else if (v === "skills") loadSkills();
|
|
3124
|
+
else if (v === "accounts") { if (!accountsLoaded) { accountsLoaded = true; loadAccounts(); } }
|
|
3125
|
+
else if (v === "usage") { loadProviderStatus(); if (!usageAccountsLoaded) { usageAccountsLoaded = true; loadUsageAccounts(); } }
|
|
3126
|
+
else if (v === "resources") loadResources();
|
|
3116
3127
|
// The chart was sized while its view may have been hidden; nudge it on return.
|
|
3117
|
-
if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
|
|
3128
|
+
else if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
|
|
3118
3129
|
}
|
|
3119
3130
|
|
|
3120
3131
|
// --- resources subpage (system cleanup; destructive, origin-guarded) ---
|
|
@@ -3172,23 +3183,118 @@ export {
|
|
|
3172
3183
|
});
|
|
3173
3184
|
}
|
|
3174
3185
|
|
|
3175
|
-
// ---
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3186
|
+
// --- scope (global vs a project) ---------------------------------------------
|
|
3187
|
+
// A header selector switches the whole dashboard between Global and a specific
|
|
3188
|
+
// project. Project scope is loopback-only and only ever touches a registered
|
|
3189
|
+
// project (the server validates the path on create). Settings + Skills become
|
|
3190
|
+
// scope-aware; the global-only views are disabled while a project is selected.
|
|
3191
|
+
let projectsCache = [];
|
|
3192
|
+
let currentScope = "global";
|
|
3193
|
+
try { currentScope = localStorage.getItem("enigmaScope") || "global"; } catch { /* private mode */ }
|
|
3194
|
+
function isProjectScope() { return currentScope !== "global"; }
|
|
3195
|
+
function scopeLabel() { const p = projectsCache.find((x) => x.path === currentScope); return p ? p.label : currentScope; }
|
|
3196
|
+
|
|
3197
|
+
function populateScopeSelector() {
|
|
3198
|
+
const sel = $("scopeSel");
|
|
3199
|
+
if (!sel) return;
|
|
3200
|
+
// A saved project that no longer exists falls back to Global.
|
|
3201
|
+
if (isProjectScope() && !projectsCache.some((p) => p.path === currentScope)) {
|
|
3202
|
+
currentScope = "global"; try { localStorage.setItem("enigmaScope", "global"); } catch { /* */ }
|
|
3203
|
+
}
|
|
3204
|
+
sel.innerHTML = '<option value="global">Global</option>'
|
|
3205
|
+
+ projectsCache.map((p) => '<option value="' + esc(p.path) + '">' + esc(p.label) + "</option>").join("")
|
|
3206
|
+
+ '<option value="__create__">+ Create new project</option>';
|
|
3207
|
+
sel.value = currentScope;
|
|
3208
|
+
}
|
|
3209
|
+
async function loadProjectsList() {
|
|
3210
|
+
try { projectsCache = ((await (await fetch("/api/projects", { cache: "no-store" })).json()).projects) || []; }
|
|
3211
|
+
catch { projectsCache = []; }
|
|
3212
|
+
populateScopeSelector();
|
|
3213
|
+
}
|
|
3214
|
+
function baseName(p) { return (p || "").trim().replace(/[\\/]+$/, "").split(/[\\/]/).pop() || ""; }
|
|
3215
|
+
function openCreateProject() {
|
|
3216
|
+
const back = document.createElement("div");
|
|
3217
|
+
back.className = "modal-back";
|
|
3218
|
+
back.innerHTML =
|
|
3219
|
+
'<div class="modal" role="dialog" aria-modal="true">'
|
|
3220
|
+
+ '<div class="modal-title">Create new project</div>'
|
|
3221
|
+
+ '<label class="modal-fl">Path</label><input class="modal-input" data-f="path" placeholder="/home/you/project" autocomplete="off" spellcheck="false">'
|
|
3222
|
+
+ '<div class="modal-err" data-e="path"></div>'
|
|
3223
|
+
+ '<label class="modal-fl">Name</label><input class="modal-input" data-f="name" placeholder="project name" autocomplete="off" spellcheck="false">'
|
|
3224
|
+
+ '<div class="modal-err" data-e="name"></div>'
|
|
3225
|
+
+ '<label class="modal-fl">Description <span class="modal-opt">(optional)</span></label><input class="modal-input" data-f="desc" placeholder="what this project is" autocomplete="off">'
|
|
3226
|
+
+ '<div class="modal-acts"><button type="button" class="toggle modal-cancel">Cancel</button><button type="button" class="toggle on modal-ok" disabled>Create</button></div></div>';
|
|
3227
|
+
document.body.appendChild(back);
|
|
3228
|
+
requestAnimationFrame(() => back.classList.add("show"));
|
|
3229
|
+
const $f = (s) => back.querySelector(s);
|
|
3230
|
+
const pathInp = $f('[data-f="path"]'), nameInp = $f('[data-f="name"]'), descInp = $f('[data-f="desc"]');
|
|
3231
|
+
const pathErr = $f('[data-e="path"]'), nameErr = $f('[data-e="name"]'), okBtn = $f(".modal-ok");
|
|
3232
|
+
let nameEdited = false, pathOk = false, nameOk = false, reqId = 0, debounce;
|
|
3233
|
+
let done = false;
|
|
3234
|
+
const close = () => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); };
|
|
3235
|
+
const refreshOk = () => { okBtn.disabled = !(pathOk && nameOk); };
|
|
3236
|
+
// Name: required + not a duplicate (checked live against the loaded list; the server re-checks on add).
|
|
3237
|
+
const checkName = () => {
|
|
3238
|
+
const nm = nameInp.value.trim();
|
|
3239
|
+
if (!nm) { nameErr.textContent = "Enter a name."; nameOk = false; }
|
|
3240
|
+
else if (projectsCache.some((p) => p.label.toLowerCase() === nm.toLowerCase())) { nameErr.textContent = "A project with this name already exists."; nameOk = false; }
|
|
3241
|
+
else { nameErr.textContent = ""; nameOk = true; }
|
|
3242
|
+
refreshOk();
|
|
3243
|
+
};
|
|
3244
|
+
// Path: client format check, then a debounced server check (exists on disk + not already a project).
|
|
3245
|
+
const validatePath = async () => {
|
|
3246
|
+
const myId = ++reqId, path = pathInp.value.trim();
|
|
3247
|
+
try {
|
|
3248
|
+
const out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "validate", path }) })).json();
|
|
3249
|
+
if (myId !== reqId) return; // a newer keystroke superseded this
|
|
3250
|
+
pathErr.textContent = out.pathError || ""; pathOk = !out.pathError; refreshOk();
|
|
3251
|
+
} catch { /* keep the client-side state */ }
|
|
3252
|
+
};
|
|
3253
|
+
const onPath = () => {
|
|
3254
|
+
const path = pathInp.value.trim();
|
|
3255
|
+
if (!nameEdited) { nameInp.value = baseName(path); checkName(); }
|
|
3256
|
+
if (!path) { pathErr.textContent = "Enter a path."; pathOk = false; refreshOk(); return; }
|
|
3257
|
+
if (!/^(\/|[A-Za-z]:[\\/]|\\\\)/.test(path)) { pathErr.textContent = "Enter an absolute path (e.g. /home/you/app or C:\\path)."; pathOk = false; refreshOk(); return; }
|
|
3258
|
+
pathErr.textContent = "Checking..."; pathOk = false; refreshOk();
|
|
3259
|
+
clearTimeout(debounce); debounce = setTimeout(validatePath, 300);
|
|
3260
|
+
};
|
|
3261
|
+
pathInp.addEventListener("input", onPath);
|
|
3262
|
+
nameInp.addEventListener("input", () => { nameEdited = true; checkName(); });
|
|
3263
|
+
back.querySelector(".modal-cancel").addEventListener("click", close);
|
|
3264
|
+
back.addEventListener("mousedown", (e) => { if (e.target === back) close(); });
|
|
3265
|
+
back.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); close(); } else if (e.key === "Enter" && !okBtn.disabled) { e.preventDefault(); okBtn.click(); } });
|
|
3266
|
+
okBtn.addEventListener("click", async () => {
|
|
3267
|
+
if (okBtn.disabled) return;
|
|
3268
|
+
const before = new Set(projectsCache.map((p) => p.path));
|
|
3269
|
+
let out;
|
|
3270
|
+
try { out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "add", path: pathInp.value.trim(), name: nameInp.value.trim(), description: descInp.value.trim() }) })).json(); }
|
|
3271
|
+
catch { pathErr.textContent = "Could not reach the server."; return; }
|
|
3272
|
+
if (!out.ok) { pathErr.textContent = out.error || "Could not add the project."; pathOk = false; refreshOk(); return; }
|
|
3273
|
+
projectsCache = out.projects || projectsCache;
|
|
3274
|
+
const added = projectsCache.find((p) => !before.has(p.path)) || projectsCache[projectsCache.length - 1];
|
|
3275
|
+
currentScope = added ? added.path : "global";
|
|
3276
|
+
try { localStorage.setItem("enigmaScope", currentScope); } catch { /* */ }
|
|
3277
|
+
populateScopeSelector();
|
|
3278
|
+
close();
|
|
3279
|
+
location.hash = "#/settings"; route();
|
|
3280
|
+
});
|
|
3281
|
+
pathInp.focus();
|
|
3282
|
+
}
|
|
3283
|
+
function wireScope() {
|
|
3284
|
+
const sel = $("scopeSel");
|
|
3285
|
+
if (sel) sel.addEventListener("change", () => {
|
|
3286
|
+
const val = sel.value;
|
|
3287
|
+
if (val === "__create__") { sel.value = currentScope; openCreateProject(); return; }
|
|
3288
|
+
currentScope = val;
|
|
3289
|
+
try { localStorage.setItem("enigmaScope", val); } catch { /* */ }
|
|
3290
|
+
route();
|
|
3291
|
+
});
|
|
3292
|
+
// One delegated handler for every project-scope control across Settings + Skills.
|
|
3293
|
+
document.body.addEventListener("click", onProjectControlClick);
|
|
3294
|
+
document.body.addEventListener("change", onProjectControlChange);
|
|
3191
3295
|
}
|
|
3296
|
+
|
|
3297
|
+
// --- project-scope rendering (reuses settingsBody / skillsBody) ----------------
|
|
3192
3298
|
function projConfigRow(c) {
|
|
3193
3299
|
const asSwitch = !c.choices || (c.choices.length === 2 && c.choices.indexOf(c.offChoice) !== -1);
|
|
3194
3300
|
const ctl = !asSwitch
|
|
@@ -3199,100 +3305,103 @@ export {
|
|
|
3199
3305
|
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
3306
|
+ '<div class="set-ctl ladd" style="align-items:center">' + ctl + reset + "</div></div>";
|
|
3201
3307
|
}
|
|
3202
|
-
function
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
const
|
|
3207
|
-
|
|
3308
|
+
async function fetchProjectDetail() {
|
|
3309
|
+
return (await (await fetch("/api/projects/detail?path=" + encodeURIComponent(currentScope), { cache: "no-store" })).json());
|
|
3310
|
+
}
|
|
3311
|
+
async function loadProjectSettings() {
|
|
3312
|
+
const el = $("settingsBody");
|
|
3313
|
+
const port = $("dashPortPanel"); if (port) port.style.display = "none"; // global-only
|
|
3314
|
+
el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
|
|
3315
|
+
let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
|
|
3316
|
+
if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
|
|
3208
3317
|
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
3318
|
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
|
-
|
|
3218
|
-
|
|
3219
|
-
el.style.display = "";
|
|
3319
|
+
const gate = d.gate ? '<span class="tag">initialized</span> <button type="button" class="toggle" data-proj-act="gate-eject">Eject</button>' : '<button type="button" class="toggle on" data-proj-act="gate-init">Initialize gate</button>';
|
|
3320
|
+
const cfg = (d.config || []).map(projConfigRow).join("") || '<div class="empty" style="padding:8px 0">No project settings.</div>';
|
|
3321
|
+
const descLine = d.description ? " · " + esc(d.description) : "";
|
|
3220
3322
|
el.innerHTML =
|
|
3221
|
-
'<div class="
|
|
3222
|
-
+ '<div class="
|
|
3223
|
-
+ '<div class="set-cat-h">Setup</div><div class="set-grid">'
|
|
3323
|
+
'<div class="scope-banner">Editing <b>' + esc(d.label) + "</b> · " + esc(d.path) + descLine + " · " + agentsLine + "</div>"
|
|
3324
|
+
+ '<div class="panel"><div class="set-cat-h">Setup</div><div class="set-grid">'
|
|
3224
3325
|
+ '<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"
|
|
3227
|
-
+ '<div class="set-cat-h"
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
}
|
|
3233
|
-
async function
|
|
3234
|
-
|
|
3326
|
+
+ '<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></div>"
|
|
3327
|
+
+ '<div class="panel"><div class="set-cat-h">Project config</div><div class="set-cat-b">applies only to this project (its .enigma.json). Settings not shown here are global-only - switch Scope to Global for those.</div><div class="set-grid">' + cfg + "</div></div>"
|
|
3328
|
+
+ '<div class="panel"><div class="set-cat-h">Manage</div><div class="set-grid">'
|
|
3329
|
+
+ '<div class="set-row"><div class="set-meta"><div class="set-name">Name</div><div class="set-hint">display name for this project (must be unique)</div></div><div class="set-ctl"><input class="linput" data-proj-name value="' + esc(d.label) + '" spellcheck="false" style="width:200px"></div></div>'
|
|
3330
|
+
+ '<div class="set-row"><div class="set-meta"><div class="set-name">Description</div><div class="set-hint">optional note shown in the header</div></div><div class="set-ctl ladd" style="align-items:center"><input class="linput" data-proj-desc value="' + esc(d.description || "") + '" placeholder="optional" style="flex:none;width:240px"><button type="button" class="toggle on" data-proj-save-meta>Save</button></div></div>'
|
|
3331
|
+
+ '<div class="set-row"><div class="set-meta"><div class="set-name">Forget project</div><div class="set-hint">remove it from the dashboard list - its files are not touched</div></div><div class="set-ctl"><button type="button" class="toggle" data-proj-forget>Forget</button></div></div>'
|
|
3332
|
+
+ "</div></div>";
|
|
3333
|
+
}
|
|
3334
|
+
async function saveProjectMeta() {
|
|
3335
|
+
if (!isProjectScope()) return;
|
|
3336
|
+
const nameInp = document.querySelector("[data-proj-name]"), descInp = document.querySelector("[data-proj-desc]");
|
|
3337
|
+
const name = nameInp ? nameInp.value.trim() : undefined, description = descInp ? descInp.value.trim() : "";
|
|
3235
3338
|
try {
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3339
|
+
const out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "update", path: currentScope, name, description }) })).json();
|
|
3340
|
+
if (!out.ok) { setNote("settingsNote", out.error || "Could not save.", "err"); return; }
|
|
3341
|
+
projectsCache = out.projects || projectsCache;
|
|
3342
|
+
populateScopeSelector(); // reflect the renamed project in the selector
|
|
3343
|
+
setNote("settingsNote", "Saved.", "ok");
|
|
3344
|
+
loadProjectSettings();
|
|
3345
|
+
} catch { setNote("settingsNote", "Could not reach the server.", "err"); }
|
|
3346
|
+
}
|
|
3347
|
+
async function loadProjectSkills() {
|
|
3348
|
+
const el = $("skillsBody");
|
|
3349
|
+
const ss = $("skillSettings"); if (ss) ss.innerHTML = '<div class="set-cat-b" style="padding:0 0 4px">Skill settings are global - switch Scope to Global to change them.</div>';
|
|
3350
|
+
el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
|
|
3351
|
+
let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
|
|
3352
|
+
if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
|
|
3353
|
+
const deployed = new Set(); (d.agents || []).forEach((a) => (a.deployed || []).forEach((s) => deployed.add(s)));
|
|
3354
|
+
const rows = (d.available || []).map((s) => {
|
|
3355
|
+
const on = deployed.has(s.name);
|
|
3356
|
+
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>"
|
|
3357
|
+
+ '<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>";
|
|
3358
|
+
}).join("") || '<div class="empty" style="padding:8px 0">No skills.</div>';
|
|
3359
|
+
el.innerHTML = '<div class="scope-banner">Skills for <b>' + esc(d.label) + "</b> · deploy or remove for this project</div>"
|
|
3360
|
+
+ '<div class="panel"><div class="set-grid">' + rows + "</div></div>";
|
|
3240
3361
|
}
|
|
3241
3362
|
async function projAction(payload, confirmMsg) {
|
|
3242
|
-
if (!
|
|
3243
|
-
|
|
3244
|
-
|
|
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();
|
|
3363
|
+
if (!isProjectScope()) return;
|
|
3364
|
+
const danger = payload.op === "gate" || payload.op === "hooks";
|
|
3365
|
+
if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, danger))) return;
|
|
3256
3366
|
try {
|
|
3257
|
-
const out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
|
|
3258
|
-
if (out.ok)
|
|
3259
|
-
else setNote("
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
});
|
|
3367
|
+
const out = await (await fetch("/api/projects/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: currentScope, ...payload }) })).json();
|
|
3368
|
+
if (!out.ok) setNote(activeView === "skills" ? "skillsNote" : "settingsNote", out.error || "Action failed.", "err");
|
|
3369
|
+
else if (out.note) setNote(activeView === "skills" ? "skillsNote" : "settingsNote", out.note, "ok");
|
|
3370
|
+
if (activeView === "skills") loadProjectSkills(); else loadProjectSettings();
|
|
3371
|
+
loadProjectsList(); // refresh selector badges
|
|
3372
|
+
} catch { setNote(activeView === "skills" ? "skillsNote" : "settingsNote", "Could not reach the server.", "err"); }
|
|
3373
|
+
}
|
|
3374
|
+
async function forgetProject() {
|
|
3375
|
+
if (!isProjectScope()) return;
|
|
3376
|
+
if (!(await askConfirm("Forget project", "Stop managing " + scopeLabel() + "? Its files are not touched.", false))) return;
|
|
3377
|
+
const path = currentScope;
|
|
3378
|
+
try { await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "remove", path }) }); } catch { /* */ }
|
|
3379
|
+
currentScope = "global"; try { localStorage.setItem("enigmaScope", "global"); } catch { /* */ }
|
|
3380
|
+
await loadProjectsList();
|
|
3381
|
+
location.hash = "#/settings"; route();
|
|
3382
|
+
}
|
|
3383
|
+
function onProjectControlClick(e) {
|
|
3384
|
+
if (!isProjectScope()) return;
|
|
3385
|
+
const sk = e.target.closest("[data-proj-skill]");
|
|
3386
|
+
if (sk) { projAction({ op: "skill", name: sk.dataset.projSkill, on: sk.dataset.on !== "1" }); return; }
|
|
3387
|
+
const cb = e.target.closest("[data-proj-cfg-bool]");
|
|
3388
|
+
if (cb) { projAction({ op: "config-set", key: cb.dataset.projCfgBool, value: cb.dataset.on !== "1" }); return; }
|
|
3389
|
+
const rs = e.target.closest("[data-proj-cfg-reset]");
|
|
3390
|
+
if (rs) { projAction({ op: "config-unset", key: rs.dataset.projCfgReset }); return; }
|
|
3391
|
+
if (e.target.closest("[data-proj-save-meta]")) { saveProjectMeta(); return; }
|
|
3392
|
+
if (e.target.closest("[data-proj-forget]")) { forgetProject(); return; }
|
|
3393
|
+
const act = e.target.closest("[data-proj-act]");
|
|
3394
|
+
if (act) {
|
|
3395
|
+
const a = act.dataset.projAct;
|
|
3396
|
+
if (a === "hooks") projAction({ op: "hooks" }, "Install enigma git hooks in this repo?");
|
|
3397
|
+
else if (a === "gate-init") projAction({ op: "gate", gateOp: "init" }, "Initialize the AI quality gate for this project?");
|
|
3398
|
+
else if (a === "gate-eject") projAction({ op: "gate", gateOp: "eject" }, "Eject the gate from this project?");
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
function onProjectControlChange(e) {
|
|
3402
|
+
if (!isProjectScope()) return;
|
|
3403
|
+
const sel = e.target.closest("select[data-proj-cfg]");
|
|
3404
|
+
if (sel) projAction({ op: "config-set", key: sel.dataset.projCfg, value: sel.value });
|
|
3296
3405
|
}
|
|
3297
3406
|
|
|
3298
3407
|
initChart();
|
|
@@ -3308,7 +3417,7 @@ export {
|
|
|
3308
3417
|
wireAccounts();
|
|
3309
3418
|
wireUsage();
|
|
3310
3419
|
wireResources();
|
|
3311
|
-
|
|
3420
|
+
wireScope();
|
|
3312
3421
|
$("cfgExport").addEventListener("click", exportConfig);
|
|
3313
3422
|
$("cfgImport").addEventListener("click", importConfig);
|
|
3314
3423
|
$("updateBtn").addEventListener("click", runUpdate);
|
|
@@ -3322,6 +3431,7 @@ export {
|
|
|
3322
3431
|
finally { b.classList.remove("spinning"); }
|
|
3323
3432
|
});
|
|
3324
3433
|
window.addEventListener("hashchange", route);
|
|
3434
|
+
loadProjectsList().then(route);
|
|
3325
3435
|
route();
|
|
3326
3436
|
</script>
|
|
3327
3437
|
</body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
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": [
|