- Add Recommended Remote LAN Setup option in Settings for trusted LAN configurations - Implement loopback recovery bootstrap for local browser sessions - Automatically rotate legacy token hashes to new plaintext tokens - Update documentation to reflect new remote setup process and security features - Add runtime write authorization status indicators in Assets Manager - Improve client-side token handling and session management - Update environment variables guide with new bootstrap capabilities
220 lines
7.9 KiB
JavaScript
220 lines
7.9 KiB
JavaScript
/**
|
|
* Runtime status dashboard widget for Majoor.
|
|
* Renders a floating overlay inside the Assets Manager panel showing live
|
|
* DB / enrichment-queue / watcher metrics fetched from the backend.
|
|
*/
|
|
|
|
import { getRuntimeStatus } from "../../api/client.js";
|
|
import { getSecuritySettings } from "../../api/client.js";
|
|
import { t } from "../i18n.js";
|
|
|
|
const RUNTIME_DASHBOARD_ID = "mjr-runtime-status-dashboard";
|
|
const RUNTIME_TOKEN_KEY = "__mjr_write_token";
|
|
|
|
function readRuntimeWriteToken() {
|
|
try {
|
|
return String(sessionStorage?.getItem?.(RUNTIME_TOKEN_KEY) || "").trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function ensureDashboardLine(el, key) {
|
|
const storeKey = key === "auth" ? "__mjrAuthLine" : "__mjrMetricsLine";
|
|
if (el?.[storeKey]) {
|
|
return el[storeKey];
|
|
}
|
|
const line = document.createElement("div");
|
|
line.style.whiteSpace = "nowrap";
|
|
line.style.lineHeight = "1.35";
|
|
if (key === "auth") {
|
|
line.style.marginTop = "4px";
|
|
line.style.fontWeight = "600";
|
|
}
|
|
el.appendChild(line);
|
|
el[storeKey] = line;
|
|
return line;
|
|
}
|
|
|
|
function buildWriteAuthSummary(prefs) {
|
|
const token = readRuntimeWriteToken();
|
|
const tokenHint = String(prefs?.token_hint || "").trim();
|
|
const sessionHint = tokenHint || (token ? `...${token.slice(-4)}` : "");
|
|
const allowWrite = prefs?.allow_write !== false;
|
|
const requireAuth = prefs?.require_auth === true;
|
|
const tokenConfigured = prefs?.token_configured === true;
|
|
|
|
if (!allowWrite) {
|
|
return {
|
|
text: t("runtime.writeAuthBlocked", "Write auth: writes blocked by server"),
|
|
color: "#ff9b9b",
|
|
};
|
|
}
|
|
if (token) {
|
|
return {
|
|
text: t("runtime.writeAuthActive", "Write auth: active {tokenHint}", {
|
|
tokenHint: sessionHint || "(session)",
|
|
}),
|
|
color: "#7ee0a0",
|
|
};
|
|
}
|
|
if (requireAuth && tokenConfigured) {
|
|
return {
|
|
text: t(
|
|
"runtime.writeAuthMissing",
|
|
"Write auth: missing in this browser {tokenHint}",
|
|
{ tokenHint: sessionHint || "(server token configured)" },
|
|
),
|
|
color: "#f1c36d",
|
|
};
|
|
}
|
|
if (requireAuth) {
|
|
return {
|
|
text: t("runtime.writeAuthRequired", "Write auth: required"),
|
|
color: "#f1c36d",
|
|
};
|
|
}
|
|
if (prefs && typeof prefs === "object") {
|
|
return {
|
|
text: t("runtime.writeAuthNotRequired", "Write auth: not required"),
|
|
color: "#8fd0ff",
|
|
};
|
|
}
|
|
return {
|
|
text: t("runtime.writeAuthUnknown", "Write auth: unknown"),
|
|
color: "#c8ced8",
|
|
};
|
|
}
|
|
|
|
// ─── DOM helpers ──────────────────────────────────────────────────────────
|
|
|
|
function ensureRuntimeStatusDashboard() {
|
|
try {
|
|
const host = document.querySelector(".mjr-assets-manager.mjr-am-container");
|
|
const existing = document.getElementById(RUNTIME_DASHBOARD_ID);
|
|
if (!host) {
|
|
try {
|
|
existing?.remove?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Anchor to the Assets Manager container (not the scrollable grid),
|
|
// so the widget stays fixed while the grid scrolls and disappears when panel closes.
|
|
try {
|
|
const hostPos = String(getComputedStyle(host).position || "").toLowerCase();
|
|
if (!hostPos || hostPos === "static") {
|
|
host.style.position = "relative";
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
let el = document.getElementById(RUNTIME_DASHBOARD_ID);
|
|
if (!el) {
|
|
el = document.createElement("div");
|
|
el.id = RUNTIME_DASHBOARD_ID;
|
|
el.style.position = "absolute";
|
|
el.style.bottom = "10px";
|
|
el.style.right = "10px";
|
|
el.style.zIndex = "9999";
|
|
el.style.padding = "6px 10px";
|
|
el.style.borderRadius = "10px";
|
|
el.style.border = "1px solid rgba(255,255,255,0.16)";
|
|
el.style.background = "rgba(0,0,0,0.45)";
|
|
el.style.backdropFilter = "blur(4px)";
|
|
el.style.color = "var(--content-fg, #fff)";
|
|
el.style.fontSize = "11px";
|
|
el.style.pointerEvents = "none";
|
|
el.style.display = "flex";
|
|
el.style.flexDirection = "column";
|
|
host.appendChild(el);
|
|
} else if (el.parentElement !== host) {
|
|
host.appendChild(el);
|
|
}
|
|
return el;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function refreshRuntimeStatusDashboard() {
|
|
const el = ensureRuntimeStatusDashboard();
|
|
if (!el) return false;
|
|
const metricsEl = ensureDashboardLine(el, "metrics");
|
|
const authEl = ensureDashboardLine(el, "auth");
|
|
try {
|
|
const [runtimeRes, securityRes] = await Promise.all([
|
|
getRuntimeStatus(),
|
|
getSecuritySettings(),
|
|
]);
|
|
let runtimeTitle = t("runtime.unavailable", "Runtime: unavailable");
|
|
if (!runtimeRes?.ok || !runtimeRes?.data) {
|
|
metricsEl.textContent = runtimeTitle;
|
|
} else {
|
|
const db = runtimeRes.data.db || {};
|
|
const idx = runtimeRes.data.index || {};
|
|
const w = runtimeRes.data.watcher || {};
|
|
const active = Number(db.active_connections || 0);
|
|
const enrichQ = Number(idx.enrichment_queue_length || 0);
|
|
const pending = Number(w.pending_files || 0);
|
|
metricsEl.textContent = t(
|
|
"runtime.metricsLine",
|
|
"DB active: {active} | Enrich Q: {enrichQ} | Watcher pending: {pending}",
|
|
{ active, enrichQ, pending },
|
|
);
|
|
runtimeTitle = t(
|
|
"runtime.metricsTitle",
|
|
"Runtime Metrics\nDB active connections: {active}\nEnrichment queue: {enrichQ}\nWatcher pending files: {pending}",
|
|
{ active, enrichQ, pending },
|
|
);
|
|
}
|
|
const authSummary = buildWriteAuthSummary(securityRes?.data?.prefs || null);
|
|
authEl.textContent = authSummary.text;
|
|
authEl.style.color = authSummary.color;
|
|
el.title = `${runtimeTitle}\n${authSummary.text}`;
|
|
return true;
|
|
} catch {
|
|
metricsEl.textContent = t("runtime.unavailable", "Runtime: unavailable");
|
|
authEl.textContent = t("runtime.writeAuthUnknown", "Write auth: unknown");
|
|
authEl.style.color = "#c8ced8";
|
|
el.title = `${t("runtime.unavailable", "Runtime: unavailable")}\n${authEl.textContent}`;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function startRuntimeStatusDashboard() {
|
|
try {
|
|
refreshRuntimeStatusDashboard().catch(() => {});
|
|
if (window.__MJR_RUNTIME_STATUS_INFLIGHT__ == null) {
|
|
window.__MJR_RUNTIME_STATUS_INFLIGHT__ = false;
|
|
}
|
|
if (window.__MJR_RUNTIME_STATUS_MISS_COUNT__ == null) {
|
|
window.__MJR_RUNTIME_STATUS_MISS_COUNT__ = 0;
|
|
}
|
|
if (!window.__MJR_RUNTIME_STATUS_INTERVAL__) {
|
|
window.__MJR_RUNTIME_STATUS_INTERVAL__ = setInterval(() => {
|
|
if (window.__MJR_RUNTIME_STATUS_INFLIGHT__) return;
|
|
window.__MJR_RUNTIME_STATUS_INFLIGHT__ = true;
|
|
refreshRuntimeStatusDashboard()
|
|
.then((visible) => {
|
|
if (visible) {
|
|
window.__MJR_RUNTIME_STATUS_MISS_COUNT__ = 0;
|
|
return;
|
|
}
|
|
window.__MJR_RUNTIME_STATUS_MISS_COUNT__ =
|
|
Number(window.__MJR_RUNTIME_STATUS_MISS_COUNT__ || 0) + 1;
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
window.__MJR_RUNTIME_STATUS_INFLIGHT__ = false;
|
|
});
|
|
}, 10000);
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|