V0.32.6 #1

Merged
gru merged 4 commits from v0.32.6 into master 2026-10-14 13:05:10 +02:00
16 changed files with 1796 additions and 1022 deletions
Showing only changes of commit d2c6cf59a9 - Show all commits

No files matched your search

+22 -12
View File
@@ -5,7 +5,26 @@
## 0.32.6 ## 0.32.6
- **Development-branch diagnostics charts (no version bump):** publish cached - **Development-branch Appearance layout:** replace the long
half-width Overview cards settings list with a full-width, compact horizontal
ribbon. Keep every visibility switch and reorder control operational, add
left/right browsing, and provide an optional expanded grid. Preserve stored
layout preferences, theme shapes, and the visual layout editor.
- **Development-branch dashboard polish:** keep the Cache
hits/misses/bypasses per-second chart visually continuous across browser-tab
minimize/restore by reusing the last valid rate sample until a fresh interval
is available again, rather than leaving a visible gap. Preserve the 3 s
Overview cadence and do not change server polling.
- **Development-branch mobile/layout polish:** make the
Overview page-intro action buttons equal-height and symmetric on mobile,
tighten the Appearance → Dashboard layout → Overview cards editor into a more
horizontal wrapped control strip, increase the gap between the `p` and the right border of the
RustProxy logo mark, and normalize the Overview canvas gutter so small-small,
small-large and large-large dashboard block transitions use the same spacing.
- **Development-branch diagnostics charts:** publish cached
CPU, process CPU/RSS and per-interface RX/TX samples in compact live status; CPU, process CPU/RSS and per-interface RX/TX samples in compact live status;
charts follow the Overview update cadence (default 3 s, accepting saved 2 s) charts follow the Overview update cadence (default 3 s, accepting saved 2 s)
while all Process details still follow the selected System-info refresh. The while all Process details still follow the selected System-info refresh. The
@@ -14,7 +33,7 @@
for reported WebKit jumps near Process/Network; physical iOS 27 validation is for reported WebKit jumps near Process/Network; physical iOS 27 validation is
still required. Match the Origin protocol badge's radius to neighboring still required. Match the Origin protocol badge's radius to neighboring
policy pills for both default and configured corner styles, preserving colors. policy pills for both default and configured corner styles, preserving colors.
- **Development-branch rate continuity (no version bump):** preserve cache - **Development-branch rate continuity:** preserve cache
HIT/MISS and other counter-derived chart histories when a browser tab is HIT/MISS and other counter-derived chart histories when a browser tab is
suspended, minimized or restored from BFCache. Treat resumed/overdue suspended, minimized or restored from BFCache. Treat resumed/overdue
observations as new counter baselines, not artificial 0/s samples. Show observations as new counter baselines, not artificial 0/s samples. Show
@@ -22,7 +41,7 @@
values. Apply one shared rule to Cache, Transform, Overview, backend, values. Apply one shared rule to Cache, Transform, Overview, backend,
vhost and Stream charts without adding polling or changing System-info vhost and Stream charts without adding polling or changing System-info
scheduling. Add isolated JavaScript pause/resume and reset tests. scheduling. Add isolated JavaScript pause/resume and reset tests.
- **Development-branch follow-up (no version bump):** update existing network-interface - **Development-branch follow-up:** update existing network-interface
diagnostic chart nodes in place during live refresh, preserving DOM identity diagnostic chart nodes in place during live refresh, preserving DOM identity
and avoiding scroll-anchor loss around the Process and Network panels on touch and avoiding scroll-anchor loss around the Process and Network panels on touch
browsers. Keep network-interface history advancing on every live Overview update browsers. Keep network-interface history advancing on every live Overview update
@@ -46,8 +65,6 @@
## 0.32.5 ## 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 - Add bounded DNS/DDNS hostnames as **allow-only** client ACL rules in reusable
profiles, host policies, vhosts, locations and Docker ACL assignments. profiles, host policies, vhosts, locations and Docker ACL assignments.
- Refresh A/AAAA answers asynchronously every 60 seconds and after config - Refresh A/AAAA answers asynchronously every 60 seconds and after config
@@ -60,13 +77,6 @@
## 0.32.4 ## 0.32.4
- Fix Rust `E0382` in retry construction: clone the upstream URI before
passing it into the request builder so authority normalization can borrow
the original URI afterward. Keep backend destination, Host and retry rules.
- Reduce Overview gutters uniformly (normal 4px, dense 3px, comfortable
12px). Override the older dashboard-density spacing selectors for the
aligned grid so tile-to-tile and tile-to-panel transitions match in both
directions without changing row symmetry.
- Allow `preserve_host` for HTTP/1.1, HTTPS Auto H2/H1 ALPN, legacy compatible - Allow `preserve_host` for HTTP/1.1, HTTPS Auto H2/H1 ALPN, legacy compatible
HTTP/2 and strict H2/H2C. Forward the public Host consistently as HTTP/2 HTTP/2 and strict H2/H2C. Forward the public Host consistently as HTTP/2
`:authority` while keeping DNS/TCP pinned to the configured origin URL and `:authority` while keeping DNS/TCP pinned to the configured origin URL and
+9 -3
View File
@@ -9,7 +9,13 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
## What changed in 0.32.6 ## What changed in 0.32.6
- **Rate-chart pause recovery (0.32.6 development branch):** share one - **Appearance Dashboard layout ** Overview cards now use a
full-width horizontal strip instead of a long list in the right half of
Settings. Navigate sideways with the arrow buttons, toggle card visibility
or reorder in place, or select **Show all cards** to expand to a full grid.
The existing visual editor and saved per-account preferences are unchanged.
- **Rate-chart pause recovery:** share one
counter continuity policy across Cache HIT/MISS/BYPASS, Transform, HTTP counter continuity policy across Cache HIT/MISS/BYPASS, Transform, HTTP
Overview/protocols, optional wide/small charts, backend and Stream/Vhost Overview/protocols, optional wide/small charts, backend and Stream/Vhost
traffic charts. After tab hide/pagehide (including BFCache) or a delayed traffic charts. After tab hide/pagehide (including BFCache) or a delayed
@@ -18,7 +24,7 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
zero still appears as 0/s. Cache and Transform intervals and the separate zero still appears as 0/s. Cache and Transform intervals and the separate
System information interval remain configurable; no additional polling. System information interval remain configurable; no additional polling.
- **Development fixes (same 0.32.6 version):** keep Network diagnostics - **Development fixes :** keep Network diagnostics
chart DOM stable across live refreshes, preventing repeated row/SVG replacement chart DOM stable across live refreshes, preventing repeated row/SVG replacement
during scrolling. Network-interface chart history advances at the Overview during scrolling. Network-interface chart history advances at the Overview
live-status cadence using the latest sampled rates; fetching system data still live-status cadence using the latest sampled rates; fetching system data still
@@ -147,7 +153,7 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
- **Overview readability at 90% UI scale:** compare the supplied 0.31.9 source with 0.32.0, remove the latter's forced desktop card heights and restore content-sized rows. The six-column desktop layout and visible chart legends remain intact. Increase compact Dashboard headings (10 → 11px), values (22 → 25px), secondary text (8.5 → 10px) and the Payload breakdown (9 → 10px). - **Overview readability at 90% UI scale:** compare the supplied 0.31.9 source with 0.32.0, remove the latter's forced desktop card heights and restore content-sized rows. The six-column desktop layout and visible chart legends remain intact. Increase compact Dashboard headings (10 → 11px), values (22 → 25px), secondary text (8.5 → 10px) and the Payload breakdown (9 → 10px).
- **Consistent tile spacing:** Overview now uses `--ui-gap`, the same density-aware spacing variable as other Dashboard sections, instead of its own fixed 10/8/6px spacing in normal/compact/ultra Dashboard modes. UI scale (including 90%) affects all gaps equally. - **Consistent tile spacing:** Overview now uses `--ui-gap`, the same density-aware spacing variable as other Dashboard sections, instead of its own fixed 10/8/6px spacing in normal/compact/ultra Dashboard modes. UI scale (including 90%) affects all gaps equally.
- **Appearance hints without horizontal scrolling:** Dashboard layout Stream card descriptions now use native, localized browser tooltips and accessible button descriptions instead of absolute-positioned custom bubbles. The settings grid fits narrower containers and obsolete tooltip CSS has been removed. - **Appearance hints without overflow-causing tooltips:** Dashboard layout Stream card descriptions now use native, localized browser tooltips and accessible button descriptions instead of absolute-positioned custom bubbles. The settings grid fits narrower containers and obsolete tooltip CSS has been removed.
- Preserve Dashboard visibility/order preferences, density choices, responsive column breakpoints and opt-in Stream cards. No runtime, API, SQLite, proxy routing or language-pack changes. The Rust-independent `./dev.sh test` checks passed, then stopped with code 127 because Cargo is unavailable; **Cargo/build and authenticated-browser validation remain open** until executed on a supported runtime. - Preserve Dashboard visibility/order preferences, density choices, responsive column breakpoints and opt-in Stream cards. No runtime, API, SQLite, proxy routing or language-pack changes. The Rust-independent `./dev.sh test` checks passed, then stopped with code 127 because Cargo is unavailable; **Cargo/build and authenticated-browser validation remain open** until executed on a supported runtime.
## Previously in 0.32.0 ## Previously in 0.32.0
+15 -13
View File
@@ -2,17 +2,17 @@
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
a402ee07826dfab1b1d63b98c4a9f75809f056b908c92abacf0bba816d7ad133 AGENTS.md a402ee07826dfab1b1d63b98c4a9f75809f056b908c92abacf0bba816d7ad133 AGENTS.md
54383ec0610f11c61ef64d3cf0afa63f5667b212c03db653f37a9277a637df8c CHANGELOG.md e4a565f39f4ecca622f0eb27822c7399e45b4c4d1a2fd1f669185d53196a969b CHANGELOG.md
934ec0e7f8a307b00319606a9c5d7e91dac6449a23dd1f3abcefe4304eb4de06 Cargo.lock 934ec0e7f8a307b00319606a9c5d7e91dac6449a23dd1f3abcefe4304eb4de06 Cargo.lock
8a7606650ce03f1faf36c893a17ccf1c9e8da4259e0248ac2c9bf030a64872d1 Cargo.toml 8a7606650ce03f1faf36c893a17ccf1c9e8da4259e0248ac2c9bf030a64872d1 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
dc35bafda41c2e0db266523f553d0803740c5e842edfbdd7af202041c53aa359 README.md 0f8cedf595f57f96644e8d4dc608b43e6ebd9a73b31157a210d624492a6c5206 README.md
da519345b2bce492e747c268df2730971543d1bdb198c3e854671a7d107e5609 ROADMAP.md da519345b2bce492e747c268df2730971543d1bdb198c3e854671a7d107e5609 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service 4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
c51ac385fa992cdcfade11aba0151e20f1d2d01362deac7c04e585dfbc287f19 dev.sh 26a607151fd343a59c6f147e70897c4b1bfeab376348f3270eb8a373503d7be5 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -61,9 +61,9 @@ d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.m
ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md 007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
46d3cb5a2b5909902c85de193bc5129c751c5ebd19dc752db282588d54b5e25c docs/SETTINGS.md a41cef4a4da5e098fe3e9dfa0fffb3625f2e09792eacdf5dbf8d8fd532146d26 docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md 8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
81997f880b89ea68b2820f47a83db030425e91ef5da90d1bd459754a1eb63f6c docs/TEST_REPORT.md 130befc0f8216214567dd4b2df2574ea751b56b22c8f2cc3ba48e8d0aaccc3b5 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
@@ -95,8 +95,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md 71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
fc6695db8c2a9896dc97642ef70d56de86657521d0f4e1f2f57b99ac97c4f2dc langs/en_US.json a1f873bfe87d090bac46962598aeb90ddfa62ae257b3b76ec1af011c74197bac langs/en_US.json
a65d977f8380113d3ec7db5d51bffe50b87f5d93c5b139371f2351093b547216 langs/pl_pl.json ca95ffe6919c07699155d1c49fa711916f91f0fbe928e5a36be23f05519ffbbb langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -182,10 +182,10 @@ dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/
e5928664c259feab3e9cfd3be69d05571f3aa60445ca373560f50857ecc8bfc0 src/admin/web/app/overview-extra-metrics.js e5928664c259feab3e9cfd3be69d05571f3aa60445ca373560f50857ecc8bfc0 src/admin/web/app/overview-extra-metrics.js
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js
c4d59e129837db854f2851ac69130325f6d2f25fd1b67a009354b83da1dfbf02 src/admin/web/app/overview-wide-metrics.js c4d59e129837db854f2851ac69130325f6d2f25fd1b67a009354b83da1dfbf02 src/admin/web/app/overview-wide-metrics.js
a73d4efc9cf2f6bf7117f6f6dcaaea106e03c328451abbe4c10281179750899d src/admin/web/app/preferences.js 7e0a4dc7274a1d1e4cc928e360a6b77c48a71d586b585a9e28c3bb2eeb17d46a src/admin/web/app/preferences.js
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js 8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js 43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
96b19ad3fe98541013f278d63ccbe5c3f5a7f9b04a02167780176e6b15532057 src/admin/web/app/runtime.js 7eee9fd6f930288ee3f87dd7a2c64bddb24ae5326cf69600accc376fb015fc87 src/admin/web/app/runtime.js
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js 31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js 86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
fbd2c402828088695f5f5f4b210d1ea8da24ae6425f61248c82c5b07b14afd56 src/admin/web/app/stream-dashboard.js fbd2c402828088695f5f5f4b210d1ea8da24ae6425f61248c82c5b07b14afd56 src/admin/web/app/stream-dashboard.js
@@ -213,7 +213,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js 7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
2574793d504ec835e477c599187b98d50014686da215cc8adc7548c1e2bdbe5e src/admin/web/index.html 1335cf75c5da63bdb2b9978aa79ec3ad37fb66e6c3cb38647375227c37c2442f src/admin/web/index.html
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js 03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
@@ -223,7 +223,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html 8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
aed93c1eb1d18735d9058755f3e648f0f74f2797855446f1d10d330dbf306edd src/admin/web/style.css cd89785fc8f27353983846c799f9256a1a893d775cb4853fb62b76c29e42f8c7 src/admin/web/style.css
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js 69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
@@ -498,7 +498,7 @@ e5c58fc9b7685ec911bfa86f6ca4f0cc97c19a90c0cff5b60ea9887f1afe9ced tools/checks/a
0060f8217640403c6af2d5091fdc9087d0fb2fc30cd545a3c58277a396d8121c tools/checks/admin_public_origin_0314_sim.mjs 0060f8217640403c6af2d5091fdc9087d0fb2fc30cd545a3c58277a396d8121c tools/checks/admin_public_origin_0314_sim.mjs
fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py
c9d0bef4b8ed7a2f49b0fad3e116cace62d3e6e1ca31e02a3073d1fdde5157d8 tools/checks/admin_themes_0323_ui_test.mjs c9d0bef4b8ed7a2f49b0fad3e116cace62d3e6e1ca31e02a3073d1fdde5157d8 tools/checks/admin_themes_0323_ui_test.mjs
02b02ad7831524f963da3b80b1b42bb6efef9c4a63ce89649b18f70a0110d32d tools/checks/appearance_ux_0323_browser.py 5b7d146a4558b2435fb4e8813db9e20e446988003fff884b3e132b28d926fa3c tools/checks/appearance_ux_0323_browser.py
8b6e1a355cae58901118b399fa2d165f7e3fd8465c89cac82776906b8b23f0a6 tools/checks/appearance_ux_0323_ui_test.mjs 8b6e1a355cae58901118b399fa2d165f7e3fd8465c89cac82776906b8b23f0a6 tools/checks/appearance_ux_0323_ui_test.mjs
f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_ui_test.mjs f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_ui_test.mjs
e82765ae946461952e68369f0b16f0a99f01e625bd61d2551513504f7b2f4d46 tools/checks/backend_dashboard_0323_ui_test.mjs e82765ae946461952e68369f0b16f0a99f01e625bd61d2551513504f7b2f4d46 tools/checks/backend_dashboard_0323_ui_test.mjs
@@ -507,6 +507,8 @@ a135c9456143975279f18233caab44b8c64f5025ef4ceeb1f2ad22090b588019 tools/checks/b
a1fb118e3a32819689cb607d574b0060ca29ff1f6a68c505231a4e88ea93b5de tools/checks/certificate_api_0319.py a1fb118e3a32819689cb607d574b0060ca29ff1f6a68c505231a4e88ea93b5de tools/checks/certificate_api_0319.py
526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs 526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs
71cf48ba645ae2b7a95c73faa362d12942553e37d79680b796edf29c1484d9eb tools/checks/check_source.py 71cf48ba645ae2b7a95c73faa362d12942553e37d79680b796edf29c1484d9eb tools/checks/check_source.py
a1ac2ab24958c6e8527c7797f224f94e765abd7173d4e5085a64362b193112c1 tools/checks/dashboard_appearance_ribbon_0326_browser.py
0ab28370c06b2258321e20d27d63098c339fd9dc3b6f62aef6f5266c81ff28b5 tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs
dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs
7508f232a9e48049002dd4c9f34f9737bc92d304c51219f50430578dbc721855 tools/checks/dashboard_inline_layout_0323_ui_test.mjs 7508f232a9e48049002dd4c9f34f9737bc92d304c51219f50430578dbc721855 tools/checks/dashboard_inline_layout_0323_ui_test.mjs
dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/dashboard_layout_editor_0323_ui_test.mjs dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/dashboard_layout_editor_0323_ui_test.mjs
@@ -553,7 +555,7 @@ db8881e46622ca2dbab4c29df003020006c611cd7fe087878c3e9c930cdd9853 tools/checks/s
011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py 011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py
37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py 37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py
be78df1e438d76be83e15398b310c9843ae5965b0ec6b41279b3a9650c313d8a tools/checks/stream_0320_ui_test.mjs be78df1e438d76be83e15398b310c9843ae5965b0ec6b41279b3a9650c313d8a tools/checks/stream_0320_ui_test.mjs
e04f41b09c95e7bf67944c85690aafd825822877e9e0058cc872fc9e29bac78e tools/checks/stream_0320_visibility_regression.mjs 6c7d20fa2abb55de997c19abd07ec1afa8d5121c91e07f231976bcdfbfbba778 tools/checks/stream_0320_visibility_regression.mjs
8542f5c668155aa9f70de11e6fdaf64c5a80629a15ecc5f1d8c40f3d655f9b55 tools/checks/system_info_0301_ui_test.mjs 8542f5c668155aa9f70de11e6fdaf64c5a80629a15ecc5f1d8c40f3d655f9b55 tools/checks/system_info_0301_ui_test.mjs
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py 543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py
e650225258168eeb9623694c6684f9a5ace92ca94debdff2558b5c77f7c998bc tools/checks/upstream_protocols_0323.py e650225258168eeb9623694c6684f9a5ace92ca94debdff2558b5c77f7c998bc tools/checks/upstream_protocols_0323.py
+1
View File
@@ -128,6 +128,7 @@ if [[ "$mode" == "test" ]]; then
node tools/checks/dashboard_inline_layout_0323_ui_test.mjs node tools/checks/dashboard_inline_layout_0323_ui_test.mjs
node tools/checks/admin_themes_0323_ui_test.mjs node tools/checks/admin_themes_0323_ui_test.mjs
node tools/checks/appearance_ux_0323_ui_test.mjs node tools/checks/appearance_ux_0323_ui_test.mjs
node tools/checks/dashboard_appearance_ribbon_0326_ui_test.mjs
node tools/checks/backend_protocol_0326_ui_test.mjs node tools/checks/backend_protocol_0326_ui_test.mjs
node tools/checks/overview_extra_metrics_0323_ui_test.mjs node tools/checks/overview_extra_metrics_0323_ui_test.mjs
node tools/checks/rate_pause_0326_ui_test.mjs node tools/checks/rate_pause_0326_ui_test.mjs
+11
View File
@@ -37,6 +37,17 @@ Open **Settings → Certificate API** (`/settings?tab=certificate-api`) to manag
The existing **Generate / rotate token** form still creates new Key IDs (and retains its previous ability to rotate a Key ID by supplying it). Use **Edit settings** to modify settings without rotating, and **Regenerate token** to rotate without changing any settings. See [API](API.md) and [access control](API_ACCESS.md). The existing **Generate / rotate token** form still creates new Key IDs (and retains its previous ability to rotate a Key ID by supplying it). Use **Edit settings** to modify settings without rotating, and **Regenerate token** to rotate without changing any settings. See [API](API.md) and [access control](API_ACCESS.md).
## Compact Overview card controls (0.32.6 development branch)
**Settings → Appearance → Dashboard layout** now puts Dashboard density on a
compact row and gives the Overview card controls the full width of the section.
The card selector is a single horizontally scrollable strip by default, with
left/right browsing buttons and accessible keyboard focus. **Show all cards**
expands the same 62 visibility/reorder controls into a wrapped grid. Use the
existing visual layout editor for drag-and-drop arrangement. Reordering and
visibility still save through `/api/preferences` per account; this UI-only
change does not alter server polling, proxy routing or backend metrics.
## Refresh preferences (0.31.0) ## Refresh preferences (0.31.0)
**Settings -> Appearance -> Data refresh** saves an additional `refresh` object in the existing `admin_preferences` document for the signed-in account. It does not modify proxy configuration or create any migration. Older documents, including partially populated `refresh` objects, use defaults. Browser and server enforce exact allowed values: **Settings -> Appearance -> Data refresh** saves an additional `refresh` object in the existing `admin_preferences` document for the signed-in account. It does not modify proxy configuration or create any migration. Older documents, including partially populated `refresh` objects, use defaults. Browser and server enforce exact allowed values:
+32
View File
@@ -1,5 +1,37 @@
# Verification report - RustProxy 0.32.6 # Verification report - RustProxy 0.32.6
## 0.32.6 development follow-up — compact Appearance Dashboard layout
- The previous right-half, wrapping Overview card list left most of the screen
empty and grew excessively tall with the 62-card registry. Moved Dashboard
density to a compact full-width row and made the selector a full-width,
single-line horizontal ribbon. Added previous/next browsing, a **Show all**
expanded grid, horizontal earlier/later order buttons and Polish/English
strings. Every existing toggle, order save, visual editor and account-scoped
`/api/preferences` path remains intact.
- Chromium fixture with real CSS, real 62-card renderer and mocked preferences:
at 1500px, the default ribbon is 47px high and the Appearance dashboard
section about 274px high; at 393px, the ribbon is 47px, the section about
419px, and there is no page-level horizontal overflow. Programmatic browse,
visibility-toggle, retained scroll position and expand/collapse all passed.
`tools/checks/dashboard_appearance_ribbon_0326_browser.py` reproduces this
test. This is a simulated interface test, not a compiled Rust or iOS Safari test.
- Source/Node checks additionally verify translations, all card IDs and the
unchanged preference persistence calls. Full Rust build and physical iOS
acceptance remain pending.
## 0.32.6 development follow-up — logo right-edge spacing
- Corrected the previous logo-spacing adjustment: the desired clearance is
between the final `p` glyph and the **right border of the logo frame**,
not between `/` and `p`. Removed extra slash margins and added a 2px
right-side flex-box inset without changing the frame size or color.
- Browser regression checks the text glyph's bounding rectangle against the
frame at desktop and mobile widths; Chromium measures approximately 5px
clearance compared with approximately 2px before correction. This does not
require Rust compilation or imply a physical iOS Safari verification.
## 0.32.6 development follow-up — live diagnostic charts / mobile scroll ## 0.32.6 development follow-up — live diagnostic charts / mobile scroll
- The existing server diagnostics collector samples OS and process metrics in - The existing server diagnostics collector samples OS and process metrics in
+7
View File
@@ -2210,6 +2210,13 @@
"dashboardDensityHelp": "Controls Overview telemetry card size without changing the rest of the interface.", "dashboardDensityHelp": "Controls Overview telemetry card size without changing the rest of the interface.",
"dashboardWidgets": "Overview cards", "dashboardWidgets": "Overview cards",
"dashboardWidgetsHelp": "Show, hide and reorder all Overview blocks, including charts, diagnostics and the backend table.", "dashboardWidgetsHelp": "Show, hide and reorder all Overview blocks, including charts, diagnostics and the backend table.",
"dashboardWidgetsScrollHelp": "Scroll horizontally to browse cards, or expand the list.",
"dashboardWidgetsMoveEarlier": "Move card earlier",
"dashboardWidgetsMoveLater": "Move card later",
"dashboardWidgetsScrollNext": "Scroll cards right",
"dashboardWidgetsScrollBack": "Scroll cards left",
"dashboardWidgetsCompact": "Compact view",
"dashboardWidgetsShowAll": "Show all cards",
"dashboardWidgetVisible": "Visible", "dashboardWidgetVisible": "Visible",
"moveUp": "Move up", "moveUp": "Move up",
"moveDown": "Move down", "moveDown": "Move down",
+7
View File
@@ -2210,6 +2210,13 @@
"dashboardDensityHelp": "Steruje rozmiarem kafelków telemetrii w Overview bez zmiany reszty interfejsu.", "dashboardDensityHelp": "Steruje rozmiarem kafelków telemetrii w Overview bez zmiany reszty interfejsu.",
"dashboardWidgets": "Kafelki Overview", "dashboardWidgets": "Kafelki Overview",
"dashboardWidgetsHelp": "Pokazuj, ukrywaj i układaj wszystkie bloki Overview, także wykresy, diagnostykę i tabelę backendów.", "dashboardWidgetsHelp": "Pokazuj, ukrywaj i układaj wszystkie bloki Overview, także wykresy, diagnostykę i tabelę backendów.",
"dashboardWidgetsScrollHelp": "Przewijaj kafelki poziomo lub rozwiń całą listę.",
"dashboardWidgetsMoveEarlier": "Przesuń kafelek wcześniej",
"dashboardWidgetsMoveLater": "Przesuń kafelek później",
"dashboardWidgetsScrollNext": "Przewiń kafelki w prawo",
"dashboardWidgetsScrollBack": "Przewiń kafelki w lewo",
"dashboardWidgetsCompact": "Widok kompaktowy",
"dashboardWidgetsShowAll": "Pokaż wszystkie",
"dashboardWidgetVisible": "Widoczny", "dashboardWidgetVisible": "Widoczny",
"moveUp": "Przenieś wyżej", "moveUp": "Przenieś wyżej",
"moveDown": "Przenieś niżej", "moveDown": "Przenieś niżej",
+47 -7
View File
@@ -96,8 +96,6 @@ function applyDashboardLayout(order, hidden) {
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden)); const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden));
const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] }; const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node])); const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
// Re-appending an already ordered live grid can break touch-scroll
// anchoring in mobile browsers. Move the DOM only on a real order change.
const orderedNodes = normalizedOrder.map(id => nodes.get(id)).filter(Boolean); const orderedNodes = normalizedOrder.map(id => nodes.get(id)).filter(Boolean);
for (const node of orderedNodes) node.hidden = hiddenSet.has(node.dataset.dashboardWidget); for (const node of orderedNodes) node.hidden = hiddenSet.has(node.dataset.dashboardWidget);
const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")]; const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")];
@@ -129,23 +127,25 @@ function dashboardWidgetTypeBadge(id) {
} }
function renderDashboardWidgetSettings() { function renderDashboardWidgetSettings() {
const list = $("dashboard-widget-list"); if (!list) return; const list = $("dashboard-widget-list"); if (!list) return;
const previousScroll = list.scrollLeft;
const focusedRow = document.activeElement?.closest?.("[data-dashboard-widget-setting]");
const focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null;
const focusedAction = focusedId ? document.activeElement?.dataset.dashboardWidgetAction : null;
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets)); const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
const rows = order.map((id, index) => { const rows = order.map((id, index) => {
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id; const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
const visible = !hidden.has(id); const visible = !hidden.has(id);
const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`); const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.dataset.dashboardWidgetAction = "toggle"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
const descriptionKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key; const descriptionKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
if (descriptionKey) { if (descriptionKey) {
const description = t(descriptionKey); const description = t(descriptionKey);
row.title = description; row.title = description;
toggle.setAttribute("aria-description", description); toggle.setAttribute("aria-description", description);
} }
// Type badges belong in the visual layout editor, not the long
// Appearance settings list where they crowd the visibility controls.
toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label")); toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
const actions = el("span", undefined, "dashboard-widget-setting-actions"); const actions = el("span", undefined, "dashboard-widget-setting-actions");
const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp")); const up = el("button", "←", "quiet"); up.type = "button"; up.dataset.dashboardWidgetAction = "up"; up.disabled = index === 0; up.setAttribute("aria-label", t("dashboardWidgetsMoveEarlier"));
const down = el("button", "↓", "quiet"); down.type = "button"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("moveDown")); const down = el("button", "→", "quiet"); down.type = "button"; down.dataset.dashboardWidgetAction = "down"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("dashboardWidgetsMoveLater"));
toggle.addEventListener("click", () => { toggle.addEventListener("click", () => {
const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id); const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id);
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings(); if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings();
@@ -160,6 +160,46 @@ function renderDashboardWidgetSettings() {
up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(toggle, actions); return row; up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(toggle, actions); return row;
}); });
list.replaceChildren(...rows); list.replaceChildren(...rows);
list.scrollLeft = previousScroll;
if (focusedId && focusedAction) {
const row = [...list.children].find(item => item.dataset.dashboardWidgetSetting === focusedId);
row?.querySelector(`[data-dashboard-widget-action="${focusedAction}"]`)?.focus({ preventScroll: true });
}
syncDashboardWidgetListView();
if (typeof requestAnimationFrame === "function") requestAnimationFrame(syncDashboardWidgetScrollButtons);
}
let dashboardWidgetListExpanded = false;
function syncDashboardWidgetListView() {
const list = $("dashboard-widget-list"), expand = $("dashboard-widget-expand");
if (!list || !expand) return;
list.classList.toggle("is-expanded", dashboardWidgetListExpanded);
expand.setAttribute("aria-expanded", String(dashboardWidgetListExpanded));
const key = dashboardWidgetListExpanded ? "dashboardWidgetsCompact" : "dashboardWidgetsShowAll";
expand.dataset.i18n = key;
expand.textContent = t(key);
for (const id of ["dashboard-widget-scroll-prev", "dashboard-widget-scroll-next"]) {
const button = $(id); if (button) button.hidden = dashboardWidgetListExpanded;
}
syncDashboardWidgetScrollButtons();
}
function syncDashboardWidgetScrollButtons() {
const list = $("dashboard-widget-list"); if (!list) return;
const previous = $("dashboard-widget-scroll-prev"), next = $("dashboard-widget-scroll-next");
if (previous) previous.disabled = dashboardWidgetListExpanded || list.scrollLeft <= 1;
if (next) next.disabled = dashboardWidgetListExpanded || list.scrollLeft + list.clientWidth >= list.scrollWidth - 1;
}
$("dashboard-widget-expand")?.addEventListener("click", () => {
dashboardWidgetListExpanded = !dashboardWidgetListExpanded;
syncDashboardWidgetListView();
});
$("dashboard-widget-list")?.addEventListener?.("scroll", syncDashboardWidgetScrollButtons, { passive: true });
window.addEventListener?.("resize", syncDashboardWidgetScrollButtons);
for (const [id, direction] of [["dashboard-widget-scroll-prev", -1], ["dashboard-widget-scroll-next", 1]]) {
$(id)?.addEventListener("click", () => {
const list = $("dashboard-widget-list");
if (!list) return;
list.scrollBy({ left: direction * Math.max(160, list.clientWidth * .8), behavior: "smooth" });
});
} }
function refreshChartStylePalette(active = adminPreferences?.chart_style || "default") { function refreshChartStylePalette(active = adminPreferences?.chart_style || "default") {
const target = $("chart-style-palette"); if (!target || !languagePacks.size) return; const target = $("chart-style-palette"); if (!target || !languagePacks.size) return;
+11 -16
View File
@@ -1,4 +1,3 @@
function verifyAdminModules() { function verifyAdminModules() {
const required = [ const required = [
["rustProxyIntegrations", "bindings"], ["rustProxyIntegrations", "bindings"],
@@ -12,7 +11,6 @@ function verifyAdminModules() {
const missing = required.find(([module, method]) => typeof window[module]?.[method] !== "function"); const missing = required.find(([module, method]) => typeof window[module]?.[method] !== "function");
if (missing) throw new Error(`Panel resources incomplete (${missing[0]}). Reload to retry.`); if (missing) throw new Error(`Panel resources incomplete (${missing[0]}). Reload to retry.`);
} }
async function startAdminPanel() { async function startAdminPanel() {
try { try {
verifyAdminModules(); verifyAdminModules();
@@ -33,7 +31,6 @@ async function startAdminPanel() {
} }
if (document.readyState === "complete") startAdminPanel(); if (document.readyState === "complete") startAdminPanel();
else document.addEventListener("DOMContentLoaded", startAdminPanel, { once: true }); else document.addEventListener("DOMContentLoaded", startAdminPanel, { once: true });
function certificateUsesExternalFiles(cert) { function certificateUsesExternalFiles(cert) {
return String(cert?.cert_file || "").startsWith("/") && String(cert?.key_file || "").startsWith("/"); return String(cert?.cert_file || "").startsWith("/") && String(cert?.key_file || "").startsWith("/");
} }
@@ -53,7 +50,6 @@ function syncCertificateFileSource() {
$("certificate-external-acl-example").hidden = !external; $("certificate-external-acl-example").hidden = !external;
} }
$("certificate-file-source").addEventListener("change", syncCertificateFileSource); $("certificate-file-source").addEventListener("change", syncCertificateFileSource);
function renderCertificates() { function renderCertificates() {
if (!envelope) return; if (!envelope) return;
const settings = envelope.config.tls || {}; const settings = envelope.config.tls || {};
@@ -69,7 +65,6 @@ function renderCertificates() {
if (settings.default_certificate === cert.id) used.push(t("defaultLabel")); if (settings.default_certificate === cert.id) used.push(t("defaultLabel"));
const managed = !!cert.issuer, inUse = used.length > 0; const managed = !!cert.issuer, inUse = used.length > 0;
const box = el("div", undefined, "entity certificate-card"); const box = el("div", undefined, "entity certificate-card");
const head = el("div", undefined, "certificate-card-head"); const head = el("div", undefined, "certificate-card-head");
const identity = el("div", undefined, "certificate-card-identity"); const identity = el("div", undefined, "certificate-card-identity");
identity.append(el("span", cert.id, "entity-title")); identity.append(el("span", cert.id, "entity-title"));
@@ -152,7 +147,7 @@ $("reload-certificates").addEventListener("click", async () => {
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); }); $("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
syncCertificateFileSource(); syncCertificateFileSource();
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, lastCacheOutcomeRates = null,
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set(); processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
const hiddenNetworkInterfaces = (() => { const hiddenNetworkInterfaces = (() => {
try { try {
@@ -206,8 +201,7 @@ function svgNode(tag, attrs = {}) {
for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value); for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
return node; return node;
} }
// Diagnostic charts keep their SVG and label nodes across samples. Updating the
// existing Text node avoids remove/insert mutations while the user is scrolling.
function setChartText(node, value) { function setChartText(node, value) {
const text = String(value); const text = String(value);
if (node.firstChild?.nodeType === 3 && node.firstChild === node.lastChild) { if (node.firstChild?.nodeType === 3 && node.firstChild === node.lastChild) {
@@ -216,8 +210,7 @@ function setChartText(node, value) {
node.textContent = text; node.textContent = text;
} }
} }
// Preserve stable Overview DOM while live samples arrive: replacing network SVGs
// during an iOS WebKit momentum scroll can invalidate the current scroll anchor.
let latestNetworkInterfaceSnapshot = []; let latestNetworkInterfaceSnapshot = [];
function renderNetworkInterfaceCharts(interfaces, appendSample) { function renderNetworkInterfaceCharts(interfaces, appendSample) {
const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : []; const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : [];
@@ -305,8 +298,7 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
children.push(empty); children.push(empty);
} }
} }
// The common live-sample path updates only text and SVG path attributes.
// Reparent rows solely when interfaces or visibility have actually changed.
if (children.length !== target.children.length || children.some((node, index) => node !== target.children[index])) { if (children.length !== target.children.length || children.some((node, index) => node !== target.children[index])) {
target.replaceChildren(...children); target.replaceChildren(...children);
} }
@@ -316,7 +308,6 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
$("metric-h3").textContent = fmt(m.http3_requests); $("metric-h3").textContent = fmt(m.http3_requests);
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`; $("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`; $("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
// Slow details vs cached live chart telemetry.
if (systemSample) { if (systemSample) {
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes); if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
$("process-summary").hidden = d.process_threads == null; $("process-summary").hidden = d.process_threads == null;
@@ -358,9 +349,13 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
const currentCounters = [c.hits, c.misses, c.bypasses].map(Number); const currentCounters = [c.hits, c.misses, c.bypasses].map(Number);
if (currentCounters.every(Number.isFinite)) { if (currentCounters.every(Number.isFinite)) {
const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds")); const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
if (delta.deltas || !lastCacheCounters) { const values = delta.deltas || [0, 0, 0]; const values = delta.deltas || lastCacheOutcomeRates || (!lastCacheCounters ? [0, 0, 0] : null);
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]); cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60); if (values) {
} lastCacheCounters = delta.next; cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]);
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
lastCacheOutcomeRates = [...values];
}
lastCacheCounters = delta.next;
} }
} }
if (appendSample) { if (appendSample) {
+31 -7
View File
@@ -2477,8 +2477,9 @@
data-appearance-reset="dashboard" data-i18n="resetSection" data-appearance-reset="dashboard" data-i18n="resetSection"
type="button">Reset section</button> type="button">Reset section</button>
</div> </div>
<div class="appearance-settings-grid"> <div class="appearance-settings-grid dashboard-appearance-grid">
<label><span data-i18n="dashboardDensity">Dashboard density</span><select <label class="dashboard-density-setting"><span
data-i18n="dashboardDensity">Dashboard density</span><select
id="ui-dashboard-density"> id="ui-dashboard-density">
<option data-i18n="densityNormal" value="normal">Normal</option> <option data-i18n="densityNormal" value="normal">Normal</option>
<option data-i18n="densityCompact" value="compact">Compact</option> <option data-i18n="densityCompact" value="compact">Compact</option>
@@ -2486,15 +2487,38 @@
</select><small class="fine" data-i18n="dashboardDensityHelp">Controls </select><small class="fine" data-i18n="dashboardDensityHelp">Controls
Overview telemetry card size without changing the rest of the Overview telemetry card size without changing the rest of the
interface.</small></label> interface.</small></label>
<div class="dashboard-widget-settings"><span <div class="dashboard-widget-settings">
data-i18n="dashboardWidgets">Overview cards</span><small class="fine" <div class="dashboard-widget-settings-head">
data-i18n="dashboardWidgetsHelp">Show, hide and reorder the telemetry <div class="dashboard-widget-settings-intro">
cards at the top of Overview.</small> <span data-i18n="dashboardWidgets">Overview cards</span>
<small class="fine" data-i18n="dashboardWidgetsHelp">Show, hide and
reorder all Overview blocks, including charts, diagnostics and
the backend table.</small>
</div>
<div class="dashboard-widget-settings-controls">
<button class="outline dashboard-layout-open-settings" type="button" <button class="outline dashboard-layout-open-settings" type="button"
id="overview-layout-open-settings" id="overview-layout-open-settings"
data-i18n="overviewLayoutEditorOpenSettings">Visual layout data-i18n="overviewLayoutEditorOpenSettings">Visual layout
editor</button> editor</button>
<div class="dashboard-widget-list" id="dashboard-widget-list"></div> <button class="quiet" id="dashboard-widget-expand" type="button"
aria-expanded="false" aria-controls="dashboard-widget-list"
data-i18n="dashboardWidgetsShowAll">Show all</button>
<button class="quiet dashboard-widget-scroll-button"
id="dashboard-widget-scroll-prev" type="button"
data-i18n-aria="dashboardWidgetsScrollBack"
aria-label="Scroll cards left">&larr;</button>
<button class="quiet dashboard-widget-scroll-button"
id="dashboard-widget-scroll-next" type="button"
data-i18n-aria="dashboardWidgetsScrollNext"
aria-label="Scroll cards right">&rarr;</button>
</div>
</div>
<div class="dashboard-widget-list" id="dashboard-widget-list" role="group"
data-i18n-aria="dashboardWidgets" aria-label="Overview cards"
tabindex="0"></div>
<small class="fine dashboard-widget-scroll-hint"
data-i18n="dashboardWidgetsScrollHelp">Scroll horizontally to browse
cards, or expand the list.</small>
<small class="fine" data-i18n="streamWidgetSettingsHelp">Stream cards are <small class="fine" data-i18n="streamWidgetSettingsHelp">Stream cards are
opt-in and only change Dashboard presentation, not Stream routing or opt-in and only change Dashboard presentation, not Stream routing or
monitoring.</small> monitoring.</small>
+1460 -962
View File
File diff suppressed because it is too large. Load diff
@@ -65,6 +65,18 @@ with sync_playwright() as playwright:
assert page.locator('#test-action').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'button') assert page.locator('#test-action').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'button')
assert page.locator('#test-input').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'input') assert page.locator('#test-input').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'input')
page.evaluate('document.documentElement.dataset.theme = "dark"; document.documentElement.dataset.cornerStyle = "default"') page.evaluate('document.documentElement.dataset.theme = "dark"; document.documentElement.dataset.cornerStyle = "default"')
# The last glyph must have room before the outlined logo's right edge,
# including the 34px mobile variant; the slash-to-p spacing is unchanged.
for width in (1320, 790, 450, 345):
page.set_viewport_size({'width': width, 'height': 950})
inset = page.locator('#test-logo').evaluate('''mark => {
const p = [...mark.childNodes].find(node => node.nodeType === 3 && node.textContent.trim() === 'p');
const range = document.createRange();
range.selectNodeContents(p);
return mark.getBoundingClientRect().right - range.getBoundingClientRect().right;
}''')
assert inset >= 4, (width, inset)
page.set_viewport_size({'width': 1320, 'height': 950})
original_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor') original_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor')
page.evaluate('document.documentElement.dataset.theme = "ultra_dark"') page.evaluate('document.documentElement.dataset.theme = "ultra_dark"')
black_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor') black_logo = page.locator('#test-logo').evaluate('(n) => getComputedStyle(n).borderTopColor')
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Optional real-CSS/JS Chromium regression for the horizontal Overview card ribbon.
Mocks authenticated preferences; no server or physical iOS device involved.
Run with: python tools/checks/dashboard_appearance_ribbon_0326_browser.py
"""
from pathlib import Path
from playwright.sync_api import sync_playwright
import shutil
import json
root=Path(__file__).resolve().parents[2]
html=(root/'src/admin/web/index.html').read_text()
css=(root/'src/admin/web/style.css').read_text()
prefs=(root/'src/admin/web/app/preferences.js').read_text()
registry=json.loads((root/'src/admin/web/dashboard-widgets.json').read_text())
translations=json.loads((root/'langs/pl_pl.json').read_text())['translations']
heading=html.index('data-i18n="dashboardLayout"')
section_start=html.rfind('<section class="appearance-section">',0,heading)
section_end=html.index('</section>',heading)+len('</section>')
assert section_start >= 0
source=html[section_start:section_end]
start=prefs.index('function renderDashboardWidgetSettings() {')
end=prefs.index('function refreshChartStylePalette(',start)
code=prefs[start:end]
stub='''
const $ = id => document.getElementById(id);
const el = (tag, txt, classes) => { let node=document.createElement(tag); if (txt!==undefined) node.textContent=String(txt); if (classes) node.className=classes; return node; };
const labels = __TRANSLATIONS__;
const registry = __REGISTRY__;
const ADMIN_DASHBOARD_BY_ID = new Map(registry.map(def=>[def.id,def]));
const order = registry.map(def=>def.id);
let adminPreferences = {dashboard_widgets:order, dashboard_hidden_widgets:registry.filter(def=>def.default_hidden).map(def=>def.id)};
const ADMIN_APPEARANCE_DEFAULTS = adminPreferences;
function t(key){ return labels[key] || key; }
function dashboardWidgetLabel(id) { return t(ADMIN_DASHBOARD_BY_ID.get(id)?.label_key || id); }
function normalizeDashboardWidgets(value){return Array.isArray(value)?value:order;}
function normalizeHiddenDashboardWidgets(value){return Array.isArray(value)?value:[];}
function applyDashboardLayout(order,hidden){ window.__state={order,hidden}; }
async function saveAppearancePreference(next){ Object.assign(adminPreferences,next);return {}; }
'''.replace('__TRANSLATIONS__',json.dumps(translations,ensure_ascii=False)).replace('__REGISTRY__',json.dumps(registry))
with sync_playwright() as p:
browser=p.chromium.launch(executable_path=shutil.which('chromium') or shutil.which('chromium-browser'),args=['--no-sandbox'],headless=True)
for width,height in [(1500,950),(393,850)]:
page=browser.new_page(viewport={'width':width,'height':height},device_scale_factor=1)
page.set_content('<!doctype html><html data-theme="dark" lang="pl"><head><style>'+css+'</style></head><body><main style="margin:0; padding:28px 16px; width:100%; max-width:100%; box-sizing:border-box">'+source+'</main></body></html>',wait_until='domcontentloaded')
page.add_script_tag(content=stub+code+'\nrenderDashboardWidgetSettings();')
page.wait_for_timeout(90)
metrics=page.evaluate('''() => {
const list=document.getElementById('dashboard-widget-list'), section=document.querySelector('.appearance-section');
const parent=list.closest('.dashboard-widget-settings');
return { total: list.children.length, listHeight:list.getBoundingClientRect().height,
listWidth:list.getBoundingClientRect().width, contentWidth:list.scrollWidth,
sectionHeight:section.getBoundingClientRect().height,
overflowX:getComputedStyle(list).overflowX,
firstWidth:list.firstElementChild.getBoundingClientRect().width,
density: document.querySelector('.dashboard-density-setting').getBoundingClientRect().width,
widget:parent.getBoundingClientRect().width,
viewportOverflow:document.documentElement.scrollWidth>window.innerWidth,
nextDisabled:document.getElementById('dashboard-widget-scroll-next').disabled};
}''')
print(width,'COMPACT',metrics)
assert metrics['total']==len(registry)
assert metrics['listHeight']<66,(width,metrics)
assert metrics['contentWidth']>metrics['listWidth'],(width,metrics)
assert metrics['sectionHeight'] < (320 if width>700 else 450), (width,metrics)
assert not metrics['viewportOverflow']
page.locator('#dashboard-widget-scroll-next').click()
page.wait_for_timeout(450)
pos=page.evaluate('document.getElementById("dashboard-widget-list").scrollLeft')
print(width,'scroll after next:',pos)
assert pos>10
page.locator('.dashboard-widget-setting').nth(7).locator('button.dashboard-widget-toggle').click(force=True)
page.wait_for_timeout(60)
state=page.evaluate('window.__state')
assert state and len(state['order'])==len(registry)
retained=page.evaluate('document.getElementById("dashboard-widget-list").scrollLeft')
assert retained>=pos-15,(pos,retained)
page.locator('#dashboard-widget-expand').click()
page.wait_for_timeout(60)
expanded=page.evaluate('''() => { const list = document.getElementById("dashboard-widget-list"); return { height:list.getBoundingClientRect().height, mode:getComputedStyle(list).display, count:list.children.length, expanded:document.getElementById('dashboard-widget-expand').getAttribute('aria-expanded'), prevHidden:document.getElementById('dashboard-widget-scroll-prev').hidden } }''')
print(width,'EXPANDED',expanded)
assert expanded['mode']=='grid'
assert expanded['expanded']=='true' and expanded['prevHidden']
assert expanded['height']>metrics['listHeight']*3
page.locator('#dashboard-widget-expand').click()
assert page.locator('#dashboard-widget-expand').get_attribute('aria-expanded')=='false'
outfile=str(root.parent / f'dashboard_appearance_0326_preview_{width}.png')
page.locator('.appearance-section').screenshot(path=outfile)
print('screenshot',outfile)
page.close()
browser.close()
@@ -0,0 +1,36 @@
// Appearance ribbon: compact by default, with full-width expansion and the same
// persisted visibility/reorder controls. No browser or Rust service required.
import assert from 'node:assert/strict';
import fs from 'node:fs';
const html = fs.readFileSync('src/admin/web/index.html', 'utf8');
const css = fs.readFileSync('src/admin/web/style.css', 'utf8');
const js = fs.readFileSync('src/admin/web/app/preferences.js', 'utf8');
const registry = JSON.parse(fs.readFileSync('src/admin/web/dashboard-widgets.json', 'utf8'));
const locales = ['en_US', 'pl_pl'].map(id => JSON.parse(fs.readFileSync(`langs/${id}.json`, 'utf8')).translations);
for (const id of ['dashboard-widget-list', 'dashboard-widget-expand', 'dashboard-widget-scroll-prev',
'dashboard-widget-scroll-next', 'overview-layout-open-settings', 'ui-dashboard-density']) {
assert(html.includes(`id="${id}"`), `missing ${id}`);
}
assert.match(html, /class="appearance-settings-grid dashboard-appearance-grid"/);
assert.match(css, /\.appearance-settings-grid\.dashboard-appearance-grid\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/s,
'dashboard setting layout must use the full available width');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\s*\{[^}]*flex-wrap:\s*nowrap;[^}]*overflow-x:\s*auto;/s,
'the default card selector must be a horizontal ribbon, not a tall two-column list');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\.is-expanded\s*\{[^}]*display:\s*grid;/s);
assert.match(css, /\.dashboard-widget-settings-controls > \.dashboard-widget-scroll-button/);
assert(js.includes('list.scrollLeft = previousScroll'), 'rebuilding settings must preserve horizontal scroll position');
assert(js.includes('focusedId') && js.includes('preventScroll: true'), 'keyboard focus must survive updates');
assert(js.includes('saveAppearancePreference({ dashboard_hidden_widgets:') &&
js.includes('saveAppearancePreference({ dashboard_widgets:'), 'both APIs must retain persistence');
assert(js.includes('const up = el("button", "←"') && js.includes('const down = el("button", "→"'),
'horizontal order controls must align with the ribbon');
assert.equal(registry.length, 62, 'all configured Overview cards must remain available');
for (const translation of locales) {
for (const key of ['dashboardWidgetsShowAll', 'dashboardWidgetsCompact', 'dashboardWidgetsScrollBack',
'dashboardWidgetsScrollNext', 'dashboardWidgetsScrollHelp',
'dashboardWidgetsMoveEarlier', 'dashboardWidgetsMoveLater']) {
assert(translation[key], `missing ${key}`);
}
}
console.log('PASS: full-width compact Appearance ribbon with expandable 62-card layout, keyboard focus and preference persistence');
@@ -100,8 +100,10 @@ assert(ids.every(id => cards.get(id).hidden), 'hiding cards must still work');
assert(!cards.get('requests').hidden, 'legacy widgets must retain their visibility'); assert(!cards.get('requests').hidden, 'legacy widgets must retain their visibility');
// Native title tooltips do not participate in layout or create horizontal scroll. // Native title tooltips do not participate in layout or create horizontal scroll.
assert.doesNotMatch(css, /dashboard-widget-(?:description|setting-label-block)/, 'obsolete custom tooltip styles must be removed'); assert.doesNotMatch(css, /dashboard-widget-(?:description|setting-label-block)/, 'obsolete custom tooltip styles must be removed');
assert.match(css, /grid-template-columns: repeat\(auto-fit, minmax\(min\(260px, 100%\), 1fr\)\)/, assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\s*\{[^}]*overflow-x: auto;/s,
'effective Appearance card columns must preserve a readable name and right-aligned arrows'); 'Appearance must use a compact horizontal ribbon instead of long half-width columns');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list > \.dashboard-widget-setting\s*\{[^}]*min-width: 226px;/s,
'Appearance ribbon must retain readable labels and right-aligned reorder buttons');
assert.doesNotMatch(prefsSource, /role", "tooltip"|aria-describedby|dashboard-widget-description/, assert.doesNotMatch(prefsSource, /role", "tooltip"|aria-describedby|dashboard-widget-description/,
'old popover DOM and associations must be removed'); 'old popover DOM and associations must be removed');
const widgetList = { children: [], replaceChildren(...children) { this.children = children; } }; const widgetList = { children: [], replaceChildren(...children) { this.children = children; } };