V0.32.6 #1

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

No files matched your search

+13 -2
View File
@@ -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.
+10 -1
View File
@@ -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.
+8
View File
@@ -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
View File
@@ -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
+38
View File
@@ -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
View File
@@ -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",
+96 -37
View File
@@ -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;
+2 -9
View File
@@ -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;
+12 -14
View File
@@ -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;
}); });
+5 -2
View File
@@ -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, '{' | '}' | ';' | ',' | '>')
} }
+25
View File
@@ -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");
}
} }
+148
View File
@@ -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()
+10
View File
@@ -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
View File
@@ -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()