@enigmax/dashboard 0.1.61 → 0.1.63
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 +367 -48
- package/package.json +1 -1
package/assets/index.html
CHANGED
|
@@ -200,19 +200,92 @@
|
|
|
200
200
|
background: var(--surface2); color: var(--text); border: 1px solid var(--border);
|
|
201
201
|
border-radius: var(--radius-sm); padding: 6px 10px; font-size: 12px; font-family: var(--mono); cursor: pointer;
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
/* Top-of-page "update available" banner: spans the content width, amber-tinted so it
|
|
204
|
+
reads as a notice without alarming. Hidden until the stats poll reports an update. */
|
|
205
|
+
.update-alert {
|
|
206
|
+
display: flex; align-items: center; gap: 12px;
|
|
207
|
+
max-width: 1040px; margin: 16px auto -8px; padding: 11px 16px;
|
|
208
|
+
background: rgba(215,135,95,0.10); border: 1px solid rgba(215,135,95,0.45);
|
|
209
|
+
border-radius: var(--radius-sm); color: var(--text); font-size: 13px;
|
|
210
|
+
}
|
|
211
|
+
.update-alert[hidden] { display: none; }
|
|
212
|
+
.update-alert .ua-ico { width: 18px; height: 18px; color: var(--accent); flex: none; }
|
|
213
|
+
.update-alert .ua-msg { flex: 1; }
|
|
214
|
+
.update-alert .ua-x { background: none; border: none; color: var(--muted); font-size: 20px; line-height: 1; cursor: pointer; padding: 0 4px; }
|
|
215
|
+
.update-alert .ua-x:hover { color: var(--text); }
|
|
216
|
+
/* Status notes now surface as toasts; the inline element stays for call compatibility but
|
|
217
|
+
collapses when empty so no leftover green/red line shows. */
|
|
218
|
+
.set-note { color: var(--accent); font-size: 12px; margin-top: 10px; }
|
|
219
|
+
.set-note:empty { display: none; margin: 0; }
|
|
205
220
|
.set-note.ok { color: var(--good); }
|
|
206
221
|
.set-note.err { color: var(--bad); }
|
|
207
|
-
/* Toasts
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
/* Toasts (Sonner-style, ported to vanilla). Stacked bottom-right: the newest is in front,
|
|
223
|
+
older ones peek behind and fan out on hover. Rich-coloured per type, self-dismissing,
|
|
224
|
+
swipe- or click-to-close - so a result is never a cutre green line again. */
|
|
225
|
+
#toaster {
|
|
226
|
+
position: fixed; right: var(--t-offset); bottom: var(--t-offset); z-index: 999999;
|
|
227
|
+
width: var(--t-width); list-style: none; margin: 0; padding: 0; outline: none;
|
|
228
|
+
--t-offset: 24px; --t-width: 356px; --t-gap: 14px; --t-front: 0px;
|
|
229
|
+
font-family: var(--sans);
|
|
230
|
+
--t-bg: var(--surface); --t-border: var(--border); --t-text: var(--text);
|
|
231
|
+
--success-bg: hsl(150,38%,9%); --success-border: hsl(147,55%,18%); --success-text: hsl(150,80%,72%);
|
|
232
|
+
--error-bg: hsl(358,42%,12%); --error-border: hsl(357,55%,24%); --error-text: hsl(358,100%,82%);
|
|
233
|
+
--info-bg: hsl(215,45%,11%); --info-border: hsl(223,50%,24%); --info-text: hsl(216,87%,74%);
|
|
234
|
+
--warning-bg: hsl(40,55%,9%); --warning-border: hsl(40,55%,20%); --warning-text: hsl(46,87%,70%);
|
|
235
|
+
transition: transform .35s ease;
|
|
236
|
+
}
|
|
237
|
+
#toaster[data-expanded="true"] { transform: translateY(-6px); }
|
|
238
|
+
.etoast {
|
|
239
|
+
position: absolute; bottom: 0; right: 0; left: 0; width: 100%; box-sizing: border-box;
|
|
240
|
+
display: flex; align-items: center; gap: 10px; padding: 15px 16px;
|
|
241
|
+
background: var(--t-bg); border: 1px solid var(--t-border); color: var(--t-text);
|
|
242
|
+
border-radius: 12px; box-shadow: 0 6px 22px rgba(0,0,0,.34); font-size: 13px;
|
|
243
|
+
z-index: var(--z); opacity: 0; touch-action: none; overflow-wrap: anywhere;
|
|
244
|
+
transform: translateY(110%); transform-origin: bottom center;
|
|
245
|
+
transition: transform .42s cubic-bezier(.21,1.02,.73,1), opacity .35s, height .35s, box-shadow .2s;
|
|
246
|
+
}
|
|
247
|
+
.etoast[data-mounted="true"] { opacity: 1; transform: translateY(0); }
|
|
248
|
+
/* Collapsed (default): older toasts sit behind the front one, scaled + clipped to its height. */
|
|
249
|
+
.etoast[data-front="false"][data-expanded="false"] {
|
|
250
|
+
transform: translateY(calc(var(--toasts-before) * -14px)) scale(calc(1 - var(--toasts-before) * .06));
|
|
251
|
+
height: var(--t-front);
|
|
252
|
+
}
|
|
253
|
+
.etoast[data-front="false"][data-expanded="false"] > * { opacity: 0; }
|
|
254
|
+
/* Expanded (hover): fan out, each lifted by the summed height of the ones in front of it. */
|
|
255
|
+
.etoast[data-expanded="true"] { transform: translateY(calc(-1 * var(--offset))); }
|
|
256
|
+
.etoast[data-visible="false"] { opacity: 0; pointer-events: none; }
|
|
257
|
+
.etoast[data-removed="true"] { opacity: 0; }
|
|
258
|
+
.etoast[data-removed="true"][data-expanded="false"] { transform: translateY(150%) scale(.9); }
|
|
259
|
+
.etoast[data-removed="true"][data-expanded="true"] { transform: translateY(calc(-1 * var(--offset) + 110%)); }
|
|
260
|
+
.etoast[data-swiping="true"] { transition: opacity .2s; transform: translateX(var(--swipe-x, 0px)); }
|
|
261
|
+
.etoast > * { transition: opacity .35s; }
|
|
262
|
+
.etoast-icon { display: flex; align-items: center; flex-shrink: 0; width: 20px; height: 20px; }
|
|
263
|
+
.etoast-icon svg { width: 20px; height: 20px; display: block; }
|
|
264
|
+
.etoast-content { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
|
|
265
|
+
.etoast-title { font-weight: 600; line-height: 1.4; }
|
|
266
|
+
.etoast-desc { font-size: 12px; line-height: 1.45; color: var(--muted); }
|
|
267
|
+
.etoast-close {
|
|
268
|
+
position: absolute; top: 0; left: 0; transform: translate(-35%,-35%);
|
|
269
|
+
width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
|
|
270
|
+
padding: 0; border-radius: 50%; cursor: pointer; opacity: 0; z-index: 1;
|
|
271
|
+
background: var(--t-bg); border: 1px solid var(--t-border); color: var(--t-text);
|
|
272
|
+
transition: opacity .15s, background .2s, border-color .2s;
|
|
273
|
+
}
|
|
274
|
+
.etoast:hover .etoast-close, .etoast-close:focus-visible { opacity: 1; }
|
|
275
|
+
.etoast-close:hover { background: var(--surface2); }
|
|
276
|
+
.etoast[data-type="success"] { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); }
|
|
277
|
+
.etoast[data-type="error"] { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); }
|
|
278
|
+
.etoast[data-type="info"] { background: var(--info-bg); border-color: var(--info-border); color: var(--info-text); }
|
|
279
|
+
.etoast[data-type="warning"] { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); }
|
|
280
|
+
.etoast[data-type="success"] .etoast-desc, .etoast[data-type="error"] .etoast-desc,
|
|
281
|
+
.etoast[data-type="info"] .etoast-desc, .etoast[data-type="warning"] .etoast-desc { color: inherit; opacity: .82; }
|
|
282
|
+
.etoast[data-type] .etoast-close { background: inherit; border-color: currentColor; color: inherit; }
|
|
283
|
+
.etoast-spin { width: 18px; height: 18px; animation: etoast-spin 0.9s linear infinite; transform-origin: center; }
|
|
284
|
+
@keyframes etoast-spin { to { transform: rotate(360deg); } }
|
|
285
|
+
@media (max-width: 600px) {
|
|
286
|
+
#toaster { right: 14px; left: 14px; width: auto; --t-width: 100%; }
|
|
287
|
+
}
|
|
288
|
+
@media (prefers-reduced-motion) { .etoast, .etoast > * { transition: opacity .2s !important; } }
|
|
216
289
|
/* Settings: two-column grid of rows on wide screens, so a long config list stays compact. */
|
|
217
290
|
.set-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 0 32px; }
|
|
218
291
|
.set-grid .set-row { padding: 10px 0; }
|
|
@@ -245,6 +318,9 @@
|
|
|
245
318
|
.btn-danger.icon { display: inline-flex; align-items: center; justify-content: center; padding: 6px 9px; }
|
|
246
319
|
.btn-danger.icon:hover { color: var(--bad); border-color: var(--bad); }
|
|
247
320
|
.btn-danger svg { width: 15px; height: 15px; display: block; }
|
|
321
|
+
/* Per-memory delete in the Recall list: kept to the right and revealed on row hover/focus. */
|
|
322
|
+
.recall-del { margin-left: auto; opacity: 0; transition: opacity .12s; }
|
|
323
|
+
.set-row:hover .recall-del, .recall-del:focus-visible { opacity: 1; }
|
|
248
324
|
/* Per-agent skill toggles: one pill per app (the app's icon), green when the skill is on for it; the app name is the tooltip. */
|
|
249
325
|
.set-agents { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
250
326
|
.achip { display: inline-flex; align-items: center; justify-content: center; padding: 5px 8px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); cursor: pointer; text-decoration: none; transition: color .15s, background .15s, border-color .15s; }
|
|
@@ -394,6 +470,19 @@
|
|
|
394
470
|
.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; }
|
|
395
471
|
.dd-opt:hover, .dd-opt:focus { background: var(--surface2); outline: none; }
|
|
396
472
|
.dd-opt.sel { color: var(--accent); }
|
|
473
|
+
.dd-btn:focus-visible { outline: none; border-color: var(--accent); }
|
|
474
|
+
/* Enhanced native <select>s (enhanceSelect): same look, but they hug their content instead
|
|
475
|
+
of the wider provider picker, and fill a flex slot when the original select did. */
|
|
476
|
+
.ddx { display: inline-flex; vertical-align: middle; }
|
|
477
|
+
.ddx .dd-btn { min-width: 120px; }
|
|
478
|
+
.ddx .dd-menu { max-height: 320px; overflow-y: auto; }
|
|
479
|
+
/* Native selects that stay native (inside modals) get a custom caret so they match the
|
|
480
|
+
styled dropdowns - no default OS chevron. */
|
|
481
|
+
select.linput, select.modal-input, select.scope-sel, select.choice {
|
|
482
|
+
-webkit-appearance: none; -moz-appearance: none; appearance: none;
|
|
483
|
+
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%23808a99' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
|
|
484
|
+
background-repeat: no-repeat; background-position: right 9px center; padding-right: 28px;
|
|
485
|
+
}
|
|
397
486
|
/* Icon picker: clickable app cards (a native <select> can't render SVG in options). */
|
|
398
487
|
.modal-picker { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
|
|
399
488
|
.pick-card {
|
|
@@ -445,6 +534,13 @@
|
|
|
445
534
|
</div>
|
|
446
535
|
</header>
|
|
447
536
|
|
|
537
|
+
<div id="updateAlert" class="update-alert" hidden>
|
|
538
|
+
<svg class="ua-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56"/><path d="M12 7v5l3 2"/></svg>
|
|
539
|
+
<span class="ua-msg" id="updateAlertMsg"></span>
|
|
540
|
+
<button type="button" id="updateAlertBtn" class="toggle on">Update now</button>
|
|
541
|
+
<button type="button" id="updateAlertDismiss" class="ua-x" title="Dismiss" aria-label="Dismiss">×</button>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
448
544
|
<main id="view-savings">
|
|
449
545
|
<div class="grid">
|
|
450
546
|
<div class="card hero"><div class="label">Money Saved</div><div id="money" class="value good">-</div><div id="moneyFoot" class="foot">Estimated, by source</div></div>
|
|
@@ -1995,23 +2091,28 @@ export {
|
|
|
1995
2091
|
try { await fetch("/api/plan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value: Number(value) }) }); refreshAll(true); }
|
|
1996
2092
|
catch { /* leave as-is */ }
|
|
1997
2093
|
}
|
|
1998
|
-
//
|
|
1999
|
-
//
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2002
|
-
|
|
2094
|
+
// Shared dropdown core. Renders one styled listbox over a `.dd` root (button + menu) from a
|
|
2095
|
+
// model: entries() -> [{ value, label, icon }], get() -> current value, set(value) -> commit.
|
|
2096
|
+
// Icons are optional per entry, so the SAME component drives the icon pickers (provider,
|
|
2097
|
+
// usage) AND every plain selector on the page (see enhanceSelect) - one look, one set of
|
|
2098
|
+
// keyboard semantics, with or without logos.
|
|
2099
|
+
function bindDropdown(root, model) {
|
|
2100
|
+
const btn = root.querySelector(".dd-btn"), menu = root.querySelector(".dd-menu");
|
|
2003
2101
|
const btnIc = btn.querySelector(".dd-ic"), btnLabel = btn.querySelector(".dd-label");
|
|
2004
2102
|
const opts = () => [...menu.querySelectorAll(".dd-opt")];
|
|
2103
|
+
const ic = (svg) => '<span class="dd-ic"' + (svg ? "" : ' style="display:none"') + ">" + (svg || "") + "</span>";
|
|
2005
2104
|
const render = () => {
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2105
|
+
const entries = model.entries(), cur = model.get();
|
|
2106
|
+
const e = entries.find((x) => x.value === cur) || entries[0] || { label: "", icon: "" };
|
|
2107
|
+
btnIc.innerHTML = e.icon || ""; btnIc.style.display = e.icon ? "" : "none";
|
|
2108
|
+
btnLabel.textContent = e.label || "";
|
|
2008
2109
|
menu.innerHTML = entries.map((x) => '<li class="dd-opt' + (x.value === cur ? " sel" : "")
|
|
2009
2110
|
+ '" role="option" tabindex="-1" data-val="' + esc(x.value) + '" aria-selected="' + (x.value === cur)
|
|
2010
|
-
+ '"
|
|
2111
|
+
+ '">' + ic(x.icon) + "<span>" + esc(x.label) + "</span></li>").join("");
|
|
2011
2112
|
};
|
|
2012
2113
|
const close = () => { menu.hidden = true; btn.setAttribute("aria-expanded", "false"); };
|
|
2013
|
-
const open = () => { menu.hidden = false; btn.setAttribute("aria-expanded", "true");
|
|
2014
|
-
const pick = (li) => { close(); btn.focus(); if (li && li.dataset.val)
|
|
2114
|
+
const open = () => { render(); menu.hidden = false; btn.setAttribute("aria-expanded", "true"); const f = menu.querySelector(".sel") || opts()[0]; if (f) f.focus(); };
|
|
2115
|
+
const pick = (li) => { close(); btn.focus(); if (li && li.dataset.val != null) { model.set(li.dataset.val); render(); } };
|
|
2015
2116
|
btn.addEventListener("click", (e) => { e.stopPropagation(); menu.hidden ? open() : close(); });
|
|
2016
2117
|
btn.addEventListener("keydown", (e) => { if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); } });
|
|
2017
2118
|
menu.addEventListener("click", (e) => pick(e.target.closest(".dd-opt")));
|
|
@@ -2021,11 +2122,65 @@ export {
|
|
|
2021
2122
|
else if (e.key === "ArrowDown") { e.preventDefault(); (list[i + 1] || list[0]).focus(); }
|
|
2022
2123
|
else if (e.key === "ArrowUp") { e.preventDefault(); (list[i - 1] || list[list.length - 1]).focus(); }
|
|
2023
2124
|
else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(document.activeElement); }
|
|
2125
|
+
else if (e.key === "Tab") close();
|
|
2024
2126
|
});
|
|
2025
|
-
document.addEventListener("click", close);
|
|
2026
2127
|
render();
|
|
2027
2128
|
return { render };
|
|
2028
2129
|
}
|
|
2130
|
+
// One global outside-click closes any open dropdown - cheaper and leak-free vs a per-instance
|
|
2131
|
+
// listener (enhanceSelect can create/replace many over a session). A `.dd-btn` click stops
|
|
2132
|
+
// propagation, so opening one never trips this.
|
|
2133
|
+
document.addEventListener("click", () => {
|
|
2134
|
+
document.querySelectorAll(".dd-menu:not([hidden])").forEach((m) => {
|
|
2135
|
+
m.hidden = true;
|
|
2136
|
+
const b = m.parentElement && m.parentElement.querySelector(".dd-btn");
|
|
2137
|
+
if (b) b.setAttribute("aria-expanded", "false");
|
|
2138
|
+
});
|
|
2139
|
+
});
|
|
2140
|
+
// entries: [{ value, label, icon }] bound to a pre-rendered `.dd` root by id.
|
|
2141
|
+
function iconDropdown(rootId, entries, getVal, onPick) {
|
|
2142
|
+
return bindDropdown($(rootId), { entries: () => entries, get: getVal, set: onPick });
|
|
2143
|
+
}
|
|
2144
|
+
// Markup for an enhanced dropdown's button + menu, matching the hand-written provider picker.
|
|
2145
|
+
const DD_INNER = '<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><ul class="dd-menu" role="listbox" hidden></ul>';
|
|
2146
|
+
// Progressive enhancement: replace a native <select> with the shared pretty dropdown while
|
|
2147
|
+
// keeping the <select> itself as the source of truth (hidden). Its .value and `change`
|
|
2148
|
+
// events are untouched - we set .value and dispatch a real `change` on pick - so every
|
|
2149
|
+
// existing reader and delegated handler keeps working. Icons are opt-in via
|
|
2150
|
+
// data-dd-icons="agent" (option value -> brand logo) or per-option data-icon="<key>".
|
|
2151
|
+
function enhanceSelect(select) {
|
|
2152
|
+
if (select.__dd || select.dataset.noDd != null) return;
|
|
2153
|
+
if (select.closest(".modal-back")) return; // a popup menu inside an animated modal would clip; left native (still styled)
|
|
2154
|
+
const iconMode = select.dataset.ddIcons || "";
|
|
2155
|
+
const iconFor = (opt) => { const k = opt.dataset.icon || (iconMode === "agent" ? opt.value : ""); return k ? (agentSvg(k) || "") : ""; };
|
|
2156
|
+
const root = document.createElement("span");
|
|
2157
|
+
root.className = "dd ddx";
|
|
2158
|
+
root.innerHTML = DD_INNER;
|
|
2159
|
+
select.style.display = "none";
|
|
2160
|
+
select.insertAdjacentElement("afterend", root);
|
|
2161
|
+
const api = bindDropdown(root, {
|
|
2162
|
+
entries: () => [...select.options].map((o) => ({ value: o.value, label: o.textContent, icon: iconFor(o) })),
|
|
2163
|
+
get: () => select.value,
|
|
2164
|
+
set: (v) => { if (v !== select.value) { select.value = v; select.dispatchEvent(new Event("change", { bubbles: true })); } api.render(); }
|
|
2165
|
+
});
|
|
2166
|
+
select.__dd = api;
|
|
2167
|
+
// Re-sync the button when options are rebuilt (innerHTML) or the value changes.
|
|
2168
|
+
new MutationObserver(() => api.render()).observe(select, { childList: true });
|
|
2169
|
+
select.addEventListener("change", () => api.render());
|
|
2170
|
+
return api;
|
|
2171
|
+
}
|
|
2172
|
+
// Push a programmatic `select.value = x` (no `change` event) into the enhanced dropdown.
|
|
2173
|
+
const syncDD = (el) => { if (el && el.__dd) el.__dd.render(); };
|
|
2174
|
+
function enhanceAll(root) { (root || document).querySelectorAll("select:not([data-no-dd])").forEach(enhanceSelect); }
|
|
2175
|
+
// Any selector added later (settings rows, project config, account profiles...) is enhanced
|
|
2176
|
+
// automatically, so there is nothing to wire per-view.
|
|
2177
|
+
new MutationObserver((muts) => {
|
|
2178
|
+
for (const m of muts) for (const n of m.addedNodes) {
|
|
2179
|
+
if (n.nodeType !== 1) continue;
|
|
2180
|
+
if (n.tagName === "SELECT") enhanceSelect(n);
|
|
2181
|
+
else if (n.querySelectorAll) n.querySelectorAll("select:not([data-no-dd])").forEach(enhanceSelect);
|
|
2182
|
+
}
|
|
2183
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
2029
2184
|
let providerDD = null;
|
|
2030
2185
|
function wireUsage() {
|
|
2031
2186
|
// The provider picker is a custom icon dropdown; the account one stays a native select.
|
|
@@ -2102,6 +2257,7 @@ export {
|
|
|
2102
2257
|
+ names.map((n) => '<option value="' + esc(n) + '">' + esc(accountLabel(n)) + "</option>").join("");
|
|
2103
2258
|
}
|
|
2104
2259
|
sel.value = usageAccount;
|
|
2260
|
+
syncDD(sel);
|
|
2105
2261
|
}
|
|
2106
2262
|
|
|
2107
2263
|
// Fill the panel from one scope: the whole report (isAll) or one account's sub-report.
|
|
@@ -2330,6 +2486,7 @@ export {
|
|
|
2330
2486
|
if (loadedUi === null) loadedUi = data.ui;
|
|
2331
2487
|
else if (data.ui !== loadedUi) { location.reload(); return; }
|
|
2332
2488
|
}
|
|
2489
|
+
renderUpdateAlert(data.updates);
|
|
2333
2490
|
priceOverride = Number(data.priceOverride) || 0;
|
|
2334
2491
|
speedOverride = Number(data.speedOverride) || 0;
|
|
2335
2492
|
const s = data.stats || {};
|
|
@@ -2388,6 +2545,35 @@ export {
|
|
|
2388
2545
|
}
|
|
2389
2546
|
}
|
|
2390
2547
|
|
|
2548
|
+
// --- package update alert --------------------------------------------------
|
|
2549
|
+
// Server-side npm version check (CLI + dashboard + linter), surfaced in /api/stats and
|
|
2550
|
+
// refreshed every poll. Dismiss is sticky only for the exact versions shown - a later
|
|
2551
|
+
// release re-shows the banner. The "Update now" button runs `enigma update` behind the
|
|
2552
|
+
// scenes; the alert then clears on its own as the next polls see the new versions.
|
|
2553
|
+
let dismissedUpdateSig = "";
|
|
2554
|
+
function renderUpdateAlert(u) {
|
|
2555
|
+
const el = $("updateAlert");
|
|
2556
|
+
if (!el) return;
|
|
2557
|
+
const items = ((u && u.packages) || []).filter((p) => p.hasUpdate);
|
|
2558
|
+
const sig = items.map((p) => p.name + "@" + p.latest).join(",");
|
|
2559
|
+
if (!items.length || sig === dismissedUpdateSig) { el.hidden = true; return; }
|
|
2560
|
+
const list = items.map((p) => esc(p.label) + " " + esc(p.installed) + " -> " + esc(p.latest)).join(", ");
|
|
2561
|
+
$("updateAlertMsg").innerHTML = "<strong>Update available</strong> " + list;
|
|
2562
|
+
el.dataset.sig = sig;
|
|
2563
|
+
el.hidden = false;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
async function runFullUpdate() {
|
|
2567
|
+
const btn = $("updateAlertBtn");
|
|
2568
|
+
if (btn) { btn.disabled = true; btn.textContent = "Updating..."; }
|
|
2569
|
+
try {
|
|
2570
|
+
const res = await fetch("/api/run-update", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
|
|
2571
|
+
const out = await res.json();
|
|
2572
|
+
toast(out.note || (out.ok ? "Update started." : "Could not start the update."), out.ok ? "ok" : "err");
|
|
2573
|
+
} catch { toast("Could not reach the server to update.", "err"); }
|
|
2574
|
+
finally { if (btn) { btn.disabled = false; btn.textContent = "Update now"; } }
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2391
2577
|
// --- dashboard self-update -------------------------------------------------
|
|
2392
2578
|
// Where the latest published dashboard version lives: the raw package.json on the
|
|
2393
2579
|
// repo's main branch (no API, no auth, no rate limit). A repo rename 404s here and is
|
|
@@ -2435,24 +2621,153 @@ export {
|
|
|
2435
2621
|
// --- settings subpage (mirrors the TUI registry over /api/settings) ---
|
|
2436
2622
|
function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); }
|
|
2437
2623
|
|
|
2438
|
-
//
|
|
2439
|
-
//
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2624
|
+
// --- Toasts (vanilla port of Sonner) -------------------------------------------------
|
|
2625
|
+
// A stacked, self-dismissing notification system: the newest sits in front, older ones
|
|
2626
|
+
// peek behind and fan out on hover; rich-coloured per type; swipe- or click-to-close.
|
|
2627
|
+
// API: toast(msg, opts) and toast.success/error/info/warning/loading/message/dismiss.
|
|
2628
|
+
// opts: { description, duration, id }. A loading toast has Infinity duration; calling a
|
|
2629
|
+
// type with the same id UPDATES it in place (loading -> success/error), so an async flow
|
|
2630
|
+
// is a single morphing toast, not a pile.
|
|
2631
|
+
const toast = (() => {
|
|
2632
|
+
const ICONS = {
|
|
2633
|
+
success: '<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd"/></svg>',
|
|
2634
|
+
error: '<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/></svg>',
|
|
2635
|
+
info: '<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd"/></svg>',
|
|
2636
|
+
warning: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clip-rule="evenodd"/></svg>',
|
|
2637
|
+
loading: '<svg class="etoast-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" opacity="0.85"/></svg>',
|
|
2638
|
+
close: '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
|
|
2639
|
+
};
|
|
2640
|
+
const VISIBLE = 3, DURATION = { default: 4000, success: 4000, info: 4000, warning: 5000, error: 6000, loading: Infinity };
|
|
2641
|
+
let list = null, counter = 1, expanded = false;
|
|
2642
|
+
const items = []; // newest-first; { id, el, type, duration, remaining, startedAt, timer, removed }
|
|
2643
|
+
const mount = () => { if (!list) { list = document.createElement("ol"); list.id = "toaster"; list.setAttribute("aria-live", "polite"); list.tabIndex = -1; document.body.appendChild(list); wireHover(); } return list; };
|
|
2644
|
+
const wireHover = () => {
|
|
2645
|
+
list.addEventListener("mouseenter", () => { expanded = true; list.dataset.expanded = "true"; pauseAll(); place(); });
|
|
2646
|
+
list.addEventListener("mouseleave", () => { expanded = false; list.dataset.expanded = "false"; resumeAll(); place(); });
|
|
2647
|
+
};
|
|
2648
|
+
const front = () => items.find((t) => !t.removed);
|
|
2649
|
+
const pause = (t) => { if (t.timer) { clearTimeout(t.timer); t.timer = null; t.remaining -= Date.now() - t.startedAt; } };
|
|
2650
|
+
const resume = (t) => { if (t.duration === Infinity || t.removed || t.timer) return; t.startedAt = Date.now(); t.timer = setTimeout(() => dismiss(t.id), Math.max(0, t.remaining)); };
|
|
2651
|
+
const pauseAll = () => items.forEach(pause);
|
|
2652
|
+
const resumeAll = () => items.forEach(resume);
|
|
2653
|
+
// Position every toast: front shows fully, the rest peek behind (collapsed) or fan out
|
|
2654
|
+
// (expanded). --offset is the summed height of the live toasts in front of this one.
|
|
2655
|
+
function place() {
|
|
2656
|
+
let acc = 0, vi = 0;
|
|
2657
|
+
for (const t of items) {
|
|
2658
|
+
if (t.removed) { t.el.style.setProperty("--z", "0"); continue; }
|
|
2659
|
+
// The front toast (and any toast while the stack is expanded) is never
|
|
2660
|
+
// height-clamped, so its offsetHeight is the true natural height - cache it.
|
|
2661
|
+
if (vi === 0 && !expanded) t.height = t.el.offsetHeight;
|
|
2662
|
+
if (vi === 0) list.style.setProperty("--t-front", (t.height || t.el.offsetHeight) + "px");
|
|
2663
|
+
t.el.dataset.front = vi === 0 ? "true" : "false";
|
|
2664
|
+
t.el.dataset.expanded = expanded ? "true" : "false";
|
|
2665
|
+
t.el.dataset.visible = vi < VISIBLE ? "true" : "false";
|
|
2666
|
+
t.el.style.setProperty("--toasts-before", String(vi));
|
|
2667
|
+
t.el.style.setProperty("--offset", acc + "px");
|
|
2668
|
+
t.el.style.setProperty("--z", String(1000 - vi));
|
|
2669
|
+
acc += (t.height || t.el.offsetHeight) + 14;
|
|
2670
|
+
vi++;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
function dismiss(id) {
|
|
2674
|
+
const t = items.find((x) => x.id === id);
|
|
2675
|
+
if (!t || t.removed) return id;
|
|
2676
|
+
t.removed = true; if (t.timer) clearTimeout(t.timer);
|
|
2677
|
+
t.el.dataset.removed = "true"; t.el.dataset.expanded = expanded ? "true" : "false";
|
|
2678
|
+
place();
|
|
2679
|
+
setTimeout(() => { const i = items.indexOf(t); if (i !== -1) items.splice(i, 1); t.el.remove(); place(); }, 420);
|
|
2680
|
+
return id;
|
|
2681
|
+
}
|
|
2682
|
+
function build(t, type, title, description) {
|
|
2683
|
+
t.el.dataset.type = type;
|
|
2684
|
+
t.el.setAttribute("role", type === "error" ? "alert" : "status");
|
|
2685
|
+
const icon = ICONS[type] ? '<div class="etoast-icon">' + ICONS[type] + "</div>" : "";
|
|
2686
|
+
t.el.innerHTML = icon
|
|
2687
|
+
+ '<div class="etoast-content"><div class="etoast-title"></div>'
|
|
2688
|
+
+ (description ? '<div class="etoast-desc"></div>' : "") + "</div>"
|
|
2689
|
+
+ '<button type="button" class="etoast-close" aria-label="Close">' + ICONS.close + "</button>";
|
|
2690
|
+
t.el.querySelector(".etoast-title").textContent = title == null ? "" : String(title);
|
|
2691
|
+
if (description) t.el.querySelector(".etoast-desc").textContent = String(description);
|
|
2692
|
+
t.el.querySelector(".etoast-close").addEventListener("click", (e) => { e.stopPropagation(); dismiss(t.id); });
|
|
2693
|
+
}
|
|
2694
|
+
// Horizontal swipe-to-dismiss (pointer drag), matching Sonner's feel.
|
|
2695
|
+
function wireSwipe(t) {
|
|
2696
|
+
let startX = null;
|
|
2697
|
+
t.el.addEventListener("pointerdown", (e) => { if (e.target.closest(".etoast-close")) return; startX = e.clientX; t.el.setPointerCapture(e.pointerId); pause(t); });
|
|
2698
|
+
t.el.addEventListener("pointermove", (e) => {
|
|
2699
|
+
if (startX == null) return;
|
|
2700
|
+
const dx = e.clientX - startX;
|
|
2701
|
+
t.el.dataset.swiping = "true"; t.el.style.setProperty("--swipe-x", dx + "px");
|
|
2702
|
+
});
|
|
2703
|
+
const end = (e) => {
|
|
2704
|
+
if (startX == null) return;
|
|
2705
|
+
const dx = e.clientX - startX; startX = null; t.el.dataset.swiping = "false";
|
|
2706
|
+
if (Math.abs(dx) > 80) dismiss(t.id);
|
|
2707
|
+
else { t.el.style.removeProperty("--swipe-x"); if (!expanded) resume(t); }
|
|
2708
|
+
};
|
|
2709
|
+
t.el.addEventListener("pointerup", end);
|
|
2710
|
+
t.el.addEventListener("pointercancel", end);
|
|
2711
|
+
}
|
|
2712
|
+
function create(opts) {
|
|
2713
|
+
const type = opts.type || "default";
|
|
2714
|
+
const id = opts.id != null ? opts.id : counter++;
|
|
2715
|
+
const duration = opts.duration != null ? opts.duration : DURATION[type];
|
|
2716
|
+
const existing = items.find((x) => x.id === id && !x.removed);
|
|
2717
|
+
if (existing) { // update in place (e.g. loading -> success)
|
|
2718
|
+
build(existing, type, opts.title, opts.description);
|
|
2719
|
+
existing.duration = duration; existing.remaining = duration; existing.startedAt = Date.now();
|
|
2720
|
+
if (existing.timer) { clearTimeout(existing.timer); existing.timer = null; }
|
|
2721
|
+
if (!expanded) resume(existing);
|
|
2722
|
+
place();
|
|
2723
|
+
return id;
|
|
2724
|
+
}
|
|
2725
|
+
mount();
|
|
2726
|
+
const el = document.createElement("li");
|
|
2727
|
+
el.className = "etoast"; el.tabIndex = 0;
|
|
2728
|
+
const t = { id, el, type, duration, remaining: duration, startedAt: Date.now(), timer: null, removed: false };
|
|
2729
|
+
build(t, type, opts.title, opts.description);
|
|
2730
|
+
wireSwipe(t);
|
|
2731
|
+
items.unshift(t);
|
|
2732
|
+
list.prepend(el);
|
|
2733
|
+
place();
|
|
2734
|
+
requestAnimationFrame(() => { el.dataset.mounted = "true"; place(); if (!expanded) resume(t); });
|
|
2735
|
+
return id;
|
|
2736
|
+
}
|
|
2737
|
+
// Back-compat: a string 2nd arg is the legacy kind ("ok"/"err"); an object is opts.
|
|
2738
|
+
const KIND = { ok: "success", err: "error" };
|
|
2739
|
+
const api = (msg, o) => create(typeof o === "string" ? { type: KIND[o] || "default", title: msg } : { ...o, type: "default", title: msg });
|
|
2740
|
+
["success", "error", "info", "warning", "loading", "message"].forEach((k) => {
|
|
2741
|
+
api[k] = (msg, o) => create({ ...o, type: k === "message" ? "default" : k, title: msg });
|
|
2742
|
+
});
|
|
2743
|
+
api.dismiss = (id) => { if (id == null) items.slice().forEach((t) => dismiss(t.id)); else dismiss(id); return id; };
|
|
2744
|
+
return api;
|
|
2745
|
+
})();
|
|
2746
|
+
|
|
2747
|
+
// Bridge the old inline-note API onto toasts so every existing call site upgrades at once.
|
|
2748
|
+
// A flow reuses ONE note target (e.g. "settingsNote") start to finish, so we key a single
|
|
2749
|
+
// toast per target: a progress message ("Saving...") opens a loading toast, and the ok/err
|
|
2750
|
+
// result morphs that same toast - no pile-up, no leftover green text. `restartNote`-style
|
|
2751
|
+
// long messages split into title + description at the first sentence break.
|
|
2752
|
+
const noteToastId = new Map();
|
|
2753
|
+
function noteKey(el) { return typeof el === "string" ? el : (el && el.id) || "__note__"; }
|
|
2754
|
+
function splitMsg(msg) {
|
|
2755
|
+
msg = String(msg);
|
|
2756
|
+
const m = msg.match(/^(.*?[.!])\s+(.+)$/s);
|
|
2757
|
+
return m && m[1].length <= 64 ? { title: m[1], description: m[2] } : { title: msg };
|
|
2758
|
+
}
|
|
2759
|
+
// Drop-in replacement for the former setNote(el, msg, kind). `el` is ignored for display
|
|
2760
|
+
// (kept for call compatibility); everything surfaces as a toast.
|
|
2452
2761
|
function setNote(el, msg, kind) {
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
if (
|
|
2762
|
+
const key = noteKey(el);
|
|
2763
|
+
const prev = noteToastId.get(key);
|
|
2764
|
+
if (msg == null || msg === "") { if (prev != null) { toast.dismiss(prev); noteToastId.delete(key); } return; }
|
|
2765
|
+
const isProgress = !kind && /(\.\.\.|…)\s*$/.test(String(msg));
|
|
2766
|
+
const type = kind === "ok" ? "success" : kind === "err" ? "error" : isProgress ? "loading" : "info";
|
|
2767
|
+
const parts = splitMsg(msg);
|
|
2768
|
+
const id = toast[type](parts.title, { id: prev, description: parts.description });
|
|
2769
|
+
if (isProgress) noteToastId.set(key, id);
|
|
2770
|
+
else noteToastId.delete(key);
|
|
2456
2771
|
}
|
|
2457
2772
|
|
|
2458
2773
|
// Capitalize a choice value for display ("on-demand" -> "On demand"), keeping the raw
|
|
@@ -2599,7 +2914,7 @@ export {
|
|
|
2599
2914
|
const want = isBool ? (value === true || value === 1) : String(value);
|
|
2600
2915
|
const prevBool = ctl.dataset.on === "1";
|
|
2601
2916
|
const prevChoice = ctl.dataset.prev != null ? ctl.dataset.prev : ctl.value; // <select> already changed natively
|
|
2602
|
-
const rollback = () => { if (isBool) applyBool(ctl, prevBool); else ctl.value = prevChoice; };
|
|
2917
|
+
const rollback = () => { if (isBool) applyBool(ctl, prevBool); else { ctl.value = prevChoice; syncDD(ctl); } };
|
|
2603
2918
|
if (isBool) applyBool(ctl, want);
|
|
2604
2919
|
setNote(el, "Saving " + key + "...");
|
|
2605
2920
|
try {
|
|
@@ -2608,7 +2923,7 @@ export {
|
|
|
2608
2923
|
if (!out.ok) { rollback(); setNote(el, "Could not change " + key + ": " + (out.error || "error"), "err"); return; }
|
|
2609
2924
|
const s = out.setting;
|
|
2610
2925
|
if (s && isBool) applyBool(ctl, s.value);
|
|
2611
|
-
else if (s) { ctl.value = s.choice; ctl.dataset.prev = s.choice; }
|
|
2926
|
+
else if (s) { ctl.value = s.choice; ctl.dataset.prev = s.choice; syncDD(ctl); }
|
|
2612
2927
|
if (s && (isBool ? s.value !== want : String(s.choice) !== want)) { setNote(el, key + " did not change (the write had no effect).", "err"); return; }
|
|
2613
2928
|
setNote(el, out.restartNote || ("Saved " + key + "."), "ok");
|
|
2614
2929
|
} catch { rollback(); setNote(el, "Could not reach the server to change " + key + ".", "err"); }
|
|
@@ -3387,15 +3702,15 @@ export {
|
|
|
3387
3702
|
if (!v.enabled) {
|
|
3388
3703
|
$("recallBody").innerHTML = '<div class="panel"><div class="set-cat-h">Session memory is off</div>'
|
|
3389
3704
|
+ '<div class="set-hint" style="padding:8px 0">Build a searchable memory of your coding sessions from your own transcripts.</div>'
|
|
3390
|
-
+ '<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><button type="button" class="toggle" data-recall-on="1">Enable recall</button>'
|
|
3391
|
-
+ '<span class="set-hint">or run <code>enigma config recall on</code></span></div></div>';
|
|
3705
|
+
+ '<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"><button type="button" class="toggle" data-kind="bool" role="switch" aria-checked="false" data-recall-on="1" aria-label="Session memory" title="Turn session memory on">Enable recall</button>'
|
|
3706
|
+
+ '<span class="set-hint">Turn on, or run <code>enigma config recall on</code></span></div></div>';
|
|
3392
3707
|
$("recallSearch").style.display = "none"; $("recallProviderPanel").style.display = "none"; $("recallAddPanel").style.display = "none"; $("recallList").innerHTML = ""; return;
|
|
3393
3708
|
}
|
|
3394
3709
|
const s = v.stats || {};
|
|
3395
3710
|
const types = Object.entries(s.byType || {}).map(([t, n]) => '<span class="tag">' + esc(t) + " " + n + "</span>").join(" ");
|
|
3396
3711
|
const last = v.lastSync ? "last sync " + new Date(v.lastSync).toLocaleString() : "not synced yet";
|
|
3397
3712
|
$("recallBody").innerHTML = '<div class="panel"><div style="display:flex;justify-content:space-between;align-items:center;gap:10px"><div class="set-cat-h" style="margin:0">Stored memory</div>'
|
|
3398
|
-
+ '<button type="button" class="toggle on" data-recall-on="0" title="Turn session memory off">Disable</button></div>'
|
|
3713
|
+
+ '<button type="button" class="toggle on" data-kind="bool" role="switch" aria-checked="true" data-recall-on="0" aria-label="Session memory" title="Turn session memory off">Disable</button></div>'
|
|
3399
3714
|
+ '<div class="set-hint" style="padding:6px 0">' + (s.observations || 0) + " observations · " + (s.sessions || 0) + " sessions · "
|
|
3400
3715
|
+ (s.projects || 0) + " projects · " + Math.round((s.dbBytes || 0) / 1024) + " KB · " + esc(last) + "</div>"
|
|
3401
3716
|
+ (types ? '<div style="padding-top:6px">' + types + "</div>" : "") + "</div>";
|
|
@@ -3466,8 +3781,7 @@ export {
|
|
|
3466
3781
|
+ facts
|
|
3467
3782
|
+ (it.concepts && it.concepts.length ? '<div style="margin-top:6px">' + tags(it.concepts) + "</div>" : "")
|
|
3468
3783
|
+ list("Modified", it.files) + list("Read", it.filesRead)
|
|
3469
|
-
+ '<div style="margin-top:8px
|
|
3470
|
-
+ (it.id ? '<button type="button" class="btn-danger" data-del="' + it.id + '" title="Delete this memory">Delete</button>' : "") + "</div>"
|
|
3784
|
+
+ '<div style="margin-top:8px"><button type="button" class="toggle" data-tl="' + it.id + '">Show timeline</button></div>'
|
|
3471
3785
|
+ '<div class="rtl" id="rtl-' + it.id + '"></div></div>';
|
|
3472
3786
|
}
|
|
3473
3787
|
function renderRecallList(items) {
|
|
@@ -3475,7 +3789,8 @@ export {
|
|
|
3475
3789
|
if (!items || !items.length) { $("recallList").innerHTML = '<div class="empty" style="padding:16px 0">No memories yet - run a sync, or adjust your search.</div>'; return; }
|
|
3476
3790
|
$("recallList").innerHTML = '<div class="panel">' + items.map((it) => {
|
|
3477
3791
|
if (it.id) recallItemsById[it.id] = it;
|
|
3478
|
-
|
|
3792
|
+
const del = it.id ? '<button type="button" class="btn-danger icon recall-del" title="Delete memory" aria-label="Delete this memory" data-del="' + it.id + '">' + TRASH_ICON + "</button>" : "";
|
|
3793
|
+
return '<div class="set-row" style="cursor:pointer" data-rid="' + (it.id || "") + '">' + recallRowHtml(it) + del + "</div>"
|
|
3479
3794
|
+ '<div id="rd-' + (it.id || "") + '" style="display:none"></div>';
|
|
3480
3795
|
}).join("") + "</div>";
|
|
3481
3796
|
}
|
|
@@ -3611,6 +3926,7 @@ export {
|
|
|
3611
3926
|
+ projectsCache.map((p) => '<option value="' + esc(p.path) + '">' + esc(p.label) + "</option>").join("")
|
|
3612
3927
|
+ '<option value="__create__">+ Create new project</option>';
|
|
3613
3928
|
sel.value = currentScope;
|
|
3929
|
+
syncDD(sel);
|
|
3614
3930
|
}
|
|
3615
3931
|
async function loadProjectsList() {
|
|
3616
3932
|
try { projectsCache = ((await (await fetch("/api/projects", { cache: "no-store" })).json()).projects) || []; }
|
|
@@ -3690,7 +4006,7 @@ export {
|
|
|
3690
4006
|
const sel = $("scopeSel");
|
|
3691
4007
|
if (sel) sel.addEventListener("change", () => {
|
|
3692
4008
|
const val = sel.value;
|
|
3693
|
-
if (val === "__create__") { sel.value = currentScope; openCreateProject(); return; }
|
|
4009
|
+
if (val === "__create__") { sel.value = currentScope; syncDD(sel); openCreateProject(); return; }
|
|
3694
4010
|
currentScope = val;
|
|
3695
4011
|
try { localStorage.setItem("enigmaScope", val); } catch { /* */ }
|
|
3696
4012
|
route();
|
|
@@ -3882,10 +4198,13 @@ export {
|
|
|
3882
4198
|
wireResources();
|
|
3883
4199
|
wireRecall();
|
|
3884
4200
|
wireScope();
|
|
4201
|
+
enhanceAll(); // upgrade every static <select> into the shared pretty dropdown
|
|
3885
4202
|
$("cfgExport").addEventListener("click", exportConfig);
|
|
3886
4203
|
$("cfgImport").addEventListener("click", importConfig);
|
|
3887
4204
|
$("updateBtn").addEventListener("click", runUpdate);
|
|
3888
4205
|
$("fixPathBtn").addEventListener("click", runFixPath);
|
|
4206
|
+
$("updateAlertBtn").addEventListener("click", runFullUpdate);
|
|
4207
|
+
$("updateAlertDismiss").addEventListener("click", () => { dismissedUpdateSig = $("updateAlert").dataset.sig || ""; $("updateAlert").hidden = true; });
|
|
3889
4208
|
$("refreshBtn").addEventListener("click", async () => {
|
|
3890
4209
|
const b = $("refreshBtn");
|
|
3891
4210
|
if (b.classList.contains("spinning")) return; // already refreshing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
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": [
|