diff --git a/AGENTS.md b/AGENTS.md index 78b1021..68a34d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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.26.3. +compatibility. Current source version: 0.26.4. ## Delivery diff --git a/CHANGELOG.md b/CHANGELOG.md index ac48bb6..f086cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ > test-only changes and packaging implementation notes were moved to > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). +## 0.26.4 + +- Add 404-host editors to the session draft dock so unfinished 404 forms can be minimized, switched away from and restored like Web Server, Vhost, Redirect and Stream drafts. +- Complete Polish Button style translations and align compact toolbar search/filter inputs with the selected button height. +- Standardize modal Enabled controls for 404 hosts, Redirects and Streams on the vhost state-control presentation. +- Replace the Overview-card checkbox list in Appearance with compact responsive visibility/reorder tiles. +- Make age-based upstream client recycling run from backend maintenance even without traffic, fixing idle backends that could remain at rotation count 0 for hours. +- Keep SQLite schema 8 and the saved proxy configuration format unchanged; no migration is required from 0.26.3. + ## 0.26.3 - Make the **Settings -> Appearance** interface scale global across the authenticated administration UI, including dialogs, overlays and other top-level surfaces, and extend the selectable range from 80-125% to 70-125%. Form fields also receive a bounded proportional size adjustment so they track the selected scale more visibly. diff --git a/Cargo.lock b/Cargo.lock index f6201dd..8c33f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.26.3" +version = "0.26.4" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index b7a8d9e..1ed21bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.26.3" +version = "0.26.4" edition = "2021" rust-version = "1.88" description = "RustProxy - independent reverse proxy with local administration" diff --git a/README.md b/README.md index da2e923..26d93a0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# RustProxy 0.26.3 +# RustProxy 0.26.4 Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration, streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools. -**0.26.3 release source.** +**0.26.4 release source.** ## Layer-4 streams @@ -11,7 +11,19 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. -## What changed in 0.26.3 +## What changed in 0.26.4 + +- Extend session-only editor drafts to **404 hosts**, preserving the complete unfinished form state when another editor is opened or the dialog is minimized. +- Complete the Polish translation for **Settings -> Appearance -> Button style** and keep the button-size preference independent from global interface scale. +- Keep search/filter inputs in compact toolbars no taller than the active button style, including Backends, vhost-style lists, Docker, Logs and Settings search controls. +- Reuse the vhost Enabled control presentation in 404 hosts, Redirects and Streams so modal enable/disable state is visually consistent. +- Replace the tall Overview-card checkbox list in Appearance with a compact responsive tile grid; each tile toggles visibility and retains reorder controls. +- Run time-based upstream-client pool recycling from backend maintenance as well as request dispatch, so an idle backend rotates after its configured age instead of remaining on generation 0 until the next request. +- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.3 requires no data migration. + +0.26.4 retains the 0.26.3 appearance and administration workflow changes below. + +### Retained 0.26.3 release notes - Make interface scaling truly global: the 70-125% setting now scales the authenticated dashboard, navigation, dialogs, overlays, draft dock and other top-level administration UI consistently. Form fields receive an additional proportional size adjustment so inputs/selects/textarea do not remain visually oversized at low scale or undersized at high scale. - Tighten the Overview metric cards so wide dashboards fit more telemetry with less empty vertical space. @@ -39,7 +51,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost - Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`). - Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration. -The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.3 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md). +The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.4 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md). ## Documentation @@ -117,7 +129,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \ CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked ``` -The 0.26.3 source archive preserves the resolved `Cargo.lock` supplied with the source tree and +The 0.26.4 source archive preserves the resolved `Cargo.lock` supplied with the source tree and updates only this package's own version entry. This packaging environment has no Cargo, so the dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a @@ -356,7 +368,7 @@ python3 scripts/release_smoke.py # Runs those checks first, then the Rust suite when Cargo is available: bash dev.sh test # Source-only package, with no Git checkout required: -python3 make_zip.py /tmp/RustProxy-0.26.3.zip +python3 make_zip.py /tmp/RustProxy-0.26.4.zip ``` ## New management and test-origin guide diff --git a/ROADMAP.md b/ROADMAP.md index bb6065e..d925486 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,18 @@ # RustProxy roadmap +## 0.26.4 - Administration consistency and backend rotation reliability + +Current direction: 0.26.3 -> 0.26.4 + +- [x] Add 404-host forms to the session-only draft workflow. +- [x] Complete Polish translations for the Button style appearance setting. +- [x] Align toolbar search/filter input height with the active button style. +- [x] Reuse the vhost Enabled control styling in 404 hosts, Redirects and Streams. +- [x] Replace the Overview-card checkbox list with compact visibility/reorder tiles. +- [x] Run age-based backend client recycling independently of request arrival. +- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.3. +- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host. + ## 0.26.3 - Administration appearance and draft workflow polish Current direction: 0.26.2 -> 0.26.3 diff --git a/SHA256SUMS b/SHA256SUMS index 8460a14..3d54010 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,19 +1,19 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore -78bfdaf9ea3b88e84a6acf6720dcef80c4b3f4eaab233ed202d17c33393c7e5f AGENTS.md -8a4a00a320fece83cf042bcd9145da98b8951d76d3e2bd191ab6a8fd668efbb5 CHANGELOG.md -d1b0cf138814b68b272c32616aac2334699f906b0b43489601f21cf36b513088 Cargo.lock -61d358ab03a30e1425bb91f84b70221b8b39c5fd6324475ec098c5b1f53ac959 Cargo.toml +3bb6c6c9cb4711103174833994befd1f25814c609ae99d444b6f6b5bc2c3c6a0 AGENTS.md +6e4073eae721bc224dc6551caf0928d9fefadf7d120ecfa02f2162157e10c1a0 CHANGELOG.md +56a24128244b2e0cab7050f0f83acae255dc97720fa165d06ea09bc14f50252f Cargo.lock +c5e5651b1a1dbfbe524ad2e6b131e971d6ffb1038c9519677f067d2d35bc4e07 Cargo.toml d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile -612e0d17a2fcda2a5799a44d8fd099ab285c284c1327fb84c1475d8e71ec116c README.md -6aab906375937d1d0ccea95280a6d1efa41089b8d150278e1d73ca50dfb7dfc2 ROADMAP.md +82b353c623d514e9404eead2803f61addedaf4f3e27117c0b7c5341a3af6c512 README.md +fb8affd22820c5ef43a922ec8fa08ffe27d988bd4e24d4f2c4a0886dfabc1d7a ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml 928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs -f8cc5f463b4642faadfa2bada5fb09298b6947c7284d938ae3ba4db12cb448ee debian/README.md -fe510e22d31893d4df86de9bb48196dfed31ecb16d5c7649bb1fdba246fd49ce debian/build-package.sh +8eb7682c9d1e261fbc201bee412d52a823c9ad3015aae363c844394814426efe debian/README.md +37641fa2d47a0331249ccea8028bce95c8b88d2c2a13000a8a09116efb2cd6cf debian/build-package.sh 0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh -b2579151f48cd68a751d8d99043de2a1b78cfaa51514fc6becf1223f2decb144 debian/changelog +0af7b8ccded17dd9de34b23a9be73ffca0d3e420ab5902531889460f8cd22c20 debian/changelog c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment @@ -24,7 +24,7 @@ cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustpro e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format 385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md 7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service -4e766ffd22c9a096890a2555a7eac0ebafbe0599e3fc462413c1b782c393e450 dev.sh +4353c58f038fbd83fda1a9879bb4765b924896e61c2676be6da8ab7154604807 dev.sh 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example @@ -33,14 +33,14 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d 9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go 3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go -1803047c9588c2892b6846072ca23d7db20f09d35591c47f1460beb828e47de9 docker-agent/model.go +eec37615a32bad4fab987f619734864a980e83ac09ace445a2e921bc17f8d326 docker-agent/model.go e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go 634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service 5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go 802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md -35d798657697a8031cac5efd417b6bb8de819dd7d4e79666e61d1805d8545d6e docs/API_ACCESS.md +24cfd6b15a149d8dca0a334a8ac841ec008e6f64d3a11aac0563196ae5e1b168 docs/API_ACCESS.md 0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md 9cb338ed07ac1f92ad491e2099a7e3af63e66262417b1fef24d98c6a791d13fa docs/CACHE_DIAGNOSTICS.md @@ -49,9 +49,9 @@ db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER 9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md 7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md 86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md -1594b5eee60bace5111d2a7ea3d59f6d6a479d8515c2e9256c52387c64cf18cd docs/FILE_LOGS.md +8e9df23ee4539162955f9ef86a09a3e227ec7c34c3ee1206b7c14ac6926da19d docs/FILE_LOGS.md df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md -5c753bf5de6709b8dd55ac449a4ef38757d9c7c8b55fbe1f5fd9b9caf999ec58 docs/INSTALL.md +bd94e892186c612459d890f4a1195986e7034b8b3b9d1980090f71b1d07e0263 docs/INSTALL.md 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md 7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md @@ -62,7 +62,7 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND 3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md 712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md 572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md -cb8ebf15c59eacd4d5567b98c13101a1fd225d43c6dd5586100727fd00917fea docs/README.md +a4329e7e8f2149e87c8d5610fbea6fe7bbf40b4d01a74b79a60b5cda09808bfe docs/README.md 6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md 0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md @@ -72,7 +72,7 @@ a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY. 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md f8baa8bdc8a67bf4e4a6e6d233435f5a43075a09bf08b08b597fcef1853ce5c5 docs/SETTINGS.md 3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md -c8eeed55e34d1a33a2d9d7ea61569eec786060b4f9d8342ea39b2eeaa1be9116 docs/TEST_REPORT.md +2fe8ac5e426daec07395b4b1dde4cbd6a6a7def033ac062f6183ad53e29b8606 docs/TEST_REPORT.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md 8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md @@ -103,6 +103,7 @@ bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_A 05e0b4cc074b07ff37d45fabc20c7272bada217286b3db3d7997cae25c1f6f11 docs/V0.26.1_IMPLEMENTATION.md 91774f2c23f587a502c65fe2ffaf009aba81b221cb3ede4861ea79abb4d4a528 docs/V0.26.2_IMPLEMENTATION.md 57edc3d5ca6c268b2b6b2e8027e81fb543c0dab86e8f69c4ee5776ea8df901c7 docs/V0.26.3_IMPLEMENTATION.md +da5ec87b9482f2fc506c4334d9365f3bfaec89254a14e5585dd6460326b20e10 docs/V0.26.4_IMPLEMENTATION.md ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md b979afd3d56731eeb06e9dc38d37730693e7fbf3aa78467932842c0e712e7b26 docs/VHOST_ACCESS.md f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md @@ -117,14 +118,14 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock- 5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json -ce0094ff903e0fcc979efbcd0a79d7aeb6d59eb8aa212c8c31fdbe8c742fdd12 grafana/README.md -e93062ce0b61ddc6ee46f868c7de80a8ae00dfbe23542a887c9d096dadbbfd47 grafana/rustproxy-overview.json +5c30f169bd7a034a843c7a4e6ccfb923ce009561c8d30c37a70d3c9841bbdbf1 grafana/README.md +ca48eeeb4c7019647fb94ab7d4ca594d1cf03a556c1a8d1c5cfe1fd2d4e96300 grafana/rustproxy-overview.json 30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh 858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh 29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md -17e39fd69378d4fdad965dcce96006393c8d2469b91d78611b73aacbd6a4ad74 langs/en_US.json -f67f061e72bc14ebfcf883fab8c10e625e399157017e696e4a55cc26854cff08 langs/pl_pl.json -089cd7c39e6e3c79f5e02b7ffe38648cef6b5cb89cc5ac6d6e7e249f0f91ee0b make_zip.py +8878f138bc909c004181e6ba91bad14c0bf96ea81235c972c3b8d86a640e1b4e langs/en_US.json +695ba1b195fb302dd05574049db98ee0bbe07fe71ef6a1c98dd43776d354235f langs/pl_pl.json +038191475df4ecde40905bae55615ca207aeb05d17aa9bdc13728d4854dd77b5 make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql @@ -143,11 +144,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py 20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py -2f91b721757d75e88fdef4cdfd1fcb87a56f95b4470cbb8b86894fb23d6976c7 scripts/check_source.py +9fd134172caa4de5308eb7437a1f7ad1f992ab0053d01b4b7d1f4cf6804e6389 scripts/check_source.py 187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py -39932ae4797c966703fce9e2f999891066f7c3541dd171f02d2e37e88be2ef0d scripts/deploy_core.py +a763d2dc2fec8cffe10714063364f40e4d08bd6412fabcfdefab828580b8d9cd scripts/deploy_core.py f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py -fab405bd0bfa9a8f8f70462aa536b4fc8ac9976e6bab25751726309ec679c6f7 scripts/docs_check.py +f399b2ca97ba0c11675197ddf96ce6d8f233a2e1983619041c26e70bbef65558 scripts/docs_check.py 6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py 2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py @@ -157,8 +158,8 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a 2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py 2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh -d3c8a25c1b80eb00df494a1a8dab4a6325dd0a7b6e0d9f07e5bbfa511e9eb500 scripts/preflight.py -31f0a16da2d97740be5130a76b128b6de617c21f1a66990f1c6192e1a72c2ec9 scripts/release_smoke.py +4b000848d10f552d39cab5be309fc455e2f3b90490762fe1055e7abd06968bc3 scripts/preflight.py +40c260046c93a43e12cd90e303b92de2c65d30dcdb365d68a739510749172b95 scripts/release_smoke.py 45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example 3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros 7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore @@ -170,11 +171,11 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s 1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py 295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py 6fefb5c459aacdef3dcabe9c5e6e7718a2145a0190b165c29d7a46216ac53bad scripts/source_module_contracts.py -44d2b525cc7caf8fcfa4db67e3ec6b550bcb48623893acf4bacf0fb6b0d3bf60 scripts/swagger_assets.py +28f516f8c5f686dfc72ebef0cb0338d06cbd4eb9f5ff93602f0dfbae3e9a871f scripts/swagger_assets.py 102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py -d5937dca7ae4e360c6d9bdbd2386f52826d0e5554d0db9abd617bab9cefb80fb scripts/upgrade.py +010bc2454701e42e458e07a86c16ca62a9fa00449a9638ab0425b08739f07a91 scripts/upgrade.py 0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f scripts/web_assets.py -0b3556f0cbaa4fe9025634ebac49a6eeea7151879fa7c72419432033b5375b28 scripts/web_server_contracts.py +f22b1b4211b76851fb8be7d5330af0b31fe7732a080f342ce5116bb27582a26a scripts/web_server_contracts.py ced042b5e283fd40fe25c261457dceb57d946dffebd89876cd80d69b92823932 scripts/web_service_ui_test.mjs d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs 4e3be1ea1be0a81529c7c9021fb9bb77c24e5044acd54094972745443ad2c26a src/acl/tests.rs @@ -187,13 +188,13 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi 55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs 85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs 1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs -e4aac0947a38d0cf6160328db853df940546a7b6b758fea4a33eedaf56f9f791 src/admin/documentation/documents.rs +eab5d50e7326e2224b41fc8b5018a89a06ed9aa1f99954f84bf17d992e78bcf2 src/admin/documentation/documents.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs 002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs 58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs -20c73fc2d6cc0efda6016d2a9830815bea6882faf23f5a3fdb23bb1af67e48f7 src/admin/openapi.json +8b8ae7ebefe9a74ac47e3e024da2abb38fdde02109dfe7924eca8413745ff157 src/admin/openapi.json a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs e9ea015aabdfdbb2f8dd72ea734002b5d648dc1bbef4cc4a0b85d2bcc059e9a5 src/admin/preferences.rs b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs @@ -211,10 +212,10 @@ f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/test 5fa006413fd93529ac9aaa4ffb28bf6b0b88d0a55a0bf0c49576db4a06fe9fc3 src/admin/tests/part_07.rs 9afbca5b4f3d17690c2a7aedf0fd08de06e04c5e492b151d9969a326e07b19b8 src/admin/web/acme.js 446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js -77db5269365ee387f4e83d08e665bc1939586676431dc9153f874267b3613100 src/admin/web/app/core.js -520de5c5df22aa0c26d472e83533ca288cc71ca0f75db62912b2c2fb7ab1f1c3 src/admin/web/app/preferences.js +860bce21c27ced60f3d37344c1715d9c60899fa08f5f01c590f0ea81690b014a src/admin/web/app/core.js +145430fa86e8111f2cc8c0ee484eb732df84d42cc8b628f4c2097b8f33bf51d9 src/admin/web/app/preferences.js d7f6675277c927f4eae1b83c7aa91b315429e7034dfe61c489c2f737a9c19f07 src/admin/web/app/runtime.js -7642150e62978fb7dc1362b862a3805ee2e4f764a5a31767226825d32097c62a src/admin/web/app/settings.js +8afb11a17fa61d42390667d076ae1485a7398be9b095d235f1486d552d410620 src/admin/web/app/settings.js 642d013e2e18dc8a8eccc7a2fe93d6570521dc9a0644beac7af4bbe8c5cea1ee src/admin/web/cache-panel.js 50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html 3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js @@ -230,21 +231,21 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js -dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js -ab1726532cae53df4e653d80c4855ccad069fbe0446c513492c64cede361b69d src/admin/web/index.html +631860ab3793ffd827c7cf19ddd049d14419509f2c2d8b27994b0e54baa0e32a src/admin/web/host404.js +b2b8b7ac06c79c7d69125d2cf092b33114d052b517dff37312eda023019c39a3 src/admin/web/index.html 24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html 45ca44ca82e43373f018629839287645218f225747f02b9b4a5d80e2bc8821cf src/admin/web/login.js 1b096d96a9d6b904a87bd4194fa40f22310a42c765f3ffa299c4da1f14c6e248 src/admin/web/management.js -e65311ad425bd91364ee8c0cc3f35671e1bcc726bea533e4a10d9e1f57706e12 src/admin/web/redirects.js +a2dbdd205b899b92cb9817f76d0c79a27b153c80ac0fd60da435c7a4fddbb958 src/admin/web/redirects.js f1ab20afa09f75f3635511756e36afea46a7917f04937d6f4e89c4e9f3f43204 src/admin/web/settings.js -f25a4395dc3ba7bf152fffb577aedfbf0aa4d06ea442ea78ef7f2a62e9922816 src/admin/web/streams.js -f2ff4b717fdb12003f2c9841c455a783c336a1e9a78ca1ca228c645578c1badb src/admin/web/style.css +f57c79477160dda93ba90aecb4c9293383caef3b3f8829a25608ad85954356f8 src/admin/web/streams.js +778b0b5fba6a774afb3c79a5b57b4591221cfd1ae770322342deab99e6179603 src/admin/web/style.css a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js 6856c230c4f6b18145e16efb6636433f8b21ce3ec5d34246d6561f0150ea0554 src/admin/web/vhosts/core.js -d4938e0bf8cf41f79e9b49cf9b5cf2c8340668e792b73d5350cb1872e7ef1edf src/admin/web/vhosts/editor.js +4d4ee863a3d77e5213ae4bdcad1f533a6222353b9c32e0b5f12f7ea60ab62bea src/admin/web/vhosts/editor.js e0011edc708d245cb514be170c1bbe846a0298b81d86276fe29af05010d215a4 src/admin/web/vhosts/render.js b3d8d3e22e4cbab10c5d588c268da6eb3075a9ec857728ce6b5d56e3ffd3c9a6 src/admin/web/web-server.js 98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs @@ -259,7 +260,7 @@ b0efb68dec4b0e8611684457b21044d136882ddb5c529f934061a55d377d9b28 src/backup.rs c8ee6afde0e463828757a9c2aca25506eb0a7b3b9369f4ec955857ab04b1f9d1 src/backup/validation.rs 426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs 3d4b3986f54bed99212063b6d135d40a202d556a3e4d253e24c74be4cb9e8ebc src/balance/health.rs -5fef8187a5ca3d22cc76afc72bac211e502c7cffd230353190277d8b9daff583 src/balance/lease_runtime.rs +dc9335e018d61cddfe3b4c9fa480913f4444c83e4fb1384e3b046cc40940a2c3 src/balance/lease_runtime.rs 91c5cef82ece69765858c22c61feb2fb90f38d292cd5bce7ee4fca8f4cb5cd42 src/balance/model.rs b5bb2e5d389fc0290c362af8b28c7d625c836876e308bbdcdd7cffcfee2e5e6d src/balance/pool_core.rs 477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs @@ -350,11 +351,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo 17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs 3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs 46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs -777cc490f53417047b90ba359aa400961f10234b88684b47df9a5025e3929c36 src/main/bootstrap.rs +d92c3414cf7a86b027042d14d5cedc8dd8adeeafd7bc961032e2c5f322d01d4f src/main/bootstrap.rs 5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs 4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs 0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs -689d0eb4637b826c3802818a00f689eb7b40c3c96918d86377efb9ce047df01b src/main/tasks.rs +88edd345a0ea0eb9dd6800e990661639ec810997bc7b1206006a3d85707af76c src/main/tasks.rs 49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs 49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs @@ -449,8 +450,8 @@ a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/ 639ed68b2c62a9c8ef23beac480ed4ecb0a7d31672177c9e0bde6c223137fac1 src/update_check.rs c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs 0399f6211c6e33fa12298acc40957f1532ace235c3950c89050a3450ee127299 src/upstream.rs -c16b95930f3e5e700961ebc2817c271931b78739707bbfafcb41d67d95a8d19b src/upstream/client.rs -aec8868ba0823bc5ea4ab690caff91cc972b333464993d494ced8a949b5ff173 src/upstream/client_tests.rs +19d707f8b14ea1b2828a8d51bbacf7505bb0fad79c458d855b80297b879e9ccc src/upstream/client.rs +4aa2420a92d33c50f7027f57ac4eccdc050bdb7e9ec8fa6e0eaa809228b71cd8 src/upstream/client_tests.rs 3a4b097c0d5c0d88fbe881d84bd26a89789f68e4c46ea68b9e4a923dd4977d0b src/upstream/tests.rs e4c1ff8f53112e7cfad101a266355f7e47b82895480db431662f4cc59a1d9702 src/vhost.rs f5bf21d13cb094706ae42c8b5cb760e12b4cf9c71a559c57ac0e1661b40cc17c src/vhost/model.rs @@ -461,7 +462,7 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali 9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs 6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs 86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs -f11eb148a9f5f3c8f65ad067f497d88e19375306cfd10016110863b4a2bd6c75 src/web_server/fastcgi/protocol.rs +1e5b3265e4aca4de07dc93a471faeaf248dbf843ed845a11b50226e9b61c950d src/web_server/fastcgi/protocol.rs 134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs 8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs 0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs @@ -471,14 +472,14 @@ f11eb148a9f5f3c8f65ad067f497d88e19375306cfd10016110863b4a2bd6c75 src/web_server 80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs 67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs -6b5506a79c48c58b9ee69bdcbed16fba63bd3b2dc5c11ddb2deb9b087cc8f35c src/web_server/response/file.rs +4ef82ec2746afd520c43f7c823a059a1ae903f840680f3da812aeaf8d8655a51 src/web_server/response/file.rs 9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs 2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs 400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs 3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs -aa142445a5c7a50f6f667a2b84ec25f80ea79790bb0d0aca3c922dfc3118594b src/web_server/tests/part_02.rs +51b15848e0a43b08299c1e0420614966559c387a7f8731885b50399aa4ca8c6d src/web_server/tests/part_02.rs e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json 1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs diff --git a/debian/README.md b/debian/README.md index 5a3d3a3..2018d7b 100644 --- a/debian/README.md +++ b/debian/README.md @@ -1,6 +1,6 @@ # Debian packaging -This directory builds the `rustproxy` binary package for RustProxy 0.26.3. +This directory builds the `rustproxy` binary package for RustProxy 0.26.4. The package installs: - `/usr/bin/rust-proxy` diff --git a/debian/build-package.sh b/debian/build-package.sh index 42a9808..b95ce92 100755 --- a/debian/build-package.sh +++ b/debian/build-package.sh @@ -21,6 +21,6 @@ cargo fetch --locked dpkg-buildpackage -us -uc -b mkdir -p dist -find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.3_*.deb' -o -name 'rustproxy_0.26.3_*.changes' -o -name 'rustproxy_0.26.3_*.buildinfo' \) \ +find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.4_*.deb' -o -name 'rustproxy_0.26.4_*.changes' -o -name 'rustproxy_0.26.4_*.buildinfo' \) \ -exec cp -f {} dist/ \; echo "Packages copied to $ROOT/dist" diff --git a/debian/changelog b/debian/changelog index fc34548..214bfcd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +rustproxy (0.26.4) stable; urgency=medium + + * Add 404-host draft support and standardize modal Enabled controls. + * Compact toolbar inputs and Overview-card appearance controls. + * Recycle aged backend client pools without waiting for another request. + + -- RustProxy Maintainers Tue, 06 Oct 2026 11:05:00 +0200 + rustproxy (0.26.2) stable; urgency=medium * Attribute persistent protocol errors to configured vhosts using route, Host/authority or SNI. diff --git a/dev.sh b/dev.sh index 0487b5a..0cd39f4 100755 --- a/dev.sh +++ b/dev.sh @@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi case "$mode" in help|-h|--help) cat <<'HELP' -RustProxy 0.26.3 +RustProxy 0.26.4 ./dev.sh Build and start proxy + local panel ./dev.sh run [serve flags] Same, with extra server options ./dev.sh admin setup-token Generate/print a one-time setup capability before first login @@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then printf 'Checking all Rust targets before tests...\n' >&2 cargo check --locked --all-targets fi -printf 'Building RustProxy 0.26.3...\n' >&2 +printf 'Building RustProxy 0.26.4...\n' >&2 cargo build --locked BIN="$CARGO_TARGET_DIR/debug/rust-proxy" case "$mode" in diff --git a/docker-agent/model.go b/docker-agent/model.go index 151b54a..fe1d4cc 100644 --- a/docker-agent/model.go +++ b/docker-agent/model.go @@ -1,6 +1,6 @@ package main -const version = "0.26.3" +const version = "0.26.4" type config struct { Listen string diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md index 905ca53..281eca7 100644 --- a/docs/API_ACCESS.md +++ b/docs/API_ACCESS.md @@ -1,4 +1,4 @@ -# Administration API access - 0.26.3 +# Administration API access - 0.26.4 ## Sign-in boundary diff --git a/docs/FILE_LOGS.md b/docs/FILE_LOGS.md index f15e706..00fa9d2 100644 --- a/docs/FILE_LOGS.md +++ b/docs/FILE_LOGS.md @@ -1,6 +1,6 @@ # Stored log explorer -RustProxy 0.26.3 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent. +RustProxy 0.26.4 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent. ## Separation from Live Logs diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 5f6d00a..be90a59 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,4 +1,4 @@ -# Installing and updating RustProxy 0.26.3 +# Installing and updating RustProxy 0.26.4 The scripts prepare a Linux/systemd installation. This source archive is **not a verified production build**: Rust/Cargo, real CA operations, live systemd startup diff --git a/docs/README.md b/docs/README.md index da24fc2..90744e6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic - [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards. - [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status. - [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates. +- [0.26.4 implementation note](V0.26.4_IMPLEMENTATION.md) - 404 drafts, consistent modal controls, compact Overview settings and idle age-based backend rotation. - [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish. - [0.26.2 implementation note](V0.26.2_IMPLEMENTATION.md) - unified Logs navigation, field-query search, dynamic JSON columns and RAW parsing. - [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - historical vhost-aware protocol logging and Stored Logs UX milestone. @@ -63,7 +64,7 @@ This is the canonical index for the current project documentation. The authentic ## Historical or version-labelled context -Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md); [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) is retained as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence. +Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.26.4_IMPLEMENTATION.md](V0.26.4_IMPLEMENTATION.md); [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md) is retained as the prior appearance/UI milestone, [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence. Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index c93a8bd..eec0096 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,20 +1,18 @@ -# Verification report - RustProxy 0.26.3 +# Verification report - RustProxy 0.26.4 ## Scope -RustProxy 0.26.3 keeps the 0.26.2 log workflow and focuses on administration appearance, scaling and draft/settings ergonomics. Stored Logs and Live Logs now share one top-bar Logs menu while retaining independent data paths. +RustProxy 0.26.4 is a compatibility-preserving administration/UI and backend-client lifecycle maintenance release. SQLite remains schema 8 and the saved proxy configuration format is unchanged from 0.26.3. -Stored Logs replaces the fixed event/status/client/vhost controls with an optional source selector, one bounded field-query input and page size. The query parser supports `field:value`, explicit `AND`, `!` negation, quoted values, wildcards, numeric comparisons and aliases such as `req` -> `method`. JSON/JSONL records expose a bounded discovered-field list for dynamic columns; protocol-specific fields such as `matched_by` and `sni` are no longer hard-coded table columns. RAW error-log lines are best-effort parsed into timestamp, level, target, message and detected `key=value` fields while preserving `raw`. +The release adds 404-host forms to the existing session-only draft dock; completes the Polish Button style translation; makes compact toolbar search/filter inputs follow the selected button height; reuses the vhost Enabled-state presentation in 404-host, Redirect and Stream editors; replaces the tall Overview-card checkbox list with compact visibility/reorder tiles; and makes age-based upstream-client recycling run from backend maintenance even when no request arrives. -The dashboard removes the Configured files inventory but keeps an optional source selector with `All configured files` as the default, moves the column picker to the table toolbar, expands selected rows as parsed fields plus optional raw JSON, and uses `↑`/`↓` for backend sort direction. SQLite schema 8 and the saved proxy configuration format remain unchanged. - -This workspace retains session-only draft docking for dirty Web Server, Vhost, Redirect and Stream editors; opening a second editor no longer reuses or blocks the suspended form state. In 0.26.3 the dock uses the same square-corner panel language as the rest of RustProxy, can collapse to one bar and shows draft age. **Settings -> Appearance** persists theme, language, 70-125% global interface scale, 12/24-hour display, eight chart styles, Normal/Compact button style, table/log density, dashboard density and Overview-card layout/visibility in the existing account-scoped administrator preference document. Scaling includes dialogs, overlays and the draft dock; form fields also receive a bounded additional size response so control proportions follow 70-125% more visibly. Compact buttons restore the smaller source-style control dimensions without changing the whole-interface scale and now apply explicitly to generated vhost/entity actions and dialog tab buttons. Settings adds content search; Appearance exposes chart palette previews, per-section resets and a full reset. Expanded Stored Logs details remain compact. SQLite schema 8 remains unchanged. +The backend change keeps request-count recycling request-driven. The new maintenance path checks `pool_recycle_seconds`, uses the existing atomic generation/rotation guard and starts an idle-maintenance generation with zero requests. A Rust unit regression is included for age-based rotation without a triggering request, but this workspace cannot execute Rust tests because Cargo/rustc are unavailable. ## Source-level regression coverage -0.26.2 log-query Rust tests continue to cover field aliases, explicit AND, negation, quoted values, wildcard/status/numeric matching and built-in RAW error parsing. Retained 0.26.1 tests continue to cover canonical field recovery, safe source derivation, symlink refusal, bounded reads and protocol diagnostic normalization. +Rust-independent contracts now require 404 draft registration, the shared Enabled-control helper, compact Overview tiles, toolbar controls bounded by `--button-min-height`, and backend maintenance calls for ordinary and WebSocket clients. Existing source, security, memory, Web Server, documentation, packaging, upgrade and SQLite migration contracts remain enabled. -Administration preference validation now permits up to 64 bounded, control-free Stored Logs field names so dynamically discovered JSON fields can be persisted per account. It also validates the language, 70-125% interface-scale, time-format, expanded chart-style fields, Normal/Compact button style, table/log density, dashboard density, dashboard card ordering and hidden-card list while retaining defaults for legacy preference JSON. Browser JavaScript is syntax-checked, the Web Server UI harness loads the draft manager in production order, and the language/documentation/source-contract suites cover the new UI strings and release identity. +Browser JavaScript syntax checks cover the changed core/preferences/404/Redirect/Stream/vhost editor files. The simulated Web Server UI test remains green. Language packs and OpenAPI JSON parse successfully, and the Docker Agent Go tests pass. ## Verification status in this workspace @@ -26,19 +24,19 @@ Administration preference validation now permits up to 64 bounded, control-free | `python3 scripts/security_contracts.py` | PASS | | `python3 scripts/security_dependency_contracts.py` | PASS | | `python3 scripts/source_module_contracts.py` | PASS | -| `python3 scripts/rust_module_contracts.py` | PASS after keeping modified Rust modules within the 500-line contract | +| `python3 scripts/rust_module_contracts.py` | PASS | | `python3 scripts/docs_check.py` | PASS | -| `node scripts/web_service_ui_test.mjs` | PASS | -| `node --check` for `settings.js`, `drafts.js`, `app/preferences.js`, `traffic-panel.js` and `file-logs.js` | PASS | -| language/OpenAPI JSON parse | PASS | | `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime as not verified | -| `python3 scripts/release_smoke.py` | PASS, including upgrade/package/reproducibility contracts | +| `python3 scripts/release_smoke.py` | PASS, including SQLite migrations, stopped-source upgrade and deterministic source packaging contracts | +| `node scripts/web_service_ui_test.mjs` | PASS | +| `node --check` for changed administration JavaScript | PASS | +| language/OpenAPI JSON parse | PASS | | `go test ./...` in `docker-agent` | PASS | | `./dev.sh test` | Rust-independent gates PASS; exits 127 when the Rust stage is reached because Cargo is unavailable | | `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | | `cargo fmt --check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | | `cargo clippy --all-targets --all-features -- -D warnings` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | -| `cargo test --all-targets --all-features` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | -| `bash dev.sh` complete release build | BLOCKED at Rust compilation: Rust/Cargo toolchain unavailable in this workspace | +| `cargo test --all-targets --all-features` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace; the new age-rotation unit test is therefore not runtime-executed here | +| complete release build | BLOCKED at Rust compilation: Rust/Cargo toolchain unavailable in this workspace | -The source archive must not be described as compiler-verified until the Rust-enabled release-host gates pass. Rust-independent gates verify source structure, security contracts, documentation, browser JavaScript, packaging invariants and deterministic archive behavior, but they cannot substitute for Rust parsing, type checking, Clippy or runtime tests. +The 0.26.4 source archive must not be described as compiler-verified until the Rust-enabled release-host gates pass. Rust-independent checks verify source structure, UI/translation contracts, security invariants, migrations, upgrades, documentation, JavaScript syntax, Go-agent behavior and deterministic archive behavior, but they do not substitute for Rust parsing, type checking, Clippy or runtime tests. diff --git a/docs/V0.26.4_IMPLEMENTATION.md b/docs/V0.26.4_IMPLEMENTATION.md new file mode 100644 index 0000000..174c037 --- /dev/null +++ b/docs/V0.26.4_IMPLEMENTATION.md @@ -0,0 +1,30 @@ +# RustProxy 0.26.4 implementation note + +## Scope + +0.26.4 is a compatibility-preserving administration and backend-lifecycle maintenance release. SQLite remains schema 8 and the saved proxy configuration document is unchanged from 0.26.3. + +## Administration UI + +- The 404-host editor now registers with the existing session-only draft manager. Dirty forms are captured with their host/listener editor nodes and read closures, can be minimized into the draft dock, and restore the exact unfinished form state. +- The shared Enabled presentation used by vhosts is exposed as one administration helper and reused by 404 hosts, Redirects and Streams. State text, hint text and enabled/disabled visual treatment therefore stay consistent across these editors. +- Polish Button style labels/help are translated instead of falling back to English values. +- Search/filter controls in compact toolbars use the active button minimum height so they do not visually exceed adjacent actions. Ordinary form controls keep their existing sizing. +- Appearance -> Overview cards uses a responsive tile grid instead of one checkbox row per metric card. A tile toggles visibility while the existing up/down controls retain ordering behavior and account-scoped persistence. + +## Backend client rotation + +Request-count recycling remains request-driven. Age-based recycling is now also checked by the existing backend health/maintenance loop. This removes the dependency on a new request arriving after `pool_recycle_seconds` and allows idle backend clients to advance generation/rotation counters on schedule. + +Maintenance-driven rotation starts the new generation with zero requests; request-driven rotation continues to count the request that triggered the swap in the new generation. The rebuild remains guarded by the existing generation/rotation atomics, so concurrent request and maintenance checks cannot double-rotate the same generation. + +## Compatibility + +- SQLite schema: 8 (unchanged). +- Saved proxy configuration format: unchanged. +- Administration preferences: existing fields only; no migration required. +- Upgrade from 0.26.3: no data migration required. + +## Verification + +See [TEST_REPORT.md](TEST_REPORT.md). This workspace can run the Rust-independent source, documentation, browser-JavaScript, packaging and Go-agent gates. Rust compiler gates must still be run on a release host with Cargo before calling the archive compiler-verified. diff --git a/grafana/README.md b/grafana/README.md index 77a186c..e86e237 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw are intentionally absent from Prometheus labels. Import the JSON in Grafana and select a Prometheus datasource when prompted. The -dashboard expects RustProxy 0.26.3 metric names and a scrape interval around 15s. +dashboard expects RustProxy 0.26.4 metric names and a scrape interval around 15s. Collectors can be disabled in RustProxy settings; panels for disabled collectors will simply show no series. diff --git a/grafana/rustproxy-overview.json b/grafana/rustproxy-overview.json index c88ee37..7c41be6 100644 --- a/grafana/rustproxy-overview.json +++ b/grafana/rustproxy-overview.json @@ -4529,9 +4529,9 @@ }, "timepicker": {}, "timezone": "browser", - "title": "RustProxy 0.26.3 - Operations", + "title": "RustProxy 0.26.4 - Operations", "uid": "rustproxy-0170", "version": 5, "weekStart": "", - "description": "RustProxy 0.26.3 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic." + "description": "RustProxy 0.26.4 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic." } diff --git a/langs/en_US.json b/langs/en_US.json index 057edda..5f3f213 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -2072,6 +2072,7 @@ "buttonStyle": "Button style", "buttonStyleNormal": "Normal", "buttonStyleCompact": "Compact", - "buttonStyleHelp": "Normal uses the refreshed controls; Compact restores the smaller source-style buttons across all actions and dialog tabs." + "buttonStyleHelp": "Normal uses the refreshed controls; Compact restores the smaller source-style buttons across all actions and dialog tabs.", + "entityEnableHint": "Controls whether this item is active." } } diff --git a/langs/pl_pl.json b/langs/pl_pl.json index 5562416..ab055e5 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -2070,8 +2070,9 @@ "draftAgeHours": "{count} godz.", "draftAgeDays": "{count} d", "buttonStyle": "Styl przycisków", - "buttonStyleNormal": "Normal", - "buttonStyleCompact": "Compact", - "buttonStyleHelp": "Normal używa odświeżonych przycisków; Compact przywraca drobniejsze przyciski ze źródłowego interfejsu we wszystkich akcjach i zakładkach modali." + "buttonStyleNormal": "Normalny", + "buttonStyleCompact": "Kompaktowy", + "buttonStyleHelp": "Normalny używa odświeżonych przycisków; Kompaktowy przywraca mniejsze przyciski ze źródłowego interfejsu we wszystkich akcjach i zakładkach modali.", + "entityEnableHint": "Określa, czy ten element jest aktywny." } } diff --git a/make_zip.py b/make_zip.py index 2c81dcc..77197f8 100755 --- a/make_zip.py +++ b/make_zip.py @@ -88,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.3.zip') + parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.4.zip') output = parser.parse_args().output.resolve() paths = files() checksum = ROOT / 'SHA256SUMS' diff --git a/scripts/check_source.py b/scripts/check_source.py index d2288a4..4ae8f6a 100755 --- a/scripts/check_source.py +++ b/scripts/check_source.py @@ -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.26.3" +assert manifest["package"]["version"] == "0.26.4" assert manifest["dependencies"]["quinn"]["version"] == "0.11.12" h3dep = manifest["dependencies"]["h3"] assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" 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.26.3"' in lock + assert 'name = "rust-proxy"\nversion = "0.26.4"' 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.26.3", - "CHANGELOG.md": "## 0.26.3", - "ROADMAP.md": "## 0.26.3", - "AGENTS.md": "Current source version: 0.26.3.", - "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.3", + "README.md": "# RustProxy 0.26.4", + "CHANGELOG.md": "## 0.26.4", + "ROADMAP.md": "## 0.26.4", + "AGENTS.md": "Current source version: 0.26.4.", + "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.4", }.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" @@ -160,6 +160,11 @@ assert 'id="security-password-editor"' in html and 'id="security-policy-editor"' assert 'docker-service-editor' in (root / "src/admin/web/docker.js").read_text() and 'docker-agent-details' in (root / "src/admin/web/docker.js").read_text() balance_source = rust_source(root, "src/balance.rs") assert 'mode: HealthMode::Tcp' in balance_source +assert 'member.backend.client.maintain_rotation();' in balance_source +assert 'member.backend.websocket_client.maintain_rotation();' in balance_source +upstream_client_source = rust_source(root, "src/upstream/client.rs") +assert 'fn maintain_rotation(&self)' in upstream_client_source +assert 'self.rotate(self.generation.load(Ordering::Acquire), "age", false);' in upstream_client_source for marker in [ 'pub struct PassiveHealth', 'enabled: false', 'pub passive_health: PassiveHealth', 'passive_health_failures_total', 'http_5xx_total', 'timeouts_total', @@ -185,13 +190,17 @@ assert '"forward-client-ip", "vhForwardClientIp"' in vhosts_js and 'forward_clie assert 'id="vhost-search"' in html and '$("vhost-search").addEventListener("input", render)' in vhosts_js for marker in ['id="pool-search"', 'id="pool-sort"', 'data-pool-column-toggle="vhost"', 'data-pool-column="generationAge"']: assert marker in html, marker -for marker in ['toggleVhostEnabled', 'vhQuickEnable', 'vhQuickDisable', 'backend_table_columns', 'applyPoolTableView', 'saveSection("vhosts", next.vhosts || [])', 'vh-enable-control']: +for marker in ['toggleVhostEnabled', 'vhQuickEnable', 'vhQuickDisable', 'backend_table_columns', 'applyPoolTableView', 'saveSection("vhosts", next.vhosts || [])', 'rustProxyEnableControl']: assert marker in vhosts_js, marker assert 'rustproxy-backend-table-v1' not in vhosts_js and 'poolTablePreferenceKey' not in vhosts_js assert 'window.rustProxyPreferences.update({' in vhosts_js and 'backend_table_sort' in vhosts_js and 'backend_table_descending' in vhosts_js style_ui = (root / 'src/admin/web/style.css').read_text() -for marker in ['--scrollbar-thumb', '*::-webkit-scrollbar-thumb', '.backend-table-tools', '.vh-enable-control', '.vhost-quick-toggle']: +for marker in ['--scrollbar-thumb', '*::-webkit-scrollbar-thumb', '.backend-table-tools', '.vh-enable-control', '.vhost-quick-toggle', '.dashboard-widget-toggle', 'height: var(--button-min-height)']: assert marker in style_ui, marker +preferences_js = (root / "src/admin/web/app/preferences.js").read_text() +assert 'dashboard-widget-toggle' in preferences_js and 'visible.type = "checkbox"' not in preferences_js +core_js = (root / "src/admin/web/app/core.js").read_text() +assert 'window.rustProxyEnableControl = enhanceEnabledControl' in core_js assert 'id="admin-session-days"' in html and 'min="7" max="30"' in html assert 'member-local-risk' in vhosts_js and 'vhLocalListenerRiskConfirm' in vhosts_js assert 'accept_local_listener_risk: yes("member-local-risk", row)' in vhosts_js @@ -240,13 +249,13 @@ 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.26.3"', '"/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.26.4"', '"/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 assert 'id="panel-notfound"' in html and 'id="notfound-dialog"' in html and 'id="notfound-hosts-editor"' in html host404_js = (root / "src/admin/web/host404.js").read_text() -for marker in ['rustProxyNotFound', 'saveSection("not_found_hosts"', 'rustProxyIntegrations.hostnames', 'rustProxyIntegrations.bindings', 'BUILTIN_STATUSES', 'ERROR_STATUSES']: +for marker in ['rustProxyNotFound', 'saveSection("not_found_hosts"', 'rustProxyIntegrations.hostnames', 'rustProxyIntegrations.bindings', 'BUILTIN_STATUSES', 'ERROR_STATUSES', 'rustProxyDrafts.register("notfound"', 'rustProxyEnableControl($("notfound-enabled"))']: assert marker in host404_js, marker assert 'for (let code = 200; code <= 599; code++)' not in host404_js assert 'preserve_host: true' in vhosts_js, "new HTTP/1.1 backend drafts must preserve the public Host" @@ -1203,7 +1212,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.26.3"' in openapi +assert '"RuntimeInformation"' in openapi and '"version": "0.26.4"' in openapi # 0.24.1 compiler and hostname-cardinality regressions. tls_0241 = rust_source(root, "src/tls.rs") @@ -1248,5 +1257,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]: node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"] assert "maxItems" not in node, schema_name -print("PASS: 0.26.3 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.") +print("PASS: 0.26.4 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.") print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.") diff --git a/scripts/deploy_core.py b/scripts/deploy_core.py index 38b4241..815c754 100755 --- a/scripts/deploy_core.py +++ b/scripts/deploy_core.py @@ -97,7 +97,7 @@ def digest(path: Path) -> str: def fetch_https(url: str, max_bytes: int) -> bytes: if urllib.parse.urlparse(url).scheme != 'https': raise ValueError('GeoIP downloads require HTTPS.') - request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.3'}) + request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.4'}) with urllib.request.urlopen(request, timeout=30) as response: if urllib.parse.urlparse(response.geturl()).scheme != 'https': raise ValueError('GeoIP download redirected away from HTTPS.') diff --git a/scripts/docs_check.py b/scripts/docs_check.py index e0e4e41..ee5ec96 100755 --- a/scripts/docs_check.py +++ b/scripts/docs_check.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Rust-independent documentation/Reader consistency checks for the 0.26.3 release.""" +"""Rust-independent documentation/Reader consistency checks for the 0.26.4 release.""" from pathlib import Path import re from rust_source import rust_source @@ -55,16 +55,16 @@ 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.26.3") +assert readme.startswith("# RustProxy 0.26.4") 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 -assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.3" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap +assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap assert changelog.startswith("# Changelog\n") -for release in ["0.26.3", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]: +for release in ["0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]: assert f"## {release}" in changelog, release -assert changelog.index("## 0.26.3") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5") +assert changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5") assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]: assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}" diff --git a/scripts/preflight.py b/scripts/preflight.py index 2b3d4b6..822c84f 100755 --- a/scripts/preflight.py +++ b/scripts/preflight.py @@ -75,10 +75,10 @@ def check(root: Path, repair_known: bool = False) -> list[str]: raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}") for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]: if not (root / path).is_file(): - raise ValueError(f"Incomplete RustProxy 0.26.3 source tree: missing {path}") + raise ValueError(f"Incomplete RustProxy 0.26.4 source tree: missing {path}") text = (root / "src/lib.rs").read_text(encoding="utf-8") if '#[path = "auth/mod.rs"]' not in text: - raise ValueError("src/lib.rs is not the 0.26.3 entry point; extract the complete package, not individual files.") + raise ValueError("src/lib.rs is not the 0.26.4 entry point; extract the complete package, not individual files.") return messages diff --git a/scripts/release_smoke.py b/scripts/release_smoke.py index 1b17613..173170a 100755 --- a/scripts/release_smoke.py +++ b/scripts/release_smoke.py @@ -123,7 +123,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.26.3"' in (target / "Cargo.toml").read_text(encoding="utf-8") + assert 'version = "0.26.4"' 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() @@ -134,7 +134,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.26.3" + assert manifest["to_version"] == "0.26.4" assert manifest["removed_obsolete_runtime_state"] is True assert not legacy.exists() assert not (backup / "legacy-data").exists() @@ -381,7 +381,7 @@ def ui_contract_smoke() -> None: assert "/assets/host404.js" in html for marker in ["rustProxyRedirects", 'saveSection("redirects"', "redirectTargetType", "redirectPathMode", "redirectQueryMode", "redirectCacheSeconds"]: assert marker in redirects, marker - for marker in ["rustProxyNotFound", 'saveSection("not_found_hosts"', "rustProxyIntegrations.hostnames", "rustProxyIntegrations.bindings", "notfound-mode", "notfound-status", "BUILTIN_STATUSES", "ERROR_STATUSES"]: + for marker in ["rustProxyNotFound", 'saveSection("not_found_hosts"', "rustProxyIntegrations.hostnames", "rustProxyIntegrations.bindings", "notfound-mode", "notfound-status", "BUILTIN_STATUSES", "ERROR_STATUSES", 'rustProxyDrafts.register("notfound"', 'rustProxyEnableControl($("notfound-enabled"))']: assert marker in host404, marker assert 'for (let code = 200; code <= 599; code++)' not in host404 assert 'id="docker-search"' in html and 'id="docker-show-hidden"' in html @@ -392,14 +392,14 @@ def ui_contract_smoke() -> None: assert "vhForceSslNeedsTls" in vhosts and "vhForceSslNeedsHttp" in vhosts and "vhForceSslPortConflict" in vhosts assert "tlsPorts.has(443)" in vhosts assert '$("vhost-search").addEventListener("input", render)' in vhosts and "vhSearchEmpty" in vhosts - for marker in ['toggleVhostEnabled', 'vhQuickEnable', 'vhQuickDisable', 'backend_table_columns', 'applyPoolTableView', 'vh-enable-control']: + for marker in ['toggleVhostEnabled', 'vhQuickEnable', 'vhQuickDisable', 'backend_table_columns', 'applyPoolTableView', 'rustProxyEnableControl']: assert marker in vhosts, marker assert 'rustproxy-backend-table-v1' not in vhosts and 'poolTablePreferenceKey' not in vhosts assert 'window.rustProxyPreferences.update({' in vhosts and 'backend_table_sort' in vhosts and 'backend_table_descending' in vhosts for marker in ['id="pool-search"', 'id="pool-sort"', 'data-pool-column-toggle="vhost"']: assert marker in html, marker style = (ROOT / "src/admin/web/style.css").read_text(encoding="utf-8") - for marker in ['--scrollbar-thumb', '*::-webkit-scrollbar-thumb', '.backend-table-tools', '.vhost-quick-toggle']: + for marker in ['--scrollbar-thumb', '*::-webkit-scrollbar-thumb', '.backend-table-tools', '.vhost-quick-toggle', '.dashboard-widget-toggle', 'height: var(--button-min-height)']: assert marker in style, marker assert "pool-vhost-disabled" in vhosts for marker in ["locationMatchRegex", "headersVisualBuilder", "headersAdvancedJson", "openLocationHelp", "cloudflare-client-ip", "timing-headers"]: @@ -554,7 +554,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.26.3"' in openapi_0241 + assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.4"' 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 @@ -1011,7 +1011,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.26.3"', '"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.26.4"', '"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"']: @@ -1186,7 +1186,7 @@ def main() -> None: source_contract_smoke() container_contract_smoke() debian_grafana_contract_smoke() - print("PASS: 0.26.3 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.") + print("PASS: 0.26.4 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.") if __name__ == "__main__": diff --git a/scripts/swagger_assets.py b/scripts/swagger_assets.py index 0f0253a..dbe4ef8 100755 --- a/scripts/swagger_assets.py +++ b/scripts/swagger_assets.py @@ -85,7 +85,7 @@ def download(destination: Path) -> None: """TLS verification uses Python's default trust store; never executes a shell.""" started = time.monotonic() opener = build_opener(HttpsRedirects()) - request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.3"}) + request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.4"}) with opener.open(request, timeout=30) as response, destination.open("wb") as output: length = response.headers.get("Content-Length") if length and int(length) > MAX_ARCHIVE: diff --git a/scripts/upgrade.py b/scripts/upgrade.py index 2bf1d41..bc38bb5 100755 --- a/scripts/upgrade.py +++ b/scripts/upgrade.py @@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None for name in MANAGED: if (target / name).is_symlink(): raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.") - backup = target / ".upgrade-backup" / ("before-0.26.3-" + uuid.uuid4().hex[:12]) + backup = target / ".upgrade-backup" / ("before-0.26.4-" + uuid.uuid4().hex[:12]) backup.mkdir(parents=True, mode=0o700) installed: list[str] = [] moved: list[str] = [] @@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None remove_legacy = legacy.exists() if remove_legacy and (legacy.is_symlink() or not legacy.is_file()): raise ValueError("Obsolete renewal observation sidecar must be a regular file.") - (backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.3", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8") + (backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.4", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8") if remove_legacy: legacy.unlink() except BaseException: @@ -115,7 +115,7 @@ def main() -> None: backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir) except (OSError, ValueError) as error: parser.exit(2, f"Upgrade refused or rolled back: {error}\n") - print(f"Installed RustProxy 0.26.3. Previous managed source files: {backup}") + print(f"Installed RustProxy 0.26.4. Previous managed source files: {backup}") print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.") print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh") diff --git a/scripts/web_server_contracts.py b/scripts/web_server_contracts.py index 078f44d..0ae85be 100755 --- a/scripts/web_server_contracts.py +++ b/scripts/web_server_contracts.py @@ -96,7 +96,7 @@ need(web, 'HeaderValue::from_static("no-store")', "builtin_error_response(StatusCode::FORBIDDEN, &method)", "empty_index_can_go_directly_to_directory_listing", - "RustProxy 0.26.3", + "RustProxy 0.26.4", "traversal_and_symlink_escape_are_not_served", "try_files_rewrites_cache_rules_headers_and_downloads_work", "precompressed_sidecars_keep_mime_validators_and_range_semantics", diff --git a/src/admin/documentation/documents.rs b/src/admin/documentation/documents.rs index ccf92bb..3f1fc70 100644 --- a/src/admin/documentation/documents.rs +++ b/src/admin/documentation/documents.rs @@ -399,6 +399,14 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[ "/docs/V0.25.1_AUDIT.md" )), ), + ( + "docs/V0.26.4_IMPLEMENTATION.md", + "technical", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/docs/V0.26.4_IMPLEMENTATION.md" + )), + ), ( "docs/V0.26.3_IMPLEMENTATION.md", "technical", diff --git a/src/admin/openapi.json b/src/admin/openapi.json index 3ff03c6..69ad4e6 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.26.3", + "version": "0.26.4", "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": [ diff --git a/src/admin/web/app/core.js b/src/admin/web/app/core.js index c659423..198697a 100644 --- a/src/admin/web/app/core.js +++ b/src/admin/web/app/core.js @@ -174,6 +174,22 @@ function uptime(value) { } function rateBytes(value) { value = Math.max(0, Number(value) || 0); if (value === 0) return "0.0 KiB"; value /= 1024; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { if (value < 1024 || unit === "TiB") { if (unit === "KiB" && value < 0.1) return "<0.1 KiB"; const digits = value < 10 ? 1 : 0; return `${Number(value.toFixed(digits))} ${unit}`; } value /= 1024; } } function el(tag, text, cls) { const e = document.createElement(tag); if (text !== undefined) e.textContent = text; if (cls) e.className = cls; return e; } +function enhanceEnabledControl(input, hintKey = "entityEnableHint") { + const label = input?.closest("label"); if (!label) return () => {}; + label.classList.add("vh-enable-control", "vh-wide"); + let hint = label.querySelector(".vh-enable-hint"); + if (!hint) { hint = el("small", undefined, "vh-enable-hint"); label.append(hint); } + hint.dataset.i18n = hintKey; hint.textContent = t(hintKey); + let state = label.querySelector(".vh-enable-state"); + if (!state) { state = el("strong", undefined, "vh-enable-state"); label.append(state); } + const sync = () => { + const key = input.checked ? "vhEnabled" : "vhDisabled"; + label.classList.toggle("is-disabled", !input.checked); label.classList.toggle("is-enabled", input.checked); + state.dataset.i18n = key; state.textContent = t(key); + }; + input.addEventListener("change", sync); sync(); return sync; +} +window.rustProxyEnableControl = enhanceEnabledControl; function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; } function renderStatus(s) { const firstStatus = latestStatus === null; latestStatus = s; window.rustProxySettings?.render(); if (firstStatus) renderSettings(); diff --git a/src/admin/web/app/preferences.js b/src/admin/web/app/preferences.js index ea664a4..864ae8f 100644 --- a/src/admin/web/app/preferences.js +++ b/src/admin/web/app/preferences.js @@ -92,15 +92,16 @@ function renderDashboardWidgetSettings() { const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets)); const rows = order.map((id, index) => { const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id; - const visible = document.createElement("input"); visible.type = "checkbox"; visible.checked = !hidden.has(id); visible.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`); - const label = el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"); + const visible = !hidden.has(id); + const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`); + toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label")); const actions = el("span", undefined, "dashboard-widget-setting-actions"); const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp")); const down = el("button", "↓", "quiet"); down.type = "button"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("moveDown")); - visible.addEventListener("change", () => { - const previous = [...hidden], next = new Set(hidden); if (visible.checked) next.delete(id); else next.add(id); + toggle.addEventListener("click", () => { + const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id); if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings(); - void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, visible, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous); renderDashboardWidgetSettings(); }); + void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, toggle, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous); renderDashboardWidgetSettings(); }); }); const move = delta => { const previous = [...order], next = [...order], at = next.indexOf(id), target = at + delta; if (at < 0 || target < 0 || target >= next.length) return; @@ -108,7 +109,7 @@ function renderDashboardWidgetSettings() { applyDashboardLayout(next, [...hidden]); renderDashboardWidgetSettings(); void saveAppearancePreference({ dashboard_widgets: next }, null, () => { if (adminPreferences) adminPreferences.dashboard_widgets = previous; applyDashboardLayout(previous, [...hidden]); renderDashboardWidgetSettings(); }); }; - up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(visible, label, actions); return row; + up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(toggle, actions); return row; }); list.replaceChildren(...rows); } diff --git a/src/admin/web/app/settings.js b/src/admin/web/app/settings.js index 443d874..2315423 100644 --- a/src/admin/web/app/settings.js +++ b/src/admin/web/app/settings.js @@ -6,7 +6,7 @@ function renderSettings() { if (!settingsDirty.has("response-form")) { const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {}; $("expose-request-id").checked = !!diagnostics.request_id; - $("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.3"; + $("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.4"; for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name]; $("compress-min").value = compression.min_bytes ?? 1024; syncServerMode(); } diff --git a/src/admin/web/host404.js b/src/admin/web/host404.js index e457790..5aafd87 100644 --- a/src/admin/web/host404.js +++ b/src/admin/web/host404.js @@ -3,6 +3,7 @@ const form = $("notfound-form"), dialog = $("notfound-dialog"), list = $("notfound-list"); if (!form || !dialog || !list) return; let editing = null, revision = null, dirty = false, readBindings = () => [], readHosts = () => []; + const syncEnabledControl = window.rustProxyEnableControl($("notfound-enabled")); const ERROR_STATUSES = [400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504]; const BUILTIN_STATUSES = [401, 403, 404, 429, 503, 504]; const STATUS_NAMES = { 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 408: "Request Timeout", 413: "Payload Too Large", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout" }; @@ -38,11 +39,26 @@ editing = item.id || null; revision = envelope?.revision ?? null; dirty = false; $("notfound-editing").textContent = editing || t("notfoundNewLabel"); $("notfound-id").value = item.id || ""; $("notfound-id").disabled = Boolean(editing); $("notfound-name").value = item.name || ""; - $("notfound-enabled").checked = item.enabled !== false; $("notfound-http2").checked = item.http2_enabled !== false; $("notfound-http3").checked = item.http3_enabled !== false; + $("notfound-enabled").checked = item.enabled !== false; syncEnabledControl(); $("notfound-http2").checked = item.http2_enabled !== false; $("notfound-http3").checked = item.http3_enabled !== false; const hostHolder = $("notfound-hosts-editor"); hostHolder.replaceChildren(); readHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "notfoundHosts", helpKey: "vhHostsHelp", requiredKey: "notfoundHostsRequired", onChange: changed }); listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id); $("notfound-mode").value = item.page?.kind || "welcome"; populateStatuses(item.page?.status ?? 404); $("notfound-html").value = item.page?.html || ""; syncMode(); dialog.showModal(); } + function suspendDraft() { + const state = { + editing, revision, label: $("notfound-editing").textContent, controls: window.rustProxyDrafts.captureControls(form), + hosts: window.rustProxyDrafts.takeChildren($("notfound-hosts-editor")), listeners: window.rustProxyDrafts.takeChildren($("notfound-listeners")), + readBindings, readHosts + }; + const title = value("notfound-id") || editing || t("draftUntitled"); editing = null; revision = null; dirty = false; dialog.close(); return { title, state }; + } + function restoreDraft(state) { + window.rustProxyDrafts.restoreChildren($("notfound-hosts-editor"), state.hosts); window.rustProxyDrafts.restoreChildren($("notfound-listeners"), state.listeners); + readBindings = state.readBindings; readHosts = state.readHosts; editing = state.editing; revision = state.revision; dirty = true; $("notfound-editing").textContent = state.label; + window.rustProxyDrafts.restoreControls(form, state.controls); syncEnabledControl(); syncMode(); if (!dialog.open) dialog.showModal(); + } + function closeEditor() { if (dirty) return window.rustProxyDrafts.dock("notfound"); if (dialog.open) dialog.close(); return true; } + function collect() { const id = value("notfound-id"), name = value("notfound-name") || id, hosts = readHosts(), listener_ids = readBindings(), kind = value("notfound-mode"), status = Number(value("notfound-status")), html = $("notfound-html").value; if (!listener_ids.length) throw new Error(t("notfoundListenersRequired")); @@ -57,14 +73,15 @@ list.replaceChildren(...entries.map(item => { const card = el("article", undefined, "vhost-card redirect-card"), head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity"), title = el("div", undefined, "vhost-title-line"); title.append(el("strong", item.name || item.id, "entity-title")); if (item.enabled === false) title.append(el("span", t("vhDisabled"), "pill")); identity.append(title, el("div", (item.frontend?.hosts || []).join(" · "), "vhost-hosts")); head.append(identity); - const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => load(structuredClone(item))), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.not_found_hosts || []).filter(value => value.id !== item.id); if (await saveSection("not_found_hosts", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head); + const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => { window.rustProxyDrafts.prepare(); load(structuredClone(item)); }), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.not_found_hosts || []).filter(value => value.id !== item.id); if (await saveSection("not_found_hosts", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head); const detail = el("div", undefined, "vhost-detail-strip"); for (const [key, text] of [["notfoundStatus", String(item.page?.status ?? 404)], ["notfoundMode", t(`notfoundMode${(item.page?.kind || "welcome").replace(/^./, c => c.toUpperCase())}`)], ["redirectListeners", (item.frontend?.listener_ids || []).join(", ")], ["vhCertificate", item.certificate_id || t("vhCertificateDefault")]]) { const node = el("div", undefined, "vhost-detail"); node.append(el("span", t(key), "small-label"), el("b", text)); detail.append(node); } card.append(detail); return card; })); if (!entries.length) list.append(el("p", t(query ? "notfoundSearchEmpty" : "notfoundEmpty"), "fine")); } form.addEventListener("input", changed); form.addEventListener("change", changed); $("notfound-mode").addEventListener("change", syncMode); $("notfound-status").addEventListener("change", syncMode); form.addEventListener("submit", async event => { event.preventDefault(); if (!envelope || busy) return; const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) { invalid.reportValidity(); return; } if (revision !== envelope.revision) { toast(t("stale"), true); return; } try { const item = collect(), next = structuredClone(envelope.config.not_found_hosts || []); if (!editing && next.some(value => value.id === item.id)) throw new Error(t("notfoundDuplicate")); const index = next.findIndex(value => value.id === editing); if (index < 0) next.push(item); else next[index] = item; if (await saveSection("not_found_hosts", next)) { dirty = false; editing = item.id; dialog.close(); render(); } } catch (error) { toast(error.message, true); } }); - $("notfound-new").addEventListener("click", async () => { if (await allowDiscard()) load(); }); $("notfound-search").addEventListener("input", render); $("notfound-close").addEventListener("click", async () => { if (await allowDiscard()) dialog.close(); }); $("notfound-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.not_found_hosts || []).find(item => item.id === editing) || defaults()) : defaults()); }); dialog.addEventListener("cancel", event => { if (!dirty) return; event.preventDefault(); void allowDiscard().then(ok => { if (ok) dialog.close(); }); }); + $("notfound-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); }); $("notfound-search").addEventListener("input", render); $("notfound-close").addEventListener("click", closeEditor); $("notfound-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.not_found_hosts || []).find(item => item.id === editing) || defaults()) : defaults()); }); dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); }); + window.rustProxyDrafts.register("notfound", { labelKey: "notfound", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } }); window.rustProxyNotFound = { render, reset() { dialog.close(); editing = null; revision = null; dirty = false; list.replaceChildren(); $("notfound-search").value = ""; } }; if (envelope) render(); })(); diff --git a/src/admin/web/index.html b/src/admin/web/index.html index a2afff5..e879dc9 100644 --- a/src/admin/web/index.html +++ b/src/admin/web/index.html @@ -1877,7 +1877,7 @@

Diagnostic headers

@@ -2676,7 +2676,7 @@
-