v0.33.1
This commit is contained in:
1 parent
2ccc598b12
commit
b757b34a31
26 files changed
+671
-116
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.0.
|
||||
compatibility. Current source version: 0.33.1.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
> Operator-facing release history. Historical engineering notes are kept in
|
||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
## 0.33.1
|
||||
|
||||
- Fix intermittent inline Overview hide/show reversions: backend and Stream
|
||||
telemetry renders now honor the unlocked layout draft until Save/Cancel.
|
||||
Unrelated preference refreshes no longer overwrite the draft.
|
||||
- Add a single bundled `dashboard-defaults.json` defining the factory ordering,
|
||||
visibility and compact density provided by the operator, with 600 initial
|
||||
Overview history samples. Apply only to new accounts and explicit resets;
|
||||
preserve saved user layouts and browser-stored sample preferences.
|
||||
- Add live UI regressions for draft add/hide, backend/Stream status updates,
|
||||
cancel/save and default preservation.
|
||||
- Preserve counter baselines and chart history while Overview widgets are hidden, avoiding discontinuities when shown again.
|
||||
- Clarify the distinction between lifetime Cache HIT ratio and recent interval HIT ratio.
|
||||
- Document safe dashboard preference export in the browser Console without `copy()`.
|
||||
|
||||
## 0.33.0
|
||||
|
||||
- Move the Overview history sample selector to the connection-status header row
|
||||
|
||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.33.0"
|
||||
version = "0.33.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.33.0"
|
||||
version = "0.33.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.99"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
# RustProxy 0.33.0
|
||||
# RustProxy 0.33.1
|
||||
|
||||
## 0.33.1: factory Overview preset and live layout preview
|
||||
|
||||
- `src/admin/web/dashboard-defaults.json` defines the factory Overview card order,
|
||||
visibility, density and initial 600-sample history. Both the Rust preference
|
||||
defaults and authenticated JavaScript use this same file. New accounts without
|
||||
stored preferences use the full preset; **Reset to default** restores card
|
||||
order/visibility, not density or browser-local sample history. Existing saved
|
||||
layouts and browser-persisted sample choices are preserved. Future registry additions remain opt-in.
|
||||
- An unlocked inline layout is now authoritative during live backend/Stream
|
||||
updates and unrelated preference refreshes. Showing/hiding those cards takes
|
||||
effect immediately and stays stable until **Save and lock** or **Cancel**.
|
||||
Cancel restores the saved layout. No extra polling, storage migrations or
|
||||
production request-path changes are introduced.
|
||||
|
||||
**0.31.0 panel/cache additions:** browser-negotiated Zstd/Brotli/gzip for HTML, JS and CSS (Settings → Administration); optional HTTP BAN/PURGE restricted to explicit IP/CIDR allowlist (Settings → Cache, disabled by default); System information Refresh button beside the API badge. See [Settings](docs/SETTINGS.md) and [Cache](docs/CACHE.md).
|
||||
|
||||
@@ -832,3 +846,33 @@ it is a loopback-only development peer, never an authentication product.
|
||||
### Docker vhosts operational controls
|
||||
|
||||
Docker vhosts now publish/unpublish immediately per service. Each Docker agent has a configurable inventory polling interval, while the optional Live view refreshes cached runtime state only. Docker-backed applications can use ACL profiles/inline ACLs and per-vhost proxy connect/header/read/send timeouts, matching the ordinary vhost proxy controls.
|
||||
|
||||
|
||||
## Dashboard layout export (0.33.1)
|
||||
|
||||
To share a preferred Overview arrangement for a future factory preset, open the authenticated
|
||||
Overview and run this in the browser developer console (no privileged database access):
|
||||
|
||||
```javascript
|
||||
(async () => {
|
||||
const response = await fetch('/api/preferences', { credentials: 'same-origin' });
|
||||
if (!response.ok) throw new Error(`Preferences HTTP ${response.status}`);
|
||||
const prefs = await response.json();
|
||||
const exportData = {
|
||||
dashboard_widgets: prefs.dashboard_widgets,
|
||||
dashboard_hidden_widgets: prefs.dashboard_hidden_widgets,
|
||||
dashboard_density: prefs.dashboard_density,
|
||||
overview_samples: localStorage.getItem('rustproxy.overview.liveChartSamples')
|
||||
};
|
||||
console.log(JSON.stringify(exportData, null, 2));
|
||||
})();
|
||||
```
|
||||
|
||||
Copy the JSON shown in Console using the browser's selection or context menu.
|
||||
Do not export the full preferences response or session cookies. This is an export
|
||||
for review, not an automatic factory-default change; existing account layouts
|
||||
remain unchanged.
|
||||
|
||||
The original Cache HIT donut shows accumulated cache counters; the optional recent
|
||||
Cache HIT chart displays interval-delta counters. They intentionally measure
|
||||
different windows and can produce different percentages.
|
||||
@@ -1,5 +1,14 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.33.1 - Factory dashboard preset and inline live preview
|
||||
|
||||
- [x] Ship the operator-supplied default ordering, hidden cards and compact density
|
||||
in one shared Rust/UI JSON preset; use 600 as the first-use history sample default.
|
||||
- [x] Keep all existing explicitly saved preferences and restore the factory preset
|
||||
only for new accounts or an explicit dashboard reset.
|
||||
- [x] Honor unsaved inline hide/show changes while backend and Stream status refresh;
|
||||
preserve the draft across unrelated preference updates.
|
||||
|
||||
## 0.33.0 - Web Server observability (implementation complete; live acceptance pending)
|
||||
|
||||
- [x] Add one Overview 60/120/300/600 history selector on the connection-status header row, with temporary changes and explicit browser-persistent choice. Use the same source stream and leave the Cache and Transform selectors independent.
|
||||
|
||||
+25
-22
@@ -1,13 +1,13 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
12bc9d0f2444dc556ab922b534bd5b7c1602d804325cc0fb3d4d0d28a76cd4ce AGENTS.md
|
||||
09472318ab840fdffc9f1517f4a7a5e4baa46abfba1f96b7d921e69e8a9d25e9 CHANGELOG.md
|
||||
f6df6e080138068fecfe4e6ad7f271c450d158633315da8dd5f2e7377c3674b3 Cargo.lock
|
||||
bcdfe0d135824ad7b570b2070074b29783b449b4b7ff2cc001c29b36ac451168 Cargo.toml
|
||||
72d08ff6d852ab20c16d9a4396053dfb49c4acfd817af683838783ac5285ceea AGENTS.md
|
||||
1769a5bfa31abc9158b9f39e7f86279a54f196d2934f93fdfb4e7d248a86fada CHANGELOG.md
|
||||
8f4e05b7681384329a9a126d48b9eb9a0a8d5bc0597997d67d2f7551a47dac2e Cargo.lock
|
||||
ac9205ad566ed4c2fe8e862b2241eb4d34bcfaaff1242e7a0ec5f49731d7059a Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
acca7f883bb48101b4385625b940e852d8a888ac3955160d978e84e59aa350c1 README.md
|
||||
5104ae23146a728c7561ff5d6aeaa787ee755b379b1ac1d4ed4bbf2e897c46b3 ROADMAP.md
|
||||
13ad656e45773ad5581c1fdb54635dc78a48e5f99c93ec589469882e85d2177e README.md
|
||||
92a11de221459b5678659b1d79118e5318d404f91eda9ac7fadd69764c8ced45 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||
@@ -35,6 +35,7 @@ fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md
|
||||
3822d72e12f82c37a9e70ab40c7e89e89145ad37f29914e4702db4bf024a5501 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
||||
c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md
|
||||
833ddb12f23dcac796223521388b19c6ec3ea04c5550cf1c5f4ea4eb7c58f3a0 docs/CUSTOM_LOCATIONS.md
|
||||
3f2376d8a54229cad88706d4edda5a07db33f5ae81c5aa70d3a9f2b38389906c docs/DASHBOARD_PRESET.md
|
||||
4d70797bda8d227787fcff897951e702e2458fc6c6f5352aa0b5577595bd834b docs/DOCKER_DISCOVERY.md
|
||||
46811a09a03d83f4af686c33e30e3f94d0508afe2c75147a26885c7247c49cbd docs/ERROR_LOGGING.md
|
||||
f749f7bd2eb4ae9c484f77a3cd8cd5ff21445319a3938e0384a3414134fe713a docs/FILE_LOGS.md
|
||||
@@ -82,7 +83,7 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
|
||||
1b9120b1d64eb07060bcb84df404c28ed375da9e0214d39cf01e167dd46e17f9 grafana/rustproxy-overview.json
|
||||
abd03fbc4c24317871978b88930e175cc2ab1c20c69ce754a20ac8f8a6e9088a grafana/rustproxy-overview.json
|
||||
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
|
||||
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
||||
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
||||
@@ -96,7 +97,7 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||
b5922257df0009d3e0778d2a08c926952fed6db6c013049e04ca8b63eaa6df6d langs/en_US.json
|
||||
f195133531f91a3c139979df7f14814e87e008899e5ccd123bfbeadec447cee7 langs/pl_pl.json
|
||||
52a3d1dcc5e2517f4906fb71b1c6d9c915036a0d69e9ceec143bb2b27582611b langs/pl_pl.json
|
||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -129,12 +130,13 @@ e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtim
|
||||
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py
|
||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
|
||||
52eacb467df123dc4a35f14933e006b090c9238df1b14dcff856a16b37c8063f scripts/tests/dashboard_layout_0331_browser_test.py
|
||||
276e732fc2dfb0d39badb159e4fdcba0d7d73509c60ad5c9df49621c03046f1b scripts/tests/dashboard_metric_visuals_ui_test.mjs
|
||||
aa12d00acdd226602b3efc90221fbc936f69f73bcabbaa6c01dfb0e05d08c505 scripts/tests/overview_samples_0330_ui_test.mjs
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py
|
||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py
|
||||
91bf803f05b97b4654a6e607de40fccdd0268882039d6d1c5d677edce9459006 scripts/tests/web_observability_0330_browser_test.py
|
||||
38ac7c7cc8c789f407cb2543a8a7dedcc8cbfce13eac89f5ec6537f8944d83f9 scripts/tests/web_observability_0330_source_test.py
|
||||
fc982558e5c27281d71c2b3cbc926c415ab9134cf595e7ac12b7214f8a180447 scripts/tests/web_observability_0330_source_test.py
|
||||
8b600edbe0aff817f871f2bf3821a35ca8d2d29870c0f6b6bf3c0cef2139e221 scripts/tests/web_server_0330_ui_test.py
|
||||
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
|
||||
55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs
|
||||
@@ -162,17 +164,17 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
|
||||
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
||||
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
|
||||
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
|
||||
5b75274164d97b2c7a3acf5d82f494c6de8a41e357fa9e5b07df60bf0fcebe8f src/admin/openapi.json
|
||||
38351a070d610e96105bc10441e1f50608542a7e51fb18eeecef7819f48f9ed5 src/admin/openapi.json
|
||||
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||
c10f0d277ca5f1fd84eea3705360a81bb0923e10b7e5f5d25378b55ee080de40 src/admin/preferences.rs
|
||||
cd0fda2b5a7b86a6619d6fa7cf5cef5a2598f033a8e9304475c4a05a169c958c src/admin/preferences_columns.rs
|
||||
d94640019bc1679c5ac39110ba90399d8228fe8261bafac456be46ec2c8781ab src/admin/preferences_tests.rs
|
||||
421c8284e0f45e9bb938e41feda4d97b7a17028d51ff3f27ce51ce063058257d src/admin/preferences.rs
|
||||
fd19a594502c678f0f72efa29dc678ee477b956d47b6784c857b95f6265accf1 src/admin/preferences_columns.rs
|
||||
1abda2a65cbee89248c8c252bc75fb6c4ffae1ac753b6b147866a51581f54f9f src/admin/preferences_tests.rs
|
||||
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
|
||||
ef82ece387c6a8987aafbc08ca04ccadf1043b9f9a49c02efb28b9df456da429 src/admin/router.rs
|
||||
6d12bb97819ff05773d017d6b73bd6659dc9f2345f49d449fbaf9e34b936aab8 src/admin/router.rs
|
||||
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
|
||||
77fe317c15bf2e03301b22e0552518fe2d7e1ad8d80afaba4e16cd2bf8ddf10c src/admin/status.rs
|
||||
4b8a0483ede651d08489a4d6e5bb616f06423094bbbefcee70b8755d65c04398 src/admin/status_live.rs
|
||||
@@ -180,25 +182,26 @@ c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/stat
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
18b778c2c4e723e2fe5cbaaef20effa6ea50fcc35d507ebde2361c9b6eda62b2 src/admin/tests.rs
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
4722d47fce7bdb45f0205cc364cb81e94c06abbdfd026d566907eb969ff7e2a2 src/admin/web/app/backend-dashboard.js
|
||||
fc89214fef20a4f5ad7958a7c98df62bd43e8d8e3f895a96ed632c9a3c204d48 src/admin/web/app/backend-dashboard.js
|
||||
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
|
||||
8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js
|
||||
dec0b875086e3567486191231056bfc6c1edef006f7aad7c88f19ef37c034570 src/admin/web/app/dashboard-inline-layout.js
|
||||
e71f77d459d7894f6a0920e9adbe6393c85ed3113e40582fde0c814561da5fd3 src/admin/web/app/dashboard-inline-layout.js
|
||||
dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/app/dashboard-layout-editor.js
|
||||
2f17473efa4147b04bcf8b0ad91e936a35ccf7c44771de95dec115d770f1ee42 src/admin/web/app/overview-extra-metrics.js
|
||||
1a3782c81e1848df738eab0ff0e38f7b35b839fe553612f3387c517ca2461bbb src/admin/web/app/overview-extra-metrics.js
|
||||
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js
|
||||
5cf84000b52815177792fd1e0d64b0cac93ecb1ae72ec8a9fa391fbe11fd9794 src/admin/web/app/overview-samples.js
|
||||
fbac70623c8b16b577bcc79d499c739458d08b0ae7346d39c5bc0c9db38d3d39 src/admin/web/app/overview-wide-metrics.js
|
||||
4ead1e93be94e411aefc4b944355414f034d1b0edfb31b402328fc80cdaba6dd src/admin/web/app/preferences.js
|
||||
b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js
|
||||
646d939eb9275d2aee369053d8da4b2e6502da61d072919e05dc530465b4f330 src/admin/web/app/overview-wide-metrics.js
|
||||
fb6c48b2367f9e1bfeced2cbb4362d1d85fe1ca0c1627c4660b4d4475ea70977 src/admin/web/app/preferences.js
|
||||
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
|
||||
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
|
||||
b97095ae0c94184693bcd3929724c456edf3d66119b32db4fcdad5ae5b7a84bc src/admin/web/app/runtime.js
|
||||
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
||||
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
||||
db25aeb84827b6c921519a1f17be607319c50add4b0f9ea57436a387829cf4bf src/admin/web/app/stream-dashboard.js
|
||||
0029667b44b904bd4f81be9885248ddc717ca7be9a12a0923b019706b571c5ba src/admin/web/app/web-overview.js
|
||||
972a499ef7cd7956308c114a9a37fa567347ac7c554e979b3e648ec59bf34d76 src/admin/web/app/stream-dashboard.js
|
||||
20ff05cfddc7f48931b0e9348b63188f82e8eaec38a7fe3e1829fd2e87301568 src/admin/web/app/web-overview.js
|
||||
0603dc8793a731a0e582cbbec53aa6d1cc6e007faafcec5d8843730b2cdc39e1 src/admin/web/cache-panel.js
|
||||
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
|
||||
62c84ba46b231c9c62d00e910d182ad42ddb832bb04d3cbd158c296fdc78effd src/admin/web/dashboard-defaults.json
|
||||
797e2935a34b974cb0e07a88056c31e6948cc11385bf2a076d902a332882d0a0 src/admin/web/dashboard-widgets.json
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
d01a2e48f344fe074c1e63ee0a9bbf7151dfe26140690418535197e13a2a16d7 src/admin/web/docker.js
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Factory Overview dashboard preset (0.33.1)
|
||||
|
||||
`src/admin/web/dashboard-defaults.json` is the single factory configuration
|
||||
for the Rust admin preference API and the authenticated JavaScript panel.
|
||||
|
||||
The preset contains:
|
||||
|
||||
- `dashboard_widgets`: complete card identifier order, including hidden cards;
|
||||
- `dashboard_hidden_widgets`: identifiers hidden in the factory Overview;
|
||||
- `dashboard_density`: `normal`, `compact` or `ultra`;
|
||||
- `overview_samples`: `60`, `120`, `300` or `600` samples for first use in a browser.
|
||||
|
||||
Card IDs and labels are defined in `src/admin/web/dashboard-widgets.json`.
|
||||
The preset may omit newly registered card IDs: the server/client will append
|
||||
those cards and apply the registry's `default_hidden` flag. The preset must
|
||||
not contain unknown or duplicate IDs, or hidden IDs absent from its order.
|
||||
|
||||
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.
|
||||
|
||||
## Deploying an edited preset
|
||||
|
||||
1. Edit `src/admin/web/dashboard-defaults.json` in the source tree.
|
||||
2. Validate the JSON and identifiers
|
||||
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.
|
||||
|
||||
## Existing installation and reset behavior
|
||||
|
||||
- Admin accounts with stored preferences in SQLite retain their chosen
|
||||
ordering, card visibility and density on upgrade. The preset never rewrites
|
||||
these saved records. Accounts without saved preferences receive the new
|
||||
factory defaults, even on upgraded installations.
|
||||
- In Overview, **Arrange cards visually → Restore defaults** restores the
|
||||
factory card order/visibility after the user confirms; it does not reset
|
||||
unrelated appearance or sample-history settings.
|
||||
- The history selector uses browser-local storage key
|
||||
`rustproxy.overview.liveChartSamples`. An explicitly saved choice (including
|
||||
60) wins over the factory sample count. A browser without a saved choice
|
||||
starts with 600. The sample limit does not change polling frequency.
|
||||
- While layout is unlocked, hide/show/reorder changes are temporary and stay
|
||||
effective during backend/Stream status refresh. **Cancel changes** restores
|
||||
the saved layout; **Save and lock** persists the current draft.
|
||||
|
||||
No SQLite schema migration or additional HTTP endpoint is required.
|
||||
@@ -5640,7 +5640,7 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "RustProxy 0.33.0 - Operations",
|
||||
"title": "RustProxy 0.33.1 - Operations",
|
||||
"uid": "rustproxy-0170",
|
||||
"version": 6,
|
||||
"weekStart": "",
|
||||
|
||||
+1
-1
@@ -2557,7 +2557,7 @@
|
||||
"overviewExtraTlsErrorRateDescription": "Tempo wyliczone z przyrostu licznika błędów handshake TLS.",
|
||||
"overviewExtraRequestErrorRate": "Błędy żądań/s",
|
||||
"overviewExtraRequestErrorRateDescription": "Tempo wyliczone z przyrostu licznika błędów żądań.",
|
||||
"overviewExtraCacheHitRate": "Udział trafień cache",
|
||||
"overviewExtraCacheHitRate": "Udział trafień cache (ostatnie próbki)",
|
||||
"overviewExtraCacheHitRateDescription": "Udział HIT w nowych trafieniach i chybieniach; bez BYPASS.",
|
||||
"overviewExtraCacheMemory": "Wykorzystanie pamięci HOT cache",
|
||||
"overviewExtraCacheMemoryDescription": "Pamięć rezydentna cache względem limitu HOT RAM, gdy jest dostępny.",
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
#!/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)
|
||||
|
||||
|
||||
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]:
|
||||
context = browser.new_context(viewport={'width': width, 'height': 950})
|
||||
page = context.new_page()
|
||||
errors = []
|
||||
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||
load(page)
|
||||
result = page.evaluate('''() => {
|
||||
const prefs = ADMIN_APPEARANCE_DEFAULTS;
|
||||
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;
|
||||
currentPanel = 'overview';
|
||||
window.__savedDashboard = JSON.parse(JSON.stringify(prefs));
|
||||
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);
|
||||
return {
|
||||
order: ADMIN_DASHBOARD_WIDGETS,
|
||||
hidden: ADMIN_DEFAULT_HIDDEN_WIDGETS,
|
||||
density: document.documentElement.dataset.dashboardDensity,
|
||||
samples:window.rustProxyOverviewSamples.getLimit(),
|
||||
serviceCount:document.querySelectorAll('#dashboard-overview-grid [data-dashboard-widget]').length,
|
||||
};
|
||||
}''')
|
||||
assert result['order'] == PRESET['dashboard_widgets'], (width, result['order'])
|
||||
assert result['hidden'] == PRESET['dashboard_hidden_widgets'], (width, result['hidden'])
|
||||
assert result['samples'] == 600 and result['density'] == 'compact' and result['serviceCount'] == 78, result
|
||||
assert_visible(page, ['backend_traffic', 'extra_response_latency', 'wide_transform_activity'],
|
||||
['backend_health', 'stream_sessions', 'www_status', 'bandwidth'])
|
||||
|
||||
# Reproduce the previously flaky operation: opt in hidden backend +
|
||||
# Stream cards and opt out a visible backend card, WITHOUT saving yet.
|
||||
page.locator('#overview-layout-unlock').click()
|
||||
page.wait_for_function('window.rustProxyInlineLayout.isEditing()')
|
||||
outcome = page.evaluate('''() => {
|
||||
const add = id => {
|
||||
const label = dashboardWidgetLabel(id);
|
||||
const item = [...document.querySelectorAll('#overview-inline-hidden-list .overview-inline-hidden-item')]
|
||||
.find(node => node.firstElementChild.textContent === label);
|
||||
if (!item) throw new Error('no hidden card to restore: ' + id);
|
||||
item.querySelector('button').click();
|
||||
};
|
||||
add('backend_health');
|
||||
add('stream_sessions');
|
||||
const card = document.querySelector('[data-dashboard-widget="backend_traffic"]');
|
||||
card.querySelector('.overview-inline-hide').click();
|
||||
return window.rustProxyInlineLayout.currentDraft();
|
||||
}''')
|
||||
assert 'backend_traffic' in outcome['hidden'] and 'backend_health' not in outcome['hidden']
|
||||
assert 'stream_sessions' not in outcome['hidden']
|
||||
assert_visible(page, ['backend_health', 'stream_sessions'], ['backend_traffic'])
|
||||
for _ in range(5):
|
||||
page.evaluate('''() => {
|
||||
const status = {revision:4,backend_pools:[],streams:{active:[],desired:[],metrics:{}}};
|
||||
window.rustProxyBackendDashboard.render(status);
|
||||
window.rustProxyStreamDashboard.render(status);
|
||||
}''')
|
||||
assert_visible(page, ['backend_health', 'stream_sessions'], ['backend_traffic'])
|
||||
# Unrelated settings update must not repaint the saved layout over the draft.
|
||||
page.evaluate('''() => applyAdminPreferences({...window.__savedDashboard, ui_scale:105})''')
|
||||
assert_visible(page, ['backend_health', 'stream_sessions'], ['backend_traffic'])
|
||||
assert page.evaluate('window.rustProxyInlineLayout.isEditing()')
|
||||
page.locator('#overview-inline-cancel').click()
|
||||
assert not page.evaluate('window.rustProxyInlineLayout.isEditing()')
|
||||
assert_visible(page, ['backend_traffic'], ['backend_health', 'stream_sessions'])
|
||||
|
||||
# Save under live traffic, ensure the in-flight preference application
|
||||
# uses the current draft before the editor exits.
|
||||
page.locator('#overview-layout-unlock').click()
|
||||
page.wait_for_function('window.rustProxyInlineLayout.isEditing()')
|
||||
page.evaluate('''() => {
|
||||
const label = dashboardWidgetLabel('backend_health');
|
||||
const item = [...document.querySelectorAll('#overview-inline-hidden-list .overview-inline-hidden-item')]
|
||||
.find(node => node.firstElementChild.textContent === label);
|
||||
item.querySelector('button').click();
|
||||
document.querySelector('[data-dashboard-widget="backend_traffic"] .overview-inline-hide').click();
|
||||
}''')
|
||||
page.locator('#overview-inline-save').click()
|
||||
page.wait_for_function('!window.rustProxyInlineLayout.isEditing()')
|
||||
assert_visible(page, ['backend_health'], ['backend_traffic'])
|
||||
saved = page.evaluate('''() => window.__savedDashboard.dashboard_hidden_widgets''')
|
||||
assert 'backend_traffic' in saved and 'backend_health' not in saved
|
||||
page.evaluate('''() => {
|
||||
const status={backend_pools:[],streams:{active:[],desired:[],metrics:{}}};
|
||||
window.rustProxyBackendDashboard.render(status);
|
||||
window.rustProxyStreamDashboard.render(status);
|
||||
}''')
|
||||
assert_visible(page, ['backend_health'], ['backend_traffic'])
|
||||
# Restore defaults in the modal after a custom layout was saved.
|
||||
page.locator('#overview-layout-open').evaluate('(node) => node.hidden = false')
|
||||
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_visible(page, ['backend_traffic'], ['backend_health', 'stream_sessions', 'www_status'])
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets']
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_hidden_widgets') == PRESET['dashboard_hidden_widgets']
|
||||
assert page.evaluate('document.documentElement.scrollWidth - window.innerWidth') <= 2
|
||||
assert not errors, (width, errors)
|
||||
context.close()
|
||||
print('PASS inline layout defaults, add/hide, live ticks, other preferences, cancel/save', width)
|
||||
# An already-persisted sample choice must override the factory 600.
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
# This sandbox blocks all network origins; stub browser storage before
|
||||
# loading the bundle rather than navigating to a real host.
|
||||
page.evaluate("""() => Object.defineProperty(window, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {getItem: key => key === 'rustproxy.overview.liveChartSamples' ? '120' : null,
|
||||
setItem: () => undefined},
|
||||
})""")
|
||||
load(page)
|
||||
assert page.evaluate('window.rustProxyOverviewSamples.getLimit()') == 120
|
||||
context.close()
|
||||
browser.close()
|
||||
print('PASS RustProxy 0.33.1 simulated browser layout regression')
|
||||
@@ -10,7 +10,7 @@ read = lambda file: (ROOT / file).read_text()
|
||||
manifest = tomllib.loads(read('Cargo.toml'))
|
||||
lock = tomllib.loads(read('Cargo.lock'))
|
||||
version = manifest['package']['version']
|
||||
assert version == '0.33.0'
|
||||
assert tuple(map(int, version.split('.'))) >= (0, 33, 0)
|
||||
assert [p['version'] for p in lock['package'] if p['name'] == 'rust-proxy'] == [version]
|
||||
|
||||
registry = json.loads(read('src/admin/web/dashboard-widgets.json'))
|
||||
@@ -61,7 +61,7 @@ assert re.search(r'\["GET", "POST", "PUT", "DELETE", "other"\]', families)
|
||||
assert 'let wl = labels(&[("web_service", service.id.as_str())]);' in families
|
||||
|
||||
j = json.loads(read('grafana/rustproxy-overview.json'))
|
||||
assert j['title'].startswith('RustProxy 0.33.0')
|
||||
assert j['title'].startswith('RustProxy 0.33.')
|
||||
ids = [p['id'] for p in j['panels']]
|
||||
assert len(ids) == len(set(ids))
|
||||
assert any(v.get('name') == 'web_service' and v.get('multi') and v.get('includeAll') for v in j['templating']['list'])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.33.0",
|
||||
"version": "0.33.1",
|
||||
"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": [
|
||||
|
||||
+19
-10
@@ -25,20 +25,29 @@ fn default_admin_table_density() -> String {
|
||||
"normal".to_owned()
|
||||
}
|
||||
fn default_admin_dashboard_density() -> String {
|
||||
"compact".to_owned()
|
||||
DASHBOARD_DEFAULT_PRESET.dashboard_density.clone()
|
||||
}
|
||||
fn default_admin_dashboard_widgets() -> Vec<String> {
|
||||
DASHBOARD_WIDGETS
|
||||
.iter()
|
||||
.map(|widget| widget.id.clone())
|
||||
.collect()
|
||||
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> {
|
||||
DASHBOARD_WIDGETS
|
||||
.iter()
|
||||
.filter(|widget| widget.default_hidden)
|
||||
.map(|widget| widget.id.clone())
|
||||
.collect()
|
||||
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
|
||||
|
||||
@@ -87,3 +87,61 @@ static DASHBOARD_WIDGETS: std::sync::LazyLock<Vec<DashboardWidgetDefinition>> =
|
||||
serde_json::from_str(include_str!("web/dashboard-widgets.json"))
|
||||
.expect("invalid bundled Overview Cards registry")
|
||||
});
|
||||
|
||||
// A single operator-curated factory preset feeds both the Rust API defaults
|
||||
// and the authenticated dashboard. New registry widgets remain opt-in unless
|
||||
// explicitly added to the preset; saved user preferences are never rewritten.
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DashboardDefaultPreset {
|
||||
dashboard_widgets: Vec<String>,
|
||||
dashboard_hidden_widgets: Vec<String>,
|
||||
dashboard_density: String,
|
||||
overview_samples: u16,
|
||||
}
|
||||
static DASHBOARD_DEFAULT_PRESET: std::sync::LazyLock<DashboardDefaultPreset> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
let preset: DashboardDefaultPreset =
|
||||
serde_json::from_str(include_str!("web/dashboard-defaults.json"))
|
||||
.expect("invalid factory Overview layout preset");
|
||||
let registry = DASHBOARD_WIDGETS
|
||||
.iter()
|
||||
.map(|w| w.id.as_str())
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let order = preset
|
||||
.dashboard_widgets
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let hidden = preset
|
||||
.dashboard_hidden_widgets
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
assert_eq!(
|
||||
order.len(),
|
||||
preset.dashboard_widgets.len(),
|
||||
"duplicate factory widget ID"
|
||||
);
|
||||
assert_eq!(
|
||||
hidden.len(),
|
||||
preset.dashboard_hidden_widgets.len(),
|
||||
"duplicate hidden factory widget ID"
|
||||
);
|
||||
assert!(
|
||||
order.is_subset(®istry) && hidden.is_subset(&order),
|
||||
"unknown widget in factory layout"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
preset.dashboard_density.as_str(),
|
||||
"normal" | "compact" | "ultra"
|
||||
),
|
||||
"invalid factory density"
|
||||
);
|
||||
assert!(
|
||||
matches!(preset.overview_samples, 60 | 120 | 300 | 600),
|
||||
"invalid factory sample count"
|
||||
);
|
||||
preset
|
||||
});
|
||||
@@ -1,62 +1,42 @@
|
||||
#[test]
|
||||
fn overview_registry_is_single_source_of_identifiers_and_visibility_defaults() {
|
||||
fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
let order = default_admin_dashboard_widgets();
|
||||
let hidden = default_hidden_dashboard_widgets();
|
||||
let unique: std::collections::HashSet<&str> = order.iter().map(String::as_str).collect();
|
||||
assert_eq!(unique.len(), order.len());
|
||||
assert_eq!(order.len(), DASHBOARD_WIDGETS.len());
|
||||
assert_eq!(order.first().map(String::as_str), Some("requests"));
|
||||
assert!(order.contains(&"http3".to_owned()));
|
||||
assert!(hidden.contains(&"stream_overview".to_owned()));
|
||||
assert!(hidden.contains(&"stream_backend_health".to_owned()));
|
||||
assert_eq!(order, DASHBOARD_DEFAULT_PRESET.dashboard_widgets);
|
||||
assert_eq!(hidden, DASHBOARD_DEFAULT_PRESET.dashboard_hidden_widgets);
|
||||
assert_eq!(default_admin_dashboard_density(), "compact");
|
||||
assert_eq!(DASHBOARD_DEFAULT_PRESET.overview_samples, 600);
|
||||
for id in [
|
||||
"backend_health",
|
||||
"backend_activity",
|
||||
"backend_errors",
|
||||
"backend_latency",
|
||||
"backend_traffic",
|
||||
"backend_protocols",
|
||||
"backend_requests",
|
||||
"backend_capacity",
|
||||
"backend_http5xx",
|
||||
"backend_timeouts",
|
||||
"backend_network",
|
||||
"backend_retries",
|
||||
"backend_queue",
|
||||
"backend_rotations",
|
||||
] {
|
||||
assert!(order.contains(&id.to_owned()));
|
||||
assert!(hidden.contains(&id.to_owned()));
|
||||
}
|
||||
for id in [
|
||||
"extra_process_cpu",
|
||||
"extra_process_rss",
|
||||
"extra_process_fds",
|
||||
"extra_open_connections",
|
||||
"extra_websocket_active",
|
||||
"extra_tls_error_rate",
|
||||
"extra_request_error_rate",
|
||||
"extra_cache_hit_rate",
|
||||
"extra_cache_memory",
|
||||
"extra_h2_share",
|
||||
"backend_latency",
|
||||
"extra_response_latency",
|
||||
"extra_backend_unhealthy",
|
||||
] {
|
||||
assert!(order.contains(&id.to_owned()));
|
||||
assert!(hidden.contains(&id.to_owned()));
|
||||
}
|
||||
for id in [
|
||||
"traffic_chart",
|
||||
"system_chart",
|
||||
"connection_chart",
|
||||
"cache_diagnostics",
|
||||
"process_diagnostics",
|
||||
"network_diagnostics",
|
||||
"wide_transform_activity",
|
||||
"wide_compression_activity",
|
||||
"cache",
|
||||
"backend_table",
|
||||
] {
|
||||
assert!(order.contains(&id.to_owned()));
|
||||
assert!(!hidden.contains(&id.to_owned()));
|
||||
assert!(order.contains(&id.to_owned()), "not registered: {id}");
|
||||
assert!(
|
||||
!hidden.contains(&id.to_owned()),
|
||||
"should start visible: {id}"
|
||||
);
|
||||
}
|
||||
for id in [
|
||||
"bandwidth",
|
||||
"backend_health",
|
||||
"extra_process_cpu",
|
||||
"stream_traffic",
|
||||
"wide_cache_activity",
|
||||
"www_status",
|
||||
"www_requests",
|
||||
"www_top_services",
|
||||
] {
|
||||
assert!(hidden.contains(&id.to_owned()), "should start hidden: {id}");
|
||||
}
|
||||
assert!(!hidden.contains(&"requests".to_owned()));
|
||||
assert!(hidden.iter().all(|id| order.contains(id)));
|
||||
assert!(validate_admin_preferences(&AdminPreferences::default()).is_ok());
|
||||
}
|
||||
@@ -180,6 +160,38 @@ fn old_widget_preferences_preserve_order_opt_in_and_survive_unknown_ids() {
|
||||
assert!(validate_admin_preferences(&prefs).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_dashboard_order_and_visibility_survive_new_factory_preset() {
|
||||
let mut prefs: AdminPreferences = serde_json::from_str(
|
||||
r#"{
|
||||
"dashboard_density":"normal",
|
||||
"dashboard_widgets":["errors","requests","backend_traffic"],
|
||||
"dashboard_hidden_widgets":["backend_traffic"]
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
reconcile_dashboard_widgets(&mut prefs);
|
||||
assert_eq!(prefs.dashboard_density, "normal");
|
||||
assert_eq!(
|
||||
&prefs.dashboard_widgets[..3],
|
||||
&["errors", "requests", "backend_traffic"]
|
||||
);
|
||||
assert!(!prefs
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"errors".to_owned()));
|
||||
assert!(!prefs
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"requests".to_owned()));
|
||||
assert!(prefs
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"backend_traffic".to_owned()));
|
||||
// WWW was absent from the saved document and is introduced only as opt-in.
|
||||
assert!(prefs
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"www_status".to_owned()));
|
||||
assert!(validate_admin_preferences(&prefs).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_visible_stream_opt_in_is_not_reset_when_normalizing_saved_order() {
|
||||
let mut prefs = AdminPreferences::default();
|
||||
|
||||
@@ -54,6 +54,9 @@ fn admin_app_javascript() -> &'static str {
|
||||
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
|
||||
[
|
||||
"\"use strict\";",
|
||||
"const ADMIN_DASHBOARD_DEFAULTS = Object.freeze(",
|
||||
include_str!("web/dashboard-defaults.json"),
|
||||
");",
|
||||
// The samples controller is needed by top-level listeners in core.js.
|
||||
// Define it first; DOM initialization and translations happen later.
|
||||
include_str!("web/app/overview-samples.js"),
|
||||
|
||||
@@ -18,7 +18,8 @@ window.rustProxyBackendDashboard = (() => {
|
||||
}
|
||||
function syncVisibility(hiddenOverride = null) {
|
||||
const prefs = window.rustProxyPreferences?.current?.();
|
||||
const hidden = new Set(hiddenOverride ?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
|
||||
const hidden = new Set(hiddenOverride ?? window.rustProxyInlineLayout?.currentDraft?.()?.hidden
|
||||
?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
|
||||
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
|
||||
if (![...charts].some(visible)) reset();
|
||||
}
|
||||
@@ -48,8 +49,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
window.rustProxyExtraOverview?.meter?.("backend-dashboard-capacity", null);
|
||||
reset();
|
||||
}
|
||||
// Counter deltas are matched to stable (pool, member) IDs, not aggregate
|
||||
// totals: removed/rebuilt pools must never become negative or spike rates.
|
||||
|
||||
function memberCounters(members) {
|
||||
const values = new Map();
|
||||
for (const { pool, member } of members) {
|
||||
@@ -71,7 +71,6 @@ window.rustProxyBackendDashboard = (() => {
|
||||
for (const [id, next] of current) {
|
||||
const old = earlier.get(id);
|
||||
if (!old) continue;
|
||||
// A generation reset on a member invalidates its entire interval.
|
||||
if (tracked.some(field => next[field] < old[field])) continue;
|
||||
for (const field of tracked) total[field] += next[field] - old[field];
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
}
|
||||
async function save() {
|
||||
if (!draft || saving) return;
|
||||
if (!dirty()) { finish(true); return; }
|
||||
if (!dirty()) { finish(false); return; }
|
||||
saving = true;
|
||||
saveButton.disabled = cancelButton.disabled = unlock.disabled = true;
|
||||
try {
|
||||
@@ -196,5 +196,9 @@ window.rustProxyInlineLayout = (() => {
|
||||
unlock?.addEventListener("click", () => { if (draft) void save(); else void begin(); });
|
||||
saveButton?.addEventListener("click", () => { void save(); });
|
||||
cancelButton?.addEventListener("click", cancel);
|
||||
return { isEditing: () => !!draft, cancel };
|
||||
return {
|
||||
isEditing: () => !!draft,
|
||||
currentDraft: () => draft ? { order: [...draft.order], hidden: [...draft.hidden] } : null,
|
||||
cancel,
|
||||
};
|
||||
})();
|
||||
@@ -1,5 +1,4 @@
|
||||
// Opt-in compact metrics and bounded visualizations. All readings reuse the
|
||||
// existing Overview live-status payload (no additional requests or timers).
|
||||
|
||||
window.rustProxyExtraOverview = (() => {
|
||||
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 60;
|
||||
const names = [
|
||||
@@ -87,13 +86,17 @@ 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) =>
|
||||
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
|
||||
unit: "count", detail: "" };
|
||||
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: ""
|
||||
};
|
||||
}
|
||||
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);
|
||||
@@ -123,8 +126,10 @@ 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 "-";
|
||||
@@ -144,7 +149,7 @@ window.rustProxyExtraOverview = (() => {
|
||||
const now = performance.now();
|
||||
for (const id of names) {
|
||||
const tile = node(id);
|
||||
if (!tile || tile.hidden) { lastCounter.delete(id); continue; }
|
||||
if (!tile) { lastCounter.delete(id); continue; }
|
||||
const metric = read(id, status, now, record);
|
||||
const value = metric.value;
|
||||
if (lastUnits.get(id) !== metric.unit) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
window.rustProxyOverviewSamples = (() => {
|
||||
const allowed = new Set([60, 120, 300, 600]);
|
||||
const storageKey = "rustproxy.overview.liveChartSamples";
|
||||
let saved = 60;
|
||||
const factory = typeof ADMIN_DASHBOARD_DEFAULTS === "object" ? Number(ADMIN_DASHBOARD_DEFAULTS.overview_samples) : 60;
|
||||
let saved = allowed.has(factory) ? factory : 60;
|
||||
try {
|
||||
const stored = Number(localStorage.getItem(storageKey));
|
||||
if (allowed.has(stored)) saved = stored;
|
||||
|
||||
@@ -84,7 +84,8 @@ window.rustProxyWideOverview = (() => {
|
||||
const now = performance.now();
|
||||
for (const [id, config] of Object.entries(configurations)) {
|
||||
const node = tile(id);
|
||||
if (!node || node.hidden) { baselines.delete(id); continue; }
|
||||
if (!node) { baselines.delete(id); continue; }
|
||||
|
||||
const values = config.counters ? counterRates(id, config.counters(status), now, record)
|
||||
: config.values(status).map(finite);
|
||||
const series = history.get(id) || config.names.map(() => []);
|
||||
|
||||
@@ -4,12 +4,16 @@ const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contra
|
||||
const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]);
|
||||
const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]);
|
||||
const ADMIN_CORNER_STYLES = new Set(["default", "square", "subtle", "rounded", "extra"]);
|
||||
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => widget.id));
|
||||
const ADMIN_DASHBOARD_BY_ID = new Map(ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => [widget.id, widget]));
|
||||
const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze(ADMIN_DASHBOARD_WIDGET_REGISTRY.filter(widget => widget.default_hidden).map(widget => widget.id));
|
||||
const ADMIN_PRESET_ORDER = Object.freeze(ADMIN_DASHBOARD_DEFAULTS.dashboard_widgets.filter(id => ADMIN_DASHBOARD_BY_ID.has(id)));
|
||||
const ADMIN_DASHBOARD_WIDGETS = Object.freeze([...new Set([...ADMIN_PRESET_ORDER, ...ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => widget.id)])]);
|
||||
const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
||||
...ADMIN_DASHBOARD_DEFAULTS.dashboard_hidden_widgets.filter(id => ADMIN_DASHBOARD_BY_ID.has(id)),
|
||||
...ADMIN_DASHBOARD_WIDGET_REGISTRY.filter(widget => widget.default_hidden && !ADMIN_PRESET_ORDER.includes(widget.id)).map(widget => widget.id),
|
||||
])]);
|
||||
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: "compact", 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],
|
||||
});
|
||||
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;
|
||||
@@ -215,13 +219,19 @@ function formatDateTime(value) { const date = value instanceof Date ? value : ne
|
||||
function formatTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleTimeString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
|
||||
function applyAdminPreferences(value, cache = true) {
|
||||
if (!value) return value;
|
||||
const dashboard = applyDashboardLayout(value.dashboard_widgets, normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets));
|
||||
// A live inline edit is a draft, not a saved preference. Background updates
|
||||
// (including an unrelated preference save) must not repaint the old layout.
|
||||
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 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: dashboard.order, dashboard_hidden_widgets: dashboard.hidden,
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
window.rustProxyRefresh?.update(normalized.refresh);
|
||||
@@ -376,7 +386,7 @@ 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: "compact", 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] },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
}));
|
||||
|
||||
@@ -16,7 +16,8 @@ window.rustProxyStreamDashboard = (() => {
|
||||
}
|
||||
function syncVisibility(hiddenOverride = null) {
|
||||
const prefs = window.rustProxyPreferences?.current?.();
|
||||
const hidden = new Set(hiddenOverride ?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
|
||||
const hidden = new Set(hiddenOverride ?? window.rustProxyInlineLayout?.currentDraft?.()?.hidden
|
||||
?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
|
||||
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
|
||||
if (card("stream_traffic")?.hidden && wasVisible) clearTraffic();
|
||||
wasVisible = !card("stream_traffic")?.hidden;
|
||||
|
||||
@@ -88,7 +88,7 @@ window.rustProxyWebOverview = (() => {
|
||||
}
|
||||
function smallPath(values) {
|
||||
const peak = Math.max(1, ...values.filter(v => v != null));
|
||||
return values.map((value, i) => `${i * 300 / Math.max(1, values.length - 1)},${55 - (value || 0) / peak * 50}`).join(" ");
|
||||
return values.filter(value => value !== null).map((value, i, present) => `${i * 300 / Math.max(1, present.length - 1)},${55 - value / peak * 50}`).join(" ");
|
||||
}
|
||||
function ring(id, amounts) {
|
||||
const total = amounts.reduce((n, value) => n + value, 0);
|
||||
@@ -154,7 +154,7 @@ window.rustProxyWebOverview = (() => {
|
||||
meter("www_error_rate", r?.errorRate);
|
||||
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
|
||||
show("www_php_requests-value", fmtRate(r?.phpRps));
|
||||
if (record && !tile("www_php_requests")?.hidden) {
|
||||
if (record) {
|
||||
const values = histories.get("www_php_requests") || [];
|
||||
values.push(r?.phpRps ?? null); window.rustProxyOverviewSamples.trim(values);
|
||||
histories.set("www_php_requests", values);
|
||||
@@ -166,7 +166,7 @@ window.rustProxyWebOverview = (() => {
|
||||
};
|
||||
for (const [id, names] of Object.entries(seriesNames)) {
|
||||
const series = histories.get(id) || names.map(() => []);
|
||||
if (record && !tile(id)?.hidden) {
|
||||
if (record) {
|
||||
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
|
||||
histories.set(id, series);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"dashboard_widgets": [
|
||||
"requests",
|
||||
"active",
|
||||
"bandwidth",
|
||||
"errors",
|
||||
"memory",
|
||||
"websocket",
|
||||
"backend_traffic",
|
||||
"extra_response_latency",
|
||||
"http_protocol",
|
||||
"tls",
|
||||
"websocket_protocol",
|
||||
"backend_latency",
|
||||
"stream_overview",
|
||||
"stream_traffic",
|
||||
"stream_sessions",
|
||||
"stream_backend_health",
|
||||
"backend_health",
|
||||
"backend_activity",
|
||||
"backend_errors",
|
||||
"backend_retries",
|
||||
"http2",
|
||||
"backend_protocols",
|
||||
"backend_requests",
|
||||
"backend_capacity",
|
||||
"backend_http5xx",
|
||||
"backend_timeouts",
|
||||
"backend_network",
|
||||
"http3",
|
||||
"backend_queue",
|
||||
"extra_open_connections",
|
||||
"extra_process_cpu",
|
||||
"extra_process_rss",
|
||||
"extra_process_fds",
|
||||
"backend_rotations",
|
||||
"extra_websocket_active",
|
||||
"extra_tls_error_rate",
|
||||
"extra_request_error_rate",
|
||||
"extra_cache_hit_rate",
|
||||
"extra_backend_active",
|
||||
"extra_h2_share",
|
||||
"extra_cache_memory",
|
||||
"extra_backend_unhealthy",
|
||||
"extra_total_rps",
|
||||
"extra_active_exchanges",
|
||||
"extra_h1_share",
|
||||
"extra_h3_share",
|
||||
"extra_cache_miss_rate",
|
||||
"extra_cold_storage",
|
||||
"wide_protocol_rates",
|
||||
"wide_cpu",
|
||||
"wide_memory",
|
||||
"wide_cache_activity",
|
||||
"wide_connections",
|
||||
"wide_backend_health",
|
||||
"wide_latency",
|
||||
"cache",
|
||||
"traffic_chart",
|
||||
"system_chart",
|
||||
"connection_chart",
|
||||
"wide_transform_activity",
|
||||
"wide_compression_activity",
|
||||
"cache_diagnostics",
|
||||
"extra_cold_queue_memory",
|
||||
"process_diagnostics",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"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"
|
||||
],
|
||||
"dashboard_hidden_widgets": [
|
||||
"stream_overview",
|
||||
"stream_backend_health",
|
||||
"tls",
|
||||
"stream_sessions",
|
||||
"backend_health",
|
||||
"backend_activity",
|
||||
"backend_errors",
|
||||
"backend_protocols",
|
||||
"backend_requests",
|
||||
"backend_timeouts",
|
||||
"backend_network",
|
||||
"extra_process_cpu",
|
||||
"extra_process_rss",
|
||||
"extra_process_fds",
|
||||
"extra_websocket_active",
|
||||
"extra_tls_error_rate",
|
||||
"extra_request_error_rate",
|
||||
"extra_h2_share",
|
||||
"extra_backend_unhealthy",
|
||||
"extra_total_rps",
|
||||
"extra_h1_share",
|
||||
"extra_h3_share",
|
||||
"wide_memory",
|
||||
"wide_connections",
|
||||
"wide_backend_health",
|
||||
"wide_protocol_rates",
|
||||
"extra_active_exchanges",
|
||||
"extra_cache_miss_rate",
|
||||
"extra_cache_hit_rate",
|
||||
"backend_capacity",
|
||||
"stream_traffic",
|
||||
"backend_http5xx",
|
||||
"bandwidth",
|
||||
"backend_queue",
|
||||
"wide_cpu",
|
||||
"wide_latency",
|
||||
"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",
|
||||
"extra_cold_queue_memory",
|
||||
"wide_cache_activity",
|
||||
"backend_rotations"
|
||||
],
|
||||
"dashboard_density": "compact",
|
||||
"overview_samples": 600
|
||||
}
|
||||
Reference in new issue
Block a user