V0.32.6 #1

Merged
gru merged 4 commits from v0.32.6 into master 2026-10-14 13:05:10 +02:00
39 changed files with 695 additions and 232 deletions
Showing only changes of commit fc8711d4d8 - Show all commits

No files matched your search

+17
View File
@@ -5,6 +5,23 @@
## 0.32.6
- **Development-branch diagnostics charts (no version bump):** publish cached
CPU, process CPU/RSS and per-interface RX/TX samples in compact live status;
charts follow the Overview update cadence (default 3 s, accepting saved 2 s)
while all Process details still follow the selected System-info refresh. The
OS collector cadence remains unchanged, with no extra probe per SSE event.
Remove nested mobile scrolling in the interface chart list as a mitigation
for reported WebKit jumps near Process/Network; physical iOS 27 validation is
still required. Match the Origin protocol badge's radius to neighboring
policy pills for both default and configured corner styles, preserving colors.
- **Development-branch rate continuity (no version bump):** preserve cache
HIT/MISS and other counter-derived chart histories when a browser tab is
suspended, minimized or restored from BFCache. Treat resumed/overdue
observations as new counter baselines, not artificial 0/s samples. Show
a missing-rate marker until the next fresh interval; retain real 0/s
values. Apply one shared rule to Cache, Transform, Overview, backend,
vhost and Stream charts without adding polling or changing System-info
scheduling. Add isolated JavaScript pause/resume and reset tests.
- **Development-branch follow-up (no version bump):** update existing network-interface
diagnostic chart nodes in place during live refresh, preserving DOM identity
and avoiding scroll-anchor loss around the Process and Network panels on touch
+19 -3
View File
@@ -9,12 +9,28 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
## What changed in 0.32.6
- **Rate-chart pause recovery (0.32.6 development branch):** share one
counter continuity policy across Cache HIT/MISS/BYPASS, Transform, HTTP
Overview/protocols, optional wide/small charts, backend and Stream/Vhost
traffic charts. After tab hide/pagehide (including BFCache) or a delayed
refresh, keep plotted history, use the first reading as a new baseline,
and resume rate points on the next normal interval. A genuine measured
zero still appears as 0/s. Cache and Transform intervals and the separate
System information interval remain configurable; no additional polling.
- **Development fixes (same 0.32.6 version):** keep Network diagnostics
chart DOM stable across live refreshes, preventing repeated row/SVG replacement
during scrolling. Network-interface chart history advances at the Overview
live-status cadence using the latest sampled rates; fetching system data still
follows the independent System information interval in Settings. Repair CSS
descendant selector transformation and protect admin responses with
follows the independent System information interval in Settings. Current
CPU/RSS and per-interface RX/TX chart points now come from the existing
background OS sampler through compact SSE/fallback data, every **3 s** by
default (saved **2 s** settings remain valid), while Process details still
follow the configured System information refresh. On mobile the Network
interface chart list no longer traps touch scrolling in a nested scroll area.
Origin protocol badges use exactly the same rounding as adjacent pills for
all corner presets, without changing their color. Repair CSS descendant
selector transformation and protect admin responses with
`Cache-Control: no-transform`. Real iOS 27 scrolling remains unverified.
- **Mobile Overview:** display two metric tiles per row on phone-width canvases,
@@ -230,7 +246,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
- Improved mobile panel loading behind reverse proxies: one ordered JavaScript resource, versioned assets and retry of temporary configuration errors.
- **Panel startup/mobile follow-up:** replace the former white loading state with a RustProxy spinner, an automatic delayed reload suggestion and an error screen with reload link. Paint the shell before waiting for preferences/configuration/initial status; the header's **Refresh now** action sits beside the Overview title without adding a row. Negotiate Zstd/Brotli/gzip for fixed panel HTML and assets, with one-time encoding per resource; preserve public login and identity clients.
- Refresh Overview through a shared, authenticated SSE channel with per-administrator live pacing (1, 2, 5 or 10 seconds); fallback status/log polling is active only after SSE failure. Full configuration and system diagnostics are not serialized on every live event.
- Refresh Overview through a shared, authenticated SSE channel with per-administrator live pacing (1, 2, 3, 5 or 10 seconds; default 3); fallback status/log polling is active only after SSE failure. Detailed system diagnostics remain in the separately scheduled API; live status carries a small cached CPU/RSS/network chart snapshot.
- **Settings → Appearance → Data refresh** now stores per-account Live enable/disable and independent Traffic, Cache and System information intervals. Manual mode (`0`) and the global Live switch disable automatic requests while preserving manual refresh, charts, filters and pagination.
- Poll only the current Traffic/Cache view, and fetch system diagnostics independently from `/api/system-info` when Overview or the relevant Settings section needs them. The previous full `/api/status` response remains supported, while `?view=live` provides compact fallback.
- Centralize dashboard polling with cancellation/in-flight guards, hidden-tab pause and logout cleanup; retain SQLite schema 9, previous preferences, proxy traffic and the existing authentication model.
+12 -3
View File
@@ -2,11 +2,20 @@
## 0.32.6 - Mobile Overview and upstream protocol visibility (runtime acceptance pending)
- [x] Share counter-rate pause/resume handling between Cache, Transform,
Overview, backend, Stream and Vhost plots. Retain historical points after
backgrounding, skip suspended intervals and restart normal samples after
one fresh counter baseline, with no new polling.
- [ ] Validate actual minimized-tab/BFCache chart behavior in iOS 27 Safari.
- [x] Development follow-up: preserve Network diagnostic chart DOM through
periodic refresh and fix descendant CSS selector minification.
- [x] Keep network-interface charts advancing with live Overview status without
increasing the separately configured system-info polling rate; move the
per-backend protocol badges alongside the other Vhost policy badges.
- [x] Feed Process CPU/RSS and per-interface network charts from the cached
background OS sampler through live status (default 3 s). Leave slow Process
details on `system_info_seconds` and retain valid 2-s saved preferences; do
not add per-request probes. Group upstream badges with other Vhost badges
and match their corner radius while preserving observed protocol colors.
- [x] On phone-size Overview, remove the nested Network interface chart scroll
region, while retaining chart SVG/text nodes on ordinary live updates.
- [ ] Confirm scroll positions with a real iOS 27 phone while live updates are
enabled, including the Process / Network diagnostics boundary.
+38 -35
View File
@@ -2,17 +2,17 @@
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
a402ee07826dfab1b1d63b98c4a9f75809f056b908c92abacf0bba816d7ad133 AGENTS.md
7714a8877ae51e8ba453c54c4fb0d9684cd50a46688536e0511d7a1346a3e306 CHANGELOG.md
54383ec0610f11c61ef64d3cf0afa63f5667b212c03db653f37a9277a637df8c CHANGELOG.md
934ec0e7f8a307b00319606a9c5d7e91dac6449a23dd1f3abcefe4304eb4de06 Cargo.lock
8a7606650ce03f1faf36c893a17ccf1c9e8da4259e0248ac2c9bf030a64872d1 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
7f337f1d8d82cf296859daf17101abb4f095cd0f3e4c1538d2307f1ae1382ddf README.md
5253814978618396d472b5469dcbf66d2e8f7da53e20496d4c8f66bb3cd76f15 ROADMAP.md
dc35bafda41c2e0db266523f553d0803740c5e842edfbdd7af202041c53aa359 README.md
da519345b2bce492e747c268df2730971543d1bdb198c3e854671a7d107e5609 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
ab6ebc1a886b9ed7d29a44c1676641a6b5d2a94a6b027da8f75e8e82ac02ec67 dev.sh
c51ac385fa992cdcfade11aba0151e20f1d2d01362deac7c04e585dfbc287f19 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -27,7 +27,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
1dc71311873be1372ae2383487b6c5924e288f5774496e315edf24faa35cf08a docs/API.md
627f1d8727321ef8961b153aabab7448f239fa383177bb9ef115cfeef48452da docs/API.md
f27053f8a93d7883025a04922d015e0b697cad653e714be632b757e167297de1 docs/API_ACCESS.md
6595694c6fb6c52e26c4a15822b5ca8564d4e0de7f5a7fea5c24ab67c24cb49a docs/ARCHITECTURE.md
fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md
@@ -61,9 +61,9 @@ d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.m
ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
5e615c119848cc474f4b005dbe3b58a7ed8f29375ff409282285c39b0bdc196a docs/SETTINGS.md
46d3cb5a2b5909902c85de193bc5129c751c5ebd19dc752db282588d54b5e25c docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
c2715cfa0bf20185e5cd94b2219a958f0515a488395f9f05a92350369cd96f27 docs/TEST_REPORT.md
81997f880b89ea68b2820f47a83db030425e91ef5da90d1bd459754a1eb63f6c docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
@@ -154,9 +154,9 @@ b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/cert
ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs
c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
7de7da489a367658b700f00776fa43604d3554500b5fbef4da2764e402e68afa src/admin/logging_api.rs
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
5efda93041025b9a347e7d95af8af642c5cd3c5a2dd2d3ee5c6df2f9101bdc42 src/admin/openapi.json
9426fba372656cb5abcc76e88e3885d1e9abc7ecf04653abc8c31803aeece019 src/admin/openapi.json
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
@@ -165,30 +165,31 @@ cd0fda2b5a7b86a6619d6fa7cf5cef5a2598f033a8e9304475c4a05a169c958c src/admin/pref
c17ccba5202d375d4cc593f7e20979f2b796362d87660b898feca0af5a135bfc src/admin/preferences_tests.rs
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs
223310e1339094941c45a5cb65d650673267b7fe8a3aa0927c8c1e3d72de68cd src/admin/router.rs
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
a9a8b9cce86990d5759eba2f9b29dce885fef056d4f81775bcbd429f4c00bba3 src/admin/router.rs
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
e7355ef15e4377cd71e47d054c607079dd8e6711f1d86824f716b8003210a046 src/admin/status.rs
e79d3dc0c4fde1ca4a69c204fb41cc5aa1db02aefdabff3435f27b5db0c6eb8d src/admin/status_live.rs
479eca6162df5152ce536727b1090dd8e2e262775f2c8cf5bb2c8fa40cb2a600 src/admin/status_live.rs
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
7be070a259d534fd6d1069207a464434602d94143f8f463b1dddb1c45f3dc797 src/admin/tests.rs
18b778c2c4e723e2fe5cbaaef20effa6ea50fcc35d507ebde2361c9b6eda62b2 src/admin/tests.rs
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
f7760d52336fa3968ed793806e68d8ffa15299e567cef90cd7a1ce66dd30488d src/admin/web/app/backend-dashboard.js
326e404ea67b23318a22cc6161ea42b93644b16c3897e5d72e95636a6ea2fccc src/admin/web/app/backend-dashboard.js
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
446ad63b6ff96e9ebc6455c454c539703aa751d4c26386abbb8512ceff9e3d42 src/admin/web/app/core.js
acb778341181553585b66ba35e7b71fda33246570f125ffce349ba2cf0394fa4 src/admin/web/app/core.js
dec0b875086e3567486191231056bfc6c1edef006f7aad7c88f19ef37c034570 src/admin/web/app/dashboard-inline-layout.js
dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/app/dashboard-layout-editor.js
fc5b025bad26684c8cf0a9917a7a24fdac3aa79414001335f29fb7360c3fa703 src/admin/web/app/overview-extra-metrics.js
e5928664c259feab3e9cfd3be69d05571f3aa60445ca373560f50857ecc8bfc0 src/admin/web/app/overview-extra-metrics.js
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js
79d31e2002f8cf334f87285f6f2e6dbf69f3647de784ae26efadb92c126bc7a0 src/admin/web/app/overview-wide-metrics.js
c4d59e129837db854f2851ac69130325f6d2f25fd1b67a009354b83da1dfbf02 src/admin/web/app/overview-wide-metrics.js
a73d4efc9cf2f6bf7117f6f6dcaaea106e03c328451abbe4c10281179750899d src/admin/web/app/preferences.js
6a544baaf40763b4b612e53845b3bc33396b9bb6518565fd6b7c128c278b8d5d src/admin/web/app/refresh.js
3b4a7c1538d9e3fe7f9eb3522fcebe077c336d4441c2ee6253d48483cf47c121 src/admin/web/app/runtime.js
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
96b19ad3fe98541013f278d63ccbe5c3f5a7f9b04a02167780176e6b15532057 src/admin/web/app/runtime.js
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
cc566f8293cc935f5080ebfb8076293c256bb6a87e1e6a69be03512448871ec9 src/admin/web/app/stream-dashboard.js
a854e08f947a4d13fc4b27286a096dcb2773dc0358a28b27703a2320ba7e8bf7 src/admin/web/cache-panel.js
fbd2c402828088695f5f5f4b210d1ea8da24ae6425f61248c82c5b07b14afd56 src/admin/web/app/stream-dashboard.js
0603dc8793a731a0e582cbbec53aa6d1cc6e007faafcec5d8843730b2cdc39e1 src/admin/web/cache-panel.js
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
da3683f3aa0cec4f86b0ff0fb105959caac4ce65c41a9391f5adb3c289e8ff1c src/admin/web/dashboard-widgets.json
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
@@ -212,7 +213,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
c24411dd62afaecfc631682554d4a037c7dd27100d817e9901ccdfd505c2ed5b src/admin/web/index.html
2574793d504ec835e477c599187b98d50014686da215cc8adc7548c1e2bdbe5e src/admin/web/index.html
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
@@ -221,16 +222,16 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
e3075af59644e42fa5735574d8e9caa1bcebfe8de184b78aff9f4026b91915f1 src/admin/web/streams.js
9671caf7468d9e7aaf679e6f02a118603c1e9b88b6c3a009297b265c463eda1e src/admin/web/style.css
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
aed93c1eb1d18735d9058755f3e648f0f74f2797855446f1d10d330dbf306edd src/admin/web/style.css
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
b31782c61da3623389b57fdbba5bd41d2287d45fc172e9c14360a9dd7accf061 src/admin/web/vhosts/editor.js
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
823038b8eb335ca766f03b5c718e093f5f026c85dad06ace05ff70c4a7c1410f src/admin/web/vhosts/render.js
b991e81a98dd604a1d7c30158c0cd8cd825479bd2cd94b082b3550de1071bb58 src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
355684980d1caf8918d1773ce17934b51e3a85a60f38491217d10035bf68ce9f src/admin/web/web-server.js
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
@@ -500,7 +501,7 @@ c9d0bef4b8ed7a2f49b0fad3e116cace62d3e6e1ca31e02a3073d1fdde5157d8 tools/checks/a
02b02ad7831524f963da3b80b1b42bb6efef9c4a63ce89649b18f70a0110d32d tools/checks/appearance_ux_0323_browser.py
8b6e1a355cae58901118b399fa2d165f7e3fd8465c89cac82776906b8b23f0a6 tools/checks/appearance_ux_0323_ui_test.mjs
f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_ui_test.mjs
f34f0f78edc4b6c6226006337090c72d1f8e57ddd77939b37cf00538a05abfa8 tools/checks/backend_dashboard_0323_ui_test.mjs
e82765ae946461952e68369f0b16f0a99f01e625bd61d2551513504f7b2f4d46 tools/checks/backend_dashboard_0323_ui_test.mjs
45cd21192248ffe6f66c20bab03da1a5ce299f040883764d09a640859e19924d tools/checks/backend_health_0300_ui_test.mjs
a135c9456143975279f18233caab44b8c64f5025ef4ceeb1f2ad22090b588019 tools/checks/backend_protocol_0326_ui_test.mjs
a1fb118e3a32819689cb607d574b0060ca29ff1f6a68c505231a4e88ea93b5de tools/checks/certificate_api_0319.py
@@ -519,24 +520,26 @@ dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/d
72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs
c081925edadee3ffb21867765fcd08d6a536093999ee99c5590b1d5980f8e4b3 tools/checks/h3_navigation_0316.py
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
e1a8ae9ab6b4ef8387ac0a84e8825e2bfed70e9b64f7cb23ed542fce242a141e tools/checks/ios_scroll_0326_browser.py
fb8619444edf81afa604194f750dc76335394a73c74324eda59e392874f29ee8 tools/checks/ios_scroll_0326_browser.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
c56a7763250f8cf1ac011feb0c90a06309bd680aeb6da6e1d1f825fbb2a573aa tools/checks/live_diagnostics_0326_browser.py
a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py
c0f08ee32fb506e946a2cf3308c061da7351e6022337aa9114b21d7ad19203c8 tools/checks/mobile_protocol_0326.py
c7ddc69c8bfe5a675ae48d02c76c85d0d0490bc1da1833a5f99dc459d7fe9639 tools/checks/mobile_protocol_0326_browser.py
db40b3940989dd6e32459574b4a5ba3219ebec8e2e68dea1f71ec4a9e53e78b0 tools/checks/mobile_protocol_0326_browser.py
af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs
0387a9e29244b58cf37a6673a88ae2b7fea3aa965c032423f3783a84eab4ba05 tools/checks/overview_0321_layout_regression.mjs
53917376134d73fff32a68239b513c4eedf98bb65224c1c41c0e5cfc8fcb119b tools/checks/overview_extra_metrics_0323_ui_test.mjs
7fda61ae66a6e3f1ada1304440c09e1f43d0e577b3a5eac8331afe86fc5eebcb tools/checks/overview_extra_metrics_0323_ui_test.mjs
48dd1e13b83dc897c735028c93e17200cf4ed9ce175475dc5641ec3236569c38 tools/checks/overview_grid_spacing_0323_browser.py
d530cb9b783d6e7ea3afd400a35a64865d16e11800bbf8910cb7edb9648f5510 tools/checks/overview_gutters_0324_browser.py
b565aef4594265350240c24067a554f91fd605d0e920c03eef0b5b62c3934aad tools/checks/overview_registry_0323_ui_test.mjs
f450e97dccf75c24e228e696f21ed4ae826a823ea4edde2429894a6b6b36eaab tools/checks/overview_uniform_grid_0323_ui_test.mjs
a1e68daf3a72b65545b0f5346094cbc0357c80d88504603090b776ef770f08de tools/checks/overview_wide_masonry_0323_ui_test.mjs
644ca805fd7072a3087cfb8c4a41fc53eefe0e902e5d1524d8f892657970493f tools/checks/overview_wide_masonry_0323_ui_test.mjs
1ac812929c2a8c0699ae3bdebd0d7e05b5bcaf86c87533bb669b5ba7955e3245 tools/checks/panel_cache_http_0310_ui_test.mjs
eea7cdc9c7d8dd0f5fb079cfff657a6feadfc0b856f63265bfbd8257f1beca19 tools/checks/preflight.py
cdd59fdc84eea535584a52143e0c69285c76dfcc6d02587cd707aa4a5f220ca3 tools/checks/preserve_host_0324.py
3352941648add6d8a8044c0b5cad6bf264546d38c884a5beed64cd95addd850c tools/checks/preserve_host_0324_ui_test.mjs
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
64984e55d93617ac61c83f19b91a2f70402057a47a1962dcbcfd0b7d904f6098 tools/checks/rate_pause_0326_ui_test.mjs
599e0fa12fe95c7087a25801a26a590bbb473f24f038e0be157449daecec5593 tools/checks/refresh_0310_ui_test.mjs
c11583dad0899f8b945cae6e2f6cb0bdb55f5526bd5f42c248ac41badec83679 tools/checks/release_0317.py
b84b66a6f8f63cd0a354ef607b96ff60c77ef37f80f49ea9a9036b33147c0b6a tools/checks/release_0317_ui_test.mjs
c122954d9ce7165dfef1ce3e71a786b7bc86e9076f66d9a87baf14d269392757 tools/checks/release_0318.py
@@ -549,8 +552,8 @@ adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/s
db8881e46622ca2dbab4c29df003020006c611cd7fe087878c3e9c930cdd9853 tools/checks/security_logs_0315_ui_test.mjs
011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py
37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py
bb862620104f925488e36904e258b7c934a097362ef879ecb275c64008cebb83 tools/checks/stream_0320_ui_test.mjs
19cb347c87b0fa462fc2f3f2f5bef62e93628334ab4b8a1776df417e47c91ef0 tools/checks/stream_0320_visibility_regression.mjs
be78df1e438d76be83e15398b310c9843ae5965b0ec6b41279b3a9650c313d8a tools/checks/stream_0320_ui_test.mjs
e04f41b09c95e7bf67944c85690aafd825822877e9e0058cc872fc9e29bac78e tools/checks/stream_0320_visibility_regression.mjs
8542f5c668155aa9f70de11e6fdaf64c5a80629a15ecc5f1d8c40f3d655f9b55 tools/checks/system_info_0301_ui_test.mjs
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py
e650225258168eeb9623694c6684f9a5ace92ca94debdff2558b5c77f7c998bc tools/checks/upstream_protocols_0323.py
@@ -558,7 +561,7 @@ a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/v
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
bdc20ccb7ee8c67eab6ef22b2011a99079d69fea49d03ab634da7de1b1e7b909 tools/checks/vhost_protocol_badge_group_0326_browser.py
ea98e8ce2bdc841814e486ea4c13f94e284b2c50a5887598b7a288f75a14deaf tools/checks/vhost_www_ui_test.mjs
37a74b5fea295efc3ff77609bedf0c3c5352b92418157961419b914d738efe6b tools/checks/web_assets.py
2df48af5b438eb811a5babd6401db830c6fa77ed5bb03bf80f689da8440c93e1 tools/checks/web_assets.py
8d2e1837f94302d09a704aca452cc367e6892f289aca3dc2d82c086a2e7de26d tools/checks/web_cache_revalidation_0322.py
8ec055a16198701dd4fa35eac42a71b666d2b9bbe90edb1c797c7ed759615487 tools/checks/web_server_contracts.py
35f76060e585a96c623ef83bd5f771969f5be16a0e1ae626a3092d7458b84e84 tools/checks/web_service_ui_test.mjs
+1
View File
@@ -130,6 +130,7 @@ if [[ "$mode" == "test" ]]; then
node tools/checks/appearance_ux_0323_ui_test.mjs
node tools/checks/backend_protocol_0326_ui_test.mjs
node tools/checks/overview_extra_metrics_0323_ui_test.mjs
node tools/checks/rate_pause_0326_ui_test.mjs
node tools/checks/overview_wide_masonry_0323_ui_test.mjs
node tools/checks/overview_uniform_grid_0323_ui_test.mjs
node tools/checks/overview_0321_layout_regression.mjs
+2 -2
View File
@@ -94,7 +94,7 @@ framework extraction failures are not guaranteed to have that JSON shape.
`status.streams` contains `active`, `desired`, `restart_required` and a `metrics` map keyed by startup-active stream ID. Stream counters include active/total/completed sessions, rejects, errors, RX/TX bytes and UDP packet counts; the dashboard derives rates from byte-counter deltas.
| `GET /api/logs` | Recent bounded access entries, ring capacity and contention-drop count |
| `GET /api/audit` | Last 100 audit entries; database retention is 10,000 |
| `GET /api/events` | SSE `snapshot` events containing status and logs |
| `GET /api/events` | SSE `snapshot` events containing status and logs; the compact status includes `live_system` for CPU/RSS/network charts |
| `GET /metrics` | Prometheus text exposition |
| `GET /api/metrics` | Alias of `/metrics` |
@@ -102,7 +102,7 @@ Except for health and authentication/bootstrap routes, these endpoints require a
session. These administrative previews are not anonymous. The separate configurable
exporter uses its own CIDRs and optional bearer token; see the scrape section below.
SSE has eight viewer slots, generates a snapshot every 1/2/5/10 seconds per authenticated client (via `?interval=`, default 2)
SSE has eight viewer slots, generates a snapshot every 1/2/3/5/10 seconds per authenticated client (via `?interval=`, default 3)
and rechecks session validity. It does not queue an unlimited per-viewer event log
or guarantee replay. The browser falls back to compact status/log polling only after SSE fails and stops polling after its recovery. Invalid SSE intervals are rejected with 422. An active dashboard renews
idle activity, not the absolute session deadline.
+2 -2
View File
@@ -44,14 +44,14 @@ The existing **Generate / rotate token** form still creates new Key IDs (and ret
| Preference | Default | Allowed seconds |
|---|---:|---|
| `refresh.live_enabled` | `true` | `true`, `false` |
| `refresh.overview_seconds` | 2 | 1, 2, 5, 10 |
| `refresh.overview_seconds` | 3 | 1, 2, 3, 5, 10 |
| `refresh.traffic_seconds` | 5 | 0, 3, 5, 10, 30 |
| `refresh.cache_seconds` | 5 | 0, 3, 5, 10, 30 |
| `refresh.system_info_seconds` | 60 | 0, 15, 30, 60, 300 |
`live_enabled=false` disables automatic SSE **and all polling**, including system information. `0` disables only that source's automatic polling. Overview and System information have **Refresh now** buttons; Traffic and Cache retain their own buttons, filters, charts and pagination. The per-view Auto checkboxes for Traffic/Cache/Transform and the Docker live switch remain independent opt-outs, and none changes another administrator's preferences. **Reset section** restores refresh defaults without changing theme or unrelated preferences.
The main `/api/events?interval=N` connection (allowlisted 1/2/5/10, default 2 seconds) carries compact live status plus the existing bounded log snapshot. The server checks the session on every event and retains its heartbeat, viewer cap and normal disconnect cleanup. On an SSE error the browser polls `/api/status?view=live` and `/api/logs` until snapshots resume, not concurrently during healthy streaming. The old full `/api/status` response is kept for API clients and initial diagnostics. The separate authenticated `/api/system-info` reuses cached diagnostics and ACME metadata, never runs diagnostic subprocesses per request, and is fetched automatically only for Overview or the relevant Settings pages. A hidden browser tab pauses the channel and all automatic polling; returning revalidates its session before normal operation. All interval changes take effect immediately, without reloading or changing any server-global cadence.
The main `/api/events?interval=N` connection (allowlisted 1/2/3/5/10, default 3 seconds) carries compact live status, cached CPU/RSS/aggregate and per-interface network chart samples from the server's existing background OS collector, plus the existing bounded log snapshot. The server checks the session on every event and retains its heartbeat, viewer cap and normal disconnect cleanup. On an SSE error the browser polls `/api/status?view=live` and `/api/logs` until snapshots resume, not concurrently during healthy streaming. The old full `/api/status` response is kept for API clients and initial diagnostics. Process/System detail values still update only after the separate authenticated `/api/system-info` read at `refresh.system_info_seconds`, while CPU/RSS and network **charts** follow the Overview live cadence (3 seconds by default). The `/api/system-info` endpoint reuses cached diagnostics and ACME metadata, never runs diagnostic subprocesses per request, and is fetched automatically only for Overview or the relevant Settings pages. A hidden browser tab pauses the channel and all automatic polling; returning revalidates its session before normal operation. All interval changes take effect immediately, without reloading or changing any server-global cadence.
## System information (0.30.1)
+48
View File
@@ -1,5 +1,53 @@
# Verification report - RustProxy 0.32.6
## 0.32.6 development follow-up — live diagnostic charts / mobile scroll
- The existing server diagnostics collector samples OS and process metrics in
the background, with a 2-second idle delay between collection passes
(sampling work can add time). Compact live status / SSE now
exposes its **cached** `live_system` (sample timestamp, host/process CPU,
process RSS, aggregate RX/TX rates and interface rates), without invoking a
new OS measurement or fetching `/api/system-info` on each live event.
- Live Overview charts use the administrator's `overview_seconds` (default **3**,
accepted 1/2/3/5/10, including existing persisted 2-second preferences).
Process/System *detail text* still updates only with the configured
`system_info_seconds` (default **60**). Network chart RX/TX now reflects
recently sampled interface rates rather than 60-second-old repeated values.
- Disabled the nested `overflow:auto` network-interface list on narrow
Overview containers. Stable SVG/text DOM remains in place, to reduce
unexpected WebKit touch/momentum scroll anchoring. This is a mitigation;
the exact source of the iOS 27 jump remains unverified on physical Safari.
- Protocol badge Origin uses the same control radius as the adjacent policy
pills for custom presets, and the same 0px baseline for the default preset;
observed green/unknown neutral colors are unchanged.
- `live_diagnostics_0326_browser.py`: real diagnostic renderer with 35 live
updates verifies fast chart data and slow Process details, no chart DOM
replacement or scroll movement, no extra sample from a slow-system-only
refresh, touch-sized network list and both custom/default badge radii.
`mobile_protocol_0326_browser.py` covers all custom radii at six widths.
- Prior source/UI tests: all pre-Cargo checks passed; compilation and Rust
integration tests not run (Cargo absent). The Chromium fixture does **not**
establish Safari/iOS 27 acceptance.
## 0.32.6 development follow-up — suspended rate charts
- Added `src/admin/web/app/rate-sampling.js` to the authenticated Rust
administration bundle. The shared policy records tab background/pagehide
epochs and ignores intervals longer than three configured refresh periods
(at least 10 seconds). It validates counter monotonicity and declines
counter-derived rates across the gap, preserving existing history.
- Updated Cache HIT/MISS/BYPASS, Transform metrics, Overview HTTP/protocol/
cache-outcome deltas, optional small/wide rate and share charts, HTTP
backend monitor, Stream traffic and Vhost/Stream detailed charts. Fresh
absolute CPU/RAM/system observations and independent system-info reads
retain their previous behavior. Visible missing rates show `-`, not `0/s`.
- Added `tools/checks/rate_pause_0326_ui_test.mjs`: real Cache/Transform
chart functions with a simulated DOM and clock. Tests normal idle 0/s,
long gaps, short hide/show, pagehide/BFCache, counter resets, sample
continuity and 30-second refresh support. This is not a live Safari test.
- **Pending:** Cargo checks/compilation, real server/SSE data, mobile Safari
tab suspension and BFCache tests.
## 0.32.6 development follow-up — badge grouping and chart cadence
- Moved each observed upstream HTTP protocol badge into its Vhost's existing
+2 -2
View File
@@ -309,8 +309,8 @@ async fn events(
headers: HeaderMap,
Query(query): Query<EventsQuery>,
) -> Result<Response, ApiError> {
let interval = query.interval.unwrap_or(2);
if ![1, 2, 5, 10].contains(&interval) {
let interval = query.interval.unwrap_or(3);
if ![1, 2, 3, 5, 10].contains(&interval) {
return Err(ApiError(StatusCode::UNPROCESSABLE_ENTITY, "invalid SSE interval".into()));
}
let secret = auth::cookie_secret(&headers)
+22 -3
View File
@@ -1237,6 +1237,23 @@
"properties": {},
"additionalProperties": true
},
"live_system": {
"type": "object",
"description": "Cached OS/process metrics for fast SSE/live charts only; full System details remain on /api/system-info",
"properties": {
"sampled_at_ms": { "type": "integer", "format": "int64" },
"cpu_percent": { "type": "number", "nullable": true },
"process_cpu_percent": { "type": "number", "nullable": true },
"process_rss_bytes": { "type": "integer", "format": "int64", "nullable": true },
"network_rx_bytes_per_second": { "type": "number", "nullable": true },
"network_tx_bytes_per_second": { "type": "number", "nullable": true },
"network_interfaces": {
"type": "array",
"items": { "type": "object", "additionalProperties": true }
}
},
"additionalProperties": false
},
"backend_pools": {
"type": "array",
"items": {
@@ -3826,10 +3843,11 @@
"enum": [
1,
2,
3,
5,
10
],
"default": 2
"default": 3
},
"traffic_seconds": {
"type": "integer",
@@ -9037,7 +9055,7 @@
}
}
},
"description": "SSE snapshot events contain compact volatile status and bounded logs. Session validity is checked before each event. Native reconnect and keepalive apply. Each viewer selects 1/2/5/10 second pacing; invalid values return 422.",
"description": "SSE snapshot events contain compact volatile status and bounded logs. Session validity is checked before each event. Native reconnect and keepalive apply. Each viewer selects 1/2/3/5/10 second pacing; invalid values return 422.",
"parameters": [
{
"name": "interval",
@@ -9049,10 +9067,11 @@
"enum": [
1,
2,
3,
5,
10
],
"default": 2
"default": 3
}
}
]
+2 -2
View File
@@ -13,7 +13,7 @@ impl Default for RefreshPreferences {
fn default() -> Self {
Self {
live_enabled: true,
overview_seconds: 2,
overview_seconds: 3,
traffic_seconds: 5,
cache_seconds: 5,
system_info_seconds: 60,
@@ -22,7 +22,7 @@ impl Default for RefreshPreferences {
}
fn validate_refresh_preferences(refresh: &RefreshPreferences) -> Result<(), ApiError> {
if ![1, 2, 5, 10].contains(&refresh.overview_seconds)
if ![1, 2, 3, 5, 10].contains(&refresh.overview_seconds)
|| ![0, 3, 5, 10, 30].contains(&refresh.traffic_seconds)
|| ![0, 3, 5, 10, 30].contains(&refresh.cache_seconds)
|| ![0, 15, 30, 60, 300].contains(&refresh.system_info_seconds)
+1
View File
@@ -56,6 +56,7 @@ fn admin_app_javascript() -> &'static str {
include_str!("web/app/core.js"),
include_str!("web/app/session-resume.js"),
include_str!("web/app/refresh.js"),
include_str!("web/app/rate-sampling.js"),
"const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze(",
include_str!("web/dashboard-widgets.json"),
");",
+17
View File
@@ -1,3 +1,19 @@
// The OS collector already samples in the background. Expose just the
// chart inputs in compact SSE/fallback snapshots; the full, slower System
// information document remains behind /api/system-info.
fn live_chart_system_payload(state: &App) -> Value {
let view = state.diagnostics.view();
json!({
"sampled_at_ms": view.sampled_at_ms,
"cpu_percent": view.cpu_percent,
"process_cpu_percent": view.process_cpu_percent,
"process_rss_bytes": view.process_rss_bytes,
"network_rx_bytes_per_second": view.network_rx_bytes_per_second,
"network_tx_bytes_per_second": view.network_tx_bytes_per_second,
"network_interfaces": view.network_interfaces
})
}
fn live_status_payload(state: &App) -> Value {
let snapshot = state.config.load_full();
let stream_metrics = state.active_streams.iter().filter_map(|stream| {
@@ -13,6 +29,7 @@ fn live_status_payload(state: &App) -> Value {
"restart_required": snapshot.listeners != state.active_listeners
},
"metrics": state.metrics.view(),
"live_system": live_chart_system_payload(state),
"cache": state.cache.view(),
"transform_metrics": state.transform_metrics.total(),
"logs_dropped": state.logs.dropped.load(Ordering::Relaxed),
+13 -1
View File
@@ -1663,7 +1663,7 @@ mod tests {
assert_eq!(
initial["refresh"],
json!({
"live_enabled":true, "overview_seconds":2, "traffic_seconds":5,
"live_enabled":true, "overview_seconds":3, "traffic_seconds":5,
"cache_seconds":5, "system_info_seconds":60
})
);
@@ -2914,6 +2914,13 @@ mod tests {
assert_eq!(live.status(), StatusCode::OK);
let live = json_body(live).await;
assert!(live.get("metrics").is_some());
// Compact snapshots contain only already-collected data required for
// fast Overview charts, not the expensive System details document.
assert!(live["live_system"].get("sampled_at_ms").is_some());
assert!(live["live_system"].get("process_cpu_percent").is_some());
assert!(live["live_system"].get("process_rss_bytes").is_some());
assert!(live["live_system"].get("network_interfaces").is_some());
assert!(live["live_system"].get("open_fds").is_none());
assert!(live.get("backend_pools").is_some());
assert!(live["listeners"].get("active_endpoints").is_some());
assert!(live.get("system").is_none());
@@ -2966,6 +2973,7 @@ mod tests {
assert_eq!(partial.traffic_seconds, 0);
assert_eq!(partial.system_info_seconds, 60);
assert!(validate_admin_preferences(&loaded).is_ok());
assert_eq!(loaded.refresh.overview_seconds, 3);
}
#[test]
@@ -2975,6 +2983,10 @@ mod tests {
value.overview_seconds = seconds;
assert!(validate_refresh_preferences(&value).is_err());
}
value.overview_seconds = 3;
assert!(validate_refresh_preferences(&value).is_ok());
value.overview_seconds = 2; // previous installations stay valid
assert!(validate_refresh_preferences(&value).is_ok());
value.overview_seconds = 1;
for seconds in [1, 2, 4, 31] {
value.traffic_seconds = seconds;
+17 -14
View File
@@ -136,12 +136,14 @@ window.rustProxyBackendDashboard = (() => {
let inPerSecond = samples.inbound.at(-1) ?? 0;
let outPerSecond = samples.outbound.at(-1) ?? 0;
const additional = { http5xx: 0, timeouts: 0, network: 0, retries: 0, queue: 0 };
let fraction = null;
let fraction = null, pausedRate = false;
if (recordSample) {
const now = performance.now();
const next = memberCounters(members);
const elapsed = previous ? (now - previous.when) / 1000 : 0;
if (elapsed > 0) {
const valid = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds"));
pausedRate = previous !== null && !valid;
if (valid) {
const elapsed = (now - previous.at) / 1000;
const diff = deltas(next, previous.counters);
requestsPerSecond = diff.requests / elapsed;
errorsPerSecond = diff.errors / elapsed;
@@ -150,23 +152,24 @@ window.rustProxyBackendDashboard = (() => {
fraction = diff.requests ? 100 * diff.errors / diff.requests : null;
for (const name of Object.keys(additional)) additional[name] = diff[name] / elapsed;
} else { requestsPerSecond = 0; errorsPerSecond = 0; inPerSecond = 0; outPerSecond = 0; }
previous = { when: now, counters: next };
previous = { ...rustProxyRateSampling.stamp(now), counters: next };
if (visible("backend_activity")) push("active", active);
if (visible("backend_requests")) push("requests", requestsPerSecond);
if (!pausedRate && visible("backend_requests")) push("requests", requestsPerSecond);
if (visible("backend_capacity") && capacityPercent !== null) push("capacity", capacityPercent);
if (visible("backend_errors")) push("errors", errorsPerSecond);
if (!pausedRate && visible("backend_errors")) push("errors", errorsPerSecond);
if (visible("backend_latency") && latency !== null) push("latency", latency);
if (visible("backend_traffic")) { push("inbound", inPerSecond); push("outbound", outPerSecond); }
for (const name of Object.keys(additional)) if (visible(`backend_${name === "http5xx" ? "http5xx" : name}`)) push(name, additional[name]);
if (!pausedRate && visible("backend_traffic")) { push("inbound", inPerSecond); push("outbound", outPerSecond); }
if (!pausedRate) for (const name of Object.keys(additional)) if (visible(`backend_${name === "http5xx" ? "http5xx" : name}`)) push(name, additional[name]);
}
if (!recordSample) for (const name of Object.keys(additional)) additional[name] = samples[name].at(-1) ?? 0;
const displayRate = value => pausedRate ? "-" : shortRate(value);
if (visible("backend_activity")) {
set("backend-dashboard-activity", fmt(active));
set("backend-dashboard-activity-detail", t("backendWidgetInFlight"));
paintSeries("active", "backend-dashboard-active-line", Math.max(1, ...samples.active));
}
if (visible("backend_requests")) {
set("backend-dashboard-requests", shortRate(requestsPerSecond));
set("backend-dashboard-requests", displayRate(requestsPerSecond));
set("backend-dashboard-requests-detail", `${t("backendWidgetRequestsTotal")}: ${fmt(totalRequests)}`);
paintSeries("requests", "backend-dashboard-request-line", Math.max(1, ...samples.requests));
}
@@ -178,7 +181,7 @@ window.rustProxyBackendDashboard = (() => {
}
if (visible("backend_errors")) {
set("backend-dashboard-errors", fmt(totalErrors));
set("backend-dashboard-errors-detail", `${t("backendWidgetErrorsRate")}: ${shortRate(errorsPerSecond)}${fraction === null ? "" : ` · ${fraction.toFixed(1)}% ${t("backendWidgetErrorPercent")}`}`);
set("backend-dashboard-errors-detail", `${t("backendWidgetErrorsRate")}: ${displayRate(errorsPerSecond)}${fraction === null ? "" : ` · ${fraction.toFixed(1)}% ${t("backendWidgetErrorPercent")}`}`);
paintSeries("errors", "backend-dashboard-error-line", Math.max(1, ...samples.errors));
}
if (visible("backend_latency")) {
@@ -187,9 +190,9 @@ window.rustProxyBackendDashboard = (() => {
paintSeries("latency", "backend-dashboard-latency-line", Math.max(1, ...samples.latency));
}
if (visible("backend_traffic")) {
set("backend-dashboard-traffic", `${bytes(Math.round(inPerSecond + outPerSecond))}/s`);
set("backend-dashboard-in-rate", `${bytes(Math.round(inPerSecond))}/s`);
set("backend-dashboard-out-rate", `${bytes(Math.round(outPerSecond))}/s`);
set("backend-dashboard-traffic", pausedRate ? "-" : `${bytes(Math.round(inPerSecond + outPerSecond))}/s`);
set("backend-dashboard-in-rate", pausedRate ? "-" : `${bytes(Math.round(inPerSecond))}/s`);
set("backend-dashboard-out-rate", pausedRate ? "-" : `${bytes(Math.round(outPerSecond))}/s`);
const peak = Math.max(1, ...samples.inbound, ...samples.outbound);
paintSeries("inbound", "backend-dashboard-in-line", peak);
paintSeries("outbound", "backend-dashboard-out-line", peak);
@@ -207,7 +210,7 @@ window.rustProxyBackendDashboard = (() => {
name === "network" ? ["connect_errors_total", "dns_errors_total", "tls_errors_total", "io_errors_total"].reduce((v, key) => v + counter(member[key]), 0) :
counter(member.queue_timeouts_total) + counter(member.capacity_rejections_total)), 0);
set(field, fmt(total));
let detail = `${t("backendWidgetRateDetail")}: ${shortRate(additional[name])}`;
let detail = `${t("backendWidgetRateDetail")}: ${displayRate(additional[name])}`;
if (name === "retries") detail += ` · ${t("backendWidgetRetriesSuccess")}: ${fmt(members.reduce((n, { member }) => n + counter(member.retry_successes_total), 0))}`;
if (name === "queue") detail += ` · ${t("backendWidgetQueueRejected")}: ${fmt(members.reduce((n, { member }) => n + counter(member.capacity_rejections_total), 0))}`;
if (name === "network") detail += ` · ${t("backendWidgetNetworkBreakdown")}`;
+13 -15
View File
@@ -230,18 +230,17 @@ function renderStatus(s, systemSample = false) {
const now = performance.now();
const transformed = s.transform_metrics || {};
const counters = { total: Number(m.requests) || 0, h1: Number(m.http1_requests) || 0, h2: Number(m.http2_requests) || 0, h3: Number(m.http3_requests) || 0, transform: (Number(transformed.applied) || 0) + (Number(transformed.cache_hits) || 0) };
const rates = { total: 0, h1: 0, h2: 0, h3: 0, transform: 0 };
if (previous) {
const elapsed = Math.max(.001, (now - previous.time) / 1000);
for (const key of ["total", "h1", "h2", "h3", "transform"]) if (counters[key] >= previous[key]) rates[key] = (counters[key] - previous[key]) / elapsed;
const keys = ["total", "h1", "h2", "h3", "transform"]; const rateSample = rustProxyRateSampling.difference(previous, keys.map(key => counters[key]), now, rustProxyRateSampling.cadence("overview_seconds"));
const pausedRate = previous !== null && rateSample.deltas === null; previous = rateSample.next;
const rates = Object.fromEntries(keys.map((key, i) => [key, rateSample.deltas ? rateSample.deltas[i] / rateSample.seconds : 0]));
if (!pausedRate) {
samples.push(rates.total); samples = samples.slice(-30);
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-30); }
}
previous = { time: now, ...counters };
samples.push(rates.total); samples = samples.slice(-30);
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-30); }
websocketProtocolSamples.h1.push(Number(m.websocket_h1_active) || 0); websocketProtocolSamples.h1 = websocketProtocolSamples.h1.slice(-30);
websocketProtocolSamples.h2.push(Number(m.websocket_h2_active) || 0); websocketProtocolSamples.h2 = websocketProtocolSamples.h2.slice(-30);
websocketProtocolSamples.h3.push(Number(m.websocket_h3_active) || 0); websocketProtocolSamples.h3 = websocketProtocolSamples.h3.slice(-30);
transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-30);
if (!pausedRate) { transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-30); }
const peak = Math.max(1, ...samples, ...transformSamples);
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
$("sparkline").setAttribute("points", points(samples));
@@ -249,12 +248,11 @@ function renderStatus(s, systemSample = false) {
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
$("sparkline-transform").setAttribute("points", points(transformSamples));
$("request-rate").textContent = `${rates.total.toFixed(1)} req/s`;
$("rate-total").textContent = `${rates.total.toFixed(1)}/s`; $("rate-h1").textContent = `${rates.h1.toFixed(1)}/s`; $("rate-h2").textContent = `${rates.h2.toFixed(1)}/s`; $("rate-h3").textContent = `${rates.h3.toFixed(1)}/s`; $("rate-transform").textContent = `${rates.transform.toFixed(1)}/s`;
$("metric-http-proto-total").textContent = `${rates.total.toFixed(1)}/s`;
$("metric-http-proto-h1").textContent = `${rates.h1.toFixed(1)}/s`;
$("metric-http-proto-h2").textContent = `${rates.h2.toFixed(1)}/s`;
$("metric-http-proto-h3").textContent = `${rates.h3.toFixed(1)}/s`;
const rateText = key => pausedRate ? "-" : `${rates[key].toFixed(1)}/s`;
$("request-rate").textContent = pausedRate ? "- req/s" : `${rates.total.toFixed(1)} req/s`;
$("rate-total").textContent = rateText("total"); $("rate-h1").textContent = rateText("h1"); $("rate-h2").textContent = rateText("h2"); $("rate-h3").textContent = rateText("h3"); $("rate-transform").textContent = rateText("transform");
$("metric-http-proto-total").textContent = rateText("total"); $("metric-http-proto-h1").textContent = rateText("h1");
$("metric-http-proto-h2").textContent = rateText("h2"); $("metric-http-proto-h3").textContent = rateText("h3");
$("http-protocol-h1").setAttribute("points", sparklinePoints(protocolSamples.h1, 300, 60, 5));
$("http-protocol-h2").setAttribute("points", sparklinePoints(protocolSamples.h2, 300, 60, 5));
$("http-protocol-h3").setAttribute("points", sparklinePoints(protocolSamples.h3, 300, 60, 5));
@@ -302,7 +300,7 @@ function stopLive() {
}
function startLive() {
if (!authenticated || document.hidden || typeof EventSource === "undefined") return;
const seconds = window.rustProxyRefresh?.current().overview_seconds || 2;
const seconds = window.rustProxyRefresh?.current().overview_seconds || 3;
if (liveEvents && liveInterval === seconds) return;
stopLive();
liveInterval = seconds;
+6 -7
View File
@@ -16,16 +16,15 @@ window.rustProxyExtraOverview = (() => {
const node = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
function elapsedDelta(id, counters, now) {
const old = lastCounter.get(id);
const next = { values: counters, at: now };
lastCounter.set(id, next);
if (!old || now <= old.at || counters.some((v, i) => v < old.values[i])) return null;
return { deltas: counters.map((v, i) => v - old.values[i]), seconds: (now - old.at) / 1000 };
const result = rustProxyRateSampling.difference(old, counters, now, rustProxyRateSampling.cadence("overview_seconds"));
lastCounter.set(id, result.next);
return result.deltas ? result : null;
}
function read(id, status, now, record) {
const m = status.metrics || {}, d = status.system || {}, c = status.cache || {};
const m = status.metrics || {}, d = status.system || {}, live = status.live_system || d, c = status.cache || {};
const direct = {
extra_process_cpu: d.process_cpu_percent,
extra_process_rss: d.process_rss_bytes,
extra_process_cpu: live.process_cpu_percent,
extra_process_rss: live.process_rss_bytes,
extra_process_fds: d.open_fds,
extra_open_connections: m.active_connections,
extra_websocket_active: m.websocket_active,
+5 -8
View File
@@ -7,11 +7,11 @@ window.rustProxyWideOverview = (() => {
},
wide_cpu: {
unit: "percent", names: ["system", "process"], values: s =>
[s.system?.cpu_percent, s.system?.process_cpu_percent]
[(s.live_system || s.system)?.cpu_percent, (s.live_system || s.system)?.process_cpu_percent]
},
wide_memory: {
unit: "bytes", names: ["process", "cache"], values: s =>
[s.system?.process_rss_bytes, s.cache?.resident_bytes]
[(s.live_system || s.system)?.process_rss_bytes, s.cache?.resident_bytes]
},
wide_cache_activity: {
unit: "rate", names: ["hits", "misses"], counters: s =>
@@ -51,12 +51,9 @@ window.rustProxyWideOverview = (() => {
const counters = values.map(finite);
if (counters.some(n => n === null)) { baselines.delete(id); return counters.map(() => null); }
if (!record) return last.get(id) || counters.map(() => null);
const previous = baselines.get(id);
baselines.set(id, { counters, now });
if (!previous || now <= previous.now || counters.some((n, i) => n < previous.counters[i]))
return counters.map(() => null);
const elapsed = (now - previous.now) / 1000;
return counters.map((n, i) => (n - previous.counters[i]) / elapsed);
const result = rustProxyRateSampling.difference(baselines.get(id), counters, now, rustProxyRateSampling.cadence("overview_seconds"));
baselines.set(id, result.next);
return result.deltas ? result.deltas.map(delta => delta / result.seconds) : counters.map(() => null);
}
function path(values, peak) {
const count = values.length;
+32
View File
@@ -0,0 +1,32 @@
// One continuity policy for all browser-side counter-derived rates.
// Background tabs cannot supply a valid instantaneous rate for an interval
// spanning the pause. Keep the plotted history and rebase on the next sample.
const rustProxyRateSampling = (() => {
let generation = 0;
const pause = () => { generation += 1; };
document.addEventListener?.("visibilitychange", () => { if (document.hidden) pause(); });
window.addEventListener?.("pagehide", pause);
function cadence(key) {
const value = window.rustProxyRefresh?.current?.()?.[key];
const fallback = key === "cache_seconds" ? 5 : 3;
return Number.isFinite(value) && value > 0 ? value : fallback;
}
function stamp(at) { return { at, generation }; }
function continuous(previous, at, intervalSeconds = 3) {
const seconds = Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds : 3;
return Boolean(previous && !document.hidden && previous.generation === generation &&
Number.isFinite(at) && Number.isFinite(previous.at) && at > previous.at &&
at - previous.at <= Math.max(10000, seconds * 3000));
}
function difference(previous, values, at, intervalSeconds = 3) {
const next = { ...stamp(at), values: [...values] };
if (!continuous(previous, at, intervalSeconds) ||
values.some((value, i) => !Number.isFinite(value) || value < 0 ||
!Number.isFinite(previous.values[i]) || value < previous.values[i])) {
return { next, deltas: null, seconds: null };
}
return { next, deltas: values.map((value, i) => value - previous.values[i]),
seconds: (at - previous.at) / 1000 };
}
return { cadence, stamp, continuous, difference };
})();
+2 -2
View File
@@ -1,9 +1,9 @@
const REFRESH_DEFAULTS = Object.freeze({
live_enabled: true, overview_seconds: 2, traffic_seconds: 5,
live_enabled: true, overview_seconds: 3, traffic_seconds: 5,
cache_seconds: 5, system_info_seconds: 60
});
const REFRESH_CHOICES = Object.freeze({
overview_seconds: [1, 2, 5, 10], traffic_seconds: [0, 3, 5, 10, 30],
overview_seconds: [1, 2, 3, 5, 10], traffic_seconds: [0, 3, 5, 10, 30],
cache_seconds: [0, 3, 5, 10, 30], system_info_seconds: [0, 15, 30, 60, 300]
});
function normalizeRefreshPreferences(input = {}) {
+68 -68
View File
@@ -312,30 +312,30 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
}
}
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
const d = status.system || {}, c = status.cache || {}, m = status.metrics || {};
const d = status.system || {}, live = status.live_system || d, c = status.cache || {}, m = status.metrics || {};
$("metric-h3").textContent = fmt(m.http3_requests);
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
$("process-summary").hidden = d.process_threads == null;
if (d.process_threads != null) $("process-summary").textContent = `${fmt(d.process_threads)} ${t("threads")}`;
const systemCpuSupported = d.cpu_percent != null;
setOptionalMetric("system-cpu", systemCpuSupported, systemCpuSupported ? `${fmt(d.cpu_percent, 1)}%` : "");
$("system-chart").hidden = !systemCpuSupported;
const sourceSupported = !!d.source && d.source !== "unsupported";
$("system-source").hidden = !sourceSupported;
if (sourceSupported) $("system-source").textContent = t("realSample");
if (systemSample && d.cpu_percent != null) { systemSamples.push(d.cpu_percent); systemSamples = systemSamples.slice(-30); }
// Slow details vs cached live chart telemetry.
if (systemSample) {
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
$("process-summary").hidden = d.process_threads == null;
if (d.process_threads != null) $("process-summary").textContent = `${fmt(d.process_threads)} ${t("threads")}`;
setOptionalMetric("system-cpu", d.cpu_percent != null, `${fmt(d.cpu_percent, 1)}%`);
const sourceSupported = !!d.source && d.source !== "unsupported";
$("system-source").hidden = !sourceSupported; if (sourceSupported) $("system-source").textContent = t("realSample");
const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value));
setOptionalMetric("system-memory", systemMemory != null, systemMemory);
$("memory-meter").hidden = systemMemory == null;
if (d.memory_used_bytes != null && d.memory_total_bytes > 0) $("memory-meter").value = d.memory_used_bytes / d.memory_total_bytes * 100; else $("memory-meter").removeAttribute("value");
const systemSwap = knownPair(d.system_swap_used_bytes, d.system_swap_total_bytes, value => fmtBytes(value));
setOptionalMetric("system-swap", systemSwap != null, systemSwap);
const loadSupported = Array.isArray(d.load_average) && d.load_average.length > 0;
setOptionalMetric("system-load", loadSupported, loadSupported ? d.load_average.map(n => fmt(n, 2)).join(" / ") : "");
setOptionalMetric("system-uptime", d.system_uptime_seconds != null, d.system_uptime_seconds == null ? "" : uptime(d.system_uptime_seconds));
}
$("system-chart").hidden = live.cpu_percent == null; if (appendSample && live.cpu_percent != null) { systemSamples.push(Number(live.cpu_percent)); systemSamples = systemSamples.slice(-30); }
$("system-sparkline").setAttribute("points", systemSamples.map((n, i) => `${i * 300 / Math.max(1, systemSamples.length - 1)},${95 - Math.max(0, Math.min(100, n)) * .9}`).join(" "));
const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value));
setOptionalMetric("system-memory", systemMemory != null, systemMemory);
$("memory-meter").hidden = systemMemory == null;
if (d.memory_used_bytes != null && d.memory_total_bytes > 0) $("memory-meter").value = d.memory_used_bytes / d.memory_total_bytes * 100; else $("memory-meter").removeAttribute("value");
const systemSwap = knownPair(d.system_swap_used_bytes, d.system_swap_total_bytes, value => fmtBytes(value));
setOptionalMetric("system-swap", systemSwap != null, systemSwap);
const loadSupported = Array.isArray(d.load_average) && d.load_average.length > 0;
setOptionalMetric("system-load", loadSupported, loadSupported ? d.load_average.map(n => fmt(n, 2)).join(" / ") : "");
setOptionalMetric("system-uptime", d.system_uptime_seconds != null, d.system_uptime_seconds == null ? "" : uptime(d.system_uptime_seconds));
$("cache-state").textContent = t(envelope?.config.cache?.enabled ? "enabled" : "notEnabled");
$("cache-counters").textContent = [c.hits, c.misses, c.bypasses].map(n => fmt(n)).join(" / ");
$("cache-objects").textContent = `${fmt(c.entries)} / ${fmtBytes(c.resident_bytes)}`;
@@ -357,15 +357,15 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
if (c.cold_bytes != null) { cacheColdStorageSamples.push(Number(c.cold_bytes)); cacheColdStorageSamples = cacheColdStorageSamples.slice(-60); }
const currentCounters = [c.hits, c.misses, c.bypasses].map(Number);
if (currentCounters.every(Number.isFinite)) {
const delta = lastCacheCounters ? currentCounters.map((value, index) => value >= lastCacheCounters[index] ? value - lastCacheCounters[index] : 0) : [0, 0, 0];
cacheHitSamples.push(delta[0]); cacheMissSamples.push(delta[1]); cacheBypassSamples.push(delta[2]);
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
lastCacheCounters = currentCounters;
const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
if (delta.deltas || !lastCacheCounters) { const values = delta.deltas || [0, 0, 0];
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]); cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
} lastCacheCounters = delta.next;
}
}
if (systemSample) {
if (d.process_cpu_percent != null) { processCpuSamples.push(Number(d.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-60); }
if (d.process_rss_bytes != null) { processRssSamples.push(Number(d.process_rss_bytes)); processRssSamples = processRssSamples.slice(-60); }
if (appendSample) {
if (live.process_cpu_percent != null) { processCpuSamples.push(Number(live.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-60); }
if (live.process_rss_bytes != null) { processRssSamples.push(Number(live.process_rss_bytes)); processRssSamples = processRssSamples.slice(-60); }
}
const cacheOutcomePeak = Math.max(1, ...cacheHitSamples, ...cacheMissSamples, ...cacheBypassSamples);
$("cache-hit-sparkline").setAttribute("points", sparklinePoints(cacheHitSamples, 300, 60, 5, cacheOutcomePeak));
@@ -389,47 +389,49 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
const coldLimit = Number(c.cold_limit_bytes), coldPeak = Number.isFinite(coldLimit) && coldLimit > 0 ? Math.max(coldLimit, ...cacheColdStorageSamples) : null;
$("cache-cold-sparkline").setAttribute("points", sparklinePoints(cacheColdStorageSamples, 300, 60, 5, coldPeak));
setChartText($("cache-cold-chart-value"), coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled"));
const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform);
setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
const fds = knownPair(d.open_fds, d.max_fds, value => fmt(value), t("unlimited"));
setOptionalMetric("process-fds", fds != null, fds);
setOptionalMetric("process-threads", d.process_threads != null, fmt(d.process_threads));
const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
setOptionalMetric("process-virtual-memory", virtualMemory != null, virtualMemory);
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000));
setOptionalMetric("process-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds));
setOptionalMetric("process-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes));
const memory = status.memory_accounting || {};
const hotMemory = knownPair(memory.cache_hot_bytes, memory.cache_hot_limit_bytes, value => fmtBytes(value));
const transientMemory = knownPair(memory.cache_transient_bytes, memory.cache_transient_limit_bytes, value => fmtBytes(value));
const transformMemory = knownPair(memory.transform_reserved_bytes, memory.transform_budget_bytes, value => fmtBytes(value));
const transformInflight = knownPair(memory.transform_inflight, memory.transform_max_inflight, value => fmt(value));
setOptionalMetric("memory-cache-hot", hotMemory != null, hotMemory);
setOptionalMetric("memory-cache-transient", transientMemory != null, transientMemory);
setOptionalMetric("memory-transform", transformMemory != null, transformMemory);
setOptionalMetric("memory-transform-inflight", transformInflight != null, transformInflight);
setOptionalMetric("memory-non-cache-rss", memory.non_cache_rss_bytes != null, fmtBytes(memory.non_cache_rss_bytes));
setOptionalMetric("process-rss-anon", d.process_rss_anon_bytes != null, fmtBytes(d.process_rss_anon_bytes));
const rssFile = knownPair(d.process_rss_file_bytes, d.process_rss_shmem_bytes, value => fmtBytes(value));
setOptionalMetric("process-rss-file", rssFile != null, rssFile);
const dataSwap = knownPair(d.process_data_bytes, d.process_swap_bytes, value => fmtBytes(value));
setOptionalMetric("process-data-swap", dataSwap != null, dataSwap);
const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory);
if (systemSample) {
const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform);
setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
const fds = knownPair(d.open_fds, d.max_fds, value => fmt(value), t("unlimited"));
setOptionalMetric("process-fds", fds != null, fds);
setOptionalMetric("process-threads", d.process_threads != null, fmt(d.process_threads));
const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
setOptionalMetric("process-virtual-memory", virtualMemory != null, virtualMemory);
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000));
setOptionalMetric("process-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds));
setOptionalMetric("process-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes));
const memory = status.memory_accounting || {};
const hotMemory = knownPair(memory.cache_hot_bytes, memory.cache_hot_limit_bytes, value => fmtBytes(value));
const transientMemory = knownPair(memory.cache_transient_bytes, memory.cache_transient_limit_bytes, value => fmtBytes(value));
const transformMemory = knownPair(memory.transform_reserved_bytes, memory.transform_budget_bytes, value => fmtBytes(value));
const transformInflight = knownPair(memory.transform_inflight, memory.transform_max_inflight, value => fmt(value));
setOptionalMetric("memory-cache-hot", hotMemory != null, hotMemory);
setOptionalMetric("memory-cache-transient", transientMemory != null, transientMemory);
setOptionalMetric("memory-transform", transformMemory != null, transformMemory);
setOptionalMetric("memory-transform-inflight", transformInflight != null, transformInflight);
setOptionalMetric("memory-non-cache-rss", memory.non_cache_rss_bytes != null, fmtBytes(memory.non_cache_rss_bytes));
setOptionalMetric("process-rss-anon", d.process_rss_anon_bytes != null, fmtBytes(d.process_rss_anon_bytes));
const rssFile = knownPair(d.process_rss_file_bytes, d.process_rss_shmem_bytes, value => fmtBytes(value));
setOptionalMetric("process-rss-file", rssFile != null, rssFile);
const dataSwap = knownPair(d.process_data_bytes, d.process_swap_bytes, value => fmtBytes(value));
setOptionalMetric("process-data-swap", dataSwap != null, dataSwap);
const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory);
}
$("process-cpu-sparkline").setAttribute("points", sparklinePoints(processCpuSamples));
setChartText($("process-cpu-chart-value"), d.process_cpu_percent == null ? "" : `${fmt(d.process_cpu_percent, 1)}%`);
setChartText($("process-cpu-chart-value"), live.process_cpu_percent == null ? "" : `${fmt(live.process_cpu_percent, 1)}%`);
$("process-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples));
setChartText($("process-rss-chart-value"), d.process_rss_bytes == null ? "" : fmtBytes(d.process_rss_bytes));
$("process-cpu-sparkline").closest(".mini-live-chart").hidden = d.process_cpu_percent == null;
$("process-rss-sparkline").closest(".mini-live-chart").hidden = d.process_rss_bytes == null;
setChartText($("process-rss-chart-value"), live.process_rss_bytes == null ? "" : fmtBytes(live.process_rss_bytes));
$("process-cpu-sparkline").closest(".mini-live-chart").hidden = live.process_cpu_percent == null;
$("process-rss-sparkline").closest(".mini-live-chart").hidden = live.process_rss_bytes == null;
updateCacheMemorySizing();
const networkRateSupported = d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null;
setOptionalMetric("system-network", networkRateSupported, networkRateSupported ? `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}` : "");
const networkRateSupported = live.network_rx_bytes_per_second != null && live.network_tx_bytes_per_second != null;
if (systemSample) setOptionalMetric("system-network", d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null, `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}`);
setOptionalMetric("mean-latency", m.mean_duration_ms != null, m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`);
if (appendSample) {
if (d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null) {
networkRxSamples.push(Number(d.network_rx_bytes_per_second)); networkTxSamples.push(Number(d.network_tx_bytes_per_second));
if (networkRateSupported) {
networkRxSamples.push(Number(live.network_rx_bytes_per_second)); networkTxSamples.push(Number(live.network_tx_bytes_per_second));
networkRxSamples = networkRxSamples.slice(-60); networkTxSamples = networkTxSamples.slice(-60);
}
if (m.mean_duration_ms != null) { responseLatencySamples.push(Number(m.mean_duration_ms)); responseLatencySamples = responseLatencySamples.slice(-60); }
@@ -437,12 +439,10 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
const networkPeak = Math.max(1, ...networkRxSamples, ...networkTxSamples);
$("network-rx-sparkline").setAttribute("points", sparklinePoints(networkRxSamples, 300, 60, 5, networkPeak));
$("network-tx-sparkline").setAttribute("points", sparklinePoints(networkTxSamples, 300, 60, 5, networkPeak));
setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}` : "");
setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(live.network_rx_bytes_per_second)} / ${fmtBytes(live.network_tx_bytes_per_second)}` : "");
$("network-rx-sparkline").closest(".mini-live-chart").hidden = !networkRateSupported;
// Advance interface charts at the live Overview cadence, just like the
// aggregate network chart. Between slower system-info reads, repeat the
// latest observed rates without initiating another system data fetch.
renderNetworkInterfaceCharts(d.network_interfaces || [], appendSample);
// Cache-sampled OS chart rates are independent from slow System details.
renderNetworkInterfaceCharts(live.network_interfaces || [], appendSample);
$("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples));
setChartText($("latency-chart-value"), m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`);
$("latency-sparkline").closest(".mini-live-chart").hidden = m.mean_duration_ms == null;
+10 -10
View File
@@ -88,17 +88,17 @@ window.rustProxyStreamDashboard = (() => {
const rxTotal = views.reduce((sum, view) => sum + (Number(view.bytes_in) || 0), 0);
const txTotal = views.reduce((sum, view) => sum + (Number(view.bytes_out) || 0), 0);
const now = performance.now();
let rx = 0, tx = 0;
if (history.last) {
const seconds = Math.max(.001, (now - history.last.when) / 1000);
if (rxTotal >= history.last.rx) rx = (rxTotal - history.last.rx) / seconds;
if (txTotal >= history.last.tx) tx = (txTotal - history.last.tx) / seconds;
const sample = rustProxyRateSampling.difference(history.last, [rxTotal, txTotal], now, rustProxyRateSampling.cadence("overview_seconds"));
const pausedRate = history.last !== null && sample.deltas === null;
history.last = sample.next;
const rx = sample.deltas ? sample.deltas[0] / sample.seconds : 0;
const tx = sample.deltas ? sample.deltas[1] / sample.seconds : 0;
if (!pausedRate) {
history.rx.push(rx); history.tx.push(tx);
if (history.rx.length > 60) { history.rx.shift(); history.tx.shift(); }
}
history.last = { when: now, rx: rxTotal, tx: txTotal };
history.rx.push(rx); history.tx.push(tx);
if (history.rx.length > 60) { history.rx.shift(); history.tx.shift(); }
set("stream-dashboard-traffic", `${bytes(Math.round(rx))}/s`);
trafficRates(`${bytes(Math.round(rx))}/s`, `${bytes(Math.round(tx))}/s`);
set("stream-dashboard-traffic", pausedRate ? "-" : `${bytes(Math.round(rx))}/s`);
trafficRates(pausedRate ? "-" : `${bytes(Math.round(rx))}/s`, pausedRate ? "-" : `${bytes(Math.round(tx))}/s`);
$("stream-dashboard-rx")?.setAttribute("points", line(history.rx));
$("stream-dashboard-tx")?.setAttribute("points", line(history.tx));
}
+15 -12
View File
@@ -1,7 +1,7 @@
"use strict";
(() => {
let loading = false, offset = 0, generation = 0;
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null;
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
const liveChartSamples = { hits: [], misses: [], bypasses: [] };
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
const LIVE_CHART_STORAGE_KEY = "rustproxy.cache.liveChartSamples";
@@ -68,7 +68,7 @@
};
const reason = value => { const key = `cacheReason_${value}`, text = t(key); return text === key ? value : text; };
function resetLiveChart(selected = scope()) {
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null;
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null;
for (const values of Object.values(liveChartSamples)) values.length = 0;
for (const id of ["cache-live-hit-line", "cache-live-miss-line", "cache-live-bypass-line"]) $(id)?.setAttribute("points", "0,170 900,170");
for (const id of ["cache-live-hit-rate", "cache-live-miss-rate", "cache-live-bypass-rate"]) if ($(id)) $(id).textContent = "0.0/s";
@@ -100,18 +100,21 @@
hits: Math.max(0, Number(counters.hits) || 0),
misses: Math.max(0, Number(counters.misses) || 0),
bypasses: Math.max(0, Number(counters.bypasses) || 0)
}, rates = { hits: 0, misses: 0, bypasses: 0 };
if (liveChartPrevious && liveChartPreviousAt != null) {
const elapsed = Math.max(.001, (now - liveChartPreviousAt) / 1000);
for (const key of Object.keys(rates)) if (current[key] >= liveChartPrevious[key]) rates[key] = (current[key] - liveChartPrevious[key]) / elapsed;
};
const keys = Object.keys(current);
const previous = liveChartPrevious && liveChartPreviousAt != null
? { values: keys.map(key => liveChartPrevious[key]), at: liveChartPreviousAt, generation: liveChartGeneration } : null;
const sample = rustProxyRateSampling.difference(previous, keys.map(key => current[key]), now, rustProxyRateSampling.cadence("cache_seconds"));
liveChartPrevious = current; liveChartPreviousAt = now; liveChartGeneration = sample.next.generation;
if (sample.deltas) {
keys.forEach((key, i) => { liveChartSamples[key].push(sample.deltas[i] / sample.seconds); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); });
}
liveChartPrevious = current; liveChartPreviousAt = now;
for (const key of Object.keys(rates)) { liveChartSamples[key].push(rates[key]); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); }
redrawLiveChart();
$("cache-live-hit-rate").textContent = `${rates.hits.toFixed(1)}/s`;
$("cache-live-miss-rate").textContent = `${rates.misses.toFixed(1)}/s`;
$("cache-live-bypass-rate").textContent = `${rates.bypasses.toFixed(1)}/s`;
$("cache-live-chart-value").textContent = `Σ ${(rates.hits + rates.misses + rates.bypasses).toFixed(1)}/s`;
const rate = key => sample.deltas ? `${(sample.deltas[keys.indexOf(key)] / sample.seconds).toFixed(1)}/s` : "-";
$("cache-live-hit-rate").textContent = rate("hits");
$("cache-live-miss-rate").textContent = rate("misses");
$("cache-live-bypass-rate").textContent = rate("bypasses");
$("cache-live-chart-value").textContent = sample.deltas ? `Σ ${(sample.deltas.reduce((a, b) => a + b, 0) / sample.seconds).toFixed(1)}/s` : "-";
}
function configure() {
const selected = scope(); const items = [el("option", t("logAllVhosts"))]; items[0].value = "*";
+2 -1
View File
@@ -2520,7 +2520,8 @@
<select id="refresh-overview-seconds"
data-refresh-preference="overview_seconds">
<option value="1">1 s</option>
<option selected value="2">2 s</option>
<option value="2">2 s</option>
<option selected value="3">3 s</option>
<option value="5">5 s</option>
<option value="10">10 s</option>
</select></label>
+8 -5
View File
@@ -250,14 +250,17 @@
const set = (suffix, value) => document.querySelectorAll(`[data-${suffix.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}="${CSS.escape(id)}"]`).forEach(node => node.textContent = value);
set("streamState", state); set("streamActive", data ? fmt(data.active) : "-"); set("streamSessions", data ? fmt(data.sessions_total) : "-"); set("streamErrors", data ? fmt(data.errors) : "-"); set("streamRejected", data ? fmt(data.rejected) : "-"); set("streamTransferred", data ? bytes((Number(data.bytes_in) || 0) + (Number(data.bytes_out) || 0)) : "-");
if (!data || currentPanel !== "streams") continue;
const history = chartHistory.get(id) || { lastTime: now, lastIn: Number(data.bytes_in) || 0, lastOut: Number(data.bytes_out) || 0, points: [], bucketSize: 1, pendingRx: 0, pendingTx: 0, pendingCount: 0 };
const elapsed = Math.max(.001, (now - history.lastTime) / 1000), inBytes = Number(data.bytes_in) || 0, outBytes = Number(data.bytes_out) || 0;
const rx = inBytes >= history.lastIn ? (inBytes - history.lastIn) / elapsed : 0, tx = outBytes >= history.lastOut ? (outBytes - history.lastOut) / elapsed : 0;
history.lastTime = now; history.lastIn = inBytes; history.lastOut = outBytes; addPoint(history, rx, tx); chartHistory.set(id, history);
const history = chartHistory.get(id) || { last: null, points: [], bucketSize: 1, pendingRx: 0, pendingTx: 0, pendingCount: 0 };
const sample = rustProxyRateSampling.difference(history.last, [Number(data.bytes_in) || 0, Number(data.bytes_out) || 0], now, rustProxyRateSampling.cadence("overview_seconds"));
const pausedRate = history.last !== null && sample.deltas === null;
history.last = sample.next;
const rx = sample.deltas ? sample.deltas[0] / sample.seconds : 0, tx = sample.deltas ? sample.deltas[1] / sample.seconds : 0;
if (!pausedRate) addPoint(history, rx, tx);
chartHistory.set(id, history);
const points = history.pendingCount ? [...history.points, [history.pendingRx / history.pendingCount, history.pendingTx / history.pendingCount]] : history.points;
const peak = Math.max(1, ...points.flat()); const toPoints = index => points.map((pair, pointIndex) => `${pointIndex * 320 / Math.max(1, points.length - 1)},${50 - pair[index] / peak * 44}`).join(" ");
const svg = document.querySelector(`[data-stream-chart="${CSS.escape(id)}"]`); svg?.querySelector(".stream-chart-rx")?.setAttribute("points", toPoints(0)); svg?.querySelector(".stream-chart-tx")?.setAttribute("points", toPoints(1));
const label = document.querySelector(`[data-stream-chart-value="${CSS.escape(id)}"]`); if (label) label.textContent = `RX ${bytes(Math.round(rx))}/s · TX ${bytes(Math.round(tx))}/s`;
const label = document.querySelector(`[data-stream-chart-value="${CSS.escape(id)}"]`); if (label) label.textContent = pausedRate ? "RX - · TX -" : `RX ${bytes(Math.round(rx))}/s · TX ${bytes(Math.round(tx))}/s`;
}
if (editing) { const data = metrics[editing]; $("stream-live-summary").textContent = data ? `${t("streamActiveSessions")}: ${fmt(data.active)} · RX ${bytes(data.bytes_in)} · TX ${bytes(data.bytes_out)} · ${t("streamErrors")}: ${fmt(data.errors)}` : t("streamNoLiveMetrics"); }
}
+13 -3
View File
@@ -10713,13 +10713,13 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
.dashboard-widget-setting, .overview-layout-tile, .overview-layout-hidden-item,
.overview-inline-hidden-item, .overview-inline-hidden, .notice, .toast,
.chart-style-choice, .stream-dashboard-card, .pool-card, .vhost-card,
.vhost-backend-protocol-badge, .vh-backend-card, .entity, .settings-filter-row,
.vh-backend-card, .entity, .settings-filter-row,
.stream-backend-row,
.file-log-advanced-parser, .table-container, .metric-info-popover
) { border-radius: var(--rp-radius-surface) !important; }
html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(
button:not(.nav):not(.file-log-tab), input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
select, textarea, .pill, .overview-widget-type, .badge, .quiet, .outline, .primary,
select, textarea, .pill, .vhost-backend-protocol-badge, .overview-widget-type, .badge, .quiet, .outline, .primary,
.code, .command, .dashboard-widget-toggle, .settings-tabs button,
.overview-inline-hidden-toggle, .overview-inline-restore
) { border-radius: var(--rp-radius-control) !important; }
@@ -10743,7 +10743,6 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
border: 1px solid var(--pill-line);
background: var(--panel2);
color: var(--muted);
border-radius: 4px;
padding: 3px 7px;
font: 10px var(--mono);
overflow-wrap: anywhere;
@@ -10752,3 +10751,14 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
color: var(--green);
border-color: var(--pill-live-line);
}
/* Phone Overview has one vertical scroll surface. Nested overflow on the
interface chart stack can steal a WebKit touch/momentum gesture and appear
to jump back to Process / Network. Desktop retains its bounded list. */
@container overview-canvas (max-width: 700px) {
#panel-overview .network-interface-charts {
max-height: none;
overflow: visible;
overscroll-behavior: auto;
}
}
+16 -13
View File
@@ -1,7 +1,7 @@
"use strict";
(() => {
let loading = false, generation = 0, lastData = null;
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null;
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
const liveChartSamples = { applied: [], cache_hits: [], skipped: [], errors: [] };
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
const LIVE_CHART_STORAGE_KEY = "rustproxy.transform.liveChartSamples";
@@ -20,7 +20,7 @@
};
const emptyCounters = () => ({ examined: 0, applied: 0, skipped: 0, errors: 0, cache_hits: 0, original_bytes: 0, transformed_bytes: 0, saved_bytes: 0, duration_us: 0, by_kind: {}, reasons: {} });
function resetLiveChart(selected = scope()) {
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null;
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null;
for (const values of Object.values(liveChartSamples)) values.length = 0;
for (const id of ["transform-live-applied-line", "transform-live-cache-line", "transform-live-skipped-line", "transform-live-error-line"]) $(id)?.setAttribute("points", "0,170 900,170");
for (const id of ["transform-live-applied-rate", "transform-live-cache-rate", "transform-live-skipped-rate", "transform-live-error-rate"]) if ($(id)) $(id).textContent = "0.0/s";
@@ -54,19 +54,22 @@
cache_hits: Math.max(0, Number(counters.cache_hits) || 0),
skipped: Math.max(0, Number(counters.skipped) || 0),
errors: Math.max(0, Number(counters.errors) || 0)
}, rates = { applied: 0, cache_hits: 0, skipped: 0, errors: 0 };
if (liveChartPrevious && liveChartPreviousAt != null) {
const elapsed = Math.max(.001, (now - liveChartPreviousAt) / 1000);
for (const key of Object.keys(rates)) if (current[key] >= liveChartPrevious[key]) rates[key] = (current[key] - liveChartPrevious[key]) / elapsed;
};
const keys = Object.keys(current);
const previous = liveChartPrevious && liveChartPreviousAt != null
? { values: keys.map(key => liveChartPrevious[key]), at: liveChartPreviousAt, generation: liveChartGeneration } : null;
const sample = rustProxyRateSampling.difference(previous, keys.map(key => current[key]), now, 3);
liveChartPrevious = current; liveChartPreviousAt = now; liveChartGeneration = sample.next.generation;
if (sample.deltas) {
keys.forEach((key, i) => { liveChartSamples[key].push(sample.deltas[i] / sample.seconds); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); });
}
liveChartPrevious = current; liveChartPreviousAt = now;
for (const key of Object.keys(rates)) { liveChartSamples[key].push(rates[key]); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); }
redrawLiveChart();
$("transform-live-applied-rate").textContent = `${rates.applied.toFixed(1)}/s`;
$("transform-live-cache-rate").textContent = `${rates.cache_hits.toFixed(1)}/s`;
$("transform-live-skipped-rate").textContent = `${rates.skipped.toFixed(1)}/s`;
$("transform-live-error-rate").textContent = `${rates.errors.toFixed(1)}/s`;
$("transform-live-chart-value").textContent = `Σ ${(rates.applied + rates.cache_hits + rates.skipped + rates.errors).toFixed(1)}/s`;
const rate = key => sample.deltas ? `${(sample.deltas[keys.indexOf(key)] / sample.seconds).toFixed(1)}/s` : "-";
$("transform-live-applied-rate").textContent = rate("applied");
$("transform-live-cache-rate").textContent = rate("cache_hits");
$("transform-live-skipped-rate").textContent = rate("skipped");
$("transform-live-error-rate").textContent = rate("errors");
$("transform-live-chart-value").textContent = sample.deltas ? `Σ ${(sample.deltas.reduce((a, b) => a + b, 0) / sample.seconds).toFixed(1)}/s` : "-";
}
function countersFor(data) {
if (scope() === "*") return data?.metrics?.total || emptyCounters();
+16 -11
View File
@@ -264,10 +264,10 @@ function backendTrafficRate(poolId, member, snapshot, now, seen) {
const reported = Math.max(0, Number(member.traffic_bytes_per_second) || 0);
const previous = backendTrafficHistory.get(key);
if (previous?.snapshot === snapshot) return previous.rate;
const elapsed = previous ? Math.max(.001, (now - previous.at) / 1000) : 0;
const observed = previous && total >= previous.total ? (total - previous.total) / elapsed : 0;
const continuous = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds"));
const observed = continuous && total >= previous.total ? (total - previous.total) / ((now - previous.at) / 1000) : 0;
const rate = reported > 0 ? reported : observed;
backendTrafficHistory.set(key, { total, at: now, snapshot, rate });
backendTrafficHistory.set(key, { ...rustProxyRateSampling.stamp(now), total, snapshot, rate });
return rate;
}
function vhostTrafficRate(id, metrics, snapshot, now, seen) {
@@ -275,9 +275,9 @@ function vhostTrafficRate(id, metrics, snapshot, now, seen) {
const total = Math.max(0, Number(metrics?.bytes_in) || 0) + Math.max(0, Number(metrics?.bytes_out) || 0);
const previous = vhostTrafficHistory.get(id);
if (previous?.snapshot === snapshot) return previous.rate;
const elapsed = previous ? Math.max(.001, (now - previous.at) / 1000) : 0;
const rate = previous && total >= previous.total ? (total - previous.total) / elapsed : 0;
vhostTrafficHistory.set(id, { total, at: now, snapshot, rate });
const continuous = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds"));
const rate = continuous && total >= previous.total ? (total - previous.total) / ((now - previous.at) / 1000) : 0;
vhostTrafficHistory.set(id, { ...rustProxyRateSampling.stamp(now), total, snapshot, rate });
return rate;
}
function renderStatus(status) {
@@ -357,14 +357,19 @@ function renderStatus(status) {
fill("vhostLatency", d => d.latency > 0 ? `${fmt(d.latency, 1)} ms` : "-"); fill("vhostTraffic", d => `${rateBytes(d.traffic)}/s`);
const now = performance.now();
for (const [id, data] of aggregate) {
const history = chartHistory.get(id) || { lastTime: now, lastRequests: data.requests, points: [], bucketSize: 1, pendingSum: 0, pendingCount: 0 };
const elapsed = Math.max(.001, (now - history.lastTime) / 1000); const rate = data.requests >= history.lastRequests ? (data.requests - history.lastRequests) / elapsed : 0; history.lastTime = now; history.lastRequests = data.requests;
history.pendingSum += rate; history.pendingCount += 1;
if (history.pendingCount >= history.bucketSize) { history.points.push(history.pendingSum / history.pendingCount); history.pendingSum = 0; history.pendingCount = 0; }
const history = chartHistory.get(id) || { last: null, points: [], bucketSize: 1, pendingSum: 0, pendingCount: 0 };
const sample = rustProxyRateSampling.difference(history.last, [data.requests], now, rustProxyRateSampling.cadence("overview_seconds"));
const pausedRate = history.last !== null && sample.deltas === null;
history.last = sample.next;
const rate = sample.deltas ? sample.deltas[0] / sample.seconds : 0;
if (!pausedRate) {
history.pendingSum += rate; history.pendingCount += 1;
if (history.pendingCount >= history.bucketSize) { history.points.push(history.pendingSum / history.pendingCount); history.pendingSum = 0; history.pendingCount = 0; }
}
if (history.points.length >= 120) { const compacted = []; for (let i = 0; i < history.points.length; i += 2) compacted.push((history.points[i] + history.points[i + 1]) / 2); history.points = compacted; history.bucketSize *= 2; }
chartHistory.set(id, history);
const displayPoints = history.pendingCount ? [...history.points, history.pendingSum / history.pendingCount] : history.points; const actualPeak = Math.max(0, ...displayPoints), scalePeak = Math.max(1, actualPeak); const points = displayPoints.map((value, index) => `${index * 320 / Math.max(1, displayPoints.length - 1)},${50 - value / scalePeak * 44}`).join(" ");
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`); svg?.querySelector("polyline")?.setAttribute("points", points); const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`); if (label) label.textContent = `${rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${actualPeak.toFixed(1)}`;
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`); svg?.querySelector("polyline")?.setAttribute("points", points); const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`); if (label) label.textContent = `${pausedRate ? "-" : rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${actualPeak.toFixed(1)}`;
}
const editingVhost = editing ? configuredVhosts.get(editing) : null;
const editingHasProxy = editingVhost && (editingVhost.source?.type !== "web_server" || (editingVhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy"));
@@ -29,6 +29,7 @@ const ctx = {
bytes: v => `${v} B`, currentPanel: 'overview', performance: { now: () => now },
};
vm.createContext(ctx);
vm.runInContext(fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8'), ctx);
vm.runInContext(source, ctx);
const module = window.rustProxyBackendDashboard;
const data = {
+1 -1
View File
@@ -122,7 +122,7 @@ with sync_playwright() as p:
assert history['before'] == 1 and history['live'] == 46, history
assert history['afterSystem'] == 46 and history['afterLive'] == 47, history
assert history['newest'] == 500 and '500 B' in history['label'], history
assert 'renderNetworkInterfaceCharts(d.network_interfaces || [], appendSample);' in source
assert 'renderNetworkInterfaceCharts(live.network_interfaces || [], appendSample);' in source
refresh = (WEB / 'app/refresh.js').read_text()
assert 'interval: () => options.system_info_seconds' in refresh
assert 'renderDiagnostics(latestStatus, false, true);' in refresh
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Real diagnostic renderer/CSS in Chromium mobile emulation (not iOS Safari)."""
from pathlib import Path
import shutil
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
WEB = ROOT / 'src/admin/web'
source = (WEB / 'app/runtime.js').read_text()
soup = BeautifulSoup((WEB / 'index.html').read_text(), 'html.parser')
process = soup.select_one('[data-dashboard-widget="process_diagnostics"]')
network = soup.select_one('[data-dashboard-widget="network_diagnostics"]')
assert process and network
# Run the real renderDiagnostics and interface component as shipped, without
# creating a fake SSE server or depending on Rust/Cargo in this test environment.
start = source.index('let systemSamples =')
end = source.index('function renderHeaderRules(', start)
renderer = source[start:end]
html = ('<!doctype html><html data-theme="ultra_dark" data-corner-style="rounded" '
'data-dashboard-density="compact"><head><meta name="viewport" '
'content="width=device-width, initial-scale=1"><style>'
+ (WEB / 'style.css').read_text() + '</style></head>'
'<body style="margin:0"><div style="height:1100px">Earlier dashboard cards</div>'
'<section id="panel-overview"><div id="dashboard-overview-grid" '
'class="stats-grid dashboard-overview-grid is-aligned">'
+ str(process) + str(network) + '</div></section>'
'<div class="vhost-policy-pills"><small class="pill">H1</small>'
'<small class="vhost-backend-protocol-badge" data-observed="true">AUTO → H2</small></div>'
'<div style="height:900px">Footer</div></body></html>')
stubs = '''
const dummy = new Map();
const $ = id => document.getElementById(id) || (() => {
if (!dummy.has(id)) dummy.set(id, { textContent:'-', value:'', hidden:false,
classList:{ toggle(){} }, setAttribute(){}, removeAttribute(){}, append(){},
replaceChildren(){}, closest(){ return {hidden:false}; } });
return dummy.get(id);
})();
const envelope = {config:{cache:{enabled:true,cold_enabled:false}}};
const t = key => ({networkInterfacesVisibleCount:'{visible} / {total}',
networkInterfaceRate:'Rate',networkInterfacesHistory:'Traffic',hideMetric:'Hide',
showMetric:'Show',hiddenInterfaces:'Hidden',networkNoInterfaces:'None',
networkNoVisibleInterfaces:'None visible',notEnabled:'Disabled',
activeListeners:'Listeners',restartPending:'Restart',realSample:'Sampled',
threads:'threads',unavailable:'Unavailable',unlimited:'Unlimited'})[key] || key;
const bytes = value => `${Number(value)} B`;
const locale = () => 'en-US';
const uptime = seconds => `${seconds}s`;
const formatDateTime = seconds => String(seconds);
const action = (text, fn) => { const node=document.createElement('button'); node.textContent=text;
node.addEventListener('click',fn); return node; };
const el = (tag, text, cls) => {const node=document.createElement(tag);
if(text!==undefined) node.textContent=text; if(cls) node.className=cls; return node;};
const updateCacheMemorySizing = () => {};
const rustProxyRateSampling = {cadence:()=>3,difference:(previous,values,at)=>({next:{values,at},
deltas:previous?values.map((v,i)=>v-previous.values[i]):null,seconds:3})};
'''
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, executable_path=shutil.which('chromium'), args=['--no-sandbox'])
ctx = browser.new_context(viewport={'width':393,'height':844}, device_scale_factor=3,
is_mobile=True, has_touch=True)
page = ctx.new_page()
page.set_content(html, wait_until='domcontentloaded')
page.on('pageerror', lambda error: print('BROWSER ERROR:', error))
page.add_script_tag(content=stubs + renderer)
result = page.evaluate('''() => {
const system = {source:'sysinfo', platform:'Linux', process_threads:5,
process_cpu_percent:5,process_rss_bytes:100,cpu_percent:15,
network_rx_bytes_per_second:20, network_tx_bytes_per_second:10,
sampled_at_ms:Date.now(),network_interfaces:[
{name:'eth0',rx_bytes_per_second:10,tx_bytes_per_second:5}]};
const s = {system,cache:{hits:50,misses:30,bypasses:10,resident_bytes:4096},
metrics:{http3_requests:2,mean_duration_ms:0.5},uptime_seconds:100,
listeners:{active_endpoints:[]},proxy_listen:null,tls_listen:null};
renderDiagnostics(s,true,true);
const process = document.querySelector('[data-dashboard-widget="process_diagnostics"]');
const network = document.querySelector('[data-dashboard-widget="network_diagnostics"]');
const target = document.getElementById('network-interface-charts');
const firstRow = target.firstElementChild;
const initialDetail = document.getElementById('process-cpu').textContent;
const initialHistory = processCpuSamples.length;
window.scrollTo(0, Math.round(network.getBoundingClientRect().top + window.scrollY + 125));
const y = window.scrollY, height = process.getBoundingClientRect().height;
for (let i=0; i<35; i++) {
s.live_system = {sampled_at_ms:Date.now(),process_cpu_percent:7+i,
process_rss_bytes:150+i, cpu_percent:22+i,
network_rx_bytes_per_second:100+i*20,network_tx_bytes_per_second:10+i,
network_interfaces:[{name:'eth0',rx_bytes_per_second:100+i*20,
tx_bytes_per_second:10+i}]};
renderDiagnostics(s,true,false);
}
const unchangedDetails = document.getElementById('process-cpu').textContent === initialDetail;
const recentChart = document.getElementById('process-cpu-chart-value').textContent;
const iface = target.querySelector('.network-interface-rate').textContent;
const beforeSystem = processCpuSamples.length;
const y2 = window.scrollY;
s.system = {...system, process_cpu_percent:93,process_rss_bytes:3000,
network_rx_bytes_per_second:9000, network_tx_bytes_per_second:900};
renderDiagnostics(s,false,true);
const slowlyUpdated = document.getElementById('process-cpu').textContent;
const pill = document.querySelector('.pill');
const origin = document.querySelector('.vhost-backend-protocol-badge');
const roundedOrigin = getComputedStyle(origin).borderTopLeftRadius;
document.documentElement.dataset.cornerStyle = 'default';
const defaultPillRadius = getComputedStyle(pill).borderTopLeftRadius;
const defaultOriginRadius = getComputedStyle(origin).borderTopLeftRadius;
document.documentElement.dataset.cornerStyle = 'rounded';
return {initialDetail,initialHistory,unchangedDetails,recentChart,iface,
beforeSystem,afterSystem:processCpuSamples.length,slowlyUpdated,
rowStable:target.firstElementChild===firstRow,
heightDelta:process.getBoundingClientRect().height-height,
scrollDelta:y2-y,postSystemScrollDelta:window.scrollY-y,
nestedScroll:getComputedStyle(target).overflowY,
nestedMaxHeight:getComputedStyle(target).maxHeight,
pillRadius:getComputedStyle(pill).borderTopLeftRadius,
originRadius:roundedOrigin,defaultPillRadius,defaultOriginRadius,
originColor:getComputedStyle(origin).color};
}''')
assert result['initialDetail'] == '5%', result
assert result['initialHistory'] == 1, result
assert result['unchangedDetails'] and result['recentChart'] == '41%', result
assert result['beforeSystem'] == 36 == result['afterSystem'], result
assert result['slowlyUpdated'] == '93%', result
assert result['rowStable'] and '780 B' in result['iface'], result
assert abs(result['heightDelta']) < 2 and abs(result['scrollDelta']) < 2, result
assert abs(result['postSystemScrollDelta']) < 2, result
assert result['nestedScroll'] == 'visible' and result['nestedMaxHeight'] == 'none', result
assert result['pillRadius'] == '8px' and result['originRadius'] == '8px', result
assert result['defaultPillRadius'] == result['defaultOriginRadius'], result
assert result['originColor'] != '', result
print('PASS: 35 live CPU/RSS/network samples, slow Process metadata, stable mobile layout/scroll, badge radii', result)
browser.close()
+6 -3
View File
@@ -114,10 +114,13 @@ with sync_playwright() as browser_tool:
assert moves == 0, (width, moves)
assert abs(page.evaluate('window.scrollY')-before) <= 2
print(f'PASS: {width}px touch emulation, two metric columns, no overflow or scroll jumps')
for style,surface in (('square','0px'), ('subtle','5px'), ('rounded','12px'), ('extra','18px')):
for style,surface,control in (('square','0px','0px'), ('subtle','5px','4px'),
('rounded','12px','8px'), ('extra','18px','12px')):
page.evaluate('(s) => document.documentElement.dataset.cornerStyle=s', style)
for selector in ('#vhost-test','#entity-test','#badge-test','.stat'):
for selector in ('#vhost-test','#entity-test','.stat'):
radius = page.locator(selector).first.evaluate('node=>getComputedStyle(node).borderTopLeftRadius')
assert radius == surface, (style,selector,radius)
print('PASS: vhost, entity, backend badge and metric corner presets')
radius = page.locator('#badge-test').evaluate('node=>getComputedStyle(node).borderTopLeftRadius')
assert radius == control, (style,'origin protocol badge',radius)
print('PASS: vhost, entity, badge/control and metric corner presets')
chromium.close()
@@ -21,6 +21,7 @@ const context = {
document: { querySelector: selector => widgets.get(selector.match(/data-dashboard-widget="([^"]+)"/)?.[1]) || null },
};
vm.createContext(context);
vm.runInContext(fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8'), context);
vm.runInContext(source, context);
const render = context.window.rustProxyExtraOverview.render;
const text = id => field(`overview-extra-${id}`).textContent;
@@ -16,7 +16,8 @@ function $(id) {
let now = 1000;
const ctx = {window:{},currentPanel:'overview',performance:{now:()=>now},fmt:(n,places=0)=>Number(n).toFixed(places),fmtBytes:n=>`${n} B`,$,
document:{querySelector:selector=>nodes.get(selector.match(/data-dashboard-widget="([^"]+)"/)?.[1])||null}};
vm.createContext(ctx);vm.runInContext(source,ctx);
vm.createContext(ctx);vm.runInContext(fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8'), ctx);
vm.runInContext(source,ctx);
const render=ctx.window.rustProxyWideOverview.render;
const text=id=>$(id).textContent;
const baseline = { revision:1,
+126
View File
@@ -0,0 +1,126 @@
// Counter-derived chart regression: pauses must not become fake 0/s samples.
// Real Cache / Transform chart functions run against simulated DOM and clock.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const samplingSource = fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8');
const panels = {
cache: {
path: 'src/admin/web/cache-panel.js', stop: ' function configure()',
counters: (hit, miss) => ({ hits: hit, misses: miss, bypasses: 0 }),
graph: 'cache-live-hit-line', rate: 'cache-live-hit-rate',
keys: ['hits', 'misses', 'bypasses'], seconds: 5,
},
transform: {
path: 'src/admin/web/transform-panel.js', stop: ' function countersFor(data)',
counters: (hit, miss) => ({ applied: hit, cache_hits: miss, skipped: 0, errors: 0 }),
graph: 'transform-live-applied-line', rate: 'transform-live-applied-rate',
keys: ['applied', 'cache_hits', 'skipped', 'errors'], seconds: 3,
},
};
function fixture(name, config) {
const source = fs.readFileSync(config.path, 'utf8');
const end = source.indexOf(config.stop);
assert(end > 0, `unable to isolate ${name} live chart`);
const documentListeners = new Map(), windowListeners = new Map(), nodes = new Map();
let now = 1000;
const node = id => {
if (!nodes.has(id)) nodes.set(id, {
value: id.endsWith('-scope') ? '*' : '', textContent: '',
attrs: {}, setAttribute(key, value) { this.attrs[key] = value; },
});
return nodes.get(id);
};
const document = {
hidden: false,
addEventListener(name, listener) { documentListeners.set(name, listener); },
};
const window = {
addEventListener(name, listener) { windowListeners.set(name, listener); },
rustProxyRefresh: { current: () => ({ overview_seconds: 2, cache_seconds: config.seconds }) },
};
const ctx = {
document, window, $: node, performance: { now: () => now },
sparklinePoints: samples => samples.join(','),
};
vm.createContext(ctx);
vm.runInContext(samplingSource, ctx);
vm.runInContext(source.slice(0, end) + '\nwindow.liveChartTest = { renderLiveChart, resetLiveChart, liveChartSamples };\n})();', ctx);
const chart = window.liveChartTest;
const line = () => node(config.graph).attrs.points;
const rate = () => node(config.rate).textContent;
const length = () => chart.liveChartSamples[config.keys[0]].length;
const sample = (stamp, hits, misses) => {
now = stamp;
chart.renderLiveChart(config.counters(hits, misses));
};
return { ctx, chart, line, rate, length, sample, document, documentListeners, windowListeners };
}
for (const [name, config] of Object.entries(panels)) {
const f = fixture(name, config);
f.sample(1000, 100, 50); // no deltas exist before the first observation
assert.equal(f.length(), 0, `${name}: first reading is a baseline, not 0/s`);
assert.equal(f.rate(), '-');
f.sample(1000 + config.seconds * 1000, 130, 65);
assert.equal(f.rate(), `${(30 / config.seconds).toFixed(1)}/s`);
assert.equal(f.length(), 1);
f.sample(1000 + config.seconds * 2000, 130, 65);
assert.equal(f.rate(), '0.0/s', `${name}: real idle interval is still zero`);
assert.equal(f.length(), 2);
const graphBefore = f.line();
const delayed = 1000 + config.seconds * 2000 + 120000;
f.sample(delayed, 500, 200);
assert.equal(f.rate(), '-', `${name}: long pause cannot be averaged into a misleading rate`);
assert.equal(f.length(), 2, `${name}: do not append fake drop`);
assert.equal(f.line(), graphBefore, `${name}: keep old chart path`);
f.sample(delayed + config.seconds * 1000, 530, 215);
assert.equal(f.rate(), `${(30 / config.seconds).toFixed(1)}/s`, `${name}: next fresh interval resumes`);
assert.equal(f.length(), 3);
f.document.hidden = true;
f.documentListeners.get('visibilitychange')?.();
f.document.hidden = false;
f.documentListeners.get('visibilitychange')?.();
const afterBackground = delayed + config.seconds * 1000 + 200;
f.sample(afterBackground, 533, 219);
assert.equal(f.length(), 3, `${name}: even a short background hide must not append a rate`);
assert.equal(f.rate(), '-');
f.sample(afterBackground + config.seconds * 1000, 533, 219);
assert.equal(f.rate(), '0.0/s', `${name}: genuine zero after resume`);
assert.equal(f.length(), 4);
f.windowListeners.get('pagehide')?.();
f.sample(afterBackground + config.seconds * 2000, 540, 220);
assert.equal(f.length(), 4, `${name}: pagehide / BFCache must rebase`);
f.sample(afterBackground + config.seconds * 3000, 550, 230);
assert.equal(f.length(), 5);
f.sample(afterBackground + config.seconds * 4000, 1, 1);
assert.equal(f.length(), 5, `${name}: counters reset must not be interpreted as 0/s`);
f.sample(afterBackground + config.seconds * 5000, 3, 3);
assert.equal(f.length(), 6, `${name}: counter reset recovery`);
}
// All remaining rate charts share this exact continuity check, including
// configured 30-second Cache refresh (not falsely treated as a pause).
{
const listeners = new Map(), document = { hidden: false, addEventListener(key, fn) { listeners.set(key, fn); } };
const context = { window: {}, document };
vm.createContext(context);
vm.runInContext(samplingSource, context);
const sample = vm.runInContext('rustProxyRateSampling', context);
let baseline = sample.difference(null, [100], 1000, 30).next;
let value = sample.difference(baseline, [130], 31000, 30);
assert.equal(value.deltas[0], 30, '30-second chart interval is valid');
baseline = value.next;
value = sample.difference(baseline, [200], 124000, 30);
assert.equal(value.deltas, null, 'large delay is a gap');
document.hidden = true;
listeners.get('visibilitychange')?.();
document.hidden = false;
assert.equal(sample.continuous(value.next, 124001, 30), false, 'hide invalidates even a short elapsed period');
}
console.log('PASS: Cache/Transform history survives hidden tabs, long pauses, BFCache and counter resets; real 0/s and configured cadence remain intact.');
+3 -3
View File
@@ -95,7 +95,7 @@ function fixture({ withoutSse = false } = {}) {
const h = fixture();
h.registerViews();
assert.equal(h.refresh.current().overview_seconds, 2);
assert.equal(h.refresh.current().overview_seconds, 3);
assert.equal(h.refresh.current().system_info_seconds, 60);
assert.equal(h.refresh.current().traffic_seconds, 5);
assert.equal(h.refresh.current().cache_seconds, 5);
@@ -103,7 +103,7 @@ assert.equal(h.refresh.current().live_enabled, true);
assert.equal(h.streams.length, 0);
h.refresh.start(); await h.advance();
assert.equal(h.streams.length, 1, "one shared SSE connection");
assert.equal(h.streams[0].url, "/api/events?interval=2");
assert.equal(h.streams[0].url, "/api/events?interval=3");
assert.equal(h.count("/api/traffic"), 0, "no Traffic on Overview");
assert.equal(h.count("/api/cache"), 0, "no Cache on Overview");
assert.equal(h.count("/api/system-info"), 1, "Overview uses slow diagnostics");
@@ -231,7 +231,7 @@ const invalid = runInNewContext(scheduler + ";normalizeRefreshPreferences({ live
liveFallback: false, stopLive() {}, startLive() {}, $: () => null
});
assert.equal(invalid.live_enabled, true);
assert.equal(invalid.overview_seconds, 2);
assert.equal(invalid.overview_seconds, 3);
assert.equal(invalid.traffic_seconds, 5);
assert.equal(invalid.cache_seconds, 0);
assert.equal(invalid.system_info_seconds, 300);
+1 -1
View File
@@ -26,7 +26,7 @@ const scriptContext = { window, document: { querySelector(selector) {
] } }, latestStatus: null, currentPanel: 'overview',
performance: { now: () => clock }, t: key => key, fmt: n => `${n}`, bytes: n => `${n} B`, Math,
};
vm.runInNewContext(src, scriptContext);
vm.runInNewContext(fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8') + '\n' + src, scriptContext);
const widget = window.rustProxyStreamDashboard;
const status = { streams: { active: [
{ id: 'tcp', enabled: true, protocol: 'tcp' },
@@ -65,6 +65,7 @@ const applyLayout = prefsSource.slice(prefsSource.indexOf('function applyDashboa
const applyPreferences = prefsSource.slice(prefsSource.indexOf('function applyAdminPreferences'), prefsSource.indexOf('try { applyAdminTheme('));
vm.runInContext(`${registryPrelude}\n${constants}\n${normalize}\nlet adminPreferences = null;\n${applyLayout}\n${applyPreferences}\nthis.getPrefs = () => adminPreferences;\nthis.applyAdminPreferencesTest = applyAdminPreferences;`, context);
window.rustProxyPreferences = { current: () => context.getPrefs() };
vm.runInContext(fs.readFileSync('src/admin/web/app/rate-sampling.js', 'utf8'), context);
vm.runInContext(streamSource, context);
// First load (or page reload): defaults hide the new widgets.
+1 -1
View File
@@ -1,6 +1,6 @@
from pathlib import Path
APP_PARTS = ("core.js", "session-resume.js", "refresh.js", "preferences.js", "dashboard-layout-editor.js", "dashboard-inline-layout.js", "stream-dashboard.js", "backend-dashboard.js", "config-history.js", "runtime.js", "settings.js")
APP_PARTS = ("core.js", "session-resume.js", "refresh.js", "rate-sampling.js", "preferences.js", "dashboard-layout-editor.js", "dashboard-inline-layout.js", "stream-dashboard.js", "backend-dashboard.js", "config-history.js", "runtime.js", "settings.js")
PANEL_PARTS = (
"management.js", "drafts.js", "integrations.js", "vhosts.js",
"web-server-profiles.js", "web-server.js", "docker.js", "streams.js",