@enigmax/dashboard 0.1.35 → 0.1.38

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 +79 -14
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -334,13 +334,15 @@
334
334
  .modal-input:focus { outline: none; border-color: var(--accent); }
335
335
  .modal-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
336
336
  .modal-ok.danger { background: var(--accent2); border-color: var(--accent2); color: #2a0f08; }
337
+ .modal-ok:disabled { opacity: .45; cursor: not-allowed; }
338
+ .modal-err { color: var(--bad); font-size: 12px; margin-top: 8px; }
337
339
  </style>
338
340
  </head>
339
341
  <body>
340
342
  <header>
341
343
  <div class="brand">
342
344
  <div class="wordmark">Enigma<span aria-hidden="true">.</span></div>
343
- <div class="sub">Savings dashboard</div>
345
+ <div class="sub">Control panel</div>
344
346
  </div>
345
347
  <nav class="tabs">
346
348
  <a href="#/" data-view="savings" class="tab active">Savings</a>
@@ -565,6 +567,19 @@
565
567
  </div>
566
568
  <div id="settingsNote" class="set-note"></div>
567
569
  <div id="settingsBody"><div class="empty" style="padding:24px 0">Loading settings...</div></div>
570
+ <div class="panel" id="dashPortPanel">
571
+ <div class="set-cat-h">Dashboard port</div>
572
+ <div class="set-cat-b">the port this dashboard binds. Leave 0 for auto (tries 80, then 24282, then an ephemeral port). Changing it needs a dashboard restart.</div>
573
+ <div class="set-grid">
574
+ <div class="set-row">
575
+ <div class="set-meta"><div class="set-name">Preferred port</div><div class="set-hint" id="dashPortHint">loopback only; never network-facing</div></div>
576
+ <div class="set-ctl" style="display:flex;gap:8px;align-items:center">
577
+ <input id="dashPortInput" type="number" min="0" max="65535" step="1" class="choice" style="width:120px" inputmode="numeric" />
578
+ <button id="dashPortSave" type="button" class="toggle on">Save</button>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </div>
568
583
  </section>
569
584
 
570
585
  <section id="view-resources" style="display:none">
@@ -2096,7 +2111,8 @@ export {
2096
2111
  inner = '<select class="modal-input">' + field.options.map((o) =>
2097
2112
  '<option value="' + esc(o.value) + '"' + (o.value === field.value ? " selected" : "") + ">" + esc(o.label || o.value) + "</option>").join("") + "</select>";
2098
2113
  } else if (field) {
2099
- inner = '<input class="modal-input" type="text" value="' + esc(field.value || "") + '" placeholder="' + esc(field.placeholder || "") + '">';
2114
+ inner = '<input class="modal-input" type="text" value="' + esc(field.value || "") + '" placeholder="' + esc(field.placeholder || "") + '">'
2115
+ + (field.validate ? '<div class="modal-err" role="alert"></div>' : "");
2100
2116
  }
2101
2117
  back.innerHTML = '<div class="modal" role="dialog" aria-modal="true">'
2102
2118
  + '<div class="modal-title">' + esc(title) + "</div>"
@@ -2107,21 +2123,32 @@ export {
2107
2123
  document.body.appendChild(back);
2108
2124
  requestAnimationFrame(() => back.classList.add("show"));
2109
2125
  const inp = back.querySelector(".modal-input");
2126
+ const okBtn = back.querySelector(".modal-ok");
2127
+ const validate = field && field.validate, errEl = back.querySelector(".modal-err");
2128
+ let invalid = false;
2129
+ const runValidate = () => {
2130
+ if (!validate || !inp) return;
2131
+ const v = String(inp.value).trim(), msg = v ? validate(v) : null;
2132
+ invalid = !!msg || v === ""; // empty is incomplete (block submit) but shows no error text
2133
+ if (errEl) errEl.textContent = msg || "";
2134
+ okBtn.disabled = invalid;
2135
+ };
2110
2136
  let done = false;
2111
2137
  const close = (val) => { if (done) return; done = true; back.classList.remove("show"); setTimeout(() => back.remove(), 200); resolve(val); };
2112
- const ok = () => close(field ? (inp ? inp.value : true) : true);
2138
+ const ok = () => { if (invalid) return; close(field ? (inp ? inp.value : true) : true); };
2113
2139
  const cancel = () => close(field ? null : false);
2114
- back.querySelector(".modal-ok").addEventListener("click", ok);
2140
+ okBtn.addEventListener("click", ok);
2141
+ if (inp && validate) { inp.addEventListener("input", runValidate); runValidate(); }
2115
2142
  back.querySelector(".modal-cancel").addEventListener("click", cancel);
2116
2143
  back.addEventListener("mousedown", (e) => { if (e.target === back) cancel(); });
2117
2144
  back.addEventListener("keydown", (e) => {
2118
2145
  if (e.key === "Escape") { e.preventDefault(); cancel(); }
2119
2146
  else if (e.key === "Enter" && (!inp || inp.tagName !== "TEXTAREA")) { e.preventDefault(); ok(); }
2120
2147
  });
2121
- if (inp) { inp.focus(); if (inp.select) inp.select(); } else back.querySelector(".modal-ok").focus();
2148
+ if (inp) { inp.focus(); if (inp.select) inp.select(); } else okBtn.focus();
2122
2149
  });
2123
2150
  }
2124
- const askText = (title, value, message) => openModal({ title, message, field: { value: value || "" } }).then((v) => (v == null ? null : String(v).trim()));
2151
+ const askText = (title, value, message, validate) => openModal({ title, message, field: { value: value || "", validate } }).then((v) => (v == null ? null : String(v).trim()));
2125
2152
  const askChoice = (title, options, value, message) => openModal({ title, message, field: { options, value } });
2126
2153
  const askConfirm = (title, message, danger) => openModal({ title, message, danger, okLabel: danger ? "Remove" : "OK" }).then(Boolean);
2127
2154
 
@@ -2181,6 +2208,8 @@ export {
2181
2208
 
2182
2209
  function renderSettings(cats) {
2183
2210
  const el = $("settingsBody");
2211
+ // Skill-related settings live in the Skills tab; drop them here so they aren't shown twice.
2212
+ cats = (cats || []).map((c) => ({ ...c, settings: (c.settings || []).filter((s) => SKILL_SETTING_KEYS.indexOf(s.key) === -1) })).filter((c) => c.settings.length);
2184
2213
  if (!cats.length) { el.innerHTML = '<div class="empty" style="padding:24px 0">No settings available.</div>'; return; }
2185
2214
  // One panel per category; settings laid out in a responsive 2-column grid so a
2186
2215
  // long config list stays scannable on a wide screen.
@@ -2239,16 +2268,39 @@ export {
2239
2268
  const inp = e.target.closest("input.linput");
2240
2269
  if (inp) { e.preventDefault(); postListOp(inp.dataset.skey, "add", inp.value); inp.value = ""; }
2241
2270
  });
2271
+ const save = $("dashPortSave"), portInp = $("dashPortInput");
2272
+ if (save) save.addEventListener("click", saveDashPort);
2273
+ if (portInp) portInp.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); saveDashPort(); } });
2242
2274
  }
2243
2275
 
2244
2276
  let settingsLoaded = false;
2245
2277
  async function loadSettings() {
2246
2278
  try {
2247
- const res = await fetch("/api/settings", { cache: "no-store" });
2248
- renderSettings((await res.json()).categories || []);
2279
+ const data = await (await fetch("/api/settings", { cache: "no-store" })).json();
2280
+ renderSettings(data.categories || []);
2281
+ renderDashPort(data);
2249
2282
  } catch { $("settingsBody").innerHTML = '<div class="empty" style="padding:24px 0">Settings unavailable.</div>'; }
2250
2283
  }
2251
2284
 
2285
+ // Numeric dashboard-port control (outside the boolean/choice registry, like token-price).
2286
+ function renderDashPort(data) {
2287
+ const inp = $("dashPortInput");
2288
+ if (!inp) return;
2289
+ inp.value = data && data.dashboardPort ? data.dashboardPort : "";
2290
+ const running = data && data.runningPort;
2291
+ $("dashPortHint").textContent = running ? ("currently running on port " + running + " - loopback only") : "loopback only; never network-facing";
2292
+ }
2293
+ async function saveDashPort() {
2294
+ const raw = $("dashPortInput").value.trim();
2295
+ const value = raw === "" ? 0 : Number(raw);
2296
+ if (!Number.isInteger(value) || value < 0 || value > 65535) { $("settingsNote").textContent = "Port must be 0 (auto) or 1-65535."; return; }
2297
+ $("settingsNote").textContent = "Saving dashboard port...";
2298
+ try {
2299
+ const out = await (await fetch("/api/dashboard-port", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ value }) })).json();
2300
+ $("settingsNote").textContent = out.ok ? (out.restartNote || ("Saved dashboard port = " + (value || "auto") + ".")) : ("Could not save: " + (out.error || "error"));
2301
+ } catch { $("settingsNote").textContent = "Could not reach the server to set the port."; }
2302
+ }
2303
+
2252
2304
  // --- "Enigma systems" overview (factual state, never invented savings) ---
2253
2305
  function pill(state, text) { return '<span class="pill ' + state + '">' + esc(text) + "</span>"; }
2254
2306
  function boolPill(b) { return b ? pill("on", "on") : pill("off", "off"); }
@@ -2571,7 +2623,19 @@ export {
2571
2623
 
2572
2624
  // --- accounts & profiles subpage (mirrors the TUI hub over /api/accounts) ---
2573
2625
  let accountsLoaded = false;
2574
- let accTools = [];
2626
+ let accTools = [], accAccounts = [], accProfiles = [];
2627
+ // Real-time name validator mirroring the server (accounts.ts validateAccountName + the
2628
+ // duplicate checks), so the modal blocks an invalid or already-used name as the user types.
2629
+ // `taken` = names already in scope (per-tool for accounts, global for profiles); `current`
2630
+ // = the name being renamed (allowed to keep itself), or null when adding.
2631
+ function nameValidator(taken, current) {
2632
+ return (v) => {
2633
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(v)) return "Use letters, digits, '.', '_' or '-' (max 64).";
2634
+ if (v === "default") return "'default' is a reserved name.";
2635
+ if (v !== current && taken.indexOf(v) !== -1) return "That name is already in use.";
2636
+ return null;
2637
+ };
2638
+ }
2575
2639
  async function loadAccounts() {
2576
2640
  try { const r = await fetch("/api/accounts", { cache: "no-store" }); renderAccounts(await r.json()); }
2577
2641
  catch { $("accountsBody").innerHTML = '<div class="empty" style="padding:24px 0">Accounts unavailable.</div>'; }
@@ -2614,7 +2678,7 @@ export {
2614
2678
  function renderAccounts(d) {
2615
2679
  d = d || {};
2616
2680
  const accounts = d.accounts || [], profiles = d.profiles || [], tools = d.tools || [];
2617
- accTools = tools;
2681
+ accTools = tools; accAccounts = accounts; accProfiles = profiles;
2618
2682
  $("accountsBody").innerHTML = accounts.length ? accounts.map(accountRow).join("") : '<div class="empty" style="padding:20px 0">No accounts.</div>';
2619
2683
  $("profilesBody").innerHTML = profiles.length ? profiles.map((p) => profileRow(p, tools, accounts)).join("")
2620
2684
  : '<div class="empty" style="padding:20px 0">No profiles yet. Add one to pin an account per tool.</div>';
@@ -2633,16 +2697,17 @@ export {
2633
2697
  const tool = accTools.length === 1 ? accTools[0].name
2634
2698
  : await askChoice("Add account", accTools.map((t) => ({ value: t.name, label: t.label || t.name })), accTools[0] ? accTools[0].name : "", "Which tool is this account for?");
2635
2699
  if (!tool) return;
2636
- const name = await askText("Add account", "", "New account name (e.g. work):");
2700
+ const taken = accAccounts.filter((a) => a.tool === tool).map((a) => a.name);
2701
+ const name = await askText("Add account", "", "New account name (e.g. work):", nameValidator(taken, null));
2637
2702
  if (name) accAction("account.add", { tool, name });
2638
2703
  });
2639
- $("profAdd").addEventListener("click", async () => { const name = await askText("Add profile", "", "New profile name:"); if (name) accAction("profile.add", { name }); });
2704
+ $("profAdd").addEventListener("click", async () => { const name = await askText("Add profile", "", "New profile name:", nameValidator(accProfiles.map((p) => p.name), null)); if (name) accAction("profile.add", { name }); });
2640
2705
  $("accountsBody").addEventListener("click", async (e) => {
2641
2706
  const b = e.target.closest("button[data-acc]"); if (!b) return;
2642
2707
  const op = b.dataset.acc, tool = b.dataset.tool, name = b.dataset.name;
2643
2708
  if (op === "activate") accAction("account.activate", { tool, name });
2644
2709
  else if (op === "remove") { if (await askConfirm("Remove account", "Remove account '" + name + "' and delete its local config dir?", true)) accAction("account.remove", { tool, name }); }
2645
- else if (op === "rename") { const nn = await askText("Rename account", name, "Rename '" + name + "' to:"); if (nn && nn !== name) accAction("account.rename", { tool, name, newName: nn }); }
2710
+ else if (op === "rename") { const taken = accAccounts.filter((a) => a.tool === tool).map((a) => a.name); const nn = await askText("Rename account", name, "Rename '" + name + "' to:", nameValidator(taken, name)); if (nn && nn !== name) accAction("account.rename", { tool, name, newName: nn }); }
2646
2711
  else if (op === "login") { $("accountsNote").textContent = "To log in, run this in a terminal: enigma " + tool + " " + name; }
2647
2712
  });
2648
2713
  $("profilesBody").addEventListener("click", async (e) => {
@@ -2650,7 +2715,7 @@ export {
2650
2715
  const op = b.dataset.profAct, name = b.dataset.name;
2651
2716
  if (op === "activate") accAction("profile.activate", { name });
2652
2717
  else if (op === "remove") { if (await askConfirm("Remove profile", "Remove profile '" + name + "'? (its accounts are kept)", true)) accAction("profile.remove", { name }); }
2653
- else if (op === "rename") { const nn = await askText("Rename profile", name, "Rename profile '" + name + "' to:"); if (nn && nn !== name) accAction("profile.rename", { name, newName: nn }); }
2718
+ else if (op === "rename") { const nn = await askText("Rename profile", name, "Rename profile '" + name + "' to:", nameValidator(accProfiles.map((p) => p.name), name)); if (nn && nn !== name) accAction("profile.rename", { name, newName: nn }); }
2654
2719
  });
2655
2720
  $("profilesBody").addEventListener("change", (e) => {
2656
2721
  const sel = e.target.closest("select[data-prof-tool]"); if (!sel) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/dashboard",
3
- "version": "0.1.35",
3
+ "version": "0.1.38",
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": [