@enigmax/dashboard 0.1.44 → 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 +206 -20
  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;
@@ -401,6 +413,7 @@
401
413
  <a href="#/resources" data-view="resources" class="tab">Resources</a>
402
414
  </nav>
403
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>
404
417
  <span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
405
418
  <nav class="links">
406
419
  <a href="https://github.com/FJRG2007/enigma" target="_blank" rel="noopener noreferrer" title="GitHub repository" aria-label="GitHub repository">
@@ -2541,7 +2554,9 @@ export {
2541
2554
  const el = $("settingsBody");
2542
2555
  el.addEventListener("click", (e) => {
2543
2556
  const b = e.target.closest("button.toggle");
2544
- 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; }
2545
2560
  const rm = e.target.closest("button.lx");
2546
2561
  if (rm) { postListOp(rm.dataset.skey, "remove", rm.dataset.item); return; }
2547
2562
  const add = e.target.closest("button.laddb");
@@ -2551,7 +2566,7 @@ export {
2551
2566
  }
2552
2567
  });
2553
2568
  el.addEventListener("change", (e) => {
2554
- const sel = e.target.closest("select.choice");
2569
+ const sel = e.target.closest("select.choice[data-skey]");
2555
2570
  if (sel) postSetting(sel.dataset.skey, sel.value, sel);
2556
2571
  });
2557
2572
  el.addEventListener("keydown", (e) => {
@@ -2566,6 +2581,8 @@ export {
2566
2581
 
2567
2582
  let settingsLoaded = false;
2568
2583
  async function loadSettings() {
2584
+ if (isProjectScope()) return loadProjectSettings();
2585
+ const port = $("dashPortPanel"); if (port) port.style.display = ""; // global-only panel
2569
2586
  try {
2570
2587
  const data = await (await fetch("/api/settings", { cache: "no-store" })).json();
2571
2588
  renderSettings(data.categories || []);
@@ -2745,8 +2762,12 @@ export {
2745
2762
  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>',
2746
2763
  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>',
2747
2764
  };
2748
- // App label -> internal name, so the "where deployed" tag (which carries labels) maps to icons.
2749
- const AGENT_NAME_BY_LABEL = { "Claude Code": "claude", "OpenAI Codex": "codex", "OpenCode": "opencode" };
2765
+ // App label -> internal name. Normalized (substring match) so every label variant maps
2766
+ // to an icon: "Claude Code", "Codex"/"OpenAI Codex", "opencode"/"OpenCode".
2767
+ const agentName = (label) => {
2768
+ const s = String(label || "").toLowerCase();
2769
+ return s.includes("claude") ? "claude" : s.includes("opencode") ? "opencode" : s.includes("codex") ? "codex" : "";
2770
+ };
2750
2771
  const agentSvg = (name) => AGENT_ICONS[name] || "";
2751
2772
 
2752
2773
  // Compression sources -> a brand icon. Reuses the app icons; the CLI source is enigma itself
@@ -2762,14 +2783,18 @@ export {
2762
2783
  // Last-edited date (from skill.json), shown relative via the vendored <relative-time>.
2763
2784
  const edited = s.updated ? '<span class="tag" title="Last edited ' + esc(new Date(s.updated).toLocaleString()) + '">edited '
2764
2785
  + rtime(new Date(s.updated).getTime(), 'format="relative"', new Date(s.updated).toLocaleDateString()) + "</span>" : "";
2765
- // Show one app icon per agent that holds the skill (name in the tooltip), not the names.
2766
- const where = s.agents && s.agents.length
2767
- ? '<span class="aicons">' + s.agents.map((l) => {
2768
- const ic = agentSvg(AGENT_NAME_BY_LABEL[l]);
2769
- return ic ? '<span class="aicon" title="' + esc(l) + '" aria-label="' + esc(l) + '">' + ic + "</span>"
2770
- : '<span class="tag">' + esc(l) + "</span>";
2771
- }).join("") + "</span>"
2772
- : (s.discarded ? '<span class="tag">disabled</span>' : '<span class="tag">not deployed</span>');
2786
+ // Per-app deployment is shown by the on/off chips below (one per CLI) for an enabled
2787
+ // enigma skill, so the static "where deployed" icons would be redundant there - only
2788
+ // show them when there are no chips (external or disabled skills).
2789
+ const hasChips = s.source === "enigma" && !s.discarded && s.agentStates && s.agentStates.length;
2790
+ const where = hasChips ? ""
2791
+ : s.agents && s.agents.length
2792
+ ? '<span class="aicons">' + s.agents.map((l) => {
2793
+ const ic = agentSvg(agentName(l));
2794
+ return ic ? '<span class="aicon" title="' + esc(l) + '" aria-label="' + esc(l) + '">' + ic + "</span>"
2795
+ : '<span class="tag">' + esc(l) + "</span>";
2796
+ }).join("") + "</span>"
2797
+ : (s.discarded ? '<span class="tag">disabled</span>' : '<span class="tag">not deployed</span>');
2773
2798
  const ext = s.source === "external" ? '<span class="tag ext">external</span>' : "";
2774
2799
  const upd = s.source !== "enigma" ? ""
2775
2800
  : s.update === "update" ? '<span class="tag warn">update available</span>'
@@ -2789,7 +2814,7 @@ export {
2789
2814
  // Per-app on/off chips (Claude Code, Codex, OpenCode...). Only for an enabled enigma
2790
2815
  // skill: when the whole skill is disabled the per-agent state is moot. data-off is the
2791
2816
  // value to SET on click (turn off if currently on, on if currently off).
2792
- const agentChips = (s.source === "enigma" && !s.discarded && s.agentStates && s.agentStates.length)
2817
+ const agentChips = hasChips
2793
2818
  ? '<div class="set-agents">' + s.agentStates.map((a) =>
2794
2819
  '<button type="button" class="achip' + (a.deployed ? " on" : "") + '" data-action="agent-toggle" data-name="' + esc(s.name)
2795
2820
  + '" data-agent="' + esc(a.name) + '" data-off="' + (a.deployed ? 1 : 0) + '" aria-label="' + esc(a.label) + '" title="'
@@ -2916,6 +2941,7 @@ export {
2916
2941
 
2917
2942
  let skillsLoaded = false;
2918
2943
  async function loadSkills() {
2944
+ if (isProjectScope()) return loadProjectSkills();
2919
2945
  loadSkillSettings();
2920
2946
  try {
2921
2947
  const res = await fetch("/api/skills", { cache: "no-store" });
@@ -3076,18 +3102,27 @@ export {
3076
3102
  // --- hash routing between the subpages ---
3077
3103
  // Single source of truth for the subpage ids (savings is the default/fallback).
3078
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";
3079
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));
3080
3112
  const h = (location.hash || "").replace(/^#\/?/, "");
3081
- 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;
3082
3116
  VIEWS.forEach((view) => { $("view-" + view).style.display = view === v ? "" : "none"; });
3083
3117
  document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.view === v));
3084
- if (v === "settings" && !settingsLoaded) { settingsLoaded = true; loadSettings(); }
3085
- if (v === "skills" && !skillsLoaded) { skillsLoaded = true; loadSkills(); }
3086
- if (v === "accounts" && !accountsLoaded) { accountsLoaded = true; loadAccounts(); }
3087
- if (v === "usage") { loadProviderStatus(); if (!usageAccountsLoaded) { usageAccountsLoaded = true; loadUsageAccounts(); } }
3088
- if (v === "resources") loadResources();
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();
3089
3124
  // The chart was sized while its view may have been hidden; nudge it on return.
3090
- 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 */ } } }
3091
3126
  }
3092
3127
 
3093
3128
  // --- resources subpage (system cleanup; destructive, origin-guarded) ---
@@ -3145,6 +3180,155 @@ export {
3145
3180
  });
3146
3181
  }
3147
3182
 
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);
3239
+ }
3240
+
3241
+ // --- project-scope rendering (reuses settingsBody / skillsBody) ----------------
3242
+ function projConfigRow(c) {
3243
+ const asSwitch = !c.choices || (c.choices.length === 2 && c.choices.indexOf(c.offChoice) !== -1);
3244
+ const ctl = !asSwitch
3245
+ ? '<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>";
3247
+ const reset = c.overridden ? ' <button type="button" class="iconbtn" data-proj-cfg-reset="' + esc(c.key) + '" title="Inherit global">reset</button>' : "";
3248
+ const badge = c.overridden ? ' <span class="tag">project</span>' : ' <span class="tag">inherited</span>';
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>"
3250
+ + '<div class="set-ctl ladd" style="align-items:center">' + ctl + reset + "</div></div>";
3251
+ }
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; }
3261
+ const agentsLine = (d.agents || []).map((a) => esc(a.label) + (a.installed ? "" : " (not installed)")).join(", ") || "no agents detected";
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>');
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>';
3265
+ el.innerHTML =
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">'
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>"
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>";
3287
+ }
3288
+ async function projAction(payload, confirmMsg) {
3289
+ if (!isProjectScope()) return;
3290
+ const danger = payload.op === "gate" || payload.op === "hooks";
3291
+ if (confirmMsg && !(await askConfirm("Confirm", confirmMsg, danger))) return;
3292
+ try {
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 });
3330
+ }
3331
+
3148
3332
  initChart();
3149
3333
  poll(true);
3150
3334
  loadProviderStatus();
@@ -3158,6 +3342,7 @@ export {
3158
3342
  wireAccounts();
3159
3343
  wireUsage();
3160
3344
  wireResources();
3345
+ wireScope();
3161
3346
  $("cfgExport").addEventListener("click", exportConfig);
3162
3347
  $("cfgImport").addEventListener("click", importConfig);
3163
3348
  $("updateBtn").addEventListener("click", runUpdate);
@@ -3171,6 +3356,7 @@ export {
3171
3356
  finally { b.classList.remove("spinning"); }
3172
3357
  });
3173
3358
  window.addEventListener("hashchange", route);
3359
+ loadProjectsList().then(route);
3174
3360
  route();
3175
3361
  </script>
3176
3362
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.44",
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": [