v0.33.3
This commit is contained in:
1 parent
5a36bb0c4b
commit
78ad28ef4d
27 files changed
+1446
-232
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
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
|
||||
compatibility. Current source version: 0.33.2.
|
||||
compatibility. Current source version: 0.33.3.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -3,6 +3,37 @@
|
||||
> Operator-facing release history. Historical engineering notes are kept in
|
||||
> [`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
|
||||
|
||||
- Synchronize Cache occupancy progressbar colors with the selected Appearance
|
||||
|
||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.33.2"
|
||||
version = "0.33.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.33.2"
|
||||
version = "0.33.3"
|
||||
edition = "2021"
|
||||
rust-version = "1.99"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+22
-1
@@ -1,5 +1,27 @@
|
||||
# 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
|
||||
|
||||
- [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
|
||||
region, while retaining chart SVG/text nodes on ordinary live updates.
|
||||
|
||||
|
||||
## 0.32.6
|
||||
|
||||
- [x] Reinstate two small Overview cards per row across phone-size widths.
|
||||
|
||||
+26
-22
@@ -1,13 +1,13 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
78027147942d2a355ad3bb92492faf70917908df879b10330bb9e42edfbd5d11 AGENTS.md
|
||||
9a5f4c3478b4fba206fc8a3d6616f86b2cf2d640ee51f935aa5b0baeb9c0d7b7 CHANGELOG.md
|
||||
53338adb66b8c6c345d742f1a8fca48b7965e70ee072b2f4069020d3d73f1be7 Cargo.lock
|
||||
5ab876b82db0e76ae52ac1723555ebeef9182e1feedabd0a72f936d0940e4373 Cargo.toml
|
||||
676a30c813406f4e17972ee8bea175a7e3d5ee282158283a9cbe920f2b65da7f AGENTS.md
|
||||
3a6bb0fdf41ef4519beefa9f4d9c124a4883c304e49e6a76bb64cd5333ffb174 CHANGELOG.md
|
||||
1a59436e03172328606fc0c2142da05319e7673ae2a87197409a06aa22d87b27 Cargo.lock
|
||||
551040f53df829264a6784c9a2ab4fe2cb4299d5d4f517857a74ee58797ab5b7 Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
f73f11aade119401aff40f7929f08b9f8f889fbf89d0cd8be7a1efd69487fffd README.md
|
||||
55a71d11fb998cf31874d1e7669175b51251f38f48349538b94b03e27283726c ROADMAP.md
|
||||
6aa95661d76c2ce0f317fe7004791568dbcc552ccff87fe6bdd2f0f235d6ef7c README.md
|
||||
97becee1b75d0dde90dd412e8fdb8aa68fd11c5e1872bc15095ac6632b2ea0e9 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||
@@ -35,7 +35,7 @@ fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md
|
||||
3822d72e12f82c37a9e70ab40c7e89e89145ad37f29914e4702db4bf024a5501 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
||||
c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md
|
||||
833ddb12f23dcac796223521388b19c6ec3ea04c5550cf1c5f4ea4eb7c58f3a0 docs/CUSTOM_LOCATIONS.md
|
||||
15153ba0eb325b374aa3a278f677900516665c2b131b55b6074ec5105924f41d docs/DASHBOARD_PRESET.md
|
||||
3cbcf562114fae47f01ed4614d49844b88e7a4d8aee7eb724400d5681be90936 docs/DASHBOARD_PRESET.md
|
||||
4d70797bda8d227787fcff897951e702e2458fc6c6f5352aa0b5577595bd834b docs/DOCKER_DISCOVERY.md
|
||||
46811a09a03d83f4af686c33e30e3f94d0508afe2c75147a26885c7247c49cbd docs/ERROR_LOGGING.md
|
||||
f749f7bd2eb4ae9c484f77a3cd8cd5ff21445319a3938e0384a3414134fe713a docs/FILE_LOGS.md
|
||||
@@ -64,7 +64,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
|
||||
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
||||
4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md
|
||||
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
|
||||
87279fa139a62cca82a4c97c1edae0b97e70ed9ff09af55773faecf581be763f docs/TEST_REPORT.md
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
||||
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
|
||||
@@ -83,7 +83,7 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
|
||||
cb4fe36423eca2cbc7961df04f28c6870e4303e4c16c7fe2d3f7b7bbb04d8dad grafana/rustproxy-overview.json
|
||||
ea8aaf0257a5ab75153ee2fd383a4d5c2ac725c47042f9770949451636d442f1 grafana/rustproxy-overview.json
|
||||
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
|
||||
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
||||
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
||||
@@ -96,8 +96,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||
b5922257df0009d3e0778d2a08c926952fed6db6c013049e04ca8b63eaa6df6d langs/en_US.json
|
||||
52a3d1dcc5e2517f4906fb71b1c6d9c915036a0d69e9ceec143bb2b27582611b langs/pl_pl.json
|
||||
fe738b800824e8700fb8a4796b7316f5d4882e0d43aed7e743fc996d42aa99c1 langs/en_US.json
|
||||
d9878b53adc3bdcf20fecb6b4ae6270158c9150e9df39186dc2c4b571d948aa2 langs/pl_pl.json
|
||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -130,10 +130,14 @@ e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtim
|
||||
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.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
|
||||
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
|
||||
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
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py
|
||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py
|
||||
@@ -166,13 +170,13 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
|
||||
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
||||
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
|
||||
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
|
||||
fc07433afa4c152e6ad5cc95676a0727fdb8a48e04a22c2b761e4a4f125a9e93 src/admin/openapi.json
|
||||
8e20a424d5e251ac713baaee063d9dfddcfe06063ee68eb9eac37bf9215c7664 src/admin/openapi.json
|
||||
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||
b0f0c1799439f596fbd3c68126fb1ea229c75dfa3ebb48abff1cd5da355e993f src/admin/preferences.rs
|
||||
a02e17edd5eeb3a842658235af0a73b66e70e870e725ff9b673b76de840e42fc src/admin/preferences_columns.rs
|
||||
908ef7bb71496a3cb2a565147a427d61026bb4d01d804d76bcb10f071b9122a3 src/admin/preferences_tests.rs
|
||||
92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs
|
||||
4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs
|
||||
9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs
|
||||
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
|
||||
@@ -187,16 +191,16 @@ cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail
|
||||
c0d08c5b8fc13dec9bf53fd842d224b9df3874fc3bdf31413e3ab182a8cec2aa src/admin/web/app/backend-dashboard.js
|
||||
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
|
||||
8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js
|
||||
e71f77d459d7894f6a0920e9adbe6393c85ed3113e40582fde0c814561da5fd3 src/admin/web/app/dashboard-inline-layout.js
|
||||
dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/app/dashboard-layout-editor.js
|
||||
f3b566d79ba47848021a74653dbd7fec72f0fb819c6b2a07259e047bc8c427fd src/admin/web/app/dashboard-inline-layout.js
|
||||
851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.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
|
||||
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
|
||||
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
|
||||
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
||||
8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js
|
||||
@@ -226,7 +230,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.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
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||
@@ -236,7 +240,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
|
||||
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
|
||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||
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
|
||||
69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js
|
||||
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
the newly built binary. The preset is **embedded**; editing the installed
|
||||
source JSON alone does not update a running binary.
|
||||
|
||||
@@ -5640,7 +5640,7 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "RustProxy 0.33.2 - Operations",
|
||||
"title": "RustProxy 0.33.3 - Operations",
|
||||
"uid": "rustproxy-0170",
|
||||
"version": 6,
|
||||
"weekStart": "",
|
||||
|
||||
+5
-1
@@ -2705,5 +2705,9 @@
|
||||
"webLiveKinds": "Static / PHP / other",
|
||||
"webLiveBytes": "Body delivered",
|
||||
"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
@@ -2705,5 +2705,9 @@
|
||||
"webLiveKinds": "Statyczne / PHP / inne",
|
||||
"webLiveBytes": "Wysłana treść",
|
||||
"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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"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."
|
||||
},
|
||||
"servers": [
|
||||
@@ -3413,7 +3413,7 @@
|
||||
"dashboard_widgets": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 62,
|
||||
"maxItems": 78,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
@@ -3479,7 +3479,23 @@
|
||||
"cache_diagnostics",
|
||||
"process_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": [
|
||||
@@ -3548,9 +3564,19 @@
|
||||
],
|
||||
"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": {
|
||||
"type": "array",
|
||||
"maxItems": 62,
|
||||
"maxItems": 78,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
@@ -3616,7 +3642,23 @@
|
||||
"cache_diagnostics",
|
||||
"process_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": [
|
||||
|
||||
+18
-23
@@ -27,29 +27,6 @@ fn default_admin_table_density() -> String {
|
||||
fn default_admin_dashboard_density() -> String {
|
||||
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
|
||||
// binary, and append new registry widgets according to their opt-in policy.
|
||||
// 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| {
|
||||
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() {
|
||||
if !seen.contains(&widget.id) {
|
||||
prefs.dashboard_widgets.push(widget.id.clone());
|
||||
@@ -147,6 +131,10 @@ struct AdminPreferences {
|
||||
dashboard_widgets: Vec<String>,
|
||||
#[serde(default = "default_hidden_dashboard_widgets")]
|
||||
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")]
|
||||
file_log_columns: Vec<String>,
|
||||
#[serde(default = "default_file_log_sort")]
|
||||
@@ -200,6 +188,7 @@ impl Default for AdminPreferences {
|
||||
dashboard_density: default_admin_dashboard_density(),
|
||||
dashboard_widgets: default_admin_dashboard_widgets(),
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
file_log_columns: default_file_log_columns(),
|
||||
file_log_sort: default_file_log_sort(),
|
||||
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()
|
||||
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
|
||||
{
|
||||
|
||||
@@ -81,6 +81,7 @@ const STREAM_LIST_SORT_COLUMNS: &[&str] = &[
|
||||
struct DashboardWidgetDefinition {
|
||||
id: String,
|
||||
default_hidden: bool,
|
||||
display_type: String,
|
||||
}
|
||||
static DASHBOARD_WIDGETS: std::sync::LazyLock<Vec<DashboardWidgetDefinition>> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
@@ -145,3 +146,49 @@ static DASHBOARD_DEFAULT_PRESET: std::sync::LazyLock<DashboardDefaultPreset> =
|
||||
);
|
||||
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
|
||||
}
|
||||
@@ -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!(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());
|
||||
}
|
||||
@@ -15,14 +15,20 @@ window.rustProxyInlineLayout = (() => {
|
||||
let opening = false;
|
||||
let saving = false;
|
||||
|
||||
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
|
||||
const visibleIds = () => draft.order.filter(id => !draft.hidden.has(id))
|
||||
.sort((a, b) => groupFor(a) - groupFor(b));
|
||||
const visibleIds = () => dashboardVisualOrder(draft.order, draft.sizes)
|
||||
.filter(id => !draft.hidden.has(id));
|
||||
function enterFreeLayout() {
|
||||
if (draft.sizes === null) {
|
||||
draft.order = dashboardVisualOrder(draft.order, null);
|
||||
draft.sizes = {};
|
||||
}
|
||||
}
|
||||
const nodes = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
|
||||
const nodeFor = id => nodes().find(node => node.dataset.dashboardWidget === id);
|
||||
const label = id => dashboardWidgetLabel(id);
|
||||
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() {
|
||||
if (!hiddenList || !draft) return;
|
||||
@@ -46,16 +52,24 @@ window.rustProxyInlineLayout = (() => {
|
||||
if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); }
|
||||
}
|
||||
function paint() {
|
||||
applyDashboardLayout(draft.order, [...draft.hidden]);
|
||||
applyDashboardLayout(draft.order, [...draft.hidden], draft.sizes);
|
||||
renderHidden();
|
||||
const visible = visibleIds();
|
||||
for (const node of nodes()) {
|
||||
const i = visible.indexOf(node.dataset.dashboardWidget);
|
||||
const up = node.querySelector(".overview-inline-up");
|
||||
const down = node.querySelector(".overview-inline-down");
|
||||
if (up) up.disabled = i <= 0 || groupFor(visible[i - 1]) !== groupFor(node.dataset.dashboardWidget);
|
||||
if (down) down.disabled = i < 0 || i === visible.length - 1 ||
|
||||
groupFor(visible[i + 1]) !== groupFor(node.dataset.dashboardWidget);
|
||||
if (up) up.disabled = i <= 0;
|
||||
if (down) down.disabled = i < 0 || i === visible.length - 1;
|
||||
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() {
|
||||
@@ -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");
|
||||
}
|
||||
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();
|
||||
if (!visible.includes(id) || !visible.includes(target)) return;
|
||||
visible.splice(visible.indexOf(id), 1);
|
||||
visible.splice(visible.indexOf(target) + (after ? 1 : 0), 0, id);
|
||||
const iter = visible.values();
|
||||
draft.order = draft.order.map(item => draft.hidden.has(item) ? item : iter.next().value);
|
||||
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) {
|
||||
const visible = visibleIds();
|
||||
const index = visible.indexOf(id), target = visible[index + delta];
|
||||
if (!target || groupFor(id) !== groupFor(target)) return;
|
||||
if (!target) return;
|
||||
move(id, target, delta > 0);
|
||||
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", () => { });
|
||||
grip.draggable = true;
|
||||
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("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)),
|
||||
control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id)));
|
||||
@@ -125,7 +158,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
|
||||
if (hiddenList) hiddenList.replaceChildren();
|
||||
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;
|
||||
}
|
||||
async function begin() {
|
||||
@@ -137,8 +170,9 @@ window.rustProxyInlineLayout = (() => {
|
||||
const saved = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
const order = normalizeDashboardWidgets(saved.dashboard_widgets);
|
||||
const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets));
|
||||
original = { order: [...order], hidden: new Set(hidden) };
|
||||
draft = { order: [...order], hidden: new Set(hidden) };
|
||||
const sizes = normalizeDashboardWidgetSizes(saved.dashboard_widget_sizes);
|
||||
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();
|
||||
} catch (error) { if (error?.status !== 401) toast(error.message, true); }
|
||||
finally { opening = false; unlock.disabled = false; }
|
||||
@@ -151,6 +185,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
|
||||
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
|
||||
});
|
||||
finish(true);
|
||||
toast(t("overviewLayoutEditorSaved"));
|
||||
@@ -177,8 +212,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
grid?.addEventListener("dragover", event => {
|
||||
if (!draft || !dragging) return;
|
||||
const target = dropTarget(event);
|
||||
if (!target || target.dataset.dashboardWidget === dragging ||
|
||||
groupFor(target.dataset.dashboardWidget) !== groupFor(dragging)) return;
|
||||
if (!target || target.dataset.dashboardWidget === dragging) return;
|
||||
event.preventDefault();
|
||||
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");
|
||||
@@ -198,7 +232,10 @@ window.rustProxyInlineLayout = (() => {
|
||||
cancelButton?.addEventListener("click", cancel);
|
||||
return {
|
||||
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,
|
||||
};
|
||||
})();
|
||||
@@ -20,43 +20,60 @@ window.rustProxyDashboardLayout = (() => {
|
||||
function initialDraft() {
|
||||
const prefs = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
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() {
|
||||
return {
|
||||
order: [...ADMIN_DASHBOARD_WIDGETS],
|
||||
hidden: new Set(ADMIN_DEFAULT_HIDDEN_WIDGETS),
|
||||
sizes: null,
|
||||
};
|
||||
}
|
||||
function label(id) {
|
||||
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
|
||||
return widget ? t(widget.label_key) : id;
|
||||
}
|
||||
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
|
||||
function visibleIds() {
|
||||
return draft.order.filter(id => !draft.hidden.has(id))
|
||||
.sort((a, b) => groupFor(a) - groupFor(b));
|
||||
return dashboardVisualOrder(draft.order, draft.sizes)
|
||||
.filter(id => !draft.hidden.has(id));
|
||||
}
|
||||
function enterFreeLayout() {
|
||||
if (draft.sizes === null) {
|
||||
draft.order = dashboardVisualOrder(draft.order, null);
|
||||
draft.sizes = {};
|
||||
}
|
||||
}
|
||||
function reorderVisible(nextVisible) {
|
||||
enterFreeLayout();
|
||||
const iter = nextVisible.values();
|
||||
draft.order = draft.order.map(id => draft.hidden.has(id) ? id : iter.next().value);
|
||||
render();
|
||||
}
|
||||
function move(id, target, after = false) {
|
||||
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(target) + (after ? 1 : 0), 0, id);
|
||||
reorderVisible(next);
|
||||
}
|
||||
function nudge(id, 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]];
|
||||
reorderVisible(next);
|
||||
visibleRoot.querySelectorAll(".overview-layout-tile").forEach(node => {
|
||||
if (node.dataset.previewWidget === id) node.querySelector(".overview-layout-up")?.focus();
|
||||
});
|
||||
visibleRoot.querySelector(`[data-preview-widget="${id}"] .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) {
|
||||
const original = document.querySelector(`#panel-overview [data-dashboard-widget="${id}"]`);
|
||||
@@ -94,6 +111,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
const tiles = visible.map((id, index) => {
|
||||
const tile = el("div", undefined, "overview-layout-tile");
|
||||
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");
|
||||
tile.draggable = true;
|
||||
tile.setAttribute("role", "listitem");
|
||||
@@ -102,19 +120,39 @@ window.rustProxyDashboardLayout = (() => {
|
||||
const grip = el("span", "⠿", "overview-layout-grip");
|
||||
grip.setAttribute("aria-hidden", "true");
|
||||
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.addEventListener("click", () => nudge(id, -1));
|
||||
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.addEventListener("click", () => nudge(id, 1));
|
||||
const hide = el("button", t("overviewLayoutEditorHide"), "quiet overview-layout-hide");
|
||||
hide.type = "button";
|
||||
hide.setAttribute("aria-label", `${t("overviewLayoutEditorHide")}: ${label(id)}`);
|
||||
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");
|
||||
controls.append(up, down, hide);
|
||||
controls.append(sizeActions, orderActions);
|
||||
toolbar.append(grip, dashboardWidgetTypeBadge(id), controls);
|
||||
tile.append(toolbar, preview(id));
|
||||
tile.addEventListener("dragstart", event => {
|
||||
@@ -125,7 +163,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
tile.classList.add("is-dragging");
|
||||
});
|
||||
tile.addEventListener("dragover", event => {
|
||||
if (!dragging || dragging === id || groupFor(dragging) !== groupFor(id)) return;
|
||||
if (!dragging || dragging === id) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
tile.classList.add("is-drop-target");
|
||||
@@ -201,6 +239,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...defaults.order], dashboard_hidden_widgets: [...defaults.hidden],
|
||||
dashboard_widget_sizes: null,
|
||||
});
|
||||
resetConfirm.close();
|
||||
dialog.close();
|
||||
@@ -215,6 +254,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
|
||||
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
|
||||
});
|
||||
dialog.close();
|
||||
toast(t("overviewLayoutEditorSaved"));
|
||||
|
||||
@@ -87,17 +87,13 @@ window.rustProxyExtraOverview = (() => {
|
||||
}
|
||||
if (id === "extra_backend_active") {
|
||||
const members = Array.isArray(status.backend_pools) ? status.backend_pools.flatMap(pool => pool.members || []) : null;
|
||||
return {
|
||||
value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
|
||||
unit: "count", detail: ""
|
||||
};
|
||||
return { value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
|
||||
unit: "count", detail: "" };
|
||||
}
|
||||
if (id === "extra_backend_unhealthy") {
|
||||
return {
|
||||
value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
|
||||
return { value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
|
||||
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_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes);
|
||||
@@ -127,10 +123,8 @@ window.rustProxyExtraOverview = (() => {
|
||||
const { deltas, seconds } = result;
|
||||
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
|
||||
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
|
||||
return {
|
||||
value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
|
||||
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null
|
||||
};
|
||||
return { value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
|
||||
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null };
|
||||
}
|
||||
function format(value, unit) {
|
||||
if (value == null) return "-";
|
||||
|
||||
@@ -9,16 +9,19 @@ window.rustProxyMasonry = (() => {
|
||||
|
||||
function refresh() {
|
||||
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)
|
||||
.map(item => item.node);
|
||||
.map(item => item.node) : previous;
|
||||
if (ordered.some((node, i) => node !== previous[i])) grid.append(...ordered);
|
||||
let lastGroup = -1;
|
||||
for (const node of ordered) {
|
||||
for (const key of ["position", "left", "top", "width", "minHeight", "gridColumnStart",
|
||||
"gridColumnEnd", "gridRowStart", "gridRowEnd", "alignSelf"]) node.style[key] = "";
|
||||
node.classList.remove("overview-row-start");
|
||||
if (node.hidden) continue;
|
||||
if (node.hidden || !legacy) continue;
|
||||
const current = group(node);
|
||||
if (current !== lastGroup) node.classList.add("overview-row-start");
|
||||
lastGroup = current;
|
||||
|
||||
@@ -13,7 +13,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
||||
])]);
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
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" });
|
||||
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
|
||||
@@ -41,6 +41,47 @@ function normalizeHiddenDashboardWidgets(value, savedOrder = ADMIN_DASHBOARD_WID
|
||||
}
|
||||
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() {
|
||||
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;
|
||||
@@ -96,19 +137,25 @@ function applyAdminDashboardDensity(value) {
|
||||
const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = 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 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 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]")];
|
||||
if (orderedNodes.some((node, index) => node !== currentNodes[index])) grid.append(...orderedNodes);
|
||||
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
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); }
|
||||
return { order: normalizedOrder, hidden: [...hiddenSet] };
|
||||
return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
|
||||
}
|
||||
function dashboardWidgetLabel(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 focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null;
|
||||
const focusedAction = focusedId ? document.activeElement?.dataset.dashboardWidgetAction : null;
|
||||
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
|
||||
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = dashboardVisualOrder(current.dashboard_widgets, current.dashboard_widget_sizes), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
|
||||
const rows = order.map((id, index) => {
|
||||
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
|
||||
const visible = !hidden.has(id);
|
||||
@@ -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"));
|
||||
toggle.addEventListener("click", () => {
|
||||
const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id);
|
||||
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings();
|
||||
void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, toggle, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous); 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, current.dashboard_widget_sizes); renderDashboardWidgetSettings(); });
|
||||
});
|
||||
const move = delta => {
|
||||
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;
|
||||
applyDashboardLayout(next, [...hidden]); renderDashboardWidgetSettings();
|
||||
void saveAppearancePreference({ dashboard_widgets: next }, null, () => { if (adminPreferences) adminPreferences.dashboard_widgets = previous; applyDashboardLayout(previous, [...hidden]); renderDashboardWidgetSettings(); });
|
||||
[next[at], next[target]] = [next[target], next[at]];
|
||||
const previousSizes = current.dashboard_widget_sizes;
|
||||
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;
|
||||
});
|
||||
@@ -224,14 +276,15 @@ function applyAdminPreferences(value, cache = true) {
|
||||
const editing = window.rustProxyInlineLayout?.currentDraft?.();
|
||||
const savedOrder = normalizeDashboardWidgets(value.dashboard_widgets);
|
||||
const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets);
|
||||
if (editing) applyDashboardLayout(editing.order, editing.hidden);
|
||||
else applyDashboardLayout(savedOrder, savedHidden);
|
||||
const savedSizes = normalizeDashboardWidgetSizes(value.dashboard_widget_sizes);
|
||||
if (editing) applyDashboardLayout(editing.order, editing.hidden, editing.sizes);
|
||||
else applyDashboardLayout(savedOrder, savedHidden, savedSizes);
|
||||
const normalized = {
|
||||
...value,
|
||||
refresh: normalizeRefreshPreferences(value.refresh),
|
||||
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),
|
||||
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;
|
||||
window.rustProxyRefresh?.update(normalized.refresh);
|
||||
@@ -279,7 +332,7 @@ function updateAdminPreferences(patch) {
|
||||
next.refresh = normalizeRefreshPreferences(next.refresh);
|
||||
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.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) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
return applyAdminPreferences(saved);
|
||||
@@ -378,7 +431,7 @@ $("ui-dashboard-density")?.addEventListener("change", () => {
|
||||
async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") {
|
||||
if (control) control.disabled = true; let previous = null;
|
||||
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));
|
||||
} catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); }
|
||||
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", () => {
|
||||
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" },
|
||||
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);
|
||||
}));
|
||||
$("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();
|
||||
@@ -256,7 +256,7 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
const actions = el("span", undefined, "network-interface-head-actions");
|
||||
actions.append(rate, hide);
|
||||
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("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" }));
|
||||
|
||||
+88
-63
@@ -154,7 +154,8 @@
|
||||
</section>
|
||||
<section class="panel" id="panel-overview">
|
||||
<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>
|
||||
<details class="overview-inline-hidden" id="overview-inline-hidden" hidden>
|
||||
<summary><span data-i18n="overviewLayoutInlineHidden">Hidden blocks</span> <span
|
||||
@@ -226,8 +227,9 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="http_protocol"><span
|
||||
data-i18n="httpProtocolMix">HTTP protocol
|
||||
mix</span><strong id="metric-http-proto-total">0.0/s</strong><svg
|
||||
aria-label="HTTP protocol activity" data-i18n-aria="httpProtocolMix"
|
||||
id="http-protocol-chart" role="img" viewBox="0 0 300 60">
|
||||
preserveAspectRatio="none" aria-label="HTTP protocol activity"
|
||||
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>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="http-protocol-h1"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -241,7 +243,7 @@
|
||||
id="metric-http-proto-h3">0.0/s</b></span></small></article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="websocket_protocol"><span
|
||||
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"
|
||||
id="ws-protocol-chart" role="img" viewBox="0 0 300 60">
|
||||
<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>
|
||||
<span data-i18n="streamWidgetTraffic">Stream traffic</span><strong
|
||||
id="stream-dashboard-traffic">-</strong>
|
||||
<svg aria-label="Stream traffic RX/TX" data-i18n-aria="streamWidgetTraffic" role="img"
|
||||
viewBox="0 0 300 60">
|
||||
<svg preserveAspectRatio="none" aria-label="Stream traffic RX/TX"
|
||||
data-i18n-aria="streamWidgetTraffic" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line stream-chart-rx" id="stream-dashboard-rx"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -297,7 +299,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_activity" hidden>
|
||||
<span data-i18n="backendWidgetActivity">Active backend requests</span><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>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-active-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -306,7 +309,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_errors" hidden>
|
||||
<span data-i18n="backendWidgetErrors">Backend errors</span><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>
|
||||
<polyline class="diagnostic-line proto-h3-line" id="backend-dashboard-error-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -315,7 +319,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_latency" hidden>
|
||||
<span data-i18n="backendWidgetLatency">Health probe RTT</span><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>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-latency-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -324,7 +329,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_traffic" hidden>
|
||||
<span data-i18n="backendWidgetTraffic">Backend traffic</span><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>
|
||||
<polyline class="diagnostic-line stream-chart-rx" id="backend-dashboard-in-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -348,7 +354,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_requests" hidden>
|
||||
<span data-i18n="backendWidgetRequests">Backend request rate</span><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>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-request-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -366,7 +373,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden>
|
||||
<span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><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>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-5xx-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -375,7 +383,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_timeouts" hidden>
|
||||
<span data-i18n="backendWidgetTimeouts">backendWidgetTimeouts</span><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>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-timeout-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -384,7 +393,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_network" hidden>
|
||||
<span data-i18n="backendWidgetNetwork">backendWidgetNetwork</span><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>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-network-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -393,7 +403,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_retries" hidden>
|
||||
<span data-i18n="backendWidgetRetries">backendWidgetRetries</span><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>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-retry-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -402,7 +413,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_queue" hidden>
|
||||
<span data-i18n="backendWidgetQueue">backendWidgetQueue</span><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>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-queue-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -416,7 +428,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_cpu" hidden>
|
||||
<span data-i18n="overviewExtraProcessCpu">overviewExtraProcessCpu</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_process_cpu-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -425,7 +438,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_rss" hidden>
|
||||
<span data-i18n="overviewExtraProcessRss">overviewExtraProcessRss</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_process_rss-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -444,7 +458,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_open_connections" hidden>
|
||||
<span data-i18n="overviewExtraOpenConnections">overviewExtraOpenConnections</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_open_connections-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -453,7 +468,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_websocket_active" hidden>
|
||||
<span data-i18n="overviewExtraWebsocketActive">overviewExtraWebsocketActive</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_websocket_active-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -462,7 +478,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_tls_error_rate" hidden>
|
||||
<span data-i18n="overviewExtraTlsErrorRate">overviewExtraTlsErrorRate</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_tls_error_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -471,7 +488,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_request_error_rate" hidden>
|
||||
<span data-i18n="overviewExtraRequestErrorRate">overviewExtraRequestErrorRate</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_request_error_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -544,7 +562,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_response_latency" hidden>
|
||||
<span data-i18n="overviewExtraResponseLatency">overviewExtraResponseLatency</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_response_latency-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -553,7 +572,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_backend_unhealthy" hidden>
|
||||
<span data-i18n="overviewExtraBackendUnhealthy">overviewExtraBackendUnhealthy</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_backend_unhealthy-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -562,7 +582,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_total_rps" hidden>
|
||||
<span data-i18n="overviewExtraTotalRps">HTTP requests / s</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_total_rps-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -572,7 +593,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_active_exchanges" hidden>
|
||||
<span data-i18n="overviewExtraActiveExchanges">Active HTTP exchanges</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_active_exchanges-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -602,7 +624,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheMissRate">Cache misses / s</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_cache_miss_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -612,7 +635,8 @@
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_backend_active" hidden>
|
||||
<span data-i18n="overviewExtraBackendActive">In-flight backend requests</span><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>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_backend_active-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -625,7 +649,7 @@
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_protocol_rates-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -649,7 +673,7 @@
|
||||
<h2 data-i18n="overviewWideCpu">System and process CPU</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_cpu-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -671,7 +695,7 @@
|
||||
<h2 data-i18n="overviewWideMemory">Process RAM and cache memory</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_memory-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -694,7 +718,7 @@
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_cache_activity-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -715,7 +739,7 @@
|
||||
<h2 data-i18n="overviewWideConnections">Connections and WebSocket tunnels</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_connections-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -738,7 +762,7 @@
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_backend_health-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -760,7 +784,7 @@
|
||||
<h2 data-i18n="overviewWideLatency">Response and backend probe latency</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_latency-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -783,7 +807,7 @@
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_transform_activity-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
@@ -816,7 +840,7 @@
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_compression_activity-summary">-</span>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<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>
|
||||
<span data-i18n="webMetricPhpRequests">PHP requests/s</span><strong
|
||||
id="www_php_requests-value">-</strong>
|
||||
<svg role="img" viewBox="0 0 300 60" aria-label="PHP requests per second"
|
||||
data-i18n-aria="webMetricPhpRequests">
|
||||
<svg preserveAspectRatio="none" role="img" viewBox="0 0 300 60"
|
||||
aria-label="PHP requests per second" data-i18n-aria="webMetricPhpRequests">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="www_php_requests-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -928,7 +952,7 @@
|
||||
<h2 data-i18n="webMetricRequests">webMetricRequests</h2><span
|
||||
class="overview-wide-summary" id="www_requests-value">-</span>
|
||||
</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">
|
||||
<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"
|
||||
@@ -949,7 +973,7 @@
|
||||
<h2 data-i18n="webMetricLatency">webMetricLatency</h2><span
|
||||
class="overview-wide-summary" id="www_latency-value">-</span>
|
||||
</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">
|
||||
<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"
|
||||
@@ -970,7 +994,7 @@
|
||||
<h2 data-i18n="webMetricFailures">webMetricFailures</h2><span
|
||||
class="overview-wide-summary" id="www_failures-value">-</span>
|
||||
</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">
|
||||
<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"
|
||||
@@ -1001,7 +1025,7 @@
|
||||
<h2 data-i18n="webMetricTraffic">webMetricTraffic</h2><span
|
||||
class="overview-wide-summary" id="www_traffic-value">-</span>
|
||||
</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">
|
||||
<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"
|
||||
@@ -1022,7 +1046,7 @@
|
||||
<h2 data-i18n="webMetricStatusTrend">webMetricStatusTrend</h2><span
|
||||
class="overview-wide-summary" id="www_status_trend-value">-</span>
|
||||
</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">
|
||||
<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"
|
||||
@@ -1062,8 +1086,8 @@
|
||||
<div><span class="small-label" data-i18n="liveTraffic">LIVE TRAFFIC</span>
|
||||
<h2 data-i18n="requestsPerSecond">Requests per second</h2>
|
||||
</div><span class="pill live" data-i18n="live">LIVE</span>
|
||||
</div><svg aria-label="Recent request rate" id="traffic-chart" role="img"
|
||||
viewbox="0 0 600 130">
|
||||
</div><svg preserveAspectRatio="none" aria-label="Recent request rate" id="traffic-chart"
|
||||
role="img" viewbox="0 0 600 130">
|
||||
<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>
|
||||
@@ -1095,8 +1119,8 @@
|
||||
<div><span class="small-label" data-i18n="systemLoad">SYSTEM LOAD</span>
|
||||
<h2><span id="system-cpu">-</span> <small>CPU</small></h2>
|
||||
</div><span class="meta" id="system-source">-</span>
|
||||
</div><svg aria-label="System CPU history" data-i18n-aria="systemChart" id="system-chart"
|
||||
role="img" viewbox="0 0 300 100">
|
||||
</div><svg preserveAspectRatio="none" aria-label="System CPU history"
|
||||
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>
|
||||
<polyline id="system-sparkline" points="0,95 300,95"></polyline>
|
||||
</svg>
|
||||
@@ -1121,8 +1145,9 @@
|
||||
connections</small></h2>
|
||||
</div><span class="pill live" data-i18n="live">LIVE</span>
|
||||
</div>
|
||||
<svg aria-label="Connection activity history" data-i18n-aria="connectionActivity"
|
||||
id="connection-chart" role="img" viewbox="0 0 300 100">
|
||||
<svg preserveAspectRatio="none" aria-label="Connection activity history"
|
||||
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>
|
||||
<polyline class="connection-open-line" id="connection-open-sparkline"
|
||||
points="0,95 300,95"></polyline>
|
||||
@@ -1171,8 +1196,8 @@
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="cacheOutcomeHistory">HIT / MISS /
|
||||
BYPASS activity</span><b id="cache-outcome-chart-value">-</b></div>
|
||||
<svg aria-label="HIT MISS BYPASS activity" data-i18n-aria="cacheOutcomeHistory"
|
||||
role="img" viewbox="0 0 300 60">
|
||||
<svg preserveAspectRatio="none" aria-label="HIT MISS BYPASS activity"
|
||||
data-i18n-aria="cacheOutcomeHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line cache-hit-line" id="cache-hit-sparkline"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -1189,8 +1214,8 @@
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="cacheHotMemoryHistory">HOT RAM
|
||||
usage / limit</span><b id="cache-chart-value">-</b></div>
|
||||
<svg aria-label="HOT cache RAM usage history" data-i18n-aria="cacheHotMemoryHistory"
|
||||
role="img" viewbox="0 0 300 60">
|
||||
<svg preserveAspectRatio="none" aria-label="HOT cache RAM usage history"
|
||||
data-i18n-aria="cacheHotMemoryHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line cache-hot-line" id="cache-memory-sparkline"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -1200,7 +1225,7 @@
|
||||
<div class="mini-live-chart-head"><span
|
||||
data-i18n="cacheTransientMemoryHistory">Transient
|
||||
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">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line cache-transient-total-line"
|
||||
@@ -1223,7 +1248,7 @@
|
||||
<div class="mini-live-chart-head"><span data-i18n="cacheColdStorageHistory">COLD
|
||||
storage
|
||||
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">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<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-head"><span data-i18n="processCpuHistory">Process CPU
|
||||
history</span><b id="process-cpu-chart-value">-</b></div>
|
||||
<svg aria-label="Process CPU history" data-i18n-aria="processCpuHistory" role="img"
|
||||
viewbox="0 0 300 60">
|
||||
<svg preserveAspectRatio="none" aria-label="Process CPU history"
|
||||
data-i18n-aria="processCpuHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="process-cpu-sparkline"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -1335,8 +1360,8 @@
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="processRssHistory">Process RSS
|
||||
history</span><b id="process-rss-chart-value">-</b></div>
|
||||
<svg aria-label="Process RSS history" data-i18n-aria="processRssHistory" role="img"
|
||||
viewbox="0 0 300 60">
|
||||
<svg preserveAspectRatio="none" aria-label="Process RSS history"
|
||||
data-i18n-aria="processRssHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line process-rss-line" id="process-rss-sparkline"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -1367,8 +1392,8 @@
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="networkHistory">RX / TX
|
||||
history</span><b id="network-chart-value">-</b></div>
|
||||
<svg aria-label="Network throughput history" data-i18n-aria="networkHistory"
|
||||
role="img" viewbox="0 0 300 60">
|
||||
<svg preserveAspectRatio="none" aria-label="Network throughput history"
|
||||
data-i18n-aria="networkHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line network-rx-line" id="network-rx-sparkline"
|
||||
points="0,55 300,55"></polyline>
|
||||
@@ -1395,7 +1420,7 @@
|
||||
</div>
|
||||
</details><b id="latency-chart-value">-</b>
|
||||
</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">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="latency-sparkline" points="0,55 300,55">
|
||||
|
||||
+246
-44
@@ -72,6 +72,9 @@
|
||||
--rp-progress-fill: var(--chart-accent);
|
||||
--rp-progress-track: var(--line);
|
||||
--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-h2: var(--chart-secondary);
|
||||
--traffic-h3: var(--chart-tertiary);
|
||||
@@ -135,6 +138,10 @@ html[data-theme=light] {
|
||||
--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] {
|
||||
color-scheme: dark;
|
||||
--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 {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
@@ -533,7 +530,8 @@ nav {
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border-left: 2px solid transparent
|
||||
border-left: 2px solid transparent;
|
||||
border-radius: var(--rp-radius-control, 0)
|
||||
}
|
||||
|
||||
.nav span {
|
||||
@@ -546,13 +544,18 @@ nav {
|
||||
}
|
||||
|
||||
.nav.active {
|
||||
background: var(--nav-active-bg);
|
||||
color: var(--green);
|
||||
border-left-color: var(--green)
|
||||
background: var(--rp-nav-active-bg);
|
||||
color: var(--rp-nav-active-color);
|
||||
border-left-color: var(--rp-nav-active-color)
|
||||
}
|
||||
|
||||
.nav:hover {
|
||||
background: var(--nav-hover-bg)
|
||||
.nav:not(.active):hover {
|
||||
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 {
|
||||
@@ -1213,7 +1216,7 @@ button.is-saving {
|
||||
}
|
||||
|
||||
.nav.active {
|
||||
border-bottom-color: var(--green)
|
||||
border-bottom-color: var(--rp-nav-active-color)
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -1270,7 +1273,8 @@ button.is-saving {
|
||||
letter-spacing: .4px
|
||||
}
|
||||
|
||||
.breadcrumb span {
|
||||
.breadcrumb span,
|
||||
.login-footer span {
|
||||
margin: 0 5px
|
||||
}
|
||||
|
||||
@@ -1360,10 +1364,6 @@ button.is-saving {
|
||||
padding: 20px
|
||||
}
|
||||
|
||||
.login-footer span {
|
||||
margin: 0 5px
|
||||
}
|
||||
|
||||
.toast {
|
||||
right: 17px;
|
||||
bottom: 17px
|
||||
@@ -2077,7 +2077,7 @@ td {
|
||||
}
|
||||
|
||||
[dir=rtl] .nav.active {
|
||||
border-right-color: var(--green)
|
||||
border-right-color: var(--rp-nav-active-color)
|
||||
}
|
||||
|
||||
[dir=rtl] table {
|
||||
@@ -7879,9 +7879,9 @@ button.config-history-day.is-selected {
|
||||
|
||||
.mobile-menu-toggle:hover,
|
||||
.mobile-menu-toggle[aria-expanded=true] {
|
||||
color: var(--text);
|
||||
border-color: var(--border-hover);
|
||||
background: var(--nav-active-bg);
|
||||
color: var(--rp-nav-active-color);
|
||||
border-color: var(--rp-nav-active-color);
|
||||
background: var(--rp-nav-active-bg);
|
||||
}
|
||||
|
||||
.mobile-menu-icon {
|
||||
@@ -7921,8 +7921,13 @@ button.config-history-day.is-selected {
|
||||
|
||||
.sidebar .main-navigation .nav.active {
|
||||
border-color: var(--line);
|
||||
border-left-color: var(--green);
|
||||
background: var(--nav-active-bg);
|
||||
border-left-color: var(--rp-nav-active-color);
|
||||
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 {
|
||||
padding: 14px 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
height: 34px;
|
||||
}
|
||||
@@ -10220,12 +10223,10 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
|
||||
.overview-layout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--ui-gap, 10px);
|
||||
}
|
||||
|
||||
.overview-layout-tile {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
cursor: grab;
|
||||
@@ -10246,7 +10247,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
|
||||
.overview-layout-tile-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 4px;
|
||||
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 {
|
||||
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 {
|
||||
font-size: clamp(19px, 5vw, 22px);
|
||||
font-size: calc(clamp(19px, 5vw, 22px));
|
||||
}
|
||||
|
||||
#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 {
|
||||
vector-effect: non-scaling-stroke;
|
||||
stroke: var(--line);
|
||||
fill: none;
|
||||
stroke-width: 1;
|
||||
@@ -10891,6 +10892,7 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
|
||||
|
||||
#panel-overview .overview-wide-series {
|
||||
fill: none;
|
||||
vector-effect: non-scaling-stroke;
|
||||
stroke-width: 2;
|
||||
stroke-linejoin: 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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -11260,11 +11261,9 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
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 {
|
||||
display: block;
|
||||
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 {
|
||||
transition: none;
|
||||
}
|
||||
@@ -11423,7 +11430,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
@media(max-width:700px) {
|
||||
.page-intro .overview-samples-toolbar {
|
||||
margin-left: 0;
|
||||
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 {
|
||||
display: block;
|
||||
height: 100%;
|
||||
transition: width 180ms ease;
|
||||
transition: width .18s;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@media(prefers-reduced-motion:reduce) {
|
||||
#panel-overview .www-segmented span {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user