This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 13:00:44 +02:00
1 parent 5a36bb0c4b
commit 78ad28ef4d
27 files changed
+1448 -234

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.33.2. compatibility. Current source version: 0.33.3.
## Delivery ## Delivery
+31
View File
@@ -3,6 +3,37 @@
> Operator-facing release history. Historical engineering notes are kept in > Operator-facing release history. Historical engineering notes are kept in
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.33.3
- Make main sidebar navigation highlight depend on the Appearance **interface
theme** rather than Chart style. Selected/hover backgrounds reuse theme nav
tokens; active text and the selection marker use readable theme colors.
Preserve corner rounding, mobile navigation and RTL selection indicators.
Browser-test palette independence across four themes, eight chart palettes
and five corner settings.
- Repair **Arrange Overview cards** modal after introducing variable card widths:
use a readable three/two/one-column editor preview rather than squeezing six
2/12-width cards and overflowing controls into a 12-column mini-dashboard.
Width buttons still edit true Overview spans, while grouped toolbar actions,
hidden-card controls and modal footer wrap without horizontal scrolling.
Add a Chromium regression for Polish labels, card controls and narrow screens.
- Fix Overview chart content not filling resized cards: preserveAspectRatio=none
on time-series SVGs (including dynamically created network interface charts),
with non-scaling strokes so data fills the width without distorting donuts.
No sample frequency, persistence or API changes.
- Add 2/3/4/6/12-column width controls to both Overview layout editors.
Small number/sparkline tiles can grow to double width and full-size charts
can shrink to 3/12, allowing four charts per row on wide screens.
- Remove old cross-type drag restrictions after entering free layout: cards
can move between the top numeric area, compact charts and diagnostic panels.
Preserve legacy grouped layouts for existing saved preferences until edited.
- Save card widths in account preferences with strict widget/type validation,
preserve the existing factory preset and hide/show state, and restore widths
on cancel or when resetting to defaults. No DB schema migration.
- Use responsive dense CSS Grid layout and add real-browser resize, ordering,
modal editing, save/cancel and compatibility regression coverage.
## 0.33.2 ## 0.33.2
- Synchronize Cache occupancy progressbar colors with the selected Appearance - Synchronize Cache occupancy progressbar colors with the selected Appearance
Generated
+1 -1
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.33.2" version = "0.33.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rust-proxy" name = "rust-proxy"
version = "0.33.2" version = "0.33.3"
edition = "2021" edition = "2021"
rust-version = "1.99" rust-version = "1.99"
description = "RustProxy - independent reverse proxy with local administration" description = "RustProxy - independent reverse proxy with local administration"
+41 -1
View File
@@ -1,4 +1,44 @@
# RustProxy 0.33.2 # RustProxy 0.33.3
## 0.33.3 sidebar appearance follow-up
- Main navigation takes active/hover backgrounds and selection colors from the
selected **interface theme**, not the chart color palette. Dark and ultra-dark
use theme brand tones; light and cream blend theme brand/text for contrast.
Switching the chart palette no longer changes sidebar or mobile-menu highlighting.
- Sidebar navigation and the mobile menu follow the existing Appearance → Corner
rounding setting. Changes take effect live with the current preferences; no
additional JavaScript, API changes or preference migrations are required.
## 0.33.3: resizable Overview dashboard and free card placement
- **Visual layout editor fix:** **Arrange Overview cards** uses uniformly sized,
readable preview tiles (three columns on desktop, two on tablets, one on
phones), not the 12-column canvas used by the actual Overview. Its `2/12`
through `12/12` width controls still resize the real dashboard cards.
Toolbars and hidden-card actions wrap cleanly without horizontal modal scroll;
dragging, hiding, Save/Cancel and factory reset work as before.
- Fixed time-series SVG viewport scaling after widening cards. All Overview line plots
(numeric sparklines, full-width charts and diagnostic/network interface charts)
now span their available width while keeping their vertical sizing and stroke
thickness; donut charts remain circular. No new status polling is required.
- In **Unlock layout** or **Arrange cards visually**, use − / + to change a
card's width in the 12-column dashboard grid. Available widths are 2, 3, 4,
6 and 12 columns, restricted by card type for readability. **3 columns fits
four graphs in one row** on a desktop canvas wider than 1080px. Even the
smallest number/sparkline cards can be expanded to 4 columns (twice their
original width) and moved to the very top, beside regular charts.
- The first manual move or resize enables a freely ordered layout, with no
enforced separation between numbers, small charts, wide charts or panels.
Responsive widths clamp to six/two columns on smaller canvases; dense grid
packing reduces empty gaps without changing chart sampling or polling.
- Card sizes are saved with the account's existing Overview preferences in
nullable `dashboard_widget_sizes`; **null retains 0.33.2 group ordering**
for older saved layouts, while `{}` enables free order with normal widths.
Save, cancel, factory reset and modal preview include dimensions. Existing
per-account layouts, factory visibility and 600-sample default are preserved.
No SQLite migration or extra status fetches are needed. Runtime Rust acceptance
remains required.
## 0.33.2: unified progressbar appearance ## 0.33.2: unified progressbar appearance
+22 -1
View File
@@ -1,5 +1,27 @@
# RustProxy roadmap # RustProxy roadmap
## 0.33.3 - Adjustable Overview card widths and cross-type ordering
- [x] Apply Appearance **theme** highlighting and corner radius to sidebar
navigation, including mobile layout and accessible contrast on light themes;
keep menu colors independent of the chart palette.
- [x] Test navigation colors in Chromium across 4 themes and 8 chart palettes
(invariance within each theme), 5 corner settings, hover, mobile and RTL.
- [x] Keep the visual layout editor readable at 320-1920px with stable
three/two/one-column previews, wrapping controls and no horizontal overflow;
leave actual dashboard widths and free ordering unchanged.
- [x] Stretch plotted SVG history across the actual card width when resizing,
including dynamic network-interface plots, without stretching donut charts.
- [x] Allow widths of 2, 3, 4, 6 and 12/12 columns in inline and visual editors;
keep the smallest text/sparkline cards expandable and add a 4-chart desktop row.
- [x] Allow free card placement across former size groups, including at the
top of Overview, and reduce unused row gaps with CSS dense packing.
- [x] Preserve legacy grouped layouts until edited, retain saved hidden/order
preferences, and support cancel, save, modal preview and reset.
- [x] Add browser coverage for real move/size clicks and responsive layouts.
## 0.33.2 - Shared progressbar visual settings ## 0.33.2 - Shared progressbar visual settings
- [x] Apply Appearance corner rounding to native Cache progressbars and - [x] Apply Appearance corner rounding to native Cache progressbars and
@@ -61,7 +83,6 @@
- [x] On phone-size Overview, remove the nested Network interface chart scroll - [x] On phone-size Overview, remove the nested Network interface chart scroll
region, while retaining chart SVG/text nodes on ordinary live updates. region, while retaining chart SVG/text nodes on ordinary live updates.
## 0.32.6 ## 0.32.6
- [x] Reinstate two small Overview cards per row across phone-size widths. - [x] Reinstate two small Overview cards per row across phone-size widths.
+26 -22
View File
@@ -1,13 +1,13 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
78027147942d2a355ad3bb92492faf70917908df879b10330bb9e42edfbd5d11 AGENTS.md 676a30c813406f4e17972ee8bea175a7e3d5ee282158283a9cbe920f2b65da7f AGENTS.md
9a5f4c3478b4fba206fc8a3d6616f86b2cf2d640ee51f935aa5b0baeb9c0d7b7 CHANGELOG.md 3a6bb0fdf41ef4519beefa9f4d9c124a4883c304e49e6a76bb64cd5333ffb174 CHANGELOG.md
53338adb66b8c6c345d742f1a8fca48b7965e70ee072b2f4069020d3d73f1be7 Cargo.lock 1a59436e03172328606fc0c2142da05319e7673ae2a87197409a06aa22d87b27 Cargo.lock
5ab876b82db0e76ae52ac1723555ebeef9182e1feedabd0a72f936d0940e4373 Cargo.toml 551040f53df829264a6784c9a2ab4fe2cb4299d5d4f517857a74ee58797ab5b7 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
f73f11aade119401aff40f7929f08b9f8f889fbf89d0cd8be7a1efd69487fffd README.md 6aa95661d76c2ce0f317fe7004791568dbcc552ccff87fe6bdd2f0f235d6ef7c README.md
55a71d11fb998cf31874d1e7669175b51251f38f48349538b94b03e27283726c ROADMAP.md 97becee1b75d0dde90dd412e8fdb8aa68fd11c5e1872bc15095ac6632b2ea0e9 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
@@ -35,7 +35,7 @@ fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md
3822d72e12f82c37a9e70ab40c7e89e89145ad37f29914e4702db4bf024a5501 docs/CHANGELOG_TECHNICAL_ARCHIVE.md 3822d72e12f82c37a9e70ab40c7e89e89145ad37f29914e4702db4bf024a5501 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md
833ddb12f23dcac796223521388b19c6ec3ea04c5550cf1c5f4ea4eb7c58f3a0 docs/CUSTOM_LOCATIONS.md 833ddb12f23dcac796223521388b19c6ec3ea04c5550cf1c5f4ea4eb7c58f3a0 docs/CUSTOM_LOCATIONS.md
15153ba0eb325b374aa3a278f677900516665c2b131b55b6074ec5105924f41d docs/DASHBOARD_PRESET.md 3cbcf562114fae47f01ed4614d49844b88e7a4d8aee7eb724400d5681be90936 docs/DASHBOARD_PRESET.md
4d70797bda8d227787fcff897951e702e2458fc6c6f5352aa0b5577595bd834b docs/DOCKER_DISCOVERY.md 4d70797bda8d227787fcff897951e702e2458fc6c6f5352aa0b5577595bd834b docs/DOCKER_DISCOVERY.md
46811a09a03d83f4af686c33e30e3f94d0508afe2c75147a26885c7247c49cbd docs/ERROR_LOGGING.md 46811a09a03d83f4af686c33e30e3f94d0508afe2c75147a26885c7247c49cbd docs/ERROR_LOGGING.md
f749f7bd2eb4ae9c484f77a3cd8cd5ff21445319a3938e0384a3414134fe713a docs/FILE_LOGS.md f749f7bd2eb4ae9c484f77a3cd8cd5ff21445319a3938e0384a3414134fe713a docs/FILE_LOGS.md
@@ -64,7 +64,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md 007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md 4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md 8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
87279fa139a62cca82a4c97c1edae0b97e70ed9ff09af55773faecf581be763f docs/TEST_REPORT.md e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 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
@@ -83,7 +83,7 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md 1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
cb4fe36423eca2cbc7961df04f28c6870e4303e4c16c7fe2d3f7b7bbb04d8dad grafana/rustproxy-overview.json ea8aaf0257a5ab75153ee2fd383a4d5c2ac725c47042f9770949451636d442f1 grafana/rustproxy-overview.json
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh 909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md 42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
@@ -96,8 +96,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
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md 13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
b5922257df0009d3e0778d2a08c926952fed6db6c013049e04ca8b63eaa6df6d langs/en_US.json fe738b800824e8700fb8a4796b7316f5d4882e0d43aed7e743fc996d42aa99c1 langs/en_US.json
52a3d1dcc5e2517f4906fb71b1c6d9c915036a0d69e9ceec143bb2b27582611b langs/pl_pl.json d9878b53adc3bdcf20fecb6b4ae6270158c9150e9df39186dc2c4b571d948aa2 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
@@ -130,10 +130,14 @@ e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtim
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh 174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py 046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
aa3ca7419d6d7d506c5aaf4d624d20cc7e8ee66bec3cb0cf9b5e7d164bcbab57 scripts/tests/dashboard_chart_width_0333_browser_test.py
52eacb467df123dc4a35f14933e006b090c9238df1b14dcff856a16b37c8063f scripts/tests/dashboard_layout_0331_browser_test.py 52eacb467df123dc4a35f14933e006b090c9238df1b14dcff856a16b37c8063f scripts/tests/dashboard_layout_0331_browser_test.py
276e732fc2dfb0d39badb159e4fdcba0d7d73509c60ad5c9df49621c03046f1b scripts/tests/dashboard_metric_visuals_ui_test.mjs 276e732fc2dfb0d39badb159e4fdcba0d7d73509c60ad5c9df49621c03046f1b scripts/tests/dashboard_metric_visuals_ui_test.mjs
61a0fb2c4de86f25debec9662aedd923bce8c2ccd51e75593d485b2e0c9b8551 scripts/tests/dashboard_modal_0333_browser_test.py
0c8a566979b51cd62601d138697ccd5c4ed47b8fa33e8faa28fa3a085a72ad97 scripts/tests/dashboard_resizing_0333_browser_test.py
aa12d00acdd226602b3efc90221fbc936f69f73bcabbaa6c01dfb0e05d08c505 scripts/tests/overview_samples_0330_ui_test.mjs aa12d00acdd226602b3efc90221fbc936f69f73bcabbaa6c01dfb0e05d08c505 scripts/tests/overview_samples_0330_ui_test.mjs
0ec69b7811db3dcd51cba60387900ca8df50eb836b22cc2a3ecfcc22b40a2edd scripts/tests/progressbar_0332_browser_test.py 0ec69b7811db3dcd51cba60387900ca8df50eb836b22cc2a3ecfcc22b40a2edd scripts/tests/progressbar_0332_browser_test.py
222077dab4d4928ae9110a9f2d64c07cc8386ea14099819b43304bbd2364e5b5 scripts/tests/sidebar_nav_0333_browser_test.py
d5f675b97a85220043a9475347412972db904e559d863ce5058aa18f649f9e16 scripts/tests/system_ram_meter_0332_browser_test.py d5f675b97a85220043a9475347412972db904e559d863ce5058aa18f649f9e16 scripts/tests/system_ram_meter_0332_browser_test.py
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py 046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py
@@ -166,13 +170,13 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs 74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs 33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
fc07433afa4c152e6ad5cc95676a0727fdb8a48e04a22c2b761e4a4f125a9e93 src/admin/openapi.json 8e20a424d5e251ac713baaee063d9dfddcfe06063ee68eb9eac37bf9215c7664 src/admin/openapi.json
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs 30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs 75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
b0f0c1799439f596fbd3c68126fb1ea229c75dfa3ebb48abff1cd5da355e993f src/admin/preferences.rs 92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs
a02e17edd5eeb3a842658235af0a73b66e70e870e725ff9b673b76de840e42fc src/admin/preferences_columns.rs 4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs
908ef7bb71496a3cb2a565147a427d61026bb4d01d804d76bcb10f071b9122a3 src/admin/preferences_tests.rs 9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs 055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs 7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs 9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
@@ -187,16 +191,16 @@ cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail
c0d08c5b8fc13dec9bf53fd842d224b9df3874fc3bdf31413e3ab182a8cec2aa src/admin/web/app/backend-dashboard.js c0d08c5b8fc13dec9bf53fd842d224b9df3874fc3bdf31413e3ab182a8cec2aa src/admin/web/app/backend-dashboard.js
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js 8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js
e71f77d459d7894f6a0920e9adbe6393c85ed3113e40582fde0c814561da5fd3 src/admin/web/app/dashboard-inline-layout.js f3b566d79ba47848021a74653dbd7fec72f0fb819c6b2a07259e047bc8c427fd src/admin/web/app/dashboard-inline-layout.js
dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/app/dashboard-layout-editor.js 851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.js
2f1669cbeb9f49aef43a548e2096825aa14c64c7097ffad520822bbbaefb2b4c src/admin/web/app/overview-extra-metrics.js 2f1669cbeb9f49aef43a548e2096825aa14c64c7097ffad520822bbbaefb2b4c src/admin/web/app/overview-extra-metrics.js
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js 5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js
b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js
107b010a64ecc4135d610eecf4f8d4fb12329806003d286b4328e31e9e3d859f src/admin/web/app/overview-wide-metrics.js 107b010a64ecc4135d610eecf4f8d4fb12329806003d286b4328e31e9e3d859f src/admin/web/app/overview-wide-metrics.js
fb6c48b2367f9e1bfeced2cbb4362d1d85fe1ca0c1627c4660b4d4475ea70977 src/admin/web/app/preferences.js 5bde5f0881320464eb133cd44de9eff5211e6816e79f7f7e9574b16784fab126 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
48896f36d282a2ec34b50052023146d712799b721033c895e43501992659966b src/admin/web/app/runtime.js daa20d634f921191cb4ac37889504f203ab39ad7e8fd8f33e1b182fc4206c24d 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
8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js 8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js
@@ -226,7 +230,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
cd9505a1d30df205540a905235f3f78bd176ab7bba8f83f760394bba63d86082 src/admin/web/index.html da102bf2c797c0614e964f535908e89064abd75172395bbca98da68b5e4edcc3 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
@@ -236,7 +240,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
b6335c26d05f41aa2da33dc045e117e128db84b1c2036e1c0d3b2308df9b8ab0 src/admin/web/style.css 3a869db8f9cdf23eef6c99a52b892f14adf2210bce7f153351341247a878024c 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
+29 -2
View File
@@ -1,4 +1,4 @@
# Factory Overview dashboard preset (0.33.1) # Factory Overview dashboard preset (0.33.3)
`src/admin/web/dashboard-defaults.json` is the single factory configuration `src/admin/web/dashboard-defaults.json` is the single factory configuration
for the Rust admin preference API and the authenticated JavaScript panel. for the Rust admin preference API and the authenticated JavaScript panel.
@@ -19,10 +19,37 @@ The bundled 0.33.1 preset reproduces the supplied operator layout: 78 ordered
cards, 51 hidden, compact density and 600 initial samples. The packaged cards, 51 hidden, compact density and 600 initial samples. The packaged
Web Server cards remain hidden by default. Web Server cards remain hidden by default.
## User-selected card widths (0.33.3)
The factory preset intentionally retains the original 0.33.1 widths and
legacy grouped presentation. Version 0.33.3 adds a separate nullable preference:
- `dashboard_widget_sizes: null` (including omitted in an old record): retain
previous size groups, so upgrades do not spontaneously rearrange cards.
- `dashboard_widget_sizes: {}`: freely order all card types, while leaving
their widths at the original values.
- `dashboard_widget_sizes: { "http_protocol": 4, "wide_transform_activity": 3 }`:
also override individual widths, in units of the 12-column Overview grid.
Permitted widths: 2, 3, 4, 6 and 12 columns; regular charts need at least 3,
full diagnostic panels at least 4, and the backend table at least 6. At the
largest canvas four 3-column charts fit alongside each other. On small screens
the renderer reduces to a six- or two-column grid; the saved dimensions do not
change. In **Unlock layout**, click − / + or drag cards across their old groups;
**Arrange cards visually** provides the same controls. Save persists order,
visibility and sizes in the administrator's current preference document.
**Cancel** restores the saved layout and sizes. **Restore defaults** restores
the original factory order/visibility and width defaults, but does not change
the independently saved Overview sample limit. Dense packing reduces gaps,
so when columns do not divide evenly, a later narrow tile may fill an earlier
free space without changing the saved DOM or keyboard traversal order.
## Deploying an edited preset ## Deploying an edited preset
1. Edit `src/admin/web/dashboard-defaults.json` in the source tree. 1. Edit `src/admin/web/dashboard-defaults.json` in the source tree.
2. Validate the JSON and identifiers 2. Validate the JSON and identifiers (e.g. run
`python3 scripts/tests/dashboard_layout_0331_browser_test.py`).
3. Rebuild RustProxy (`./dev.sh check` / the normal release build) and deploy 3. Rebuild RustProxy (`./dev.sh check` / the normal release build) and deploy
the newly built binary. The preset is **embedded**; editing the installed the newly built binary. The preset is **embedded**; editing the installed
source JSON alone does not update a running binary. source JSON alone does not update a running binary.
+2 -2
View File
@@ -5640,9 +5640,9 @@
}, },
"timepicker": {}, "timepicker": {},
"timezone": "browser", "timezone": "browser",
"title": "RustProxy 0.33.2 - Operations", "title": "RustProxy 0.33.3 - Operations",
"uid": "rustproxy-0170", "uid": "rustproxy-0170",
"version": 6, "version": 6,
"weekStart": "", "weekStart": "",
"description": "RustProxy 0.29.2 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic." "description": "RustProxy 0.29.2 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
} }
+5 -1
View File
@@ -2705,5 +2705,9 @@
"webLiveKinds": "Static / PHP / other", "webLiveKinds": "Static / PHP / other",
"webLiveBytes": "Body delivered", "webLiveBytes": "Body delivered",
"webLiveNoPhp": "PHP disabled" "webLiveNoPhp": "PHP disabled"
} },
"overviewLayoutNarrow": "Decrease card width",
"overviewLayoutWiden": "Increase card width",
"overviewLayoutWidth": "Card width in 12-column grid",
"overviewLayoutInlineHint": "Layout unlocked: move any card anywhere; use − / + to resize (2, 3, 4, 6 or 12 columns). Save and lock or cancel to revert."
} }
+5 -1
View File
@@ -2705,5 +2705,9 @@
"webLiveKinds": "Statyczne / PHP / inne", "webLiveKinds": "Statyczne / PHP / inne",
"webLiveBytes": "Wysłana treść", "webLiveBytes": "Wysłana treść",
"webLiveNoPhp": "PHP wyłączone" "webLiveNoPhp": "PHP wyłączone"
} },
"overviewLayoutNarrow": "Zmniejsz szerokość karty",
"overviewLayoutWiden": "Zwiększ szerokość karty",
"overviewLayoutWidth": "Szerokość karty w siatce 12 kolumn",
"overviewLayoutInlineHint": "Układ odblokowany: przenoś dowolne karty i zmieniaj ich szerokość przyciskami − / + (2, 3, 4, 6 lub 12 kolumn). Zapisz lub anuluj zmiany."
} }
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Real-browser regression for Overview chart content when card width changes.
Runs the production HTML/CSS/assembled admin JS against a simulated DOM and
fake network-interface samples. This does not exercise Rust HTTP/SSE.
"""
import json
import re
from pathlib import Path
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
HTML = (ROOT / 'src/admin/web/index.html').read_text()
CSS = (ROOT / 'src/admin/web/style.css').read_text()
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
def app_bundle():
match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
assert match
body = ROUTER[match.end():]
body = body[:re.search(r'\]\s*\.join\("\\n"\)', body).start()]
scripts = []
for line in body.splitlines():
line = line.strip()
include = re.search(r'include_str!\("([^"]+)"\)', line)
if include:
scripts.append((ROOT / 'src/admin' / include.group(1)).read_text())
elif line == 'admin_vhosts_javascript(),':
scripts.append('(() => {\n' + '\n'.join(
(ROOT / 'src/admin/web/vhosts' / (name + '.js')).read_text()
for name in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();')
elif line.startswith('"') and line.endswith('",'):
scripts.append(json.loads(line[:-1]))
return '\n'.join(scripts)
def inspect(page):
return page.evaluate('''() => {
const selectors = {
'websocket_protocol':'[data-dashboard-widget="websocket_protocol"] svg',
'wide_transform_activity':'[data-dashboard-widget="wide_transform_activity"] svg',
'www_requests':'[data-dashboard-widget="www_requests"] svg',
'traffic_chart':'[data-dashboard-widget="traffic_chart"] svg',
'system_chart':'[data-dashboard-widget="system_chart"] svg',
'cache_diagnostics':'[data-dashboard-widget="cache_diagnostics"] .mini-live-chart svg',
'network_diagnostics':'[data-dashboard-widget="network_diagnostics"] .mini-live-chart svg',
'network_interface':'[data-dashboard-widget="network_diagnostics"] .network-interface-row svg',
};
return Object.fromEntries(Object.entries(selectors).map(([name, selector]) => {
const svg = document.querySelector(selector);
if (!svg) throw new Error('Missing SVG: ' + name);
const box = svg.getBoundingClientRect();
const matrix = svg.getScreenCTM();
const viewBox = svg.viewBox.baseVal;
const contentWidth = Math.hypot(matrix.a, matrix.b) * viewBox.width;
const series = svg.querySelector('.overview-wide-series');
return [name, {width:box.width, contentWidth,
preservation:svg.getAttribute('preserveAspectRatio'),
lineStroke: series ? getComputedStyle(series).vectorEffect : null}];
}));
}''')
with sync_playwright() as browser_api:
browser = browser_api.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
for screen_width in (320, 375, 800, 1440, 1920):
page = browser.new_page(viewport={'width': screen_width, 'height': 920})
errors = []
page.on('pageerror', lambda error: errors.append(str(error)))
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
page.add_style_tag(content=CSS)
page.add_script_tag(content=app_bundle())
page.evaluate('''() => {
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
document.getElementById('workspace').hidden = false;
document.getElementById('panel-overview').hidden = false;
currentPanel = 'overview';
renderNetworkInterfaceCharts([{name:'eth0',rx_bytes_per_second:12500,tx_bytes_per_second:5200}], true);
}''')
ids = ['websocket_protocol', 'wide_transform_activity', 'www_requests', 'traffic_chart',
'system_chart', 'cache_diagnostics', 'network_diagnostics']
results = []
for span in (3, 12):
page.evaluate('''({ids,span}) => {
const hidden = ADMIN_DASHBOARD_WIDGETS.filter(id => !ids.includes(id));
const sizes = Object.fromEntries(ids.map(id => [id, span]));
applyDashboardLayout([...ids, ...hidden], hidden, sizes);
}''', {'ids': ids, 'span': span})
measured = inspect(page)
results.append(measured)
for chart, value in measured.items():
assert value['width'] > 50, (screen_width, span, chart, value)
assert abs(value['width'] - value['contentWidth']) <= 2, (
'chart not filling its own SVG viewport', screen_width, span, chart, value)
assert value['preservation'] == 'none', (screen_width, chart, value)
if value['lineStroke'] is not None:
assert value['lineStroke'] == 'non-scaling-stroke', (chart, value)
if screen_width >= 1440:
for name in results[0]:
assert results[1][name]['width'] > results[0][name]['width'] * 1.4, (
'chart does not grow when widening card', screen_width, name, results)
assert page.evaluate('document.documentElement.scrollWidth - innerWidth') <= 2, screen_width
assert not errors, (screen_width, errors)
print('PASS SVG chart uses full width at spans 3 and 12:', screen_width)
page.close()
browser.close()
print('PASS resizable Overview chart content regression')
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Regression: Overview visual layout editor fits without horizontal overflow.
Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences.
This is browser layout testing, not Rust server acceptance.
"""
import json
import re
from pathlib import Path
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
HTML = (ROOT / 'src/admin/web/index.html').read_text()
CSS = (ROOT / 'src/admin/web/style.css').read_text()
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
PACKS = {
name: {'id': name, **json.loads((ROOT / 'langs' / (name + '.json')).read_text())}
for name in ('en_US', 'pl_pl')
}
def app_bundle():
match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
assert match
body = ROUTER[match.end():]
body = body[:re.search(r'\]\s*\.join\("\\n"\)', body).start()]
result = []
for line in body.splitlines():
line = line.strip()
include = re.search(r'include_str!\("([^"]+)"\)', line)
if include:
result.append((ROOT / 'src/admin' / include.group(1)).read_text())
elif line == 'admin_vhosts_javascript(),':
result.append('(() => {\n' + '\n'.join((ROOT / 'src/admin/web/vhosts' / (name + '.js')).read_text()
for name in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();')
elif line.startswith('"') and line.endswith('",'):
result.append(json.loads(line[:-1]))
return '\n'.join(result)
BUNDLE = app_bundle()
def load(page):
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
page.add_style_tag(content=CSS)
page.add_script_tag(content=BUNDLE)
page.wait_for_function('window.rustProxyInlineLayout && window.rustProxyPreferences && window.rustProxyDashboardLayout')
page.evaluate('''packs => {
languagePacks = new Map(Object.entries(packs));
defaultLanguage = 'en_US';
language = 'pl_pl';
document.documentElement.lang = 'pl-PL';
document.querySelectorAll('#overview-layout-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
}''', PACKS)
page.evaluate('''() => {
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
document.getElementById('workspace').hidden = false;
document.getElementById('panel-overview').hidden = false;
document.getElementById('overview-layout-unlock').hidden = false;
document.getElementById('overview-layout-open').hidden = false;
currentPanel = 'overview';
window.__savedDashboard = structuredClone(ADMIN_APPEARANCE_DEFAULTS);
window.rustProxyPreferences.load = async () => window.__savedDashboard;
window.rustProxyPreferences.current = () => window.__savedDashboard;
window.rustProxyPreferences.update = async patch => {
window.__savedDashboard = {...window.__savedDashboard, ...patch};
applyAdminPreferences(window.__savedDashboard);
return window.__savedDashboard;
};
applyAdminPreferences(window.__savedDashboard);
language = 'pl_pl';
document.documentElement.lang = 'pl-PL';
document.querySelectorAll('#overview-layout-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
}''')
def geometry(page):
return page.evaluate('''() => {
const d = document.getElementById('overview-layout-dialog');
const grid = document.getElementById('overview-layout-visible');
const rect = node => node.getBoundingClientRect();
const tiles = Array.from(grid.querySelectorAll('.overview-layout-tile'));
const overflows = [];
for (const tile of tiles) {
const r = rect(tile);
const toolbar = tile.querySelector('.overview-layout-tile-toolbar');
if (tile.scrollWidth > tile.clientWidth + 2)
overflows.push([tile.dataset.previewWidget, 'tile-scroll', tile.clientWidth, tile.scrollWidth]);
for (const button of toolbar.querySelectorAll('button')) {
const b = rect(button);
if (b.left < r.left - 2 || b.right > r.right + 2)
overflows.push([tile.dataset.previewWidget, 'button-outside', button.className, b.left, b.right, r.left, r.right]);
}
const buttons = Array.from(toolbar.querySelectorAll('button'));
for (let i = 0; i < buttons.length; i++) {
const a = rect(buttons[i]);
for (let j = i + 1; j < buttons.length; j++) {
const b = rect(buttons[j]);
if (a.right - b.left > 1 && b.right - a.left > 1 && a.bottom - b.top > 1 && b.bottom - a.top > 1)
overflows.push([tile.dataset.previewWidget, 'overlapping-buttons', buttons[i].className, buttons[j].className]);
}
}
}
const hiddenList = document.getElementById('overview-layout-hidden');
for (const row of hiddenList.querySelectorAll('.overview-layout-hidden-item')) {
const r = rect(row), button = row.querySelector('button');
const b = rect(button);
if (b.right > r.right + 2 || b.left < r.left - 2)
overflows.push(['hidden card', 'button-outside', b.left, b.right, r.left, r.right]);
}
const panelPreview = grid.querySelector('[data-preview-widget="network_diagnostics"]');
const dialogRect = rect(d);
return {
cols: getComputedStyle(grid).gridTemplateColumns.split(' ').length,
panelWidth: panelPreview ? rect(panelPreview).width : null,
dialogWidth: d.clientWidth, dialogScrollWidth: d.scrollWidth,
gridWidth: grid.clientWidth, gridScrollWidth: grid.scrollWidth,
dialogLeft:dialogRect.left, dialogRight:dialogRect.right,
visibleCount:tiles.length, overflows,
firstTileWidth:rect(tiles[0]).width,
};
}''')
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
for width in (320, 375, 550, 620, 800, 900, 960, 1158, 1440, 1920):
context = browser.new_context(viewport={'width': width, 'height': 823})
page = context.new_page()
errors = []
page.on('pageerror', lambda err: errors.append(str(err)))
load(page)
page.locator('#overview-layout-open').click()
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
check = geometry(page)
expected = 1 if width <= 620 else 2 if width <= 900 else 3
assert check['cols'] == expected, (width, check)
assert check['visibleCount'] > 20
assert check['panelWidth'] is not None
assert abs(check['firstTileWidth'] - check['panelWidth']) < 3, (width, check)
assert check['dialogLeft'] >= -1 and check['dialogRight'] <= width + 1, (width, check)
assert check['dialogScrollWidth'] <= check['dialogWidth'] + 2, (width, check)
assert check['gridScrollWidth'] <= check['gridWidth'] + 2, (width, check)
assert not check['overflows'], (width, check['overflows'][:20])
tile = '#overview-layout-visible [data-preview-widget="http_protocol"]'
before = page.locator(tile).evaluate('(el) => el.getBoundingClientRect().width')
page.locator(tile + ' .overview-layout-widen').click()
assert page.locator(tile).get_attribute('data-dashboard-span') == '3'
after = page.locator(tile).evaluate('(el) => el.getBoundingClientRect().width')
assert abs(before - after) < 2, (width, before, after)
assert not geometry(page)['overflows']
page.locator(tile + ' .overview-layout-narrow').click()
assert page.locator(tile).get_attribute('data-dashboard-span') == '2'
if width == 1158:
page.screenshot(path=str(ROOT.parent / 'modal_preview_1158.png'))
if width == 375:
page.screenshot(path=str(ROOT.parent / 'modal_preview_375.png'))
page.locator(tile + ' .overview-layout-hide').click()
assert page.locator(tile).count() == 0
assert not geometry(page)['overflows']
page.locator('#overview-layout-cancel').click()
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
assert not errors, (width, errors)
context.close()
print('PASS visual modal grid, no overlap/scroll, width/hide/cancel:', width)
browser.close()
print('PASS 0.33.3 modal regression (simulated API)')
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""0.33.1 factory/default + unsaved layout regression against the full bundled JS.
The authenticated app bundle runs against simulated status/preferences; this is
not a Rust server integration test.
"""
import json
import re
from pathlib import Path
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
PRESET = json.loads((ROOT / 'src/admin/web/dashboard-defaults.json').read_text())
REGISTRY = json.loads((ROOT / 'src/admin/web/dashboard-widgets.json').read_text())
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
def app_bundle():
match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
assert match
body = ROUTER[match.end():]
body = body[:re.search(r'\]\s*\.join\("\\n"\)', body).start()]
result = []
for line in body.splitlines():
line = line.strip()
include = re.search(r'include_str!\("([^"]+)"\)', line)
if include:
result.append((ROOT / 'src/admin' / include.group(1)).read_text())
elif line == 'admin_vhosts_javascript(),':
result.append('(() => {\n' + '\n'.join((ROOT / 'src/admin/web/vhosts' / (name + '.js')).read_text()
for name in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();')
elif line.startswith('"') and line.endswith('",'):
result.append(json.loads(line[:-1]))
return '\n'.join(result)
assert len(PRESET['dashboard_widgets']) == len(REGISTRY)
assert set(PRESET['dashboard_widgets']) == {w['id'] for w in REGISTRY}
assert len(set(PRESET['dashboard_hidden_widgets'])) == len(PRESET['dashboard_hidden_widgets'])
assert set(PRESET['dashboard_hidden_widgets']).issubset(set(PRESET['dashboard_widgets']))
assert PRESET['dashboard_density'] == 'compact' and PRESET['overview_samples'] == 600
assert sum(w['id'].startswith('www_') and w['id'] in PRESET['dashboard_hidden_widgets'] for w in REGISTRY) == 12
HTML = (ROOT / 'src/admin/web/index.html').read_text()
CSS = (ROOT / 'src/admin/web/style.css').read_text()
BUNDLE = app_bundle()
assert BUNDLE.index('const ADMIN_DASHBOARD_DEFAULTS =') < BUNDLE.index('window.rustProxyOverviewSamples =')
def load(page):
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
page.add_style_tag(content=CSS)
page.add_script_tag(content=BUNDLE)
page.wait_for_function('window.rustProxyInlineLayout && window.rustProxyPreferences && window.rustProxyBackendDashboard')
def inspect(page, ids):
return page.evaluate('''ids => Object.fromEntries(ids.map(id => {
const node = document.querySelector(`[data-dashboard-widget="${id}"]`);
return [id, {hidden:node.hidden, display:getComputedStyle(node).display}];
}))''', ids)
def assert_visible(page, visible, hidden):
result = inspect(page, visible + hidden)
for widget in visible:
assert result[widget]['hidden'] is False and result[widget]['display'] != 'none', (widget, result)
for widget in hidden:
assert result[widget]['hidden'] is True and result[widget]['display'] == 'none', (widget, result)
# Test actual production CSS/JS in a browser with simulated preferences/status.
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
for width in [320, 375, 800, 1440, 1920]:
context = browser.new_context(viewport={'width': width, 'height': 1000})
page = context.new_page()
errors = []
page.on('pageerror', lambda error: errors.append(str(error)))
load(page)
page.evaluate('''() => {
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
document.getElementById('workspace').hidden = false;
document.getElementById('panel-overview').hidden = false;
document.getElementById('overview-layout-unlock').hidden = false;
document.getElementById('overview-layout-open').hidden = false;
currentPanel = 'overview';
window.__savedDashboard = structuredClone(ADMIN_APPEARANCE_DEFAULTS);
window.rustProxyPreferences.load = async () => window.__savedDashboard;
window.rustProxyPreferences.current = () => window.__savedDashboard;
window.rustProxyPreferences.update = async patch => {
window.__savedDashboard = {...window.__savedDashboard, ...patch};
applyAdminPreferences(window.__savedDashboard);
return window.__savedDashboard;
};
applyAdminPreferences(window.__savedDashboard);
}''')
# Unchanged previous accounts preserve historic grouped rows.
assert page.evaluate('''() => ({
legacy: document.getElementById('dashboard-overview-grid').dataset.overviewLayoutMode,
widths:window.__savedDashboard.dashboard_widget_sizes,
})''') == {'legacy':'legacy','widths':None}
page.locator('#overview-layout-unlock').click()
page.wait_for_function('window.rustProxyInlineLayout.isEditing()')
# Widen a number+sparkline card from 2 to 3 and then 4 columns.
selector = '[data-dashboard-widget="http_protocol"]'
old_width = page.locator(selector).evaluate('(node) => node.getBoundingClientRect().width')
page.locator(selector + ' .overview-inline-widen').click()
assert page.locator(selector).get_attribute('data-dashboard-span') == '3'
page.locator(selector + ' .overview-inline-widen').click()
assert page.locator(selector).get_attribute('data-dashboard-span') == '4'
assert page.locator(selector).evaluate('(node) => node.getBoundingClientRect().width') >= old_width * 1.7
assert page.evaluate('window.rustProxyInlineLayout.currentDraft().sizes.http_protocol') == 4
# Minimize a large multi-series chart to the new 3-column option.
for _ in range(2):
page.locator('[data-dashboard-widget="wide_transform_activity"] .overview-inline-narrow').click()
assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '3'
# Real UI controls allow crossing old groups: send mini-chart to the front
# with the upward arrow, then across the original stat/chart boundary.
# A direct drag onto the first card also uses the actual event handlers.
page.evaluate('''() => {
const from = document.querySelector('[data-dashboard-widget="wide_transform_activity"] .overview-inline-grip');
const to = document.querySelector('[data-dashboard-widget="requests"] .overview-inline-grip');
const transfer = new DataTransfer();
from.dispatchEvent(new DragEvent('dragstart', {bubbles:true, cancelable:true, dataTransfer:transfer}));
to.dispatchEvent(new DragEvent('drop', {bubbles:true, cancelable:true, dataTransfer:transfer, clientX:0}));
from.dispatchEvent(new DragEvent('dragend', {bubbles:true}));
}''')
assert page.evaluate('window.rustProxyInlineLayout.currentDraft().order[0]') == 'wide_transform_activity'
# Background status and unrelated preference changes cannot reset live sizes.
page.evaluate('''() => {
const status={revision:5,backend_pools:[],streams:{active:[],desired:[],metrics:{}}};
window.rustProxyBackendDashboard.render(status);
window.rustProxyStreamDashboard.render(status);
applyAdminPreferences({...window.__savedDashboard, chart_style:'soft'});
}''')
assert page.locator(selector).get_attribute('data-dashboard-span') == '4'
# Cancel should remove both size and layout changes.
page.locator('#overview-inline-cancel').click()
assert page.locator(selector).get_attribute('data-dashboard-span') == '2'
assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '6'
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None
# In a fresh draft, the modal editor also provides width controls.
page.locator('#overview-layout-open').click()
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
tile = '#overview-layout-visible [data-preview-widget="wide_transform_activity"]'
page.locator(tile + ' .overview-layout-narrow').click()
assert page.locator(tile).get_attribute('data-dashboard-span') == '4'
page.locator(tile + ' .overview-layout-narrow').click()
assert page.locator(tile).get_attribute('data-dashboard-span') == '3'
# Modal drag-and-drop also works across former small/wide size groups.
page.evaluate('''() => {
const from = document.querySelector('[data-preview-widget="wide_transform_activity"]');
const to = document.querySelector('[data-preview-widget="requests"]');
const transfer = new DataTransfer();
from.dispatchEvent(new DragEvent('dragstart', {bubbles:true,cancelable:true,dataTransfer:transfer}));
to.dispatchEvent(new DragEvent('drop', {bubbles:true,cancelable:true,dataTransfer:transfer}));
from.dispatchEvent(new DragEvent('dragend', {bubbles:true}));
}''')
page.locator('#overview-layout-save').click()
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes.wide_transform_activity') == 3
assert page.evaluate('window.__savedDashboard.dashboard_widgets[0]') == 'wide_transform_activity'
# Reload account settings: new card width persists.
page.evaluate('applyAdminPreferences(window.__savedDashboard)')
assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '3'
assert page.evaluate('window.__savedDashboard.dashboard_density') == 'compact'
# Four charts in one row, set through the same layout function used by UI.
data = page.evaluate('''() => {
const ids=['wide_transform_activity','wide_compression_activity','http_protocol','websocket_protocol'];
const order=[...ids,...ADMIN_DASHBOARD_WIDGETS.filter(id => !ids.includes(id))];
const hidden=[...ADMIN_DASHBOARD_WIDGETS.filter(id => !ids.includes(id))];
const sizes=Object.fromEntries(ids.map(id => [id,3]));
applyDashboardLayout(order,hidden,sizes);
const grid=document.getElementById('dashboard-overview-grid');
return { gridWidth:grid.getBoundingClientRect().width,
template:getComputedStyle(grid).gridTemplateColumns.split(' ').length,
boxes:ids.map(id=>{const b=document.querySelector(`[data-dashboard-widget="${id}"]`).getBoundingClientRect();return {id,x:Math.round(b.x),y:Math.round(b.y),w:Math.round(b.width),scroll:b.width};})
};
}''')
if data['template'] == 12:
assert len(set(b['y'] for b in data['boxes'])) == 1, (width, data)
assert all(b['w'] > 150 for b in data['boxes']), (width,data)
assert page.evaluate('document.documentElement.scrollWidth - window.innerWidth') <= 2, (width, data)
page.locator('#overview-layout-open').click()
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
page.locator('#overview-layout-default').click()
page.locator('#overview-layout-save').click()
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None
assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets']
assert page.evaluate('document.getElementById("dashboard-overview-grid").dataset.overviewLayoutMode') == 'legacy'
assert not errors, (width, errors)
print('PASS free-order, resize, live refresh, cancel/save, four chart grid', width, data['template'])
context.close()
browser.close()
print('PASS 0.33.3 resizable Overview browser regression')
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Verify sidebar colors use only the UI theme; chart palettes affect charts only.
Uses actual production HTML/CSS in headless Chromium, not a live RustProxy
server or authenticated preference/session roundtrip.
"""
from pathlib import Path
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
HTML = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
CSS = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
THEMES = ('dark', 'ultra_dark', 'light', 'cream')
PALETTES = ('default', 'soft', 'technical', 'high_contrast', 'neon', 'monochrome', 'warm', 'cool')
CORNERS = {'default': 0, 'square': 0, 'subtle': 4, 'rounded': 8, 'extra': 12}
STYLE_INFO = r'''() => {
const color = value => {
const c = document.createElement('canvas'); c.width = c.height = 1;
const ctx = c.getContext('2d'); ctx.fillStyle = value; ctx.fillRect(0, 0, 1, 1);
return [...ctx.getImageData(0, 0, 1, 1).data].slice(0, 3);
};
const root = getComputedStyle(document.documentElement);
const active = document.querySelector('.sidebar .main-navigation .nav.active');
const idle = document.querySelector('.sidebar .main-navigation .nav:not(.active)');
const sa = getComputedStyle(active), si = getComputedStyle(idle);
return {
chartAccent:color(root.getPropertyValue('--chart-accent').trim()),
themeSelected:color(root.getPropertyValue('--nav-active-bg').trim()),
themeHovered:color(root.getPropertyValue('--nav-hover-bg').trim()),
foreground:color(sa.color),
background:color(sa.backgroundColor),
borderLeft:color(sa.borderLeftColor),
borderRight:color(sa.borderRightColor),
radius:parseFloat(sa.borderTopLeftRadius),
idleBackground:color(si.backgroundColor),
mobileBackground:color(getComputedStyle(document.querySelector('.mobile-menu-toggle')).backgroundColor),
mobileColor:color(getComputedStyle(document.querySelector('.mobile-menu-toggle')).color),
navWidth:document.querySelector('.main-navigation').scrollWidth,
navClientWidth:document.querySelector('.main-navigation').clientWidth,
activeWidth:active.scrollWidth,
activeClientWidth:active.clientWidth,
};
}'''
def contrast(fg, bg):
def luminance(rgb):
channels = [x / 255 for x in rgb]
channels = [x / 12.92 if x <= 0.04045 else ((x + 0.055) / 1.055) ** 2.4 for x in channels]
return .2126 * channels[0] + .7152 * channels[1] + .0722 * channels[2]
lo, hi = sorted((luminance(fg), luminance(bg)))
return (hi + .05) / (lo + .05)
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
for viewport in (375, 1200):
page = browser.new_page(viewport={'width': viewport, 'height': 820})
errors = []
page.on('pageerror', lambda error: errors.append(str(error)))
page.set_content(HTML.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', '').replace('/assets/app___RUSTPROXY_ASSET_ID__.js', ''))
page.add_style_tag(content=CSS)
page.evaluate('''() => {
document.documentElement.classList.remove('app-loading');
document.getElementById('app-startup').remove();
document.querySelector('.sidebar').classList.add('mobile-nav-open');
document.querySelector('.mobile-menu-toggle')?.setAttribute('aria-expanded', 'true');
}''')
theme_colors = []
for theme in THEMES:
baseline = None
chart_accents = set()
for palette in PALETTES:
page.evaluate('''([theme, palette]) => {
document.documentElement.dataset.theme = theme;
document.documentElement.dataset.chartStyle = palette;
}''', [theme, palette])
for corner, expected_radius in CORNERS.items():
page.evaluate('corner => document.documentElement.dataset.cornerStyle = corner', corner)
style = page.evaluate(STYLE_INFO)
assert style['radius'] == expected_radius, (viewport, theme, palette, corner, style['radius'])
assert style['borderLeft'] == style['foreground'], (viewport, theme, palette, corner, style)
assert style['background'] == style['themeSelected'], (viewport, theme, palette, corner, style)
if viewport == 375:
assert style['mobileBackground'] == style['background'], (viewport, theme, palette, corner, style)
assert style['mobileColor'] == style['foreground'], (viewport, theme, palette, corner, style)
assert contrast(style['foreground'], style['background']) >= 4.5, (viewport, theme, palette, corner, style)
assert style['navWidth'] <= style['navClientWidth'] + 2, (viewport, theme, palette, corner, style)
assert style['activeWidth'] <= style['activeClientWidth'] + 2, (viewport, theme, palette, corner, style)
chart_accents.add(tuple(style['chartAccent']))
# The entire selected state is unaffected by chart-style changes.
nav_colors = (tuple(style['foreground']), tuple(style['background']), tuple(style['borderLeft']))
if baseline is None:
baseline = nav_colors
else:
assert baseline == nav_colors, (viewport, theme, palette, baseline, nav_colors)
page.locator('.nav[data-panel="vhosts"]').hover()
hovered = page.evaluate(STYLE_INFO)
assert hovered['idleBackground'] == hovered['themeHovered'], (viewport, theme, palette, hovered)
assert hovered['idleBackground'] != style['idleBackground'], (viewport, theme, palette)
page.mouse.move(0, 0)
if viewport == 1200 and theme == 'ultra_dark' and palette == 'cool':
page.screenshot(path=str(ROOT.parent / 'sidebar_preview_desktop.png'))
if viewport == 375 and theme == 'dark' and palette == 'soft':
page.screenshot(path=str(ROOT.parent / 'sidebar_preview_mobile.png'))
assert len(chart_accents) > 1, (viewport, theme, chart_accents)
theme_colors.append(baseline)
assert len(set(theme_colors)) == len(THEMES), (viewport, theme_colors)
page.evaluate("document.documentElement.dir = 'rtl'")
rtl = page.evaluate(STYLE_INFO)
assert rtl['borderRight'] == rtl['foreground'], (viewport, rtl)
assert not errors, errors
print('PASS themed navigation independence: viewport', viewport, 'themes', len(THEMES), 'palettes', len(PALETTES), 'corners', len(CORNERS))
page.close()
browser.close()
print('PASS: sidebar changes with UI theme and corner setting, never with chart style')
+47 -5
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "RustProxy Administration API", "title": "RustProxy Administration API",
"version": "0.33.2", "version": "0.33.3",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust." "description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
}, },
"servers": [ "servers": [
@@ -3413,7 +3413,7 @@
"dashboard_widgets": { "dashboard_widgets": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
"maxItems": 62, "maxItems": 78,
"uniqueItems": true, "uniqueItems": true,
"items": { "items": {
"type": "string", "type": "string",
@@ -3479,7 +3479,23 @@
"cache_diagnostics", "cache_diagnostics",
"process_diagnostics", "process_diagnostics",
"network_diagnostics", "network_diagnostics",
"backend_table" "backend_table",
"extra_cold_queue_memory",
"extra_cold_storage",
"wide_transform_activity",
"wide_compression_activity",
"www_status",
"www_kind",
"www_capacity",
"www_methods",
"www_error_rate",
"www_php_requests",
"www_requests",
"www_latency",
"www_failures",
"www_traffic",
"www_top_services",
"www_status_trend"
] ]
}, },
"default": [ "default": [
@@ -3548,9 +3564,19 @@
], ],
"description": "Account-scoped order for configurable Overview telemetry cards." "description": "Account-scoped order for configurable Overview telemetry cards."
}, },
"dashboard_widget_sizes": {
"type": "object",
"nullable": true,
"maxProperties": 78,
"additionalProperties": {
"type": "integer",
"enum": [2, 3, 4, 6, 12]
},
"description": "Nullable account-scoped per-card widths (12-column grid). Null preserves the legacy grouped visual arrangement; an object, including an empty one, enables freely ordered cards. Key must be a registered Overview widget ID. Panels require width >= 4, charts >= 3 and backend table >= 6."
},
"dashboard_hidden_widgets": { "dashboard_hidden_widgets": {
"type": "array", "type": "array",
"maxItems": 62, "maxItems": 78,
"uniqueItems": true, "uniqueItems": true,
"items": { "items": {
"type": "string", "type": "string",
@@ -3616,7 +3642,23 @@
"cache_diagnostics", "cache_diagnostics",
"process_diagnostics", "process_diagnostics",
"network_diagnostics", "network_diagnostics",
"backend_table" "backend_table",
"extra_cold_queue_memory",
"extra_cold_storage",
"wide_transform_activity",
"wide_compression_activity",
"www_status",
"www_kind",
"www_capacity",
"www_methods",
"www_error_rate",
"www_php_requests",
"www_requests",
"www_latency",
"www_failures",
"www_traffic",
"www_top_services",
"www_status_trend"
] ]
}, },
"default": [ "default": [
+18 -23
View File
@@ -27,29 +27,6 @@ fn default_admin_table_density() -> String {
fn default_admin_dashboard_density() -> String { fn default_admin_dashboard_density() -> String {
DASHBOARD_DEFAULT_PRESET.dashboard_density.clone() DASHBOARD_DEFAULT_PRESET.dashboard_density.clone()
} }
fn default_admin_dashboard_widgets() -> Vec<String> {
let mut order = DASHBOARD_DEFAULT_PRESET.dashboard_widgets.clone();
for widget in DASHBOARD_WIDGETS.iter() {
if !order.contains(&widget.id) {
order.push(widget.id.clone());
}
}
order
}
fn default_hidden_dashboard_widgets() -> Vec<String> {
let mut hidden = DASHBOARD_DEFAULT_PRESET.dashboard_hidden_widgets.clone();
for widget in DASHBOARD_WIDGETS.iter() {
if !DASHBOARD_DEFAULT_PRESET
.dashboard_widgets
.contains(&widget.id)
&& widget.default_hidden
{
hidden.push(widget.id.clone());
}
}
hidden
}
// Preserve the user's saved order/visibility, drop identifiers unknown to this // Preserve the user's saved order/visibility, drop identifiers unknown to this
// binary, and append new registry widgets according to their opt-in policy. // binary, and append new registry widgets according to their opt-in policy.
// Stored preferences from older/newer releases are normalized on read. // Stored preferences from older/newer releases are normalized on read.
@@ -62,6 +39,13 @@ fn reconcile_dashboard_widgets(prefs: &mut AdminPreferences) {
prefs.dashboard_hidden_widgets.retain(|id| { prefs.dashboard_hidden_widgets.retain(|id| {
DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) && seen_hidden.insert(id.clone()) DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) && seen_hidden.insert(id.clone())
}); });
if let Some(sizes) = prefs.dashboard_widget_sizes.as_mut() {
sizes.retain(|id, span| {
DASHBOARD_WIDGETS
.iter()
.any(|widget| widget.id == *id && valid_dashboard_span(&widget.display_type, *span))
});
}
for widget in DASHBOARD_WIDGETS.iter() { for widget in DASHBOARD_WIDGETS.iter() {
if !seen.contains(&widget.id) { if !seen.contains(&widget.id) {
prefs.dashboard_widgets.push(widget.id.clone()); prefs.dashboard_widgets.push(widget.id.clone());
@@ -147,6 +131,10 @@ struct AdminPreferences {
dashboard_widgets: Vec<String>, dashboard_widgets: Vec<String>,
#[serde(default = "default_hidden_dashboard_widgets")] #[serde(default = "default_hidden_dashboard_widgets")]
dashboard_hidden_widgets: Vec<String>, dashboard_hidden_widgets: Vec<String>,
// Null/absent: retain legacy visual grouping for existing installations.
// A map (including {}) means that order is fully user-controlled.
#[serde(default)]
dashboard_widget_sizes: Option<std::collections::BTreeMap<String, u8>>,
#[serde(default = "default_file_log_columns")] #[serde(default = "default_file_log_columns")]
file_log_columns: Vec<String>, file_log_columns: Vec<String>,
#[serde(default = "default_file_log_sort")] #[serde(default = "default_file_log_sort")]
@@ -200,6 +188,7 @@ impl Default for AdminPreferences {
dashboard_density: default_admin_dashboard_density(), dashboard_density: default_admin_dashboard_density(),
dashboard_widgets: default_admin_dashboard_widgets(), dashboard_widgets: default_admin_dashboard_widgets(),
dashboard_hidden_widgets: default_hidden_dashboard_widgets(), dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
dashboard_widget_sizes: None,
file_log_columns: default_file_log_columns(), file_log_columns: default_file_log_columns(),
file_log_sort: default_file_log_sort(), file_log_sort: default_file_log_sort(),
file_log_descending: default_file_log_descending(), file_log_descending: default_file_log_descending(),
@@ -385,6 +374,12 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE
)); ));
} }
} }
if !valid_dashboard_sizes(&preferences.dashboard_widget_sizes) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid dashboard card width preference".into(),
));
}
if preferences.backend_table_columns.is_empty() if preferences.backend_table_columns.is_empty()
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len() || preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
{ {
+47
View File
@@ -81,6 +81,7 @@ const STREAM_LIST_SORT_COLUMNS: &[&str] = &[
struct DashboardWidgetDefinition { struct DashboardWidgetDefinition {
id: String, id: String,
default_hidden: bool, default_hidden: bool,
display_type: String,
} }
static DASHBOARD_WIDGETS: std::sync::LazyLock<Vec<DashboardWidgetDefinition>> = static DASHBOARD_WIDGETS: std::sync::LazyLock<Vec<DashboardWidgetDefinition>> =
std::sync::LazyLock::new(|| { std::sync::LazyLock::new(|| {
@@ -145,3 +146,49 @@ static DASHBOARD_DEFAULT_PRESET: std::sync::LazyLock<DashboardDefaultPreset> =
); );
preset preset
}); });
// None denotes the legacy grouped layout. Some({}) switches to freely
// ordered 12-column CSS Grid while retaining every historical card width.
fn valid_dashboard_span(display_type: &str, span: u8) -> bool {
match display_type {
"table" => matches!(span, 6 | 12),
"panel" => matches!(span, 4 | 6 | 12),
"chart" => matches!(span, 3 | 4 | 6 | 12),
_ => matches!(span, 2 | 3 | 4 | 6 | 12),
}
}
fn valid_dashboard_sizes(sizes: &Option<std::collections::BTreeMap<String, u8>>) -> bool {
sizes.as_ref().is_none_or(|sizes| {
sizes.len() <= DASHBOARD_WIDGETS.len()
&& sizes.iter().all(|(id, span)| {
DASHBOARD_WIDGETS.iter().any(|widget| {
widget.id == *id && valid_dashboard_span(&widget.display_type, *span)
})
})
})
}
fn default_hidden_dashboard_widgets() -> Vec<String> {
let mut hidden = DASHBOARD_DEFAULT_PRESET.dashboard_hidden_widgets.clone();
for widget in DASHBOARD_WIDGETS.iter() {
if !DASHBOARD_DEFAULT_PRESET
.dashboard_widgets
.contains(&widget.id)
&& widget.default_hidden
{
hidden.push(widget.id.clone());
}
}
hidden
}
fn default_admin_dashboard_widgets() -> Vec<String> {
let mut order = DASHBOARD_DEFAULT_PRESET.dashboard_widgets.clone();
for widget in DASHBOARD_WIDGETS.iter() {
if !order.contains(&widget.id) {
order.push(widget.id.clone());
}
}
order
}
+53
View File
@@ -202,3 +202,56 @@ fn explicit_visible_stream_opt_in_is_not_reset_when_normalizing_saved_order() {
assert_eq!(prefs.dashboard_widgets, default_admin_dashboard_widgets()); assert_eq!(prefs.dashboard_widgets, default_admin_dashboard_widgets());
assert!(validate_admin_preferences(&prefs).is_ok()); assert!(validate_admin_preferences(&prefs).is_ok());
} }
#[test]
fn dashboard_width_preferences_preserve_legacy_and_allow_custom_grids() {
let old: AdminPreferences = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();
assert!(old.dashboard_widget_sizes.is_none());
let mut prefs = AdminPreferences::default();
prefs.dashboard_widget_sizes = Some(std::collections::BTreeMap::new());
assert!(validate_admin_preferences(&prefs).is_ok());
for (id, span) in [
("http_protocol", 4),
("http_protocol", 3),
("requests", 4),
("www_requests", 3),
("backend_table", 6),
] {
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert(id.into(), span);
}
assert!(validate_admin_preferences(&prefs).is_ok());
let saved = serde_json::to_string(&prefs).unwrap();
let restored: AdminPreferences = serde_json::from_str(&saved).unwrap();
assert_eq!(
restored.dashboard_widget_sizes,
prefs.dashboard_widget_sizes
);
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert("backend_table".into(), 2);
assert!(validate_admin_preferences(&prefs).is_err());
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert("backend_table".into(), 12);
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert("not_a_card".into(), 3);
assert!(validate_admin_preferences(&prefs).is_err());
reconcile_dashboard_widgets(&mut prefs);
assert!(!prefs
.dashboard_widget_sizes
.as_ref()
.unwrap()
.contains_key("not_a_card"));
assert!(validate_admin_preferences(&prefs).is_ok());
}
+55 -18
View File
@@ -15,14 +15,20 @@ window.rustProxyInlineLayout = (() => {
let opening = false; let opening = false;
let saving = false; let saving = false;
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0; const visibleIds = () => dashboardVisualOrder(draft.order, draft.sizes)
const visibleIds = () => draft.order.filter(id => !draft.hidden.has(id)) .filter(id => !draft.hidden.has(id));
.sort((a, b) => groupFor(a) - groupFor(b)); function enterFreeLayout() {
if (draft.sizes === null) {
draft.order = dashboardVisualOrder(draft.order, null);
draft.sizes = {};
}
}
const nodes = () => [...grid.children].filter(node => node.dataset.dashboardWidget); const nodes = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
const nodeFor = id => nodes().find(node => node.dataset.dashboardWidget === id); const nodeFor = id => nodes().find(node => node.dataset.dashboardWidget === id);
const label = id => dashboardWidgetLabel(id); const label = id => dashboardWidgetLabel(id);
const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i]) const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i])
|| [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id))); || [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id))
|| JSON.stringify(draft.sizes) !== JSON.stringify(original.sizes));
function renderHidden() { function renderHidden() {
if (!hiddenList || !draft) return; if (!hiddenList || !draft) return;
@@ -46,16 +52,24 @@ window.rustProxyInlineLayout = (() => {
if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); } if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); }
} }
function paint() { function paint() {
applyDashboardLayout(draft.order, [...draft.hidden]); applyDashboardLayout(draft.order, [...draft.hidden], draft.sizes);
renderHidden(); renderHidden();
const visible = visibleIds(); const visible = visibleIds();
for (const node of nodes()) { for (const node of nodes()) {
const i = visible.indexOf(node.dataset.dashboardWidget); const i = visible.indexOf(node.dataset.dashboardWidget);
const up = node.querySelector(".overview-inline-up"); const up = node.querySelector(".overview-inline-up");
const down = node.querySelector(".overview-inline-down"); const down = node.querySelector(".overview-inline-down");
if (up) up.disabled = i <= 0 || groupFor(visible[i - 1]) !== groupFor(node.dataset.dashboardWidget); if (up) up.disabled = i <= 0;
if (down) down.disabled = i < 0 || i === visible.length - 1 || if (down) down.disabled = i < 0 || i === visible.length - 1;
groupFor(visible[i + 1]) !== groupFor(node.dataset.dashboardWidget); const widths = dashboardAllowedWidths(node.dataset.dashboardWidget);
const span = dashboardWidgetWidth(node.dataset.dashboardWidget, draft.sizes);
const at = widths.indexOf(span);
const value = node.querySelector(".overview-inline-width-value");
if (value) value.textContent = `${span}/12`;
const narrow = node.querySelector(".overview-inline-narrow");
const widen = node.querySelector(".overview-inline-widen");
if (narrow) narrow.disabled = at <= 0;
if (widen) widen.disabled = at === widths.length - 1;
} }
} }
function clearDrag() { function clearDrag() {
@@ -63,19 +77,31 @@ window.rustProxyInlineLayout = (() => {
for (const node of nodes()) node.classList.remove("is-overview-dragging", "is-overview-drop-before", "is-overview-drop-after"); for (const node of nodes()) node.classList.remove("is-overview-dragging", "is-overview-drop-before", "is-overview-drop-after");
} }
function move(id, target, after = false) { function move(id, target, after = false) {
if (!draft || id === target || groupFor(id) !== groupFor(target)) return; if (!draft || id === target) return;
if (!visibleIds().includes(id) || !visibleIds().includes(target)) return;
enterFreeLayout();
const visible = visibleIds(); const visible = visibleIds();
if (!visible.includes(id) || !visible.includes(target)) return;
visible.splice(visible.indexOf(id), 1); visible.splice(visible.indexOf(id), 1);
visible.splice(visible.indexOf(target) + (after ? 1 : 0), 0, id); visible.splice(visible.indexOf(target) + (after ? 1 : 0), 0, id);
const iter = visible.values(); const iter = visible.values();
draft.order = draft.order.map(item => draft.hidden.has(item) ? item : iter.next().value); draft.order = draft.order.map(item => draft.hidden.has(item) ? item : iter.next().value);
paint(); paint();
} }
function resize(id, delta) {
if (!draft) return;
const widths = dashboardAllowedWidths(id);
const next = widths[widths.indexOf(dashboardWidgetWidth(id, draft.sizes)) + delta];
if (next === undefined) return;
enterFreeLayout();
if (next === dashboardDefaultWidth(id)) delete draft.sizes[id];
else draft.sizes[id] = next;
paint();
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-narrow" : ".overview-inline-widen")?.focus();
}
function nudge(id, delta) { function nudge(id, delta) {
const visible = visibleIds(); const visible = visibleIds();
const index = visible.indexOf(id), target = visible[index + delta]; const index = visible.indexOf(id), target = visible[index + delta];
if (!target || groupFor(id) !== groupFor(target)) return; if (!target) return;
move(id, target, delta > 0); move(id, target, delta > 0);
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-up" : ".overview-inline-down")?.focus(); nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-up" : ".overview-inline-down")?.focus();
} }
@@ -94,7 +120,14 @@ window.rustProxyInlineLayout = (() => {
const grip = control("⠿", `${t("overviewLayoutInlineDrag")}: ${title}`, "overview-inline-grip", () => { }); const grip = control("⠿", `${t("overviewLayoutInlineDrag")}: ${title}`, "overview-inline-grip", () => { });
grip.draggable = true; grip.draggable = true;
grip.dataset.overviewDragHandle = id; grip.dataset.overviewDragHandle = id;
bar.append(grip, dashboardWidgetTypeBadge(id), const sizeControls = el("span", undefined, "overview-inline-width");
const widthValue = el("span", "", "overview-inline-width-value");
widthValue.setAttribute("aria-label", t("overviewLayoutWidth"));
sizeControls.append(
control("−", `${t("overviewLayoutNarrow")}: ${title}`, "overview-inline-narrow", () => resize(id, -1)),
widthValue,
control("+", `${t("overviewLayoutWiden")}: ${title}`, "overview-inline-widen", () => resize(id, 1)));
bar.append(grip, dashboardWidgetTypeBadge(id), sizeControls,
control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)), control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)),
control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)), control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)),
control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id))); control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id)));
@@ -125,7 +158,7 @@ window.rustProxyInlineLayout = (() => {
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; } if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
if (hiddenList) hiddenList.replaceChildren(); if (hiddenList) hiddenList.replaceChildren();
if (modalButton) modalButton.disabled = false; if (modalButton) modalButton.disabled = false;
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden]); if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden], original.sizes);
draft = original = null; draft = original = null;
} }
async function begin() { async function begin() {
@@ -137,8 +170,9 @@ window.rustProxyInlineLayout = (() => {
const saved = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS; const saved = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
const order = normalizeDashboardWidgets(saved.dashboard_widgets); const order = normalizeDashboardWidgets(saved.dashboard_widgets);
const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets)); const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets));
original = { order: [...order], hidden: new Set(hidden) }; const sizes = normalizeDashboardWidgetSizes(saved.dashboard_widget_sizes);
draft = { order: [...order], hidden: new Set(hidden) }; original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
showEditing(); showEditing();
} catch (error) { if (error?.status !== 401) toast(error.message, true); } } catch (error) { if (error?.status !== 401) toast(error.message, true); }
finally { opening = false; unlock.disabled = false; } finally { opening = false; unlock.disabled = false; }
@@ -151,6 +185,7 @@ window.rustProxyInlineLayout = (() => {
try { try {
await window.rustProxyPreferences.update({ await window.rustProxyPreferences.update({
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden], dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
}); });
finish(true); finish(true);
toast(t("overviewLayoutEditorSaved")); toast(t("overviewLayoutEditorSaved"));
@@ -177,8 +212,7 @@ window.rustProxyInlineLayout = (() => {
grid?.addEventListener("dragover", event => { grid?.addEventListener("dragover", event => {
if (!draft || !dragging) return; if (!draft || !dragging) return;
const target = dropTarget(event); const target = dropTarget(event);
if (!target || target.dataset.dashboardWidget === dragging || if (!target || target.dataset.dashboardWidget === dragging) return;
groupFor(target.dataset.dashboardWidget) !== groupFor(dragging)) return;
event.preventDefault(); event.preventDefault();
for (const node of nodes()) node.classList.remove("is-overview-drop-before", "is-overview-drop-after"); for (const node of nodes()) node.classList.remove("is-overview-drop-before", "is-overview-drop-after");
target.classList.add(positionAfter(target, event) ? "is-overview-drop-after" : "is-overview-drop-before"); target.classList.add(positionAfter(target, event) ? "is-overview-drop-after" : "is-overview-drop-before");
@@ -198,7 +232,10 @@ window.rustProxyInlineLayout = (() => {
cancelButton?.addEventListener("click", cancel); cancelButton?.addEventListener("click", cancel);
return { return {
isEditing: () => !!draft, isEditing: () => !!draft,
currentDraft: () => draft ? { order: [...draft.order], hidden: [...draft.hidden] } : null, currentDraft: () => draft ? {
order: [...draft.order], hidden: [...draft.hidden],
sizes: draft.sizes === null ? null : { ...draft.sizes },
} : null,
cancel, cancel,
}; };
})(); })();
+53 -13
View File
@@ -20,43 +20,60 @@ window.rustProxyDashboardLayout = (() => {
function initialDraft() { function initialDraft() {
const prefs = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS; const prefs = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
const order = normalizeDashboardWidgets(prefs.dashboard_widgets); const order = normalizeDashboardWidgets(prefs.dashboard_widgets);
return { order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)) }; return { order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)),
sizes: normalizeDashboardWidgetSizes(prefs.dashboard_widget_sizes) };
} }
function defaultDraft() { function defaultDraft() {
return { return {
order: [...ADMIN_DASHBOARD_WIDGETS], order: [...ADMIN_DASHBOARD_WIDGETS],
hidden: new Set(ADMIN_DEFAULT_HIDDEN_WIDGETS), hidden: new Set(ADMIN_DEFAULT_HIDDEN_WIDGETS),
sizes: null,
}; };
} }
function label(id) { function label(id) {
const widget = ADMIN_DASHBOARD_BY_ID.get(id); const widget = ADMIN_DASHBOARD_BY_ID.get(id);
return widget ? t(widget.label_key) : id; return widget ? t(widget.label_key) : id;
} }
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
function visibleIds() { function visibleIds() {
return draft.order.filter(id => !draft.hidden.has(id)) return dashboardVisualOrder(draft.order, draft.sizes)
.sort((a, b) => groupFor(a) - groupFor(b)); .filter(id => !draft.hidden.has(id));
}
function enterFreeLayout() {
if (draft.sizes === null) {
draft.order = dashboardVisualOrder(draft.order, null);
draft.sizes = {};
}
} }
function reorderVisible(nextVisible) { function reorderVisible(nextVisible) {
enterFreeLayout();
const iter = nextVisible.values(); const iter = nextVisible.values();
draft.order = draft.order.map(id => draft.hidden.has(id) ? id : iter.next().value); draft.order = draft.order.map(id => draft.hidden.has(id) ? id : iter.next().value);
render(); render();
} }
function move(id, target, after = false) { function move(id, target, after = false) {
const next = visibleIds(); const next = visibleIds();
if (id === target || !next.includes(id) || !next.includes(target) || groupFor(id) !== groupFor(target)) return; if (id === target || !next.includes(id) || !next.includes(target)) return;
next.splice(next.indexOf(id), 1); next.splice(next.indexOf(id), 1);
next.splice(next.indexOf(target) + (after ? 1 : 0), 0, id); next.splice(next.indexOf(target) + (after ? 1 : 0), 0, id);
reorderVisible(next); reorderVisible(next);
} }
function nudge(id, direction) { function nudge(id, direction) {
const next = visibleIds(), index = next.indexOf(id), other = index + direction; const next = visibleIds(), index = next.indexOf(id), other = index + direction;
if (index < 0 || other < 0 || other >= next.length || groupFor(id) !== groupFor(next[other])) return; if (index < 0 || other < 0 || other >= next.length) return;
[next[index], next[other]] = [next[other], next[index]]; [next[index], next[other]] = [next[other], next[index]];
reorderVisible(next); reorderVisible(next);
visibleRoot.querySelectorAll(".overview-layout-tile").forEach(node => { visibleRoot.querySelector(`[data-preview-widget="${id}"] .overview-layout-up`)?.focus();
if (node.dataset.previewWidget === id) node.querySelector(".overview-layout-up")?.focus(); }
}); function resize(id, delta) {
if (!draft) return;
const widths = dashboardAllowedWidths(id);
const next = widths[widths.indexOf(dashboardWidgetWidth(id, draft.sizes)) + delta];
if (next === undefined) return;
enterFreeLayout();
if (next === dashboardDefaultWidth(id)) delete draft.sizes[id];
else draft.sizes[id] = next;
render();
visibleRoot.querySelector(`[data-preview-widget="${id}"] .${delta < 0 ? "overview-layout-narrow" : "overview-layout-widen"}`)?.focus();
} }
function preview(id) { function preview(id) {
const original = document.querySelector(`#panel-overview [data-dashboard-widget="${id}"]`); const original = document.querySelector(`#panel-overview [data-dashboard-widget="${id}"]`);
@@ -94,6 +111,7 @@ window.rustProxyDashboardLayout = (() => {
const tiles = visible.map((id, index) => { const tiles = visible.map((id, index) => {
const tile = el("div", undefined, "overview-layout-tile"); const tile = el("div", undefined, "overview-layout-tile");
tile.dataset.previewWidget = id; tile.dataset.previewWidget = id;
tile.dataset.dashboardSpan = String(dashboardWidgetWidth(id, draft.sizes));
if (ADMIN_DASHBOARD_BY_ID.get(id)?.category === "panels") tile.classList.add("is-panel"); if (ADMIN_DASHBOARD_BY_ID.get(id)?.category === "panels") tile.classList.add("is-panel");
tile.draggable = true; tile.draggable = true;
tile.setAttribute("role", "listitem"); tile.setAttribute("role", "listitem");
@@ -102,19 +120,39 @@ window.rustProxyDashboardLayout = (() => {
const grip = el("span", "⠿", "overview-layout-grip"); const grip = el("span", "⠿", "overview-layout-grip");
grip.setAttribute("aria-hidden", "true"); grip.setAttribute("aria-hidden", "true");
const up = el("button", "←", "quiet overview-layout-up"); const up = el("button", "←", "quiet overview-layout-up");
up.type = "button"; up.disabled = index === 0 || groupFor(id) !== groupFor(visible[index - 1]); up.type = "button"; up.disabled = index === 0;
up.setAttribute("aria-label", `${t("moveUp")}: ${label(id)}`); up.setAttribute("aria-label", `${t("moveUp")}: ${label(id)}`);
up.addEventListener("click", () => nudge(id, -1)); up.addEventListener("click", () => nudge(id, -1));
const down = el("button", "→", "quiet overview-layout-down"); const down = el("button", "→", "quiet overview-layout-down");
down.type = "button"; down.disabled = index === visible.length - 1 || groupFor(id) !== groupFor(visible[index + 1]); down.type = "button"; down.disabled = index === visible.length - 1;
down.setAttribute("aria-label", `${t("moveDown")}: ${label(id)}`); down.setAttribute("aria-label", `${t("moveDown")}: ${label(id)}`);
down.addEventListener("click", () => nudge(id, 1)); down.addEventListener("click", () => nudge(id, 1));
const hide = el("button", t("overviewLayoutEditorHide"), "quiet overview-layout-hide"); const hide = el("button", t("overviewLayoutEditorHide"), "quiet overview-layout-hide");
hide.type = "button"; hide.type = "button";
hide.setAttribute("aria-label", `${t("overviewLayoutEditorHide")}: ${label(id)}`); hide.setAttribute("aria-label", `${t("overviewLayoutEditorHide")}: ${label(id)}`);
hide.addEventListener("click", () => { draft.hidden.add(id); render(); }); hide.addEventListener("click", () => { draft.hidden.add(id); render(); });
const widths = dashboardAllowedWidths(id);
const width = dashboardWidgetWidth(id, draft.sizes);
const at = widths.indexOf(width);
const narrow = el("button", "−", "quiet overview-layout-narrow");
narrow.type = "button"; narrow.disabled = at <= 0;
narrow.setAttribute("aria-label", `${t("overviewLayoutNarrow")}: ${label(id)}`);
narrow.addEventListener("click", () => resize(id, -1));
const widen = el("button", "+", "quiet overview-layout-widen");
widen.type = "button"; widen.disabled = at === widths.length - 1;
widen.setAttribute("aria-label", `${t("overviewLayoutWiden")}: ${label(id)}`);
widen.addEventListener("click", () => resize(id, 1));
const widthValue = el("span", `${width}/12`, "overview-layout-width-value");
widthValue.title = t("overviewLayoutWidth");
// The visual editor intentionally keeps uniformly sized previews.
// Group sizing separately from ordering/hiding so both sets wrap
// inside a narrow dialog rather than spilling into adjacent tiles.
const sizeActions = el("span", undefined, "overview-layout-size-actions");
sizeActions.append(narrow, widthValue, widen);
const orderActions = el("span", undefined, "overview-layout-order-actions");
orderActions.append(up, down, hide);
const controls = el("span", undefined, "overview-layout-controls"); const controls = el("span", undefined, "overview-layout-controls");
controls.append(up, down, hide); controls.append(sizeActions, orderActions);
toolbar.append(grip, dashboardWidgetTypeBadge(id), controls); toolbar.append(grip, dashboardWidgetTypeBadge(id), controls);
tile.append(toolbar, preview(id)); tile.append(toolbar, preview(id));
tile.addEventListener("dragstart", event => { tile.addEventListener("dragstart", event => {
@@ -125,7 +163,7 @@ window.rustProxyDashboardLayout = (() => {
tile.classList.add("is-dragging"); tile.classList.add("is-dragging");
}); });
tile.addEventListener("dragover", event => { tile.addEventListener("dragover", event => {
if (!dragging || dragging === id || groupFor(dragging) !== groupFor(id)) return; if (!dragging || dragging === id) return;
event.preventDefault(); event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
tile.classList.add("is-drop-target"); tile.classList.add("is-drop-target");
@@ -201,6 +239,7 @@ window.rustProxyDashboardLayout = (() => {
try { try {
await window.rustProxyPreferences.update({ await window.rustProxyPreferences.update({
dashboard_widgets: [...defaults.order], dashboard_hidden_widgets: [...defaults.hidden], dashboard_widgets: [...defaults.order], dashboard_hidden_widgets: [...defaults.hidden],
dashboard_widget_sizes: null,
}); });
resetConfirm.close(); resetConfirm.close();
dialog.close(); dialog.close();
@@ -215,6 +254,7 @@ window.rustProxyDashboardLayout = (() => {
try { try {
await window.rustProxyPreferences.update({ await window.rustProxyPreferences.update({
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden], dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
}); });
dialog.close(); dialog.close();
toast(t("overviewLayoutEditorSaved")); toast(t("overviewLayoutEditorSaved"));
+7 -13
View File
@@ -87,17 +87,13 @@ window.rustProxyExtraOverview = (() => {
} }
if (id === "extra_backend_active") { if (id === "extra_backend_active") {
const members = Array.isArray(status.backend_pools) ? status.backend_pools.flatMap(pool => pool.members || []) : null; const members = Array.isArray(status.backend_pools) ? status.backend_pools.flatMap(pool => pool.members || []) : null;
return { return { value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null, unit: "count", detail: "" };
unit: "count", detail: ""
};
} }
if (id === "extra_backend_unhealthy") { if (id === "extra_backend_unhealthy") {
return { return { value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) => sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null, unit: "count", detail: "" };
unit: "count", detail: ""
};
} }
if (id === "extra_cache_memory") return ramBudget(c.resident_bytes, c.hot_limit_bytes); if (id === "extra_cache_memory") return ramBudget(c.resident_bytes, c.hot_limit_bytes);
if (id === "extra_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes); if (id === "extra_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes);
@@ -127,10 +123,8 @@ window.rustProxyExtraOverview = (() => {
const { deltas, seconds } = result; const { deltas, seconds } = result;
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" }; if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1]; const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
return { return { value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "", slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null };
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null
};
} }
function format(value, unit) { function format(value, unit) {
if (value == null) return "-"; if (value == null) return "-";
+6 -3
View File
@@ -9,16 +9,19 @@ window.rustProxyMasonry = (() => {
function refresh() { function refresh() {
const previous = widgets(); const previous = widgets();
const ordered = previous.map((node, index) => ({ node, index })) // Existing layouts (without a width map) retain their historical rows.
// Once a user moves/resizes a block, DOM order is the actual order.
const legacy = grid.dataset.overviewLayoutMode !== "custom";
const ordered = legacy ? previous.map((node, index) => ({ node, index }))
.sort((a, b) => group(a.node) - group(b.node) || a.index - b.index) .sort((a, b) => group(a.node) - group(b.node) || a.index - b.index)
.map(item => item.node); .map(item => item.node) : previous;
if (ordered.some((node, i) => node !== previous[i])) grid.append(...ordered); if (ordered.some((node, i) => node !== previous[i])) grid.append(...ordered);
let lastGroup = -1; let lastGroup = -1;
for (const node of ordered) { for (const node of ordered) {
for (const key of ["position", "left", "top", "width", "minHeight", "gridColumnStart", for (const key of ["position", "left", "top", "width", "minHeight", "gridColumnStart",
"gridColumnEnd", "gridRowStart", "gridRowEnd", "alignSelf"]) node.style[key] = ""; "gridColumnEnd", "gridRowStart", "gridRowEnd", "alignSelf"]) node.style[key] = "";
node.classList.remove("overview-row-start"); node.classList.remove("overview-row-start");
if (node.hidden) continue; if (node.hidden || !legacy) continue;
const current = group(node); const current = group(node);
if (current !== lastGroup) node.classList.add("overview-row-start"); if (current !== lastGroup) node.classList.add("overview-row-start");
lastGroup = current; lastGroup = current;
+71 -18
View File
@@ -13,7 +13,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
])]); ])]);
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({ const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default", theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null,
}); });
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" }); const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false; let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
@@ -41,6 +41,47 @@ function normalizeHiddenDashboardWidgets(value, savedOrder = ADMIN_DASHBOARD_WID
} }
return hidden; return hidden;
} }
// An absent map preserves the previous grouped visual order after upgrading.
// An explicit object (even empty) opts into free ordering and card resizing.
const DASHBOARD_WIDTHS = Object.freeze([2, 3, 4, 6, 12]);
function dashboardAllowedWidths(id) {
switch (dashboardWidgetType(id)) {
case "table": return [6, 12];
case "panel": return [4, 6, 12];
case "chart": return [3, 4, 6, 12];
default: return DASHBOARD_WIDTHS;
}
}
function dashboardDefaultWidth(id) {
const node = document.querySelector(`#dashboard-overview-grid [data-dashboard-widget="${id}"]`);
if (!node) return 2;
if (node.classList.contains("pool-card")) return 12;
if (node.classList.contains("overview-wide-chart")) return 6;
if (node.classList.contains("stat")) return 2;
return 4;
}
function normalizeDashboardWidgetSizes(value) {
if (value === null || value === undefined) return null;
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const result = {};
for (const [id, span] of Object.entries(value)) {
if (ADMIN_DASHBOARD_BY_ID.has(id) && dashboardAllowedWidths(id).includes(span)
&& span !== dashboardDefaultWidth(id)) result[id] = span;
}
return result;
}
function dashboardWidgetWidth(id, sizes) {
return sizes?.[id] ?? dashboardDefaultWidth(id);
}
function dashboardVisualOrder(order, sizes = null) {
const normalized = normalizeDashboardWidgets(order);
if (sizes !== null) return normalized;
// Preserve the old grouped appearance until the first intentional edit.
return normalized.map((id, index) => ({ id, index }))
.sort((a, b) => (window.rustProxyMasonry?.groupFor?.(a.id) ?? 0) -
(window.rustProxyMasonry?.groupFor?.(b.id) ?? 0) || a.index - b.index)
.map(item => item.id);
}
function browserLanguage() { function browserLanguage() {
const packs = [...languagePacks.values()], candidates = (navigator.languages || [navigator.language]).filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase()); const packs = [...languagePacks.values()], candidates = (navigator.languages || [navigator.language]).filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase());
return candidates.map(value => packs.find(pack => pack.meta.locale.toLowerCase() === value || pack.id.replaceAll("_", "-").toLowerCase() === value) || packs.find(pack => pack.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean)?.id || defaultLanguage; return candidates.map(value => packs.find(pack => pack.meta.locale.toLowerCase() === value || pack.id.replaceAll("_", "-").toLowerCase() === value) || packs.find(pack => pack.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean)?.id || defaultLanguage;
@@ -96,19 +137,25 @@ function applyAdminDashboardDensity(value) {
const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = density; const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = density;
const select = $("ui-dashboard-density"); if (select) select.value = density; return density; const select = $("ui-dashboard-density"); if (select) select.value = density; return density;
} }
function applyDashboardLayout(order, hidden) { function applyDashboardLayout(order, hidden, sizes = null) {
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 widths = normalizeDashboardWidgetSizes(sizes);
const grid = document.querySelector("#panel-overview .stats-grid");
if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
grid.dataset.overviewLayoutMode = widths === null ? "legacy" : "custom";
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]));
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);
node.dataset.dashboardSpan = String(dashboardWidgetWidth(node.dataset.dashboardWidget, widths));
}
const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")]; const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")];
if (orderedNodes.some((node, index) => node !== currentNodes[index])) grid.append(...orderedNodes); if (orderedNodes.some((node, index) => node !== currentNodes[index])) grid.append(...orderedNodes);
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]); window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]); window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
window.rustProxyMasonry?.refresh?.(); window.rustProxyMasonry?.refresh?.();
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); } if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
return { order: normalizedOrder, hidden: [...hiddenSet] }; return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
} }
function dashboardWidgetLabel(id) { function dashboardWidgetLabel(id) {
const widget = ADMIN_DASHBOARD_BY_ID.get(id); const widget = ADMIN_DASHBOARD_BY_ID.get(id);
@@ -135,7 +182,7 @@ function renderDashboardWidgetSettings() {
const focusedRow = document.activeElement?.closest?.("[data-dashboard-widget-setting]"); const focusedRow = document.activeElement?.closest?.("[data-dashboard-widget-setting]");
const focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null; const focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null;
const focusedAction = focusedId ? document.activeElement?.dataset.dashboardWidgetAction : 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 = dashboardVisualOrder(current.dashboard_widgets, current.dashboard_widget_sizes), 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);
@@ -152,14 +199,19 @@ function renderDashboardWidgetSettings() {
const down = el("button", "→", "quiet"); down.type = "button"; down.dataset.dashboardWidgetAction = "down"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("dashboardWidgetsMoveLater")); 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], current.dashboard_widget_sizes); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, toggle, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous); renderDashboardWidgetSettings(); }); void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, toggle, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous, current.dashboard_widget_sizes); renderDashboardWidgetSettings(); });
}); });
const move = delta => { const move = delta => {
const previous = [...order], next = [...order], at = next.indexOf(id), target = at + delta; if (at < 0 || target < 0 || target >= next.length) return; const previous = [...order], next = [...order], at = next.indexOf(id), target = at + delta; if (at < 0 || target < 0 || target >= next.length) return;
[next[at], next[target]] = [next[target], next[at]]; if (adminPreferences) adminPreferences.dashboard_widgets = next; [next[at], next[target]] = [next[target], next[at]];
applyDashboardLayout(next, [...hidden]); renderDashboardWidgetSettings(); const previousSizes = current.dashboard_widget_sizes;
void saveAppearancePreference({ dashboard_widgets: next }, null, () => { if (adminPreferences) adminPreferences.dashboard_widgets = previous; applyDashboardLayout(previous, [...hidden]); renderDashboardWidgetSettings(); }); if (adminPreferences) { adminPreferences.dashboard_widgets = next; adminPreferences.dashboard_widget_sizes = normalizeDashboardWidgetSizes(previousSizes) ?? {}; }
applyDashboardLayout(next, [...hidden], current.dashboard_widget_sizes); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_widgets: next, dashboard_widget_sizes: current.dashboard_widget_sizes }, null, () => {
if (adminPreferences) { adminPreferences.dashboard_widgets = previous; adminPreferences.dashboard_widget_sizes = previousSizes; }
applyDashboardLayout(previous, [...hidden], previousSizes); 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;
}); });
@@ -224,14 +276,15 @@ function applyAdminPreferences(value, cache = true) {
const editing = window.rustProxyInlineLayout?.currentDraft?.(); const editing = window.rustProxyInlineLayout?.currentDraft?.();
const savedOrder = normalizeDashboardWidgets(value.dashboard_widgets); const savedOrder = normalizeDashboardWidgets(value.dashboard_widgets);
const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets); const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets);
if (editing) applyDashboardLayout(editing.order, editing.hidden); const savedSizes = normalizeDashboardWidgetSizes(value.dashboard_widget_sizes);
else applyDashboardLayout(savedOrder, savedHidden); if (editing) applyDashboardLayout(editing.order, editing.hidden, editing.sizes);
else applyDashboardLayout(savedOrder, savedHidden, savedSizes);
const normalized = { const normalized = {
...value, ...value,
refresh: normalizeRefreshPreferences(value.refresh), refresh: normalizeRefreshPreferences(value.refresh),
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache), theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density), time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes,
}; };
adminPreferences = normalized; adminPreferences = normalized;
window.rustProxyRefresh?.update(normalized.refresh); window.rustProxyRefresh?.update(normalized.refresh);
@@ -279,7 +332,7 @@ function updateAdminPreferences(patch) {
next.refresh = normalizeRefreshPreferences(next.refresh); next.refresh = normalizeRefreshPreferences(next.refresh);
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale); next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density); next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes);
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) }); const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
if (!authenticated || epoch !== authEpoch) return adminPreferences; if (!authenticated || epoch !== authEpoch) return adminPreferences;
return applyAdminPreferences(saved); return applyAdminPreferences(saved);
@@ -378,7 +431,7 @@ $("ui-dashboard-density")?.addEventListener("change", () => {
async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") { async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") {
if (control) control.disabled = true; let previous = null; if (control) control.disabled = true; let previous = null;
try { try {
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets] } : null; const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets], dashboard_widget_sizes: current.dashboard_widget_sizes === null ? null : { ...current.dashboard_widget_sizes } } : null;
if (current) applyAdminPreferences({ ...current, ...patch }); await updateAdminPreferences(patch); toast(t(messageKey)); if (current) applyAdminPreferences({ ...current, ...patch }); await updateAdminPreferences(patch); toast(t(messageKey));
} catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); } } catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); }
finally { if (control) control.disabled = false; } finally { if (control) control.disabled = false; }
@@ -386,9 +439,9 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => { document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
const patches = { const patches = {
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal", corner_style: "default" }, charts: { chart_style: "default" }, tables: { table_density: "normal" }, interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal", corner_style: "default" }, charts: { chart_style: "default" }, tables: { table_density: "normal" },
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] }, dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null },
}; };
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control); void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
})); }));
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] }, $("appearance-reset"), "appearanceResetDone")); $("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null }, $("appearance-reset"), "appearanceResetDone"));
refreshAppearanceUi(); refreshAppearanceUi();
+1 -1
View File
@@ -256,7 +256,7 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
const actions = el("span", undefined, "network-interface-head-actions"); const actions = el("span", undefined, "network-interface-head-actions");
actions.append(rate, hide); actions.append(rate, hide);
head.append(el("b", name, "mono"), actions); head.append(el("b", name, "mono"), actions);
const svg = svgNode("svg", { viewBox: "0 0 300 60", role: "img", "aria-label": `${name} ${t("networkInterfacesHistory")}` }); const svg = svgNode("svg", { viewBox: "0 0 300 60", preserveAspectRatio: "none", role: "img", "aria-label": `${name} ${t("networkInterfacesHistory")}` });
svg.append(svgNode("path", { class: "chart-grid", d: "M0 15H300 M0 30H300 M0 45H300" })); svg.append(svgNode("path", { class: "chart-grid", d: "M0 15H300 M0 30H300 M0 45H300" }));
svg.append(svgNode("polyline", { class: "diagnostic-line network-rx-line", points: "0,55 300,55" })); svg.append(svgNode("polyline", { class: "diagnostic-line network-rx-line", points: "0,55 300,55" }));
svg.append(svgNode("polyline", { class: "diagnostic-line network-tx-line", points: "0,55 300,55" })); svg.append(svgNode("polyline", { class: "diagnostic-line network-tx-line", points: "0,55 300,55" }));
+88 -63
View File
@@ -154,7 +154,8 @@
</section> </section>
<section class="panel" id="panel-overview"> <section class="panel" id="panel-overview">
<p class="fine overview-inline-hint" id="overview-inline-hint" role="status" aria-live="polite" <p class="fine overview-inline-hint" id="overview-inline-hint" role="status" aria-live="polite"
hidden data-i18n="overviewLayoutInlineHint">Layout unlocked: reorder blocks within size groups, hidden data-i18n="overviewLayoutInlineHint">Layout unlocked: move cards freely, resize with − /
+,
hide with × and save or cancel.</p> hide with × and save or cancel.</p>
<details class="overview-inline-hidden" id="overview-inline-hidden" hidden> <details class="overview-inline-hidden" id="overview-inline-hidden" hidden>
<summary><span data-i18n="overviewLayoutInlineHidden">Hidden blocks</span> <span <summary><span data-i18n="overviewLayoutInlineHidden">Hidden blocks</span> <span
@@ -226,8 +227,9 @@
<article class="stat stat-chart" data-dashboard-widget="http_protocol"><span <article class="stat stat-chart" data-dashboard-widget="http_protocol"><span
data-i18n="httpProtocolMix">HTTP protocol data-i18n="httpProtocolMix">HTTP protocol
mix</span><strong id="metric-http-proto-total">0.0/s</strong><svg mix</span><strong id="metric-http-proto-total">0.0/s</strong><svg
aria-label="HTTP protocol activity" data-i18n-aria="httpProtocolMix" preserveAspectRatio="none" aria-label="HTTP protocol activity"
id="http-protocol-chart" role="img" viewBox="0 0 300 60"> data-i18n-aria="httpProtocolMix" id="http-protocol-chart" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h1-line" id="http-protocol-h1" <polyline class="diagnostic-line proto-h1-line" id="http-protocol-h1"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -241,7 +243,7 @@
id="metric-http-proto-h3">0.0/s</b></span></small></article> id="metric-http-proto-h3">0.0/s</b></span></small></article>
<article class="stat stat-chart" data-dashboard-widget="websocket_protocol"><span <article class="stat stat-chart" data-dashboard-widget="websocket_protocol"><span
data-i18n="wsProtocolMix">WebSocket protocol data-i18n="wsProtocolMix">WebSocket protocol
mix</span><strong id="metric-ws-proto-total">0</strong><svg mix</span><strong id="metric-ws-proto-total">0</strong><svg preserveAspectRatio="none"
aria-label="WebSocket protocol activity" data-i18n-aria="wsProtocolMix" aria-label="WebSocket protocol activity" data-i18n-aria="wsProtocolMix"
id="ws-protocol-chart" role="img" viewBox="0 0 300 60"> id="ws-protocol-chart" role="img" viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
@@ -264,8 +266,8 @@
<article class="stat stat-chart" data-dashboard-widget="stream_traffic" hidden> <article class="stat stat-chart" data-dashboard-widget="stream_traffic" hidden>
<span data-i18n="streamWidgetTraffic">Stream traffic</span><strong <span data-i18n="streamWidgetTraffic">Stream traffic</span><strong
id="stream-dashboard-traffic">-</strong> id="stream-dashboard-traffic">-</strong>
<svg aria-label="Stream traffic RX/TX" data-i18n-aria="streamWidgetTraffic" role="img" <svg preserveAspectRatio="none" aria-label="Stream traffic RX/TX"
viewBox="0 0 300 60"> data-i18n-aria="streamWidgetTraffic" role="img" viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line stream-chart-rx" id="stream-dashboard-rx" <polyline class="diagnostic-line stream-chart-rx" id="stream-dashboard-rx"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -297,7 +299,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_activity" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_activity" hidden>
<span data-i18n="backendWidgetActivity">Active backend requests</span><strong <span data-i18n="backendWidgetActivity">Active backend requests</span><strong
id="backend-dashboard-activity">-</strong> id="backend-dashboard-activity">-</strong>
<svg data-i18n-aria="backendWidgetActivity" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetActivity" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-active-line" <polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-active-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -306,7 +309,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_errors" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_errors" hidden>
<span data-i18n="backendWidgetErrors">Backend errors</span><strong <span data-i18n="backendWidgetErrors">Backend errors</span><strong
id="backend-dashboard-errors">-</strong> id="backend-dashboard-errors">-</strong>
<svg data-i18n-aria="backendWidgetErrors" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetErrors" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h3-line" id="backend-dashboard-error-line" <polyline class="diagnostic-line proto-h3-line" id="backend-dashboard-error-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -315,7 +319,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_latency" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_latency" hidden>
<span data-i18n="backendWidgetLatency">Health probe RTT</span><strong <span data-i18n="backendWidgetLatency">Health probe RTT</span><strong
id="backend-dashboard-latency">-</strong> id="backend-dashboard-latency">-</strong>
<svg data-i18n-aria="backendWidgetLatency" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetLatency" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-latency-line" <polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-latency-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -324,7 +329,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_traffic" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_traffic" hidden>
<span data-i18n="backendWidgetTraffic">Backend traffic</span><strong <span data-i18n="backendWidgetTraffic">Backend traffic</span><strong
id="backend-dashboard-traffic">-</strong> id="backend-dashboard-traffic">-</strong>
<svg data-i18n-aria="backendWidgetTraffic" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetTraffic" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line stream-chart-rx" id="backend-dashboard-in-line" <polyline class="diagnostic-line stream-chart-rx" id="backend-dashboard-in-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -348,7 +354,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_requests" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_requests" hidden>
<span data-i18n="backendWidgetRequests">Backend request rate</span><strong <span data-i18n="backendWidgetRequests">Backend request rate</span><strong
id="backend-dashboard-requests">-</strong> id="backend-dashboard-requests">-</strong>
<svg data-i18n-aria="backendWidgetRequests" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetRequests" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-request-line" <polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-request-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -366,7 +373,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden>
<span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><strong <span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><strong
id="backend-dashboard-http5xx">-</strong> id="backend-dashboard-http5xx">-</strong>
<svg data-i18n-aria="backendWidgetHttp5xx" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetHttp5xx" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-5xx-line" <polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-5xx-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -375,7 +383,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_timeouts" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_timeouts" hidden>
<span data-i18n="backendWidgetTimeouts">backendWidgetTimeouts</span><strong <span data-i18n="backendWidgetTimeouts">backendWidgetTimeouts</span><strong
id="backend-dashboard-timeouts">-</strong> id="backend-dashboard-timeouts">-</strong>
<svg data-i18n-aria="backendWidgetTimeouts" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetTimeouts" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-timeout-line" <polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-timeout-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -384,7 +393,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_network" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_network" hidden>
<span data-i18n="backendWidgetNetwork">backendWidgetNetwork</span><strong <span data-i18n="backendWidgetNetwork">backendWidgetNetwork</span><strong
id="backend-dashboard-network">-</strong> id="backend-dashboard-network">-</strong>
<svg data-i18n-aria="backendWidgetNetwork" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetNetwork" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-network-line" <polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-network-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -393,7 +403,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_retries" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_retries" hidden>
<span data-i18n="backendWidgetRetries">backendWidgetRetries</span><strong <span data-i18n="backendWidgetRetries">backendWidgetRetries</span><strong
id="backend-dashboard-retries">-</strong> id="backend-dashboard-retries">-</strong>
<svg data-i18n-aria="backendWidgetRetries" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetRetries" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-retry-line" <polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-retry-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -402,7 +413,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_queue" hidden> <article class="stat stat-chart" data-dashboard-widget="backend_queue" hidden>
<span data-i18n="backendWidgetQueue">backendWidgetQueue</span><strong <span data-i18n="backendWidgetQueue">backendWidgetQueue</span><strong
id="backend-dashboard-queue">-</strong> id="backend-dashboard-queue">-</strong>
<svg data-i18n-aria="backendWidgetQueue" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="backendWidgetQueue" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-queue-line" <polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-queue-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -416,7 +428,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_process_cpu" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_process_cpu" hidden>
<span data-i18n="overviewExtraProcessCpu">overviewExtraProcessCpu</span><strong <span data-i18n="overviewExtraProcessCpu">overviewExtraProcessCpu</span><strong
id="overview-extra-extra_process_cpu">-</strong> id="overview-extra-extra_process_cpu">-</strong>
<svg data-i18n-aria="overviewExtraProcessCpu" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraProcessCpu" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_process_cpu-line" <polyline class="diagnostic-line" id="overview-extra-extra_process_cpu-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -425,7 +438,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_process_rss" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_process_rss" hidden>
<span data-i18n="overviewExtraProcessRss">overviewExtraProcessRss</span><strong <span data-i18n="overviewExtraProcessRss">overviewExtraProcessRss</span><strong
id="overview-extra-extra_process_rss">-</strong> id="overview-extra-extra_process_rss">-</strong>
<svg data-i18n-aria="overviewExtraProcessRss" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraProcessRss" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_process_rss-line" <polyline class="diagnostic-line" id="overview-extra-extra_process_rss-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -444,7 +458,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_open_connections" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_open_connections" hidden>
<span data-i18n="overviewExtraOpenConnections">overviewExtraOpenConnections</span><strong <span data-i18n="overviewExtraOpenConnections">overviewExtraOpenConnections</span><strong
id="overview-extra-extra_open_connections">-</strong> id="overview-extra-extra_open_connections">-</strong>
<svg data-i18n-aria="overviewExtraOpenConnections" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraOpenConnections" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_open_connections-line" <polyline class="diagnostic-line" id="overview-extra-extra_open_connections-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -453,7 +468,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_websocket_active" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_websocket_active" hidden>
<span data-i18n="overviewExtraWebsocketActive">overviewExtraWebsocketActive</span><strong <span data-i18n="overviewExtraWebsocketActive">overviewExtraWebsocketActive</span><strong
id="overview-extra-extra_websocket_active">-</strong> id="overview-extra-extra_websocket_active">-</strong>
<svg data-i18n-aria="overviewExtraWebsocketActive" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraWebsocketActive" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_websocket_active-line" <polyline class="diagnostic-line" id="overview-extra-extra_websocket_active-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -462,7 +478,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_tls_error_rate" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_tls_error_rate" hidden>
<span data-i18n="overviewExtraTlsErrorRate">overviewExtraTlsErrorRate</span><strong <span data-i18n="overviewExtraTlsErrorRate">overviewExtraTlsErrorRate</span><strong
id="overview-extra-extra_tls_error_rate">-</strong> id="overview-extra-extra_tls_error_rate">-</strong>
<svg data-i18n-aria="overviewExtraTlsErrorRate" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraTlsErrorRate" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_tls_error_rate-line" <polyline class="diagnostic-line" id="overview-extra-extra_tls_error_rate-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -471,7 +488,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_request_error_rate" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_request_error_rate" hidden>
<span data-i18n="overviewExtraRequestErrorRate">overviewExtraRequestErrorRate</span><strong <span data-i18n="overviewExtraRequestErrorRate">overviewExtraRequestErrorRate</span><strong
id="overview-extra-extra_request_error_rate">-</strong> id="overview-extra-extra_request_error_rate">-</strong>
<svg data-i18n-aria="overviewExtraRequestErrorRate" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraRequestErrorRate" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_request_error_rate-line" <polyline class="diagnostic-line" id="overview-extra-extra_request_error_rate-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -544,7 +562,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_response_latency" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_response_latency" hidden>
<span data-i18n="overviewExtraResponseLatency">overviewExtraResponseLatency</span><strong <span data-i18n="overviewExtraResponseLatency">overviewExtraResponseLatency</span><strong
id="overview-extra-extra_response_latency">-</strong> id="overview-extra-extra_response_latency">-</strong>
<svg data-i18n-aria="overviewExtraResponseLatency" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraResponseLatency" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_response_latency-line" <polyline class="diagnostic-line" id="overview-extra-extra_response_latency-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -553,7 +572,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_backend_unhealthy" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_backend_unhealthy" hidden>
<span data-i18n="overviewExtraBackendUnhealthy">overviewExtraBackendUnhealthy</span><strong <span data-i18n="overviewExtraBackendUnhealthy">overviewExtraBackendUnhealthy</span><strong
id="overview-extra-extra_backend_unhealthy">-</strong> id="overview-extra-extra_backend_unhealthy">-</strong>
<svg data-i18n-aria="overviewExtraBackendUnhealthy" role="img" viewBox="0 0 300 60"> <svg preserveAspectRatio="none" data-i18n-aria="overviewExtraBackendUnhealthy" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_backend_unhealthy-line" <polyline class="diagnostic-line" id="overview-extra-extra_backend_unhealthy-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -562,7 +582,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_total_rps" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_total_rps" hidden>
<span data-i18n="overviewExtraTotalRps">HTTP requests / s</span><strong <span data-i18n="overviewExtraTotalRps">HTTP requests / s</span><strong
id="overview-extra-extra_total_rps">-</strong> id="overview-extra-extra_total_rps">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="HTTP requests / s"> <svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="HTTP requests / s">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_total_rps-line" <polyline class="diagnostic-line" id="overview-extra-extra_total_rps-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -572,7 +593,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_active_exchanges" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_active_exchanges" hidden>
<span data-i18n="overviewExtraActiveExchanges">Active HTTP exchanges</span><strong <span data-i18n="overviewExtraActiveExchanges">Active HTTP exchanges</span><strong
id="overview-extra-extra_active_exchanges">-</strong> id="overview-extra-extra_active_exchanges">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="Active HTTP exchanges"> <svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="Active HTTP exchanges">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_active_exchanges-line" <polyline class="diagnostic-line" id="overview-extra-extra_active_exchanges-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -602,7 +624,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden>
<span data-i18n="overviewExtraCacheMissRate">Cache misses / s</span><strong <span data-i18n="overviewExtraCacheMissRate">Cache misses / s</span><strong
id="overview-extra-extra_cache_miss_rate">-</strong> id="overview-extra-extra_cache_miss_rate">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="Cache misses / s"> <svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="Cache misses / s">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_cache_miss_rate-line" <polyline class="diagnostic-line" id="overview-extra-extra_cache_miss_rate-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -612,7 +635,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_backend_active" hidden> <article class="stat stat-chart" data-dashboard-widget="extra_backend_active" hidden>
<span data-i18n="overviewExtraBackendActive">In-flight backend requests</span><strong <span data-i18n="overviewExtraBackendActive">In-flight backend requests</span><strong
id="overview-extra-extra_backend_active">-</strong> id="overview-extra-extra_backend_active">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="In-flight backend requests"> <svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="In-flight backend requests">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_backend_active-line" <polyline class="diagnostic-line" id="overview-extra-extra_backend_active-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -625,7 +649,7 @@
<span class="overview-wide-summary" <span class="overview-wide-summary"
id="overview-wide-wide_protocol_rates-summary">-</span> id="overview-wide-wide_protocol_rates-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="HTTP/1.1 · HTTP/2 · HTTP/3 traffic"> aria-label="HTTP/1.1 · HTTP/2 · HTTP/3 traffic">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -649,7 +673,7 @@
<h2 data-i18n="overviewWideCpu">System and process CPU</h2> <h2 data-i18n="overviewWideCpu">System and process CPU</h2>
<span class="overview-wide-summary" id="overview-wide-wide_cpu-summary">-</span> <span class="overview-wide-summary" id="overview-wide-wide_cpu-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="System and process CPU"> aria-label="System and process CPU">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -671,7 +695,7 @@
<h2 data-i18n="overviewWideMemory">Process RAM and cache memory</h2> <h2 data-i18n="overviewWideMemory">Process RAM and cache memory</h2>
<span class="overview-wide-summary" id="overview-wide-wide_memory-summary">-</span> <span class="overview-wide-summary" id="overview-wide-wide_memory-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Process RAM and cache memory"> aria-label="Process RAM and cache memory">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -694,7 +718,7 @@
<span class="overview-wide-summary" <span class="overview-wide-summary"
id="overview-wide-wide_cache_activity-summary">-</span> id="overview-wide-wide_cache_activity-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Cache hits / misses per second"> aria-label="Cache hits / misses per second">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -715,7 +739,7 @@
<h2 data-i18n="overviewWideConnections">Connections and WebSocket tunnels</h2> <h2 data-i18n="overviewWideConnections">Connections and WebSocket tunnels</h2>
<span class="overview-wide-summary" id="overview-wide-wide_connections-summary">-</span> <span class="overview-wide-summary" id="overview-wide-wide_connections-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Connections and WebSocket tunnels"> aria-label="Connections and WebSocket tunnels">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -738,7 +762,7 @@
<span class="overview-wide-summary" <span class="overview-wide-summary"
id="overview-wide-wide_backend_health-summary">-</span> id="overview-wide-wide_backend_health-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Backend availability over time"> aria-label="Backend availability over time">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -760,7 +784,7 @@
<h2 data-i18n="overviewWideLatency">Response and backend probe latency</h2> <h2 data-i18n="overviewWideLatency">Response and backend probe latency</h2>
<span class="overview-wide-summary" id="overview-wide-wide_latency-summary">-</span> <span class="overview-wide-summary" id="overview-wide-wide_latency-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Response and backend probe latency"> aria-label="Response and backend probe latency">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -783,7 +807,7 @@
<span class="overview-wide-summary" <span class="overview-wide-summary"
id="overview-wide-wide_transform_activity-summary">-</span> id="overview-wide-wide_transform_activity-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
data-i18n-aria="overviewWideTransformActivity" aria-label="Transformation activity"> data-i18n-aria="overviewWideTransformActivity" aria-label="Transformation activity">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -816,7 +840,7 @@
<span class="overview-wide-summary" <span class="overview-wide-summary"
id="overview-wide-wide_compression_activity-summary">-</span> id="overview-wide-wide_compression_activity-summary">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
data-i18n-aria="overviewWideCompressionActivity" aria-label="Compression activity"> data-i18n-aria="overviewWideCompressionActivity" aria-label="Compression activity">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" <path class="overview-wide-series overview-wide-series-0"
@@ -915,8 +939,8 @@
<article class="stat stat-chart" data-dashboard-widget="www_php_requests" hidden> <article class="stat stat-chart" data-dashboard-widget="www_php_requests" hidden>
<span data-i18n="webMetricPhpRequests">PHP requests/s</span><strong <span data-i18n="webMetricPhpRequests">PHP requests/s</span><strong
id="www_php_requests-value">-</strong> id="www_php_requests-value">-</strong>
<svg role="img" viewBox="0 0 300 60" aria-label="PHP requests per second" <svg preserveAspectRatio="none" role="img" viewBox="0 0 300 60"
data-i18n-aria="webMetricPhpRequests"> aria-label="PHP requests per second" data-i18n-aria="webMetricPhpRequests">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="www_php_requests-line" <polyline class="diagnostic-line proto-h2-line" id="www_php_requests-line"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -928,7 +952,7 @@
<h2 data-i18n="webMetricRequests">webMetricRequests</h2><span <h2 data-i18n="webMetricRequests">webMetricRequests</h2><span
class="overview-wide-summary" id="www_requests-value">-</span> class="overview-wide-summary" id="www_requests-value">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server request rate" data-i18n-aria="webMetricRequests"> aria-label="Web Server request rate" data-i18n-aria="webMetricRequests">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_requests-all-line" <path class="overview-wide-series overview-wide-series-0" id="www_requests-all-line"
@@ -949,7 +973,7 @@
<h2 data-i18n="webMetricLatency">webMetricLatency</h2><span <h2 data-i18n="webMetricLatency">webMetricLatency</h2><span
class="overview-wide-summary" id="www_latency-value">-</span> class="overview-wide-summary" id="www_latency-value">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server header latency" data-i18n-aria="webMetricLatency"> aria-label="Web Server header latency" data-i18n-aria="webMetricLatency">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_latency-web-line" <path class="overview-wide-series overview-wide-series-0" id="www_latency-web-line"
@@ -970,7 +994,7 @@
<h2 data-i18n="webMetricFailures">webMetricFailures</h2><span <h2 data-i18n="webMetricFailures">webMetricFailures</h2><span
class="overview-wide-summary" id="www_failures-value">-</span> class="overview-wide-summary" id="www_failures-value">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="PHP FastCGI failures" data-i18n-aria="webMetricFailures"> aria-label="PHP FastCGI failures" data-i18n-aria="webMetricFailures">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_failures-timeout-line" <path class="overview-wide-series overview-wide-series-0" id="www_failures-timeout-line"
@@ -1001,7 +1025,7 @@
<h2 data-i18n="webMetricTraffic">webMetricTraffic</h2><span <h2 data-i18n="webMetricTraffic">webMetricTraffic</h2><span
class="overview-wide-summary" id="www_traffic-value">-</span> class="overview-wide-summary" id="www_traffic-value">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server body byte rate" data-i18n-aria="webMetricTraffic"> aria-label="Web Server body byte rate" data-i18n-aria="webMetricTraffic">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_traffic-nonphp-line" <path class="overview-wide-series overview-wide-series-0" id="www_traffic-nonphp-line"
@@ -1022,7 +1046,7 @@
<h2 data-i18n="webMetricStatusTrend">webMetricStatusTrend</h2><span <h2 data-i18n="webMetricStatusTrend">webMetricStatusTrend</h2><span
class="overview-wide-summary" id="www_status_trend-value">-</span> class="overview-wide-summary" id="www_status_trend-value">-</span>
</div> </div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img" <svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server HTTP statuses" data-i18n-aria="webMetricStatusTrend"> aria-label="Web Server HTTP statuses" data-i18n-aria="webMetricStatusTrend">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path> <path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_status_trend-2xx-line" <path class="overview-wide-series overview-wide-series-0" id="www_status_trend-2xx-line"
@@ -1062,8 +1086,8 @@
<div><span class="small-label" data-i18n="liveTraffic">LIVE TRAFFIC</span> <div><span class="small-label" data-i18n="liveTraffic">LIVE TRAFFIC</span>
<h2 data-i18n="requestsPerSecond">Requests per second</h2> <h2 data-i18n="requestsPerSecond">Requests per second</h2>
</div><span class="pill live" data-i18n="live">LIVE</span> </div><span class="pill live" data-i18n="live">LIVE</span>
</div><svg aria-label="Recent request rate" id="traffic-chart" role="img" </div><svg preserveAspectRatio="none" aria-label="Recent request rate" id="traffic-chart"
viewbox="0 0 600 130"> role="img" viewbox="0 0 600 130">
<path class="chart-grid" d="M0 25H600 M0 65H600 M0 105H600"></path> <path class="chart-grid" d="M0 25H600 M0 65H600 M0 105H600"></path>
<polyline class="traffic-line traffic-line-total" id="sparkline" points="0,120 600,120"> <polyline class="traffic-line traffic-line-total" id="sparkline" points="0,120 600,120">
</polyline> </polyline>
@@ -1095,8 +1119,8 @@
<div><span class="small-label" data-i18n="systemLoad">SYSTEM LOAD</span> <div><span class="small-label" data-i18n="systemLoad">SYSTEM LOAD</span>
<h2><span id="system-cpu">-</span> <small>CPU</small></h2> <h2><span id="system-cpu">-</span> <small>CPU</small></h2>
</div><span class="meta" id="system-source">-</span> </div><span class="meta" id="system-source">-</span>
</div><svg aria-label="System CPU history" data-i18n-aria="systemChart" id="system-chart" </div><svg preserveAspectRatio="none" aria-label="System CPU history"
role="img" viewbox="0 0 300 100"> data-i18n-aria="systemChart" id="system-chart" role="img" viewbox="0 0 300 100">
<path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path> <path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path>
<polyline id="system-sparkline" points="0,95 300,95"></polyline> <polyline id="system-sparkline" points="0,95 300,95"></polyline>
</svg> </svg>
@@ -1121,8 +1145,9 @@
connections</small></h2> connections</small></h2>
</div><span class="pill live" data-i18n="live">LIVE</span> </div><span class="pill live" data-i18n="live">LIVE</span>
</div> </div>
<svg aria-label="Connection activity history" data-i18n-aria="connectionActivity" <svg preserveAspectRatio="none" aria-label="Connection activity history"
id="connection-chart" role="img" viewbox="0 0 300 100"> data-i18n-aria="connectionActivity" id="connection-chart" role="img"
viewbox="0 0 300 100">
<path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path> <path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path>
<polyline class="connection-open-line" id="connection-open-sparkline" <polyline class="connection-open-line" id="connection-open-sparkline"
points="0,95 300,95"></polyline> points="0,95 300,95"></polyline>
@@ -1171,8 +1196,8 @@
<div class="mini-live-chart"> <div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="cacheOutcomeHistory">HIT / MISS / <div class="mini-live-chart-head"><span data-i18n="cacheOutcomeHistory">HIT / MISS /
BYPASS activity</span><b id="cache-outcome-chart-value">-</b></div> BYPASS activity</span><b id="cache-outcome-chart-value">-</b></div>
<svg aria-label="HIT MISS BYPASS activity" data-i18n-aria="cacheOutcomeHistory" <svg preserveAspectRatio="none" aria-label="HIT MISS BYPASS activity"
role="img" viewbox="0 0 300 60"> data-i18n-aria="cacheOutcomeHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-hit-line" id="cache-hit-sparkline" <polyline class="diagnostic-line cache-hit-line" id="cache-hit-sparkline"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -1189,8 +1214,8 @@
<div class="mini-live-chart"> <div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="cacheHotMemoryHistory">HOT RAM <div class="mini-live-chart-head"><span data-i18n="cacheHotMemoryHistory">HOT RAM
usage / limit</span><b id="cache-chart-value">-</b></div> usage / limit</span><b id="cache-chart-value">-</b></div>
<svg aria-label="HOT cache RAM usage history" data-i18n-aria="cacheHotMemoryHistory" <svg preserveAspectRatio="none" aria-label="HOT cache RAM usage history"
role="img" viewbox="0 0 300 60"> data-i18n-aria="cacheHotMemoryHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-hot-line" id="cache-memory-sparkline" <polyline class="diagnostic-line cache-hot-line" id="cache-memory-sparkline"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -1200,7 +1225,7 @@
<div class="mini-live-chart-head"><span <div class="mini-live-chart-head"><span
data-i18n="cacheTransientMemoryHistory">Transient data-i18n="cacheTransientMemoryHistory">Transient
RAM usage / limit</span><b id="cache-transient-chart-value">-</b></div> RAM usage / limit</span><b id="cache-transient-chart-value">-</b></div>
<svg aria-label="Transient cache RAM usage history" <svg preserveAspectRatio="none" aria-label="Transient cache RAM usage history"
data-i18n-aria="cacheTransientMemoryHistory" role="img" viewbox="0 0 300 60"> data-i18n-aria="cacheTransientMemoryHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-transient-total-line" <polyline class="diagnostic-line cache-transient-total-line"
@@ -1223,7 +1248,7 @@
<div class="mini-live-chart-head"><span data-i18n="cacheColdStorageHistory">COLD <div class="mini-live-chart-head"><span data-i18n="cacheColdStorageHistory">COLD
storage storage
usage / limit</span><b id="cache-cold-chart-value">-</b></div> usage / limit</span><b id="cache-cold-chart-value">-</b></div>
<svg aria-label="COLD storage usage history" <svg preserveAspectRatio="none" aria-label="COLD storage usage history"
data-i18n-aria="cacheColdStorageHistory" role="img" viewbox="0 0 300 60"> data-i18n-aria="cacheColdStorageHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-cold-line" id="cache-cold-sparkline" <polyline class="diagnostic-line cache-cold-line" id="cache-cold-sparkline"
@@ -1325,8 +1350,8 @@
<div class="mini-live-chart"> <div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="processCpuHistory">Process CPU <div class="mini-live-chart-head"><span data-i18n="processCpuHistory">Process CPU
history</span><b id="process-cpu-chart-value">-</b></div> history</span><b id="process-cpu-chart-value">-</b></div>
<svg aria-label="Process CPU history" data-i18n-aria="processCpuHistory" role="img" <svg preserveAspectRatio="none" aria-label="Process CPU history"
viewbox="0 0 300 60"> data-i18n-aria="processCpuHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="process-cpu-sparkline" <polyline class="diagnostic-line" id="process-cpu-sparkline"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -1335,8 +1360,8 @@
<div class="mini-live-chart"> <div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="processRssHistory">Process RSS <div class="mini-live-chart-head"><span data-i18n="processRssHistory">Process RSS
history</span><b id="process-rss-chart-value">-</b></div> history</span><b id="process-rss-chart-value">-</b></div>
<svg aria-label="Process RSS history" data-i18n-aria="processRssHistory" role="img" <svg preserveAspectRatio="none" aria-label="Process RSS history"
viewbox="0 0 300 60"> data-i18n-aria="processRssHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line process-rss-line" id="process-rss-sparkline" <polyline class="diagnostic-line process-rss-line" id="process-rss-sparkline"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -1367,8 +1392,8 @@
<div class="mini-live-chart"> <div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="networkHistory">RX / TX <div class="mini-live-chart-head"><span data-i18n="networkHistory">RX / TX
history</span><b id="network-chart-value">-</b></div> history</span><b id="network-chart-value">-</b></div>
<svg aria-label="Network throughput history" data-i18n-aria="networkHistory" <svg preserveAspectRatio="none" aria-label="Network throughput history"
role="img" viewbox="0 0 300 60"> data-i18n-aria="networkHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line network-rx-line" id="network-rx-sparkline" <polyline class="diagnostic-line network-rx-line" id="network-rx-sparkline"
points="0,55 300,55"></polyline> points="0,55 300,55"></polyline>
@@ -1395,7 +1420,7 @@
</div> </div>
</details><b id="latency-chart-value">-</b> </details><b id="latency-chart-value">-</b>
</span></div> </span></div>
<svg aria-label="Mean response latency history" <svg preserveAspectRatio="none" aria-label="Mean response latency history"
data-i18n-aria="responseLatencyHistory" role="img" viewbox="0 0 300 60"> data-i18n-aria="responseLatencyHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path> <path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="latency-sparkline" points="0,55 300,55"> <polyline class="diagnostic-line" id="latency-sparkline" points="0,55 300,55">
+246 -44
View File
@@ -72,6 +72,9 @@
--rp-progress-fill: var(--chart-accent); --rp-progress-fill: var(--chart-accent);
--rp-progress-track: var(--line); --rp-progress-track: var(--line);
--rp-progress-radius: 3px; --rp-progress-radius: 3px;
--rp-nav-active-color: var(--brand-secondary);
--rp-nav-active-bg: var(--nav-active-bg);
--rp-nav-hover-bg: var(--nav-hover-bg);
--traffic-h1: var(--chart-accent); --traffic-h1: var(--chart-accent);
--traffic-h2: var(--chart-secondary); --traffic-h2: var(--chart-secondary);
--traffic-h3: var(--chart-tertiary); --traffic-h3: var(--chart-tertiary);
@@ -135,6 +138,10 @@ html[data-theme=light] {
--scrollbar-thumb-hover: #879684 --scrollbar-thumb-hover: #879684
} }
html:is([data-theme=light], [data-theme=cream]) {
--rp-nav-active-color: color-mix(in srgb, var(--brand-secondary) 45%, var(--text) 55%);
}
html[data-theme=ultra_dark] { html[data-theme=ultra_dark] {
color-scheme: dark; color-scheme: dark;
--bg: #030404; --bg: #030404;
@@ -389,16 +396,6 @@ html.app-startup-failed .app-startup-recovery {
} }
} }
@media(prefers-reduced-motion:reduce) {
.app-startup-spinner {
animation: none
}
* {
scroll-behavior: auto !important
}
}
body { body {
margin: 0; margin: 0;
background: var(--bg); background: var(--bg);
@@ -533,7 +530,8 @@ nav {
text-align: left; text-align: left;
background: transparent; background: transparent;
color: var(--muted); color: var(--muted);
border-left: 2px solid transparent border-left: 2px solid transparent;
border-radius: var(--rp-radius-control, 0)
} }
.nav span { .nav span {
@@ -546,13 +544,18 @@ nav {
} }
.nav.active { .nav.active {
background: var(--nav-active-bg); background: var(--rp-nav-active-bg);
color: var(--green); color: var(--rp-nav-active-color);
border-left-color: var(--green) border-left-color: var(--rp-nav-active-color)
} }
.nav:hover { .nav:not(.active):hover {
background: var(--nav-hover-bg) background: var(--rp-nav-hover-bg)
}
.sidebar .main-navigation .nav:focus-visible {
outline: 2px solid var(--rp-nav-active-color);
outline-offset: 2px
} }
.sidebar-bottom { .sidebar-bottom {
@@ -1213,7 +1216,7 @@ button.is-saving {
} }
.nav.active { .nav.active {
border-bottom-color: var(--green) border-bottom-color: var(--rp-nav-active-color)
} }
h1 { h1 {
@@ -1270,7 +1273,8 @@ button.is-saving {
letter-spacing: .4px letter-spacing: .4px
} }
.breadcrumb span { .breadcrumb span,
.login-footer span {
margin: 0 5px margin: 0 5px
} }
@@ -1360,10 +1364,6 @@ button.is-saving {
padding: 20px padding: 20px
} }
.login-footer span {
margin: 0 5px
}
.toast { .toast {
right: 17px; right: 17px;
bottom: 17px bottom: 17px
@@ -2077,7 +2077,7 @@ td {
} }
[dir=rtl] .nav.active { [dir=rtl] .nav.active {
border-right-color: var(--green) border-right-color: var(--rp-nav-active-color)
} }
[dir=rtl] table { [dir=rtl] table {
@@ -7879,9 +7879,9 @@ button.config-history-day.is-selected {
.mobile-menu-toggle:hover, .mobile-menu-toggle:hover,
.mobile-menu-toggle[aria-expanded=true] { .mobile-menu-toggle[aria-expanded=true] {
color: var(--text); color: var(--rp-nav-active-color);
border-color: var(--border-hover); border-color: var(--rp-nav-active-color);
background: var(--nav-active-bg); background: var(--rp-nav-active-bg);
} }
.mobile-menu-icon { .mobile-menu-icon {
@@ -7921,8 +7921,13 @@ button.config-history-day.is-selected {
.sidebar .main-navigation .nav.active { .sidebar .main-navigation .nav.active {
border-color: var(--line); border-color: var(--line);
border-left-color: var(--green); border-left-color: var(--rp-nav-active-color);
background: var(--nav-active-bg); background: var(--rp-nav-active-bg);
}
[dir=rtl] .sidebar .main-navigation .nav.active {
border-left-color: var(--line);
border-right-color: var(--rp-nav-active-color);
} }
} }
@@ -8789,7 +8794,7 @@ html[data-dashboard-density=ultra] #panel-overview .stats-grid {
} }
html[data-dashboard-density=normal] #panel-overview .stat { html[data-dashboard-density=normal] #panel-overview .stat {
padding: 14px 14px; padding: 14px;
} }
html[data-dashboard-density=normal] #panel-overview .stat>span { html[data-dashboard-density=normal] #panel-overview .stat>span {
@@ -8854,8 +8859,6 @@ html[data-dashboard-density=ultra] #panel-overview .stat-chart svg {
height: 24px; height: 24px;
} }
/* Dashboard density applies to charts and panels as well as number tiles.
Keep the Overview grid gutters independent of dashboard density. */
html[data-dashboard-density=normal] #panel-overview .stat-chart svg { html[data-dashboard-density=normal] #panel-overview .stat-chart svg {
height: 34px; height: 34px;
} }
@@ -10220,12 +10223,10 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
.overview-layout-grid { .overview-layout-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--ui-gap, 10px); gap: var(--ui-gap, 10px);
} }
.overview-layout-tile { .overview-layout-tile {
min-width: 0;
border: 1px solid var(--line); border: 1px solid var(--line);
background: var(--panel); background: var(--panel);
cursor: grab; cursor: grab;
@@ -10246,7 +10247,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
.overview-layout-tile-toolbar { .overview-layout-tile-toolbar {
display: flex; display: flex;
align-items: center;
gap: 3px; gap: 3px;
padding: 4px; padding: 4px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
@@ -10817,11 +10817,11 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
} }
html[data-dashboard-density=normal] #panel-overview .dashboard-overview-grid.is-aligned>.stat strong { html[data-dashboard-density=normal] #panel-overview .dashboard-overview-grid.is-aligned>.stat strong {
font-size: clamp(20px, 5.8vw, 26px); font-size: calc(clamp(20px, 5.8vw, 26px));
} }
html[data-dashboard-density=compact] #panel-overview .dashboard-overview-grid.is-aligned>.stat strong { html[data-dashboard-density=compact] #panel-overview .dashboard-overview-grid.is-aligned>.stat strong {
font-size: clamp(19px, 5vw, 22px); font-size: calc(clamp(19px, 5vw, 22px));
} }
#panel-overview .dashboard-overview-grid.is-aligned>.card:not(.pool-card) { #panel-overview .dashboard-overview-grid.is-aligned>.card:not(.pool-card) {
@@ -10883,6 +10883,7 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
} }
#panel-overview .overview-wide-svg .chart-grid { #panel-overview .overview-wide-svg .chart-grid {
vector-effect: non-scaling-stroke;
stroke: var(--line); stroke: var(--line);
fill: none; fill: none;
stroke-width: 1; stroke-width: 1;
@@ -10891,6 +10892,7 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
#panel-overview .overview-wide-series { #panel-overview .overview-wide-series {
fill: none; fill: none;
vector-effect: non-scaling-stroke;
stroke-width: 2; stroke-width: 2;
stroke-linejoin: round; stroke-linejoin: round;
stroke-linecap: round; stroke-linecap: round;
@@ -10964,7 +10966,6 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
.overview-layout-tile-toolbar .overview-layout-controls { .overview-layout-tile-toolbar .overview-layout-controls {
display: inline-flex; display: inline-flex;
align-items: center;
gap: 3px; gap: 3px;
margin-left: auto; margin-left: auto;
} }
@@ -11260,11 +11261,9 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
height: 100%; height: 100%;
border-radius: inherit; border-radius: inherit;
background: var(--rp-progress-fill); background: var(--rp-progress-fill);
transition: width 180ms ease; transition: width .18s;
} }
/* System Load uses the same track/fill component as other Overview meters,
while retaining its compact 5px height and existing vertical spacing. */
#panel-overview .system-card .system-memory-meter { #panel-overview .system-card .system-memory-meter {
display: block; display: block;
width: 100%; width: 100%;
@@ -11374,7 +11373,15 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
} }
} }
@media (prefers-reduced-motion: reduce) { @media(prefers-reduced-motion:reduce) {
.app-startup-spinner {
animation: none
}
* {
scroll-behavior: auto !important
}
#panel-overview .overview-meter-fill { #panel-overview .overview-meter-fill {
transition: none; transition: none;
} }
@@ -11423,7 +11430,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
white-space: nowrap; white-space: nowrap;
} }
@media (max-width: 700px) { @media(max-width:700px) {
.page-intro .overview-samples-toolbar { .page-intro .overview-samples-toolbar {
margin-left: 0; margin-left: 0;
justify-content: flex-start; justify-content: flex-start;
@@ -11514,7 +11521,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
#panel-overview .www-segmented span { #panel-overview .www-segmented span {
display: block; display: block;
height: 100%; height: 100%;
transition: width 180ms ease; transition: width .18s;
} }
#panel-overview .www-segment-0 { #panel-overview .www-segment-0 {
@@ -11639,8 +11646,203 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
margin-top: 4px; margin-top: 4px;
} }
@media (prefers-reduced-motion: reduce) { @media(prefers-reduced-motion:reduce) {
#panel-overview .www-segmented span { #panel-overview .www-segmented span {
transition: none; transition: none;
} }
}
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom] {
grid-auto-flow: row dense;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"] {
grid-column: auto/span 2;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="3"] {
grid-column: auto/span 3;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"] {
grid-column: auto/span 4;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="6"] {
grid-column: auto/span 6;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="12"] {
grid-column: 1/-1;
}
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom]>[data-dashboard-widget].overview-row-start {
grid-column-start: auto;
}
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom] .overview-wide-legend,
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom] .stat-chart-legend {
flex-wrap: wrap;
white-space: normal;
}
#panel-overview .overview-inline-width,
.overview-layout-width-value {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
font: 11px var(--mono);
color: var(--text);
}
#panel-overview .overview-inline-width {
gap: 3px;
flex: 0 0 auto;
}
#panel-overview .overview-inline-width-value {
min-width: 31px;
text-align: center;
}
#panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] {
padding-top: 101px;
}
#panel-overview .overview-inline-toolbar .overview-widget-type {
margin-right: auto;
}
.overview-layout-tile-toolbar .overview-layout-width-value {
flex: 0 0 auto;
min-width: 30px;
text-align: center;
}
.overview-layout-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-flow: row;
}
.overview-layout-grid>.overview-layout-tile.is-panel {
grid-column: auto;
}
.overview-layout-tile {
min-width: 0;
}
.overview-layout-tile-toolbar {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 5px 6px;
align-items: center;
min-width: 0;
}
.overview-layout-tile-toolbar .overview-widget-type {
min-width: 0;
max-width: 100%;
grid-column: 2;
}
.overview-layout-tile-toolbar .overview-layout-controls {
grid-column: 1/-1;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 4px 8px;
min-width: 0;
width: 100%;
margin: 0;
}
.overview-layout-order-actions,
.overview-layout-size-actions {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 3px;
min-width: 0;
}
.overview-layout-tile-toolbar .overview-layout-hide {
margin-inline-start: 0;
white-space: normal;
overflow-wrap: anywhere;
}
.overview-layout-grid .overview-layout-card-preview,
.overview-layout-grid .overview-layout-panel-preview {
min-width: 0;
max-width: 100%;
}
.overview-layout-dialog .overview-layout-footer,
.overview-layout-dialog .overview-layout-hidden-item {
flex-wrap: wrap;
}
.overview-layout-dialog .overview-layout-hidden-item>span:first-child {
flex: 1 1 125px;
}
.overview-layout-dialog .overview-layout-hidden-item>button {
flex: 0 1 auto;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
}
@container overview-canvas (max-width: 1080px) {
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"] {
grid-column: auto/span 2;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="3"] {
grid-column: auto/span 3;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"] {
grid-column: auto/span 4;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="12"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="6"] {
grid-column: 1/-1;
}
}
@container overview-canvas (max-width: 700px) {
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"].stat {
grid-column: auto/span 1;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="12"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"]:not(.stat),
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="3"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="6"] {
grid-column: 1/-1;
}
}
@media(max-width:900px) {
.overview-layout-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media(max-width:620px) {
.overview-layout-grid {
grid-template-columns: minmax(0, 1fr);
}
}
@media(max-width:550px) {
#panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] {
padding-top: 135px;
}
} }