@enigmax/dashboard 0.1.36 → 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 +76 -13
  2. package/package.json +1 -1
package/assets/index.html CHANGED
@@ -334,6 +334,8 @@
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>
@@ -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
 
@@ -2241,16 +2268,39 @@ export {
2241
2268
  const inp = e.target.closest("input.linput");
2242
2269
  if (inp) { e.preventDefault(); postListOp(inp.dataset.skey, "add", inp.value); inp.value = ""; }
2243
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(); } });
2244
2274
  }
2245
2275
 
2246
2276
  let settingsLoaded = false;
2247
2277
  async function loadSettings() {
2248
2278
  try {
2249
- const res = await fetch("/api/settings", { cache: "no-store" });
2250
- renderSettings((await res.json()).categories || []);
2279
+ const data = await (await fetch("/api/settings", { cache: "no-store" })).json();
2280
+ renderSettings(data.categories || []);
2281
+ renderDashPort(data);
2251
2282
  } catch { $("settingsBody").innerHTML = '<div class="empty" style="padding:24px 0">Settings unavailable.</div>'; }
2252
2283
  }
2253
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
+
2254
2304
  // --- "Enigma systems" overview (factual state, never invented savings) ---
2255
2305
  function pill(state, text) { return '<span class="pill ' + state + '">' + esc(text) + "</span>"; }
2256
2306
  function boolPill(b) { return b ? pill("on", "on") : pill("off", "off"); }
@@ -2573,7 +2623,19 @@ export {
2573
2623
 
2574
2624
  // --- accounts & profiles subpage (mirrors the TUI hub over /api/accounts) ---
2575
2625
  let accountsLoaded = false;
2576
- 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
+ }
2577
2639
  async function loadAccounts() {
2578
2640
  try { const r = await fetch("/api/accounts", { cache: "no-store" }); renderAccounts(await r.json()); }
2579
2641
  catch { $("accountsBody").innerHTML = '<div class="empty" style="padding:24px 0">Accounts unavailable.</div>'; }
@@ -2616,7 +2678,7 @@ export {
2616
2678
  function renderAccounts(d) {
2617
2679
  d = d || {};
2618
2680
  const accounts = d.accounts || [], profiles = d.profiles || [], tools = d.tools || [];
2619
- accTools = tools;
2681
+ accTools = tools; accAccounts = accounts; accProfiles = profiles;
2620
2682
  $("accountsBody").innerHTML = accounts.length ? accounts.map(accountRow).join("") : '<div class="empty" style="padding:20px 0">No accounts.</div>';
2621
2683
  $("profilesBody").innerHTML = profiles.length ? profiles.map((p) => profileRow(p, tools, accounts)).join("")
2622
2684
  : '<div class="empty" style="padding:20px 0">No profiles yet. Add one to pin an account per tool.</div>';
@@ -2635,16 +2697,17 @@ export {
2635
2697
  const tool = accTools.length === 1 ? accTools[0].name
2636
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?");
2637
2699
  if (!tool) return;
2638
- 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));
2639
2702
  if (name) accAction("account.add", { tool, name });
2640
2703
  });
2641
- $("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 }); });
2642
2705
  $("accountsBody").addEventListener("click", async (e) => {
2643
2706
  const b = e.target.closest("button[data-acc]"); if (!b) return;
2644
2707
  const op = b.dataset.acc, tool = b.dataset.tool, name = b.dataset.name;
2645
2708
  if (op === "activate") accAction("account.activate", { tool, name });
2646
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 }); }
2647
- 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 }); }
2648
2711
  else if (op === "login") { $("accountsNote").textContent = "To log in, run this in a terminal: enigma " + tool + " " + name; }
2649
2712
  });
2650
2713
  $("profilesBody").addEventListener("click", async (e) => {
@@ -2652,7 +2715,7 @@ export {
2652
2715
  const op = b.dataset.profAct, name = b.dataset.name;
2653
2716
  if (op === "activate") accAction("profile.activate", { name });
2654
2717
  else if (op === "remove") { if (await askConfirm("Remove profile", "Remove profile '" + name + "'? (its accounts are kept)", true)) accAction("profile.remove", { name }); }
2655
- 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 }); }
2656
2719
  });
2657
2720
  $("profilesBody").addEventListener("change", (e) => {
2658
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.36",
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": [