v0.25.5
This commit is contained in:
1 parent
1dea7f55c8
commit
c819f0a335
151 files changed
+7610
-4264
No files matched your search
@@ -390,6 +390,8 @@ function stopTunnelRefresh() { if (tunnelRefreshTimer) clearInterval(tunnelRefre
|
||||
function startTunnelRefresh() { stopTunnelRefresh(); tunnelRefreshTimer = setInterval(() => { if ($("tunnels-dialog")?.open) void loadTunnels(); }, 2000); }
|
||||
$("login-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; authMessage(""); try { await withFormSubmitState(form, event.submitter, async () => { const username = $("username").value.trim(); const password = $("password").value; if (setupRequired) { if (password !== $("password-confirm").value) throw fieldError("password-confirm", t("passwordMismatch")); await api("/api/auth/setup", { method: "POST", body: JSON.stringify({ token: $("setup-token").value.trim(), username, password }) }); setupRequired = false; syncAuthForm(); } const info = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); await activate(info); clearTimeout(toastTimer); hideToast(); }); } catch (e) { if (e.code === "auth.already_initialized") { setupRequired = false; syncAuthForm(); } $("password").value = ""; $("password-confirm").value = ""; authMessage(e.message); } });
|
||||
$("logout").addEventListener("click", async () => { try { await api("/api/auth/logout", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { if (e.status === 401) lock(); toast(e.message, true); } });
|
||||
$("about-open").addEventListener("click", () => { const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
|
||||
$("about-close").addEventListener("click", () => $("about-dialog").close());
|
||||
$("language").addEventListener("change", () => { language = $("language").value; try { localStorage.setItem("rp-language", language); } catch { } translate(); });
|
||||
$("ui-theme").addEventListener("change", async () => {
|
||||
const select = $("ui-theme"), previousTheme = adminPreferences?.theme || document.documentElement.dataset.theme || "dark", nextTheme = normalizeAdminTheme(select.value);
|
||||
@@ -398,8 +400,17 @@ $("ui-theme").addEventListener("change", async () => {
|
||||
catch (error) { applyAdminTheme(previousTheme); if (error.status !== 401) toast(error.message, true); }
|
||||
finally { select.disabled = false; }
|
||||
});
|
||||
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => setPanel(button.dataset.panel)));
|
||||
window.addEventListener("popstate", () => setPanel(panelFromLocation(), false));
|
||||
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");
|
||||
function setMobileMenu(open) {
|
||||
if (!mobileMenuToggle || !dashboardSidebar) return;
|
||||
dashboardSidebar.classList.toggle("mobile-nav-open", open);
|
||||
mobileMenuToggle.setAttribute("aria-expanded", String(open));
|
||||
}
|
||||
mobileMenuToggle?.addEventListener("click", event => { event.stopPropagation(); setMobileMenu(!dashboardSidebar.classList.contains("mobile-nav-open")); });
|
||||
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => { setPanel(button.dataset.panel); setMobileMenu(false); }));
|
||||
document.addEventListener("click", event => { if (dashboardSidebar?.classList.contains("mobile-nav-open") && !dashboardSidebar.contains(event.target)) setMobileMenu(false); });
|
||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && dashboardSidebar?.classList.contains("mobile-nav-open")) { setMobileMenu(false); mobileMenuToggle?.focus(); } });
|
||||
window.addEventListener("popstate", () => { setPanel(panelFromLocation(), false); setMobileMenu(false); });
|
||||
$("host-policy-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const hosts = [...new Set($("policy-hosts").value.split(",").map(h => h.trim().toLowerCase()).filter(Boolean))]; const acl = { default: $("policy-default").value, rules: parseRules($("policy-rules").value, $("policy-rules")) }; const next = structuredClone(envelope.config); next.host_policies = (next.host_policies || []).map(p => ({ ...p, hosts: p.hosts.filter(h => !hosts.includes(h)) })).filter(p => p.hosts.length); next.host_policies.push({ hosts, acl }); if (await saveSection("host-policies", next.host_policies)) { form.reset(); if ($("security-policy-editor")) $("security-policy-editor").open = false; } }); } catch (e) { toast(e.message, true); } });
|
||||
$("password-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; try { await withFormSubmitState(form, event.submitter, async () => { if ($("new-password").value !== $("new-password-confirm").value) throw fieldError("new-password-confirm", t("passwordMismatch")); await api("/api/auth/password", { method: "POST", body: JSON.stringify({ current_password: $("current-password").value, new_password: $("new-password").value }) }); lock(); toast(t("passwordChanged")); }); } catch (e) { toast(e.message, true); } });
|
||||
$("revoke-sessions").addEventListener("click", async () => { if (!(await appConfirm(t("confirmRevoke"), { danger: true }))) return; try { await api("/api/auth/logout-all", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { toast(e.message, true); } });
|
||||
|
||||
@@ -6,7 +6,7 @@ function renderSettings() {
|
||||
if (!settingsDirty.has("response-form")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.4";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.5";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ main {
|
||||
background: #101923;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.docs-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -312,7 +313,7 @@ main {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
|
||||
}
|
||||
|
||||
.markdown-reader :not(pre) > code {
|
||||
.markdown-reader :not(pre)>code {
|
||||
padding: 2px 4px;
|
||||
background: #edf1f4
|
||||
}
|
||||
@@ -342,4 +343,4 @@ main {
|
||||
.markdown-reader {
|
||||
padding: 18px
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-6
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -10,10 +11,12 @@
|
||||
<script defer src="/docs/swagger-ui-bundle.js"></script>
|
||||
<script defer src="/docs/init.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="docs-bar"><a class="docs-name" href="/">RustProxy</a>
|
||||
<h1 data-i18n="docsTitle">Documentation</h1>
|
||||
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout" data-i18n="docsLogout">Sign out</button>
|
||||
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout"
|
||||
data-i18n="docsLogout">Sign out</button>
|
||||
</header>
|
||||
<main>
|
||||
<section class="docs-session">
|
||||
@@ -22,14 +25,16 @@
|
||||
</section>
|
||||
<p id="docs-error" role="alert"></p>
|
||||
<nav class="docs-tabs" aria-label="Documentation sections">
|
||||
<button type="button" id="docs-tab-reader" class="active" data-docs-tab="reader" data-i18n="docsReader">Reader</button>
|
||||
<button type="button" id="docs-tab-reader" class="active" data-docs-tab="reader"
|
||||
data-i18n="docsReader">Reader</button>
|
||||
<button type="button" id="docs-tab-api" data-docs-tab="api" data-i18n="docsApi">API</button>
|
||||
</nav>
|
||||
<section id="docs-reader-pane" class="docs-pane">
|
||||
<div class="reader-layout">
|
||||
<aside class="reader-sidebar">
|
||||
<label for="docs-reader-search" data-i18n="docsReaderSearch">Search documentation</label>
|
||||
<input id="docs-reader-search" type="search" autocomplete="off" data-i18n-placeholder="docsReaderSearchPlaceholder" placeholder="Search title or path">
|
||||
<input id="docs-reader-search" type="search" autocomplete="off"
|
||||
data-i18n-placeholder="docsReaderSearchPlaceholder" placeholder="Search title or path">
|
||||
<div id="docs-reader-list" class="reader-list"></div>
|
||||
</aside>
|
||||
<article id="docs-reader-content" class="markdown-reader" aria-live="polite"></article>
|
||||
@@ -37,12 +42,15 @@
|
||||
</section>
|
||||
<section id="docs-api-pane" class="docs-pane" hidden>
|
||||
<section class="docs-api-controls">
|
||||
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write operations</span></label>
|
||||
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed here.</p>
|
||||
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write
|
||||
operations</span></label>
|
||||
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed
|
||||
here.</p>
|
||||
</section>
|
||||
<div id="swagger-ui"></div>
|
||||
<p><a href="/docs/licenses.txt" data-i18n="docsLicenses">Swagger UI licenses</a></p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
+577
-248
File diff suppressed because it is too large.
Load diff
@@ -126,7 +126,7 @@ curl --fail --show-error --request POST \
|
||||
catch (error) { $("certificate-api-status").textContent = error.message; }
|
||||
finally { certificateApiLoading = false; }
|
||||
}
|
||||
const defaults = { enabled: false, listen: "127.0.0.1:9191", path: "/metrics", allowed_cidrs: ["127.0.0.0/8", "::1/128"], bearer_token_env: null, allow_trusted_network_without_token: false, include_system: true, include_vhosts: true, include_backends: true, include_docker_vhosts: true, include_cache_domains: true, include_streams: true, include_logging: true };
|
||||
const defaults = { enabled: false, listen: "127.0.0.1:9191", path: "/metrics", allowed_cidrs: ["127.0.0.0/8", "::1/128"], bearer_token_env: null, allow_trusted_network_without_token: false, include_system: true, include_vhosts: true, include_traffic: true, include_backends: true, include_docker_vhosts: true, include_cache_domains: true, include_streams: true, include_logging: true };
|
||||
function example() {
|
||||
const addr = $("prom-listen").value.trim(), path = $("prom-path").value.trim(), env = $("prom-token-env").value.trim();
|
||||
$("prom-scrape-example").textContent = `scrape_configs:\n - job_name: rustproxy\n metrics_path: ${JSON.stringify(path)}\n static_configs:\n - targets: [${JSON.stringify(addr)}]${env ? "\n authorization:\n credentials_file: /etc/prometheus/rustproxy.token" : ""}`;
|
||||
@@ -136,7 +136,7 @@ curl --fail --show-error --request POST \
|
||||
if (!settingsDirty.has("prometheus-form")) {
|
||||
$("prom-enabled").checked = p.enabled; $("prom-listen").value = p.listen; $("prom-path").value = p.path;
|
||||
$("prom-cidrs").value = p.allowed_cidrs.join("\n"); $("prom-token-env").value = p.bearer_token_env || ""; $("prom-trusted-no-token").checked = !!p.allow_trusted_network_without_token;
|
||||
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
|
||||
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["traffic", "include_traffic"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
|
||||
}
|
||||
const status = latestStatus?.prometheus;
|
||||
$("prom-status").textContent = status?.restart_required ? t("restartPending") : status?.active?.enabled ? `${t("prometheusActive")}: ${status.active.listen}${p.path}` : t("prometheusDisabled"); example();
|
||||
@@ -170,7 +170,7 @@ curl --fail --show-error --request POST \
|
||||
bindSettingsForm("prometheus-form", next => {
|
||||
next.prometheus = {
|
||||
enabled: $("prom-enabled").checked, listen: $("prom-listen").value.trim(), path: $("prom-path").value.trim(), allowed_cidrs: $("prom-cidrs").value.split(/[\n,]/).map(v => v.trim()).filter(Boolean), bearer_token_env: $("prom-token-env").value.trim() || null, allow_trusted_network_without_token: $("prom-trusted-no-token").checked,
|
||||
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
|
||||
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_traffic: $("prom-traffic").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
|
||||
};
|
||||
}, "prometheus");
|
||||
bindSettingsForm("administration-form", next => {
|
||||
|
||||
+230
-28
@@ -3781,7 +3781,7 @@ body[data-density="dense"] .stat strong {
|
||||
max-width: none
|
||||
}
|
||||
|
||||
.settings-page>.edit-form + .edit-form {
|
||||
.settings-page>.edit-form+.edit-form {
|
||||
margin-top: var(--ui-gap)
|
||||
}
|
||||
|
||||
@@ -3920,7 +3920,8 @@ body[data-density="dense"] .stat strong {
|
||||
dialog.vhost-dialog,
|
||||
dialog.logs-dialog,
|
||||
dialog.audit-dialog,
|
||||
dialog.certificate-dialog {
|
||||
dialog.certificate-dialog,
|
||||
dialog.config-history-dialog {
|
||||
border-radius: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
@@ -4692,11 +4693,16 @@ dialog .window-heading {
|
||||
}
|
||||
|
||||
@media(max-width:1100px) {
|
||||
.traffic-wide-chart { grid-column: span 1 }
|
||||
.traffic-wide-chart {
|
||||
grid-column: span 1
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:650px) {
|
||||
.traffic-bar-row { grid-template-columns: minmax(88px, 1.2fr) minmax(90px, 2fr) minmax(60px, auto); gap: 7px }
|
||||
.traffic-bar-row {
|
||||
grid-template-columns: minmax(88px, 1.2fr) minmax(90px, 2fr) minmax(60px, auto);
|
||||
gap: 7px
|
||||
}
|
||||
}
|
||||
|
||||
.logging-grid label {
|
||||
@@ -4720,6 +4726,7 @@ dialog .window-heading {
|
||||
}
|
||||
|
||||
@media(max-width:720px) {
|
||||
|
||||
.traffic-chart-grid,
|
||||
.logging-grid {
|
||||
grid-template-columns: 1fr
|
||||
@@ -6778,7 +6785,6 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* Certificates workspace (0.20.0): use the same tab language as Settings. */
|
||||
#panel-tls .certificate-workspace-heading {
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px
|
||||
@@ -7031,7 +7037,6 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline operation feedback mirrors the login feedback pattern and remains local to the action. */
|
||||
.operation-message:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -7330,7 +7335,6 @@ body[data-density="compact"] .page-footer {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 0.24.3 dashboard refinements */
|
||||
.payload-split {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
@@ -7515,7 +7519,7 @@ body[data-density="compact"] .page-footer {
|
||||
color: var(--notice-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 0.24.8 cache occupancy charts and compact memory-accounting help */
|
||||
|
||||
.mini-heading-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -7607,7 +7611,6 @@ body[data-density="compact"] .page-footer {
|
||||
color: var(--transform-chart);
|
||||
}
|
||||
|
||||
/* Web Service editor mirrors the vhost tabbed configuration layout. */
|
||||
.web-service-pages {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -7628,21 +7631,10 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
|
||||
.config-history-dialog {
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
padding: 0;
|
||||
width: min(1180px, calc(100vw - 24px));
|
||||
max-width: calc(100vw - 24px);
|
||||
max-height: calc(100dvh - 28px);
|
||||
overflow: hidden;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 18px 70px var(--shadow-strong);
|
||||
}
|
||||
|
||||
.config-history-dialog::backdrop {
|
||||
background: var(--shadow-strong);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.config-history-layout {
|
||||
@@ -7667,7 +7659,6 @@ body[data-density="compact"] .page-footer {
|
||||
|
||||
.config-history-month {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 9px;
|
||||
background: var(--panel);
|
||||
}
|
||||
@@ -7695,7 +7686,6 @@ body[data-density="compact"] .page-footer {
|
||||
min-height: 34px;
|
||||
padding: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
@@ -7769,7 +7759,6 @@ button.config-history-day.is-selected {
|
||||
|
||||
.config-history-change {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
@@ -7797,9 +7786,222 @@ button.config-history-day.is-selected {
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.config-history-layout { grid-template-columns: 1fr; overflow: auto; }
|
||||
.config-history-browser, .config-history-detail { overflow: visible; }
|
||||
.config-history-change-values { grid-template-columns: 1fr; }
|
||||
.config-history-detail-head { flex-direction: column; }
|
||||
.config-history-detail-actions { align-items: flex-start; text-align: left; }
|
||||
.config-history-layout {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.config-history-browser,
|
||||
.config-history-detail {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.config-history-change-values {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.config-history-detail-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-history-detail-actions {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width:850px) {
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 60;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.sidebar .brand {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
min-height: 36px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: 600 11px var(--mono);
|
||||
}
|
||||
|
||||
.mobile-menu-toggle:hover,
|
||||
.mobile-menu-toggle[aria-expanded="true"] {
|
||||
color: var(--text);
|
||||
border-color: var(--border-hover);
|
||||
background: var(--nav-active-bg);
|
||||
}
|
||||
|
||||
.mobile-menu-icon {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: 15px;
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
box-shadow: 0 -5px currentColor, 0 5px currentColor;
|
||||
}
|
||||
|
||||
.sidebar .main-navigation {
|
||||
grid-column: 1 / -1;
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sidebar.mobile-nav-open .main-navigation {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.sidebar .main-navigation .nav {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-left: 2px solid transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar .main-navigation .nav.active {
|
||||
border-color: var(--line);
|
||||
border-left-color: var(--green);
|
||||
background: var(--nav-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:600px) {
|
||||
.sidebar .main-navigation .nav span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.about-dialog {
|
||||
width: min(560px, calc(100vw - 24px));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.about-dialog .window-heading button {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.about-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.about-product {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.about-product>div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.about-product strong {
|
||||
font: 600 16px var(--mono);
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.about-meta>div {
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.about-meta dt {
|
||||
margin-bottom: 5px;
|
||||
color: var(--muted);
|
||||
font: 9px var(--mono);
|
||||
letter-spacing: .8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.about-meta dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.about-card>p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.about-repository {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.about-repository span:last-child {
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media(max-width:620px) {
|
||||
.topbar {
|
||||
height: auto;
|
||||
min-height: 46px;
|
||||
padding: 7px 0;
|
||||
}
|
||||
|
||||
.topbar .breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.about-repository {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,12 @@
|
||||
liveChartLimit = LIVE_CHART_LIMITS.has(parsed) ? parsed : 60;
|
||||
for (const key of Object.keys(liveChartSamples)) liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit);
|
||||
if ($("transform-live-samples")) $("transform-live-samples").value = String(liveChartLimit);
|
||||
if (persist) { try { localStorage.setItem(LIVE_CHART_STORAGE_KEY, String(liveChartLimit)); } catch {} }
|
||||
if (persist) { try { localStorage.setItem(LIVE_CHART_STORAGE_KEY, String(liveChartLimit)); } catch { } }
|
||||
redrawLiveChart();
|
||||
}
|
||||
function initializeLiveChartLimit() {
|
||||
let saved = 60;
|
||||
try { saved = Number(localStorage.getItem(LIVE_CHART_STORAGE_KEY) || 60); } catch {}
|
||||
try { saved = Number(localStorage.getItem(LIVE_CHART_STORAGE_KEY) || 60); } catch { }
|
||||
setLiveChartLimit(saved, false);
|
||||
}
|
||||
function renderLiveChart(counters) {
|
||||
|
||||
Reference in new issue
Block a user