v0.32.6_fix1

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-14 08:19:09 +02:00
1 parent a3d183bd4b
commit e3de084149
34 files changed
+559 -85

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.32.5.
compatibility. Current source version: 0.32.6.
## Delivery
+13
View File
@@ -3,6 +3,19 @@
> Operator-facing release history. Historical engineering notes are kept in
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.32.6
- Restore two-column small metric cards on narrow mobile Overview grids, ensure
the first render uses the same responsive layout and avoid DOM operations
that can disrupt scrolling when preferences refresh.
- Extend account-selected corner presets to vhost cards, backend badges,
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
show fresh observations next to configured modes on Vhost cards. Stale or
unobserved modes show `?`, never a guessed Auto selection. No active probes.
- Add browser, JavaScript and Rust regression coverage. Rust compilation,
physical iOS Safari and live HTTP/2 ALPN remain acceptance gates.
## 0.32.5
- Fix the administration panel startup `ReferenceError: Cannot access 'i18n' before initialization` in the new DNS/DDNS ACL editor. Define its localization helper before creating the DNS hint; preserve all ACL settings and translations. Add a JavaScript startup regression test to the release checks.
Generated
+1 -1
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.32.5"
version = "0.32.6"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.32.5"
version = "0.32.6"
edition = "2021"
rust-version = "1.99"
description = "RustProxy - independent reverse proxy with local administration"
+22 -2
View File
@@ -1,4 +1,4 @@
# RustProxy 0.32.5
# RustProxy 0.32.6
**0.31.0 panel/cache additions:** browser-negotiated Zstd/Brotli/gzip for HTML, JS and CSS (Settings → Administration); optional HTTP BAN/PURGE restricted to explicit IP/CIDR allowlist (Settings → Cache, disabled by default); System information Refresh button beside the API badge. See [Settings](docs/SETTINGS.md) and [Cache](docs/CACHE.md).
@@ -7,7 +7,27 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
**0.31.9 Certificate API management: inspect and edit credential settings without exposing/rotating the current token, or regenerate one existing token with one-time display. Rust/Cargo and live testing required.**
## What changed in 0.32.5
## What changed in 0.32.6
- **Mobile Overview:** display two metric tiles per row on phone-width canvases,
including 320-430px layouts and the initial pre-script paint. Keep horizontal
overflow disabled, allow vertical touch scrolling and avoid unnecessary DOM
reparenting/scroll anchoring during Dashboard preference refreshes.
- **Global corner presets:** apply the selected Appearance shape consistently to
vhost cards, status badges, entity cards and secondary panels as well as the
existing metric and content cards. Keep the square-style draft dock unchanged.
- **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
proxy request for each origin. `AUTO → ?` means there is no response from the
past five minutes; never guess a negotiation result or run an extra probe.
The authenticated status payload includes last observation timestamp and
counters per upstream client; the active connection may vary across requests.
- **Tests:** Chromium simulated-mobile layout/scroll and shape presets,
JavaScript live-status protocol badges and Rust response-version recorder test.
The actual Rust build, iOS Safari interaction and end-to-end negotiated ALPN
verification remain pending on a deployment host.
## Previously in 0.32.5
- **ACL editor startup hotfix:** fix the JavaScript `i18n` initialization order
that could prevent the entire administration UI from loading. This does not
+14
View File
@@ -1,5 +1,19 @@
# RustProxy roadmap
## 0.32.6 - Mobile Overview and upstream protocol visibility (runtime acceptance pending)
- [x] Reinstate two small Overview cards per row across phone-size widths.
- [x] Apply global corner styles to additional vhost/entity panel surfaces.
- [x] Avoid reparenting already-ordered Overview nodes; retain natural touch
scroll and stable values/row boundaries during live updates.
- [x] Include last HTTP protocol observed from real proxy upstream responses
in the authenticated status and each Vhost's per-backend badge. Show unknown
when no fresh observation exists instead of guessing Auto's ALPN outcome.
- [x] Add simulated mobile Chromium and mocked status UI regressions plus a
Rust unit regression for protocol-version observation.
- [ ] Run Cargo checks/tests, real iPhone Safari scrolling and live upstream
TLS ALPN H1/H2 behavior before production approval.
## 0.32.5 - Dynamic DNS client ACLs (runtime acceptance pending)
- [x] Preserve CIDR/IP rules and introduce allow-only FQDNs for client ACLs.
+33 -30
View File
@@ -1,18 +1,18 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
ad0382153576411fb19861072635c02bdf33de740d6f122d8f27bd9c52ce112a AGENTS.md
4a58695f0ef64344228cb43caf2a78aeba724cf0cc573a6a8745da847681aeed CHANGELOG.md
2f1de87bae58761fc8690ec7225520e3a16213c77cbbb5981e34f12a95810668 Cargo.lock
8c76ffaf4adba93df26e30f3dd163e0e5e201d069d1ae69c071ca42d7fc7264e Cargo.toml
a402ee07826dfab1b1d63b98c4a9f75809f056b908c92abacf0bba816d7ad133 AGENTS.md
4bc02b731361f3859196a41bdf27f477c67287b1ded6fa2b297ea9c1766a7733 CHANGELOG.md
934ec0e7f8a307b00319606a9c5d7e91dac6449a23dd1f3abcefe4304eb4de06 Cargo.lock
8a7606650ce03f1faf36c893a17ccf1c9e8da4259e0248ac2c9bf030a64872d1 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
4ee3c626d1e5da5079fe65c6343f085cbb8bb53c2f2fa97e354f9633e8d415b3 README.md
fc03a8d19055642313f0b34bcd22e1cce17efc4d91be7fe7e4819e33b91c531d ROADMAP.md
a26af8c8548c4ab04566d41eab41fc4715fc8bfc05bf281dfb10e808795ec20c README.md
345ac3de56d04e41a8ad577654a51f0b81ddbbba3685378476f566b5732a1ddb ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
4d9baf35c70d9f5cfb10f8bd4caab543772fdda1e979ff74c17f1e3ebc8e3cd0 dev.sh
ab6ebc1a886b9ed7d29a44c1676641a6b5d2a94a6b027da8f75e8e82ac02ec67 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -61,9 +61,9 @@ d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.m
ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
25a06e7ddbb95d286e849b2fc5ba4adeedbddba0bad948e55bd91d962fd606de docs/SETTINGS.md
5e615c119848cc474f4b005dbe3b58a7ed8f29375ff409282285c39b0bdc196a docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
97299efb6288ee29493eb7df243055d1ff76e8d0faf04bd195e184bce2e8bb67 docs/TEST_REPORT.md
b5a7bb5bf081098624fba570760375792f0612c6022f80d6e462101912b442a7 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
@@ -95,8 +95,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
1c3bdb963bc4fa4eef48439721d7fee54375458f3a1d44f9e31c0c4ed7aca4f0 langs/en_US.json
70d8ad7d503118356721ff7953b829049da793934dd73f4abbbbd7c7f0b4fc63 langs/pl_pl.json
fc6695db8c2a9896dc97642ef70d56de86657521d0f4e1f2f57b99ac97c4f2dc langs/en_US.json
a65d977f8380113d3ec7db5d51bffe50b87f5d93c5b139371f2351093b547216 langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -156,7 +156,7 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
7de7da489a367658b700f00776fa43604d3554500b5fbef4da2764e402e68afa src/admin/logging_api.rs
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
c1b8ab15455a8199fbe011c6a7dd203572c4d4772c107d30531a6fb38a92ace2 src/admin/openapi.json
5efda93041025b9a347e7d95af8af642c5cd3c5a2dd2d3ee5c6df2f9101bdc42 src/admin/openapi.json
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
@@ -182,7 +182,7 @@ dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/
fc5b025bad26684c8cf0a9917a7a24fdac3aa79414001335f29fb7360c3fa703 src/admin/web/app/overview-extra-metrics.js
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js
79d31e2002f8cf334f87285f6f2e6dbf69f3647de784ae26efadb92c126bc7a0 src/admin/web/app/overview-wide-metrics.js
947c53fe10d97f5d3aa64ae3b819d7bcdb2a83a792747826acbc3750e313014f src/admin/web/app/preferences.js
a73d4efc9cf2f6bf7117f6f6dcaaea106e03c328451abbe4c10281179750899d src/admin/web/app/preferences.js
6a544baaf40763b4b612e53845b3bc33396b9bb6518565fd6b7c128c278b8d5d src/admin/web/app/refresh.js
41f635a510d6f08b37484f0e260929926b93a02a792bdfa05b31ed7ebd682fef src/admin/web/app/runtime.js
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
@@ -222,7 +222,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
e3075af59644e42fa5735574d8e9caa1bcebfe8de184b78aff9f4026b91915f1 src/admin/web/streams.js
065a377512c146254a19ce03c299f9d07f54e4f2cb0bccae965ac63e3a96063f src/admin/web/style.css
691ebee8f87f28f574a8dc5f0020922bd04e06f6d51e72fccd3abb6e68be7bf1 src/admin/web/style.css
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
@@ -230,7 +230,7 @@ b31782c61da3623389b57fdbba5bd41d2287d45fc172e9c14360a9dd7accf061 src/admin/web/
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
dfab53c9b9d4c633db639c37e99a46169aa86f7a28e6731e305c27ac8587a7ef src/admin/web/vhosts/render.js
9b565141991a02f83b46d535aa4f4d4430631270f8c3cabafd373e54b0e7317b src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
355684980d1caf8918d1773ce17934b51e3a85a60f38491217d10035bf68ce9f src/admin/web/web-server.js
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
@@ -250,7 +250,7 @@ a1e5d43f5417d71f3974812cb539c0932f206e2290090917f067494f3587fc72 src/balance/le
4d3aa415af2fbb42a4e19f2ca10be8b1d1ad42f366a013cd9c33806abc1fbea2 src/balance/model.rs
41c3c020a60c372dbf965bd277c330dd9cf2ab079f6608b472adf72a656f4cb3 src/balance/pool_core.rs
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
8f73e8fe7f57d8d720a8ce59ae94fc55ab03f6da59d50bfe6ae929815f4f3bfa src/balance/pool_view.rs
8abb6048303ee8c022546a528d5693167766e7450efd8e20f37ec6fd0fa9ef5e src/balance/pool_view.rs
656a6ab3c9855f92b7e3e0dd6de1bcc408da003db8bcfcda52b71c5234c56693 src/balance/tests.rs
5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
@@ -451,8 +451,8 @@ a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/
6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
9c897664a8b3b288b761b84a724b212c3f6a82b856ceebf3b4ecd6eab1846a35 src/upstream.rs
6d7a1798911add0905e316d5ef682b0977a42b8bf9e4c1976733aaf393a027bd src/upstream/client.rs
4aa2420a92d33c50f7027f57ac4eccdc050bdb7e9ec8fa6e0eaa809228b71cd8 src/upstream/client_tests.rs
9f05552d0cdc523e37bc73214e35e472f271250fd4f95ddbbefda4c39f96ebd9 src/upstream/client.rs
ad29f3fc15c9b627b5d752af5adc9f99ee1364fec2df63f1e4509cb223988efb src/upstream/client_tests.rs
75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs
8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs
a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs
@@ -502,54 +502,57 @@ c9d0bef4b8ed7a2f49b0fad3e116cace62d3e6e1ca31e02a3073d1fdde5157d8 tools/checks/a
f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_ui_test.mjs
f34f0f78edc4b6c6226006337090c72d1f8e57ddd77939b37cf00538a05abfa8 tools/checks/backend_dashboard_0323_ui_test.mjs
45cd21192248ffe6f66c20bab03da1a5ce299f040883764d09a640859e19924d tools/checks/backend_health_0300_ui_test.mjs
48359559ca415890acbef0f2717bb4b7acacdc26b6cf5f78135a9d9637f83547 tools/checks/certificate_api_0319.py
a135c9456143975279f18233caab44b8c64f5025ef4ceeb1f2ad22090b588019 tools/checks/backend_protocol_0326_ui_test.mjs
a1fb118e3a32819689cb607d574b0060ca29ff1f6a68c505231a4e88ea93b5de tools/checks/certificate_api_0319.py
526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs
d48c5080b34d2df18d0f3bf16cf7b58a36efbe4f6f2b96724581dea2b1a91544 tools/checks/check_source.py
71cf48ba645ae2b7a95c73faa362d12942553e37d79680b796edf29c1484d9eb tools/checks/check_source.py
dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs
7508f232a9e48049002dd4c9f34f9737bc92d304c51219f50430578dbc721855 tools/checks/dashboard_inline_layout_0323_ui_test.mjs
dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/dashboard_layout_editor_0323_ui_test.mjs
259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
f6a6d12275bc8ba627b4616a546d075511094bb064cd9058231cc86ca489743e tools/checks/dependency_upgrade_0312.py
7b119f62f11d65600b19010d600682955afe269d798df15676b397f70ffaa37d tools/checks/dependency_upgrade_0312.py
38e35bed4faf984932b39bb5a00819fadc664387e8050a803ff4b5348b31369f tools/checks/docker_startup_0318_ui_test.mjs
3396e5a510787e70e2ad8d1fad90356af461a88fd84429525d56fa82113da0f3 tools/checks/docs_check.py
3fc765a7c2723ec783f269cfebc29ad2c09a90f33ea4af58285f883a026c4fa2 tools/checks/dynamic_acl_0325.py
6ca752ed5e767dc1eed4e1d7b60270391e1dd0bd5c887de8fd0f9dcaf219d7bd tools/checks/docs_check.py
48f0901a709f415f518d2a5eeb779180ac18c879e18f1ad7af748b1be9ac92e1 tools/checks/dynamic_acl_0325.py
9c77764561b6ebfeecbc228f43ea29a0dd9f44121ad773c2e4299b92758e45c2 tools/checks/dynamic_acl_startup_0325_ui_test.mjs
5bf26d9c883704e5ac85670bbcb7fb0bbad5d01779ab50407ff4fd5f3022a9cb tools/checks/file_logs_tabs_0315_ui_test.mjs
72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs
1b5ead59bb2ea0e1ff32bf085f791018149cb9cc29b071fae4fb98ea80010824 tools/checks/h3_navigation_0316.py
c081925edadee3ffb21867765fcd08d6a536093999ee99c5590b1d5980f8e4b3 tools/checks/h3_navigation_0316.py
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py
bfb48f57a2e87e4b03fdb21115ecdab12187fc4d1ead9db42f5416b061d21596 tools/checks/mobile_protocol_0326.py
c7ddc69c8bfe5a675ae48d02c76c85d0d0490bc1da1833a5f99dc459d7fe9639 tools/checks/mobile_protocol_0326_browser.py
af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs
0387a9e29244b58cf37a6673a88ae2b7fea3aa965c032423f3783a84eab4ba05 tools/checks/overview_0321_layout_regression.mjs
53917376134d73fff32a68239b513c4eedf98bb65224c1c41c0e5cfc8fcb119b tools/checks/overview_extra_metrics_0323_ui_test.mjs
bc047e51ba17d7062a8a46a273c871c9b0b0ace16ef738a86e1517d0792d4c0a tools/checks/overview_grid_spacing_0323_browser.py
48dd1e13b83dc897c735028c93e17200cf4ed9ce175475dc5641ec3236569c38 tools/checks/overview_grid_spacing_0323_browser.py
d530cb9b783d6e7ea3afd400a35a64865d16e11800bbf8910cb7edb9648f5510 tools/checks/overview_gutters_0324_browser.py
b565aef4594265350240c24067a554f91fd605d0e920c03eef0b5b62c3934aad tools/checks/overview_registry_0323_ui_test.mjs
f450e97dccf75c24e228e696f21ed4ae826a823ea4edde2429894a6b6b36eaab tools/checks/overview_uniform_grid_0323_ui_test.mjs
a1e68daf3a72b65545b0f5346094cbc0357c80d88504603090b776ef770f08de tools/checks/overview_wide_masonry_0323_ui_test.mjs
1ac812929c2a8c0699ae3bdebd0d7e05b5bcaf86c87533bb669b5ba7955e3245 tools/checks/panel_cache_http_0310_ui_test.mjs
eea7cdc9c7d8dd0f5fb079cfff657a6feadfc0b856f63265bfbd8257f1beca19 tools/checks/preflight.py
200e86cb2598bd3b422187c6421d10017f0b39afb137aaf7614858e1b3924592 tools/checks/preserve_host_0324.py
cdd59fdc84eea535584a52143e0c69285c76dfcc6d02587cd707aa4a5f220ca3 tools/checks/preserve_host_0324.py
3352941648add6d8a8044c0b5cad6bf264546d38c884a5beed64cd95addd850c tools/checks/preserve_host_0324_ui_test.mjs
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
c11583dad0899f8b945cae6e2f6cb0bdb55f5526bd5f42c248ac41badec83679 tools/checks/release_0317.py
b84b66a6f8f63cd0a354ef607b96ff60c77ef37f80f49ea9a9036b33147c0b6a tools/checks/release_0317_ui_test.mjs
56f8f6839df0e65a1c9a4b64bdd5d8ce7bd3dcd76280f85181d2a6c333308e13 tools/checks/release_0318.py
c122954d9ce7165dfef1ce3e71a786b7bc86e9076f66d9a87baf14d269392757 tools/checks/release_0318.py
e73afc62214e9092c180730f624a2039ac3fc850dcd865b2b117c11f7caaf88b tools/checks/release_0318_ui_test.mjs
62350e7fb5d4c4cfbe1b68ada1176c655922706dc2ebabc70913f5189ffa4155 tools/checks/release_smoke.py
9fe94e73c3156edb5015a1c03021c76ac96535e0dfc56b5efdab17fcd197955c tools/checks/release_smoke.py
31b31e976962e2c53e0f1f71bf67f7d2686d30cb26f638defa1bd7636cf1c1e8 tools/checks/rust_module_contracts.py
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
518b6d271931df62fc61d21688399d3c9b1986b58201061ac566667a16334823 tools/checks/security_logs_0315_ui_test.mjs
db8881e46622ca2dbab4c29df003020006c611cd7fe087878c3e9c930cdd9853 tools/checks/security_logs_0315_ui_test.mjs
011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py
37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py
bb862620104f925488e36904e258b7c934a097362ef879ecb275c64008cebb83 tools/checks/stream_0320_ui_test.mjs
19cb347c87b0fa462fc2f3f2f5bef62e93628334ab4b8a1776df417e47c91ef0 tools/checks/stream_0320_visibility_regression.mjs
8542f5c668155aa9f70de11e6fdaf64c5a80629a15ecc5f1d8c40f3d655f9b55 tools/checks/system_info_0301_ui_test.mjs
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py
760188db7aaae178c5f4570c1bcfac8b6cffe2e2e0522bb317331dc44515cbe5 tools/checks/upstream_protocols_0323.py
e650225258168eeb9623694c6684f9a5ace92ca94debdff2558b5c77f7c998bc tools/checks/upstream_protocols_0323.py
a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/version_contracts.py
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
ea98e8ce2bdc841814e486ea4c13f94e284b2c50a5887598b7a288f75a14deaf tools/checks/vhost_www_ui_test.mjs
+2
View File
@@ -101,6 +101,7 @@ if [[ "$mode" == "test" ]]; then
python3 tools/checks/upstream_protocols_0323.py
python3 tools/checks/preserve_host_0324.py
python3 tools/checks/dynamic_acl_0325.py
python3 tools/checks/mobile_protocol_0326.py
python3 tools/checks/check_source.py
python3 tools/checks/release_smoke.py
python3 tools/checks/memory_contracts.py
@@ -127,6 +128,7 @@ if [[ "$mode" == "test" ]]; then
node tools/checks/dashboard_inline_layout_0323_ui_test.mjs
node tools/checks/admin_themes_0323_ui_test.mjs
node tools/checks/appearance_ux_0323_ui_test.mjs
node tools/checks/backend_protocol_0326_ui_test.mjs
node tools/checks/overview_extra_metrics_0323_ui_test.mjs
node tools/checks/overview_wide_masonry_0323_ui_test.mjs
node tools/checks/overview_uniform_grid_0323_ui_test.mjs
+15
View File
@@ -6,6 +6,21 @@
The scale applies to the complete authenticated administration surface, including navigation, workspace content, dialogs, overlays and the draft dock. Text fields, selects, textareas, checkboxes and radios also receive a small proportional control-size adjustment on top of global zoom so field proportions track the selected scale more visibly. Button style is independent: **Normal** keeps the refreshed control sizing and **Compact** restores the smaller source-style sizing, including generated entity actions and tab buttons inside dialogs. Table/log and dashboard density are additional layout controls and do not change the whole-interface scale. The dashboard editor can hide/show and reorder the telemetry cards at the top of Overview; this changes presentation only, not metric collection. Stream card explanations appear on hover and keyboard focus instead of expanding the card list. Stream cards default hidden but, once switched on, remain present even when no Stream is active (neutral no-data state). The selected time format is shared by logs, backend/runtime diagnostics, audit/session timestamps, certificate views and configuration-history timestamps. Chart presets change colors, surfaces, grid and line presentation only; they do not change sampled data, and the palette selector exposes the main colors for each preset. Each Appearance subsection has its own reset action, and **Reset appearance** restores all documented Appearance defaults without touching unrelated account preferences. A small local cache is retained only to avoid a theme/scale flash before authentication, while the authenticated SQLite preference remains authoritative.
The **Corner style** choice applies to standard dashboard/metric cards, vhost
cards, entities, notices, backend protocol badges and inputs independently from
color themes; the default preset retains the original appearance. On narrow
screens the Overview uses two small metric tiles per row rather than one.
Status refreshes update values without resetting widget order or scroll.
The Vhosts list shows one upstream protocol badge per configured reverse-proxy
backend: the configured mode (H1, Auto, H2 compatible or H2 only) plus the last
HTTP/1.1 or HTTP/2 response version observed by RustProxy while proxying a
request. An unknown or stale observation (over five minutes old) appears as `?`,
including in Auto mode; no new probe is issued, and parallel connections can
use different negotiated versions. Hover/focus displays the observation note
and aggregate response-version counters. These are authenticated live-status
observations, not the configured frontend HTTP version.
Unfinished Web Server, Vhost, Redirect and Stream editors remain session-only drafts. Their bottom-right dock uses square corners, can collapse to a single title bar, shows the age of each suspended draft and does not persist draft form values to SQLite.
The Settings page includes a local search field that filters category tabs using translated category labels and page content. Filtering never rebuilds settings forms, so unsaved values remain in the DOM. Category navigation remains sticky on desktop and marks categories containing unsaved forms.
+29 -2
View File
@@ -1,6 +1,33 @@
# Verification report - RustProxy 0.32.5
# Verification report - RustProxy 0.32.6
## Verification performed for this source package
## 0.32.6 mobile presentation and upstream protocol visibility
- `./dev.sh test` passed all pre-Cargo source, regression, simulated API,
upgrade/release-smoke, SQL/schema, documentation, localization and Node.js UI
checks. It then stopped at the expected missing-Cargo guard (`exit 127`).
`./dev.sh check` also stopped there (`exit 127`). A production Rust build
and execution of Rust unit tests **have not been performed**.
- `tools/checks/mobile_protocol_0326_browser.py`,
`appearance_ux_0323_browser.py` and `overview_grid_spacing_0323_browser.py`
passed separately with headless Chromium. These optional browser tests
require Playwright and are not a dependency of `./dev.sh test`.
- Shipped static CSS and real 62-card grid tested under Chromium mobile touch
emulation at 320, 375, 393, 402, 430 and 550px: two metric columns, no
horizontal overflow, stable viewport scroll positions after metric updates
and repeated no-op preference application. **This does not test Safari on
a physical iPhone 17 Pro.**
- Global Appearance radius presets in Chromium tested on vhost, backend badge,
entity and metric surfaces; square draft dock remains excluded.
- JavaScript Vhost badge test checks fresh H1/H2 response observations, Auto
no-observation and stale states, response counters and isolation across
vhosts with identical member IDs.
- Rust test added to assert response-version observation and counters. **The
Rust compiler and a running HTTP peer are absent; this is source coverage,
not a completed Rust unit/integration test.**
- Tests do not establish real backend HTTP/2 ALPN operation; staging validation
and full Rust acceptance remain required before production use.
## Previous 0.32.5 verification history
- **0.32.5 administration UI startup fix:** the DNS ACL translation helper in
`src/admin/web/management.js` is now initialized before the DDNS hint uses
+2
View File
@@ -353,6 +353,8 @@
"vhDrain": "Drain new sessions",
"vhPrivate": "Allow private origin IP",
"vhPreserveHost": "Preserve original client Host / HTTP/2 authority",
"vhBackendProtocolRecent": "Last HTTP version returned to a real proxy request by this origin; pooled/concurrent requests may use a different protocol. Total observed responses:",
"vhBackendProtocolUnobserved": "No backend HTTP response observed in the last five minutes; the protocol is not inferred from Auto configuration.",
"vhProtocol": "Origin protocol",
"vhProtocolHttp1": "HTTP/1.1 (TLS or cleartext)",
"vhProtocolAuto": "Auto (HTTPS ALPN: H2 + H1; HTTP: H1)",
+2
View File
@@ -353,6 +353,8 @@
"vhDrain": "Wstrzymaj nowe sesje",
"vhPrivate": "Zezwól na prywatny IP backendu",
"vhPreserveHost": "Zachowaj Host klienta / HTTP/2 :authority",
"vhBackendProtocolRecent": "Ostatnia wersja HTTP z odpowiedzi backendu na rzeczywiste żądanie. Inne połączenia mogą używać innego protokołu. Łącznie odpowiedzi:",
"vhBackendProtocolUnobserved": "Brak odpowiedzi backendu z ostatnich pięciu minut. Tryb Auto nie oznacza potwierdzonego protokołu.",
"vhProtocol": "Protokół backendu",
"vhProtocolHttp1": "HTTP/1.1 (TLS lub bez TLS)",
"vhProtocolAuto": "Auto (HTTPS ALPN: H2 + H1; HTTP: H1)",
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.32.5",
"version": "0.32.6",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
},
"servers": [
+6 -1
View File
@@ -96,7 +96,12 @@ function applyDashboardLayout(order, hidden) {
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden));
const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
for (const id of normalizedOrder) { const node = nodes.get(id); if (!node) continue; node.hidden = hiddenSet.has(id); grid.append(node); }
// Re-appending an already ordered live grid can break touch-scroll
// anchoring in mobile browsers. Move the DOM only on a real order change.
const orderedNodes = normalizedOrder.map(id => nodes.get(id)).filter(Boolean);
for (const node of orderedNodes) node.hidden = hiddenSet.has(node.dataset.dashboardWidget);
const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")];
if (orderedNodes.some((node, index) => node !== currentNodes[index])) grid.append(...orderedNodes);
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
window.rustProxyMasonry?.refresh?.();
+60 -9
View File
@@ -10399,9 +10399,11 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
#panel-overview .dashboard-overview-grid > .stat { grid-column: span 1; }
#panel-overview .dashboard-overview-grid > .card:not(.pool-card) { grid-column: 1 / -1; }
}
/* Even before the aligned layout script starts, phone-width Overview tiles
stay in two columns. A late 1-to-2-column switch can shift scroll position. */
@media (max-width: 550px) {
#panel-overview .dashboard-overview-grid { grid-template-columns: minmax(0, 1fr); }
#panel-overview .dashboard-overview-grid > .stat { grid-column: 1; }
#panel-overview .dashboard-overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
#panel-overview .dashboard-overview-grid > .stat { grid-column: auto / span 1; }
}
.overview-layout-tile.is-panel { grid-column: span 2; }
.overview-layout-grid .overview-layout-panel-preview {
@@ -10583,6 +10585,9 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
#panel-overview .dashboard-overview-grid.is-aligned.stats-grid {
gap: var(--overview-gap);
}
/* Live metric text can change while the user scrolls; do not allow the
browser to re-anchor an entire dashboard against these changing values. */
#panel-overview .dashboard-overview-grid.is-aligned { overflow-anchor: none; }
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget] {
grid-column: auto / span 2;
grid-row: auto;
@@ -10609,13 +10614,28 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
}
@container overview-canvas (max-width: 700px) {
#panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: repeat(2, minmax(0, 1fr)); }
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: auto / span 1; }
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat {
grid-column: auto / span 1;
touch-action: pan-y pinch-zoom;
overflow-wrap: anywhere;
}
/* The WebSocket tunnel action is absolutely positioned on desktop. In a
half-width phone tile it overlaps the label and value unless it flows. */
#panel-overview .dashboard-overview-grid.is-aligned > .stat.stat-with-action { padding-right: 11px; }
#panel-overview .dashboard-overview-grid.is-aligned .stat-with-action .stat-corner-action {
position: static;
display: block;
width: max-content;
max-width: 100%;
margin: 6px 0 0;
white-space: normal;
}
html[data-dashboard-density="normal"] #panel-overview .dashboard-overview-grid.is-aligned > .stat strong,
html[data-dashboard-density="compact"] #panel-overview .dashboard-overview-grid.is-aligned > .stat strong {
font-size: clamp(18px, 5.5vw, 25px);
}
#panel-overview .dashboard-overview-grid.is-aligned > .card:not(.pool-card) { grid-column: 1 / -1; }
}
@container overview-canvas (max-width: 440px) {
#panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: minmax(0, 1fr); }
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: 1; }
}
/* Keep category boundaries intact at every responsive breakpoint. */
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].overview-row-start { grid-column-start: 1; }
@@ -10691,8 +10711,10 @@ html[data-corner-style="extra"] { --rp-radius-surface: 18px; --rp-radius-control
html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(
.card, .stat, dialog, .login-card, .appearance-section, .settings-tabs,
.dashboard-widget-setting, .overview-layout-tile, .overview-layout-hidden-item,
.overview-inline-hidden-item, .notice, .toast,
.chart-style-choice, .stream-dashboard-card, .pool-card,
.overview-inline-hidden-item, .overview-inline-hidden, .notice, .toast,
.chart-style-choice, .stream-dashboard-card, .pool-card, .vhost-card,
.vhost-backend-protocol-badge, .vh-backend-card, .entity, .settings-filter-row,
.stream-backend-row,
.file-log-advanced-parser, .table-container, .metric-info-popover
) { border-radius: var(--rp-radius-surface) !important; }
html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(
@@ -10708,3 +10730,32 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
/* ACL DDNS input: keep the rule type separate from its hostname. */
.acl-profile-rule { grid-template-columns: minmax(100px, .6fr) minmax(105px, .6fr) minmax(175px, 1.5fr) auto; }
@media (max-width: 640px) { .acl-profile-rule { grid-template-columns: 1fr; } }
/* 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. */
.vhost-backend-protocols {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
min-width: 0;
margin: 8px 0;
}
.vhost-backend-protocol-badge {
display: inline-flex;
gap: 4px;
align-items: baseline;
max-width: 100%;
min-width: 0;
border: 1px solid var(--pill-line);
background: var(--panel2);
color: var(--muted);
border-radius: 4px;
padding: 3px 7px;
font: 10px var(--mono);
overflow-wrap: anywhere;
}
.vhost-backend-protocol-badge[data-observed="true"] {
color: var(--green);
border-color: var(--pill-live-line);
}
+38
View File
@@ -101,6 +101,30 @@ async function toggleVhostEnabled(vhost) {
if (await saveSection("vhosts", next.vhosts || [])) render();
}
const VHOST_PROTOCOL_FRESH_MS = 5 * 60 * 1000;
function upstreamProtocolBadgeText(backend, member, now = Date.now()) {
const mode = { http1: "H1", auto: "AUTO", http2: "H2 compatible", h2: "H2 only" }[backend.protocol || "http1"] || "H1";
const seen = Number(member?.upstream_protocol_last_seen_ms);
const recent = member && (member.upstream_protocol_observed === "h1" || member.upstream_protocol_observed === "h2") &&
Number.isFinite(seen) && seen > 0 && now >= seen && now - seen <= VHOST_PROTOCOL_FRESH_MS;
return { label: `${backend.id} · ${mode} → ${recent ? member.upstream_protocol_observed.toUpperCase() : "?"}`, recent };
}
function renderUpstreamProtocolBadges(status) {
const members = new Map((status.backend_pools || []).flatMap(pool => (pool.members || []).map(member =>
[`${pool.id}\u0000${member.id}`, member])));
const now = Date.now();
document.querySelectorAll("[data-upstream-protocol-vhost]").forEach(node => {
const vhost = node.dataset.upstreamProtocolVhost, id = node.dataset.upstreamProtocolBackend;
const backend = (envelope?.config.vhosts || []).find(host => host.id === vhost)?.backends?.find(item => item.id === id);
if (!backend) return;
const member = members.get(`${vhost}\u0000${id}`);
const observed = upstreamProtocolBadgeText(backend, member, now);
node.textContent = observed.label;
node.dataset.observed = String(!!observed.recent);
const counts = member && observed.recent ? ` H1: ${member.upstream_protocol_http1_total || 0}; H2: ${member.upstream_protocol_http2_total || 0}.` : "";
node.title = `${t(observed.recent ? "vhBackendProtocolRecent" : "vhBackendProtocolUnobserved")}${counts}`;
});
}
function render() {
if (!envelope) return;
if (!initialized) load(defaults(), [], false);
@@ -187,6 +211,19 @@ 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); }
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 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); };
@@ -246,6 +283,7 @@ function vhostTrafficRate(id, metrics, snapshot, now, seen) {
return rate;
}
function renderStatus(status) {
renderUpstreamProtocolBadges(status);
form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.());
const rows = [], summaries = new Map(), aggregate = new Map();
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set();
+7
View File
@@ -85,6 +85,13 @@ impl Pool {
"upstream_client_rotations_total".into(),
json!(client_rotation.rotations),
);
// Negotiated HTTP version from real backend response headers,
// never from the configured Auto/HTTP2 policy or health probes.
let observed = m.backend.client.protocol_stats();
item.insert("upstream_protocol_observed".into(), json!(observed.last_version));
item.insert("upstream_protocol_last_seen_ms".into(), json!(observed.observed_at_ms));
item.insert("upstream_protocol_http1_total".into(), json!(observed.http1_responses));
item.insert("upstream_protocol_http2_total".into(), json!(observed.http2_responses));
item.insert("latency_ms".into(), json!(h.latency_us as f64 / 1000.0));
item.insert("ewma_latency_ms".into(), json!(h.ewma_us as f64 / 1000.0));
item.insert("last_probe_ms".into(), json!(h.last_probe_ms));
+55 -2
View File
@@ -6,7 +6,7 @@ use crate::config::{Backend, UpstreamProtocol};
use anyhow::{Context, Result};
use arc_swap::ArcSwap;
use axum::body::Body;
use http::{Request, Response};
use http::{Request, Response, Version};
use hyper::body::Incoming;
use hyper_rustls::HttpsConnectorBuilder;
use hyper_util::{
@@ -50,6 +50,10 @@ struct RotatingClient {
generation_started_ms: AtomicU64,
rotations: AtomicU64,
rotating: AtomicBool,
observed_version: AtomicU64,
observed_at_ms: AtomicU64,
observed_h1: AtomicU64,
observed_h2: AtomicU64,
}
/// Backend HTTP client with bounded generation lifetime.
@@ -71,6 +75,14 @@ pub struct ClientRotationStats {
pub rotations: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UpstreamProtocolStats {
pub last_version: Option<&'static str>,
pub observed_at_ms: u64,
pub http1_responses: u64,
pub http2_responses: u64,
}
impl std::fmt::Debug for ProxyClient {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
@@ -91,13 +103,25 @@ impl ProxyClient {
generation_started_ms: AtomicU64::new(monotonic_millis()),
rotations: AtomicU64::new(0),
rotating: AtomicBool::new(false),
observed_version: AtomicU64::new(0),
observed_at_ms: AtomicU64::new(0),
observed_h1: AtomicU64::new(0),
observed_h2: AtomicU64::new(0),
}),
}
}
pub async fn request(&self, request: Request<Body>) -> Result<Response<Incoming>, ClientError> {
let client = self.inner.client_for_request();
client.request(request).await
let response = client.request(request).await;
if let Ok(value) = &response {
self.inner.observe_protocol(value.version());
}
response
}
pub fn protocol_stats(&self) -> UpstreamProtocolStats {
self.inner.protocol_stats()
}
pub fn rotation_stats(&self) -> ClientRotationStats {
@@ -119,6 +143,35 @@ impl ProxyClient {
}
impl RotatingClient {
fn observe_protocol(&self, version: Version) {
let code = match version {
Version::HTTP_2 => 2,
Version::HTTP_10 | Version::HTTP_11 => 1,
_ => return,
};
if code == 2 {
self.observed_h2.fetch_add(1, Ordering::Relaxed);
} else {
self.observed_h1.fetch_add(1, Ordering::Relaxed);
}
self.observed_version.store(code, Ordering::Relaxed);
self.observed_at_ms
.store(crate::logging::now_ms(), Ordering::Relaxed);
}
fn protocol_stats(&self) -> UpstreamProtocolStats {
UpstreamProtocolStats {
last_version: match self.observed_version.load(Ordering::Relaxed) {
1 => Some("h1"),
2 => Some("h2"),
_ => None,
},
observed_at_ms: self.observed_at_ms.load(Ordering::Relaxed),
http1_responses: self.observed_h1.load(Ordering::Relaxed),
http2_responses: self.observed_h2.load(Ordering::Relaxed),
}
}
fn stats(&self) -> ClientRotationStats {
let now = monotonic_millis();
ClientRotationStats {
+17
View File
@@ -11,6 +11,23 @@ mod tests {
.unwrap()
}
#[test]
fn protocol_observation_requires_a_real_http_response_version() {
let backend = backend("protocol-observed", 18090);
let client = client(&backend, AddressPolicy::new(&backend).unwrap(), false).unwrap();
assert_eq!(client.protocol_stats().last_version, None);
let shared = client.clone();
// Exercise the response-version recorder independently of the network;
// a live TLS ALPN acceptance test must still run on staging.
shared.inner.observe_protocol(Version::HTTP_11);
shared.inner.observe_protocol(Version::HTTP_2);
let observed = client.protocol_stats();
assert_eq!(observed.last_version, Some("h2"));
assert_eq!(observed.http1_responses, 1);
assert_eq!(observed.http2_responses, 1);
assert!(observed.observed_at_ms > 0);
}
#[test]
fn generation_recycles_after_request_budget() {
let backend = backend("rotation-test", 18081);
@@ -0,0 +1,54 @@
// Verifies real-status-to-badge mapping without guessing a configured Auto result.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const source = fs.readFileSync('src/admin/web/vhosts/render.js', 'utf8');
const block = source.slice(source.indexOf('const VHOST_PROTOCOL_FRESH_MS = '), source.indexOf('function render() {'));
assert(block.includes('function renderUpstreamProtocolBadges(status)'));
const rustClient = fs.readFileSync('src/upstream/client.rs', 'utf8');
const rustPool = fs.readFileSync('src/balance/pool_view.rs', 'utf8');
assert(rustClient.includes('self.inner.observe_protocol(value.version())'));
assert(rustClient.includes('let response = client.request(request).await;'));
for (const name of ['upstream_protocol_observed','upstream_protocol_last_seen_ms','upstream_protocol_http1_total','upstream_protocol_http2_total']) {
assert(rustPool.includes(`"${name}"`), name);
}
const en = JSON.parse(fs.readFileSync('langs/en_US.json', 'utf8')).translations;
const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json', 'utf8')).translations;
for(const key of ['vhBackendProtocolRecent','vhBackendProtocolUnobserved']) assert(en[key] && pl[key]);
const badge = (vhost, backend) => ({dataset:{upstreamProtocolVhost:vhost,upstreamProtocolBackend:backend},textContent:'',title:''});
const visible = [badge('api','auto-backend'),badge('api','h1-backend'),badge('other','auto-backend')];
const envelope = {config:{vhosts:[
{id:'api',backends:[{id:'auto-backend',protocol:'auto'},{id:'h1-backend',protocol:'http1'}]},
{id:'other',backends:[{id:'auto-backend',protocol:'auto'}]},
]}};
const ctx = {Date, Number, String, Map, envelope, document:{querySelectorAll:()=>visible},t:key=>en[key]};
vm.createContext(ctx);
vm.runInContext(block, ctx);
const recent = Date.now() - 2000;
let status = {backend_pools:[{id:'api',members:[
{id:'auto-backend',upstream_protocol_observed:'h2',upstream_protocol_last_seen_ms:recent,upstream_protocol_http1_total:5,upstream_protocol_http2_total:9},
{id:'h1-backend',upstream_protocol_observed:'h1',upstream_protocol_last_seen_ms:recent,upstream_protocol_http1_total:12},
]}]};
ctx.renderUpstreamProtocolBadges(status);
assert.equal(visible[0].textContent,'auto-backend · AUTO → H2');
assert.equal(visible[0].dataset.observed,'true');
assert(visible[0].title.includes('H1: 5; H2: 9'));
assert.equal(visible[1].textContent,'h1-backend · H1 → H1');
assert.equal(visible[2].textContent,'auto-backend · AUTO → ?');
assert.equal(visible[2].dataset.observed,'false');
// Old/stale response must not masquerade as a recent ALPN observation.
status.backend_pools[0].members[0].upstream_protocol_last_seen_ms = Date.now() - 3600000;
ctx.renderUpstreamProtocolBadges(status);
assert.equal(visible[0].textContent,'auto-backend · AUTO → ?');
assert.equal(visible[0].dataset.observed,'false');
assert(visible[0].title.startsWith(en.vhBackendProtocolUnobserved));
// Fresh data for a similarly named backend from another vhost must not leak.
ctx.renderUpstreamProtocolBadges({backend_pools:[{id:'other',members:[
{id:'auto-backend',upstream_protocol_observed:'h1',upstream_protocol_last_seen_ms:recent}
]}]});
assert.equal(visible[0].textContent,'auto-backend · AUTO → ?');
assert.equal(visible[2].textContent,'auto-backend · AUTO → H1');
ctx.renderUpstreamProtocolBadges({backend_pools:[]});
assert.equal(visible[2].textContent,'auto-backend · AUTO → ?');
console.log('PASS: observed Auto/H1/H2 badges, freshness, cross-vhost isolation and unknown data');
+2 -2
View File
@@ -7,7 +7,7 @@ import tomllib
root = Path(__file__).resolve().parents[2]
read = lambda path: (root / path).read_text(encoding='utf-8')
version = tomllib.loads(read('Cargo.toml'))['package']['version']
assert version == '0.32.5', version
assert version == '0.32.6', version
spec = json.loads(read('src/admin/openapi.json'))
assert spec['info']['version'] == version
paths = spec['paths']
@@ -64,5 +64,5 @@ for language in ('en_US', 'pl_pl'):
for key in ('certificateApiView', 'certificateApiEdit', 'certificateApiSave', 'certificateApiRegenerate',
'certificateApiRegenerateConfirm', 'certificateApiTokenHidden', 'certificateApiCreatedAt'):
assert strings.get(key), (language, key)
assert '0.32.5' in read('docs/TEST_REPORT.md')
assert '0.32.6' in read('docs/TEST_REPORT.md')
print('PASS: 0.31.9 per-credential API routes, separated metadata editing/token regeneration, OpenAPI security, UI and locales (source-only).')
+9 -9
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.32.5"
assert manifest["package"]["version"] == "0.32.6"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock"
if lock_path.exists():
lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.32.5"' in lock
assert 'name = "rust-proxy"\nversion = "0.32.6"' in lock
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in {
"README.md": "# RustProxy 0.32.5",
"CHANGELOG.md": "## 0.32.5",
"ROADMAP.md": "## 0.32.5",
"AGENTS.md": "Current source version: 0.32.5.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.5",
"README.md": "# RustProxy 0.32.6",
"CHANGELOG.md": "## 0.32.6",
"ROADMAP.md": "## 0.32.6",
"AGENTS.md": "Current source version: 0.32.6.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.6",
}.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.32.5"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
for marker in ['"version": "0.32.6"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
@@ -1396,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.32.5"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.32.6"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
+1 -1
View File
@@ -14,7 +14,7 @@ parser = argparse.ArgumentParser()
parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
args = parser.parse_args()
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
assert manifest['package']['version'] == '0.32.5'
assert manifest['package']['version'] == '0.32.6'
assert manifest['package']['rust-version'] == '1.99'
deps = manifest['dependencies']
expected_requirements = {
+1 -1
View File
@@ -51,7 +51,7 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.32.5")
assert readme.startswith("# RustProxy 0.32.6")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Static source contract for DDNS ACL 0.32.5; NOT a Rust test."""
"""Static source contract for DDNS ACL 0.32.6; NOT a Rust test."""
from pathlib import Path
import json
import tomllib
@@ -25,8 +25,8 @@ checks = {
'visual selector and disabled DNS deny': '.acl-profile-rule-type' in js and 'aclDnsAllowOnly' in js,
'old network field persisted': 'network: normalizeNetwork(' in js,
'PL/EN translations': all(k in json.loads(read(f'langs/{loc}.json'))['translations'] for loc in ('en_US','pl_pl') for k in ('aclRuleType','aclRuleDns','aclDnsHelp','aclDnsAllowOnly')),
'package version': tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.5',
'package version': tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.6',
}
for name, ok in checks.items():
if not ok: raise AssertionError(name)
print(f'PASS: {len(checks)} DNS ACL 0.32.5 static source checks (not compiled or network-tested)')
print(f'PASS: {len(checks)} DNS ACL 0.32.6 static source checks (not compiled or network-tested)')
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
html = (ROOT / 'src/admin/web/index.html').read_text()
assert '0.32.5' in (ROOT / "Cargo.toml").read_text()
assert '0.32.6' in (ROOT / "Cargo.toml").read_text()
assert '<a class="brand" href="/"' in html, 'logo must remain a true full-page navigation'
assert 'addEventListener("click"' not in html.split('<a class="brand" href="/"',1)[1].split('</a>',1)[0]
body = (ROOT / 'src/proxy/cache_helpers.rs').read_text()
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Source-only mobile/observed-backend UI invariants, no browser required."""
from pathlib import Path
root=Path(__file__).resolve().parents[2]
css=(root/'src/admin/web/style.css').read_text()
preferences=(root/'src/admin/web/app/preferences.js').read_text()
masonry=(root/'src/admin/web/app/overview-masonry.js').read_text()
html=(root/'src/admin/web/index.html').read_text()
render=(root/'src/admin/web/vhosts/render.js').read_text()
client=(root/'src/upstream/client.rs').read_text()
pool=(root/'src/balance/pool_view.rs').read_text()
assert 'touch-action: pan-y pinch-zoom' in css
assert 'overflow-anchor: none' in css
assert 'grid-template-columns: repeat(2, minmax(0, 1fr))' in css
assert '@container overview-canvas (max-width: 700px)' in css
assert 'overview-canvas (max-width: 440px)' not in css
assert '.vhost-backend-protocol-badge' in css
assert '.vhost-card' in css[css.index('/* Global shape preference.'):]
assert '.draft-dock,' not in css[css.index('/* Global shape preference.'):]
assert 'grid.append(...orderedNodes)' in preferences
assert 'orderedNodes.some((node, index) => node !== currentNodes[index])' in preferences
assert 'if (ordered.some((node, i) => node !== previous[i])) grid.append(...ordered);' in masonry
assert html.count('data-dashboard-widget=') >= 62
assert 'function renderUpstreamProtocolBadges(status)' in render
assert 'const VHOST_PROTOCOL_FRESH_MS = 5 * 60 * 1000' in render
assert 'self.inner.observe_protocol(value.version())' in client
assert '"upstream_protocol_observed"' in pool
print('PASS: mobile grid, shape, stable reorder and observed backend protocol source contracts')
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Mobile Dashboard scroll/layout and appearance browser regression.
Uses the shipped static web assets in a Chromium touch-device emulation, not
physical Safari, the authenticated server or a running Rust backend.
"""
from pathlib import Path
import json
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')
widgets = soup.select('#dashboard-overview-grid > [data-dashboard-widget]')
assert len(widgets) == 62
widget_ids = [widget['data-dashboard-widget'] for widget in widgets]
css = (WEB / 'style.css').read_text()
source = (WEB / 'app/preferences.js').read_text()
layout_code = source.split('function applyDashboardLayout(order, hidden) {')[1].split('function dashboardWidgetLabel(')[0]
html = ('<!doctype html><html data-theme="ultra_dark" data-dashboard-density="compact" '
'data-corner-style="default"><head><meta name="viewport" '
'content="width=device-width,initial-scale=1"><style>' + css +
'</style></head><body style="margin:0">'
'<div style="height:80px">Mobile navigation</div>'
'<section id="panel-overview" style="width:auto;padding:0 10px;box-sizing:border-box">'
'<div id="dashboard-overview-grid" class="stats-grid dashboard-overview-grid">' +
''.join(map(str, widgets)) + '</div></section>'
'<div id="radii" style="padding:10px"><article class="vhost-card" id="vhost-test">Vhost</article>'
'<div class="entity" id="entity-test">Entity</div>'
'<span class="vhost-backend-protocol-badge" id="badge-test">AUTO → H2</span></div>'
'</body></html>')
with sync_playwright() as browser_tool:
chromium = browser_tool.chromium.launch(headless=True,
executable_path=shutil.which('chromium') or shutil.which('chromium-browser'),
args=['--no-sandbox'])
context = chromium.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=(WEB / 'app/overview-masonry.js').read_text())
preference_fixture = (
'const ADMIN_DASHBOARD_WIDGETS=' + json.dumps(widget_ids) + ';' +
'const ADMIN_DEFAULT_HIDDEN_WIDGETS=[];' +
'const latestStatus=null; const currentPanel="overview";' +
'const normalizeDashboardWidgets=(ids)=>ids;' +
'const normalizeHiddenDashboardWidgets=(ids)=>ids;' +
'function applyDashboardLayout(order, hidden) {' + layout_code
)
page.add_script_tag(content=preference_fixture)
for width in (320, 375, 393, 402, 430, 550):
page.set_viewport_size({'width': width, 'height': 844})
snapshot = page.evaluate('''() => {
const grid=document.getElementById('dashboard-overview-grid');
const cards=[...grid.querySelectorAll('[data-dashboard-widget].stat')]
.filter(node => !node.hidden).slice(0,6).map(node => {
const r=node.getBoundingClientRect();
return {left:r.left,top:r.top,right:r.right,bottom:r.bottom,width:r.width};
});
return {columns:getComputedStyle(grid).gridTemplateColumns.split(' ').length,
cards,gridWidth:grid.clientWidth,gridScroll:grid.scrollWidth,
screenScroll:document.documentElement.scrollWidth,
screen:innerWidth,
touch:getComputedStyle(grid.querySelector('.stat')).touchAction};
}''')
a,b,c,d,e,f = snapshot['cards']
assert snapshot['columns'] == 2, (width, snapshot)
assert abs(a['top']-b['top']) < 1 and abs(c['top']-d['top']) < 1
assert abs(e['top']-f['top']) < 1 and c['top'] > a['top'] and e['top'] > c['top']
assert b['left'] >= a['right'] and d['left'] >= c['right']
assert a['width'] > 50 and b['width'] > 50
assert snapshot['gridScroll'] <= snapshot['gridWidth']+2, (width,snapshot)
assert snapshot['screenScroll'] <= snapshot['screen']+2, (width,snapshot)
assert 'pan-y' in snapshot['touch']
action = page.evaluate('''() => {
const tile=document.querySelector('.stat-with-action');
const label=tile.querySelector(':scope > span').getBoundingClientRect();
const button=tile.querySelector('.stat-corner-action').getBoundingClientRect();
const value=tile.querySelector('strong').getBoundingClientRect();
const card=tile.getBoundingClientRect();
return {labelBottom:label.bottom,buttonTop:button.top,
buttonBottom:button.bottom,valueTop:value.top,
buttonRight:button.right,cardRight:card.right};
}''')
assert action['buttonTop'] >= action['labelBottom'] - 2, (width,action)
assert action['valueTop'] >= action['buttonBottom'] - 2, (width,action)
assert action['buttonRight'] <= action['cardRight'] + 1, (width,action)
page.evaluate('window.scrollTo(0,350)')
before = page.evaluate('window.scrollY')
assert before > 0, width
page.evaluate('''() => {
const grid=document.getElementById('dashboard-overview-grid');
window.rustProxyMasonry.refresh();
for(const [index,node] of [...grid.querySelectorAll('.stat strong')].entries()) {
node.textContent=index%2 ? '26216' : '49,750';
}
}''')
after = page.evaluate('window.scrollY')
assert abs(after - before) <= 2, (width, before, after)
# Reapplying an unchanged preference must NOT reparent the grid children.
moves = page.evaluate('''() => {
const grid=document.getElementById('dashboard-overview-grid');
const order=[...grid.children].map(n=>n.dataset.dashboardWidget);
let moves=0;
const observer=new MutationObserver(items => { moves+=items.filter(i=>i.type==='childList').length; });
observer.observe(grid,{childList:true});
applyDashboardLayout(order,[]);
observer.takeRecords().forEach(i=>{if(i.type==='childList')moves++});
observer.disconnect();
return moves;
}''')
assert moves == 0, (width, moves)
assert abs(page.evaluate('window.scrollY')-before) <= 2
print(f'PASS: {width}px touch emulation, two metric columns, no overflow or scroll jumps')
for style,surface in (('square','0px'), ('subtle','5px'), ('rounded','12px'), ('extra','18px')):
page.evaluate('(s) => document.documentElement.dataset.cornerStyle=s', style)
for selector in ('#vhost-test','#entity-test','#badge-test','.stat'):
radius = page.locator(selector).first.evaluate('node=>getComputedStyle(node).borderTopLeftRadius')
assert radius == surface, (style,selector,radius)
print('PASS: vhost, entity, backend badge and metric corner presets')
chromium.close()
@@ -147,7 +147,7 @@ with sync_playwright() as playwright:
cases=[(1720,0,1,'normal',12),(1350,180,1,'compact',12),
(1200,260,1,'normal',6),(900,0,1,'compact',6),
(900,190,1,'normal',6),(720,190,1,'ultra',2),
(620,0,1,'compact',2),(430,0,1,'ultra',1),
(620,0,1,'compact',2),(430,0,1,'ultra',2),
(1140,200,.9,'compact',6),(1140,200,1.1,'normal',6),
(1140,200,1.3,'normal',6)]
for width,sidebar,zoom,density,max_columns in cases:
+3 -3
View File
@@ -51,6 +51,6 @@ for marker in ['margin: 0;', '--overview-gap: max(3px, calc(var(--ui-gap) - 1px)
retry = re.search(r'fn retry_request\([\s\S]*?\n}\n', proxy)
assert retry and '.uri(uri.clone())' in retry[0]
assert 'upstream_request_target(&uri, request.headers_mut()' in retry[0]
assert json.loads(read('src/admin/openapi.json'))['info']['version']=='0.32.5'
assert 'version = "0.32.5"' in read('Cargo.toml')
print('PASS: 0.32.5 origin pinning, authority, retry, CSS gutter source contracts (not Rust runtime)')
assert json.loads(read('src/admin/openapi.json'))['info']['version']=='0.32.6'
assert 'version = "0.32.6"' in read('Cargo.toml')
print('PASS: 0.32.6 origin pinning, authority, retry, CSS gutter source contracts (not Rust runtime)')
+3 -3
View File
@@ -8,9 +8,9 @@ from html.parser import HTMLParser
ROOT = Path(__file__).resolve().parents[2]
def read(rel): return (ROOT / rel).read_text(encoding='utf-8')
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.5'
assert 'name = "rust-proxy"\nversion = "0.32.5"' in read('Cargo.lock')
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.5'
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.6'
assert 'name = "rust-proxy"\nversion = "0.32.6"' in read('Cargo.lock')
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.6'
pages = read('src/pages.rs')
for code in (400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503, 504):
+4 -4
View File
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.32.5"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.32.6"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file()
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.32.5"
assert manifest["to_version"] == "0.32.6"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -617,7 +617,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.5"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.6"' in openapi_0241
assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker
@@ -1102,7 +1102,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.32.5"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
for marker in ['"version": "0.32.6"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
+2 -2
View File
@@ -21,7 +21,7 @@ const openapi = json('src/admin/openapi.json');
const en = json('langs/en_US.json').translations;
const pl = json('langs/pl_pl.json').translations;
assert.match(manifest, /version = "0\.32\.5"/);
assert.match(manifest, /version = "0\.32\.6"/);
assert.match(lib, /pub mod user_agent;/);
assert.match(model, /user_agent_blocking: crate::user_agent::Policy/);
assert.match(vhost, /user_agent_blocking: crate::user_agent::Policy/);
@@ -52,7 +52,7 @@ assert.match(fileLogs, /localStorage\.setItem\(CONSOLIDATION_STORAGE_KEY/);
assert.match(fileLogs, /aggregate_min_count/);
assert.match(fileLogs, /file-log-aggregate-copy/);
assert.equal(openapi.info.version, '0.32.5');
assert.equal(openapi.info.version, '0.32.6');
assert.ok(openapi.components.schemas.Config.properties.user_agent_blocking);
assert.ok(openapi.components.schemas.Vhost.properties.user_agent_blocking);
assert.ok(openapi.components.schemas.FileLogSource.properties.group_id);
+5 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Offline source contracts for RustProxy 0.32.5 upstream protocols and card registry.
"""Offline source contracts for RustProxy 0.32.6 upstream protocols and card registry.
These assertions supplement, rather than replace, Rust ALPN, H2C and websocket
runtime tests. No code execution against a RustProxy server is implied.
@@ -141,7 +141,7 @@ for field in ('dashboard_widgets', 'dashboard_hidden_widgets'):
assert '"version": "0.32.5"' in read('src/admin/openapi.json')
assert 'version = "0.32.5"' in read('Cargo.toml')
assert 'version = "0.32.5"' in read('Cargo.lock')
print(f'PASS: 0.32.5 upstream H1/Auto/compatible-HTTP2/strict-H2 source contracts and {len(ids)} registered Overview Cards')
assert '"version": "0.32.6"' in read('src/admin/openapi.json')
assert 'version = "0.32.6"' in read('Cargo.toml')
assert 'version = "0.32.6"' in read('Cargo.lock')
print(f'PASS: 0.32.6 upstream H1/Auto/compatible-HTTP2/strict-H2 source contracts and {len(ids)} registered Overview Cards')