diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4047a..ff9ee4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## 0.32.5 +- Fix the administration panel startup `ReferenceError: Cannot access 'i18n' before initialization` in the new DNS/DDNS ACL editor. Define its localization helper before creating the DNS hint; preserve all ACL settings and translations. Add a JavaScript startup regression test to the release checks. - 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. diff --git a/README.md b/README.md index a770cca..7f299ad 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos ## What changed in 0.32.5 +- **ACL editor startup hotfix:** fix the JavaScript `i18n` initialization order + that could prevent the entire administration UI from loading. This does not + change stored configuration or DNS ACL behavior. - **Dynamic DNS/DDNS ACL allow rules:** reusable ACL profiles, host policies, inline vhost/location ACLs, and Docker ACL assignments now accept `allow office.example.ddns.net` in place of a static client IP/CIDR. Existing IP, diff --git a/SHA256SUMS b/SHA256SUMS index a707483..842b779 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -2,17 +2,17 @@ 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore ad0382153576411fb19861072635c02bdf33de740d6f122d8f27bd9c52ce112a AGENTS.md -d304d9da32d28b866176383b3b274e6ab020a54316d7e8ffe85cecd08e150f93 CHANGELOG.md +4a58695f0ef64344228cb43caf2a78aeba724cf0cc573a6a8745da847681aeed CHANGELOG.md 2f1de87bae58761fc8690ec7225520e3a16213c77cbbb5981e34f12a95810668 Cargo.lock 8c76ffaf4adba93df26e30f3dd163e0e5e201d069d1ae69c071ca42d7fc7264e Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile -31e13d43d5432731ca7f1bfd29d73eb2b62b8d23fe8678205f712f0daf9908c6 README.md +4ee3c626d1e5da5079fe65c6343f085cbb8bb53c2f2fa97e354f9633e8d415b3 README.md fc03a8d19055642313f0b34bcd22e1cce17efc4d91be7fe7e4819e33b91c531d ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md 4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service -3e709b09ddf4e2c17497c66b416d07e1423eeeb5f29a9100b06c8d3d5cacc36a dev.sh +4d9baf35c70d9f5cfb10f8bd4caab543772fdda1e979ff74c17f1e3ebc8e3cd0 dev.sh 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example @@ -63,7 +63,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY. 007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md 25a06e7ddbb95d286e849b2fc5ba4adeedbddba0bad948e55bd91d962fd606de docs/SETTINGS.md 8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md -9cf4c4e27ba11f40143f5a158f91d72b6c88a0a612a448390428d7103bbedfb3 docs/TEST_REPORT.md +97299efb6288ee29493eb7df243055d1ff76e8d0faf04bd195e184bce2e8bb67 docs/TEST_REPORT.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md @@ -217,7 +217,7 @@ c24411dd62afaecfc631682554d4a037c7dd27100d817e9901ccdfd505c2ed5b src/admin/web/ 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js -0d0ec350d713aaf32a33607b2ddf0a8bb1858f2fd31c8396042cec4c84aee6ef src/admin/web/management.js +097fdb41aa1cd526c4d9bc65e502b05a62e664ef9fff8e69f706b93b0b124dc2 src/admin/web/management.js 90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js 8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html @@ -513,6 +513,7 @@ f6a6d12275bc8ba627b4616a546d075511094bb064cd9058231cc86ca489743e tools/checks/d 38e35bed4faf984932b39bb5a00819fadc664387e8050a803ff4b5348b31369f tools/checks/docker_startup_0318_ui_test.mjs 3396e5a510787e70e2ad8d1fad90356af461a88fd84429525d56fa82113da0f3 tools/checks/docs_check.py 3fc765a7c2723ec783f269cfebc29ad2c09a90f33ea4af58285f883a026c4fa2 tools/checks/dynamic_acl_0325.py +9c77764561b6ebfeecbc228f43ea29a0dd9f44121ad773c2e4299b92758e45c2 tools/checks/dynamic_acl_startup_0325_ui_test.mjs 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 diff --git a/dev.sh b/dev.sh index ca3cd41..2c1c6cc 100755 --- a/dev.sh +++ b/dev.sh @@ -120,6 +120,7 @@ if [[ "$mode" == "test" ]]; then node tools/checks/stream_0320_ui_test.mjs node tools/checks/stream_0320_visibility_regression.mjs node tools/checks/overview_registry_0323_ui_test.mjs + node tools/checks/dynamic_acl_startup_0325_ui_test.mjs node tools/checks/preserve_host_0324_ui_test.mjs node tools/checks/backend_dashboard_0323_ui_test.mjs node tools/checks/dashboard_layout_editor_0323_ui_test.mjs diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index e69de29..0fc6ae8 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -0,0 +1,101 @@ +# Verification report - RustProxy 0.32.5 + +## Verification performed for this source package + +- **0.32.5 administration UI startup fix:** the DNS ACL translation helper in + `src/admin/web/management.js` is now initialized before the DDNS hint uses + it. `tools/checks/dynamic_acl_startup_0325_ui_test.mjs` executes the real ACL + editor initializer with a simulated DOM and verifies that it completes and + renders the translatable DNS hint. The test reproduced the user-reported + `ReferenceError` against the unmodified 0.32.5 source before the fix. + **36/36 JavaScript checks passed** after adding this test to `dev.sh`. + The assembled JavaScript bundle passed `node --check`. The source/version, + admin surface, security, docs and language checks passed. `release_smoke.py` + exceeded the available execution timeout; the Rust compiler and live server + remain unavailable, so full release/runtime acceptance is not claimed. +- **0.32.5 build correction:** fix reported Rust `E0505` in `src/acl/dynamic.rs` by cloning the hostname into `lookup_name` for `lookup_host`. The original `name` is returned as the cache key. Added a source-contract regression check; Rust compilation is still required to confirm the correction. +- `tools/checks/dynamic_acl_0325.py`: **15/15 static source contracts passed**. +- JavaScript dashboard/settings regression scripts: **36/36 passed** (35 existing and one new startup regression). +- `tools/checks/check_source.py`, `rust_module_contracts.py`, + `docs_check.py`, `langs_check.py`, `security_contracts.py`, and + `admin_surface_audit_0310.py`: passed. +- The full `./dev.sh test` was interrupted by the environment time limit while + running expensive pre-Cargo release simulations. It was **not** a full pass. +- Cargo/rustc are unavailable here. Dynamic DNS Rust unit tests, integration + tests, real resolver timeouts/rotation and production acceptance **not run**. + +## 0.32.5 dynamic DNS ACL changes + +This build extends existing ACL `Rule.network` entries with allow-only DNS/DDNS +hostname support. Resolution is background-only and bounded (60s cadence, +4s timeout, max 8 concurrent lookups, max 128 unique names / 32 IPs per name). +Initial, expired and failed resolution states deny dynamic host authorization. +Existing numeric IP/CIDR and explicit-deny precedence remain unchanged. + +A Rust compiler and real DNS peer were not available in this environment. +The Rust tests in `src/acl/tests.rs` and `src/acl/dynamic.rs` **have not been +executed** here. Before rollout, run `cargo fmt --all -- --check`, +`cargo check --locked --all-targets` and `cargo test --locked`, then confirm +live DDNS rotation and fail-closed DNS outages on staging. + +## Previous 0.32.4 scope + +Review of the supplied RustProxy 0.32.3 source and patch to 0.32.4: +HTTP Host / HTTP/2 authority parity, explicit TCP/DNS destination and TLS/SNI +isolation, relaxed HTTPS-by-IP troubleshooting hint, and uniform Overview gutters. +Follow-up patch to the first 0.32.4 archive: +- Fix reported `E0382` in `src/proxy/types.rs`: `Request::builder().uri(uri)` + moved `uri` before `upstream_request_target(&uri, ...)` could borrow it. + `.uri(uri.clone())` preserves the original URI for authority selection. +- Make normal Overview gutters uniformly 4px (dense 3px, comfortable 12px). + Resolve the higher-specificity `html[data-dashboard-density] .stats-grid` + rule which otherwise overrode the aligned grid and kept the larger gaps. +Existing database/configuration schemas and retry/transport policies are +unchanged. Only compiling Rust and live protocol tests prove runtime behavior. + +## Available checks + +- `./dev.sh test`: **all pre-Cargo checks passed**, including source/security, + SQLite migration/upgrade/backup simulations, memory-model scripts and **35 + Node.js UI/behavior test scripts**. The command then exited **127** because + Rust/Cargo is not installed; this is **not** a passing Rust test run. +- New `preserve_host_0324.py` and `preserve_host_0324_ui_test.mjs` passed: + authority/destination split source contracts, four protocol selectors, + verified/unencrypted origin warnings, explicit diagnostics preset. +- Expanded Chromium CSS fixture `tools/checks/overview_gutters_0324_browser.py`: + **27 checks passed** (3 container widths x 3 UI density modes x 3 Dashboard + densities), checking horizontal/vertical gutters, equally high rows and + numeric-to-large-card transition. Normal/dense/comfortable gaps measured + **4/3/12px** and the outer grid bottom margin remained `0px`. + The older `overview_grid_spacing_0323_browser.py` also passed with all + 62 blocks, responsive breakpoints and browser zoom 90%-130%. + These are simulated layout tests, not a live administrator session. +- `preserve_host_0324.py` now guards against moving `uri` before its final + authority adjustment, in addition to the backend/transport source contracts. + The existing Rust unit test for retry authority remains in the source. +- Version consistency: Cargo manifest, package root in existing lockfile, + OpenAPI, README, release notes and static assertion fixtures updated to 0.32.4. + External Cargo dependency resolutions have **not** been regenerated. + +## Rust validation not performed (blocking for production) + +The user's Rust compiler reported `error[E0382]: borrow of moved value: uri` +from the first 0.32.4 archive. This has been corrected in source, but **the +corrected code has not been compiled here**: no Cargo or Rust compiler is +available in the source-editing environment. +Do not treat static tests or the simulated TLS-peer tests added to Rust source +as evidence that RustProxy builds or that an actual Angie backend accepts the +corrected requests. Run, on a Rust 1.99+ host: + +```sh +cargo fmt --all -- --check +cargo check --locked --all-targets +cargo test --locked +``` + +Then validate on staging with HTTPS-by-IP and `preserve_host: true` for +HTTP/1.1, Auto (H2 and H1 ALPN), compatible HTTP/2 and strict H2; check +`:authority` vs `Host`, SNI, certificate verification, WebSockets, H2C and +HTTP health probe `host` overrides. Inspect Angie response/error logs for +400 responses and verify backend connectivity through pool rotation/retry. +No claim of universal backend compatibility or measured throughput is made. diff --git a/src/admin/web/management.js b/src/admin/web/management.js index 8e4813f..bcf4fed 100644 --- a/src/admin/web/management.js +++ b/src/admin/web/management.js @@ -222,9 +222,9 @@ const addButton = $("acl-profile-rule-add"), importButton = $("acl-profile-import-rules"); const previewDefault = $("acl-profile-preview-default"), previewFlow = $("acl-profile-preview-flow"), previewNote = $("acl-profile-preview-note"); const countryHelp = $("acl-profile-country-help"), countryLabel = $("acl-profile-country-label"), ipHelp = $("acl-profile-ip-help"); + const i18n = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; }; const dnsHint = i18n("p", "aclDnsHelp", "fine"); ipHelp?.after(dnsHint); const modeInputs = [...form.querySelectorAll('input[name="acl-profile-mode"]')]; - const i18n = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; }; const selectedMode = () => modeInputs.find(node => node.checked)?.value || "allow_exceptions"; const setMode = value => { for (const node of modeInputs) node.checked = node.value === value; }; const guidedAction = mode => mode === "allow_exceptions" ? "allow" : "deny"; diff --git a/src/admin/web/style.css b/src/admin/web/style.css index 1af601e..ac2e468 100644 --- a/src/admin/web/style.css +++ b/src/admin/web/style.css @@ -132,6 +132,9 @@ 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; @@ -191,6 +194,8 @@ 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; @@ -248,10 +253,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); } @@ -9799,6 +9804,7 @@ 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; @@ -9850,6 +9856,10 @@ 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, @@ -9979,6 +9989,8 @@ 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; @@ -10070,6 +10082,7 @@ 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, @@ -10111,6 +10124,7 @@ 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; @@ -10127,6 +10141,7 @@ 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; @@ -10200,6 +10215,7 @@ 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; @@ -10289,7 +10305,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; @@ -10300,317 +10316,94 @@ 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 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[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; -} - +.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)); - } + .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-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; @@ -10619,45 +10412,17 @@ 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-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-widget-type { display: inline-block; width: fit-content; @@ -10671,70 +10436,31 @@ 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%; } -.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] { +/* 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] { 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; @@ -10747,56 +10473,37 @@ 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; -} - +#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; - } + .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); @@ -10804,19 +10511,13 @@ 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; @@ -10826,7 +10527,6 @@ 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; @@ -10839,12 +10539,8 @@ 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; @@ -10854,27 +10550,22 @@ 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); -} - +.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; + #panel-overview .dashboard-overview-grid.is-unlocked > [data-dashboard-widget] { padding-top: 82px; } + .overview-inline-hidden-item { max-width: 100%; } } +/* 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); @@ -10886,12 +10577,13 @@ 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; @@ -10902,294 +10594,117 @@ 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; -} - -#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; } +/* 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; } @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; } -#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; -} +/* 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; } +/* 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; } -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, +/* 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, .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; } -#settings-page-appearance .dashboard-widget-setting .overview-widget-type { - display: none; -} +/* Appearance stays badge-free; badges remain useful in the visual editor. */ +#settings-page-appearance .dashboard-widget-setting .overview-widget-type { display: none; } -.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 +/* 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; } } diff --git a/tools/checks/dynamic_acl_startup_0325_ui_test.mjs b/tools/checks/dynamic_acl_startup_0325_ui_test.mjs new file mode 100644 index 0000000..6fa3750 --- /dev/null +++ b/tools/checks/dynamic_acl_startup_0325_ui_test.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// Execute the real ACL editor initializer, not just parse it: an uninitialized +// i18n helper used to stop the entire fingerprinted dashboard bundle at load. +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; + +const source = readFileSync('src/admin/web/management.js', 'utf8'); +const start = source.lastIndexOf('(() => {'); +assert(start > 0 && source.slice(start).includes('window.rustProxyAclProfiles ='), 'ACL editor IIFE not found'); + +function element(tag = 'div') { + return { + tag, children: [], dataset: {}, value: '', checked: false, hidden: false, + classList: { add() {}, remove() {}, toggle() {} }, + append(...nodes) { this.children.push(...nodes); }, + after(...nodes) { this.insertedAfter = nodes; }, + replaceChildren(...nodes) { this.children = nodes; }, + addEventListener() {}, + reset() {}, + querySelector() { return element(); }, + querySelectorAll() { return []; }, + }; +} +const nodes = new Map(); +const get = id => { + if (!nodes.has(id)) nodes.set(id, element()); + return nodes.get(id); +}; +const modes = ['allow_exceptions', 'block_exceptions', 'custom'].map((value, i) => ({ value, checked: i === 0 })); +get('acl-profile-form').querySelectorAll = selector => selector === 'input[name="acl-profile-mode"]' ? modes : []; +const window = {}; +const context = { + $: get, window, envelope: null, + t: key => `localized:${key}`, + el: (tag, text, cls) => { + const node = element(tag); + if (text !== undefined) node.textContent = text; + if (cls) node.className = cls; + return node; + }, +}; +runInNewContext(source.slice(start), context, { filename: 'src/admin/web/management.js' }); +assert.equal(typeof window.rustProxyAclProfiles?.render, 'function', 'ACL panel initializer must finish'); +assert.equal(typeof window.rustProxyAclProfiles?.refreshTranslations, 'function'); +const hint = get('acl-profile-ip-help').insertedAfter?.[0]; +assert.equal(hint?.tag, 'p', 'DDNS hint must be rendered'); +assert.equal(hint?.dataset.i18n, 'aclDnsHelp', 'DDNS hint must remain translatable'); +assert.equal(hint?.textContent, 'localized:aclDnsHelp'); +window.rustProxyAclProfiles.reset(); +console.log('PASS: ACL/DDNS real initializer runs without i18n temporal dead zone');