v0.32.6_official

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-14 13:04:06 +02:00
1 parent fc8711d4d8
commit d2c6cf59a9
16 files changed
+1801 -1027

No files matched your search

+22 -12
View File
@@ -5,7 +5,26 @@
## 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;
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
@@ -14,7 +33,7 @@
for reported WebKit jumps near Process/Network; physical iOS 27 validation is
still required. Match the Origin protocol badge's radius to neighboring
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
suspended, minimized or restored from BFCache. Treat resumed/overdue
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,
vhost and Stream charts without adding polling or changing System-info
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
and avoiding scroll-anchor loss around the Process and Network panels on touch
browsers. Keep network-interface history advancing on every live Overview update
@@ -46,8 +65,6 @@
## 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.
- Refresh A/AAAA answers asynchronously every 60 seconds and after config
@@ -60,13 +77,6 @@
## 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
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
+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
- **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
Overview/protocols, optional wide/small charts, backend and Stream/Vhost
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
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
during scrolling. Network-interface chart history advances at the Overview
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).
- **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.
## Previously in 0.32.0
+15 -13
View File
@@ -2,17 +2,17 @@
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
a402ee07826dfab1b1d63b98c4a9f75809f056b908c92abacf0bba816d7ad133 AGENTS.md
54383ec0610f11c61ef64d3cf0afa63f5667b212c03db653f37a9277a637df8c CHANGELOG.md
e4a565f39f4ecca622f0eb27822c7399e45b4c4d1a2fd1f669185d53196a969b CHANGELOG.md
934ec0e7f8a307b00319606a9c5d7e91dac6449a23dd1f3abcefe4304eb4de06 Cargo.lock
8a7606650ce03f1faf36c893a17ccf1c9e8da4259e0248ac2c9bf030a64872d1 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
dc35bafda41c2e0db266523f553d0803740c5e842edfbdd7af202041c53aa359 README.md
0f8cedf595f57f96644e8d4dc608b43e6ebd9a73b31157a210d624492a6c5206 README.md
da519345b2bce492e747c268df2730971543d1bdb198c3e854671a7d107e5609 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
c51ac385fa992cdcfade11aba0151e20f1d2d01362deac7c04e585dfbc287f19 dev.sh
26a607151fd343a59c6f147e70897c4b1bfeab376348f3270eb8a373503d7be5 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -61,9 +61,9 @@ d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.m
ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
46d3cb5a2b5909902c85de193bc5129c751c5ebd19dc752db282588d54b5e25c docs/SETTINGS.md
a41cef4a4da5e098fe3e9dfa0fffb3625f2e09792eacdf5dbf8d8fd532146d26 docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
81997f880b89ea68b2820f47a83db030425e91ef5da90d1bd459754a1eb63f6c docs/TEST_REPORT.md
130befc0f8216214567dd4b2df2574ea751b56b22c8f2cc3ba48e8d0aaccc3b5 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
@@ -95,8 +95,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
fc6695db8c2a9896dc97642ef70d56de86657521d0f4e1f2f57b99ac97c4f2dc langs/en_US.json
a65d977f8380113d3ec7db5d51bffe50b87f5d93c5b139371f2351093b547216 langs/pl_pl.json
a1f873bfe87d090bac46962598aeb90ddfa62ae257b3b76ec1af011c74197bac langs/en_US.json
ca95ffe6919c07699155d1c49fa711916f91f0fbe928e5a36be23f05519ffbbb langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -182,10 +182,10 @@ dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/
e5928664c259feab3e9cfd3be69d05571f3aa60445ca373560f50857ecc8bfc0 src/admin/web/app/overview-extra-metrics.js
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.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
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
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
fbd2c402828088695f5f5f4b210d1ea8da24ae6425f61248c82c5b07b14afd56 src/admin/web/app/stream-dashboard.js
@@ -213,7 +213,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.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
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
@@ -223,7 +223,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
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
69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.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
fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py
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
f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_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
526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs
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
7508f232a9e48049002dd4c9f34f9737bc92d304c51219f50430578dbc721855 tools/checks/dashboard_inline_layout_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
37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py
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
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.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/admin_themes_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/overview_extra_metrics_0323_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).
## 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)
**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
## 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
- 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.",
"dashboardWidgets": "Overview cards",
"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",
"moveUp": "Move up",
"moveDown": "Move down",
+7
View File
@@ -2210,6 +2210,13 @@
"dashboardDensityHelp": "Steruje rozmiarem kafelków telemetrii w Overview bez zmiany reszty interfejsu.",
"dashboardWidgets": "Kafelki Overview",
"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",
"moveUp": "Przenieś wyż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 grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
// Re-appending an already ordered live grid can break touch-scroll
// anchoring in mobile browsers. Move the DOM only on a real order change.
const orderedNodes = normalizedOrder.map(id => nodes.get(id)).filter(Boolean);
for (const node of orderedNodes) node.hidden = hiddenSet.has(node.dataset.dashboardWidget);
const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")];
@@ -129,23 +127,25 @@ function dashboardWidgetTypeBadge(id) {
}
function renderDashboardWidgetSettings() {
const list = $("dashboard-widget-list"); if (!list) return;
const previousScroll = list.scrollLeft;
const focusedRow = document.activeElement?.closest?.("[data-dashboard-widget-setting]");
const focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null;
const focusedAction = focusedId ? document.activeElement?.dataset.dashboardWidgetAction : null;
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
const rows = order.map((id, index) => {
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
const visible = !hidden.has(id);
const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.dataset.dashboardWidgetAction = "toggle"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
const descriptionKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
if (descriptionKey) {
const description = t(descriptionKey);
row.title = description;
toggle.setAttribute("aria-description", description);
}
// Type badges belong in the visual layout editor, not the long
// Appearance settings list where they crowd the visibility controls.
toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
const actions = el("span", undefined, "dashboard-widget-setting-actions");
const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp"));
const down = el("button", "↓", "quiet"); down.type = "button"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("moveDown"));
const up = el("button", "←", "quiet"); up.type = "button"; up.dataset.dashboardWidgetAction = "up"; up.disabled = index === 0; up.setAttribute("aria-label", t("dashboardWidgetsMoveEarlier"));
const down = el("button", "→", "quiet"); down.type = "button"; down.dataset.dashboardWidgetAction = "down"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("dashboardWidgetsMoveLater"));
toggle.addEventListener("click", () => {
const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id);
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings();
@@ -160,6 +160,46 @@ function renderDashboardWidgetSettings() {
up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(toggle, actions); return row;
});
list.replaceChildren(...rows);
list.scrollLeft = previousScroll;
if (focusedId && focusedAction) {
const row = [...list.children].find(item => item.dataset.dashboardWidgetSetting === focusedId);
row?.querySelector(`[data-dashboard-widget-action="${focusedAction}"]`)?.focus({ preventScroll: true });
}
syncDashboardWidgetListView();
if (typeof requestAnimationFrame === "function") requestAnimationFrame(syncDashboardWidgetScrollButtons);
}
let dashboardWidgetListExpanded = false;
function syncDashboardWidgetListView() {
const list = $("dashboard-widget-list"), expand = $("dashboard-widget-expand");
if (!list || !expand) return;
list.classList.toggle("is-expanded", dashboardWidgetListExpanded);
expand.setAttribute("aria-expanded", String(dashboardWidgetListExpanded));
const key = dashboardWidgetListExpanded ? "dashboardWidgetsCompact" : "dashboardWidgetsShowAll";
expand.dataset.i18n = key;
expand.textContent = t(key);
for (const id of ["dashboard-widget-scroll-prev", "dashboard-widget-scroll-next"]) {
const button = $(id); if (button) button.hidden = dashboardWidgetListExpanded;
}
syncDashboardWidgetScrollButtons();
}
function syncDashboardWidgetScrollButtons() {
const list = $("dashboard-widget-list"); if (!list) return;
const previous = $("dashboard-widget-scroll-prev"), next = $("dashboard-widget-scroll-next");
if (previous) previous.disabled = dashboardWidgetListExpanded || list.scrollLeft <= 1;
if (next) next.disabled = dashboardWidgetListExpanded || list.scrollLeft + list.clientWidth >= list.scrollWidth - 1;
}
$("dashboard-widget-expand")?.addEventListener("click", () => {
dashboardWidgetListExpanded = !dashboardWidgetListExpanded;
syncDashboardWidgetListView();
});
$("dashboard-widget-list")?.addEventListener?.("scroll", syncDashboardWidgetScrollButtons, { passive: true });
window.addEventListener?.("resize", syncDashboardWidgetScrollButtons);
for (const [id, direction] of [["dashboard-widget-scroll-prev", -1], ["dashboard-widget-scroll-next", 1]]) {
$(id)?.addEventListener("click", () => {
const list = $("dashboard-widget-list");
if (!list) return;
list.scrollBy({ left: direction * Math.max(160, list.clientWidth * .8), behavior: "smooth" });
});
}
function refreshChartStylePalette(active = adminPreferences?.chart_style || "default") {
const target = $("chart-style-palette"); if (!target || !languagePacks.size) return;
+11 -16
View File
@@ -1,4 +1,3 @@
function verifyAdminModules() {
const required = [
["rustProxyIntegrations", "bindings"],
@@ -12,7 +11,6 @@ function verifyAdminModules() {
const missing = required.find(([module, method]) => typeof window[module]?.[method] !== "function");
if (missing) throw new Error(`Panel resources incomplete (${missing[0]}). Reload to retry.`);
}
async function startAdminPanel() {
try {
verifyAdminModules();
@@ -33,7 +31,6 @@ async function startAdminPanel() {
}
if (document.readyState === "complete") startAdminPanel();
else document.addEventListener("DOMContentLoaded", startAdminPanel, { once: true });
function certificateUsesExternalFiles(cert) {
return String(cert?.cert_file || "").startsWith("/") && String(cert?.key_file || "").startsWith("/");
}
@@ -53,7 +50,6 @@ function syncCertificateFileSource() {
$("certificate-external-acl-example").hidden = !external;
}
$("certificate-file-source").addEventListener("change", syncCertificateFileSource);
function renderCertificates() {
if (!envelope) return;
const settings = envelope.config.tls || {};
@@ -69,7 +65,6 @@ function renderCertificates() {
if (settings.default_certificate === cert.id) used.push(t("defaultLabel"));
const managed = !!cert.issuer, inUse = used.length > 0;
const box = el("div", undefined, "entity certificate-card");
const head = el("div", undefined, "certificate-card-head");
const identity = el("div", undefined, "certificate-card-identity");
identity.append(el("span", cert.id, "entity-title"));
@@ -152,7 +147,7 @@ $("reload-certificates").addEventListener("click", async () => {
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
syncCertificateFileSource();
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null,
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, lastCacheOutcomeRates = null,
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
const hiddenNetworkInterfaces = (() => {
try {
@@ -206,8 +201,7 @@ function svgNode(tag, attrs = {}) {
for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
return node;
}
// Diagnostic charts keep their SVG and label nodes across samples. Updating the
// existing Text node avoids remove/insert mutations while the user is scrolling.
function setChartText(node, value) {
const text = String(value);
if (node.firstChild?.nodeType === 3 && node.firstChild === node.lastChild) {
@@ -216,8 +210,7 @@ function setChartText(node, value) {
node.textContent = text;
}
}
// Preserve stable Overview DOM while live samples arrive: replacing network SVGs
// during an iOS WebKit momentum scroll can invalidate the current scroll anchor.
let latestNetworkInterfaceSnapshot = [];
function renderNetworkInterfaceCharts(interfaces, appendSample) {
const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : [];
@@ -305,8 +298,7 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
children.push(empty);
}
}
// The common live-sample path updates only text and SVG path attributes.
// Reparent rows solely when interfaces or visibility have actually changed.
if (children.length !== target.children.length || children.some((node, index) => node !== target.children[index])) {
target.replaceChildren(...children);
}
@@ -316,7 +308,6 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
$("metric-h3").textContent = fmt(m.http3_requests);
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
// Slow details vs cached live chart telemetry.
if (systemSample) {
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
$("process-summary").hidden = d.process_threads == null;
@@ -358,9 +349,13 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
const currentCounters = [c.hits, c.misses, c.bypasses].map(Number);
if (currentCounters.every(Number.isFinite)) {
const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
if (delta.deltas || !lastCacheCounters) { const values = delta.deltas || [0, 0, 0];
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]); cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
} lastCacheCounters = delta.next;
const values = delta.deltas || lastCacheOutcomeRates || (!lastCacheCounters ? [0, 0, 0] : null);
if (values) {
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]);
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
lastCacheOutcomeRates = [...values];
}
lastCacheCounters = delta.next;
}
}
if (appendSample) {
+35 -11
View File
@@ -2477,8 +2477,9 @@
data-appearance-reset="dashboard" data-i18n="resetSection"
type="button">Reset section</button>
</div>
<div class="appearance-settings-grid">
<label><span data-i18n="dashboardDensity">Dashboard density</span><select
<div class="appearance-settings-grid dashboard-appearance-grid">
<label class="dashboard-density-setting"><span
data-i18n="dashboardDensity">Dashboard density</span><select
id="ui-dashboard-density">
<option data-i18n="densityNormal" value="normal">Normal</option>
<option data-i18n="densityCompact" value="compact">Compact</option>
@@ -2486,15 +2487,38 @@
</select><small class="fine" data-i18n="dashboardDensityHelp">Controls
Overview telemetry card size without changing the rest of the
interface.</small></label>
<div class="dashboard-widget-settings"><span
data-i18n="dashboardWidgets">Overview cards</span><small class="fine"
data-i18n="dashboardWidgetsHelp">Show, hide and reorder the telemetry
cards at the top of Overview.</small>
<button class="outline dashboard-layout-open-settings" type="button"
id="overview-layout-open-settings"
data-i18n="overviewLayoutEditorOpenSettings">Visual layout
editor</button>
<div class="dashboard-widget-list" id="dashboard-widget-list"></div>
<div class="dashboard-widget-settings">
<div class="dashboard-widget-settings-head">
<div class="dashboard-widget-settings-intro">
<span data-i18n="dashboardWidgets">Overview cards</span>
<small class="fine" data-i18n="dashboardWidgetsHelp">Show, hide and
reorder all Overview blocks, including charts, diagnostics and
the backend table.</small>
</div>
<div class="dashboard-widget-settings-controls">
<button class="outline dashboard-layout-open-settings" type="button"
id="overview-layout-open-settings"
data-i18n="overviewLayoutEditorOpenSettings">Visual layout
editor</button>
<button class="quiet" id="dashboard-widget-expand" type="button"
aria-expanded="false" aria-controls="dashboard-widget-list"
data-i18n="dashboardWidgetsShowAll">Show all</button>
<button class="quiet dashboard-widget-scroll-button"
id="dashboard-widget-scroll-prev" type="button"
data-i18n-aria="dashboardWidgetsScrollBack"
aria-label="Scroll cards left">&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
opt-in and only change Dashboard presentation, not Stream routing or
monitoring.</small>
+1461 -963
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-input').evaluate('(n) => getComputedStyle(n).borderTopLeftRadius') == expected_control, (theme, style, 'input')
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')
page.evaluate('document.documentElement.dataset.theme = "ultra_dark"')
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');
// 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.match(css, /grid-template-columns: repeat\(auto-fit, minmax\(min\(260px, 100%\), 1fr\)\)/,
'effective Appearance card columns must preserve a readable name and right-aligned arrows');
assert.match(css, /\.dashboard-appearance-grid \.dashboard-widget-list\s*\{[^}]*overflow-x: auto;/s,
'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/,
'old popover DOM and associations must be removed');
const widgetList = { children: [], replaceChildren(...children) { this.children = children; } };