@enigmax/dashboard 0.1.42 → 0.1.44

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 +85 -16
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -252,6 +252,8 @@
252
252
  .aicons { display: inline-flex; gap: 5px; align-items: center; vertical-align: middle; margin-left: 6px; }
253
253
  .aicon { display: inline-flex; align-items: center; color: var(--muted); }
254
254
  .aicon svg { width: 14px; height: 14px; display: block; }
255
+ /* Brand icon + label, reused by the savings breakdowns and the recent-compressions table. */
256
+ .srctag { display: inline-flex; align-items: center; gap: 7px; }
255
257
  .set-ic { vertical-align: middle; margin-right: 7px; }
256
258
  /* List settings (guard block/allow globs, custom secret patterns): chips + add input. A list row spans the full grid width. */
257
259
  .set-row.list { grid-column: 1 / -1; align-items: stretch; flex-direction: column; }
@@ -352,6 +354,19 @@
352
354
  /* Usage view provider/account pickers. */
353
355
  .useg-pick { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
354
356
  .useg-pick select { width: auto; min-width: 170px; }
357
+ /* Icon dropdown: a native <select> can't render icons in its <option>s, so the provider
358
+ picker is a custom listbox - the logo shows on the button AND inside every option. */
359
+ .dd { position: relative; }
360
+ .dd-btn { display: inline-flex; align-items: center; gap: 8px; min-width: 170px; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 10px; font-size: 12px; font-family: var(--mono); cursor: pointer; }
361
+ .dd-btn:hover { border-color: #2f3744; }
362
+ .dd-btn .dd-label { flex: 1; text-align: left; }
363
+ .dd-btn .dd-caret { width: 13px; height: 13px; flex: 0 0 auto; color: var(--muted); }
364
+ .dd-ic { display: inline-flex; align-items: center; }
365
+ .dd-ic svg { width: 14px; height: 14px; display: block; }
366
+ .dd-menu { position: absolute; top: calc(100% + 4px); left: 0; min-width: 100%; margin: 0; padding: 4px; list-style: none; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow-2); z-index: 50; }
367
+ .dd-opt { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; font-size: 12px; color: var(--text); white-space: nowrap; cursor: pointer; }
368
+ .dd-opt:hover, .dd-opt:focus { background: var(--surface2); outline: none; }
369
+ .dd-opt.sel { color: var(--accent); }
355
370
  /* Icon picker: clickable app cards (a native <select> can't render SVG in options). */
356
371
  .modal-picker { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
357
372
  .pick-card {
@@ -505,7 +520,12 @@
505
520
  </div>
506
521
  </div>
507
522
  <div class="useg-controls" style="display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-bottom:16px">
508
- <label class="useg-pick"><span>Provider</span><select id="uProvider" class="modal-input"></select></label>
523
+ <div class="useg-pick"><span>Provider</span>
524
+ <div class="dd" id="uProviderDD">
525
+ <button type="button" class="dd-btn" aria-haspopup="listbox" aria-expanded="false"><span class="dd-ic"></span><span class="dd-label"></span><svg class="dd-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg></button>
526
+ <ul class="dd-menu" role="listbox" aria-label="Provider" hidden></ul>
527
+ </div>
528
+ </div>
509
529
  <label class="useg-pick" id="uAccountWrap"><span>Account</span><select id="uAccount" class="modal-input"></select></label>
510
530
  </div>
511
531
  <div class="panel" id="usagePanel" style="display:none">
@@ -616,8 +636,8 @@
616
636
  <div class="set-grid">
617
637
  <div class="set-row">
618
638
  <div class="set-meta"><div class="set-name">Preferred port</div><div class="set-hint" id="dashPortHint">loopback only; never network-facing</div></div>
619
- <div class="set-ctl" style="display:flex;gap:8px;align-items:center">
620
- <input id="dashPortInput" type="number" min="0" max="65535" step="1" class="choice" style="width:120px" inputmode="numeric" />
639
+ <div class="set-ctl ladd" style="align-items:center">
640
+ <input id="dashPortInput" type="number" min="0" max="65535" step="1" class="linput" style="flex:none;width:120px" inputmode="numeric" />
621
641
  <button id="dashPortSave" type="button" class="toggle on">Save</button>
622
642
  </div>
623
643
  </div>
@@ -1644,7 +1664,7 @@ export {
1644
1664
  function prettyType(k) { return TYPE_LABELS[k] || String(k).replace(/\b\w/g, (c) => c.toUpperCase()); }
1645
1665
 
1646
1666
  // One bar per key (source or type), sorted by tokens saved, with an optional $ column.
1647
- function renderBreakdown(el, map, label, money) {
1667
+ function renderBreakdown(el, map, label, money, iconFn) {
1648
1668
  const entries = Object.entries(map || {}).sort((a, b) => (b[1].tokensSaved || 0) - (a[1].tokensSaved || 0));
1649
1669
  if (!entries.length) {
1650
1670
  el.innerHTML = '<div class="empty" style="padding:20px 0">No data yet - this fills in as compressions are recorded.</div>';
@@ -1654,7 +1674,7 @@ export {
1654
1674
  el.innerHTML = entries.map(([k, v]) => {
1655
1675
  const saved = v.tokensSaved || 0, pct = Math.round(saved / max * 100);
1656
1676
  const m = money ? '<span class="money">' + fmtUsd(moneyOf(k, saved)) + '</span>' : '';
1657
- return '<div class="bar-row"><span class="name" style="width:140px">' + label(k)
1677
+ return '<div class="bar-row"><span class="name" style="width:140px">' + iconLabel(iconFn ? iconFn(k) : "", label(k))
1658
1678
  + '</span><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--accent)"></div></div>'
1659
1679
  + '<span class="num">' + fmt(saved) + '</span>' + m
1660
1680
  + '<span class="foot" style="width:64px;text-align:right;margin:0">' + fmt(v.calls || 0) + ' calls</span></div>';
@@ -1676,8 +1696,9 @@ export {
1676
1696
  + '<th class="r">Before</th><th class="r">After</th><th class="r">Saved</th></tr></thead><tbody>'
1677
1697
  + rows.map((p) => {
1678
1698
  const saved = savedOf(p), pct = p.b ? Math.round(saved / p.b * 100) : 0;
1699
+ const src = p.s || "unknown";
1679
1700
  return '<tr><td>' + new Date(p.t).toLocaleString() + '</td><td>' + prettyType(p.c || "unknown")
1680
- + '</td><td>' + prettySource(p.s || "unknown") + '</td><td class="r">' + fmt(p.b || 0)
1701
+ + '</td><td>' + iconLabel(sourceSvg(src), prettySource(src)) + '</td><td class="r">' + fmt(p.b || 0)
1681
1702
  + '</td><td class="r">' + fmt(p.a || 0) + '</td><td class="r good">' + fmt(saved) + " (" + pct + "%)</td></tr>";
1682
1703
  }).join("") + "</tbody></table>";
1683
1704
  }
@@ -1851,10 +1872,17 @@ export {
1851
1872
  el.onclick = null; el.style.cursor = ""; el.title = ""; // clear any prior incident affordance
1852
1873
  let ind = "unknown", desc = p.label + " status unavailable";
1853
1874
  try {
1854
- const r = await fetch(p.status, { cache: "no-store" });
1855
- const d = await r.json();
1856
- ind = (d.status && d.status.indicator) || "unknown";
1857
- desc = (d.status && d.status.description) || "Status unknown";
1875
+ // Prefer the same-origin server proxy: the browser's direct cross-origin fetch is
1876
+ // unreliable in the field (blockers/MITM proxies/captive portals returned a stub
1877
+ // 200 -> a bogus "Status unknown"). Fall back to direct if the server predates it.
1878
+ let s = null;
1879
+ try {
1880
+ const rp = await fetch("/api/provider-status?provider=" + encodeURIComponent(usageProvider), { cache: "no-store" });
1881
+ if (rp.ok) { const dp = await rp.json(); if (!dp.error) s = dp; }
1882
+ } catch { /* fall through to the direct statuspage */ }
1883
+ if (!s) { const d = await (await fetch(p.status, { cache: "no-store" })).json(); s = d.status || {}; }
1884
+ ind = s.indicator || "unknown";
1885
+ desc = s.description || "Status unknown";
1858
1886
  } catch { ind = "unknown"; }
1859
1887
  // Claude Code only: overlay the community incident signal. It elevates an otherwise
1860
1888
  // green/unknown pill to a warning; an official major/critical statuspage state wins.
@@ -1876,12 +1904,44 @@ export {
1876
1904
  try { await fetch("/api/plan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: Number(value) }) }); refreshAll(true); }
1877
1905
  catch { /* leave as-is */ }
1878
1906
  }
1907
+ // Accessible icon dropdown (a native <select> can't render icons in its <option>s). The
1908
+ // button shows the selected entry's icon + label; the listbox shows every entry with its
1909
+ // icon. entries: [{ value, label, icon }]; getVal()/onPick(value) bind it to state.
1910
+ function iconDropdown(rootId, entries, getVal, onPick) {
1911
+ const root = $(rootId), btn = root.querySelector(".dd-btn"), menu = root.querySelector(".dd-menu");
1912
+ const btnIc = btn.querySelector(".dd-ic"), btnLabel = btn.querySelector(".dd-label");
1913
+ const opts = () => [...menu.querySelectorAll(".dd-opt")];
1914
+ const render = () => {
1915
+ const cur = getVal(), e = entries.find((x) => x.value === cur) || entries[0];
1916
+ btnIc.innerHTML = e.icon || ""; btnLabel.textContent = e.label;
1917
+ menu.innerHTML = entries.map((x) => '<li class="dd-opt' + (x.value === cur ? " sel" : "")
1918
+ + '" role="option" tabindex="-1" data-val="' + esc(x.value) + '" aria-selected="' + (x.value === cur)
1919
+ + '"><span class="dd-ic">' + (x.icon || "") + "</span><span>" + esc(x.label) + "</span></li>").join("");
1920
+ };
1921
+ const close = () => { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); };
1922
+ const open = () => { menu.hidden = false; btn.setAttribute("aria-expanded", "true"); (menu.querySelector(".sel") || opts()[0]) && (menu.querySelector(".sel") || opts()[0]).focus(); };
1923
+ const pick = (li) => { close(); btn.focus(); if (li && li.dataset.val) onPick(li.dataset.val); };
1924
+ btn.addEventListener("click", (e) => { e.stopPropagation(); menu.hidden ? open() : close(); });
1925
+ btn.addEventListener("keydown", (e) => { if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); } });
1926
+ menu.addEventListener("click", (e) => pick(e.target.closest(".dd-opt")));
1927
+ menu.addEventListener("keydown", (e) => {
1928
+ const list = opts(), i = list.indexOf(document.activeElement);
1929
+ if (e.key === "Escape") { e.preventDefault(); close(); btn.focus(); }
1930
+ else if (e.key === "ArrowDown") { e.preventDefault(); (list[i + 1] || list[0]).focus(); }
1931
+ else if (e.key === "ArrowUp") { e.preventDefault(); (list[i - 1] || list[list.length - 1]).focus(); }
1932
+ else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(document.activeElement); }
1933
+ });
1934
+ document.addEventListener("click", close);
1935
+ render();
1936
+ return { render };
1937
+ }
1938
+ let providerDD = null;
1879
1939
  function wireUsage() {
1880
- // Provider + account selectors drive a client-side re-render from the cached report.
1881
- const ps = $("uProvider");
1882
- ps.innerHTML = Object.entries(USAGE_PROVIDERS).map(([k, v]) => '<option value="' + k + '">' + esc(v.label) + "</option>").join("");
1883
- ps.value = usageProvider;
1884
- ps.addEventListener("change", () => { usageProvider = ps.value; usageAccount = "__all__"; applyUsageView(); loadProviderStatus(); });
1940
+ // The provider picker is a custom icon dropdown; the account one stays a native select.
1941
+ providerDD = iconDropdown("uProviderDD",
1942
+ Object.entries(USAGE_PROVIDERS).map(([k, v]) => ({ value: k, label: v.label, icon: agentSvg(k) })),
1943
+ () => usageProvider,
1944
+ (val) => { if (val === usageProvider) return; usageProvider = val; usageAccount = "__all__"; applyUsageView(); loadProviderStatus(); });
1885
1945
  $("uAccount").addEventListener("change", () => { usageAccount = $("uAccount").value; applyUsageView(); });
1886
1946
  const el = $("uWindows");
1887
1947
  el.addEventListener("click", (e) => {
@@ -1992,6 +2052,7 @@ export {
1992
2052
  // measurable local store; the others show an honest "no local usage" panel instead.
1993
2053
  function applyUsageView() {
1994
2054
  const p = USAGE_PROVIDERS[usageProvider] || USAGE_PROVIDERS.claude;
2055
+ if (providerDD) providerDD.render();
1995
2056
  $("usageTitle").textContent = p.label + " Usage";
1996
2057
  const link = $("usageViewLink");
1997
2058
  link.href = p.viewHref; link.textContent = p.viewLabel + " ->";
@@ -2201,7 +2262,7 @@ export {
2201
2262
  $("removed").textContent = fmt(saved) + " (" + rate + "%)";
2202
2263
  $("reduction").textContent = before ? (100 - rate) + "% of input sent" : "-";
2203
2264
  $("ratio").textContent = after ? "~" + (before / after).toFixed(1) + ":1" : "-";
2204
- renderBreakdown($("sources"), s.bySource, prettySource, true);
2265
+ renderBreakdown($("sources"), s.bySource, prettySource, true, sourceSvg);
2205
2266
  renderBreakdown($("types"), s.byType, prettyType, false);
2206
2267
 
2207
2268
  renderUsage(data.usage);
@@ -2688,6 +2749,14 @@ export {
2688
2749
  const AGENT_NAME_BY_LABEL = { "Claude Code": "claude", "OpenAI Codex": "codex", "OpenCode": "opencode" };
2689
2750
  const agentSvg = (name) => AGENT_ICONS[name] || "";
2690
2751
 
2752
+ // Compression sources -> a brand icon. Reuses the app icons; the CLI source is enigma itself
2753
+ // (the monochrome cube mark, recoloured to currentColor so it sits uniformly in the tables).
2754
+ const ENIGMA_ICON = '<svg viewBox="0 0 2187.36 2500.37" fill="currentColor" aria-hidden="true"><path d="m2152.75 605.14c-2.56-1.46-5.13-2.93-8.06-4.76q-254.23-145.51-507.76-291.38-266.32-152.69-532.28-305.34l-6.23-3.66-6.59 2.56a202.53 202.53 0 0 0 -29.28 13.55q-231.73 135.63-463.1 270.89c-191.83 112.4-384.03 224.42-574.39 336.44-23.8 11-23.07 35.88-22.7 50.89 0 3.29.37 7 0 10.62q-.55 426.12-1.46 852.24l-.74 316.3c-1.46 19 7 35.15 21.24 41.74q330 187.24 660.05 375.23l389.15 221.54c8.79 5.49 17.57 8.42 25.63 8.42a29.12 29.12 0 0 0 18.3-6.23l564.47-325.13c160.35-92.26 320.7-184.88 479.94-276.4 17.94-8.05 28.56-26 26.36-42.83l1.83-1197.1c2.56-27.46-20.5-40.27-34.41-47.59m-1058.32 75.41 464.19 270.17-463.46 267.28-157.79-89.33q-155.94-88.41-311.9-176.45l469-271.64m527.52 233.57-504.1-293.24c-13.91-9.88-32.21-9.88-45.39-1.1l-510.73 295.77-463.83-262.12 323.62-189.26q336.62-197.15 673.6-393.55 309.15 178.47 618.68 355.47 188.91 108.74 377.8 217.09l-469.69 270.91m-1555.86-205.74 461.63 261.38 9.52 576.59c-1.1 17.93 7.69 34 21.6 40.27q153.19 88.38 307.19 176.45l197 113.12-.36 538.51-998.78-568.16zm525.7 297.62 471.88 266.88-.73 530.46-462.36-265.78zm534.85 870.56 197.32-113.12c101-58.21 202.45-116 304.22-174.63l2.93-1.83c5.49-3.29 12.81-7.69 17.93-15l2.93-4v-4.76c-1.09-127-1.09-256.63-.73-381.83 0-70.29.37-140.58 0-210.87l472.25-272.36-.73 464.56-1.1 678.36q-316.29 181.76-633 364.25l-362.79 209 .73-537.78m0-72.86.37-530.82 464.93-268v531.92q-232.29 132.89-465.3 266.87"/></svg>';
2755
+ const SOURCE_ICON_KEY = { "claude-code": "claude", "claude": "claude", "claude-desktop": "claude", "opencode": "opencode", "codex": "codex", "codex-cli": "codex" };
2756
+ function sourceSvg(k) { return k === "cli" ? ENIGMA_ICON : (AGENT_ICONS[SOURCE_ICON_KEY[k]] || ""); }
2757
+ // Brand icon + text in one inline tag; the icon is omitted when there is none (e.g. content types).
2758
+ function iconLabel(ic, text) { return '<span class="srctag">' + (ic ? '<span class="aicon">' + ic + '</span>' : '') + esc(text) + '</span>'; }
2759
+
2691
2760
  function skillRow(s) {
2692
2761
  const ver = s.version ? '<span class="tag">v' + esc(s.version) + "</span>" : "";
2693
2762
  // Last-edited date (from skill.json), shown relative via the vendored <relative-time>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
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": [