@enigmax/dashboard 0.1.45 → 0.1.46

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.
Files changed (2) hide show
  1. package/assets/index.html +168 -133
  2. 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;
@@ -397,11 +409,11 @@
397
409
  <a href="#/usage" data-view="usage" class="tab">Usage</a>
398
410
  <a href="#/accounts" data-view="accounts" class="tab">Accounts</a>
399
411
  <a href="#/skills" data-view="skills" class="tab">Skills</a>
400
- <a href="#/projects" data-view="projects" class="tab">Projects</a>
401
412
  <a href="#/settings" data-view="settings" class="tab">Settings</a>
402
413
  <a href="#/resources" data-view="resources" class="tab">Resources</a>
403
414
  </nav>
404
415
  <div class="nav">
416
+ <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
417
  <span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
406
418
  <nav class="links">
407
419
  <a href="https://github.com/FJRG2007/enigma" target="_blank" rel="noopener noreferrer" title="GitHub repository" aria-label="GitHub repository">
@@ -664,23 +676,6 @@
664
676
  <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
677
  </section>
666
678
 
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
679
  <footer>
685
680
  Polling pauses automatically when this tab is hidden, and the server caches each snapshot - so an open dashboard costs almost nothing.
686
681
  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 +2554,9 @@ export {
2559
2554
  const el = $("settingsBody");
2560
2555
  el.addEventListener("click", (e) => {
2561
2556
  const b = e.target.closest("button.toggle");
2562
- if (b) { postSetting(b.dataset.skey, b.dataset.on !== "1", b); return; }
2557
+ // In project scope this body holds project controls (data-proj-*), handled by the
2558
+ // scope delegate; only act on real global settings toggles (they carry data-skey).
2559
+ if (b && b.dataset.skey) { postSetting(b.dataset.skey, b.dataset.on !== "1", b); return; }
2563
2560
  const rm = e.target.closest("button.lx");
2564
2561
  if (rm) { postListOp(rm.dataset.skey, "remove", rm.dataset.item); return; }
2565
2562
  const add = e.target.closest("button.laddb");
@@ -2569,7 +2566,7 @@ export {
2569
2566
  }
2570
2567
  });
2571
2568
  el.addEventListener("change", (e) => {
2572
- const sel = e.target.closest("select.choice");
2569
+ const sel = e.target.closest("select.choice[data-skey]");
2573
2570
  if (sel) postSetting(sel.dataset.skey, sel.value, sel);
2574
2571
  });
2575
2572
  el.addEventListener("keydown", (e) => {
@@ -2584,6 +2581,8 @@ export {
2584
2581
 
2585
2582
  let settingsLoaded = false;
2586
2583
  async function loadSettings() {
2584
+ if (isProjectScope()) return loadProjectSettings();
2585
+ const port = $("dashPortPanel"); if (port) port.style.display = ""; // global-only panel
2587
2586
  try {
2588
2587
  const data = await (await fetch("/api/settings", { cache: "no-store" })).json();
2589
2588
  renderSettings(data.categories || []);
@@ -2942,6 +2941,7 @@ export {
2942
2941
 
2943
2942
  let skillsLoaded = false;
2944
2943
  async function loadSkills() {
2944
+ if (isProjectScope()) return loadProjectSkills();
2945
2945
  loadSkillSettings();
2946
2946
  try {
2947
2947
  const res = await fetch("/api/skills", { cache: "no-store" });
@@ -3101,20 +3101,28 @@ export {
3101
3101
 
3102
3102
  // --- hash routing between the subpages ---
3103
3103
  // Single source of truth for the subpage ids (savings is the default/fallback).
3104
- const VIEWS = ["savings", "usage", "accounts", "skills", "projects", "settings", "resources"];
3104
+ const VIEWS = ["savings", "usage", "accounts", "skills", "settings", "resources"];
3105
+ // Views that apply to a single project; the rest are global/machine-level only.
3106
+ const PROJECT_VIEWS = ["settings", "skills"];
3107
+ let activeView = "savings";
3105
3108
  function route() {
3109
+ const project = isProjectScope();
3110
+ // Gate the tabs by scope: under a project, only project-applicable views are reachable.
3111
+ document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("disabled", project && PROJECT_VIEWS.indexOf(t.dataset.view) === -1));
3106
3112
  const h = (location.hash || "").replace(/^#\/?/, "");
3107
- const v = VIEWS.includes(h) ? h : "savings";
3113
+ let v = VIEWS.includes(h) ? h : "savings";
3114
+ if (project && PROJECT_VIEWS.indexOf(v) === -1) v = "settings";
3115
+ activeView = v;
3108
3116
  VIEWS.forEach((view) => { $("view-" + view).style.display = view === v ? "" : "none"; });
3109
3117
  document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.view === v));
3110
- if (v === "settings" && !settingsLoaded) { settingsLoaded = true; loadSettings(); }
3111
- if (v === "skills" && !skillsLoaded) { skillsLoaded = true; loadSkills(); }
3112
- if (v === "accounts" && !accountsLoaded) { accountsLoaded = true; loadAccounts(); }
3113
- if (v === "usage") { loadProviderStatus(); if (!usageAccountsLoaded) { usageAccountsLoaded = true; loadUsageAccounts(); } }
3114
- if (v === "resources") loadResources();
3115
- if (v === "projects") loadProjects();
3118
+ // Settings + Skills always reload (they are scope-dependent); the rest keep their once-load.
3119
+ if (v === "settings") loadSettings();
3120
+ else if (v === "skills") loadSkills();
3121
+ else if (v === "accounts") { if (!accountsLoaded) { accountsLoaded = true; loadAccounts(); } }
3122
+ else if (v === "usage") { loadProviderStatus(); if (!usageAccountsLoaded) { usageAccountsLoaded = true; loadUsageAccounts(); } }
3123
+ else if (v === "resources") loadResources();
3116
3124
  // 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 */ } } }
3125
+ else if (v === "savings") { loadSystems(true); if (chart) { try { applyRange(); } catch { /* not ready */ } } }
3118
3126
  }
3119
3127
 
3120
3128
  // --- resources subpage (system cleanup; destructive, origin-guarded) ---
@@ -3172,23 +3180,65 @@ export {
3172
3180
  });
3173
3181
  }
3174
3182
 
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("");
3183
+ // --- scope (global vs a project) ---------------------------------------------
3184
+ // A header selector switches the whole dashboard between Global and a specific
3185
+ // project. Project scope is loopback-only and only ever touches a registered
3186
+ // project (the server validates the path on create). Settings + Skills become
3187
+ // scope-aware; the global-only views are disabled while a project is selected.
3188
+ let projectsCache = [];
3189
+ let currentScope = "global";
3190
+ try { currentScope = localStorage.getItem("enigmaScope") || "global"; } catch { /* private mode */ }
3191
+ function isProjectScope() { return currentScope !== "global"; }
3192
+ function scopeLabel() { const p = projectsCache.find((x) => x.path === currentScope); return p ? p.label : currentScope; }
3193
+
3194
+ function populateScopeSelector() {
3195
+ const sel = $("scopeSel");
3196
+ if (!sel) return;
3197
+ // A saved project that no longer exists falls back to Global.
3198
+ if (isProjectScope() && !projectsCache.some((p) => p.path === currentScope)) {
3199
+ currentScope = "global"; try { localStorage.setItem("enigmaScope", "global"); } catch { /* */ }
3200
+ }
3201
+ sel.innerHTML = '<option value="global">Global</option>'
3202
+ + projectsCache.map((p) => '<option value="' + esc(p.path) + '">' + esc(p.label) + "</option>").join("")
3203
+ + '<option value="__create__">+ Create new project</option>';
3204
+ sel.value = currentScope;
3205
+ }
3206
+ async function loadProjectsList() {
3207
+ try { projectsCache = ((await (await fetch("/api/projects", { cache: "no-store" })).json()).projects) || []; }
3208
+ catch { projectsCache = []; }
3209
+ populateScopeSelector();
3210
+ }
3211
+ async function openCreateProject() {
3212
+ const path = await askText("Create new project", "", "Absolute path to the project folder (its name becomes the label):",
3213
+ (v) => /^(\/|[A-Za-z]:[\\/]|\\\\)/.test(v) ? null : "Enter an absolute path, e.g. /home/you/app or C:\\path\\to\\app.");
3214
+ if (!path) return;
3215
+ const before = new Set(projectsCache.map((p) => p.path));
3216
+ let out;
3217
+ try { out = await (await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "add", path }) })).json(); }
3218
+ catch { await openModal({ title: "Could not add project", message: "Could not reach the server." }); return; }
3219
+ if (!out.ok) { await openModal({ title: "Could not add project", message: out.error || "That path could not be added." }); return; }
3220
+ projectsCache = out.projects || projectsCache;
3221
+ const added = projectsCache.find((p) => !before.has(p.path)) || projectsCache[projectsCache.length - 1];
3222
+ currentScope = added ? added.path : "global";
3223
+ try { localStorage.setItem("enigmaScope", currentScope); } catch { /* */ }
3224
+ populateScopeSelector();
3225
+ location.hash = "#/settings"; route();
3226
+ }
3227
+ function wireScope() {
3228
+ const sel = $("scopeSel");
3229
+ if (sel) sel.addEventListener("change", () => {
3230
+ const val = sel.value;
3231
+ if (val === "__create__") { sel.value = currentScope; openCreateProject(); return; }
3232
+ currentScope = val;
3233
+ try { localStorage.setItem("enigmaScope", val); } catch { /* */ }
3234
+ route();
3235
+ });
3236
+ // One delegated handler for every project-scope control across Settings + Skills.
3237
+ document.body.addEventListener("click", onProjectControlClick);
3238
+ document.body.addEventListener("change", onProjectControlChange);
3191
3239
  }
3240
+
3241
+ // --- project-scope rendering (reuses settingsBody / skillsBody) ----------------
3192
3242
  function projConfigRow(c) {
3193
3243
  const asSwitch = !c.choices || (c.choices.length === 2 && c.choices.indexOf(c.offChoice) !== -1);
3194
3244
  const ctl = !asSwitch
@@ -3199,100 +3249,84 @@ export {
3199
3249
  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
3250
  + '<div class="set-ctl ladd" style="align-items:center">' + ctl + reset + "</div></div>";
3201
3251
  }
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)));
3252
+ async function fetchProjectDetail() {
3253
+ return (await (await fetch("/api/projects/detail?path=" + encodeURIComponent(currentScope), { cache: "no-store" })).json());
3254
+ }
3255
+ async function loadProjectSettings() {
3256
+ const el = $("settingsBody");
3257
+ const port = $("dashPortPanel"); if (port) port.style.display = "none"; // global-only
3258
+ el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
3259
+ let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
3260
+ if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
3208
3261
  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
3262
  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 = "";
3263
+ 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
+ const cfg = (d.config || []).map(projConfigRow).join("") || '<div class="empty" style="padding:8px 0">No project settings.</div>';
3220
3265
  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) + " &middot; " + agentsLine + "</div>"
3223
- + '<div class="set-cat-h">Setup</div><div class="set-grid">'
3266
+ '<div class="scope-banner">Editing <b>' + esc(d.label) + "</b> &middot; " + esc(d.path) + " &middot; " + agentsLine + "</div>"
3267
+ + '<div class="panel"><div class="set-cat-h">Setup</div><div class="set-grid">'
3224
3268
  + '<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"); }
3269
+ + '<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
+ + '<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"><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></div></div>';
3272
+ }
3273
+ async function loadProjectSkills() {
3274
+ const el = $("skillsBody");
3275
+ 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
+ el.innerHTML = '<div class="empty" style="padding:24px 0">Loading...</div>';
3277
+ let d; try { d = await fetchProjectDetail(); } catch { el.innerHTML = '<div class="empty" style="padding:24px 0">Project unavailable.</div>'; return; }
3278
+ if (d.error) { el.innerHTML = '<div class="empty" style="padding:24px 0">' + esc(d.error) + "</div>"; return; }
3279
+ const deployed = new Set(); (d.agents || []).forEach((a) => (a.deployed || []).forEach((s) => deployed.add(s)));
3280
+ const rows = (d.available || []).map((s) => {
3281
+ const on = deployed.has(s.name);
3282
+ 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>"
3283
+ + '<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>";
3284
+ }).join("") || '<div class="empty" style="padding:8px 0">No skills.</div>';
3285
+ el.innerHTML = '<div class="scope-banner">Skills for <b>' + esc(d.label) + "</b> &middot; deploy or remove for this project</div>"
3286
+ + '<div class="panel"><div class="set-grid">' + rows + "</div></div>";
3240
3287
  }
3241
3288
  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();
3289
+ if (!isProjectScope()) return;
3290
+ const danger = payload.op === "gate" || payload.op === "hooks";
3291
+ if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, danger))) return;
3256
3292
  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
- });
3293
+ const out = await (await fetch("/api/projects/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: currentScope, ...payload }) })).json();
3294
+ if (!out.ok) setNote(activeView === "skills" ? "skillsNote" : "settingsNote", out.error || "Action failed.", "err");
3295
+ else if (out.note) setNote(activeView === "skills" ? "skillsNote" : "settingsNote", out.note, "ok");
3296
+ if (activeView === "skills") loadProjectSkills(); else loadProjectSettings();
3297
+ loadProjectsList(); // refresh selector badges
3298
+ } catch { setNote(activeView === "skills" ? "skillsNote" : "settingsNote", "Could not reach the server.", "err"); }
3299
+ }
3300
+ async function forgetProject() {
3301
+ if (!isProjectScope()) return;
3302
+ if (!(await askConfirm("Forget project", "Stop managing " + scopeLabel() + "? Its files are not touched.", false))) return;
3303
+ const path = currentScope;
3304
+ try { await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ op: "remove", path }) }); } catch { /* */ }
3305
+ currentScope = "global"; try { localStorage.setItem("enigmaScope", "global"); } catch { /* */ }
3306
+ await loadProjectsList();
3307
+ location.hash = "#/settings"; route();
3308
+ }
3309
+ function onProjectControlClick(e) {
3310
+ if (!isProjectScope()) return;
3311
+ const sk = e.target.closest("[data-proj-skill]");
3312
+ if (sk) { projAction({ op: "skill", name: sk.dataset.projSkill, on: sk.dataset.on !== "1" }); return; }
3313
+ const cb = e.target.closest("[data-proj-cfg-bool]");
3314
+ if (cb) { projAction({ op: "config-set", key: cb.dataset.projCfgBool, value: cb.dataset.on !== "1" }); return; }
3315
+ const rs = e.target.closest("[data-proj-cfg-reset]");
3316
+ if (rs) { projAction({ op: "config-unset", key: rs.dataset.projCfgReset }); return; }
3317
+ if (e.target.closest("[data-proj-forget]")) { forgetProject(); return; }
3318
+ const act = e.target.closest("[data-proj-act]");
3319
+ if (act) {
3320
+ const a = act.dataset.projAct;
3321
+ if (a === "hooks") projAction({ op: "hooks" }, "Install enigma git hooks in this repo?");
3322
+ else if (a === "gate-init") projAction({ op: "gate", gateOp: "init" }, "Initialize the AI quality gate for this project?");
3323
+ else if (a === "gate-eject") projAction({ op: "gate", gateOp: "eject" }, "Eject the gate from this project?");
3324
+ }
3325
+ }
3326
+ function onProjectControlChange(e) {
3327
+ if (!isProjectScope()) return;
3328
+ const sel = e.target.closest("select[data-proj-cfg]");
3329
+ if (sel) projAction({ op: "config-set", key: sel.dataset.projCfg, value: sel.value });
3296
3330
  }
3297
3331
 
3298
3332
  initChart();
@@ -3308,7 +3342,7 @@ export {
3308
3342
  wireAccounts();
3309
3343
  wireUsage();
3310
3344
  wireResources();
3311
- wireProjects();
3345
+ wireScope();
3312
3346
  $("cfgExport").addEventListener("click", exportConfig);
3313
3347
  $("cfgImport").addEventListener("click", importConfig);
3314
3348
  $("updateBtn").addEventListener("click", runUpdate);
@@ -3322,6 +3356,7 @@ export {
3322
3356
  finally { b.classList.remove("spinning"); }
3323
3357
  });
3324
3358
  window.addEventListener("hashchange", route);
3359
+ loadProjectsList().then(route);
3325
3360
  route();
3326
3361
  </script>
3327
3362
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
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": [