This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 00:57:19 +02:00
1 parent 81ab39e70a
commit 101df02a5f
45 files changed
+1148 -2277

No files matched your search

+4 -12
View File
@@ -86,8 +86,6 @@ function applyDashboardLayout(order, hidden) {
const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
for (const id of normalizedOrder) { const node = nodes.get(id); if (!node) continue; node.hidden = hiddenSet.has(id); grid.append(node); }
// Apply the newly selected visibility, not the previous saved preferences.
// During load, adminPreferences is assigned only after this function returns.
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview")
window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false);
@@ -104,18 +102,13 @@ function renderDashboardWidgetSettings() {
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
const visible = !hidden.has(id);
const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
const labelBlock = el("span", undefined, "dashboard-widget-setting-label-block");
labelBlock.append(el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
const descriptionKey = ({ stream_overview: "streamWidgetOverviewDescription", stream_traffic: "streamWidgetTrafficDescription", stream_sessions: "streamWidgetSessionsDescription", stream_backend_health: "streamWidgetHealthDescription" })[id];
if (descriptionKey) {
const description = el("small", t(descriptionKey), "dashboard-widget-description");
description.dataset.i18n = descriptionKey;
description.id = `dashboard-widget-help-${id}`;
description.setAttribute("role", "tooltip");
toggle.setAttribute("aria-describedby", description.id);
labelBlock.append(description);
const description = t(descriptionKey);
row.title = description;
toggle.setAttribute("aria-description", description);
}
toggle.append(el("span", undefined, "dashboard-widget-visibility"), labelBlock);
toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
const actions = el("span", undefined, "dashboard-widget-setting-actions");
const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp"));
const down = el("button", "↓", "quiet"); down.type = "button"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("moveDown"));
@@ -183,7 +176,6 @@ async function loadAdminPreferences(force = false) {
return applyAdminPreferences(value);
} catch (error) {
if (error.status === 401 || !authenticated || epoch !== authEpoch) throw error;
// Preferences are optional for startup. Keep UI and live telemetry usable.
console.warn("Preferences unavailable; using defaults", error);
adminPreferencesUnavailable = true;
return applyAdminPreferences({ ...ADMIN_APPEARANCE_DEFAULTS, refresh: REFRESH_DEFAULTS });
+28 -21
View File
@@ -1,5 +1,3 @@
// Single scheduler for all authenticated dashboard polling. SSE is controlled here
// but remains one native EventSource with its own browser reconnect behavior.
const REFRESH_DEFAULTS = Object.freeze({
live_enabled: true, overview_seconds: 2, traffic_seconds: 5,
cache_seconds: 5, system_info_seconds: 60
@@ -49,13 +47,11 @@ const rustProxyRefresh = (() => {
const controller = new AbortController(), generation = task.generation, epoch = authEpoch;
const panelAtStart = panel, tabAtStart = settingsTab;
task.controller = controller;
// A manual request remains valid with Live switched off, but not in a stale view.
const isCurrent = () => authenticated && epoch === authEpoch && !controller.signal.aborted &&
generation === task.generation && (manual ? panel === panelAtStart && settingsTab === tabAtStart : allowed(task));
const promise = Promise.resolve().then(() => task.run({ signal: controller.signal, isCurrent }));
task.inFlight = promise.catch(error => {
if (error?.name !== "AbortError" && isCurrent()) {
// A failure is isolated to this source; other timers continue.
if (task.onError) task.onError(error);
else console.warn(`Refresh ${task.id} failed:`, error);
}
@@ -161,19 +157,26 @@ const rustProxyRefresh = (() => {
if (!task) return Promise.reject(new Error(`Unknown refresh task: ${id}`));
return execute(task, true);
}
register("status-full", { interval: () => 0, run: async ({ signal, isCurrent }) => {
const value = await api("/api/status", { signal });
if (isCurrent()) renderStatus(value, true);
}});
register("status-fallback", { interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
const value = await api("/api/status?view=live", { signal });
if (isCurrent()) renderLiveStatus(value);
}, onError: error => { if (error?.status !== 401) $("connection-label").textContent = t("disconnected"); }});
register("logs-fallback", { interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
const value = await api("/api/logs", { signal });
if (isCurrent()) renderLogs(value);
}});
register("system-info", { interval: () => options.system_info_seconds,
register("status-full", {
interval: () => 0, run: async ({ signal, isCurrent }) => {
const value = await api("/api/status", { signal });
if (isCurrent()) renderStatus(value, true);
}
});
register("status-fallback", {
interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
const value = await api("/api/status?view=live", { signal });
if (isCurrent()) renderLiveStatus(value);
}, onError: error => { if (error?.status !== 401) $("connection-label").textContent = t("disconnected"); }
});
register("logs-fallback", {
interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
const value = await api("/api/logs", { signal });
if (isCurrent()) renderLogs(value);
}
});
register("system-info", {
interval: () => options.system_info_seconds,
active: () => panel === "overview" || (panel === "settings" && ["system-info", "cache"].includes(settingsTab)),
run: async ({ signal, isCurrent }) => {
const value = await api("/api/system-info", { signal });
@@ -183,11 +186,15 @@ const rustProxyRefresh = (() => {
window.rustProxySettings?.render();
}
});
register("tunnels", { interval: () => 2, active: () => !!$("tunnels-dialog")?.open,
run: async context => { await loadTunnels(context); } });
return { register, manual, start, stop, sync, update, setPanel, setSettingsTab,
register("tunnels", {
interval: () => 2, active: () => !!$("tunnels-dialog")?.open,
run: async context => { await loadTunnels(context); }
});
return {
register, manual, start, stop, sync, update, setPanel, setSettingsTab,
suspendForSessionValidation, completeSessionValidation,
current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null })) };
current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null }))
};
})();
window.rustProxyRefresh = rustProxyRefresh;
+1 -9
View File
@@ -1,5 +1,4 @@
// All modules must be available before any asynchronous session/config work can
// render the panel. DOMContentLoaded also waits for the complete deferred bundle.
function verifyAdminModules() {
const required = [
["rustProxyIntegrations", "bindings"],
@@ -17,9 +16,6 @@ function verifyAdminModules() {
async function startAdminPanel() {
try {
verifyAdminModules();
// Revalidate the session first. The anonymous document has its own login.js;
// app.js belongs to the authenticated shell and must not fall back to its
// legacy embedded sign-in form when another startup step fails.
const info = await api("/api/auth/session");
await loadLanguages();
setupRequired = false;
@@ -35,9 +31,6 @@ async function startAdminPanel() {
showStartupFailure(e);
}
}
// The previous bootstrap ran while sibling deferred scripts were still loading.
// Starting only after DOMContentLoaded prevents fast API responses from racing
// against a slower or failed JS resource on mobile/reverse-proxied connections.
if (document.readyState === "complete") startAdminPanel();
else document.addEventListener("DOMContentLoaded", startAdminPanel, { once: true });
@@ -159,7 +152,6 @@ $("reload-certificates").addEventListener("click", async () => {
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
syncCertificateFileSource();
// Dashboard metrics are actual server samples. Null means unavailable, not zero.
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null,
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
const hiddenNetworkInterfaces = (() => {
+1 -5
View File
@@ -1,5 +1,4 @@
// One in-flight validation per authenticated document. Neither BFCache restore nor
// visibility resume may trigger a reload: a 403 is a recoverable, visible error.
function revalidateSession() {
if (!authenticated) return Promise.resolve(false);
if (sessionCheckPromise) return sessionCheckPromise;
@@ -16,7 +15,6 @@ function revalidateSession() {
window.rustProxyRefresh?.completeSessionValidation(true);
return true;
} catch (error) {
// api() locks and navigates to the login document on a 401.
if (!authenticated || epoch !== authEpoch || error?.status === 401) return false;
window.rustProxyRefresh?.completeSessionValidation(false);
showSessionValidationFailure(error);
@@ -46,7 +44,6 @@ function displayStartupError(error, sessionResume = false) {
$("app-session-retry").hidden = !sessionResume;
}
function showSessionValidationFailure(error) {
// Keep the workspace inaccessible while its session cannot be verified.
displayStartupError(error, true);
}
function hideSessionValidationFailure() {
@@ -68,7 +65,6 @@ $("app-session-retry").addEventListener("click", () => {
);
});
function showStartupFailure(error) {
// All startup failure paths stop background tasks and show visible recovery.
window.rustProxyRefresh?.stop();
displayStartupError(error);
}
+1 -8
View File
@@ -1,5 +1,4 @@
// Four opt-in cards in the EXISTING dashboard widget order and status refresh.
// No timers, fetches, or independent persistence are created here.
window.rustProxyStreamDashboard = (() => {
const ids = ["stream_overview", "stream_traffic", "stream_sessions", "stream_backend_health"];
const history = { last: null, rx: [], tx: [] };
@@ -18,8 +17,6 @@ window.rustProxyStreamDashboard = (() => {
function syncVisibility(hiddenOverride = null) {
const prefs = window.rustProxyPreferences?.current?.();
const hidden = new Set(hiddenOverride ?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
// Visibility belongs exclusively to Appearance. An absent, disabled or
// restart-pending Stream must not silently override an explicit opt-in.
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
if (card("stream_traffic")?.hidden && wasVisible) clearTraffic();
wasVisible = !card("stream_traffic")?.hidden;
@@ -35,8 +32,6 @@ window.rustProxyStreamDashboard = (() => {
const active = (status?.streams?.active || []).filter(item => item.enabled !== false);
const metrics = status?.streams?.metrics || {};
if (!configured.some(item => item.enabled !== false) && !active.length) {
// A deliberately shown tile has a neutral empty state, not a
// hidden DOM node. No history is sampled without running streams.
for (const name of ["overview", "sessions", "health", "traffic"]) {
if (!card(({ overview: "stream_overview", sessions: "stream_sessions", health: "stream_backend_health", traffic: "stream_traffic" })[name])?.hidden) {
set(`stream-dashboard-${name}`, "-");
@@ -79,8 +74,6 @@ window.rustProxyStreamDashboard = (() => {
trafficRates("-", "-");
return;
}
// Switching tabs or appearance preferences paints cached numbers without
// generating an extra rate sample from the same status response.
if (!recordTrafficSample) {
if (history.rx.length) {
const rx = history.rx.at(-1), tx = history.tx.at(-1);