v0.32.6_official
This commit is contained in:
1 parent
fc8711d4d8
commit
d2c6cf59a9
16 files changed
+1801
-1027
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
@@ -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">←</button>
|
||||
<button class="quiet dashboard-widget-scroll-button"
|
||||
id="dashboard-widget-scroll-next" type="button"
|
||||
data-i18n-aria="dashboardWidgetsScrollNext"
|
||||
aria-label="Scroll cards right">→</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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user