@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.
- package/assets/index.html +76 -13
- 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
|
-
|
|
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
|
|
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
|
|
2250
|
-
renderSettings(
|
|
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
|
|
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.
|
|
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": [
|