@enigmax/dashboard 0.1.46 → 0.1.48
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 +122 -28
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -363,6 +363,9 @@
|
|
|
363
363
|
border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 12px; font-size: 13px; font-family: var(--mono);
|
|
364
364
|
}
|
|
365
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; }
|
|
366
369
|
/* Usage view provider/account pickers. */
|
|
367
370
|
.useg-pick { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
|
|
368
371
|
.useg-pick select { width: auto; min-width: 170px; }
|
|
@@ -2778,8 +2781,19 @@ export {
|
|
|
2778
2781
|
// Brand icon + text in one inline tag; the icon is omitted when there is none (e.g. content types).
|
|
2779
2782
|
function iconLabel(ic, text) { return '<span class="srctag">' + (ic ? '<span class="aicon">' + ic + '</span>' : '') + esc(text) + '</span>'; }
|
|
2780
2783
|
|
|
2784
|
+
let skillScope = "global"; // "global" | "project" - the same rows render in both
|
|
2781
2785
|
function skillRow(s) {
|
|
2782
2786
|
const ver = s.version ? '<span class="tag">v' + esc(s.version) + "</span>" : "";
|
|
2787
|
+
// Project scope: same row, but the per-app chips toggle deployment INTO this project
|
|
2788
|
+
// (data-pskill, handled by the scope delegate) and the global-only controls are hidden.
|
|
2789
|
+
if (skillScope === "project") {
|
|
2790
|
+
const chips = (s.agentStates || []).map((a) =>
|
|
2791
|
+
'<button type="button" class="achip' + (a.deployed ? " on" : "") + '" data-pskill="' + esc(s.name) + '" data-pagent="' + esc(a.name) + '" data-poff="' + (a.deployed ? 1 : 0) + '" aria-label="' + esc(a.label) + '" title="'
|
|
2792
|
+
+ (a.deployed ? esc(a.label) + " - deployed, click to remove" : esc(a.label) + " - not deployed, click to deploy") + '">' + (agentSvg(a.name) || esc(a.label)) + "</button>").join("");
|
|
2793
|
+
return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ver + "</div>"
|
|
2794
|
+
+ '<div class="set-hint">' + esc(s.description || "") + "</div>"
|
|
2795
|
+
+ '<div class="set-agents">' + (chips || '<span class="tag">no agents</span>') + "</div></div></div>";
|
|
2796
|
+
}
|
|
2783
2797
|
// Last-edited date (from skill.json), shown relative via the vendored <relative-time>.
|
|
2784
2798
|
const edited = s.updated ? '<span class="tag" title="Last edited ' + esc(new Date(s.updated).toLocaleString()) + '">edited '
|
|
2785
2799
|
+ rtime(new Date(s.updated).getTime(), 'format="relative"', new Date(s.updated).toLocaleDateString()) + "</span>" : "";
|
|
@@ -2828,11 +2842,15 @@ export {
|
|
|
2828
2842
|
let lastSkills = [];
|
|
2829
2843
|
function renderSkills(skills) {
|
|
2830
2844
|
lastSkills = skills; // kept so the editor can offer per-app save targets
|
|
2831
|
-
const enigma = skills.filter((s) => s.source === "enigma");
|
|
2832
|
-
const ext = skills.filter((s) => s.source === "external");
|
|
2833
2845
|
const section = (title, blurb, list, emptyText) => '<div class="set-cat"><div class="set-cat-h">' + title + "</div>"
|
|
2834
2846
|
+ '<div class="set-cat-b">' + blurb + "</div>"
|
|
2835
2847
|
+ (list.length ? list.map(skillRow).join("") : '<div class="empty" style="padding:16px 0">' + emptyText + "</div>") + "</div>";
|
|
2848
|
+
if (skillScope === "project") {
|
|
2849
|
+
$("skillsBody").innerHTML = section("Skills", "Deploy enigma's policy skills into this project, per app. A green chip means the skill is in that app's project folder.", skills, "No skills.");
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const enigma = skills.filter((s) => s.source === "enigma");
|
|
2853
|
+
const ext = skills.filter((s) => s.source === "external");
|
|
2836
2854
|
$("skillsBody").innerHTML =
|
|
2837
2855
|
section("Enigma skills", "Distributed and kept current by enigma. Disable to remove from your agents; enable to restore.", enigma, "No enigma skills.")
|
|
2838
2856
|
+ section("External skills", "Skills enigma did not author, found in your agents. Remove deletes the skill from every agent that has it.", ext, "No external skills found.");
|
|
@@ -2942,6 +2960,7 @@ export {
|
|
|
2942
2960
|
let skillsLoaded = false;
|
|
2943
2961
|
async function loadSkills() {
|
|
2944
2962
|
if (isProjectScope()) return loadProjectSkills();
|
|
2963
|
+
skillScope = "global";
|
|
2945
2964
|
loadSkillSettings();
|
|
2946
2965
|
try {
|
|
2947
2966
|
const res = await fetch("/api/skills", { cache: "no-store" });
|
|
@@ -3208,21 +3227,74 @@ export {
|
|
|
3208
3227
|
catch { projectsCache = []; }
|
|
3209
3228
|
populateScopeSelector();
|
|
3210
3229
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3230
|
+
function baseName(p) { return (p || "").trim().replace(/[\\/]+$/, "").split(/[\\/]/).pop() || ""; }
|
|
3231
|
+
function openCreateProject() {
|
|
3232
|
+
const back = document.createElement("div");
|
|
3233
|
+
back.className = "modal-back";
|
|
3234
|
+
back.innerHTML =
|
|
3235
|
+
'<div class="modal" role="dialog" aria-modal="true">'
|
|
3236
|
+
+ '<div class="modal-title">Create new project</div>'
|
|
3237
|
+
+ '<label class="modal-fl">Path</label><input class="modal-input" data-f="path" placeholder="/home/you/project" autocomplete="off" spellcheck="false">'
|
|
3238
|
+
+ '<div class="modal-err" data-e="path"></div>'
|
|
3239
|
+
+ '<label class="modal-fl">Name</label><input class="modal-input" data-f="name" placeholder="project name" autocomplete="off" spellcheck="false">'
|
|
3240
|
+
+ '<div class="modal-err" data-e="name"></div>'
|
|
3241
|
+
+ '<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">'
|
|
3242
|
+
+ '<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>';
|
|
3243
|
+
document.body.appendChild(back);
|
|
3244
|
+
requestAnimationFrame(() => back.classList.add("show"));
|
|
3245
|
+
const $f = (s) => back.querySelector(s);
|
|
3246
|
+
const pathInp = $f('[data-f="path"]'), nameInp = $f('[data-f="name"]'), descInp = $f('[data-f="desc"]');
|
|
3247
|
+
const pathErr = $f('[data-e="path"]'), nameErr = $f('[data-e="name"]'), okBtn = $f(".modal-ok");
|
|
3248
|
+
let nameEdited = false, pathOk = false, nameOk = false, reqId = 0, debounce;
|
|
3249
|
+
let done = false;
|
|
3250
|
+
const close = () => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); };
|
|
3251
|
+
const refreshOk = () => { okBtn.disabled = !(pathOk && nameOk); };
|
|
3252
|
+
// Name: required + not a duplicate (checked live against the loaded list; the server re-checks on add).
|
|
3253
|
+
const checkName = () => {
|
|
3254
|
+
const nm = nameInp.value.trim();
|
|
3255
|
+
if (!nm) { nameErr.textContent = "Enter a name."; nameOk = false; }
|
|
3256
|
+
else if (projectsCache.some((p) => p.label.toLowerCase() === nm.toLowerCase())) { nameErr.textContent = "A project with this name already exists."; nameOk = false; }
|
|
3257
|
+
else { nameErr.textContent = ""; nameOk = true; }
|
|
3258
|
+
refreshOk();
|
|
3259
|
+
};
|
|
3260
|
+
// Path: client format check, then a debounced server check (exists on disk + not already a project).
|
|
3261
|
+
const validatePath = async () => {
|
|
3262
|
+
const myId = ++reqId, path = pathInp.value.trim();
|
|
3263
|
+
try {
|
|
3264
|
+
const out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "validate", path }) })).json();
|
|
3265
|
+
if (myId !== reqId) return; // a newer keystroke superseded this
|
|
3266
|
+
pathErr.textContent = out.pathError || ""; pathOk = !out.pathError; refreshOk();
|
|
3267
|
+
} catch { /* keep the client-side state */ }
|
|
3268
|
+
};
|
|
3269
|
+
const onPath = () => {
|
|
3270
|
+
const path = pathInp.value.trim();
|
|
3271
|
+
if (!nameEdited) { nameInp.value = baseName(path); checkName(); }
|
|
3272
|
+
if (!path) { pathErr.textContent = "Enter a path."; pathOk = false; refreshOk(); return; }
|
|
3273
|
+
if (!/^(\/|[A-Za-z]:[\\/]|\\\\)/.test(path)) { pathErr.textContent = "Enter an absolute path (e.g. /home/you/app or C:\\path)."; pathOk = false; refreshOk(); return; }
|
|
3274
|
+
pathErr.textContent = "Checking..."; pathOk = false; refreshOk();
|
|
3275
|
+
clearTimeout(debounce); debounce = setTimeout(validatePath, 300);
|
|
3276
|
+
};
|
|
3277
|
+
pathInp.addEventListener("input", onPath);
|
|
3278
|
+
nameInp.addEventListener("input", () => { nameEdited = true; checkName(); });
|
|
3279
|
+
back.querySelector(".modal-cancel").addEventListener("click", close);
|
|
3280
|
+
back.addEventListener("mousedown", (e) => { if (e.target === back) close(); });
|
|
3281
|
+
back.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); close(); } else if (e.key === "Enter" && !okBtn.disabled) { e.preventDefault(); okBtn.click(); } });
|
|
3282
|
+
okBtn.addEventListener("click", async () => {
|
|
3283
|
+
if (okBtn.disabled) return;
|
|
3284
|
+
const before = new Set(projectsCache.map((p) => p.path));
|
|
3285
|
+
let out;
|
|
3286
|
+
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(); }
|
|
3287
|
+
catch { pathErr.textContent = "Could not reach the server."; return; }
|
|
3288
|
+
if (!out.ok) { pathErr.textContent = out.error || "Could not add the project."; pathOk = false; refreshOk(); return; }
|
|
3289
|
+
projectsCache = out.projects || projectsCache;
|
|
3290
|
+
const added = projectsCache.find((p) => !before.has(p.path)) || projectsCache[projectsCache.length - 1];
|
|
3291
|
+
currentScope = added ? added.path : "global";
|
|
3292
|
+
try { localStorage.setItem("enigmaScope", currentScope); } catch { /* */ }
|
|
3293
|
+
populateScopeSelector();
|
|
3294
|
+
close();
|
|
3295
|
+
location.hash = "#/settings"; route();
|
|
3296
|
+
});
|
|
3297
|
+
pathInp.focus();
|
|
3226
3298
|
}
|
|
3227
3299
|
function wireScope() {
|
|
3228
3300
|
const sel = $("scopeSel");
|
|
@@ -3243,7 +3315,7 @@ export {
|
|
|
3243
3315
|
const asSwitch = !c.choices || (c.choices.length === 2 && c.choices.indexOf(c.offChoice) !== -1);
|
|
3244
3316
|
const ctl = !asSwitch
|
|
3245
3317
|
? '<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>"
|
|
3246
|
-
: '<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>";
|
|
3318
|
+
: '<button type="button" class="toggle' + (c.value ? " on" : "") + '" data-kind="bool" data-proj-cfg-bool="' + esc(c.key) + '" data-on="' + (c.value ? 1 : 0) + '" title="' + (c.value ? "On" : "Off") + '">' + (c.value ? "On" : "Off") + "</button>";
|
|
3247
3319
|
const reset = c.overridden ? ' <button type="button" class="iconbtn" data-proj-cfg-reset="' + esc(c.key) + '" title="Inherit global">reset</button>' : "";
|
|
3248
3320
|
const badge = c.overridden ? ' <span class="tag">project</span>' : ' <span class="tag">inherited</span>';
|
|
3249
3321
|
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>"
|
|
@@ -3262,28 +3334,47 @@ export {
|
|
|
3262
3334
|
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>');
|
|
3263
3335
|
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>';
|
|
3264
3336
|
const cfg = (d.config || []).map(projConfigRow).join("") || '<div class="empty" style="padding:8px 0">No project settings.</div>';
|
|
3337
|
+
const descLine = d.description ? " · " + esc(d.description) : "";
|
|
3265
3338
|
el.innerHTML =
|
|
3266
|
-
'<div class="scope-banner">Editing <b>' + esc(d.label) + "</b> · " + esc(d.path) + " · " + agentsLine + "</div>"
|
|
3339
|
+
'<div class="scope-banner">Editing <b>' + esc(d.label) + "</b> · " + esc(d.path) + descLine + " · " + agentsLine + "</div>"
|
|
3267
3340
|
+ '<div class="panel"><div class="set-cat-h">Setup</div><div class="set-grid">'
|
|
3268
3341
|
+ '<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>"
|
|
3269
3342
|
+ '<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>"
|
|
3270
3343
|
+ '<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>"
|
|
3271
|
-
+ '<div class="panel"><div class="set-cat-h">Manage</div><div class="set-grid"
|
|
3344
|
+
+ '<div class="panel"><div class="set-cat-h">Manage</div><div class="set-grid">'
|
|
3345
|
+
+ '<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>'
|
|
3346
|
+
+ '<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>'
|
|
3347
|
+
+ '<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>'
|
|
3348
|
+
+ "</div></div>";
|
|
3349
|
+
}
|
|
3350
|
+
async function saveProjectMeta() {
|
|
3351
|
+
if (!isProjectScope()) return;
|
|
3352
|
+
const nameInp = document.querySelector("[data-proj-name]"), descInp = document.querySelector("[data-proj-desc]");
|
|
3353
|
+
const name = nameInp ? nameInp.value.trim() : undefined, description = descInp ? descInp.value.trim() : "";
|
|
3354
|
+
try {
|
|
3355
|
+
const out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "update", path: currentScope, name, description }) })).json();
|
|
3356
|
+
if (!out.ok) { setNote("settingsNote", out.error || "Could not save.", "err"); return; }
|
|
3357
|
+
projectsCache = out.projects || projectsCache;
|
|
3358
|
+
populateScopeSelector(); // reflect the renamed project in the selector
|
|
3359
|
+
setNote("settingsNote", "Saved.", "ok");
|
|
3360
|
+
loadProjectSettings();
|
|
3361
|
+
} catch { setNote("settingsNote", "Could not reach the server.", "err"); }
|
|
3272
3362
|
}
|
|
3273
3363
|
async function loadProjectSkills() {
|
|
3364
|
+
skillScope = "project";
|
|
3274
3365
|
const el = $("skillsBody");
|
|
3275
3366
|
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>';
|
|
3276
3367
|
el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
|
|
3277
3368
|
let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
|
|
3278
3369
|
if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3370
|
+
// Build the SAME skill objects renderSkills/skillRow use globally; agentStates carries
|
|
3371
|
+
// this project's per-app deployment so the rows render identically (chips per app).
|
|
3372
|
+
const agents = d.agents || [];
|
|
3373
|
+
const skills = (d.available || []).map((s) => ({
|
|
3374
|
+
name: s.name, description: s.description, source: "enigma",
|
|
3375
|
+
agentStates: agents.map((a) => ({ name: a.name, label: a.label, deployed: (a.deployed || []).indexOf(s.name) !== -1 })),
|
|
3376
|
+
}));
|
|
3377
|
+
renderSkills(skills);
|
|
3287
3378
|
}
|
|
3288
3379
|
async function projAction(payload, confirmMsg) {
|
|
3289
3380
|
if (!isProjectScope()) return;
|
|
@@ -3308,12 +3399,15 @@ export {
|
|
|
3308
3399
|
}
|
|
3309
3400
|
function onProjectControlClick(e) {
|
|
3310
3401
|
if (!isProjectScope()) return;
|
|
3402
|
+
const chip = e.target.closest("[data-pskill]"); // per-app deploy chip in the Skills view
|
|
3403
|
+
if (chip) { projAction({ op: "skill", name: chip.dataset.pskill, agent: chip.dataset.pagent, on: chip.dataset.poff !== "1" }); return; }
|
|
3311
3404
|
const sk = e.target.closest("[data-proj-skill]");
|
|
3312
3405
|
if (sk) { projAction({ op: "skill", name: sk.dataset.projSkill, on: sk.dataset.on !== "1" }); return; }
|
|
3313
3406
|
const cb = e.target.closest("[data-proj-cfg-bool]");
|
|
3314
3407
|
if (cb) { projAction({ op: "config-set", key: cb.dataset.projCfgBool, value: cb.dataset.on !== "1" }); return; }
|
|
3315
3408
|
const rs = e.target.closest("[data-proj-cfg-reset]");
|
|
3316
3409
|
if (rs) { projAction({ op: "config-unset", key: rs.dataset.projCfgReset }); return; }
|
|
3410
|
+
if (e.target.closest("[data-proj-save-meta]")) { saveProjectMeta(); return; }
|
|
3317
3411
|
if (e.target.closest("[data-proj-forget]")) { forgetProject(); return; }
|
|
3318
3412
|
const act = e.target.closest("[data-proj-act]");
|
|
3319
3413
|
if (act) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
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": [
|