@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.
- package/assets/index.html +79 -14
- 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">
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|
2248
|
-
renderSettings(
|
|
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
|
|
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.
|
|
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": [
|