@enigmax/dashboard 0.1.23 → 0.1.25

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 +77 -5
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -158,7 +158,7 @@
158
158
  .set-ctl { flex-shrink: 0; }
159
159
  .toggle {
160
160
  background: var(--surface2); color: var(--muted); border: 1px solid var(--border);
161
- border-radius: var(--radius-sm); padding: 6px 16px; font-size: 12px; cursor: pointer; font-family: var(--mono); min-width: 56px;
161
+ border-radius: var(--radius-sm); padding: 6px 16px; font-size: 12px; cursor: pointer; font-family: var(--mono); min-width: 56px; text-decoration: none;
162
162
  transition: color .15s, background .15s, border-color .15s;
163
163
  }
164
164
  .toggle:hover { color: var(--text); border-color: #2f3744; }
@@ -215,6 +215,11 @@
215
215
  .btn-danger.icon { display: inline-flex; align-items: center; justify-content: center; padding: 6px 9px; }
216
216
  .btn-danger.icon:hover { color: var(--bad); border-color: var(--bad); }
217
217
  .btn-danger svg { width: 15px; height: 15px; display: block; }
218
+ /* Per-agent skill toggles: one pill per app (Claude Code, Codex, OpenCode), green when the skill is on for it. */
219
+ .set-agents { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
220
+ .achip { font-family: var(--mono); font-size: 11px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); cursor: pointer; text-decoration: none; transition: color .15s, background .15s, border-color .15s; }
221
+ .achip:hover { border-color: #2f3744; color: var(--text); }
222
+ .achip.on { background: var(--good); color: #06210f; border-color: var(--good); font-weight: 600; }
218
223
  /* List settings (guard block/allow globs, custom secret patterns): chips + add input. A list row spans the full grid width. */
219
224
  .set-row.list { grid-column: 1 / -1; align-items: stretch; flex-direction: column; }
220
225
  .lchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
@@ -1677,6 +1682,7 @@ export {
1677
1682
  $("uWindows").innerHTML = '<div class="wgrid">'
1678
1683
  + windowCard(w.session, "session")
1679
1684
  + windowCard(w.weeklyAll, "weekly")
1685
+ + (w.weeklyOpus ? windowCard(w.weeklyOpus, "weeklyOpus") : "")
1680
1686
  + windowCard(w.weeklySonnet, "weeklySonnet")
1681
1687
  + "</div>";
1682
1688
  }
@@ -1911,11 +1917,19 @@ export {
1911
1917
  // update), so the served UI may be newer than this tab - reload once so a tab you
1912
1918
  // left open self-heals instead of showing stale markup.
1913
1919
  let loadedBoot = null;
1920
+ // The UI bundle version this tab loaded against. A change means the served
1921
+ // @enigmax/dashboard bundle was swapped underneath us (a background update found a
1922
+ // newer version) - reload once so the tab picks up the new markup/scripts.
1923
+ let loadedUi = null;
1914
1924
  function render(data) {
1915
1925
  if (data.boot != null) {
1916
1926
  if (loadedBoot === null) loadedBoot = data.boot;
1917
1927
  else if (data.boot !== loadedBoot) { location.reload(); return; }
1918
1928
  }
1929
+ if (data.ui != null) {
1930
+ if (loadedUi === null) loadedUi = data.ui;
1931
+ else if (data.ui !== loadedUi) { location.reload(); return; }
1932
+ }
1919
1933
  priceOverride = Number(data.priceOverride) || 0;
1920
1934
  speedOverride = Number(data.speedOverride) || 0;
1921
1935
  const s = data.stats || {};
@@ -1974,6 +1988,50 @@ export {
1974
1988
  }
1975
1989
  }
1976
1990
 
1991
+ // --- dashboard self-update -------------------------------------------------
1992
+ // Where the latest published dashboard version lives: the raw package.json on the
1993
+ // repo's main branch (no API, no auth, no rate limit). A repo rename 404s here and is
1994
+ // handled like any other failure - the check simply no-ops until the URL resolves again.
1995
+ const DASH_PKG_URL = "https://raw.githubusercontent.com/FJRG2007/enigma/main/packages/dashboard/package.json";
1996
+ const UPDATE_CHECK_MS = 15 * 60 * 1000; // every 15 min; the dashboard rarely ships
1997
+ let updating = false; // one in-flight update at a time; reload happens via the ui-version change
1998
+
1999
+ // Numeric semver-ish compare: true when `a` is strictly newer than `b` (e.g. "0.2.0" > "0.1.9").
2000
+ function isNewer(a, b) {
2001
+ const pa = String(a).split(".").map(Number), pb = String(b).split(".").map(Number);
2002
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
2003
+ const x = pa[i] || 0, y = pb[i] || 0;
2004
+ if (x !== y) return x > y;
2005
+ }
2006
+ return false;
2007
+ }
2008
+
2009
+ // Ask GitHub whether a newer dashboard bundle was published; if so, tell the local
2010
+ // server to fetch it (the ui-version change in /api/stats then reloads the page).
2011
+ // Fully fault-tolerant: an 8s timeout plus try/catch swallow offline/timeout/parse/CORS
2012
+ // failures, and it simply retries on the next interval. Skipped while the tab is hidden.
2013
+ async function checkDashboardUpdate() {
2014
+ if (document.hidden || updating || loadedUi == null) return;
2015
+ let latest;
2016
+ try {
2017
+ const ctrl = new AbortController();
2018
+ const t = setTimeout(() => ctrl.abort(), 8000);
2019
+ let res;
2020
+ try { res = await fetch(DASH_PKG_URL, { cache: "no-store", signal: ctrl.signal }); }
2021
+ finally { clearTimeout(t); }
2022
+ if (!res.ok) return;
2023
+ latest = (await res.json()).version;
2024
+ } catch { return; } // offline, timeout, blocked, or bad JSON: try again next interval
2025
+ if (!latest || !isNewer(latest, loadedUi)) return;
2026
+ // A newer bundle exists: have the local server fetch it. The reload is driven by the
2027
+ // ui-version change in the stats poll, so an npm lag (changed:false) just retries later.
2028
+ updating = true;
2029
+ try {
2030
+ await fetch("/api/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
2031
+ } catch { /* server unreachable: retry next interval */ }
2032
+ finally { updating = false; }
2033
+ }
2034
+
1977
2035
  // --- settings subpage (mirrors the TUI registry over /api/settings) ---
1978
2036
  function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c])); }
1979
2037
 
@@ -2168,6 +2226,7 @@ export {
2168
2226
  item("Context compression (MCP) · measured", boolPill(s.compress))
2169
2227
  + item("Real tool-usage stats · measured", boolPill(s.usageStats))
2170
2228
  + item("Claude Code proxy · experimental", boolPill(s.proxy))
2229
+ + item("Live usage windows · real", boolPill(s.usageApi))
2171
2230
  + proxyMeasured
2172
2231
  + proxyLast
2173
2232
  + promptGuard
@@ -2284,8 +2343,18 @@ export {
2284
2343
  + (s.discarded ? "Disabled - click to enable" : "Enabled - click to disable") + '" data-name="' + esc(s.name)
2285
2344
  + '" data-action="' + (s.discarded ? "enable" : "disable") + '">' + (s.discarded ? "Disabled" : "Enabled") + "</button>"
2286
2345
  : '<button type="button" class="btn-danger" data-name="' + esc(s.name) + '" data-action="remove">Remove</button>';
2346
+ // Per-app on/off chips (Claude Code, Codex, OpenCode...). Only for an enabled enigma
2347
+ // skill: when the whole skill is disabled the per-agent state is moot. data-off is the
2348
+ // value to SET on click (turn off if currently on, on if currently off).
2349
+ const agentChips = (s.source === "enigma" && !s.discarded && s.agentStates && s.agentStates.length)
2350
+ ? '<div class="set-agents">' + s.agentStates.map((a) =>
2351
+ '<button type="button" class="achip' + (a.deployed ? " on" : "") + '" data-action="agent-toggle" data-name="' + esc(s.name)
2352
+ + '" data-agent="' + esc(a.name) + '" data-off="' + (a.deployed ? 1 : 0) + '" title="'
2353
+ + (a.deployed ? "On for " + esc(a.label) + " - click to disable" : "Off for " + esc(a.label) + " - click to enable")
2354
+ + '">' + esc(a.label) + (a.deployed ? " on" : " off") + "</button>").join("") + "</div>"
2355
+ : "";
2287
2356
  return '<div class="set-row"><div class="set-meta"><div class="set-name">' + esc(s.name) + ext + ver + upd + where + "</div>"
2288
- + '<div class="set-hint">' + esc(s.description || "") + '</div></div><div class="set-ctl" style="display:flex;gap:8px">' + edit + main + "</div></div>";
2357
+ + '<div class="set-hint">' + esc(s.description || "") + "</div>" + agentChips + '</div><div class="set-ctl" style="display:flex;gap:8px">' + edit + main + "</div></div>";
2289
2358
  }
2290
2359
 
2291
2360
  function renderSkills(skills) {
@@ -2299,10 +2368,10 @@ export {
2299
2368
  + 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.");
2300
2369
  }
2301
2370
 
2302
- async function postSkill(name, action) {
2371
+ async function postSkill(name, action, content) {
2303
2372
  $("skillsNote").textContent = "Working...";
2304
2373
  try {
2305
- const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, action }) });
2374
+ const res = await fetch("/api/skills", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, action, content }) });
2306
2375
  const out = await res.json();
2307
2376
  if (!out.ok) { $("skillsNote").textContent = "Could not " + action + " " + name + ": " + (out.error || "error"); return; }
2308
2377
  if (out.skills) renderSkills(out.skills);
@@ -2348,6 +2417,7 @@ export {
2348
2417
  if (!b) return;
2349
2418
  const { name, action } = b.dataset;
2350
2419
  if (action === "edit") { openEditor(name, b.dataset.enigma === "1"); return; }
2420
+ if (action === "agent-toggle") { postSkill(name, action, { agent: b.dataset.agent, off: b.dataset.off === "1" }); return; }
2351
2421
  if (action === "remove" && !(await askConfirm("Remove skill", 'Remove the external skill "' + name + '" from all agents? This deletes it from disk.', true))) return;
2352
2422
  postSkill(name, action);
2353
2423
  });
@@ -2507,9 +2577,11 @@ export {
2507
2577
  initChart();
2508
2578
  poll(true);
2509
2579
  loadClaudeStatus();
2580
+ checkDashboardUpdate();
2510
2581
  setInterval(() => refreshAll(false), POLL_MS);
2511
2582
  setInterval(loadClaudeStatus, 60000);
2512
- document.addEventListener("visibilitychange", () => { if (!document.hidden) { refreshAll(true); loadClaudeStatus(); } });
2583
+ setInterval(checkDashboardUpdate, UPDATE_CHECK_MS);
2584
+ document.addEventListener("visibilitychange", () => { if (!document.hidden) { refreshAll(true); loadClaudeStatus(); checkDashboardUpdate(); } });
2513
2585
  wireSettings();
2514
2586
  wireSkills();
2515
2587
  wireAccounts();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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": [