From 4a3e4bcf8f6b6e04f0078cec82f46e10e0dd2fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 13 Oct 2026 23:34:10 +0200 Subject: [PATCH] v0.32.5 --- CHANGELOG.md | 1 + SHA256SUMS | 12 +- src/acl.rs | 16 +- src/acl/dynamic.rs | 5 +- src/admin/web/style.css | 899 ++++++++++++++---- src/config/compile.rs | 125 +-- src/main/admin_runtime.rs | 17 +- tools/checks/dynamic_acl_0325.py | 1 + tools/checks/overview_gutters_0324_browser.py | 0 tools/checks/preserve_host_0324.py | 0 10 files changed, 745 insertions(+), 331 deletions(-) mode change 100644 => 100755 tools/checks/dynamic_acl_0325.py mode change 100644 => 100755 tools/checks/overview_gutters_0324_browser.py mode change 100644 => 100755 tools/checks/preserve_host_0324.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c0ba5..ca4047a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## 0.32.5 +- Fix Rust `E0505` during dynamic ACL DNS resolution: give `lookup_host` an independently owned hostname, while returning the original normalized cache key. The resolver timeout, allowlist expiration and fail-closed behavior are unchanged. - Add bounded DNS/DDNS hostnames as **allow-only** client ACL rules in reusable profiles, host policies, vhosts, locations and Docker ACL assignments. - Refresh A/AAAA answers asynchronously every 60 seconds and after config diff --git a/SHA256SUMS b/SHA256SUMS index 631c3f6..a707483 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -2,7 +2,7 @@ 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore ad0382153576411fb19861072635c02bdf33de740d6f122d8f27bd9c52ce112a AGENTS.md -91c6942b3a0d24d61bcfd43f9ee9fc8f3f5d889aeaae63dde118849d64a283c5 CHANGELOG.md +d304d9da32d28b866176383b3b274e6ab020a54316d7e8ffe85cecd08e150f93 CHANGELOG.md 2f1de87bae58761fc8690ec7225520e3a16213c77cbbb5981e34f12a95810668 Cargo.lock 8c76ffaf4adba93df26e30f3dd163e0e5e201d069d1ae69c071ca42d7fc7264e Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile @@ -63,7 +63,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY. 007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md 25a06e7ddbb95d286e849b2fc5ba4adeedbddba0bad948e55bd91d962fd606de docs/SETTINGS.md 8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md -e79adbe06155b024adfe8d072abba36394b3c6ef2007920627c217e290c08a88 docs/TEST_REPORT.md +9cf4c4e27ba11f40143f5a158f91d72b6c88a0a612a448390428d7103bbedfb3 docs/TEST_REPORT.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md @@ -132,8 +132,8 @@ f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_s f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py 22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py 55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs -a0690d0d48bb92e53f4b94ce430ac434736ec2860c03ac4d50bd94df49706458 src/acl/dynamic.rs -2b452b2819f9af4f60c984f98b5fb1e4e79353dee4fad1add6ecb540b7be1c74 src/acl/dynamic/tests.rs +63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs +4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs 6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs 587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs @@ -512,7 +512,7 @@ dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/d f6a6d12275bc8ba627b4616a546d075511094bb064cd9058231cc86ca489743e tools/checks/dependency_upgrade_0312.py 38e35bed4faf984932b39bb5a00819fadc664387e8050a803ff4b5348b31369f tools/checks/docker_startup_0318_ui_test.mjs 3396e5a510787e70e2ad8d1fad90356af461a88fd84429525d56fa82113da0f3 tools/checks/docs_check.py -2d44f85ec083dedfee60dc15e65e040c6c208614411458b36a2fca8b5762f4f7 tools/checks/dynamic_acl_0325.py +3fc765a7c2723ec783f269cfebc29ad2c09a90f33ea4af58285f883a026c4fa2 tools/checks/dynamic_acl_0325.py 5bf26d9c883704e5ac85670bbcb7fb0bbad5d01779ab50407ff4fd5f3022a9cb tools/checks/file_logs_tabs_0315_ui_test.mjs 72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs 1b5ead59bb2ea0e1ff32bf085f791018149cb9cc29b071fae4fb98ea80010824 tools/checks/h3_navigation_0316.py @@ -541,7 +541,7 @@ e73afc62214e9092c180730f624a2039ac3fc850dcd865b2b117c11f7caaf88b tools/checks/r ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py 23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py -c8bdf822bfc972614b47caf3d6bfb392f1c4c96b43fc0113b963a8def73b7e7d tools/checks/security_logs_0315_ui_test.mjs +518b6d271931df62fc61d21688399d3c9b1986b58201061ac566667a16334823 tools/checks/security_logs_0315_ui_test.mjs 011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py 37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py bb862620104f925488e36904e258b7c934a097362ef879ecb275c64008cebb83 tools/checks/stream_0320_ui_test.mjs diff --git a/src/acl.rs b/src/acl.rs index ed7ffbd..043abe9 100644 --- a/src/acl.rs +++ b/src/acl.rs @@ -59,17 +59,13 @@ impl Acl { ensure!(rule.network.len() <= 254, "ACL network is too long"); let target = if let Ok(network) = parse_network(&rule.network) { if let IpNet::V6(net) = network { - ensure!( - net.addr().to_ipv4_mapped().is_none(), - "use IPv4 CIDR instead of IPv4-mapped IPv6 in ACL rules" - ); + ensure!(net.addr().to_ipv4_mapped().is_none(), + "use IPv4 CIDR instead of IPv4-mapped IPv6 in ACL rules"); } RuleMatch::Ip(network) } else { - ensure!( - rule.action == Action::Allow, - "dynamic DNS ACL rules only support allow; use a static IP/CIDR for deny" - ); + ensure!(rule.action == Action::Allow, + "dynamic DNS ACL rules only support allow; use a static IP/CIDR for deny"); RuleMatch::Dns(dynamic::hostname(&rule.network)?) }; rules.push((target, rule.action)); @@ -143,7 +139,9 @@ pub fn allows_with_country( && (ip_action == Some(Action::Allow) || acl.is_none_or(|value| value.default == Action::Allow)) } - crate::rate_limit::CountryMode::Allow => ip_action == Some(Action::Allow) || country_listed, + crate::rate_limit::CountryMode::Allow => { + ip_action == Some(Action::Allow) || country_listed + } } } /// Validate the already-decoded routing representation used internally after diff --git a/src/acl/dynamic.rs b/src/acl/dynamic.rs index f08ab65..4e0cdb1 100644 --- a/src/acl/dynamic.rs +++ b/src/acl/dynamic.rs @@ -86,9 +86,12 @@ pub(crate) fn configured_hostnames(config: &Config) -> Result> { } async fn resolve(name: String) -> (String, Option) { + // Keep the returned cache key owned independently of the DNS lookup future. + // The lookup result can retain the borrowed hostname until it is dropped. + let lookup_name = name.clone(); let answer = tokio::time::timeout( Duration::from_secs(LOOKUP_SECONDS), - tokio::net::lookup_host((name.as_str(), 0u16)), + tokio::net::lookup_host((lookup_name.as_str(), 0u16)), ).await; let mut ips = match answer { Ok(Ok(sockets)) => sockets.map(|socket| socket.ip()).collect::>(), diff --git a/src/admin/web/style.css b/src/admin/web/style.css index ac2e468..1af601e 100644 --- a/src/admin/web/style.css +++ b/src/admin/web/style.css @@ -132,9 +132,6 @@ html[data-theme="light"] { --scrollbar-thumb-hover: #879684 } - -/* Optional AMOLED palette: near-black surfaces, readable borders and restrained - mint/coral emphasis. Does not change the established dark appearance. */ html[data-theme="ultra_dark"] { color-scheme: dark; --bg: #030404; @@ -194,8 +191,6 @@ html[data-theme="ultra_dark"] { --scrollbar-thumb-hover: #617373; } -/* Warm, muted ivory. No pure-white canvas; numeric values and charts retain - clear contrast against their slightly lighter, neutral card surfaces. */ html[data-theme="cream"] { color-scheme: light; --bg: #f2ede3; @@ -253,10 +248,10 @@ html[data-theme="cream"] { --scrollbar-thumb-hover: #978a76; } -/* Theme palettes change color only; corners are controlled independently. */ html[data-theme="ultra_dark"] :is(.card, .stat, dialog) { box-shadow: 0 6px 24px rgb(0 0 0 / .18); } + html[data-theme="cream"] :is(.card, .stat, dialog) { box-shadow: 0 6px 20px rgb(78 58 38 / .055); } @@ -9804,7 +9799,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { } } -/* 0.31.5 traffic/log analysis actions */ .traffic-copy-value, .file-log-aggregate-copy { appearance: none; @@ -9856,10 +9850,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { } } - -/* Stored Logs on narrow screens: bounded container and a compact record list. - The complete parsed JSON is available by opening any row, while the desktop - table and the persisted column picker keep their original behavior. */ @media (max-width: 700px) { #panel-filelogs, @@ -9989,8 +9979,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { overflow-wrap: anywhere; } - /* Every user-selected column is available even when the administrator - replaces all default columns with custom parsed fields. */ #panel-filelogs .file-logs-table td[data-mobile-column="record"] { grid-column: 1 / -1; font-weight: 600; @@ -10082,7 +10070,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { } } -/* Tablet/narrow-desktop log tables scroll locally instead of expanding
. */ #panel-filelogs, #panel-filelogs .file-logs-workspace, #file-log-view-entries, @@ -10124,7 +10111,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { } } -/* 0.31.7: separate protocol tuning actions; keep them comfortably tappable. */ .listener-protocol-settings-actions { display: flex; gap: 14px; @@ -10141,7 +10127,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { display: none; } -/* 0.31.7 revision preview shows only changed leaf fields. */ .config-history-change-values { display: flex; flex-wrap: wrap; @@ -10215,7 +10200,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { overflow-wrap: anywhere; } -/* v0.32 Stream extensions reuse the responsive Vhost editor and stat cards. */ .stream-backend-pool { display: grid; gap: .8rem; @@ -10305,7 +10289,7 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { grid-template-columns: repeat(6, minmax(0, 1fr)); } } -/* Optional HTTP backend mode distribution (configured modes, not negotiated ALPN). */ + #panel-overview .backend-mode-bar { display: flex; height: 7px; @@ -10316,94 +10300,317 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { background: var(--line); margin: 7px 0 5px; } -#panel-overview .backend-mode-bar span { min-width: 0; } -#panel-overview .backend-mode-h1 { background: var(--traffic-h1); } -#panel-overview .backend-mode-auto { background: var(--traffic-h2); } -#panel-overview .backend-mode-compat { background: var(--traffic-h2); opacity: .5; } -#panel-overview .backend-mode-h2 { background: var(--traffic-h3); } -#panel-overview .backend-mode-bar[aria-label] { background-clip: padding-box; } -.vh-origin-protocol-info { color: var(--muted); } -.vh-origin-protocol-warning { color: var(--muted); } +#panel-overview .backend-mode-bar span { + min-width: 0; +} + +#panel-overview .backend-mode-h1 { + background: var(--traffic-h1); +} + +#panel-overview .backend-mode-auto { + background: var(--traffic-h2); +} + +#panel-overview .backend-mode-compat { + background: var(--traffic-h2); + opacity: .5; +} + +#panel-overview .backend-mode-h2 { + background: var(--traffic-h3); +} + +#panel-overview .backend-mode-bar[aria-label] { + background-clip: padding-box; +} + +.vh-origin-protocol-info { + color: var(--muted); +} + +.vh-origin-protocol-warning { + color: var(--muted); +} -/* Overview arrangement dialog: visual clones never participate in metric updates. */ .overview-layout-dialog { width: min(1180px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); overflow: auto; } -.overview-layout-dialog > .window-heading { + +.overview-layout-dialog>.window-heading { position: sticky; top: 0; z-index: 2; background: var(--bg); border-bottom: 1px solid var(--line); } -.overview-layout-body { display: grid; gap: 16px; padding: 14px; } -.overview-layout-body h3 { font-size: 14px; margin: 0 0 10px; } -.overview-layout-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--ui-gap, 10px); } -.overview-layout-tile { min-width: 0; border: 1px solid var(--line); background: var(--panel); cursor: grab; } -.overview-layout-tile:active { cursor: grabbing; } -.overview-layout-tile.is-dragging { opacity: .4; } -.overview-layout-tile.is-drop-target { outline: 2px solid var(--accent); outline-offset: -2px; } -.overview-layout-tile-toolbar { display: flex; align-items: center; gap: 3px; padding: 4px; border-bottom: 1px solid var(--line); } -.overview-layout-tile-toolbar button { min-height: 28px; min-width: 28px; padding: 3px 7px; } -.overview-layout-grip { padding: 0 5px; color: var(--muted); user-select: none; } -.overview-layout-hide { margin-inline-start: auto; font-size: 11px; } -.overview-layout-grid .stat { border: 0; padding: 12px; min-height: 98px; pointer-events: none; } -.overview-layout-grid .stat > span { font-size: 11px; } -.overview-layout-grid .stat strong { font-size: 24px; margin-top: 7px; overflow-wrap: anywhere; } -.overview-layout-grid .stat small { display: block; font-size: 10px; line-height: 1.4; overflow-wrap: anywhere; } -.overview-layout-grid .stat svg { width: 100%; height: 53px; } -.overview-layout-grid .stat-chart-legend { display: flex; flex-wrap: wrap; gap: 8px; } -.overview-layout-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; } -.overview-layout-section-heading input { max-width: 230px; } -.overview-layout-hidden-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; } -.overview-layout-hidden-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; border: 1px solid var(--line); padding: 8px; background: var(--panel); min-width: 0; } -.overview-layout-hidden-item span { font-size: 12px; min-width: 0; overflow-wrap: anywhere; } -.overview-layout-hidden-item button { flex-shrink: 0; } -.overview-layout-footer { position: sticky; bottom: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px; border-top: 1px solid var(--line); background: var(--bg); z-index: 2; } -.overview-layout-footer p { margin: 0 auto 0 0; font-size: 11px; overflow-wrap: anywhere; } -.dashboard-layout-open-settings { margin: 5px 0; justify-self: start; } -@media (max-width: 850px) { - .overview-layout-grid, .overview-layout-hidden-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} -@media (max-width: 550px) { - .overview-layout-grid, .overview-layout-hidden-list { grid-template-columns: minmax(0, 1fr); } - .overview-layout-section-heading { align-items: stretch; flex-direction: column; } - .overview-layout-section-heading input { max-width: none; width: 100%; } - .overview-layout-footer { flex-wrap: wrap; } - .overview-layout-footer p { width: 100%; } + +.overview-layout-body { + display: grid; + gap: 16px; + padding: 14px; +} + +.overview-layout-body h3 { + font-size: 14px; + margin: 0 0 10px; +} + +.overview-layout-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--ui-gap, 10px); +} + +.overview-layout-tile { + min-width: 0; + border: 1px solid var(--line); + background: var(--panel); + cursor: grab; +} + +.overview-layout-tile:active { + cursor: grabbing; +} + +.overview-layout-tile.is-dragging { + opacity: .4; +} + +.overview-layout-tile.is-drop-target { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + +.overview-layout-tile-toolbar { + display: flex; + align-items: center; + gap: 3px; + padding: 4px; + border-bottom: 1px solid var(--line); +} + +.overview-layout-tile-toolbar button { + min-height: 28px; + min-width: 28px; + padding: 3px 7px; +} + +.overview-layout-grip { + padding: 0 5px; + color: var(--muted); + user-select: none; +} + +.overview-layout-hide { + margin-inline-start: auto; + font-size: 11px; +} + +.overview-layout-grid .stat { + border: 0; + padding: 12px; + min-height: 98px; + pointer-events: none; +} + +.overview-layout-grid .stat>span { + font-size: 11px; +} + +.overview-layout-grid .stat strong { + font-size: 24px; + margin-top: 7px; + overflow-wrap: anywhere; +} + +.overview-layout-grid .stat small { + display: block; + font-size: 10px; + line-height: 1.4; + overflow-wrap: anywhere; +} + +.overview-layout-grid .stat svg { + width: 100%; + height: 53px; +} + +.overview-layout-grid .stat-chart-legend { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.overview-layout-section-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.overview-layout-section-heading input { + max-width: 230px; +} + +.overview-layout-hidden-list { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; +} + +.overview-layout-hidden-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + border: 1px solid var(--line); + padding: 8px; + background: var(--panel); + min-width: 0; +} + +.overview-layout-hidden-item span { + font-size: 12px; + min-width: 0; + overflow-wrap: anywhere; +} + +.overview-layout-hidden-item button { + flex-shrink: 0; +} + +.overview-layout-footer { + position: sticky; + bottom: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 12px; + border-top: 1px solid var(--line); + background: var(--bg); + z-index: 2; +} + +.overview-layout-footer p { + margin: 0 auto 0 0; + font-size: 11px; + overflow-wrap: anywhere; +} + +.dashboard-layout-open-settings { + margin: 5px 0; + justify-self: start; +} + +@media (max-width: 850px) { + + .overview-layout-grid, + .overview-layout-hidden-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 550px) { + + .overview-layout-grid, + .overview-layout-hidden-list { + grid-template-columns: minmax(0, 1fr); + } + + .overview-layout-section-heading { + align-items: stretch; + flex-direction: column; + } + + .overview-layout-section-heading input { + max-width: none; + width: 100%; + } + + .overview-layout-footer { + flex-wrap: wrap; + } + + .overview-layout-footer p { + width: 100%; + } } -/* One rearrangeable Overview canvas. Existing large charts, diagnostics and the - backend table keep their original nodes/IDs and their data update handlers. */ #panel-overview .dashboard-overview-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--ui-gap); align-items: stretch; } -#panel-overview .dashboard-overview-grid > [data-dashboard-widget] { min-width: 0; } -#panel-overview .dashboard-overview-grid > [hidden] { display: none !important; } -#panel-overview .dashboard-overview-grid > .card:not(.pool-card) { grid-column: span 2; } -#panel-overview .dashboard-overview-grid > .pool-card { grid-column: 1 / -1; margin-bottom: 0; } -#panel-overview .dashboard-overview-grid > .activity-card svg { min-width: 0; } + +#panel-overview .dashboard-overview-grid>[data-dashboard-widget] { + min-width: 0; +} + +#panel-overview .dashboard-overview-grid>[hidden] { + display: none !important; +} + +#panel-overview .dashboard-overview-grid>.card:not(.pool-card) { + grid-column: span 2; +} + +#panel-overview .dashboard-overview-grid>.pool-card { + grid-column: 1 / -1; + margin-bottom: 0; +} + +#panel-overview .dashboard-overview-grid>.activity-card svg { + min-width: 0; +} + @media (max-width: 1200px) { - #panel-overview .dashboard-overview-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } - #panel-overview .dashboard-overview-grid > .stat { grid-column: span 2; } - #panel-overview .dashboard-overview-grid > .card:not(.pool-card) { grid-column: span 2; } + #panel-overview .dashboard-overview-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + #panel-overview .dashboard-overview-grid>.stat { + grid-column: span 2; + } + + #panel-overview .dashboard-overview-grid>.card:not(.pool-card) { + grid-column: span 2; + } } + @media (max-width: 850px) { - #panel-overview .dashboard-overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - #panel-overview .dashboard-overview-grid > .stat { grid-column: span 1; } - #panel-overview .dashboard-overview-grid > .card:not(.pool-card) { grid-column: 1 / -1; } + #panel-overview .dashboard-overview-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + #panel-overview .dashboard-overview-grid>.stat { + grid-column: span 1; + } + + #panel-overview .dashboard-overview-grid>.card:not(.pool-card) { + grid-column: 1 / -1; + } } + @media (max-width: 550px) { - #panel-overview .dashboard-overview-grid { grid-template-columns: minmax(0, 1fr); } - #panel-overview .dashboard-overview-grid > .stat { grid-column: 1; } + #panel-overview .dashboard-overview-grid { + grid-template-columns: minmax(0, 1fr); + } + + #panel-overview .dashboard-overview-grid>.stat { + grid-column: 1; + } } -.overview-layout-tile.is-panel { grid-column: span 2; } + +.overview-layout-tile.is-panel { + grid-column: span 2; +} + .overview-layout-grid .overview-layout-panel-preview { display: grid; gap: 7px; @@ -10412,17 +10619,45 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { background: var(--panel); pointer-events: none; } -.overview-layout-panel-preview > strong { font-size: 13px; } -.overview-layout-panel-preview svg { width: 100%; height: 80px; } -.overview-layout-panel-preview small { color: var(--muted); } -.overview-layout-footer #overview-layout-default { margin-right: auto; } -.overview-reset-confirm { width: min(460px, calc(100vw - 24px)); padding: 16px; } -.overview-reset-confirm > p { line-height: 1.5; } -@media (max-width: 850px) { .overview-layout-tile.is-panel { grid-column: span 2; } } -@media (max-width: 550px) { .overview-layout-tile.is-panel { grid-column: span 1; } } -/* Overview block kinds come from the same server-validated registry as the - saved order. Labels distinguish numbers, plots, tables and diagnostic panels. */ +.overview-layout-panel-preview>strong { + font-size: 13px; +} + +.overview-layout-panel-preview svg { + width: 100%; + height: 80px; +} + +.overview-layout-panel-preview small { + color: var(--muted); +} + +.overview-layout-footer #overview-layout-default { + margin-right: auto; +} + +.overview-reset-confirm { + width: min(460px, calc(100vw - 24px)); + padding: 16px; +} + +.overview-reset-confirm>p { + line-height: 1.5; +} + +@media (max-width: 850px) { + .overview-layout-tile.is-panel { + grid-column: span 2; + } +} + +@media (max-width: 550px) { + .overview-layout-tile.is-panel { + grid-column: span 1; + } +} + .overview-widget-type { display: inline-block; width: fit-content; @@ -10436,31 +10671,70 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { line-height: 1.25; overflow-wrap: anywhere; } -.dashboard-widget-setting .dashboard-widget-toggle { flex-wrap: wrap; } -.dashboard-widget-setting .overview-widget-type { margin-left: 15px; } -.overview-layout-tile-toolbar .overview-widget-type { margin-left: 3px; } -.overview-layout-hidden-item .overview-widget-type { flex: 0 0 auto; } -.overview-layout-filters { display: flex; flex-wrap: wrap; gap: 8px; } -.overview-layout-filters select, .overview-layout-filters input { width: min(220px, 100%); max-width: 100%; } -/* Inline editing of the actual Overview blocks; live charts and original DOM - IDs remain in place. Dragging is enabled only on the explicit grip button. */ -.overview-inline-actions { display: inline-flex; gap: 7px; align-items: center; flex-wrap: wrap; } -.overview-inline-actions[hidden], .overview-inline-hint[hidden] { display: none !important; } -.overview-inline-hint { margin: 0 0 10px; } -#panel-overview .dashboard-overview-grid.is-unlocked > [data-dashboard-widget] { +.dashboard-widget-setting .dashboard-widget-toggle { + flex-wrap: wrap; +} + +.dashboard-widget-setting .overview-widget-type { + margin-left: 15px; +} + +.overview-layout-tile-toolbar .overview-widget-type { + margin-left: 3px; +} + +.overview-layout-hidden-item .overview-widget-type { + flex: 0 0 auto; +} + +.overview-layout-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.overview-layout-filters select, +.overview-layout-filters input { + width: min(220px, 100%); + max-width: 100%; +} + +.overview-inline-actions { + display: inline-flex; + gap: 7px; + align-items: center; + flex-wrap: wrap; +} + +.overview-inline-actions[hidden], +.overview-inline-hint[hidden] { + display: none !important; +} + +.overview-inline-hint { + margin: 0 0 10px; +} + +#panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] { position: relative; outline: 1px dashed var(--accent); outline-offset: -2px; padding-top: 66px; } -#panel-overview .dashboard-overview-grid.is-unlocked > .is-overview-dragging { opacity: .45; } -#panel-overview .dashboard-overview-grid.is-unlocked > .is-overview-drop-before { + +#panel-overview .dashboard-overview-grid.is-unlocked>.is-overview-dragging { + opacity: .45; +} + +#panel-overview .dashboard-overview-grid.is-unlocked>.is-overview-drop-before { box-shadow: inset 4px 0 var(--accent); } -#panel-overview .dashboard-overview-grid.is-unlocked > .is-overview-drop-after { + +#panel-overview .dashboard-overview-grid.is-unlocked>.is-overview-drop-after { box-shadow: inset -4px 0 var(--accent); } + .overview-inline-toolbar { position: absolute; top: 3px; @@ -10473,37 +10747,56 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { gap: 3px; pointer-events: auto; } + #panel-overview .overview-inline-toolbar button { min-width: 28px; min-height: 27px; padding: 2px 7px; font-size: 12px; } -#panel-overview .overview-inline-toolbar .overview-inline-grip { cursor: grab; user-select: none; } -#panel-overview .overview-inline-toolbar .overview-inline-grip:active { cursor: grabbing; } -#panel-overview .overview-inline-toolbar .overview-widget-type { margin-left: auto; } -@media (max-width: 600px) { - .overview-layout-hidden-item { flex-wrap: wrap; } - .overview-layout-filters select, .overview-layout-filters input { width: 100%; } - #panel-overview .dashboard-overview-grid.is-unlocked > [data-dashboard-widget] { padding-top: 68px; } + +#panel-overview .overview-inline-toolbar .overview-inline-grip { + cursor: grab; + user-select: none; } +#panel-overview .overview-inline-toolbar .overview-inline-grip:active { + cursor: grabbing; +} + +#panel-overview .overview-inline-toolbar .overview-widget-type { + margin-left: auto; +} + +@media (max-width: 600px) { + .overview-layout-hidden-item { + flex-wrap: wrap; + } + + .overview-layout-filters select, + .overview-layout-filters input { + width: 100%; + } + + #panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] { + padding-top: 68px; + } +} -/* Direct Overview editing: hide is reversible until Save and remains - recoverable later from the block selector or the compact hidden shelf. */ #panel-overview .overview-inline-toolbar .overview-inline-hide { color: var(--red); border: 1px solid var(--danger-line); background: var(--panel); - /* Use the same .quiet radius/corner preset as the arrow buttons. */ font-size: 18px; line-height: 1; } + #panel-overview .overview-inline-toolbar .overview-inline-hide:hover, #panel-overview .overview-inline-toolbar .overview-inline-hide:focus-visible { border-color: var(--danger-line-strong); background: var(--notice-bg); } + .overview-inline-hidden { margin: 0 0 10px; border: 1px solid var(--line); @@ -10511,13 +10804,19 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { background: var(--panel); color: var(--text); } -.overview-inline-hidden > summary { + +.overview-inline-hidden>summary { padding: 9px 12px; cursor: pointer; color: var(--muted); font-size: 12px; } -.overview-inline-hidden > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } + +.overview-inline-hidden>summary:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + .overview-inline-hidden-list { display: flex; flex-wrap: wrap; @@ -10527,6 +10826,7 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { padding: 10px 12px; border-top: 1px solid var(--line); } + .overview-inline-hidden-item { display: inline-flex; align-items: center; @@ -10539,8 +10839,12 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { background: var(--panel2); font-size: 12px; } -.overview-inline-hidden-item > span:not(.overview-widget-type) { overflow-wrap: anywhere; } -.overview-inline-hidden-item > button { + +.overview-inline-hidden-item>span:not(.overview-widget-type) { + overflow-wrap: anywhere; +} + +.overview-inline-hidden-item>button { flex: 0 0 auto; min-width: 27px; min-height: 27px; @@ -10550,22 +10854,27 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { color: var(--green); background: var(--panel); } -.overview-inline-hidden-item > button:hover { border-color: var(--accent); } -@media (max-width: 600px) { - #panel-overview .dashboard-overview-grid.is-unlocked > [data-dashboard-widget] { padding-top: 82px; } - .overview-inline-hidden-item { max-width: 100%; } + +.overview-inline-hidden-item>button:hover { + border-color: var(--accent); +} + +@media (max-width: 600px) { + #panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] { + padding-top: 82px; + } + + .overview-inline-hidden-item { + max-width: 100%; + } +} + +#panel-overview { + container: overview-canvas / inline-size; } -/* The original fixed Overview grid, also used for personalized layouts. - Metrics, wide charts, diagnostic panels and tables are separate rows; - every tile in a row stretches to the SAME height. This intentionally - favors symmetry over masonry or floating short cards beside tall panels. - Existing density controls define the identical horizontal/vertical gap. */ -#panel-overview { container: overview-canvas / inline-size; } #panel-overview .dashboard-overview-grid.is-aligned { display: grid; - /* A compact gutter throughout: tiles, large panels, rows and columns. - Keep the density setting, but avoid the former larger tile-to-tile gap. */ --overview-gap: max(3px, calc(var(--ui-gap) - 1px)); margin: 0; row-gap: var(--overview-gap); @@ -10577,13 +10886,12 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { align-content: start; gap: var(--overview-gap); } -/* The earlier html[data-dashboard-density] .stats-grid rules have greater - specificity than .is-aligned and otherwise restore the old, larger gutter. - Keep the compact value in the combined Overview grid at every density. */ + #panel-overview .dashboard-overview-grid.is-aligned.stats-grid { gap: var(--overview-gap); } -#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget] { + +#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget] { grid-column: auto / span 2; grid-row: auto; align-self: stretch; @@ -10594,117 +10902,294 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { margin-block: 0; position: relative; } -#panel-overview .dashboard-overview-grid.is-aligned > .card:not(.pool-card) { grid-column: auto / span 4; } -#panel-overview .dashboard-overview-grid.is-aligned > .card.overview-wide-chart { grid-column: auto / span 6; } -#panel-overview .dashboard-overview-grid.is-aligned > .pool-card { grid-column: 1 / -1; } -/* Each size category starts at the first track, even after drag/drop. */ -#panel-overview .dashboard-overview-grid.is-aligned > .overview-row-start, -#panel-overview .dashboard-overview-grid.is-aligned > .card.overview-row-start { grid-column-start: 1; } + +#panel-overview .dashboard-overview-grid.is-aligned>.card:not(.pool-card) { + grid-column: auto / span 4; +} + +#panel-overview .dashboard-overview-grid.is-aligned>.card.overview-wide-chart { + grid-column: auto / span 6; +} + +#panel-overview .dashboard-overview-grid.is-aligned>.pool-card { + grid-column: 1 / -1; +} + +#panel-overview .dashboard-overview-grid.is-aligned>.overview-row-start, +#panel-overview .dashboard-overview-grid.is-aligned>.card.overview-row-start { + grid-column-start: 1; +} + @container overview-canvas (max-width: 1080px) { - #panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: repeat(6, minmax(0, 1fr)); } - #panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: auto / span 2; } - #panel-overview .dashboard-overview-grid.is-aligned > .card:not(.pool-card) { grid-column: auto / span 3; } - #panel-overview .dashboard-overview-grid.is-aligned > .card.overview-wide-chart, - #panel-overview .dashboard-overview-grid.is-aligned > .pool-card { grid-column: 1 / -1; } + #panel-overview .dashboard-overview-grid.is-aligned { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + #panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget].stat { + grid-column: auto / span 2; + } + + #panel-overview .dashboard-overview-grid.is-aligned>.card:not(.pool-card) { + grid-column: auto / span 3; + } + + #panel-overview .dashboard-overview-grid.is-aligned>.card.overview-wide-chart, + #panel-overview .dashboard-overview-grid.is-aligned>.pool-card { + grid-column: 1 / -1; + } } + @container overview-canvas (max-width: 700px) { - #panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: repeat(2, minmax(0, 1fr)); } - #panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: auto / span 1; } - #panel-overview .dashboard-overview-grid.is-aligned > .card:not(.pool-card) { grid-column: 1 / -1; } + #panel-overview .dashboard-overview-grid.is-aligned { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + #panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget].stat { + grid-column: auto / span 1; + } + + #panel-overview .dashboard-overview-grid.is-aligned>.card:not(.pool-card) { + grid-column: 1 / -1; + } } + @container overview-canvas (max-width: 440px) { - #panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: minmax(0, 1fr); } - #panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: 1; } + #panel-overview .dashboard-overview-grid.is-aligned { + grid-template-columns: minmax(0, 1fr); + } + + #panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget].stat { + grid-column: 1; + } } -/* Keep category boundaries intact at every responsive breakpoint. */ -#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].overview-row-start { grid-column-start: 1; } -/* Full-size optional charts: static SVG geometry and bounded historical data. */ -#panel-overview .overview-wide-chart { padding: 15px; overflow: hidden; } -#panel-overview .overview-wide-chart .card-heading { min-width: 0; align-items: baseline; gap: 10px; } -#panel-overview .overview-wide-chart h2 { min-width: 0; overflow-wrap: anywhere; } -#panel-overview .overview-wide-summary { color: var(--accent); font: 500 12px var(--mono); white-space: nowrap; } -#panel-overview .overview-wide-svg { display: block; width: 100%; height: clamp(125px, 13vw, 190px); margin-block: 13px 9px; } -#panel-overview .overview-wide-svg .chart-grid { stroke: var(--line); fill: none; stroke-width: 1; opacity: .6; } -#panel-overview .overview-wide-series { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; } -#panel-overview .overview-wide-series-0 { stroke: var(--chart-accent); } -#panel-overview .overview-wide-series-1 { stroke: var(--chart-secondary); } -#panel-overview .overview-wide-series-2 { stroke: var(--chart-tertiary); } -#panel-overview .overview-wide-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; color: var(--muted); font-size: 11px; } -#panel-overview .overview-wide-legend > span { display: inline-flex; align-items: center; gap: 6px; min-width: 0; } -#panel-overview .overview-wide-legend i { width: 8px; height: 8px; border-radius: 100%; background: var(--chart-accent); } -#panel-overview .overview-wide-legend .overview-wide-key-1 { background: var(--chart-secondary); } -#panel-overview .overview-wide-legend .overview-wide-key-2 { background: var(--chart-tertiary); } -#panel-overview .overview-wide-legend b { font: 500 11px var(--mono); color: var(--text); } -/* Block metadata stays to the left of every action group. */ -.dashboard-widget-setting .dashboard-widget-toggle { flex-wrap: nowrap; } -.dashboard-widget-setting .dashboard-widget-toggle .overview-widget-type { margin: 0 0 0 auto; flex: 0 0 auto; } -.dashboard-widget-setting .dashboard-widget-setting-actions { justify-self: end; flex-shrink: 0; } -.overview-layout-tile-toolbar { flex-wrap: wrap; } -.overview-layout-tile-toolbar .overview-widget-type { margin: 0; } -.overview-layout-tile-toolbar .overview-layout-controls { display: inline-flex; align-items: center; gap: 3px; margin-left: auto; } -.overview-layout-hidden-item > button { margin-left: auto; } -.overview-layout-hidden-item > .overview-widget-type { margin: 0; } -#panel-overview .overview-inline-toolbar .overview-widget-type { margin: 0 auto 0 0; } -#panel-overview .overview-inline-toolbar .overview-inline-hide { margin-left: 0; } -.overview-inline-hidden-item > .overview-widget-type { margin-right: 0; } -.overview-inline-hidden-item > .overview-inline-restore { order: 3; margin-left: auto; } +#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget].overview-row-start { + grid-column-start: 1; +} + +#panel-overview .overview-wide-chart { + padding: 15px; + overflow: hidden; +} + +#panel-overview .overview-wide-chart .card-heading { + min-width: 0; + align-items: baseline; + gap: 10px; +} + +#panel-overview .overview-wide-chart h2 { + min-width: 0; + overflow-wrap: anywhere; +} + +#panel-overview .overview-wide-summary { + color: var(--accent); + font: 500 12px var(--mono); + white-space: nowrap; +} + +#panel-overview .overview-wide-svg { + display: block; + width: 100%; + height: clamp(125px, 13vw, 190px); + margin-block: 13px 9px; +} + +#panel-overview .overview-wide-svg .chart-grid { + stroke: var(--line); + fill: none; + stroke-width: 1; + opacity: .6; +} + +#panel-overview .overview-wide-series { + fill: none; + stroke-width: 2; + stroke-linejoin: round; + stroke-linecap: round; +} + +#panel-overview .overview-wide-series-0 { + stroke: var(--chart-accent); +} + +#panel-overview .overview-wide-series-1 { + stroke: var(--chart-secondary); +} + +#panel-overview .overview-wide-series-2 { + stroke: var(--chart-tertiary); +} + +#panel-overview .overview-wide-legend { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 16px; + color: var(--muted); + font-size: 11px; +} + +#panel-overview .overview-wide-legend>span { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +#panel-overview .overview-wide-legend i { + width: 8px; + height: 8px; + border-radius: 100%; + background: var(--chart-accent); +} + +#panel-overview .overview-wide-legend .overview-wide-key-1 { + background: var(--chart-secondary); +} + +#panel-overview .overview-wide-legend .overview-wide-key-2 { + background: var(--chart-tertiary); +} + +#panel-overview .overview-wide-legend b { + font: 500 11px var(--mono); + color: var(--text); +} + +.dashboard-widget-setting .dashboard-widget-toggle { + flex-wrap: nowrap; +} + +.dashboard-widget-setting .dashboard-widget-toggle .overview-widget-type { + margin: 0 0 0 auto; + flex: 0 0 auto; +} + +.dashboard-widget-setting .dashboard-widget-setting-actions { + justify-self: end; + flex-shrink: 0; +} + +.overview-layout-tile-toolbar { + flex-wrap: wrap; +} + +.overview-layout-tile-toolbar .overview-widget-type { + margin: 0; +} + +.overview-layout-tile-toolbar .overview-layout-controls { + display: inline-flex; + align-items: center; + gap: 3px; + margin-left: auto; +} + +.overview-layout-hidden-item>button { + margin-left: auto; +} + +.overview-layout-hidden-item>.overview-widget-type { + margin: 0; +} + +#panel-overview .overview-inline-toolbar .overview-widget-type { + margin: 0 auto 0 0; +} + +#panel-overview .overview-inline-toolbar .overview-inline-hide { + margin-left: 0; +} + +.overview-inline-hidden-item>.overview-widget-type { + margin-right: 0; +} + +.overview-inline-hidden-item>.overview-inline-restore { + order: 3; + margin-left: auto; +} -/* Appearance selector: always show the block-kind label, independently from - the visibility button, with actions pinned to the right edge. */ .dashboard-widget-list { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); } + .dashboard-widget-setting { grid-template-columns: minmax(0, 1fr) auto; gap: 6px; } + .dashboard-widget-setting .dashboard-widget-toggle { width: 100%; overflow: hidden; min-width: 0; flex-wrap: nowrap; } + .dashboard-widget-setting .dashboard-widget-setting-label { flex: 1 1 auto; min-width: 0; } -.dashboard-widget-setting > .overview-widget-type { + +.dashboard-widget-setting>.overview-widget-type { flex: none; margin: 0; white-space: nowrap; overflow-wrap: normal; } -.dashboard-widget-setting > .dashboard-widget-setting-actions { + +.dashboard-widget-setting>.dashboard-widget-setting-actions { justify-self: end; white-space: nowrap; } -/* Global shape preference. "default" applies NO overrides so existing dark, - light, cream and Ultra Dark keep the same established corner geometry. - Circular status dots, spinners, switches and chart markers retain their - semantic shape; underline tabs and navigation accent strips stay intact. */ -html[data-corner-style="square"] { --rp-radius-surface: 0px; --rp-radius-control: 0px; } -html[data-corner-style="subtle"] { --rp-radius-surface: 5px; --rp-radius-control: 4px; } -html[data-corner-style="rounded"] { --rp-radius-surface: 12px; --rp-radius-control: 8px; } -html[data-corner-style="extra"] { --rp-radius-surface: 18px; --rp-radius-control: 12px; } -html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is( - .card, .stat, dialog, .login-card, .appearance-section, .settings-tabs, +html[data-corner-style="square"] { + --rp-radius-surface: 0px; + --rp-radius-control: 0px; +} + +html[data-corner-style="subtle"] { + --rp-radius-surface: 5px; + --rp-radius-control: 4px; +} + +html[data-corner-style="rounded"] { + --rp-radius-surface: 12px; + --rp-radius-control: 8px; +} + +html[data-corner-style="extra"] { + --rp-radius-surface: 18px; + --rp-radius-control: 12px; +} + +html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(.card, .stat, dialog, .login-card, .appearance-section, .settings-tabs, .dashboard-widget-setting, .overview-layout-tile, .overview-layout-hidden-item, .overview-inline-hidden-item, .notice, .toast, .chart-style-choice, .stream-dashboard-card, .pool-card, - .file-log-advanced-parser, .table-container, .metric-info-popover -) { border-radius: var(--rp-radius-surface) !important; } -html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is( - button:not(.nav):not(.file-log-tab), input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), + .file-log-advanced-parser, .table-container, .metric-info-popover) { + border-radius: var(--rp-radius-surface) !important; +} + +html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(button:not(.nav):not(.file-log-tab), input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, textarea, .pill, .overview-widget-type, .badge, .quiet, .outline, .primary, .code, .command, .dashboard-widget-toggle, .settings-tabs button, - .overview-inline-hidden-toggle, .overview-inline-restore -) { border-radius: var(--rp-radius-control) !important; } + .overview-inline-hidden-toggle, .overview-inline-restore) { + border-radius: var(--rp-radius-control) !important; +} -/* Appearance stays badge-free; badges remain useful in the visual editor. */ -#settings-page-appearance .dashboard-widget-setting .overview-widget-type { display: none; } +#settings-page-appearance .dashboard-widget-setting .overview-widget-type { + display: none; +} -/* ACL DDNS input: keep the rule type separate from its hostname. */ -.acl-profile-rule { grid-template-columns: minmax(100px, .6fr) minmax(105px, .6fr) minmax(175px, 1.5fr) auto; } -@media (max-width: 640px) { .acl-profile-rule { grid-template-columns: 1fr; } } +.acl-profile-rule { + grid-template-columns: minmax(100px, .6fr) minmax(105px, .6fr) minmax(175px, 1.5fr) auto; +} + +@media (max-width: 640px) { + .acl-profile-rule { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/config/compile.rs b/src/config/compile.rs index 82b7eb6..b3f5165 100644 --- a/src/config/compile.rs +++ b/src/config/compile.rs @@ -48,58 +48,29 @@ pub fn compile_with_active( config.compression.validate()?; config.server_header.validate()?; let forwarding = config.forwarding.compile()?; - ensure!( - config.php_fpm_profiles.len() <= 128, - "too many PHP-FPM profiles (max 128)" - ); + ensure!(config.php_fpm_profiles.len() <= 128, "too many PHP-FPM profiles (max 128)"); let mut php_profiles = HashMap::new(); for profile in &mut config.php_fpm_profiles { - profile - .validate() - .with_context(|| format!("PHP-FPM profile {}", profile.id))?; - ensure!( - php_profiles - .insert(profile.id.clone(), profile.php.clone()) - .is_none(), - "duplicate PHP-FPM profile id" - ); + profile.validate().with_context(|| format!("PHP-FPM profile {}", profile.id))?; + ensure!(php_profiles.insert(profile.id.clone(), profile.php.clone()).is_none(), + "duplicate PHP-FPM profile id"); } - ensure!( - config.web_services.len() <= 128, - "too many Web Services (max 128)" - ); + ensure!(config.web_services.len() <= 128, "too many Web Services (max 128)"); let mut web_services = HashMap::new(); // PHP-FPM can retain FastCGI PHP_VALUE/PHP_ADMIN_VALUE directives in reused // workers. Different directive sets on one socket can leak to the next vhost. // Report unsafe legacy configurations but keep them loadable for migration. - let mut php_endpoint_policies: HashMap< - String, - ( - String, - std::collections::BTreeMap, - std::collections::BTreeMap, - ), - > = HashMap::new(); + let mut php_endpoint_policies: HashMap, std::collections::BTreeMap)> = HashMap::new(); for service in &mut config.web_services { - service - .validate() - .with_context(|| format!("Web Service {}", service.id))?; + service.validate().with_context(|| format!("Web Service {}", service.id))?; // A profile changes effective runtime settings, not the saved standalone // PHP configuration. Detaching a profile restores local settings exactly. let mut effective = service.clone(); if let Some(id) = &service.php_profile_id { - effective.php = php_profiles - .get(id) - .with_context(|| { - format!( - "Web Service {} references unknown PHP-FPM profile {}", - service.id, id - ) - })? - .clone(); - effective - .validate() - .with_context(|| format!("Web Service {} PHP-FPM profile {}", service.id, id))?; + effective.php = php_profiles.get(id).with_context(|| { + format!("Web Service {} references unknown PHP-FPM profile {}", service.id, id) + })?.clone(); + effective.validate().with_context(|| format!("Web Service {} PHP-FPM profile {}", service.id, id))?; } if effective.php.enabled { let php = &effective.php; @@ -111,25 +82,13 @@ pub fn compile_with_active( ); } } else { - php_endpoint_policies.insert( - php.endpoint.clone(), - ( - effective.id.clone(), - php.php_values.clone(), - php.php_admin_values.clone(), - ), - ); + php_endpoint_policies.insert(php.endpoint.clone(), ( + effective.id.clone(), php.php_values.clone(), php.php_admin_values.clone(), + )); } } - let compiled = Arc::new( - effective - .compile() - .with_context(|| format!("Web Service {}", service.id))?, - ); - ensure!( - web_services.insert(service.id.clone(), compiled).is_none(), - "duplicate Web Service id" - ); + let compiled = Arc::new(effective.compile().with_context(|| format!("Web Service {}", service.id))?); + ensure!(web_services.insert(service.id.clone(), compiled).is_none(), "duplicate Web Service id"); } ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)"); let mut vhost_ids = HashSet::new(); @@ -436,9 +395,7 @@ fn validate_route_rewrite( Ok(()) } -fn validate_location_redirect( - redirect: Option<&crate::vhost::LocationRedirect>, -) -> Result> { +fn validate_location_redirect(redirect: Option<&crate::vhost::LocationRedirect>) -> Result> { let Some(redirect) = redirect else { return Ok(None); }; @@ -454,50 +411,24 @@ fn validate_location_redirect( ); // Replace capture references with a harmless segment so URL structure can // be validated before requests reach the data plane. - let authority_end = redirect - .target - .find("://") - .and_then(|start| { - let rest = &redirect.target[start + 3..]; - rest.char_indices() - .find(|(_, ch)| matches!(ch, '/' | '?' | '#')) - .map(|(offset, _)| start + 3 + offset) - .or(Some(redirect.target.len())) - }) - .ok_or_else(|| anyhow::anyhow!("location redirect target must be an absolute URL"))?; - ensure!( - !redirect.target[..authority_end].contains('$'), - "location redirect captures are not allowed in the URL authority" - ); + let authority_end = redirect.target.find("://").and_then(|start| { + let rest = &redirect.target[start + 3..]; + rest.char_indices().find(|(_, ch)| matches!(ch, '/' | '?' | '#')).map(|(offset, _)| start + 3 + offset).or(Some(redirect.target.len())) + }).ok_or_else(|| anyhow::anyhow!("location redirect target must be an absolute URL"))?; + ensure!(!redirect.target[..authority_end].contains('$'), "location redirect captures are not allowed in the URL authority"); let sample = regex::Regex::new(r"\$\{?[A-Za-z0-9_]+\}?") .expect("static capture regex") .replace_all(&redirect.target, "capture"); - let parsed = - url::Url::parse(&sample).context("location redirect target must be an absolute URL")?; - ensure!( - matches!(parsed.scheme(), "http" | "https") && parsed.host_str().is_some(), - "location redirect target must use http/https and include a host" - ); - ensure!( - parsed.username().is_empty() && parsed.password().is_none(), - "location redirect target cannot contain credentials" - ); + let parsed = url::Url::parse(&sample).context("location redirect target must be an absolute URL")?; + ensure!(matches!(parsed.scheme(), "http" | "https") && parsed.host_str().is_some(), "location redirect target must use http/https and include a host"); + ensure!(parsed.username().is_empty() && parsed.password().is_none(), "location redirect target cannot contain credentials"); let rewrite_regex = match redirect.rewrite_pattern.as_deref() { None | Some("") => None, Some(pattern) => { - ensure!( - pattern.len() <= 1024 && !pattern.chars().any(char::is_control), - "invalid location redirect rewrite regex" - ); + ensure!(pattern.len() <= 1024 && !pattern.chars().any(char::is_control), "invalid location redirect rewrite regex"); let mut builder = regex::RegexBuilder::new(pattern); - builder - .size_limit(2 * 1024 * 1024) - .dfa_size_limit(2 * 1024 * 1024); - Some( - builder - .build() - .context("invalid location redirect rewrite regex")?, - ) + builder.size_limit(2 * 1024 * 1024).dfa_size_limit(2 * 1024 * 1024); + Some(builder.build().context("invalid location redirect rewrite regex")?) } }; Ok(Some(crate::config::CompiledLocationRedirect { diff --git a/src/main/admin_runtime.rs b/src/main/admin_runtime.rs index ea3d21a..a22a395 100644 --- a/src/main/admin_runtime.rs +++ b/src/main/admin_runtime.rs @@ -146,10 +146,7 @@ async fn run( } let renewal_observations = store.load_certificate_renewals().await?; let geoip_format = snapshot.envelope.config.geoip_updates.format; - let geoip = match rust_proxy::geoip::Database::load_format( - &data_dir.join("geoip"), - geoip_format, - ) { + let geoip = match rust_proxy::geoip::Database::load_format(&data_dir.join("geoip"), geoip_format) { Ok(database) => std::sync::Arc::new(database), Err(error) => { tracing::warn!(%error, "GeoIP country database unavailable; country allow lists fail closed"); @@ -436,10 +433,7 @@ async fn run( grace }; if managed_update { - tracing::info!( - budget_seconds = cache_flush_budget.as_secs(), - "managed-update hot-to-cold cache handoff started" - ); + tracing::info!(budget_seconds = cache_flush_budget.as_secs(), "managed-update hot-to-cold cache handoff started"); } let flush_result = if managed_update { tokio::time::timeout( @@ -451,9 +445,10 @@ async fn run( tokio::time::timeout(cache_flush_budget, state.cache.flush_hot_to_cold()).await }; match flush_result { - Ok(moved) if managed_update => { - tracing::info!(moved, "managed-update hot-to-cold cache handoff completed") - } + Ok(moved) if managed_update => tracing::info!( + moved, + "managed-update hot-to-cold cache handoff completed" + ), Ok(moved) if moved > 0 => tracing::info!( moved, "flushed hot cache objects to cold storage before shutdown" diff --git a/tools/checks/dynamic_acl_0325.py b/tools/checks/dynamic_acl_0325.py old mode 100644 new mode 100755 index d998c7b..6f8d923 --- a/tools/checks/dynamic_acl_0325.py +++ b/tools/checks/dynamic_acl_0325.py @@ -18,6 +18,7 @@ checks = { ]), 'bounded asynchronous DNS work': all(x in dns for x in ['REFRESH_SECONDS: u64 = 60', 'MAX_AGE_SECONDS: u64 = 180', 'MAX_NAMES: usize = 128', 'MAX_ADDRESSES: usize = 32', 'PARALLEL_LOOKUPS: usize = 8', 'LOOKUP_SECONDS: u64 = 4', 'buffer_unordered(PARALLEL_LOOKUPS)']), 'lookup on refresh, not on request': 'lookup_host' in dns and 'lookup_host' not in acl and 'dynamic::matches' in acl, + 'resolver owns the cache key independently of the DNS future (E0505)': 'let lookup_name = name.clone();' in dns and 'lookup_host((lookup_name.as_str(), 0u16))' in dns and 'lookup_host((name.as_str(), 0u16))' not in dns, 'fail closed on empty or failed resolution': 'CACHE.store(Arc::new(results))' in dns and 'return (name, None);' in dns and 'entry.valid_until > Instant::now()' in dns, 'update after configuration commit': all('crate::acl::dynamic::config_changed()' in read(p) for p in ['src/runtime/updates.rs', 'src/runtime/docker.rs', 'src/runtime/certificates.rs']), 'startup background refresh': 'state.start_dns_acl_updates();' in read('src/main/admin_runtime.rs'), diff --git a/tools/checks/overview_gutters_0324_browser.py b/tools/checks/overview_gutters_0324_browser.py old mode 100644 new mode 100755 diff --git a/tools/checks/preserve_host_0324.py b/tools/checks/preserve_host_0324.py old mode 100644 new mode 100755