v0.32.6_official

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-14 13:04:06 +02:00
1 parent fc8711d4d8
commit d2c6cf59a9
16 files changed
+1801 -1027

No files matched your search

+47 -7
View File
@@ -96,8 +96,6 @@ function applyDashboardLayout(order, hidden) {
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(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]));
// Re-appending an already ordered live grid can break touch-scroll
// anchoring in mobile browsers. Move the DOM only on a real order change.
const orderedNodes = normalizedOrder.map(id => nodes.get(id)).filter(Boolean);
for (const node of orderedNodes) node.hidden = hiddenSet.has(node.dataset.dashboardWidget);
const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")];
@@ -129,23 +127,25 @@ function dashboardWidgetTypeBadge(id) {
}
function renderDashboardWidgetSettings() {
const list = $("dashboard-widget-list"); if (!list) return;
const previousScroll = list.scrollLeft;
const focusedRow = document.activeElement?.closest?.("[data-dashboard-widget-setting]");
const focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null;
const focusedAction = focusedId ? document.activeElement?.dataset.dashboardWidgetAction : null;
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
const rows = order.map((id, index) => {
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 toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.dataset.dashboardWidgetAction = "toggle"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
const descriptionKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
if (descriptionKey) {
const description = t(descriptionKey);
row.title = description;
toggle.setAttribute("aria-description", description);
}
// Type badges belong in the visual layout editor, not the long
// Appearance settings list where they crowd the visibility controls.
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"));
const up = el("button", "←", "quiet"); up.type = "button"; up.dataset.dashboardWidgetAction = "up"; up.disabled = index === 0; up.setAttribute("aria-label", t("dashboardWidgetsMoveEarlier"));
const down = el("button", "→", "quiet"); down.type = "button"; down.dataset.dashboardWidgetAction = "down"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("dashboardWidgetsMoveLater"));
toggle.addEventListener("click", () => {
const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id);
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings();
@@ -160,6 +160,46 @@ function renderDashboardWidgetSettings() {
up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(toggle, actions); return row;
});
list.replaceChildren(...rows);
list.scrollLeft = previousScroll;
if (focusedId && focusedAction) {
const row = [...list.children].find(item => item.dataset.dashboardWidgetSetting === focusedId);
row?.querySelector(`[data-dashboard-widget-action="${focusedAction}"]`)?.focus({ preventScroll: true });
}
syncDashboardWidgetListView();
if (typeof requestAnimationFrame === "function") requestAnimationFrame(syncDashboardWidgetScrollButtons);
}
let dashboardWidgetListExpanded = false;
function syncDashboardWidgetListView() {
const list = $("dashboard-widget-list"), expand = $("dashboard-widget-expand");
if (!list || !expand) return;
list.classList.toggle("is-expanded", dashboardWidgetListExpanded);
expand.setAttribute("aria-expanded", String(dashboardWidgetListExpanded));
const key = dashboardWidgetListExpanded ? "dashboardWidgetsCompact" : "dashboardWidgetsShowAll";
expand.dataset.i18n = key;
expand.textContent = t(key);
for (const id of ["dashboard-widget-scroll-prev", "dashboard-widget-scroll-next"]) {
const button = $(id); if (button) button.hidden = dashboardWidgetListExpanded;
}
syncDashboardWidgetScrollButtons();
}
function syncDashboardWidgetScrollButtons() {
const list = $("dashboard-widget-list"); if (!list) return;
const previous = $("dashboard-widget-scroll-prev"), next = $("dashboard-widget-scroll-next");
if (previous) previous.disabled = dashboardWidgetListExpanded || list.scrollLeft <= 1;
if (next) next.disabled = dashboardWidgetListExpanded || list.scrollLeft + list.clientWidth >= list.scrollWidth - 1;
}
$("dashboard-widget-expand")?.addEventListener("click", () => {
dashboardWidgetListExpanded = !dashboardWidgetListExpanded;
syncDashboardWidgetListView();
});
$("dashboard-widget-list")?.addEventListener?.("scroll", syncDashboardWidgetScrollButtons, { passive: true });
window.addEventListener?.("resize", syncDashboardWidgetScrollButtons);
for (const [id, direction] of [["dashboard-widget-scroll-prev", -1], ["dashboard-widget-scroll-next", 1]]) {
$(id)?.addEventListener("click", () => {
const list = $("dashboard-widget-list");
if (!list) return;
list.scrollBy({ left: direction * Math.max(160, list.clientWidth * .8), behavior: "smooth" });
});
}
function refreshChartStylePalette(active = adminPreferences?.chart_style || "default") {
const target = $("chart-style-palette"); if (!target || !languagePacks.size) return;
+11 -16
View File
@@ -1,4 +1,3 @@
function verifyAdminModules() {
const required = [
["rustProxyIntegrations", "bindings"],
@@ -12,7 +11,6 @@ function verifyAdminModules() {
const missing = required.find(([module, method]) => typeof window[module]?.[method] !== "function");
if (missing) throw new Error(`Panel resources incomplete (${missing[0]}). Reload to retry.`);
}
async function startAdminPanel() {
try {
verifyAdminModules();
@@ -33,7 +31,6 @@ async function startAdminPanel() {
}
if (document.readyState === "complete") startAdminPanel();
else document.addEventListener("DOMContentLoaded", startAdminPanel, { once: true });
function certificateUsesExternalFiles(cert) {
return String(cert?.cert_file || "").startsWith("/") && String(cert?.key_file || "").startsWith("/");
}
@@ -53,7 +50,6 @@ function syncCertificateFileSource() {
$("certificate-external-acl-example").hidden = !external;
}
$("certificate-file-source").addEventListener("change", syncCertificateFileSource);
function renderCertificates() {
if (!envelope) return;
const settings = envelope.config.tls || {};
@@ -69,7 +65,6 @@ function renderCertificates() {
if (settings.default_certificate === cert.id) used.push(t("defaultLabel"));
const managed = !!cert.issuer, inUse = used.length > 0;
const box = el("div", undefined, "entity certificate-card");
const head = el("div", undefined, "certificate-card-head");
const identity = el("div", undefined, "certificate-card-identity");
identity.append(el("span", cert.id, "entity-title"));
@@ -152,7 +147,7 @@ $("reload-certificates").addEventListener("click", async () => {
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
syncCertificateFileSource();
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null,
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, lastCacheOutcomeRates = null,
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
const hiddenNetworkInterfaces = (() => {
try {
@@ -206,8 +201,7 @@ function svgNode(tag, attrs = {}) {
for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
return node;
}
// Diagnostic charts keep their SVG and label nodes across samples. Updating the
// existing Text node avoids remove/insert mutations while the user is scrolling.
function setChartText(node, value) {
const text = String(value);
if (node.firstChild?.nodeType === 3 && node.firstChild === node.lastChild) {
@@ -216,8 +210,7 @@ function setChartText(node, value) {
node.textContent = text;
}
}
// Preserve stable Overview DOM while live samples arrive: replacing network SVGs
// during an iOS WebKit momentum scroll can invalidate the current scroll anchor.
let latestNetworkInterfaceSnapshot = [];
function renderNetworkInterfaceCharts(interfaces, appendSample) {
const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : [];
@@ -305,8 +298,7 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
children.push(empty);
}
}
// The common live-sample path updates only text and SVG path attributes.
// Reparent rows solely when interfaces or visibility have actually changed.
if (children.length !== target.children.length || children.some((node, index) => node !== target.children[index])) {
target.replaceChildren(...children);
}
@@ -316,7 +308,6 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
$("metric-h3").textContent = fmt(m.http3_requests);
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
// Slow details vs cached live chart telemetry.
if (systemSample) {
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
$("process-summary").hidden = d.process_threads == null;
@@ -358,9 +349,13 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
const currentCounters = [c.hits, c.misses, c.bypasses].map(Number);
if (currentCounters.every(Number.isFinite)) {
const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
if (delta.deltas || !lastCacheCounters) { const values = delta.deltas || [0, 0, 0];
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]); cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
} lastCacheCounters = delta.next;
const values = delta.deltas || lastCacheOutcomeRates || (!lastCacheCounters ? [0, 0, 0] : null);
if (values) {
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]);
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
lastCacheOutcomeRates = [...values];
}
lastCacheCounters = delta.next;
}
}
if (appendSample) {
+35 -11
View File
@@ -2477,8 +2477,9 @@
data-appearance-reset="dashboard" data-i18n="resetSection"
type="button">Reset section</button>
</div>
<div class="appearance-settings-grid">
<label><span data-i18n="dashboardDensity">Dashboard density</span><select
<div class="appearance-settings-grid dashboard-appearance-grid">
<label class="dashboard-density-setting"><span
data-i18n="dashboardDensity">Dashboard density</span><select
id="ui-dashboard-density">
<option data-i18n="densityNormal" value="normal">Normal</option>
<option data-i18n="densityCompact" value="compact">Compact</option>
@@ -2486,15 +2487,38 @@
</select><small class="fine" data-i18n="dashboardDensityHelp">Controls
Overview telemetry card size without changing the rest of the
interface.</small></label>
<div class="dashboard-widget-settings"><span
data-i18n="dashboardWidgets">Overview cards</span><small class="fine"
data-i18n="dashboardWidgetsHelp">Show, hide and reorder the telemetry
cards at the top of Overview.</small>
<button class="outline dashboard-layout-open-settings" type="button"
id="overview-layout-open-settings"
data-i18n="overviewLayoutEditorOpenSettings">Visual layout
editor</button>
<div class="dashboard-widget-list" id="dashboard-widget-list"></div>
<div class="dashboard-widget-settings">
<div class="dashboard-widget-settings-head">
<div class="dashboard-widget-settings-intro">
<span data-i18n="dashboardWidgets">Overview cards</span>
<small class="fine" data-i18n="dashboardWidgetsHelp">Show, hide and
reorder all Overview blocks, including charts, diagnostics and
the backend table.</small>
</div>
<div class="dashboard-widget-settings-controls">
<button class="outline dashboard-layout-open-settings" type="button"
id="overview-layout-open-settings"
data-i18n="overviewLayoutEditorOpenSettings">Visual layout
editor</button>
<button class="quiet" id="dashboard-widget-expand" type="button"
aria-expanded="false" aria-controls="dashboard-widget-list"
data-i18n="dashboardWidgetsShowAll">Show all</button>
<button class="quiet dashboard-widget-scroll-button"
id="dashboard-widget-scroll-prev" type="button"
data-i18n-aria="dashboardWidgetsScrollBack"
aria-label="Scroll cards left">&larr;</button>
<button class="quiet dashboard-widget-scroll-button"
id="dashboard-widget-scroll-next" type="button"
data-i18n-aria="dashboardWidgetsScrollNext"
aria-label="Scroll cards right">&rarr;</button>
</div>
</div>
<div class="dashboard-widget-list" id="dashboard-widget-list" role="group"
data-i18n-aria="dashboardWidgets" aria-label="Overview cards"
tabindex="0"></div>
<small class="fine dashboard-widget-scroll-hint"
data-i18n="dashboardWidgetsScrollHelp">Scroll horizontally to browse
cards, or expand the list.</small>
<small class="fine" data-i18n="streamWidgetSettingsHelp">Stream cards are
opt-in and only change Dashboard presentation, not Stream routing or
monitoring.</small>
+1461 -963
View File
File diff suppressed because it is too large. Load diff