V0.32.6 #1
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.32.5"
|
||||
version = "0.32.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
@@ -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).')
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)')
|
||||
@@ -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()
|
||||
|
||||
Executable
+28
@@ -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')
|
||||
Executable
+123
@@ -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:
|
||||
|
||||
@@ -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)')
|
||||
@@ -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):
|
||||
|
||||
@@ -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"']:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
Reference in new issue
Block a user