V0.32.6 #1
No files matched your search
+13
-2
@@ -5,14 +5,25 @@
|
|||||||
|
|
||||||
## 0.32.6
|
## 0.32.6
|
||||||
|
|
||||||
|
- **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
|
||||||
|
browsers. Keep network-interface history advancing on every live Overview update
|
||||||
|
using the most recent system measurement; collect fresh system data only at the
|
||||||
|
separately configured System information interval. Real iOS 27 validation
|
||||||
|
remains pending; this does not prove all scroll jumps have been eliminated.
|
||||||
|
- Fix CSS transformation of descendant pseudo-class selectors (including
|
||||||
|
rounded-corner rules), and mark administration responses `no-transform`.
|
||||||
|
|
||||||
- Restore two-column small metric cards on narrow mobile Overview grids, ensure
|
- Restore two-column small metric cards on narrow mobile Overview grids, ensure
|
||||||
the first render uses the same responsive layout and avoid DOM operations
|
the first render uses the same responsive layout and avoid DOM operations
|
||||||
that can disrupt scrolling when preferences refresh.
|
that can disrupt scrolling when preferences refresh.
|
||||||
- Extend account-selected corner presets to vhost cards, backend badges,
|
- Extend account-selected corner presets to vhost cards, backend badges,
|
||||||
entity cards and additional panel surfaces while preserving the square dock.
|
entity cards and additional panel surfaces while preserving the square dock.
|
||||||
- Report observed HTTP/1.1 or HTTP/2 per backend in authenticated status and
|
- Report observed HTTP/1.1 or HTTP/2 per backend in authenticated status and
|
||||||
show fresh observations next to configured modes on Vhost cards. Stale or
|
show fresh observations next to configured modes in the Vhost policy-badge
|
||||||
unobserved modes show `?`, never a guessed Auto selection. No active probes.
|
group, without changing green/neutral state colors. Stale or unobserved modes
|
||||||
|
show `?`, never a guessed Auto selection. No active probes.
|
||||||
- Add browser, JavaScript and Rust regression coverage. Rust compilation,
|
- Add browser, JavaScript and Rust regression coverage. Rust compilation,
|
||||||
physical iOS Safari and live HTTP/2 ALPN remain acceptance gates.
|
physical iOS Safari and live HTTP/2 ALPN remain acceptance gates.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
|
|||||||
|
|
||||||
## What changed in 0.32.6
|
## What changed in 0.32.6
|
||||||
|
|
||||||
|
- **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
|
||||||
|
`Cache-Control: no-transform`. Real iOS 27 scrolling remains unverified.
|
||||||
|
|
||||||
- **Mobile Overview:** display two metric tiles per row on phone-width canvases,
|
- **Mobile Overview:** display two metric tiles per row on phone-width canvases,
|
||||||
including 320-430px layouts and the initial pre-script paint. Keep horizontal
|
including 320-430px layouts and the initial pre-script paint. Keep horizontal
|
||||||
overflow disabled, allow vertical touch scrolling and avoid unnecessary DOM
|
overflow disabled, allow vertical touch scrolling and avoid unnecessary DOM
|
||||||
@@ -18,7 +26,8 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
|
|||||||
existing metric and content cards. Keep the square-style draft dock unchanged.
|
existing metric and content cards. Keep the square-style draft dock unchanged.
|
||||||
- **Vhosts backend HTTP badges:** show the configured upstream mode alongside
|
- **Vhosts backend HTTP badges:** show the configured upstream mode alongside
|
||||||
the last HTTP/1.1 or HTTP/2 version actually returned by a real upstream
|
the last HTTP/1.1 or HTTP/2 version actually returned by a real upstream
|
||||||
proxy request for each origin. `AUTO → ?` means there is no response from the
|
proxy request for each origin, in the same lower badge row as the Vhost
|
||||||
|
policy indicators (retaining the original observed/unknown badge colors). `AUTO → ?` means there is no response from the
|
||||||
past five minutes; never guess a negotiation result or run an extra probe.
|
past five minutes; never guess a negotiation result or run an extra probe.
|
||||||
The authenticated status payload includes last observation timestamp and
|
The authenticated status payload includes last observation timestamp and
|
||||||
counters per upstream client; the active connection may vary across requests.
|
counters per upstream client; the active connection may vary across requests.
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
## 0.32.6 - Mobile Overview and upstream protocol visibility (runtime acceptance pending)
|
## 0.32.6 - Mobile Overview and upstream protocol visibility (runtime acceptance pending)
|
||||||
|
|
||||||
|
- [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.
|
||||||
|
- [ ] Confirm scroll positions with a real iOS 27 phone while live updates are
|
||||||
|
enabled, including the Process / Network diagnostics boundary.
|
||||||
|
|
||||||
- [x] Reinstate two small Overview cards per row across phone-size widths.
|
- [x] Reinstate two small Overview cards per row across phone-size widths.
|
||||||
- [x] Apply global corner styles to additional vhost/entity panel surfaces.
|
- [x] Apply global corner styles to additional vhost/entity panel surfaces.
|
||||||
- [x] Avoid reparenting already-ordered Overview nodes; retain natural touch
|
- [x] Avoid reparenting already-ordered Overview nodes; retain natural touch
|
||||||
|
|||||||
+13
-11
@@ -2,12 +2,12 @@
|
|||||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||||
a402ee07826dfab1b1d63b98c4a9f75809f056b908c92abacf0bba816d7ad133 AGENTS.md
|
a402ee07826dfab1b1d63b98c4a9f75809f056b908c92abacf0bba816d7ad133 AGENTS.md
|
||||||
4bc02b731361f3859196a41bdf27f477c67287b1ded6fa2b297ea9c1766a7733 CHANGELOG.md
|
7714a8877ae51e8ba453c54c4fb0d9684cd50a46688536e0511d7a1346a3e306 CHANGELOG.md
|
||||||
934ec0e7f8a307b00319606a9c5d7e91dac6449a23dd1f3abcefe4304eb4de06 Cargo.lock
|
934ec0e7f8a307b00319606a9c5d7e91dac6449a23dd1f3abcefe4304eb4de06 Cargo.lock
|
||||||
8a7606650ce03f1faf36c893a17ccf1c9e8da4259e0248ac2c9bf030a64872d1 Cargo.toml
|
8a7606650ce03f1faf36c893a17ccf1c9e8da4259e0248ac2c9bf030a64872d1 Cargo.toml
|
||||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||||
a26af8c8548c4ab04566d41eab41fc4715fc8bfc05bf281dfb10e808795ec20c README.md
|
7f337f1d8d82cf296859daf17101abb4f095cd0f3e4c1538d2307f1ae1382ddf README.md
|
||||||
345ac3de56d04e41a8ad577654a51f0b81ddbbba3685378476f566b5732a1ddb ROADMAP.md
|
5253814978618396d472b5469dcbf66d2e8f7da53e20496d4c8f66bb3cd76f15 ROADMAP.md
|
||||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||||
@@ -63,7 +63,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
|
|||||||
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
||||||
5e615c119848cc474f4b005dbe3b58a7ed8f29375ff409282285c39b0bdc196a docs/SETTINGS.md
|
5e615c119848cc474f4b005dbe3b58a7ed8f29375ff409282285c39b0bdc196a docs/SETTINGS.md
|
||||||
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
|
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
|
||||||
b5a7bb5bf081098624fba570760375792f0612c6022f80d6e462101912b442a7 docs/TEST_REPORT.md
|
c2715cfa0bf20185e5cd94b2219a958f0515a488395f9f05a92350369cd96f27 docs/TEST_REPORT.md
|
||||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||||
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
||||||
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
|
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
|
||||||
@@ -166,7 +166,7 @@ c17ccba5202d375d4cc593f7e20979f2b796362d87660b898feca0af5a135bfc src/admin/pref
|
|||||||
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
|
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
|
||||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||||
6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs
|
6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs
|
||||||
ab8d92d79d0c4d595fdb2558ea877fa8cfa17bf07708375fef4668c4367950fe src/admin/router.rs
|
223310e1339094941c45a5cb65d650673267b7fe8a3aa0927c8c1e3d72de68cd src/admin/router.rs
|
||||||
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
|
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
|
||||||
e7355ef15e4377cd71e47d054c607079dd8e6711f1d86824f716b8003210a046 src/admin/status.rs
|
e7355ef15e4377cd71e47d054c607079dd8e6711f1d86824f716b8003210a046 src/admin/status.rs
|
||||||
e79d3dc0c4fde1ca4a69c204fb41cc5aa1db02aefdabff3435f27b5db0c6eb8d src/admin/status_live.rs
|
e79d3dc0c4fde1ca4a69c204fb41cc5aa1db02aefdabff3435f27b5db0c6eb8d src/admin/status_live.rs
|
||||||
@@ -184,7 +184,7 @@ fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/
|
|||||||
79d31e2002f8cf334f87285f6f2e6dbf69f3647de784ae26efadb92c126bc7a0 src/admin/web/app/overview-wide-metrics.js
|
79d31e2002f8cf334f87285f6f2e6dbf69f3647de784ae26efadb92c126bc7a0 src/admin/web/app/overview-wide-metrics.js
|
||||||
a73d4efc9cf2f6bf7117f6f6dcaaea106e03c328451abbe4c10281179750899d src/admin/web/app/preferences.js
|
a73d4efc9cf2f6bf7117f6f6dcaaea106e03c328451abbe4c10281179750899d src/admin/web/app/preferences.js
|
||||||
6a544baaf40763b4b612e53845b3bc33396b9bb6518565fd6b7c128c278b8d5d src/admin/web/app/refresh.js
|
6a544baaf40763b4b612e53845b3bc33396b9bb6518565fd6b7c128c278b8d5d src/admin/web/app/refresh.js
|
||||||
41f635a510d6f08b37484f0e260929926b93a02a792bdfa05b31ed7ebd682fef src/admin/web/app/runtime.js
|
3b4a7c1538d9e3fe7f9eb3522fcebe077c336d4441c2ee6253d48483cf47c121 src/admin/web/app/runtime.js
|
||||||
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
||||||
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
||||||
cc566f8293cc935f5080ebfb8076293c256bb6a87e1e6a69be03512448871ec9 src/admin/web/app/stream-dashboard.js
|
cc566f8293cc935f5080ebfb8076293c256bb6a87e1e6a69be03512448871ec9 src/admin/web/app/stream-dashboard.js
|
||||||
@@ -222,7 +222,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
|
|||||||
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
|
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
|
||||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||||
e3075af59644e42fa5735574d8e9caa1bcebfe8de184b78aff9f4026b91915f1 src/admin/web/streams.js
|
e3075af59644e42fa5735574d8e9caa1bcebfe8de184b78aff9f4026b91915f1 src/admin/web/streams.js
|
||||||
691ebee8f87f28f574a8dc5f0020922bd04e06f6d51e72fccd3abb6e68be7bf1 src/admin/web/style.css
|
9671caf7468d9e7aaf679e6f02a118603c1e9b88b6c3a009297b265c463eda1e src/admin/web/style.css
|
||||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||||
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
|
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
|
||||||
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
||||||
@@ -230,7 +230,7 @@ b31782c61da3623389b57fdbba5bd41d2287d45fc172e9c14360a9dd7accf061 src/admin/web/
|
|||||||
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
|
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
|
||||||
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
|
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
|
||||||
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
|
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
|
||||||
9b565141991a02f83b46d535aa4f4d4430631270f8c3cabafd373e54b0e7317b src/admin/web/vhosts/render.js
|
823038b8eb335ca766f03b5c718e093f5f026c85dad06ace05ff70c4a7c1410f src/admin/web/vhosts/render.js
|
||||||
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
|
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
|
||||||
355684980d1caf8918d1773ce17934b51e3a85a60f38491217d10035bf68ce9f src/admin/web/web-server.js
|
355684980d1caf8918d1773ce17934b51e3a85a60f38491217d10035bf68ce9f src/admin/web/web-server.js
|
||||||
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
|
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
|
||||||
@@ -436,7 +436,7 @@ c2f802d95da4332fb8480e83f0c7502334457b3408141c1c4516c9ba320a0463 src/traffic.rs
|
|||||||
36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs
|
36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs
|
||||||
d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs
|
d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs
|
||||||
87f8af5261d79072f8215f1fb8a90715a58e7626544fd8176ce840e3111bdb92 src/transform/compile.rs
|
87f8af5261d79072f8215f1fb8a90715a58e7626544fd8176ce840e3111bdb92 src/transform/compile.rs
|
||||||
38dfd1005b1cd9ae21c1e2964a8b464368d8cdff5e525089b3132e0126f211ea src/transform/css.rs
|
b2c5236589093454ec60984b785ad6a4af6149d47f6db37d8e08c17fec7a2c1b src/transform/css.rs
|
||||||
5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs
|
5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs
|
||||||
aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs
|
aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs
|
||||||
a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs
|
a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs
|
||||||
@@ -447,7 +447,7 @@ cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/
|
|||||||
fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/model.rs
|
fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/model.rs
|
||||||
a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs
|
a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs
|
||||||
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
|
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
|
||||||
76b3cb4ebdba9cbac8e388d3a9d74bd2055db564d8e31869a60e7a439b46f7b3 src/transform/tests.rs
|
8ed4baeaf79edfa2a84912cc00f1d13c564e1b997efe6888d2e4b5a16aaa8905 src/transform/tests.rs
|
||||||
6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
|
6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
|
||||||
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
|
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
|
||||||
9c897664a8b3b288b761b84a724b212c3f6a82b856ceebf3b4ecd6eab1846a35 src/upstream.rs
|
9c897664a8b3b288b761b84a724b212c3f6a82b856ceebf3b4ecd6eab1846a35 src/upstream.rs
|
||||||
@@ -519,9 +519,10 @@ dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/d
|
|||||||
72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs
|
72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs
|
||||||
c081925edadee3ffb21867765fcd08d6a536093999ee99c5590b1d5980f8e4b3 tools/checks/h3_navigation_0316.py
|
c081925edadee3ffb21867765fcd08d6a536093999ee99c5590b1d5980f8e4b3 tools/checks/h3_navigation_0316.py
|
||||||
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
|
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
|
||||||
|
e1a8ae9ab6b4ef8387ac0a84e8825e2bfed70e9b64f7cb23ed542fce242a141e tools/checks/ios_scroll_0326_browser.py
|
||||||
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
|
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
|
||||||
a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py
|
a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py
|
||||||
bfb48f57a2e87e4b03fdb21115ecdab12187fc4d1ead9db42f5416b061d21596 tools/checks/mobile_protocol_0326.py
|
c0f08ee32fb506e946a2cf3308c061da7351e6022337aa9114b21d7ad19203c8 tools/checks/mobile_protocol_0326.py
|
||||||
c7ddc69c8bfe5a675ae48d02c76c85d0d0490bc1da1833a5f99dc459d7fe9639 tools/checks/mobile_protocol_0326_browser.py
|
c7ddc69c8bfe5a675ae48d02c76c85d0d0490bc1da1833a5f99dc459d7fe9639 tools/checks/mobile_protocol_0326_browser.py
|
||||||
af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs
|
af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs
|
||||||
0387a9e29244b58cf37a6673a88ae2b7fea3aa965c032423f3783a84eab4ba05 tools/checks/overview_0321_layout_regression.mjs
|
0387a9e29244b58cf37a6673a88ae2b7fea3aa965c032423f3783a84eab4ba05 tools/checks/overview_0321_layout_regression.mjs
|
||||||
@@ -555,6 +556,7 @@ bb862620104f925488e36904e258b7c934a097362ef879ecb275c64008cebb83 tools/checks/s
|
|||||||
e650225258168eeb9623694c6684f9a5ace92ca94debdff2558b5c77f7c998bc tools/checks/upstream_protocols_0323.py
|
e650225258168eeb9623694c6684f9a5ace92ca94debdff2558b5c77f7c998bc tools/checks/upstream_protocols_0323.py
|
||||||
a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/version_contracts.py
|
a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/version_contracts.py
|
||||||
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
|
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
|
ea98e8ce2bdc841814e486ea4c13f94e284b2c50a5887598b7a288f75a14deaf tools/checks/vhost_www_ui_test.mjs
|
||||||
37a74b5fea295efc3ff77609bedf0c3c5352b92418157961419b914d738efe6b tools/checks/web_assets.py
|
37a74b5fea295efc3ff77609bedf0c3c5352b92418157961419b914d738efe6b tools/checks/web_assets.py
|
||||||
8d2e1837f94302d09a704aca452cc367e6892f289aca3dc2d82c086a2e7de26d tools/checks/web_cache_revalidation_0322.py
|
8d2e1837f94302d09a704aca452cc367e6892f289aca3dc2d82c086a2e7de26d tools/checks/web_cache_revalidation_0322.py
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
# Verification report - RustProxy 0.32.6
|
# Verification report - RustProxy 0.32.6
|
||||||
|
|
||||||
|
## 0.32.6 development follow-up — badge grouping and chart cadence
|
||||||
|
|
||||||
|
- Moved each observed upstream HTTP protocol badge into its Vhost's existing
|
||||||
|
`vhost-policy-pills` row; preserved the badge class, observed flag, green
|
||||||
|
and neutral CSS styling. No backend or preference changes.
|
||||||
|
- Kept network-interface rows and SVGs stable (no replacement on ordinary
|
||||||
|
refresh), but restored **frequent** history progression: each Overview live
|
||||||
|
status appends the latest cached interface rates, exactly as the aggregate
|
||||||
|
network chart does. A system-info-only refresh updates visible rates but does
|
||||||
|
not append a history point. System measurements are fetched only on the
|
||||||
|
configured `system_info_seconds` cadence; no extra API call was added.
|
||||||
|
- Chromium tests: `ios_scroll_0326_browser.py` (45 live renders, unchanged
|
||||||
|
DOM/scroll/Process height, latest-value reuse and a system-info-only redraw);
|
||||||
|
`vhost_protocol_badge_group_0326_browser.py` (real Vhost render at 393/1024px,
|
||||||
|
observed green and unknown neutral colors); static source and Node.js protocol
|
||||||
|
status checks. `mobile_protocol_0326_browser.py` also passed at six
|
||||||
|
mobile widths (320-550px). `./dev.sh test` passed all pre-Cargo checks,
|
||||||
|
including its source/SQL/upgrade/release-smoke and Node.js UI stages, then
|
||||||
|
exited **127** at the missing-Cargo guard; Rust compilation/tests were not run.
|
||||||
|
These results do **not** establish iOS 27 Safari acceptance or a compiled
|
||||||
|
RustProxy binary.
|
||||||
|
|
||||||
|
## 0.32.6 development follow-up — iOS 27 scroll / CSS transform
|
||||||
|
|
||||||
|
- User video shows repeated returns toward Process during mobile scrolling;
|
||||||
|
it does not include finger touch input, so exact root cause cannot be proven
|
||||||
|
by visual inspection alone. The Network diagnostics renderer removed and
|
||||||
|
recreated all interface rows/SVGs on every live sample, potentially
|
||||||
|
invalidating WebKit's scroll anchor directly beneath Process.
|
||||||
|
- Initially changed to update chart contents, text nodes and SVG points in place
|
||||||
|
without replacing interface rows. An interim build appended interface RX/TX
|
||||||
|
history only on new system measurements; this sampling decision was superseded
|
||||||
|
by the later development follow-up above, which restores live-rate chart
|
||||||
|
progression independently of slow system-data acquisition. Retest physical
|
||||||
|
iOS 27 before declaring the scroll problem fixed.
|
||||||
|
- CSS transformer preserves descendant pseudo-class combinators; admin pages
|
||||||
|
carry `Cache-Control: no-transform`. Rust/Cargo check still pending.
|
||||||
|
|
||||||
## 0.32.6 mobile presentation and upstream protocol visibility
|
## 0.32.6 mobile presentation and upstream protocol visibility
|
||||||
|
|
||||||
- `./dev.sh test` passed all pre-Cargo source, regression, simulated API,
|
- `./dev.sh test` passed all pre-Cargo source, regression, simulated API,
|
||||||
|
|||||||
+1
-1
@@ -277,7 +277,7 @@ fn secure(mut response: Response) -> Response {
|
|||||||
let h = response.headers_mut();
|
let h = response.headers_mut();
|
||||||
h.insert(
|
h.insert(
|
||||||
"cache-control",
|
"cache-control",
|
||||||
HeaderValue::from_static("private, no-store, no-cache"),
|
HeaderValue::from_static("private, no-store, no-cache, no-transform"),
|
||||||
);
|
);
|
||||||
h.insert(
|
h.insert(
|
||||||
"x-content-type-options",
|
"x-content-type-options",
|
||||||
|
|||||||
@@ -206,8 +206,22 @@ function svgNode(tag, attrs = {}) {
|
|||||||
for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
|
for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
// Diagnostic charts keep their SVG and label nodes across samples. Updating the
|
||||||
|
// existing Text node avoids remove/insert mutations while the user is scrolling.
|
||||||
|
function setChartText(node, value) {
|
||||||
|
const text = String(value);
|
||||||
|
if (node.firstChild?.nodeType === 3 && node.firstChild === node.lastChild) {
|
||||||
|
if (node.firstChild.data !== text) node.firstChild.data = text;
|
||||||
|
} else if (node.textContent !== text) {
|
||||||
|
node.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Preserve stable Overview DOM while live samples arrive: replacing network SVGs
|
||||||
|
// during an iOS WebKit momentum scroll can invalidate the current scroll anchor.
|
||||||
|
let latestNetworkInterfaceSnapshot = [];
|
||||||
function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||||
const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : [];
|
const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : [];
|
||||||
|
latestNetworkInterfaceSnapshot = current;
|
||||||
const names = new Set(current.map(item => item.name));
|
const names = new Set(current.map(item => item.name));
|
||||||
for (const name of [...networkInterfaceSamples.keys()]) if (!names.has(name)) networkInterfaceSamples.delete(name);
|
for (const name of [...networkInterfaceSamples.keys()]) if (!names.has(name)) networkInterfaceSamples.delete(name);
|
||||||
if (appendSample) {
|
if (appendSample) {
|
||||||
@@ -222,37 +236,79 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
|||||||
}
|
}
|
||||||
const target = $("network-interface-charts");
|
const target = $("network-interface-charts");
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
target.replaceChildren();
|
|
||||||
const visible = current.filter(item => item?.name && !hiddenNetworkInterfaces.has(item.name));
|
const visible = current.filter(item => item?.name && !hiddenNetworkInterfaces.has(item.name));
|
||||||
$("network-interface-chart-value").textContent = t("networkInterfacesVisibleCount").replace("{visible}", visible.length).replace("{total}", current.length);
|
setChartText($("network-interface-chart-value"), t("networkInterfacesVisibleCount").replace("{visible}", visible.length).replace("{total}", current.length));
|
||||||
if (!current.length) { target.append(el("p", t("networkNoInterfaces"), "empty network-interface-empty")); return; }
|
const existing = new Map([...target.children].filter(node => node.classList.contains("network-interface-row"))
|
||||||
for (const item of visible) {
|
.map(node => [node.dataset.interfaceName, node]));
|
||||||
const history = networkInterfaceSamples.get(item.name) || { rx: [], tx: [] };
|
const children = [];
|
||||||
const peak = Math.max(1, ...history.rx, ...history.tx);
|
if (!current.length) {
|
||||||
const row = el("div", undefined, "network-interface-row");
|
const empty = target.querySelector(":scope > .network-interface-empty") || el("p", undefined, "empty network-interface-empty");
|
||||||
const head = el("div", undefined, "mini-live-chart-head network-interface-row-head");
|
empty.textContent = t("networkNoInterfaces");
|
||||||
const rate = el("span", `${t("networkInterfaceRate")}: ${fmtBytes(item.rx_bytes_per_second)} / ${fmtBytes(item.tx_bytes_per_second)}`, "fine");
|
children.push(empty);
|
||||||
const hide = action(t("hideMetric"), () => { hiddenNetworkInterfaces.add(item.name); saveHiddenNetworkInterfaces(); renderNetworkInterfaceCharts(current, false); });
|
} else {
|
||||||
hide.classList.add("network-interface-hide");
|
for (const item of visible) {
|
||||||
const actions = el("span", undefined, "network-interface-head-actions"); actions.append(rate, hide);
|
const name = item.name;
|
||||||
head.append(el("b", item.name || "-", "mono"), actions);
|
let row = existing.get(name);
|
||||||
const svg = svgNode("svg", { viewBox: "0 0 300 60", role: "img", "aria-label": `${item.name} ${t("networkInterfacesHistory")}` });
|
if (!row) {
|
||||||
svg.append(svgNode("path", { class: "chart-grid", d: "M0 15H300 M0 30H300 M0 45H300" }));
|
row = el("div", undefined, "network-interface-row");
|
||||||
svg.append(svgNode("polyline", { class: "diagnostic-line network-rx-line", points: sparklinePoints(history.rx, 300, 60, 5, peak) || "0,55 300,55" }));
|
row.dataset.interfaceName = name;
|
||||||
svg.append(svgNode("polyline", { class: "diagnostic-line network-tx-line", points: sparklinePoints(history.tx, 300, 60, 5, peak) || "0,55 300,55" }));
|
const head = el("div", undefined, "mini-live-chart-head network-interface-row-head");
|
||||||
row.append(head, svg); target.append(row);
|
const rate = el("span", undefined, "fine network-interface-rate");
|
||||||
}
|
const hide = action(t("hideMetric"), () => {
|
||||||
const hidden = current.filter(item => item?.name && hiddenNetworkInterfaces.has(item.name));
|
hiddenNetworkInterfaces.add(name);
|
||||||
if (hidden.length) {
|
saveHiddenNetworkInterfaces();
|
||||||
const hiddenRow = el("div", undefined, "network-interface-hidden");
|
renderNetworkInterfaceCharts(latestNetworkInterfaceSnapshot, false);
|
||||||
hiddenRow.append(el("span", t("hiddenInterfaces"), "fine"));
|
});
|
||||||
for (const item of hidden) {
|
hide.classList.add("network-interface-hide");
|
||||||
const show = action(`${t("showMetric")} ${item.name}`, () => { hiddenNetworkInterfaces.delete(item.name); saveHiddenNetworkInterfaces(); renderNetworkInterfaceCharts(current, false); });
|
const actions = el("span", undefined, "network-interface-head-actions");
|
||||||
hiddenRow.append(show);
|
actions.append(rate, hide);
|
||||||
|
head.append(el("b", name, "mono"), actions);
|
||||||
|
const svg = svgNode("svg", { viewBox: "0 0 300 60", role: "img", "aria-label": `${name} ${t("networkInterfacesHistory")}` });
|
||||||
|
svg.append(svgNode("path", { class: "chart-grid", d: "M0 15H300 M0 30H300 M0 45H300" }));
|
||||||
|
svg.append(svgNode("polyline", { class: "diagnostic-line network-rx-line", points: "0,55 300,55" }));
|
||||||
|
svg.append(svgNode("polyline", { class: "diagnostic-line network-tx-line", points: "0,55 300,55" }));
|
||||||
|
row.append(head, svg);
|
||||||
|
}
|
||||||
|
const history = networkInterfaceSamples.get(name) || { rx: [], tx: [] };
|
||||||
|
const peak = Math.max(1, ...history.rx, ...history.tx);
|
||||||
|
setChartText(row.querySelector(".network-interface-rate"), `${t("networkInterfaceRate")}: ${fmtBytes(item.rx_bytes_per_second)} / ${fmtBytes(item.tx_bytes_per_second)}`);
|
||||||
|
setChartText(row.querySelector(".network-interface-hide"), t("hideMetric"));
|
||||||
|
row.querySelector(".network-rx-line").setAttribute("points", sparklinePoints(history.rx, 300, 60, 5, peak) || "0,55 300,55");
|
||||||
|
row.querySelector(".network-tx-line").setAttribute("points", sparklinePoints(history.tx, 300, 60, 5, peak) || "0,55 300,55");
|
||||||
|
row.querySelector("svg").setAttribute("aria-label", `${name} ${t("networkInterfacesHistory")}`);
|
||||||
|
children.push(row);
|
||||||
}
|
}
|
||||||
target.append(hiddenRow);
|
const hidden = current.filter(item => item?.name && hiddenNetworkInterfaces.has(item.name));
|
||||||
} else if (!visible.length) {
|
if (hidden.length) {
|
||||||
target.append(el("p", t("networkNoVisibleInterfaces"), "empty network-interface-empty"));
|
const shelf = target.querySelector(":scope > .network-interface-hidden") || el("div", undefined, "network-interface-hidden");
|
||||||
|
const previous = [...shelf.querySelectorAll("button[data-interface-name]")].map(node => node.dataset.interfaceName);
|
||||||
|
const changed = previous.length !== hidden.length || previous.some((name, i) => name !== hidden[i].name);
|
||||||
|
if (changed) {
|
||||||
|
shelf.replaceChildren(el("span", t("hiddenInterfaces"), "fine"));
|
||||||
|
for (const item of hidden) {
|
||||||
|
const show = action(`${t("showMetric")} ${item.name}`, () => {
|
||||||
|
hiddenNetworkInterfaces.delete(item.name);
|
||||||
|
saveHiddenNetworkInterfaces();
|
||||||
|
renderNetworkInterfaceCharts(latestNetworkInterfaceSnapshot, false);
|
||||||
|
});
|
||||||
|
show.dataset.interfaceName = item.name;
|
||||||
|
shelf.append(show);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setChartText(shelf.querySelector(".fine"), t("hiddenInterfaces"));
|
||||||
|
for (const button of shelf.querySelectorAll("button[data-interface-name]")) setChartText(button, `${t("showMetric")} ${button.dataset.interfaceName}`);
|
||||||
|
}
|
||||||
|
children.push(shelf);
|
||||||
|
} else if (!visible.length) {
|
||||||
|
const empty = target.querySelector(":scope > .network-interface-empty") || el("p", undefined, "empty network-interface-empty");
|
||||||
|
empty.textContent = t("networkNoVisibleInterfaces");
|
||||||
|
children.push(empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The common live-sample path updates only text and SVG path attributes.
|
||||||
|
// Reparent rows solely when interfaces or visibility have actually changed.
|
||||||
|
if (children.length !== target.children.length || children.some((node, index) => node !== target.children[index])) {
|
||||||
|
target.replaceChildren(...children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
|
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
|
||||||
@@ -316,10 +372,10 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
|||||||
$("cache-miss-sparkline").setAttribute("points", sparklinePoints(cacheMissSamples, 300, 60, 5, cacheOutcomePeak));
|
$("cache-miss-sparkline").setAttribute("points", sparklinePoints(cacheMissSamples, 300, 60, 5, cacheOutcomePeak));
|
||||||
$("cache-bypass-sparkline").setAttribute("points", sparklinePoints(cacheBypassSamples, 300, 60, 5, cacheOutcomePeak));
|
$("cache-bypass-sparkline").setAttribute("points", sparklinePoints(cacheBypassSamples, 300, 60, 5, cacheOutcomePeak));
|
||||||
const latestCacheDelta = [cacheHitSamples.at(-1), cacheMissSamples.at(-1), cacheBypassSamples.at(-1)];
|
const latestCacheDelta = [cacheHitSamples.at(-1), cacheMissSamples.at(-1), cacheBypassSamples.at(-1)];
|
||||||
$("cache-outcome-chart-value").textContent = latestCacheDelta.every(value => value != null) ? latestCacheDelta.map(value => fmt(value)).join(" / ") : "-";
|
setChartText($("cache-outcome-chart-value"), latestCacheDelta.every(value => value != null) ? latestCacheDelta.map(value => fmt(value)).join(" / ") : "-");
|
||||||
const hotLimit = Number(c.hot_limit_bytes), hotPeak = Number.isFinite(hotLimit) && hotLimit > 0 ? Math.max(hotLimit, ...cacheMemorySamples) : null;
|
const hotLimit = Number(c.hot_limit_bytes), hotPeak = Number.isFinite(hotLimit) && hotLimit > 0 ? Math.max(hotLimit, ...cacheMemorySamples) : null;
|
||||||
$("cache-memory-sparkline").setAttribute("points", sparklinePoints(cacheMemorySamples, 300, 60, 5, hotPeak));
|
$("cache-memory-sparkline").setAttribute("points", sparklinePoints(cacheMemorySamples, 300, 60, 5, hotPeak));
|
||||||
$("cache-chart-value").textContent = occupancyText(c.resident_bytes, c.hot_limit_bytes);
|
setChartText($("cache-chart-value"), occupancyText(c.resident_bytes, c.hot_limit_bytes));
|
||||||
const transientLimit = Number(c.transient_limit_bytes);
|
const transientLimit = Number(c.transient_limit_bytes);
|
||||||
const transientPeak = Number.isFinite(transientLimit) && transientLimit > 0
|
const transientPeak = Number.isFinite(transientLimit) && transientLimit > 0
|
||||||
? Math.max(transientLimit, ...cacheTransientSamples, ...cacheFillSamples, ...cacheColdQueueSamples, ...cacheColdIoSamples)
|
? Math.max(transientLimit, ...cacheTransientSamples, ...cacheFillSamples, ...cacheColdQueueSamples, ...cacheColdIoSamples)
|
||||||
@@ -328,11 +384,11 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
|||||||
$("cache-fill-sparkline").setAttribute("points", sparklinePoints(cacheFillSamples, 300, 60, 5, transientPeak));
|
$("cache-fill-sparkline").setAttribute("points", sparklinePoints(cacheFillSamples, 300, 60, 5, transientPeak));
|
||||||
$("cache-queue-sparkline").setAttribute("points", sparklinePoints(cacheColdQueueSamples, 300, 60, 5, transientPeak));
|
$("cache-queue-sparkline").setAttribute("points", sparklinePoints(cacheColdQueueSamples, 300, 60, 5, transientPeak));
|
||||||
$("cache-io-sparkline").setAttribute("points", sparklinePoints(cacheColdIoSamples, 300, 60, 5, transientPeak));
|
$("cache-io-sparkline").setAttribute("points", sparklinePoints(cacheColdIoSamples, 300, 60, 5, transientPeak));
|
||||||
$("cache-transient-chart-value").textContent = occupancyText(c.transient_bytes, c.transient_limit_bytes);
|
setChartText($("cache-transient-chart-value"), occupancyText(c.transient_bytes, c.transient_limit_bytes));
|
||||||
const coldEnabled = envelope?.config.cache?.cold_enabled === true;
|
const coldEnabled = envelope?.config.cache?.cold_enabled === true;
|
||||||
const coldLimit = Number(c.cold_limit_bytes), coldPeak = Number.isFinite(coldLimit) && coldLimit > 0 ? Math.max(coldLimit, ...cacheColdStorageSamples) : null;
|
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));
|
$("cache-cold-sparkline").setAttribute("points", sparklinePoints(cacheColdStorageSamples, 300, 60, 5, coldPeak));
|
||||||
$("cache-cold-chart-value").textContent = coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled");
|
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(" / ");
|
const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");
|
||||||
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform);
|
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform);
|
||||||
setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
|
setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
|
||||||
@@ -362,9 +418,9 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
|||||||
const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||||
setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory);
|
setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory);
|
||||||
$("process-cpu-sparkline").setAttribute("points", sparklinePoints(processCpuSamples));
|
$("process-cpu-sparkline").setAttribute("points", sparklinePoints(processCpuSamples));
|
||||||
$("process-cpu-chart-value").textContent = d.process_cpu_percent == null ? "" : `${fmt(d.process_cpu_percent, 1)}%`;
|
setChartText($("process-cpu-chart-value"), d.process_cpu_percent == null ? "" : `${fmt(d.process_cpu_percent, 1)}%`);
|
||||||
$("process-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples));
|
$("process-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples));
|
||||||
$("process-rss-chart-value").textContent = d.process_rss_bytes == null ? "" : fmtBytes(d.process_rss_bytes);
|
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-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;
|
$("process-rss-sparkline").closest(".mini-live-chart").hidden = d.process_rss_bytes == null;
|
||||||
updateCacheMemorySizing();
|
updateCacheMemorySizing();
|
||||||
@@ -381,11 +437,14 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
|||||||
const networkPeak = Math.max(1, ...networkRxSamples, ...networkTxSamples);
|
const networkPeak = Math.max(1, ...networkRxSamples, ...networkTxSamples);
|
||||||
$("network-rx-sparkline").setAttribute("points", sparklinePoints(networkRxSamples, 300, 60, 5, networkPeak));
|
$("network-rx-sparkline").setAttribute("points", sparklinePoints(networkRxSamples, 300, 60, 5, networkPeak));
|
||||||
$("network-tx-sparkline").setAttribute("points", sparklinePoints(networkTxSamples, 300, 60, 5, networkPeak));
|
$("network-tx-sparkline").setAttribute("points", sparklinePoints(networkTxSamples, 300, 60, 5, networkPeak));
|
||||||
$("network-chart-value").textContent = networkRateSupported ? `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}` : "";
|
setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}` : "");
|
||||||
$("network-rx-sparkline").closest(".mini-live-chart").hidden = !networkRateSupported;
|
$("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);
|
renderNetworkInterfaceCharts(d.network_interfaces || [], appendSample);
|
||||||
$("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples));
|
$("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples));
|
||||||
$("latency-chart-value").textContent = m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`;
|
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;
|
$("latency-sparkline").closest(".mini-live-chart").hidden = m.mean_duration_ms == null;
|
||||||
$("compression-counters").textContent = [m.compression_gzip, m.compression_br, m.compression_zstd, m.compression_deflate].map(n => fmt(n)).join(" / ");
|
$("compression-counters").textContent = [m.compression_gzip, m.compression_br, m.compression_zstd, m.compression_deflate].map(n => fmt(n)).join(" / ");
|
||||||
const age = d.sampled_at_ms ? Math.max(0, Math.floor((Date.now() - d.sampled_at_ms) / 1000)) : null;
|
const age = d.sampled_at_ms ? Math.max(0, Math.floor((Date.now() - d.sampled_at_ms) / 1000)) : null;
|
||||||
|
|||||||
@@ -10732,15 +10732,8 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
|
|||||||
@media (max-width: 640px) { .acl-profile-rule { grid-template-columns: 1fr; } }
|
@media (max-width: 640px) { .acl-profile-rule { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
/* Latest HTTP version observed on a real request to each configured origin.
|
/* Latest HTTP version observed on a real request to each configured origin.
|
||||||
A badge without an observation must not claim that Auto selected H1 or H2. */
|
Protocol indicators share the Vhost policy-pills group but retain their
|
||||||
.vhost-backend-protocols {
|
original colors and observed/unobserved states. */
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
min-width: 0;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
.vhost-backend-protocol-badge {
|
.vhost-backend-protocol-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -211,20 +211,6 @@ function render() {
|
|||||||
];
|
];
|
||||||
for (const [key, value] of detailItems) { const item = el("div", undefined, "vhost-detail"); item.append(text("span", key, "small-label"), el("b", value)); details.append(item); }
|
for (const [key, value] of detailItems) { const item = el("div", undefined, "vhost-detail"); item.append(text("span", key, "small-label"), el("b", value)); details.append(item); }
|
||||||
box.append(details);
|
box.append(details);
|
||||||
if ((v.backends || []).length && (v.source?.type !== "web_server" || (v.routes || []).some(route =>
|
|
||||||
!route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy"))) {
|
|
||||||
const badges = el("div", undefined, "vhost-backend-protocols");
|
|
||||||
for (const backend of v.backends) {
|
|
||||||
const badge = el("span", undefined, "vhost-backend-protocol-badge");
|
|
||||||
badge.dataset.upstreamProtocolVhost = v.id;
|
|
||||||
badge.dataset.upstreamProtocolBackend = backend.id;
|
|
||||||
badge.textContent = upstreamProtocolBadgeText(backend, null).label;
|
|
||||||
badge.title = t("vhBackendProtocolUnobserved");
|
|
||||||
badges.append(badge);
|
|
||||||
}
|
|
||||||
box.append(badges);
|
|
||||||
}
|
|
||||||
|
|
||||||
const live = el("div", undefined, "vhost-live-grid");
|
const live = el("div", undefined, "vhost-live-grid");
|
||||||
const metric = (key, suffix) => { const item = el("div", undefined, "vhost-live-metric"); item.append(text("span", key), el("strong", "-")); item.querySelector("strong").dataset[suffix] = v.id; live.append(item); };
|
const metric = (key, suffix) => { const item = el("div", undefined, "vhost-live-metric"); item.append(text("span", key), el("strong", "-")); item.querySelector("strong").dataset[suffix] = v.id; live.append(item); };
|
||||||
metric("status", "vhostHealth"); metric("vhRequests", "vhostRequests"); metric("vhActive", "vhostActive"); metric("vhErrors", "vhostErrors"); metric("vhLatency", "vhostLatency"); metric("vhTraffic", "vhostTraffic");
|
metric("status", "vhostHealth"); metric("vhRequests", "vhostRequests"); metric("vhActive", "vhostActive"); metric("vhErrors", "vhostErrors"); metric("vhLatency", "vhostLatency"); metric("vhTraffic", "vhostTraffic");
|
||||||
@@ -253,6 +239,18 @@ function render() {
|
|||||||
policy.append(el("small", `WebSocket · ${transports.join("+")}`, "pill"));
|
policy.append(el("small", `WebSocket · ${transports.join("+")}`, "pill"));
|
||||||
if (v.websocket_legacy_support) policy.append(el("small", t("vhWebsocketLegacyBadge"), "pill"));
|
if (v.websocket_legacy_support) policy.append(el("small", t("vhWebsocketLegacyBadge"), "pill"));
|
||||||
}
|
}
|
||||||
|
// Keep the upstream protocol indicators with the other Vhost policy pills.
|
||||||
|
// Retain their own badge class so observed H1/H2 keeps its green styling.
|
||||||
|
if (hasProxySource) {
|
||||||
|
for (const backend of v.backends || []) {
|
||||||
|
const badge = el("span", undefined, "vhost-backend-protocol-badge");
|
||||||
|
badge.dataset.upstreamProtocolVhost = v.id;
|
||||||
|
badge.dataset.upstreamProtocolBackend = backend.id;
|
||||||
|
badge.textContent = upstreamProtocolBadgeText(backend, null).label;
|
||||||
|
badge.title = t("vhBackendProtocolUnobserved");
|
||||||
|
policy.append(badge);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (policy.children.length) box.append(policy);
|
if (policy.children.length) box.append(policy);
|
||||||
return box;
|
return box;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ pub fn minify(body: &[u8]) -> Result<Vec<u8>> {
|
|||||||
last_char = Some(' ');
|
last_char = Some(' ');
|
||||||
}
|
}
|
||||||
pending_space = false;
|
pending_space = false;
|
||||||
if matches!(ch, '{' | '}' | ':' | ';' | ',' | '>') && last_char == Some(' ') {
|
// A space before ':' may be a descendant combinator before a pseudo-class.
|
||||||
|
// E.g. `.card :is(.child)` must not become `.card:is(.child)`.
|
||||||
|
if matches!(ch, '{' | '}' | ';' | ',' | '>') && last_char == Some(' ') {
|
||||||
out.pop();
|
out.pop();
|
||||||
}
|
}
|
||||||
out.push(ch);
|
out.push(ch);
|
||||||
@@ -71,5 +73,6 @@ fn needs_space(previous: Option<char>, next: Option<char>) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
!matches!(previous, '{' | '}' | ':' | ';' | ',' | '>')
|
!matches!(previous, '{' | '}' | ':' | ';' | ',' | '>')
|
||||||
&& !matches!(next, '{' | '}' | ':' | ';' | ',' | '>')
|
// A ':' can introduce a pseudo-class after a descendant combinator.
|
||||||
|
&& !matches!(next, '{' | '}' | ';' | ',' | '>')
|
||||||
}
|
}
|
||||||
@@ -405,4 +405,29 @@ mod tests {
|
|||||||
let received = response.into_body().collect().await.unwrap().to_bytes();
|
let received = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
assert_eq!(&received[..], encoded.as_slice());
|
assert_eq!(&received[..], encoded.as_slice());
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn css_minifier_preserves_descendant_pseudoclasses() {
|
||||||
|
let source = r#"html:is([data-corner-style="square"], [data-corner-style="rounded"]) :is(.stat, .card) { border-radius: var(--rp-radius-surface) !important; }
|
||||||
|
.menu :hover { color: red; }
|
||||||
|
.menu :not(.hidden) { display: block; }
|
||||||
|
.menu:hover { color: blue; }
|
||||||
|
body { color: red; }"#;
|
||||||
|
let result = css::minify(source.as_bytes()).expect("minify CSS");
|
||||||
|
let output = std::str::from_utf8(&result).expect("UTF-8 CSS");
|
||||||
|
assert!(output.contains("html:is([data-corner-style=\"square\"],[data-corner-style=\"rounded\"]) :is(.stat,.card)"), "{output}");
|
||||||
|
assert!(output.contains(".menu :hover{"), "{output}");
|
||||||
|
assert!(output.contains(".menu :not(.hidden){"), "{output}");
|
||||||
|
assert!(output.contains(".menu:hover{"), "{output}");
|
||||||
|
assert!(output.contains("body{color:red;}"), "{output}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn css_minifier_preserves_dashboard_corner_selector() {
|
||||||
|
let input = include_str!("../admin/web/style.css");
|
||||||
|
let result = css::minify(input.as_bytes()).expect("minify panel stylesheet");
|
||||||
|
let output = std::str::from_utf8(&result).expect("UTF-8 CSS");
|
||||||
|
assert!(output.contains("html:is([data-corner-style=\"square\"],[data-corner-style=\"subtle\"],[data-corner-style=\"rounded\"],[data-corner-style=\"extra\"]) :is("), "lost descendant pseudo-class combinator");
|
||||||
|
assert!(output.contains(".card,.stat,dialog,.login-card,"), "lost card selector list");
|
||||||
|
assert!(output.contains("border-radius:var(--rp-radius-surface)!important"), "lost radius declaration");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Executable
+148
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regression for live Network diagnostic DOM stability beside Process.
|
||||||
|
|
||||||
|
Runs the actual renderer and the shipped dashboard CSS with mocked sample data
|
||||||
|
in Chromium touch emulation. This does NOT certify Safari/iOS 27 behavior.
|
||||||
|
"""
|
||||||
|
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'
|
||||||
|
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
|
||||||
|
source = (WEB / 'app/runtime.js').read_text()
|
||||||
|
start = source.index('function renderNetworkInterfaceCharts(interfaces, appendSample) {')
|
||||||
|
start = source.rfind('let latestNetworkInterfaceSnapshot', 0, start)
|
||||||
|
assert start >= 0
|
||||||
|
renderer = source[start:source.index('function renderDiagnostics(', start)]
|
||||||
|
helper = source[source.index('function setChartText('):start]
|
||||||
|
html = ('<!doctype html><html data-theme="ultra_dark" 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:1020px">Overview metrics</div>'
|
||||||
|
'<section id="panel-overview"><div class="stats-grid dashboard-overview-grid is-aligned" '
|
||||||
|
'id="dashboard-overview-grid">' + str(process) + str(network) +
|
||||||
|
'</div></section><div style="height:160px"></div></body></html>')
|
||||||
|
stubs = '''
|
||||||
|
const networkInterfaceSamples = new Map();
|
||||||
|
const hiddenNetworkInterfaces = new Set();
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const t = s => ({networkInterfacesVisibleCount:'{visible} / {total}',
|
||||||
|
networkInterfaceRate:'Rate', networkInterfacesHistory:'Traffic',
|
||||||
|
hideMetric:'Hide', showMetric:'Show',hiddenInterfaces:'Hidden',
|
||||||
|
networkNoInterfaces:'No interfaces',networkNoVisibleInterfaces:'None visible'})[s] || s;
|
||||||
|
const fmtBytes = n => `${n} B`;
|
||||||
|
const el = (tag, text, cls) => {const node=document.createElement(tag);
|
||||||
|
if(text !== undefined) node.textContent=text;if(cls) node.className=cls;return node;};
|
||||||
|
const action = (label, fn) => {const node=el('button',label);node.addEventListener('click',fn);return node;};
|
||||||
|
const svgNode = (tag, attrs={}) => {const node=document.createElementNS('http://www.w3.org/2000/svg',tag);
|
||||||
|
Object.entries(attrs).forEach(([key,value])=>node.setAttribute(key,value));return node;};
|
||||||
|
const sparklinePoints = (values,width=300,height=60,padding=5,peakOverride=null) => {
|
||||||
|
const peak=Math.max(1,peakOverride??Math.max(0,...values));
|
||||||
|
return values.map((value,index)=>`${index*width/Math.max(1,values.length-1)},${height-padding-Math.max(0,value)/peak*(height-padding*2)}`).join(' ');
|
||||||
|
};
|
||||||
|
const saveHiddenNetworkInterfaces = () => {};
|
||||||
|
'''
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True, executable_path=shutil.which('chromium'), args=['--no-sandbox'])
|
||||||
|
context = browser.new_context(viewport={'width':393,'height':844},
|
||||||
|
device_scale_factor=3, is_mobile=True, has_touch=True)
|
||||||
|
page = context.new_page()
|
||||||
|
page.set_content(html, wait_until='domcontentloaded')
|
||||||
|
page.add_script_tag(content=stubs + helper + renderer)
|
||||||
|
baseline = page.evaluate('''() => {
|
||||||
|
const items = [{name:'ens18',rx_bytes_per_second:100,tx_bytes_per_second:20},
|
||||||
|
{name:'lo',rx_bytes_per_second:3,tx_bytes_per_second:3}];
|
||||||
|
renderNetworkInterfaceCharts(items,true);
|
||||||
|
const panel = document.querySelector('[data-dashboard-widget="process_diagnostics"]');
|
||||||
|
const network = document.querySelector('[data-dashboard-widget="network_diagnostics"]');
|
||||||
|
const target = document.getElementById('network-interface-charts');
|
||||||
|
window.testNodes = {first:target.firstElementChild, svg:target.querySelector('svg'),
|
||||||
|
path:target.querySelector('.network-rx-line')};
|
||||||
|
window.testInterfaces = items;
|
||||||
|
window.scrollTo(0,Math.round(network.getBoundingClientRect().top+window.scrollY+180));
|
||||||
|
return {processHeight:panel.getBoundingClientRect().height, y:scrollY,
|
||||||
|
network:network.getBoundingClientRect().top, count:target.children.length,
|
||||||
|
scrollHeight:document.documentElement.scrollHeight};
|
||||||
|
}''')
|
||||||
|
assert baseline['y'] > 0 and baseline['count'] == 2, baseline
|
||||||
|
count = page.evaluate('''() => {
|
||||||
|
const target = document.getElementById('network-interface-charts');
|
||||||
|
let mutations=0;
|
||||||
|
const obs = new MutationObserver(list=>mutations+=list.filter(item=>item.type==='childList').length);
|
||||||
|
obs.observe(target,{childList:true,subtree:true});
|
||||||
|
const beforeY=scrollY;
|
||||||
|
const beforeHeight=document.documentElement.scrollHeight;
|
||||||
|
const beforeProcess=document.querySelector('[data-dashboard-widget="process_diagnostics"]').getBoundingClientRect().height;
|
||||||
|
for(let i=0;i<45;i++){
|
||||||
|
renderNetworkInterfaceCharts([{name:'ens18',rx_bytes_per_second:i*109,tx_bytes_per_second:12},
|
||||||
|
{name:'lo',rx_bytes_per_second:i*2,tx_bytes_per_second:3}],true);
|
||||||
|
if(target.firstElementChild!==window.testNodes.first ||
|
||||||
|
target.querySelector('svg')!==window.testNodes.svg ||
|
||||||
|
target.querySelector('.network-rx-line')!==window.testNodes.path) throw Error('Network DOM replaced');
|
||||||
|
}
|
||||||
|
obs.takeRecords().forEach(r=> {if(r.type==='childList')mutations++});obs.disconnect();
|
||||||
|
return {mutations, y:scrollY, beforeY, count:target.children.length,
|
||||||
|
scrollHeight:document.documentElement.scrollHeight, beforeHeight,
|
||||||
|
processHeight:document.querySelector('[data-dashboard-widget="process_diagnostics"]').getBoundingClientRect().height, beforeProcess,
|
||||||
|
changed:target.querySelector('.network-interface-rate').textContent.includes('4796')};
|
||||||
|
}''')
|
||||||
|
assert count['mutations'] == 0, count
|
||||||
|
assert abs(count['y'] - count['beforeY']) < 2, count
|
||||||
|
assert count['count'] == 2 and count['changed'], count
|
||||||
|
assert abs(count['scrollHeight'] - count['beforeHeight']) < 2, count
|
||||||
|
assert abs(count['processHeight'] - count['beforeProcess']) < 2, count
|
||||||
|
print('PASS: 45 updates preserve SVG nodes, Process height, document height and scroll')
|
||||||
|
|
||||||
|
# Interface charts advance at every Overview live tick like the aggregate
|
||||||
|
# network chart, repeating the last observed rate until system-info updates.
|
||||||
|
# A system-info-only refresh or Hide/Show redraw never adds history points.
|
||||||
|
history = page.evaluate('''() => {
|
||||||
|
networkInterfaceSamples.clear();
|
||||||
|
const one=[{name:'ens18',rx_bytes_per_second:100,tx_bytes_per_second:200}];
|
||||||
|
renderNetworkInterfaceCharts(one,true);
|
||||||
|
const before=networkInterfaceSamples.get('ens18').rx.length;
|
||||||
|
for(let i=0;i<45;i++) renderNetworkInterfaceCharts(one,true);
|
||||||
|
const live=networkInterfaceSamples.get('ens18').rx.length;
|
||||||
|
const measured=[{name:'ens18',rx_bytes_per_second:500,tx_bytes_per_second:250}];
|
||||||
|
renderNetworkInterfaceCharts(measured,false); // only new system-info data
|
||||||
|
const afterSystem=networkInterfaceSamples.get('ens18').rx.length;
|
||||||
|
const label=document.querySelector('.network-interface-rate').textContent;
|
||||||
|
renderNetworkInterfaceCharts(measured,true); // next live tick
|
||||||
|
const afterLive=networkInterfaceSamples.get('ens18').rx.length;
|
||||||
|
const newest=networkInterfaceSamples.get('ens18').rx.at(-1);
|
||||||
|
return {before,live,afterSystem,afterLive,label,newest};
|
||||||
|
}''')
|
||||||
|
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
|
||||||
|
refresh = (WEB / 'app/refresh.js').read_text()
|
||||||
|
assert 'interval: () => options.system_info_seconds' in refresh
|
||||||
|
assert 'renderDiagnostics(latestStatus, false, true);' in refresh
|
||||||
|
print('PASS: frequent interface chart history, configured system-info cadence, no extra samples on system-only redraw')
|
||||||
|
page.evaluate('''() => renderNetworkInterfaceCharts([
|
||||||
|
{name:'ens18',rx_bytes_per_second:100,tx_bytes_per_second:200},
|
||||||
|
{name:'lo',rx_bytes_per_second:3,tx_bytes_per_second:3}],false)''')
|
||||||
|
|
||||||
|
# The Hide / Show controls retain the newest sample on click, not the array
|
||||||
|
# captured when a chart node was created.
|
||||||
|
page.locator('.network-interface-hide').first.click()
|
||||||
|
assert page.locator('.network-interface-row').count() == 1
|
||||||
|
assert page.locator('.network-interface-hidden button').count() == 1
|
||||||
|
page.locator('.network-interface-hidden button').click()
|
||||||
|
assert page.locator('.network-interface-row').count() == 2
|
||||||
|
assert page.locator('.network-interface-hidden').count() == 0
|
||||||
|
page.evaluate('''() => renderNetworkInterfaceCharts([{name:'eth0',rx_bytes_per_second:9,tx_bytes_per_second:11}],true)''')
|
||||||
|
assert page.locator('.network-interface-row').count() == 1
|
||||||
|
assert page.locator('.network-interface-row').first.get_attribute('data-interface-name') == 'eth0'
|
||||||
|
page.evaluate('''() => renderNetworkInterfaceCharts([],true)''')
|
||||||
|
assert page.locator('.network-interface-empty').count() == 1
|
||||||
|
print('PASS: Hide/Show actions, hotplug/removal and empty state still work')
|
||||||
|
browser.close()
|
||||||
@@ -23,6 +23,16 @@ assert 'if (ordered.some((node, i) => node !== previous[i])) grid.append(...orde
|
|||||||
assert html.count('data-dashboard-widget=') >= 62
|
assert html.count('data-dashboard-widget=') >= 62
|
||||||
assert 'function renderUpstreamProtocolBadges(status)' in render
|
assert 'function renderUpstreamProtocolBadges(status)' in render
|
||||||
assert 'const VHOST_PROTOCOL_FRESH_MS = 5 * 60 * 1000' in render
|
assert 'const VHOST_PROTOCOL_FRESH_MS = 5 * 60 * 1000' in render
|
||||||
|
# Protocol badges live in the existing Vhost policy-pill group, not above the
|
||||||
|
# live traffic metrics. The class and observed-color state remain unchanged.
|
||||||
|
policy_start=render.index('const policy = el("div", undefined, "vhost-policy-pills");')
|
||||||
|
policy_end=render.index('if (policy.children.length) box.append(policy);', policy_start)
|
||||||
|
policy_block=render[policy_start:policy_end]
|
||||||
|
assert 'policy.append(badge);' in policy_block
|
||||||
|
assert 'vhost-backend-protocol-badge' in policy_block
|
||||||
|
assert 'box.append(badges)' not in render
|
||||||
|
assert '.vhost-backend-protocol-badge[data-observed="true"]' in css
|
||||||
|
assert 'color: var(--green)' in css[css.index('.vhost-backend-protocol-badge[data-observed="true"]'):]
|
||||||
assert 'self.inner.observe_protocol(value.version())' in client
|
assert 'self.inner.observe_protocol(value.version())' in client
|
||||||
assert '"upstream_protocol_observed"' in pool
|
assert '"upstream_protocol_observed"' in pool
|
||||||
print('PASS: mobile grid, shape, stable reorder and observed backend protocol source contracts')
|
print('PASS: mobile grid, shape, stable reorder and observed backend protocol source contracts')
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render real Vhost cards with mock config/status to verify badge placement/color.
|
||||||
|
|
||||||
|
Headless Chromium fixture only: does not verify authenticated Rust runtime/ALPN.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
WEB = ROOT / 'src/admin/web'
|
||||||
|
source = (WEB / 'vhosts/render.js').read_text()
|
||||||
|
start = source.index('const VHOST_PROTOCOL_FRESH_MS = ')
|
||||||
|
end = source.index('function backendTrafficRate(', start)
|
||||||
|
renderer = source[start:end]
|
||||||
|
css = (WEB / 'style.css').read_text()
|
||||||
|
html = (f'<!doctype html><html data-theme="ultra_dark" data-corner-style="rounded">'
|
||||||
|
f'<head><meta name="viewport" content="width=device-width,initial-scale=1">'
|
||||||
|
f'<style>{css}</style></head><body><section id="panel-vhosts">'
|
||||||
|
f'<div id="backend-diagnostics-card"></div><input id="vhost-search">'
|
||||||
|
f'<div id="vhost-list"></div><span id="vhost-editing"></span>'
|
||||||
|
f'</section></body></html>')
|
||||||
|
stubs = r'''
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const t = key => key;
|
||||||
|
const locale = () => 'en-US';
|
||||||
|
const el = (tag, value, cls) => {
|
||||||
|
const node=document.createElement(tag);
|
||||||
|
if (value !== undefined) node.textContent=value;
|
||||||
|
if (cls) node.className=cls;
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
const text = (tag, key, cls) => el(tag, t(key), cls);
|
||||||
|
const action = (name, fn) => {
|
||||||
|
const btn=el('button', name, 'quiet');
|
||||||
|
btn.addEventListener('click', fn);
|
||||||
|
return btn;
|
||||||
|
};
|
||||||
|
const button = (name, fn, cls) => {
|
||||||
|
const btn=action(name, fn);
|
||||||
|
if (cls) btn.className=cls;
|
||||||
|
return btn;
|
||||||
|
};
|
||||||
|
const initialized=true;
|
||||||
|
let editing=null, latestStatus=null;
|
||||||
|
const vhostListPrefs={sort:'id',descending:false};
|
||||||
|
const envelope={config:{listeners:{endpoints:[]},vhosts:[
|
||||||
|
{id:'proxy-test',enabled:true,frontend:{hosts:['proxy.example'],listener_ids:[]},
|
||||||
|
source:{type:'reverse_proxy'},force_ssl:true,websockets:true,
|
||||||
|
backends:[{id:'origin1',protocol:'auto'}, {id:'origin2',protocol:'http1'}]},
|
||||||
|
{id:'static-test',enabled:true,frontend:{hosts:['static.example'],listener_ids:[]},
|
||||||
|
source:{type:'web_server',web_service_id:'public'},
|
||||||
|
backends:[{id:'unused',protocol:'auto'}]},
|
||||||
|
]}};
|
||||||
|
'''
|
||||||
|
with sync_playwright() as playwright:
|
||||||
|
browser = playwright.chromium.launch(headless=True, executable_path=shutil.which('chromium'), args=['--no-sandbox'])
|
||||||
|
for width in (393, 1024):
|
||||||
|
page = browser.new_page(viewport={'width': width, 'height': 844})
|
||||||
|
page.set_content(html, wait_until='domcontentloaded')
|
||||||
|
page.add_script_tag(content=stubs + renderer + '\nrender();')
|
||||||
|
initial = page.evaluate('''() => {
|
||||||
|
const card=document.querySelector('[data-vhost-card="proxy-test"]');
|
||||||
|
const badge=card.querySelector('.vhost-backend-protocol-badge');
|
||||||
|
const group=card.querySelector('.vhost-policy-pills');
|
||||||
|
const staticCard=document.querySelector('[data-vhost-card="static-test"]');
|
||||||
|
return {badges:group.querySelectorAll('.vhost-backend-protocol-badge').length,
|
||||||
|
otherPills:group.querySelectorAll('.pill').length,
|
||||||
|
parent:badge.parentElement===group,
|
||||||
|
lastChild:card.lastElementChild===group,
|
||||||
|
separateRows:card.querySelectorAll('.vhost-backend-protocols').length,
|
||||||
|
staticBadges:staticCard.querySelectorAll('.vhost-backend-protocol-badge').length,
|
||||||
|
initialText:badge.textContent,
|
||||||
|
scroll:document.documentElement.scrollWidth,screen:innerWidth};
|
||||||
|
}''')
|
||||||
|
assert initial['badges'] == 2 and initial['otherPills'] == 2, initial
|
||||||
|
assert initial['parent'] and initial['lastChild'], initial
|
||||||
|
assert initial['separateRows'] == 0 and initial['staticBadges'] == 0, initial
|
||||||
|
assert initial['initialText'] == 'origin1 · AUTO → ?', initial
|
||||||
|
assert initial['scroll'] <= initial['screen'] + 2, initial
|
||||||
|
appearance = page.evaluate('''() => {
|
||||||
|
const badge=document.querySelector('[data-upstream-protocol-backend="origin1"]');
|
||||||
|
const expected=token=>{const n=document.createElement('span');n.style.color=`var(${token})`;
|
||||||
|
document.body.append(n);const c=getComputedStyle(n).color;n.remove();return c;};
|
||||||
|
const unknown=getComputedStyle(badge).color;
|
||||||
|
renderUpstreamProtocolBadges({backend_pools:[{id:'proxy-test',members:[
|
||||||
|
{id:'origin1',upstream_protocol_observed:'h2',upstream_protocol_last_seen_ms:Date.now()-1000},
|
||||||
|
{id:'origin2',upstream_protocol_observed:'h1',upstream_protocol_last_seen_ms:Date.now()-1000}
|
||||||
|
]}]});
|
||||||
|
const h2={text:badge.textContent,observed:badge.dataset.observed,
|
||||||
|
color:getComputedStyle(badge).color,expected:expected('--green')};
|
||||||
|
return {unknown, expectedUnknown:expected('--muted'), h2};
|
||||||
|
}''')
|
||||||
|
assert appearance['unknown'] == appearance['expectedUnknown'], (width, appearance)
|
||||||
|
assert appearance['h2']['color'] == appearance['h2']['expected'], (width, appearance)
|
||||||
|
assert appearance['h2']['observed'] == 'true' and appearance['h2']['text'] == 'origin1 · AUTO → H2', appearance
|
||||||
|
print(f'PASS: {width}px Vhost protocol badges share policy group and preserve green/neutral states')
|
||||||
|
page.close()
|
||||||
|
browser.close()
|
||||||
Reference in new issue
Block a user