This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 10:37:23 +02:00
1 parent 2ccc598b12
commit b757b34a31
26 files changed
+670 -115

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
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
+15
View File
@@ -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
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.33.0"
version = "0.33.1"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -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"
+45 -1
View File
@@ -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.
+9
View File
@@ -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
View File
@@ -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
+47
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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'])
+1 -1
View File
@@ -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
View File
@@ -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
+58
View File
@@ -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(&registry) && 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
});
+59 -47
View File
@@ -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();
+3
View File
@@ -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"),
+3 -4
View File
@@ -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];
}
+6 -2
View File
@@ -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,
};
})();
+14 -9
View File
@@ -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) =>
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);
@@ -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) {
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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(() => []);
+16 -6
View File
@@ -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);
}));
+2 -1
View File
@@ -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;
+3 -3
View File
@@ -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);
}
+137
View File
@@ -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
}