v0.34.9_css_refactor

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-23 11:15:35 +02:00
1 parent 2b7c67d755
commit 66ff9dfaa2
28 files changed
+11518 -13023

No files matched your search

+3
View File
@@ -4,6 +4,9 @@ Release history focused on **features, settings, default behavior, compatibility
## 0.34.9 — experimental TLS post-quantum key exchange (source candidate) ## 0.34.9 — experimental TLS post-quantum key exchange (source candidate)
- **Dashboard source maintenance:** panel styles are organized into eight modules with consolidated responsive conditions and a deterministic build step. Login and the dashboard continue to load one fingerprinted stylesheet; rebuilding the binary applies changes. Source checks and browser regression tooling cover the module order and UI cascade. Native Rust validation remains required for this source candidate.
- **CSS build preflight fix:** ship local `scripts`/`build` package markers so unrelated installed Python packages cannot hide `build.panel_css` or the application scripts. This fixes `dev.sh` startup/repair, the CSS audit and packaging imports on affected environments; generated styles remain unchanged.
- **Generated CSS recovery:** detect a root `build.rs` without the panel generator, track the generated CSS as a Cargo dependency and reject missing/empty helper output before compiling the application. Document refreshing `build.rs` after overlaying a reproducibly timestamped archive onto an existing build cache.
- **Settings → TLS policy:** new disabled-by-default experimental `post_quantum` toggle. When enabled with TLS 1.3, inbound TCP TLS and HTTP/3 QUIC use an explicit aws-lc-rs provider, prioritizing the hybrid **X25519MLKEM768** key-exchange group while retaining classical alternatives. When disabled, the original ring provider is retained. This changes KEX, not TLS cipher suites or certificate signing; no change to upstream/client-side TLS providers. - **Settings → TLS policy:** new disabled-by-default experimental `post_quantum` toggle. When enabled with TLS 1.3, inbound TCP TLS and HTTP/3 QUIC use an explicit aws-lc-rs provider, prioritizing the hybrid **X25519MLKEM768** key-exchange group while retaining classical alternatives. When disabled, the original ring provider is retained. This changes KEX, not TLS cipher suites or certificate signing; no change to upstream/client-side TLS providers.
- **Per-vhost TLS override:** may opt out when global PQC is enabled; cannot enable PQC if the global policy disables it. TLS 1.3 is required. Existing per-vhost policies lacking the field continue to default to non-PQC. HTTP/3 continues to require exactly matching global policy. - **Per-vhost TLS override:** may opt out when global PQC is enabled; cannot enable PQC if the global policy disables it. TLS 1.3 is required. Existing per-vhost policies lacking the field continue to default to non-PQC. HTTP/3 continues to require exactly matching global policy.
- **UI/docs/API:** English/Polish switch and key-exchange group readout, authenticated Reader guide linked from the TLS policy page, OpenAPI and regression unit tests. - **UI/docs/API:** English/Polish switch and key-exchange group readout, authenticated Reader guide linked from the TLS policy page, OpenAPI and regression unit tests.
+7
View File
@@ -105,6 +105,13 @@ checks. See [Linux filesystem access](docs/INSTALL.md#linux-debrpm-read-only-acc
The authenticated **`/docs`** page includes the guides and a separate interactive **OpenAPI/Swagger** tab. The authenticated **`/docs`** page includes the guides and a separate interactive **OpenAPI/Swagger** tab.
## Dashboard source maintenance
The panel stylesheet is maintained in eight ordered modules under
[`src/admin/web/css`](src/admin/web/css/README.md). The build embeds one compact,
fingerprinted stylesheet for login and the dashboard. See the module guide for
ownership, cascade rules, duplicate checks and browser regression commands.
## Status of this source archive ## Status of this source archive
Version **0.34.9** is provided as a **source-only experimental candidate**. PQC dependencies were provisionally locked using published crate metadata, not checked by Cargo in the packaging environment. Run `cargo metadata --locked`, `cargo check --locked --all-targets`, `cargo test --locked --all-targets` and protocol compatibility checks on your target system before production deployment. See [Installation](docs/INSTALL.md) and [Roadmap](ROADMAP.md). Version **0.34.9** is provided as a **source-only experimental candidate**. PQC dependencies were provisionally locked using published crate metadata, not checked by Cargo in the packaging environment. Run `cargo metadata --locked`, `cargo check --locked --all-targets`, `cargo test --locked --all-targets` and protocol compatibility checks on your target system before production deployment. See [Installation](docs/INSTALL.md) and [Roadmap](ROADMAP.md).
+37 -19
View File
@@ -1,15 +1,15 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 18ea9db93171d23888334488151554d52d9a3af701581b89ccbe4150a5b81e7f .gitignore
237ed6e2c0d4fa1f125180c349856749c13820b72593b5fc58e1b58cce47c840 AGENTS.md 237ed6e2c0d4fa1f125180c349856749c13820b72593b5fc58e1b58cce47c840 AGENTS.md
4f695dd4ded1ef0c6fbe421ae9bc01993140ddbb4f77cb7a46dd8191a2584ddb CHANGELOG.md 608cc471556fa6e29fefae1d5ea18194d6ef94b5d32410965b0140a4fcf7018c CHANGELOG.md
310d60da6c7f501ccdde86b93ee1d5b0102033ba71ac9bb42a4e3ce184692ef9 Cargo.lock 310d60da6c7f501ccdde86b93ee1d5b0102033ba71ac9bb42a4e3ce184692ef9 Cargo.lock
c18c0ba1a22a8534a4cf3b6db907f41f04e0b6cb47cd305a21af139a06e303fb Cargo.toml c18c0ba1a22a8534a4cf3b6db907f41f04e0b6cb47cd305a21af139a06e303fb Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
fb75f6ff1fee8e5af4c89943c67b7e75cbac048254cecc3dc9fd0ea33d53ac25 README.md a288a9c873bfe2e866a41b116af8cf3dac7661db68164bbe6b57706e2fca7bc6 README.md
ae6d14ada6fde7d0b8514840b821678418cf5b1816c18434e3b7fb2ae27a0b21 ROADMAP.md ae6d14ada6fde7d0b8514840b821678418cf5b1816c18434e3b7fb2ae27a0b21 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
7309cfaa188c7a1fcf30be1b8313ee99c00b7fe5d6d626ddefcc7345fa066112 build.rs ac07b3bdd46c787989e93f55ebd6ea3d7f5b150ef36f2422e2b58df3f6b7543a build.rs
2c6a0a5cb088399e2cc912073069eb334ec6615eee31734d2f0c161f1415fb6a deploy/README.md 2c6a0a5cb088399e2cc912073069eb334ec6615eee31734d2f0c161f1415fb6a deploy/README.md
13d514de5042a230cacf8c10abe1ef183c94ac799a89b8e2c111152ac7c69b48 deploy/rustproxy.service 13d514de5042a230cacf8c10abe1ef183c94ac799a89b8e2c111152ac7c69b48 deploy/rustproxy.service
c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh
@@ -36,7 +36,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
f24767c2f9d4db4db5005b700d03323dd20f0a1d180ae939be4fe54a126aaf32 docs/CONFIGURATION.md f24767c2f9d4db4db5005b700d03323dd20f0a1d180ae939be4fe54a126aaf32 docs/CONFIGURATION.md
80bcc666736b10f6202386d0342e8d6feeba858d77aea310bf3ec6de01c2bfb3 docs/CONTAINER.md 80bcc666736b10f6202386d0342e8d6feeba858d77aea310bf3ec6de01c2bfb3 docs/CONTAINER.md
dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md
9fb2d0bcac70d1883dd16557622851a1f0527b3d3569a4b5177f5e6dedd3c74d docs/DASHBOARD.md 302a9db0a5a4e5f6c49684ef0ea9ad9f1796fdbcb5e28bb5a98c75fffd4374f4 docs/DASHBOARD.md
3e2f7670d136911c1b7bfe8571c60590932f74a60bcac308add4ccb0446bf970 docs/DOCKER_DISCOVERY.md 3e2f7670d136911c1b7bfe8571c60590932f74a60bcac308add4ccb0446bf970 docs/DOCKER_DISCOVERY.md
f1baabe34e8a1b204d7c78e2f558c019c156b03adfa6ddecb931b4b8a5501f78 docs/INSTALL.md f1baabe34e8a1b204d7c78e2f558c019c156b03adfa6ddecb931b4b8a5501f78 docs/INSTALL.md
37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md 37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md
@@ -44,7 +44,7 @@ f1baabe34e8a1b204d7c78e2f558c019c156b03adfa6ddecb931b4b8a5501f78 docs/INSTALL.m
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md 888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
140b95ad778261455f269f2144a1c61c2315bd3b85300fb613822eacddf3cfc2 docs/MONITORING.md 140b95ad778261455f269f2144a1c61c2315bd3b85300fb613822eacddf3cfc2 docs/MONITORING.md
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
8b22a8f738f62cb27faf97683cc467de0fba02ab5999217b502f24a38f10ab4b docs/README.md bcd8c4eff9d57342d75441f5941a5828275d6b320317668e727467bc0d876f66 docs/README.md
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md 490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
e2b36152c13d65621a367c69dd400118dae1e4426a4d2b2139d2f63fa30442d6 docs/SECURITY.md e2b36152c13d65621a367c69dd400118dae1e4426a4d2b2139d2f63fa30442d6 docs/SECURITY.md
bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md
@@ -97,8 +97,11 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
1465396cf1358067e9fe48b3d61f28fd4ac65c577b67c502950ce6b42ecbe3b3 nagios/check_rustproxy_latency 1465396cf1358067e9fe48b3d61f28fd4ac65c577b67c502950ce6b42ecbe3b3 nagios/check_rustproxy_latency
6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues 6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml 7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
bd313cacfc1b450a90368b30ae39ab48b877c8bed3c2db8bbd4e4a1f40865c9e scripts/README.md fd1c34599f725ad5f7c3c0cc89e08b11917baef02c83af054ac8528b2e01dd8e scripts/README.md
f550ffab868c5241ff5d43d9ae569e19f6842b324b80ecefdc7034642125ed9b scripts/audit_css_usage.py 1d63e9cea61a744f1bf2085cbb7c3d8755b3eda1569c8532574dd4fb6a840678 scripts/__init__.py
1843422d648101bb675d8bfcdb109ef350bfda273999ad7395d5f6ceb923eaa1 scripts/audit_css_usage.py
5fc5c7f274aa5a6c07970f7b2e548acabae3dc2a24d598308fd768a8d9de2510 scripts/build/__init__.py
c3066f40f152a2655f88314fd9cf784eddbfa6c3238a494da13d972ef9ad930c scripts/build/panel_css.py
216abccbb8b0b77b28de96382bb5790d1ce831699746d3fb06ef13a6dd1f5d21 scripts/build/swagger_assets.py 216abccbb8b0b77b28de96382bb5790d1ce831699746d3fb06ef13a6dd1f5d21 scripts/build/swagger_assets.py
e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py
618603239b000bbe4af36a6a6ee268758af632c761fc53fc6b7f44a4a818bdf1 scripts/deploy/deploy_core.py 618603239b000bbe4af36a6a6ee268758af632c761fc53fc6b7f44a4a818bdf1 scripts/deploy/deploy_core.py
@@ -107,7 +110,7 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py 31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
3077405aae2dd0f32c0e168ef710745bd44e42283a4c566ab411e68d3bb745af scripts/preflight.py 8f9f2dc7e6d6688b915a7dfbfc05643a80e5814819926cf43458a54965cfc83c scripts/preflight.py
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh 3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py 20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
@@ -137,7 +140,7 @@ a402209141667fdce69698a60dc0b867248effa191494d0a0d93d468d7df9476 src/admin/cert
159c3970d25fa29acf2483fc9422b003e5d63884713f62baa8375e4a4020730e src/admin/dashboard_presets.rs 159c3970d25fa29acf2483fc9422b003e5d63884713f62baa8375e4a4020730e src/admin/dashboard_presets.rs
aae1ca0e70d987830bd63948d431f54302083148177c1b551565be12df7fe771 src/admin/documentation.rs aae1ca0e70d987830bd63948d431f54302083148177c1b551565be12df7fe771 src/admin/documentation.rs
da28dd57087df138913b784b71dca11d36c68100f2f100f452fbf4e4d7982433 src/admin/documentation/documents.rs da28dd57087df138913b784b71dca11d36c68100f2f100f452fbf4e4d7982433 src/admin/documentation/documents.rs
67fa0e842f6789ed38e101ed6717072736e85c34af873d1a27d7efee5dc0c83d src/admin/documentation/tests.rs 6ca14ae276c0e683903cfe326f6733cf33ea05f3abbefd5a22671ea160a58ea6 src/admin/documentation/tests.rs
1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs 1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs
ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs
d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file_logs_io.rs d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file_logs_io.rs
@@ -146,7 +149,7 @@ f66ff718f82a326f80808b0a5088bd05a48625be802e5ba305fff3c344b59948 src/admin/logg
1ba0520c6348246cbb35b60e596e99dc600137c0696de86508cf06587bd86563 src/admin/mod.rs 1ba0520c6348246cbb35b60e596e99dc600137c0696de86508cf06587bd86563 src/admin/mod.rs
8e1012a8ec776c3525310b03a70f274955dce9de3528109070ea11de0ff5c16c src/admin/openapi.json 8e1012a8ec776c3525310b03a70f274955dce9de3528109070ea11de0ff5c16c src/admin/openapi.json
7cfd6593ac9bcea3d1b9e61bbcb7ed98d65ed3190f54f94c1e104541f4fbb4f5 src/admin/operations.rs 7cfd6593ac9bcea3d1b9e61bbcb7ed98d65ed3190f54f94c1e104541f4fbb4f5 src/admin/operations.rs
f14afb6ae141053d83d0dc59df835b20e38a37a9e83f86f0f2053f2bd5d9ce57 src/admin/panel_assets.rs 257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
730c559afaa8a50affc3eb6dc6fbde7005c0356237352fcf8c5423b65725a8fe src/admin/preferences.rs 730c559afaa8a50affc3eb6dc6fbde7005c0356237352fcf8c5423b65725a8fe src/admin/preferences.rs
35a7f99c7448c90b8637f4b3a7becca138f1f378fe5dc3f87122151525bb20f9 src/admin/preferences_columns.rs 35a7f99c7448c90b8637f4b3a7becca138f1f378fe5dc3f87122151525bb20f9 src/admin/preferences_columns.rs
@@ -160,7 +163,7 @@ e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/sess
2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs 2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs 8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
666ef444c81e59a2fd47ace3be26e51a8630194d8ffd4db131eb228947fee8ca src/admin/tests.rs dacdf1b948991698f16640a375e3af90c3a18bb2247b0ad51aa97f763ee3f7bd src/admin/tests.rs
c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js 48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js 86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js
@@ -169,7 +172,6 @@ a085ee336e6669229f3ad2f74536f19bd088412ca79bcaff30dff293f1e1baf9 src/admin/web/
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
c8e1d31a7bf77d9e4f4be979bf6a7474369aae3e1e46e6b30d1a76677fa3d762 src/admin/web/app/core.js c8e1d31a7bf77d9e4f4be979bf6a7474369aae3e1e46e6b30d1a76677fa3d762 src/admin/web/app/core.js
622d34e1616fee9a480d208192fc5a84d62d84ef4233d6a7d95e2bfb8cef415b src/admin/web/app/dashboard-inline-layout.js 622d34e1616fee9a480d208192fc5a84d62d84ef4233d6a7d95e2bfb8cef415b src/admin/web/app/dashboard-inline-layout.js
851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.js
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
bfb3fbac62f9ff762a298d9c06808ae81ae55de7a41e0bb35ce5c167f333ffe2 src/admin/web/app/overview-extra-metrics.js bfb3fbac62f9ff762a298d9c06808ae81ae55de7a41e0bb35ce5c167f333ffe2 src/admin/web/app/overview-extra-metrics.js
5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js 5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js
@@ -185,6 +187,16 @@ fa00bce2651308a4e0b1303cc0146be17ed4794b67b2f0cebfaf861558d8c78c src/admin/web/
7f89bfae66e2a08a05a112733864e908f036bb1ee3d8b5b9ff23783b870acd96 src/admin/web/app/web-overview.js 7f89bfae66e2a08a05a112733864e908f036bb1ee3d8b5b9ff23783b870acd96 src/admin/web/app/web-overview.js
849b2ca9d183bdf6cb1e04651cda52da55b8a631e11ab1d827a052c8436bbd76 src/admin/web/cache-panel.js 849b2ca9d183bdf6cb1e04651cda52da55b8a631e11ab1d827a052c8436bbd76 src/admin/web/cache-panel.js
e9788a9d1e0bce8775f612a5c842afea8a0dcf01301acdef4b69ff6a52d2ef45 src/admin/web/certificate-api.js e9788a9d1e0bce8775f612a5c842afea8a0dcf01301acdef4b69ff6a52d2ef45 src/admin/web/certificate-api.js
9d000e98497b3387641500d5273d23eda45d532c714f22e187572928da152a97 src/admin/web/css/00-tokens.css
11093f12033d0c549c2d2b0dfdcbc660ed2318a4ab17a9dc562b5902d7ddfb84 src/admin/web/css/10-base.css
4aae1657e8f5faa755cab0399769e6179364148f80243e742fc7a5e6e237b3e6 src/admin/web/css/20-shell.css
31834acc1fc234ed314b0ca069658bfab175d782bcfaa5e6ee07d46fef689457 src/admin/web/css/30-overview.css
c89c877479c6fc8fc810766ea4787448953665764d18566fc550a119796121d5 src/admin/web/css/40-editors.css
86f7e08a9973ebddd67aaeca92d2b5e2c0f7f53dee45d65a93bb88b35b207cdc src/admin/web/css/50-settings.css
d019d9741951c9d652fab9e520693e0dd3f00b395e32169e5c7ca3ccaf04132c src/admin/web/css/60-monitoring.css
6afa10207938787013f5a4731910630b19bec4a22682a07c3e67707d17cfd97e src/admin/web/css/70-responsive.css
f2a2b0476cc55d8dadb4fd52c2c589d64785ad400ccc6cac5a2d10cfca729928 src/admin/web/css/README.md
8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 src/admin/web/css/manifest.json
560a8e047182fbcee08aa4f8d999400a8a5ddca0b6a4640efd46f6b5c34fde91 src/admin/web/dashboard-defaults.json 560a8e047182fbcee08aa4f8d999400a8a5ddca0b6a4640efd46f6b5c34fde91 src/admin/web/dashboard-defaults.json
14213011cfb6ac334c8711b76bbdde97588a7b7ba4eb2592a1e387f877d6cbe0 src/admin/web/dashboard-widgets.json 14213011cfb6ac334c8711b76bbdde97588a7b7ba4eb2592a1e387f877d6cbe0 src/admin/web/dashboard-widgets.json
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html 50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
@@ -208,7 +220,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js 7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
ede1f17067ac1622c66dfaa54bf7b7bbb0ce160ee883235be743ef5b2aa83243 src/admin/web/index.html 66af48d25f91f6dfe64887e20bf75709b9ca2efeb0f6dab3b139f11d8a7e9ee5 src/admin/web/index.html
7915a06cebb3dfa2c51833c326586920691ba4431b1f984429b039333f9c2a93 src/admin/web/integrations.js 7915a06cebb3dfa2c51833c326586920691ba4431b1f984429b039333f9c2a93 src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
@@ -221,7 +233,6 @@ a8afc6e70950bc70b98725eb049d73cff35dc3a3ee659229a15a8e08d3eb4f12 src/admin/web/
3ee4508cdda0b0b0e0f880ecac4b085368098b46240d24f2e6efed21c96bd5e9 src/admin/web/settings.js 3ee4508cdda0b0b0e0f880ecac4b085368098b46240d24f2e6efed21c96bd5e9 src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html 8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
6feb1996b1caa9d19e0dc56eae7ca5b48b7e7cdf0a93bb2e4b527fb8f13a2fe5 src/admin/web/streams.js 6feb1996b1caa9d19e0dc56eae7ca5b48b7e7cdf0a93bb2e4b527fb8f13a2fe5 src/admin/web/streams.js
38716fae968aeb8f6a8510f28bb9054a73a438a969c3b69f9c9d921acfed6e06 src/admin/web/style.css
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js
a55f45dad5b540def419f8487161fb506b7e52744c76a5e661c7a5fa0a98a426 src/admin/web/vhosts/core.js a55f45dad5b540def419f8487161fb506b7e52744c76a5e661c7a5fa0a98a426 src/admin/web/vhosts/core.js
@@ -330,7 +341,7 @@ c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/upda
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs 9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
61a2b01db9010f9debfb38b43a26bf588438a5e470dff938e4e54f9d7ba46732 src/headers/mod.rs 61a2b01db9010f9debfb38b43a26bf588438a5e470dff938e4e54f9d7ba46732 src/headers/mod.rs
e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs
b458510eb89ae8bf8fda84989073c6e64c69f2b12fde96c454b33f9df6a57df2 src/http3.rs 1b291ef7b06647ce820111cc802874394e3600b4b2c86ee9a1fbf7eee6cb864a src/http3.rs
0cc484bf0cc59a55a13e0e70435833a9a7a22ad7dfb5b9f86e059cf70a80cc2d src/http3/body.rs 0cc484bf0cc59a55a13e0e70435833a9a7a22ad7dfb5b9f86e059cf70a80cc2d src/http3/body.rs
741972222414c280104acbcd8f8071bed9cbfcfa2f503c487c11c56f553f53cd src/http3/diagnostics.rs 741972222414c280104acbcd8f8071bed9cbfcfa2f503c487c11c56f553f53cd src/http3/diagnostics.rs
49ce4452ef7973cafa2e4071e2e14be0e16000e931a6501e4dc613652e49e230 src/http3/identity.rs 49ce4452ef7973cafa2e4071e2e14be0e16000e931a6501e4dc613652e49e230 src/http3/identity.rs
@@ -451,8 +462,8 @@ df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 src/tls.rs
2ff3c219f87bd82677045a4809d3d94921c5b230bbc2f3e7131b07548f73ea7e src/tls/external_paths.rs 2ff3c219f87bd82677045a4809d3d94921c5b230bbc2f3e7131b07548f73ea7e src/tls/external_paths.rs
f88a6c8c2e35c3aa58b7763c71a5a2cec82a34f24478a98b0960b81659a1ec4c src/tls/external_paths_tests.rs f88a6c8c2e35c3aa58b7763c71a5a2cec82a34f24478a98b0960b81659a1ec4c src/tls/external_paths_tests.rs
7b4c0f58b3a1e1791bb3c682a001b78bfdbbdafdbc143db8063b3f22dc168616 src/tls/server_config.rs 7b4c0f58b3a1e1791bb3c682a001b78bfdbbdafdbc143db8063b3f22dc168616 src/tls/server_config.rs
68a29d90f1675c7d97953e50ebccd4fa21c47fd0a4d8a0838b436ee84399afe0 src/tls/tests.rs 7c0c06c3009c726043168ae47254a84642dbabbbce1b2e718544414312ca3e9b src/tls/tests.rs
bf0ccec8909e678aaed84d358058b1fa865b38e09d10471b02fca76eea38633d src/tls/types.rs d625e2e3f669484432ffc0bf7c1aba94aa5bb709dc4d080daa3bfa8004dee9f2 src/tls/types.rs
a535c8526b38de2990da7781e158b9ea70ef8c1f6fae0aac5ce6e9da21d3a309 src/traffic/mod.rs a535c8526b38de2990da7781e158b9ea70ef8c1f6fae0aac5ce6e9da21d3a309 src/traffic/mod.rs
dc8a83fae47da0d86e6ce0a329cb5274233264df56eebdb56b9bf37cb7be2bb4 src/traffic/tests.rs dc8a83fae47da0d86e6ce0a329cb5274233264df56eebdb56b9bf37cb7be2bb4 src/traffic/tests.rs
65fcac840f8b5f1508213e3f374aa318b450b0f9f90ba827c5ea935b9c60bee7 src/transform/apply.rs 65fcac840f8b5f1508213e3f374aa318b450b0f9f90ba827c5ea935b9c60bee7 src/transform/apply.rs
@@ -468,7 +479,7 @@ d13fa462d6df516a4cfaee9f4e184044c5b6345e311aeb3d9058efafe54e4c5e src/transform/
954b86300c3ea47281acb2f43b0771e838f83ac09eb55e882e42de26aee3128f src/transform/model.rs 954b86300c3ea47281acb2f43b0771e838f83ac09eb55e882e42de26aee3128f src/transform/model.rs
4b52257e1a3643910b5e7043852d7218230c0021c31215d90d0f8e384f2ae233 src/transform/svg/mod.rs 4b52257e1a3643910b5e7043852d7218230c0021c31215d90d0f8e384f2ae233 src/transform/svg/mod.rs
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs 0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
d1f3f53fedae0d8ef69eeee457f7d30ed5a670a1ed4ced60683400ee46680260 src/transform/tests.rs 3c99de898e4c516a805fec1bc77007a211550b7d7a563933a81ae635d235a2e6 src/transform/tests.rs
8d38f2f7d4ca4dd453cd0e2337614ec704fbd0a36d3e2374cda43f05dfcad663 src/update_check/mod.rs 8d38f2f7d4ca4dd453cd0e2337614ec704fbd0a36d3e2374cda43f05dfcad663 src/update_check/mod.rs
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
b7ee3629a8fde0972bc1822b764f5fa2c813a760dd5dfb7f1ce643a7db8f9f75 src/upstream.rs b7ee3629a8fde0972bc1822b764f5fa2c813a760dd5dfb7f1ce643a7db8f9f75 src/upstream.rs
@@ -511,11 +522,18 @@ b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server
df888adf893d0b6b99348d19fa07bd9c2b8fc0ce7f411d06be7411afb4311fe0 src/web_server/tests.rs df888adf893d0b6b99348d19fa07bd9c2b8fc0ce7f411d06be7411afb4311fe0 src/web_server/tests.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md
577302f167c9d76655f12852ce57a272712044b67a76823cf1ec625116d461e3 tools/audit/CSS_REFACTOR_REPORT.md
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py 358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
3f5c3654e9667a60d2e93558ee3f6872a4fe6839b8efcc64671c4a36fb3d85a2 tools/audit/css-refactor-results.json
915de79d40e79bb34fd3dc45eb39028442e8023ab0dedbf0028ec4e36bc65591 tools/dev/README-dashboard-css.md
7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py 7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py 2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
18f9d60dd78b6d7f6cbdc21d4cb579d4b48fbda2557c9c1f84370ac02af80a57 tools/dev/package.json
538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba tools/dev/test_compression.py 538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba tools/dev/test_compression.py
8147bbc6f6e87768f47cf7279471ad283786bdf01eeddcc2fd3582d62886bdbd tools/dev/test_dashboard_css.cjs
91937cf81f6667590e119be04c11bfcf438a77b278f97b8ac1785a17062db3b8 tools/dev/test_dashboard_css_motion.cjs
68aed10c460bd7df52aa142ffb9f97eff37b23d979158a1c281297c74ec57957 tools/dev/test_panel_css.py
c711154a9df425a56607bd88f03815ffb408213a31a2b66ed1c1c9a6301a2fc6 tools/legacy/backup.py c711154a9df425a56607bd88f03815ffb408213a31a2b66ed1c1c9a6301a2fc6 tools/legacy/backup.py
89d2b4e60a2f4cb93e8943b74b1b5ca84374b01fa74ea07bec07bdff315ac0e9 update.sh 89d2b4e60a2f4cb93e8943b74b1b5ca84374b01fa74ea07bec07bdff315ac0e9 update.sh
56c702111022e1ca2c8f6b72c635c749b05936ffe6387bdb0a202691614cbc96 upgrade.sh 56c702111022e1ca2c8f6b72c635c749b05936ffe6387bdb0a202691614cbc96 upgrade.sh
+48
View File
@@ -1,6 +1,53 @@
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::{collections::HashSet, env, fs, path::PathBuf, process::Command}; use std::{collections::HashSet, env, fs, path::PathBuf, process::Command};
fn panel_styles(
root: &std::path::Path,
out: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-env-changed=RUST_PROXY_PYTHON");
let helper = root.join("scripts/build/panel_css.py");
println!("cargo:rerun-if-changed={}", helper.display());
println!("cargo:rerun-if-changed={}", root.join("src/admin/web/css").display());
let interpreters = match env::var_os("RUST_PROXY_PYTHON") {
Some(value) if !value.is_empty() => vec![value],
_ => vec!["python3".into(), "python".into()],
};
for interpreter in interpreters {
match Command::new(&interpreter)
.arg(&helper)
.arg("--root")
.arg(root)
.arg("--out-dir")
.arg(out)
.arg("--cargo")
.status()
{
Ok(status) if status.success() => {
let generated = out.join("panel-style.css");
let metadata = fs::metadata(&generated).map_err(|error| {
format!(
"Panel CSS helper succeeded but {} is unavailable: {error}",
generated.display()
)
})?;
if !metadata.is_file() || metadata.len() == 0 {
return Err(format!(
"Panel CSS helper did not create a non-empty regular file: {}",
generated.display()
)
.into());
}
return Ok(());
}
Ok(_) => return Err("Panel CSS preparation failed; see the diagnostic above and src/admin/web/css/README.md".into()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error.into()),
}
}
Err("Python 3.10+ is required for build assets; install python3 or set RUST_PROXY_PYTHON to its executable".into())
}
fn swagger_assets( fn swagger_assets(
root: &std::path::Path, root: &std::path::Path,
out: &std::path::Path, out: &std::path::Path,
@@ -35,6 +82,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::var_os("CARGO_MANIFEST_DIR").ok_or("Cargo did not supply CARGO_MANIFEST_DIR")?, env::var_os("CARGO_MANIFEST_DIR").ok_or("Cargo did not supply CARGO_MANIFEST_DIR")?,
); );
let out = PathBuf::from(env::var_os("OUT_DIR").ok_or("Cargo did not supply OUT_DIR")?); let out = PathBuf::from(env::var_os("OUT_DIR").ok_or("Cargo did not supply OUT_DIR")?);
panel_styles(&root, &out)?;
swagger_assets(&root, &out)?; swagger_assets(&root, &out)?;
let langs = root.join("langs"); let langs = root.join("langs");
println!("cargo:rerun-if-changed={}", langs.display()); println!("cargo:rerun-if-changed={}", langs.display());
+8
View File
@@ -2,6 +2,14 @@
The authenticated panel is the primary way to manage RustProxy. Pages cover **Overview**, **Vhosts**, **Web Server**, **Certificates**, **Docker vhosts**, **Streams**, **Traffic**, **Cache**, **Logs**, **Settings** and **Documentation**. Availability of individual cards depends on enabled features and the account's display preferences. The authenticated panel is the primary way to manage RustProxy. Pages cover **Overview**, **Vhosts**, **Web Server**, **Certificates**, **Docker vhosts**, **Streams**, **Traffic**, **Cache**, **Logs**, **Settings** and **Documentation**. Availability of individual cards depends on enabled features and the account's display preferences.
## Stylesheet updates
The panel and login page load one versioned stylesheet embedded in the Rust
binary. Source styles are split into eight modules and assembled during the
build. Rebuild the binary to apply CSS changes; copying individual CSS files to
an installed server does not update the panel. The authenticated documentation
page has its own stylesheet.
## Overview cards and layouts ## Overview cards and layouts
The Overview contains live counters, charts and optional backend, cache, PHP, Docker and Stream cards. Under **Settings → Appearance → Dashboard layout** or the Overview editor, configure visibility, order, card width, density and the number of history samples. A hidden card stops consuming its optional browser-side visualization work but does not turn off its underlying proxy feature. The Overview contains live counters, charts and optional backend, cache, PHP, Docker and Stream cards. Under **Settings → Appearance → Dashboard layout** or the Overview editor, configure visibility, order, card width, density and the number of history samples. A hidden card stops consuming its optional browser-side visualization work but does not turn off its underlying proxy feature.
+1 -1
View File
@@ -27,7 +27,7 @@ Practical guides for **RustProxy 0.34.9**. This index and the listed pages are a
- [Certificates and automatic renewal](ACME_PROVIDERS.md) — PEM, HTTP-01, DNS-01, Certbot and external certificates. - [Certificates and automatic renewal](ACME_PROVIDERS.md) — PEM, HTTP-01, DNS-01, Certbot and external certificates.
- [Docker discovery](DOCKER_DISCOVERY.md) — optional Linux agent DEB/RPM or manual installation, TLS/token CLI, networking, generated Vhosts and their per-service live HTTP logs. - [Docker discovery](DOCKER_DISCOVERY.md) — optional Linux agent DEB/RPM or manual installation, TLS/token CLI, networking, generated Vhosts and their per-service live HTTP logs.
- [Other integrations](INTEGRATIONS.md) — Tinyauth, NPM migration, OPNsense and monitoring tools. - [Other integrations](INTEGRATIONS.md) — Tinyauth, NPM migration, OPNsense and monitoring tools.
- [Dashboard](DASHBOARD.md) — metrics panels, HIT/MISS/BYPASS/OTHER request mix, presets and configuration history. - [Dashboard](DASHBOARD.md) — metrics panels, HIT/MISS/BYPASS/OTHER request mix, presets, configuration history and applying stylesheet updates.
- [Monitoring](MONITORING.md) — Prometheus, full JSON and compact `summary` API, traffic, backend and cache observations. - [Monitoring](MONITORING.md) — Prometheus, full JSON and compact `summary` API, traffic, backend and cache observations.
- [Logs](LOGS.md) — live logs, stored logs, error logs and searches. - [Logs](LOGS.md) — live logs, stored logs, error logs and searches.
- [Backups and recovery](BACKUP.md) — encrypted backups, restore and Secret Store. - [Backups and recovery](BACKUP.md) — encrypted backups, restore and Secret Store.
+68
View File
@@ -10,6 +10,74 @@ External development and QA helpers live under `tools/`; external platform integ
Keep these paths stable because Rust source, `build.rs` and shell entry points reference them directly. Keep these paths stable because Rust source, `build.rs` and shell entry points reference them directly.
## Panel stylesheet sources
The dashboard stylesheet lives in `src/admin/web/css/`, with its complete cascade
order declared in `manifest.json`. Edit those modules, not a generated bundle.
`build.rs` invokes `build/panel_css.py` using Python 3.10+ (or
`RUST_PROXY_PYTHON`) and embeds the compact `OUT_DIR/panel-style.css`. Login and
the dashboard continue to load one fingerprinted stylesheet, using the existing
panel compression and access rules; no npm install or runtime CSS imports are
needed. The documentation page retains its separate `src/admin/web/docs.css`.
```bash
python3 scripts/build/panel_css.py --check
python3 tools/dev/test_panel_css.py
python3 scripts/audit_css_usage.py --strict
```
The first two commands use only the Python standard library. The optional
selector/structure audit needs `tinycss2` and `cssselect2`; it checks all manifest
modules together and reviews the documentation stylesheet separately. Use
`--json PATH` for selector references and `--structure-json PATH` for repeated
selectors, conditional blocks, properties and identical declaration blocks.
Missing class tokens are review candidates: classes assembled by JavaScript
must be checked before deleting their styles. Strict mode also rejects identical
declaration bodies within a compatible selector family. It permits separate
standard/WebKit/Gecko rules only when joining them would create an incompatible
vendor selector list. Choose shared-rule placement by checking the cascade,
not by mechanically moving every matching block to the beginning of a module.
Preflight rejects missing, repeated or unlisted CSS modules and stale monolithic
`style.css` files. `./dev.sh repair` backs up only the exact original 0.34.9
monolith; custom or unknown content requires manual review and is never moved
or discarded automatically. Rebuild the Rust binary after changing CSS.
Keep `scripts/__init__.py` and `scripts/build/__init__.py` in the source package.
They keep preflight, audit, tests and packaging imports local even when unrelated
Python packages named `scripts` or `build` are installed. An older CSS-refactor
archive without the build marker can fail
with `ModuleNotFoundError: No module named 'build.panel_css'`; restore the marker
from the complete source archive and run `bash dev.sh repair` again. Installing
another Python package is not required.
### Missing generated CSS during Rust compilation
`panel-style.css` is generated inside Cargo's `OUT_DIR`. `bash dev.sh repair`
validates the source layout and preserves known obsolete files; it does not
generate Cargo outputs. Preflight also checks that the current `build.rs` calls
the panel generator from `main` and reports a mixed source tree before Cargo.
After overlaying a source ZIP onto a previously built checkout, a compiler error
for a missing `OUT_DIR/panel-style.css` can indicate an old cached build script
or a `build.rs` from another source version. Restore the complete matching
archive, including the root `build.rs`, then run from the project root:
```bash
bash dev.sh repair
touch build.rs
bash dev.sh build
```
The source ZIP uses reproducible timestamps. Cargo's
[build-script change detection](https://doc.rust-lang.org/cargo/reference/build-scripts.html#change-detection)
uses file modification times, so refreshing `build.rs` forces regeneration after
an archive overlay. This works with the target directory selected by `dev.sh`.
The generator now also registers its output as a watched Cargo dependency and
recreates a removed bundle. An identical bundle keeps its modification time.
The Rust build script rejects a missing, empty or non-file output immediately
after the helper returns.
## Stream TCP/UDP test helpers ## Stream TCP/UDP test helpers
- `start_stream_backends.sh` — launches two TCP echo mocks (9101, 9102) and two UDP echo/probe mocks (9201, 9202), defaulting to loopback, and terminates all on Ctrl+C. - `start_stream_backends.sh` — launches two TCP echo mocks (9101, 9102) and two UDP echo/probe mocks (9201, 9202), defaulting to loopback, and terminates all on Ctrl+C.
+1
View File
@@ -0,0 +1 @@
"""Application-owned RustProxy scripts."""
+122 -11
View File
@@ -3,14 +3,14 @@
Requires: pip install tinycss2 cssselect2 (audit-only tooling, not runtime deps). Requires: pip install tinycss2 cssselect2 (audit-only tooling, not runtime deps).
Output is an inventory, NOT permission to remove dynamically generated styles. Output is an inventory, NOT permission to remove dynamically generated styles.
Selectors are considered dead only when a required class/ID token is absent Absent class/ID tokens are candidates for review, because templates can assemble
from all application templates and scripts; :not() is not treated as a requirement. class names at runtime; :not() is not treated as a positive requirement.
The Swagger UI bundle emits its own markup, so docs.css is reviewed separately. The Swagger UI bundle emits its own markup, so docs.css is reviewed separately.
""" """
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from collections import Counter from collections import defaultdict
import json import json
from pathlib import Path from pathlib import Path
import re import re
@@ -23,14 +23,106 @@ except ImportError as exc:
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
WEB = ROOT / 'src/admin/web' WEB = ROOT / 'src/admin/web'
STYLES = ('style.css', 'docs.css')
SOURCE_SUFFIXES = {'.js', '.html', '.json', '.rs'} SOURCE_SUFFIXES = {'.js', '.html', '.json', '.rs'}
UNSUPPORTED_PSEUDO_ELEMENTS = re.compile(r'::(?:-webkit-[\w-]+|-moz-[\w-]+|backdrop)\b') UNSUPPORTED_PSEUDO_ELEMENTS = re.compile(r'::(?:-webkit-[\w-]+|-moz-[\w-]+|backdrop)\b')
WORD_PATTERN = re.compile(r'(?<![\w-])[a-zA-Z_][\w-]*(?![\w-])') WORD_PATTERN = re.compile(r'(?<![\w-])[a-zA-Z_][\w-]*(?![\w-])')
VENDOR_SELECTOR = re.compile(r'::?-(webkit|moz|ms|o)-[\w-]+')
def panel_sources() -> list[Path]:
if __package__:
from .build.panel_css import source_files
else:
from build.panel_css import source_files
return source_files(ROOT)
def embedded_javascript_sources() -> list[Path]:
paths = set()
for parent in (ROOT / 'src').rglob('*.rs'):
for relative in re.findall(r'\binclude_str!\s*\(\s*"([^"\n]+\.js)"\s*\)', parent.read_text(encoding='utf-8')):
path = (parent.parent / relative).resolve()
if path.is_relative_to(WEB) and path.is_file():
paths.add(path)
return sorted(paths)
def structural_inventory(paths: list[Path]) -> dict:
"""Report repetitions in their actual conditional scope; never reorder CSS."""
selectors = defaultdict(list)
conditions = defaultdict(list)
identical_rules = defaultdict(list)
identical_blocks = defaultdict(list)
properties = []
parse_errors = []
def walk(items, path, context=()):
for item in items:
location = f'{path.relative_to(ROOT)}:{item.source_line}'
if item.type == 'error':
parse_errors.append({'location': location, 'message': item.message})
elif item.type == 'qualified-rule':
parts = selector_parts(item.prelude)
declarations = []
property_locations = defaultdict(list)
for declaration in tinycss2.parse_declaration_list(item.content, skip_whitespace=True, skip_comments=True):
if declaration.type == 'error':
parse_errors.append({'location': location, 'message': declaration.message})
elif declaration.type == 'declaration':
name = declaration.name if declaration.name.startswith('--') else declaration.lower_name
value = tinycss2.serialize(declaration.value).strip()
declarations.append((name, value, declaration.important))
property_locations[name].append(declaration.source_line)
for name, lines in property_locations.items():
if len(lines) > 1:
properties.append({'location': location, 'selector': ', '.join(parts), 'property': name, 'lines': lines})
for selector in parts:
selectors[(context, selector)].append(location)
identical_rules[(context, tuple(parts), tuple(declarations))].append(location)
if declarations:
identical_blocks[(context, tuple(declarations))].append({'location': location, 'selectors': parts})
elif item.type == 'at-rule' and item.content is not None:
prelude = re.sub(r'\s+', ' ', tinycss2.serialize(item.prelude).strip())
prelude = re.sub(r'\s*([():,])\s*', r'\1', prelude)
label = f'@{item.lower_at_keyword} {prelude}'
if item.lower_at_keyword in ('media', 'container', 'supports'):
conditions[(context, label)].append(location)
if item.lower_at_keyword in ('media', 'container', 'supports', 'layer', 'keyframes', '-webkit-keyframes'):
walk(tinycss2.parse_rule_list(item.content, skip_whitespace=True, skip_comments=True), path, context + (label,))
for path in paths:
walk(tinycss2.parse_stylesheet(path.read_text(encoding='utf-8'), skip_whitespace=True, skip_comments=True), path)
repeated_bodies = []
for (scope, _), entries in identical_blocks.items():
if len(entries) < 2:
continue
families = [tuple(sorted({family for selector in entry['selectors']
for family in VENDOR_SELECTOR.findall(selector)}))
for entry in entries]
# A browser can reject an entire comma-separated selector list when it
# contains another engine's pseudo-element. Keep one body per compatible
# family, while still rejecting copies within the same family.
vendor_only = (any(families) and all(len(family) <= 1 for family in families)
and len(families) == len(set(families)))
repeated_bodies.append({'scope': list(scope), 'rules': entries,
'vendor_compatibility_only': vendor_only})
return {
'repeated_selectors': [{'scope': list(scope), 'selector': selector, 'locations': locations}
for (scope, selector), locations in selectors.items() if len(locations) > 1],
'repeated_conditions': [{'scope': list(scope), 'condition': condition, 'locations': locations}
for (scope, condition), locations in conditions.items() if len(locations) > 1],
'identical_rules': [{'scope': list(scope), 'selectors': list(parts), 'locations': locations}
for (scope, parts, _), locations in identical_rules.items() if len(locations) > 1],
'identical_declaration_blocks': repeated_bodies,
'repeated_properties': properties,
'parse_errors': parse_errors,
}
def source_words(scope: str) -> set[str]: def source_words(scope: str) -> set[str]:
paths = list(WEB.rglob('*.js')) + list(WEB.rglob('*.html')) # Rust's include_str! lists are the asset entry points; an abandoned JS file
# beside a bundle must not make its obsolete selectors appear live.
paths = embedded_javascript_sources() + list(WEB.rglob('*.html'))
if scope == 'panel': if scope == 'panel':
paths += list(WEB.rglob('*.json')) + list((ROOT / 'src').rglob('*.rs')) paths += list(WEB.rglob('*.json')) + list((ROOT / 'src').rglob('*.rs'))
else: else:
@@ -80,8 +172,8 @@ def token_refs(node) -> list[tuple[str, str]]:
def possibly_matches(node, words: set[str]) -> bool: def possibly_matches(node, words: set[str]) -> bool:
# Deliberately conservative: class/id absence proves a positive selector # Only positive class/id requirements identify review candidates. Runtime
# impossible but does NOT make a selector in :not() impossible. # interpolation still requires manual inspection before removing a rule.
if isinstance(node, css_parser.ClassSelector): if isinstance(node, css_parser.ClassSelector):
return node.class_name in words return node.class_name in words
if isinstance(node, css_parser.IDSelector): if isinstance(node, css_parser.IDSelector):
@@ -125,7 +217,9 @@ def audit_file(path: Path, words: set[str], scope: str) -> dict:
if external: if external:
dead = False dead = False
missing = sorted({f'{typ}:{word}' for typ, word in references if word not in words}) missing = sorted({f'{typ}:{word}' for typ, word in references if word not in words})
parts.append({'selector': selector, 'dead': dead, 'missing_tokens': missing, # Retain the historical JSON key for older audit consumers;
# "dead" is a candidate flag, never proof of runtime absence.
parts.append({'selector': selector, 'dead': dead, 'review_required': dead, 'missing_tokens': missing,
'external': external}) 'external': external})
except css_parser.SelectorError as exc: except css_parser.SelectorError as exc:
parts.append({'selector': selector, 'dead': False, 'missing_tokens': [], 'unparsed': str(exc)}) parts.append({'selector': selector, 'dead': False, 'missing_tokens': [], 'unparsed': str(exc)})
@@ -146,13 +240,21 @@ def audit_file(path: Path, words: set[str], scope: str) -> dict:
def main(): def main():
cli = argparse.ArgumentParser(description=__doc__) cli = argparse.ArgumentParser(description=__doc__)
cli.add_argument('--json', type=Path, help='Write complete selector audit JSON to this file') cli.add_argument('--json', type=Path, help='Write complete selector audit JSON to this file')
cli.add_argument('--structure-json', type=Path, help='Write cross-module duplicate inventory JSON to this file')
cli.add_argument('--strict', action='store_true', help='Fail on repeated panel selectors/conditions/rules/properties, compatible declaration blocks or parse errors')
args = cli.parse_args() args = cli.parse_args()
results = [audit_file(WEB / filename, source_words('docs' if filename == 'docs.css' else 'panel'), panel = panel_sources()
'docs' if filename == 'docs.css' else 'panel') for filename in STYLES] orphan_scripts = sorted(set(WEB.rglob('*.js')) - set(embedded_javascript_sources()))
if orphan_scripts:
print('JavaScript outside embedded asset entry points (review):', ', '.join(str(path.relative_to(ROOT)) for path in orphan_scripts))
paths = [*panel, WEB / 'docs.css']
words = {'panel': source_words('panel'), 'docs': source_words('docs')}
results = [audit_file(path, words['docs' if path.name == 'docs.css' else 'panel'],
'docs' if path.name == 'docs.css' else 'panel') for path in paths]
for r in results: for r in results:
print(f"{r['file']}: {r['total_rules']} rules, {r['total_selectors']} selector branches, " print(f"{r['file']}: {r['total_rules']} rules, {r['total_selectors']} selector branches, "
f"{len(r['referenced_classes'])} referenced classes, {len(r['unreferenced_classes'])} unreferenced classes, " f"{len(r['referenced_classes'])} referenced classes, {len(r['unreferenced_classes'])} unreferenced classes, "
f"{r['unused_selector_count']} provably dead selector branches, {len(r['unparsed'])} unparsed") f"{r['unused_selector_count']} selector branches requiring review, {len(r['unparsed'])} unparsed")
print(' Unreferenced:', ', '.join(r['unreferenced_classes']) or '(none)') print(' Unreferenced:', ', '.join(r['unreferenced_classes']) or '(none)')
for e in r['entries']: for e in r['entries']:
for p in e['parts']: for p in e['parts']:
@@ -160,6 +262,15 @@ def main():
print(f" {e['line']}: {p['selector'][:170]} ({', '.join(p['missing_tokens'])})") print(f" {e['line']}: {p['selector'][:170]} ({', '.join(p['missing_tokens'])})")
if args.json: if args.json:
args.json.write_text(json.dumps(results, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') args.json.write_text(json.dumps(results, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
structure = structural_inventory(panel)
print('Panel structure:', ', '.join(f'{len(entries)} {name.replace("_", " ")}' for name, entries in structure.items()))
if args.structure_json:
args.structure_json.write_text(json.dumps(structure, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
failures = [name for name in ('repeated_selectors', 'repeated_conditions', 'identical_rules', 'repeated_properties', 'parse_errors') if structure[name]]
if any(not group['vendor_compatibility_only'] for group in structure['identical_declaration_blocks']):
failures.append('identical_declaration_blocks')
if args.strict and failures:
raise SystemExit('Panel CSS structure check failed: ' + ', '.join(failures))
if __name__ == '__main__': if __name__ == '__main__':
+1
View File
@@ -0,0 +1 @@
"""RustProxy build helpers; keep local imports separate from external build packages."""
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""Build the embedded panel stylesheet in manifest order, without npm or imports.
Only comments and formatting whitespace beside rule/declaration delimiters are
minified. Selector whitespace, escapes, strings, functions (including calc and
url), declaration values and rule order are otherwise retained verbatim.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[2]
CSS_DIRECTORY = Path("src/admin/web/css")
FILENAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]*\.css\Z")
WHITESPACE = " \t\n\r\f"
HEX = "0123456789abcdefABCDEF"
DELIMITERS = frozenset("{};")
def _escape_end(text: str, start: int) -> int:
end = start + 1
if end == len(text):
raise ValueError("Unterminated CSS escape")
if text[end] in HEX:
limit = min(end + 6, len(text))
while end < limit and text[end] in HEX:
end += 1
if end < len(text) and text[end] in WHITESPACE:
if text[end:end + 2] == "\r\n":
return end + 2
end += 1
return end
return end + (2 if text[end:end + 2] == "\r\n" else 1)
def _string_end(text: str, start: int) -> int:
end = start + 1
while end < len(text):
if text[end] == text[start]:
return end + 1
if text[end] == "\\":
end = _escape_end(text, end)
elif text[end] in "\n\r\f":
raise ValueError("Unescaped newline in CSS string")
else:
end += 1
raise ValueError("Unterminated CSS string")
def _comment_end(text: str, start: int) -> int:
end = text.find("*/", start + 2)
if end < 0:
raise ValueError("Unterminated CSS comment")
return end + 2
def _group_end(text: str, start: int) -> int:
# Do not rewrite anything in functions or attribute selectors. This also
# protects unquoted data URLs and whitespace required by calc()/CSS escapes.
closing = ")" if text[start] == "(" else "]"
is_url = (text[start] == "(" and text[max(0, start - 3):start].lower() == "url"
and (start < 4 or not (text[start - 4].isalnum() or text[start - 4] in "_-")))
end = start + 1
while end < len(text):
if text[end] == closing:
return end + 1
if text[end] in "\"'":
end = _string_end(text, end)
elif text[end] == "\\":
end = _escape_end(text, end)
elif text.startswith("/*", end) and not is_url:
end = _comment_end(text, end)
elif text[end] in "([" and not is_url:
end = _group_end(text, end)
elif text[end] in ")]" and not is_url:
raise ValueError("Mismatched CSS delimiter")
else:
# In an unquoted url(), /* and square brackets are literal data.
end += 1
raise ValueError(f"Unterminated CSS {text[start]} group")
def minify(text: str) -> str:
"""Conservatively compact a stylesheet without reordering its CSS tokens."""
tokens: list[tuple[str, str]] = []
position = 0
depth = 0
while position < len(text):
char = text[position]
if char in WHITESPACE:
end = position + 1
while end < len(text) and text[end] in WHITESPACE:
end += 1
kind = "space"
elif text.startswith("/*", position):
end, kind = _comment_end(text, position), "comment"
elif char in "\"'":
end, kind = _string_end(text, position), "protected"
elif char == "\\":
end, kind = _escape_end(text, position), "protected"
elif char in "([":
end, kind = _group_end(text, position), "protected"
else:
end, kind = position + 1, "plain"
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth < 0:
raise ValueError("Unexpected closing CSS brace")
elif char in ")]":
raise ValueError("Unexpected closing CSS delimiter")
tokens.append((kind, text[position:end]))
position = end
if depth:
raise ValueError("Unterminated CSS rule")
# Source modules must stay independent of runtime imports and encoding
# declarations; all are UTF-8 and are delivered as one embedded response.
for index, token in enumerate(tokens):
if token != ("plain", "@"):
continue
name = ""
for offset in range(index + 1, len(tokens)):
kind, value = tokens[offset]
if kind == "plain" and (value.isalnum() or value in "_-"):
name += value
elif kind == "protected" and value.startswith("\\"):
escaped = value[1:]
if escaped and escaped[0] in HEX:
codepoint = int(escaped.rstrip(WHITESPACE), 16)
name += chr(codepoint) if 0 < codepoint <= 0x10FFFF else "\ufffd"
else:
name += escaped
else:
break
if name.lower() in ("import", "charset"):
raise ValueError("CSS modules must not contain @import or @charset")
compact: list[tuple[str, str]] = []
for index, (kind, value) in enumerate(tokens):
if kind == "comment":
if value.startswith("/*!"):
compact.append(("protected", value))
continue
before = tokens[index - 1] if index else None
after = tokens[index + 1] if index + 1 < len(tokens) else None
if (before and after and before[0] != "space" and after[0] != "space"
and not (before[0] == "plain" and before[1] in DELIMITERS)
and not (after[0] == "plain" and after[1] in DELIMITERS)):
# Retain a token boundary: e.g. --value:foo/**/bar must never
# become the single identifier foobar. No guessed whitespace.
compact.append(("protected", "/**/"))
continue
if kind == "space" and compact and compact[-1][0] == "space":
compact[-1] = ("space", compact[-1][1] + value)
else:
compact.append((kind, value))
output: list[str] = []
for index, (kind, value) in enumerate(compact):
if kind == "space":
before = compact[index - 1] if index else None
after = compact[index + 1] if index + 1 < len(compact) else None
if (not before or not after
or (before[0] == "plain" and before[1] in DELIMITERS)
or (after[0] == "plain" and after[1] in DELIMITERS)):
continue
output.append(value)
return "".join(output)
def source_files(root: Path = ROOT) -> list[Path]:
directory = root / CSS_DIRECTORY
manifest = directory / "manifest.json"
if directory.is_symlink() or manifest.is_symlink():
raise ValueError("Panel CSS directory and manifest must not be symlinks")
config = json.loads(manifest.read_text(encoding="utf-8"))
if not isinstance(config, dict) or set(config) != {"files"}:
raise ValueError('CSS manifest must be an object containing only a "files" array')
names = config["files"]
if (not isinstance(names, list) or not names
or not all(isinstance(name, str) and FILENAME.fullmatch(name) for name in names)):
raise ValueError("CSS manifest must list CSS basenames without directory components")
if len(names) != len(set(names)):
raise ValueError("CSS manifest lists a source more than once")
actual = {path.name for path in directory.iterdir() if path.suffix.lower() == ".css"}
if set(names) != actual:
missing = sorted(set(names) - actual)
unlisted = sorted(actual - set(names))
raise ValueError(f"CSS manifest mismatch: missing={missing}; unlisted={unlisted}")
paths = [directory / name for name in names]
for path in paths:
if path.is_symlink() or not path.is_file():
raise ValueError(f"CSS source must be a regular, non-symlink file: {path.name}")
return paths
def bundle(root: Path = ROOT) -> tuple[str, list[Path], int]:
paths = source_files(root)
parts: list[str] = []
source_bytes = 0
for path in paths:
raw = path.read_bytes()
source_bytes += len(raw)
try:
parts.append(minify(raw.decode("utf-8")))
except ValueError as error:
raise ValueError(f"{path.name}: {error}") from error
return "\n".join(parts) + "\n", paths, source_bytes
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=ROOT)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--out-dir", type=Path, help="Write panel-style.css here")
mode.add_argument("--check", action="store_true", help="Validate sources without writing files")
parser.add_argument("--cargo", action="store_true", help="Emit Cargo input/output dependencies")
args = parser.parse_args()
try:
root = args.root.resolve()
result, paths, source_bytes = bundle(root)
encoded = result.encode("utf-8")
if args.out_dir is not None:
args.out_dir.mkdir(parents=True, exist_ok=True)
destination = args.out_dir / "panel-style.css"
if not destination.exists() or destination.read_bytes() != encoded:
destination.write_bytes(encoded)
if args.cargo:
watched = [root / CSS_DIRECTORY, root / CSS_DIRECTORY / "manifest.json", *paths]
if args.out_dir is not None:
# Cargo must rerun the generator when its cached output is
# removed even if every source module is unchanged.
watched.append(args.out_dir.resolve() / "panel-style.css")
for path in watched:
if any(char in str(path) for char in "\r\n\0"):
raise ValueError("CSS build dependency path contains a control character")
print(f"cargo:rerun-if-changed={path}")
else:
print(f"Panel CSS: {len(paths)} modules, {source_bytes} source bytes, "
f"{len(encoded)} bundled bytes; cascade order preserved.")
except (OSError, ValueError) as error:
print(f"Panel CSS preparation failed: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+74 -1
View File
@@ -8,10 +8,74 @@ from pathlib import Path
import sys import sys
LEGACY_AUTH = "4df9d473c9214365d91c83072f97aafab6ede4edcf4e1369b5c3679ded5cff85" LEGACY_AUTH = "4df9d473c9214365d91c83072f97aafab6ede4edcf4e1369b5c3679ded5cff85"
LEGACY_PANEL_CSS = "38716fae968aeb8f6a8510f28bb9054a73a438a969c3b69f9c9d921acfed6e06"
def validate_panel_css_build(root: Path) -> None:
"""Check this project's build.rs startup contract, not arbitrary Rust code."""
source = (root / "build.rs").read_text(encoding="utf-8")
# Ignore ordinary strings and comments, including nested block comments.
# The required call precedes the first nested block in the shipped main().
code: list[str] = []
position = 0
token = re.compile(r'"(?:\\.|[^"\\])*"|//[^\n]*|/\*', re.DOTALL)
while match := token.search(source, position):
code.extend((source[position:match.start()], " "))
position = match.end()
if match.group() == "/*":
depth = 1
while depth:
boundary = re.search(r'/\*|\*/', source[position:])
if boundary is None:
position = len(source)
break
depth += 1 if boundary.group() == "/*" else -1
position += boundary.end()
code.append(source[position:])
main = re.search(r'\bfn\s+main\s*\(\s*\)[^{]*\{([^{}]*)', "".join(code))
if main is None or re.search(
r'\bpanel_styles\s*\(\s*&root\s*,\s*&out\s*,?\s*\)\s*\?\s*;',
main.group(1),
) is None:
raise ValueError(
"Mixed modular panel CSS source tree: build.rs is missing the expected "
"panel_styles(&root, &out)? call in main's startup block. Restore build.rs "
"from the complete matching source archive, preserving your local edits, "
"then run 'touch build.rs' and retry ./dev.sh. "
"Automatic repair does not rewrite build.rs."
)
def preserve_legacy_panel_css(root: Path, repair_known: bool = False) -> list[str]:
old = root / "src/admin/web/style.css"
manifest = root / "src/admin/web/css/manifest.json"
if not manifest.is_file() or not (old.exists() or old.is_symlink()):
return []
if old.is_symlink() or hashlib.sha256(old.read_bytes()).hexdigest() != LEGACY_PANEL_CSS:
raise ValueError(
"Obsolete src/admin/web/style.css remains beside the modular CSS sources and "
"contains unknown/custom content. Preserve it outside src/admin/web, review "
"your changes and port them into the CSS modules before rebuilding. "
"Automatic repair does not move or discard custom styles."
)
if not repair_known:
raise ValueError(
"Known obsolete 0.34.9 src/admin/web/style.css remains beside the modular CSS sources. "
"Run ./dev.sh repair to preserve it under .upgrade-backup/panel-css/."
)
target = root / ".upgrade-backup/panel-css/style.css.0.34.9"
if target.exists() or target.is_symlink():
if target.is_symlink() or target.read_bytes() != old.read_bytes():
raise ValueError("Legacy CSS backup already contains different data; refusing to overwrite it")
old.unlink() # The exact original is already backed up.
else:
target.parent.mkdir(parents=True, exist_ok=True)
old.rename(target)
return [f"Preserved obsolete panel CSS as {target.relative_to(root)}"]
def check(root: Path, repair_known: bool = False) -> list[str]: def check(root: Path, repair_known: bool = False) -> list[str]:
messages: list[str] = [] messages: list[str] = preserve_legacy_panel_css(root, repair_known)
obsolete_split_files = ( obsolete_split_files = (
"src/admin/web/app.js", "src/admin/web/app.js",
"src/admin/web/vhosts.js", "src/admin/web/vhosts.js",
@@ -71,6 +135,15 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.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/mod.rs", "src/diagnostics.rs", "src/traffic/mod.rs", "src/system_stats/mod.rs", "src/system_stats/linux/mod.rs", "src/vhost.rs", "src/vhost/error_fallback.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/mod.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.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/session-resume.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/mod.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth/mod.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", "CHANGELOG.md", "ROADMAP.md", "scripts/runtime/acme_provider.py", "src/basic_auth/mod.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/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/MONITORING.md", "docs/CUSTOM_LOCATIONS.md", "grafana/rustproxy-overview.json"]: for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.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/mod.rs", "src/diagnostics.rs", "src/traffic/mod.rs", "src/system_stats/mod.rs", "src/system_stats/linux/mod.rs", "src/vhost.rs", "src/vhost/error_fallback.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/mod.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.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/session-resume.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/mod.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth/mod.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", "CHANGELOG.md", "ROADMAP.md", "scripts/runtime/acme_provider.py", "src/basic_auth/mod.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/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/MONITORING.md", "docs/CUSTOM_LOCATIONS.md", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file(): if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.34.2 source tree: missing {path}") raise ValueError(f"Incomplete RustProxy 0.34.2 source tree: missing {path}")
for path in ("scripts/__init__.py", "scripts/build/__init__.py", "scripts/build/panel_css.py", "src/admin/web/css/manifest.json"):
if not (root / path).is_file():
raise ValueError(f"Incomplete modular panel CSS source tree: missing {path}")
validate_panel_css_build(root)
if __package__:
from .build.panel_css import bundle
else:
from build.panel_css import bundle
bundle(root) # Validate the manifest and every module without generating files.
registry = root / "src/admin/documentation/documents.rs" registry = root / "src/admin/documentation/documents.rs"
if not registry.is_file(): if not registry.is_file():
raise ValueError("Incomplete RustProxy 0.34.2 source tree: missing src/admin/documentation/documents.rs") raise ValueError("Incomplete RustProxy 0.34.2 source tree: missing src/admin/documentation/documents.rs")
+5 -1
View File
@@ -79,7 +79,11 @@ static ASSETS: LazyLock<HashMap<&'static str, Asset>> = LazyLock::new(|| {
("/assets/streams.js", JS, include_str!("web/streams.js")), ("/assets/streams.js", JS, include_str!("web/streams.js")),
("/assets/redirects.js", JS, include_str!("web/redirects.js")), ("/assets/redirects.js", JS, include_str!("web/redirects.js")),
("/assets/host404.js", JS, include_str!("web/host404.js")), ("/assets/host404.js", JS, include_str!("web/host404.js")),
("/assets/style.css", CSS, include_str!("web/style.css")), (
"/assets/style.css",
CSS,
include_str!(concat!(env!("OUT_DIR"), "/panel-style.css")),
),
("/favicon.svg", SVG, include_str!("web/favicon.svg")), ("/favicon.svg", SVG, include_str!("web/favicon.svg")),
]; ];
entries entries
+63
View File
@@ -2966,6 +2966,69 @@ mod tests {
} }
store.close().await; store.close().await;
} }
#[tokio::test]
async fn modular_panel_css_is_one_embedded_public_asset_with_existing_compression() {
use std::io::Read;
let (router, store, _dir, secret, _) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let css = include_str!(concat!(env!("OUT_DIR"), "/panel-style.css"));
let path = format!("/assets/style_{}.css", super::asset_build_id());
for authenticated in [false, true] {
let mut req = request(&path);
if authenticated {
req = req.header("cookie", &cookie);
}
let response = router
.clone()
.oneshot(req.body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["content-type"], "text/css; charset=utf-8");
assert_eq!(response.headers()["vary"], "Accept-Encoding");
let bytes = axum::body::to_bytes(response.into_body(), css.len() + 1)
.await
.unwrap();
assert_eq!(bytes.as_ref(), css.as_bytes());
}
let response = router
.clone()
.oneshot(
request(&path)
.header("accept-encoding", "gzip")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["content-encoding"], "gzip");
let compressed = axum::body::to_bytes(response.into_body(), css.len() + 1024)
.await
.unwrap();
let mut decoded = Vec::new();
flate2::read::GzDecoder::new(compressed.as_ref())
.read_to_end(&mut decoded)
.unwrap();
assert_eq!(decoded.as_slice(), css.as_bytes());
for path in ["/assets/css/manifest.json", "/assets/css/base.css"] {
let response = router
.clone()
.oneshot(
request(path)
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{path}");
}
store.close().await;
}
#[tokio::test] #[tokio::test]
async fn public_document_and_discovery_contain_only_login_information() { async fn public_document_and_discovery_contain_only_login_information() {
let (router, store, _dir, _, _) = fixture().await; let (router, store, _dir, _, _) = fixture().await;
+518
View File
@@ -0,0 +1,518 @@
/* Theme palettes and appearance tokens. Runtime preference variables remain writable by JavaScript.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
:root {
color-scheme: dark;
--bg: #131615;
--panel: #1b1f1d;
--panel2: #202622;
--line: #323a34;
--muted: #a0aaa0;
--text: #edf0e9;
--accent: #f69b74;
--green: #bdd293;
--red: #ffaaa2;
--sidebar-bg: #101310;
--brand-secondary: #cfe0ac;
--nav-active-bg: #242b23;
--nav-hover-bg: #20251f;
--text-subtle: #879082;
--separator: #596454;
--border-hover: #89917c;
--field-bg: #131813;
--field-line: #47523f;
--placeholder: #778070;
--row-line: #2b332c;
--outline-line: #717b67;
--chart-grid: #333c33;
--code-bg: #101810;
--code-alt-bg: #101922;
--code-alt-text: #d3e4ec;
--nav-selected-bg: #34412a;
--accent-ink: #221810;
--accent-hover: #ffb090;
--selected-ink: #10251e;
--notice-text: #c9b99c;
--notice-line: #554833;
--notice-bg: #29251c;
--pill-line: #6d5a45;
--pill-live-line: #576847;
--toast-bg: #d0dfb4;
--toast-text: #1c2818;
--toast-line: #ebf2de;
--toast-error-bg: #ffd2bd;
--toast-error-line: #ff9f7a;
--toast-error-text: #42241b;
--danger-line: #644a47;
--danger-line-strong: #79514e;
--warn-line: #786647;
--blue: #8ebcff;
--transform-chart: #d7adff;
--backdrop: rgba(0, 0, 0, .68);
--shadow-mid: #0006;
--shadow-strong: #0009;
--neutral-soft: hsla(0, 0%, 49.8%, .05);
--scrollbar-track: #151a16;
--scrollbar-thumb: #4f5d50;
--scrollbar-thumb-hover: #6b7b69;
--chart-accent: var(--accent);
--chart-secondary: var(--green);
--chart-tertiary: var(--blue);
--chart-quaternary: var(--transform-chart);
--chart-quinary: var(--red);
--chart-senary: var(--notice-text);
--chart-septenary: var(--border-hover);
--chart-octonary: var(--separator);
--chart-surface: var(--panel2);
--chart-surface-alt: var(--panel);
--chart-line-width: 2;
--chart-linecap: round;
--chart-linejoin: round;
--chart-grid-opacity: .28;
--chart-grid-dash: none;
--chart-svg-filter: none;
--rp-progress-fill: var(--chart-accent);
--rp-progress-track: var(--line);
--rp-progress-radius: 3px;
--rp-nav-active-color: var(--brand-secondary);
--rp-nav-active-bg: var(--nav-active-bg);
--rp-nav-hover-bg: var(--nav-hover-bg);
--traffic-h1: var(--chart-accent);
--traffic-h2: var(--chart-secondary);
--traffic-h3: var(--chart-tertiary);
--ui-scale: 1;
--ui-field-font-size: 12px;
--ui-field-pad-y: 6px;
--ui-field-pad-x: 8px;
--ui-field-min-height: 27px;
--ui-check-size: 15px;
--ui-range-height: 18px;
--mono: ui-monospace, SFMono-Regular, Consolas, monospace;
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html[data-theme=light] {
color-scheme: light;
--bg: #f4f6f2;
--panel: #fff;
--panel2: #edf1eb;
--line: #cbd3c7;
--muted: #5e685f;
--text: #1d241f;
--accent: #b85b38;
--green: #5f783f;
--red: #b64a43;
--sidebar-bg: #eef2ec;
--brand-secondary: #6b8249;
--nav-active-bg: #e1e8dc;
--nav-hover-bg: #e8ede6;
--text-subtle: #687369;
--separator: #9ba69b;
--border-hover: #839083;
--field-bg: #fff;
--field-line: #b7c1b5;
--placeholder: #7a867b;
--row-line: #e0e5dd;
--outline-line: #98a497;
--chart-grid: #d4dbd1;
--code-bg: #edf3e9;
--code-alt-bg: #eef3f6;
--code-alt-text: #2d4049;
--nav-selected-bg: #e1e9db;
--accent-ink: #fff;
--accent-hover: #a84f30;
--selected-ink: #fff;
--notice-text: #685b45;
--notice-line: #d4c2a5;
--notice-bg: #fff9ef;
--pill-line: #cbb28f;
--pill-live-line: #a8ba8d;
--danger-line: #d7a29d;
--danger-line-strong: #c98f89;
--warn-line: #c6b27e;
--blue: #4f77a9;
--transform-chart: #7d4fa3;
--shadow-mid: rgba(31, 39, 33, .16);
--shadow-strong: rgba(31, 39, 33, .26);
--neutral-soft: rgba(70, 78, 72, .05);
--scrollbar-track: #e7ece4;
--scrollbar-thumb: #a9b5a6;
--scrollbar-thumb-hover: #879684;
}
html:is([data-theme=light], [data-theme=cream]) {
--rp-nav-active-color: color-mix(in srgb, var(--brand-secondary) 45%, var(--text) 55%);
}
html[data-theme=ultra_dark] {
color-scheme: dark;
--bg: #030404;
--panel: #0b0d0e;
--panel2: #121516;
--line: #292f30;
--muted: #a4afb0;
--text: #f1f4f2;
--accent: #93dfc8;
--logo-accent: #f69b74;
--logo-secondary: #cfe0ac;
--green: #b1dcb0;
--red: #ff9899;
--sidebar-bg: #000;
--brand-secondary: #a9cfb5;
--nav-active-bg: #172422;
--nav-hover-bg: #121819;
--text-subtle: #879695;
--separator: #526261;
--border-hover: #7c9f96;
--field-bg: #090b0c;
--field-line: #445352;
--placeholder: #879593;
--row-line: #202627;
--outline-line: #79968f;
--chart-grid: #303839;
--code-bg: #0e1313;
--code-alt-bg: #101a1d;
--code-alt-text: #d5eae9;
--nav-selected-bg: #213730;
--accent-ink: #071814;
--accent-hover: #b6f2dd;
--selected-ink: #071814;
--notice-text: #e6c99c;
--notice-line: #66533a;
--notice-bg: #201a11;
--pill-line: #625544;
--pill-live-line: #506c59;
--toast-bg: #bcf3da;
--toast-text: #10221a;
--toast-line: #d8ffe8;
--toast-error-bg: #ffd5d0;
--toast-error-line: #ff9c96;
--toast-error-text: #472625;
--danger-line: #754245;
--danger-line-strong: #965257;
--warn-line: #89754c;
--blue: #8bbdfb;
--transform-chart: #cfaeff;
--backdrop: rgba(0, 0, 0, .86);
--shadow-mid: rgba(0, 0, 0, .64);
--shadow-strong: rgba(0, 0, 0, .84);
--neutral-soft: rgba(132, 171, 158, .045);
--scrollbar-track: #060808;
--scrollbar-thumb: #364243;
--scrollbar-thumb-hover: #617373;
}
html[data-theme=cream] {
color-scheme: light;
--bg: #f2ede3;
--panel: #fcf9f2;
--panel2: #eae3d6;
--line: #d5cabb;
--muted: #6b6257;
--text: #302c27;
--accent: #8c5942;
--green: #5e7655;
--red: #a54645;
--sidebar-bg: #eae2d5;
--brand-secondary: #668367;
--nav-active-bg: #e0d3c0;
--nav-hover-bg: #e6dccc;
--text-subtle: #786e61;
--separator: #afa494;
--border-hover: #968776;
--field-bg: #fffcf7;
--field-line: #baad9a;
--placeholder: #807466;
--row-line: #e2d9cb;
--outline-line: #9c8d7c;
--chart-grid: #ddd3c4;
--code-bg: #f0e8da;
--code-alt-bg: #e7ebe9;
--code-alt-text: #364f4e;
--nav-selected-bg: #d9cbb7;
--accent-ink: #fff;
--accent-hover: #774632;
--selected-ink: #fff;
--notice-text: #765e40;
--notice-line: #d9c3a1;
--notice-bg: #f8efdf;
--pill-line: #c2a78d;
--pill-live-line: #b0bd9d;
--toast-bg: #d6ead2;
--toast-text: #263d29;
--toast-line: #b8d5b6;
--toast-error-bg: #f8ded7;
--toast-error-line: #dca99c;
--toast-error-text: #60352f;
--danger-line: #d2a29a;
--danger-line-strong: #b97870;
--warn-line: #b49c6b;
--blue: #4d7292;
--transform-chart: #83659a;
--backdrop: rgba(37, 27, 16, .44);
--shadow-mid: rgba(64, 49, 34, .13);
--shadow-strong: rgba(64, 49, 34, .24);
--neutral-soft: rgba(104, 91, 69, .045);
--scrollbar-track: #ece4d7;
--scrollbar-thumb: #b5a997;
--scrollbar-thumb-hover: #978a76;
}
body[data-density=dense] {
--ui-gap: 4px;
--ui-card: 7px;
--ui-font: 11.5px;
--ui-control: 25px;
}
body[data-density=comfortable] {
--ui-gap: 13px;
--ui-card: 17px;
--ui-font: 14px;
--ui-control: 36px;
}
html[data-button-style=compact] body {
--button-padding: 3px 7px;
--button-font-size: 10px;
--button-min-height: 22px;
}
.chart-style-choice[data-chart-preset=default] {
--sw1: var(--accent);
--sw2: var(--green);
--sw3: var(--blue);
--sw4: var(--transform-chart);
}
.chart-style-choice[data-chart-preset=soft] {
--sw1: #e8a998;
--sw2: #95c293;
--sw3: #8daef6;
--sw4: #b49de6;
}
.chart-style-choice[data-chart-preset=technical] {
--sw1: #79c9ff;
--sw2: #9adf90;
--sw3: #ffd56e;
--sw4: #c39cff;
}
.chart-style-choice[data-chart-preset=high_contrast] {
--sw1: #ffd54f;
--sw2: #8dff80;
--sw3: #6fd9ff;
--sw4: #ff78d7;
}
.chart-style-choice[data-chart-preset=neon] {
--sw1: #2cf6ff;
--sw2: #7eff85;
--sw3: #ff5cd0;
--sw4: #8b7cff;
}
.chart-style-choice[data-chart-preset=monochrome] {
--sw1: #f2f2f2;
--sw2: #b8b8b8;
--sw3: #858585;
--sw4: #595959;
}
.chart-style-choice[data-chart-preset=warm] {
--sw1: #ff9c5c;
--sw2: #f4c46b;
--sw3: #ff7b72;
--sw4: #f4a261;
}
.chart-style-choice[data-chart-preset=cool] {
--sw1: #5fb9ff;
--sw2: #45d3c3;
--sw3: #7d8fff;
--sw4: #70d7ff;
}
html[data-chart-style=default] {
--chart-accent: var(--accent);
--chart-secondary: var(--green);
--chart-tertiary: var(--blue);
--chart-quaternary: var(--transform-chart);
--chart-quinary: var(--red);
--chart-senary: var(--notice-text);
--chart-septenary: var(--border-hover);
--chart-octonary: var(--separator);
--chart-surface: var(--panel2);
--chart-surface-alt: var(--panel);
--chart-line-width: 2;
--chart-linecap: round;
--chart-linejoin: round;
--chart-grid-opacity: .28;
--chart-grid-dash: 2 5;
--chart-svg-filter: none;
}
html[data-chart-style=soft] {
--chart-accent: #e8a998;
--chart-secondary: #95c293;
--chart-tertiary: #8daef6;
--chart-quaternary: #b49de6;
--chart-quinary: #d99490;
--chart-senary: #d0b78e;
--chart-septenary: #99a790;
--chart-octonary: #728070;
--chart-surface: color-mix(in srgb, var(--panel2) 88%, #fff 12%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 92%, #fff 8%);
--chart-line-width: 2.8;
--chart-linecap: round;
--chart-linejoin: round;
--chart-grid-opacity: .42;
--chart-grid-dash: 1 0;
--chart-svg-filter: saturate(.88) brightness(1.04);
}
html[data-chart-style=technical] {
--chart-accent: #79c9ff;
--chart-secondary: #9adf90;
--chart-tertiary: #ffd56e;
--chart-quaternary: #c39cff;
--chart-quinary: #ff8c7b;
--chart-senary: #b8d0d7;
--chart-septenary: #7e9aac;
--chart-octonary: #6a7884;
--chart-surface: color-mix(in srgb, var(--panel2) 90%, #0b1a27 10%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 90%, #09151f 10%);
--chart-line-width: 1.35;
--chart-linecap: square;
--chart-linejoin: miter;
--chart-grid-opacity: .82;
--chart-grid-dash: 3 4;
--chart-svg-filter: saturate(.76) contrast(1.08);
}
html[data-chart-style=high_contrast] {
--chart-accent: #ffd54f;
--chart-secondary: #8dff80;
--chart-tertiary: #6fd9ff;
--chart-quaternary: #ff78d7;
--chart-quinary: #ff8b6f;
--chart-senary: #f2e48f;
--chart-septenary: #d7f0d9;
--chart-octonary: #cfcfcf;
--chart-surface: color-mix(in srgb, var(--panel2) 94%, #000 6%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 94%, #000 6%);
--chart-line-width: 3;
--chart-grid-opacity: .95;
--chart-grid-dash: 1 0;
--chart-svg-filter: saturate(1.6) contrast(1.22) brightness(1.08);
--chart-linecap: round;
--chart-linejoin: round;
}
html[data-chart-style=cool] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: #5fb9ff;
--chart-secondary: #45d3c3;
--chart-tertiary: #7d8fff;
--chart-quaternary: #70d7ff;
--chart-quinary: #54a0ff;
--chart-senary: #8bcbe0;
--chart-septenary: #6ea6bf;
--chart-octonary: #7d91a9;
--chart-surface: color-mix(in srgb, var(--panel2) 88%, #0d2740 12%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 90%, #0d2033 10%);
--chart-line-width: 2.2;
--chart-grid-opacity: .46;
--chart-grid-dash: 2 4;
--chart-svg-filter: saturate(1.08) brightness(1.03);
}
html[data-chart-style=monochrome] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: color-mix(in srgb, var(--text) 92%, #fff 8%);
--chart-secondary: color-mix(in srgb, var(--muted) 80%, var(--text) 20%);
--chart-tertiary: color-mix(in srgb, var(--separator) 72%, var(--text) 28%);
--chart-quaternary: color-mix(in srgb, var(--line) 45%, var(--text) 55%);
--chart-quinary: color-mix(in srgb, var(--muted) 55%, #fff 45%);
--chart-senary: color-mix(in srgb, var(--outline-line) 58%, var(--text) 42%);
--chart-septenary: color-mix(in srgb, var(--field-line) 70%, var(--text) 30%);
--chart-octonary: color-mix(in srgb, var(--scrollbar-thumb) 75%, var(--text) 25%);
--chart-surface: color-mix(in srgb, var(--panel2) 96%, var(--text) 4%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 96%, var(--text) 4%);
--chart-line-width: 2.1;
--chart-grid-opacity: .4;
--chart-grid-dash: 2 3;
--chart-svg-filter: saturate(0) contrast(1.05);
}
html[data-chart-style=neon] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: #2cf6ff;
--chart-secondary: #7eff85;
--chart-tertiary: #ff5cd0;
--chart-quaternary: #8b7cff;
--chart-quinary: #ffb454;
--chart-senary: #ffe66b;
--chart-septenary: #7edcff;
--chart-octonary: #d8b3ff;
--chart-surface: #0b1117;
--chart-surface-alt: #101720;
--chart-line-width: 2.45;
--chart-grid-opacity: .62;
--chart-grid-dash: 2 4;
--chart-svg-filter: saturate(1.25) brightness(1.12);
}
html[data-chart-style=warm] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: #ff9c5c;
--chart-secondary: #f4c46b;
--chart-tertiary: #ff7b72;
--chart-quaternary: #f4a261;
--chart-quinary: #e76f51;
--chart-senary: #d9a15e;
--chart-septenary: #c9895c;
--chart-octonary: #b37258;
--chart-surface: color-mix(in srgb, var(--panel2) 88%, #3d1f10 12%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 90%, #32180d 10%);
--chart-line-width: 2.4;
--chart-grid-opacity: .36;
--chart-grid-dash: 1 0;
--chart-svg-filter: saturate(1.1) brightness(1.04);
}
html[data-chart-style] {
--traffic-h1: var(--chart-accent);
--traffic-h2: var(--chart-secondary);
--traffic-h3: var(--chart-tertiary);
}
html[data-corner-style=square] {
--rp-radius-surface: 0px;
--rp-radius-control: 0px;
--rp-progress-radius: 0px;
}
html[data-corner-style=subtle] {
--rp-radius-surface: 5px;
--rp-radius-control: 4px;
--rp-progress-radius: 2px;
}
html[data-corner-style=rounded] {
--rp-radius-surface: 12px;
--rp-radius-control: 8px;
--rp-progress-radius: 4px;
}
html[data-corner-style=extra] {
--rp-radius-surface: 18px;
--rp-radius-control: 12px;
--rp-progress-radius: 999px;
}
File diff suppressed because it is too large. Load diff
+773
View File
@@ -0,0 +1,773 @@
/* Application startup, authentication, navigation, page chrome and draft dock.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
html.app-loading .shell {
visibility: hidden;
pointer-events: none;
}
.app-startup {
position: fixed;
inset: 0;
z-index: 10000;
display: grid;
place-items: center;
padding: 24px;
box-sizing: border-box;
background: var(--bg, #10151b);
color: var(--text, #edf2f7);
text-align: center;
}
.page-source[hidden],
#toast[popover]::backdrop,
.operation-message:empty,
#certificate-api-cancel-edit[hidden],
#certificate-api-edit-note[hidden],
#certificate-api-token-field[hidden],
html:not(.app-loading) .app-startup,
html.app-startup-failed .app-startup-message,
html.app-startup-failed .app-startup-spinner,
.login-section #auth-message:empty,
.mobile-menu-toggle,
.draft-dock-list[hidden],
#settings-page-appearance .dashboard-widget-setting .overview-widget-type,
.dashboard-appearance-grid .dashboard-widget-list.is-expanded~.dashboard-widget-scroll-hint,
.vh-group[hidden],
.stream-socket-editor input[hidden],
.vh-cache-options[hidden],
.acl-policy-custom-controls[hidden],
.listener-protocol-settings-actions button[hidden],
.logs-dialog .logs-card>.card-heading>div:first-child {
display: none;
}
.app-startup-card {
width: min(100%, 360px);
}
.app-startup-brand {
font: 700 29px var(--mono, monospace);
letter-spacing: -2px;
margin-bottom: 24px;
}
.app-startup-brand span {
color: var(--logo-secondary, var(--accent, #65cfa2));
}
.app-startup-spinner {
box-sizing: border-box;
width: 40px;
height: 40px;
margin: 0 auto 22px;
border: 3px solid var(--line, #374352);
border-top-color: var(--accent, #65cfa2);
border-radius: 50%;
animation: app-startup-spin .8s linear infinite;
}
.app-startup h2 {
margin: 0;
font-size: 19px;
font-weight: 600;
line-height: 1.4;
}
.app-startup-message,
.app-startup-recovery p {
font-size: 13px;
line-height: 1.6;
color: var(--muted, #b5c0cc);
}
.app-startup-recovery {
visibility: hidden;
animation: app-startup-reveal 0s linear 12s forwards;
}
.app-startup-recovery .outline:not([hidden]) {
margin-top: 10px;
display: inline-flex;
}
html.app-startup-failed .app-startup-recovery {
visibility: visible;
animation: none;
}
.shell {
display: grid;
min-height: calc(100vh / var(--ui-scale, 1));
grid-template-columns: 182px minmax(0, 1fr);
}
.sidebar {
position: sticky;
top: 0;
height: calc(100vh / var(--ui-scale, 1));
background: var(--sidebar-bg);
border-right: 1px solid var(--line);
display: flex;
flex-direction: column;
padding: 20px 13px;
}
.brand {
display: flex;
align-items: center;
font-weight: 780;
line-height: 1.1;
letter-spacing: -.6px;
font-size: 17px;
gap: 8px;
}
.brand-mark {
font: 700 33px var(--mono);
border: 1px solid var(--logo-accent, var(--accent));
color: var(--logo-accent, var(--accent));
place-content: center;
position: relative;
box-sizing: border-box;
padding-right: 2px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: -3px;
flex-shrink: 0;
width: 47px;
height: 47px;
font-size: 29px;
}
.brand-mark span {
font-size: 22px;
left: 29px;
top: 19px;
color: var(--logo-secondary, var(--brand-secondary));
position: static;
}
.edition {
font: 9px/1.6 var(--mono);
color: var(--muted);
letter-spacing: 1px;
margin: 17px 0 28px;
}
.edition span {
display: block;
color: var(--accent);
margin-top: 5px;
}
nav {
flex-direction: column;
display: flex;
gap: 5px;
}
.local-tag {
display: flex;
font: 9px var(--mono);
color: var(--green);
gap: 8px;
align-items: center;
}
.nav {
display: flex;
align-items: center;
gap: 13px;
text-align: left;
background: transparent;
color: var(--muted);
border-left: 2px solid transparent;
border-radius: var(--rp-radius-control, 0);
padding: 9px 10px;
}
.nav span {
font: 11px var(--mono);
opacity: .65;
}
.nav b {
font-weight: 530;
}
.nav.active {
background: var(--rp-nav-active-bg);
color: var(--rp-nav-active-color);
border-left-color: var(--rp-nav-active-color);
}
.nav:not(.active):hover {
background: var(--rp-nav-hover-bg);
}
.sidebar .main-navigation .nav:focus-visible {
outline: 2px solid var(--rp-nav-active-color);
outline-offset: 2px;
}
.sidebar-bottom {
margin-top: auto;
padding-top: 32px;
}
.sidebar-bottom p {
font-size: 11px;
line-height: 1.9;
color: var(--text-subtle);
margin: 13px 0 26px;
}
main {
min-width: 0;
width: 100%;
max-width: none;
padding: 0 16px;
margin: 0 auto;
}
.topbar {
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
height: 48px;
}
.breadcrumb {
font: 10px var(--mono);
color: var(--muted);
letter-spacing: 1px;
}
.breadcrumb span {
margin: 0 13px;
color: var(--separator);
}
.breadcrumb b {
font-weight: 400;
color: var(--text);
}
.top-actions {
display: flex;
align-items: center;
gap: 8px;
}
.system-card h2 small {
font: 10px var(--mono);
color: var(--muted);
}
.eyebrow {
font: 10px var(--mono);
letter-spacing: 2px;
color: var(--green);
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 16px;
}
.lead {
font-size: 13px;
line-height: 1.7;
color: var(--muted);
margin-top: 15px;
max-width: 690px;
}
.page-intro {
display: flex;
justify-content: space-between;
align-items: center;
padding: 13px 0 11px;
gap: 12px;
}
.login-card label {
display: block;
color: var(--muted);
font-size: 11px;
margin: 16px 0 7px;
}
.page-footer .footer-update {
color: var(--green);
text-decoration: underline;
text-underline-offset: 2px;
}
.login-section {
padding: 62px 0 40px;
max-width: 720px;
padding-top: 40px;
}
.login-section h1 {
font-size: calc(clamp(42px, 5vw, 68px));
letter-spacing: -3px;
max-width: 640px;
}
.login-section .lead {
font-size: 15px;
max-width: 570px;
}
.login-card {
background: var(--panel);
border: 1px solid var(--line);
padding: 27px;
max-width: 510px;
margin-top: 23px;
}
.login-card>p {
font-size: 12px;
color: var(--muted);
line-height: 1.7;
}
.login-card .primary {
width: 100%;
margin-top: 18px;
}
.login-card .fine {
font-size: 10px;
margin-top: 16px;
}
.login-footer {
font: 9px var(--mono);
letter-spacing: 1.7px;
color: var(--muted);
margin-top: 30px;
line-height: 2;
}
.login-footer span {
margin: 0 11px;
color: var(--accent);
}
.page-intro .eyebrow {
font-size: 9px;
margin-bottom: 4px;
}
[dir=rtl] .nav {
text-align: right;
border-left: 0;
border-right: 2px solid transparent;
}
[dir=rtl] .nav.active {
border-right-color: var(--rp-nav-active-color);
}
.app-confirm-message {
overflow-wrap: anywhere;
margin-top: 14px;
color: var(--text);
white-space: pre-line;
}
.sidebar nav {
margin-top: 22px;
}
.page-intro h1 {
font-size: 25px;
letter-spacing: -.7px;
}
.page-intro .lead {
font-size: 11px;
margin-top: 4px;
}
.app-confirm-dialog {
width: calc(min(520px, 100vw - 28px));
max-width: none;
padding: 0;
border: 1px solid var(--line);
border-radius: var(--rp-radius-surface, 4px);
background: var(--panel);
color: var(--text);
}
.app-confirm-dialog::backdrop {
background: var(--backdrop);
backdrop-filter: blur(2px);
}
.app-confirm-card {
margin: 0;
border: 0;
padding: 18px;
}
.app-confirm-actions {
justify-content: flex-end;
margin-top: 18px;
}
.app-confirm-danger {
border-color: var(--red);
color: var(--red);
}
.app-confirm-danger:hover {
border-color: var(--red);
background: rgba(255, 170, 162, .08);
}
.auth-page.auth-loading .auth-main,
.auth-page.auth-loading .auth-top {
visibility: hidden;
}
.auth-page {
min-height: 100vh;
}
.auth-top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 24px;
border-bottom: 1px solid var(--line);
}
.auth-main {
max-width: 560px;
margin: auto;
padding: 28px 20px;
}
.auth-main .login-section {
padding: 0;
max-width: none;
}
.auth-main h1 {
font-size: 1.65rem;
margin-bottom: 18px;
}
.login-section #auth-message {
margin-top: 14px;
padding: 11px 12px;
border: 1px solid var(--line);
background: var(--panel2);
color: var(--text);
font-size: 12px;
line-height: 1.5;
overflow-wrap: anywhere;
}
.auth-advanced>summary,
.vh-backend-advanced>summary {
cursor: pointer;
color: var(--muted);
font-size: 11px;
font-weight: 600;
padding: 4px 0 8px;
}
.auth-subcard {
border: 1px solid var(--line);
border-radius: var(--rp-radius-surface, 6px);
padding: 10px;
display: grid;
gap: 8px;
}
.auth-subcard>h3 {
margin: 0;
font-size: 12px;
}
.auth-advanced {
border-top: 1px solid var(--line);
padding-top: 4px;
}
body[data-density=compact] .shell {
grid-template-columns: 164px minmax(0, 1fr);
}
body[data-density=compact] .sidebar {
padding: 14px 9px;
}
body[data-density=compact] .nav,
body[data-density=compact] .stat {
padding: 6px 7px;
}
body[data-density=compact] .topbar {
height: 42px;
}
body[data-density=compact] .page-intro {
padding: 7px 0 6px;
gap: 7px;
}
body[data-density=compact] .page-intro h1 {
font-size: 20px;
}
body[data-density=compact] .page-intro .lead {
font-size: 10px;
margin-top: 2px;
}
.top-log-menu>summary,
.backend-column-picker>summary,
.file-log-search-help>summary {
list-style: none;
cursor: pointer;
user-select: none;
}
.top-log-menu-items {
position: absolute;
z-index: 120;
top: calc(100% + 6px);
inset-inline-end: 0;
display: grid;
gap: 4px;
min-width: 170px;
padding: 6px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: 0 12px 28px var(--shadow-strong);
}
.top-log-menu-items .quiet {
width: 100%;
text-align: start;
white-space: nowrap;
}
.draft-dock {
position: fixed;
right: 14px;
bottom: 14px;
z-index: 900;
width: calc(min(330px, 100vw - 28px));
padding: 0;
border: 1px solid var(--border-hover);
border-radius: var(--rp-radius-surface, 0);
background: var(--panel);
box-shadow: 0 12px 32px var(--shadow-strong);
}
.draft-dock-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 34px;
padding: 7px 9px;
border-bottom: 1px solid var(--line);
background: var(--panel2);
}
.draft-dock.is-collapsed .draft-dock-heading,
.draft-dock-item:last-child {
border-bottom: 0;
}
.draft-dock-heading>strong {
font: 700 10px var(--mono);
letter-spacing: .12em;
text-transform: uppercase;
}
.draft-dock-heading .pill {
min-width: 24px;
padding: 2px 6px;
text-align: center;
border-radius: var(--rp-radius-control, 0);
}
.draft-dock-heading-actions,
.traffic-protocol-legend span,
.stat-chart .stat-chart-legend span,
.function-live-chart-legend span,
.connection-legend span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.draft-dock-toggle {
display: inline-grid;
place-items: center;
width: 26px;
min-width: 26px;
height: 24px;
padding: 0;
border: 1px solid var(--line);
border-radius: var(--rp-radius-control, 0);
background: var(--panel);
color: var(--text);
font: 700 14px var(--mono);
}
.draft-dock-toggle:hover,
.chart-style-choice:hover {
border-color: var(--border-hover);
background: var(--nav-hover-bg);
}
.draft-dock-list {
display: grid;
gap: 0;
max-height: calc(min(42vh, 360px));
overflow: auto;
}
.draft-dock-item {
display: grid;
grid-template-columns: minmax(0, 1fr) 32px;
gap: 0;
align-items: stretch;
border-bottom: 1px solid var(--line);
}
.draft-dock-open {
min-height: 42px;
border: 0;
border-radius: 0;
background: transparent;
min-width: 0;
padding: 6px 9px;
text-align: left;
border-right: 1px solid var(--line);
}
.draft-dock-remove {
min-height: 42px;
border: 0;
border-radius: 0;
background: transparent;
padding: 0;
font-size: 18px;
color: var(--muted);
}
.draft-dock-open:hover {
background: var(--nav-hover-bg);
color: var(--text);
box-shadow: inset 3px 0 0 var(--accent);
}
.draft-dock-remove:hover {
background: var(--nav-hover-bg);
color: var(--text);
}
.draft-dock-identity {
display: grid;
min-width: 0;
gap: 1px;
}
.draft-dock-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
.draft-dock-identity small {
color: var(--muted);
font: 9px var(--mono);
text-transform: uppercase;
letter-spacing: .08em;
}
.draft-dock-identity time {
flex: 0 0 auto;
color: var(--text-subtle);
font: 9px var(--mono);
}
.draft-dock-identity b {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 650;
}
.page-intro-copy,
.acl-picker,
.vh-acl-profile-picker,
.docker-health-grid input:not([type=checkbox]),
.docker-health-grid select,
.docker-route-grid input:not([type=checkbox]),
.docker-route-grid select {
min-width: 0;
width: 100%;
}
.page-intro-title-row {
min-width: 0;
width: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
.page-intro-title-row h1 {
min-width: 0;
flex: 1 1 auto;
margin-right: auto;
}
.page-intro .page-intro-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px 16px;
min-width: 0;
margin-bottom: 4px;
}
.page-intro .page-intro-meta-row .eyebrow {
margin-bottom: 0;
flex: 0 0 auto;
}
@keyframes app-startup-spin {
to {
transform: rotate(1turn);
}
}
@keyframes app-startup-reveal {
to {
visibility: visible;
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+99
View File
@@ -0,0 +1,99 @@
# Administration stylesheet
This directory is the source of the shared dashboard and login stylesheet.
`manifest.json` defines its complete cascade order. A normal Cargo build runs
`scripts/build/panel_css.py`, compacts these sources conservatively and embeds
one `panel-style.css` from Cargo's `OUT_DIR` into the binary.
The browser continues to request `/assets/style_<build-id>.css` once. The
existing fingerprint, login access rules and cached gzip/Brotli/Zstandard
responses remain in use. Source modules are not separate HTTP assets. The
documentation Reader/API page keeps its independent `../docs.css` stylesheet.
## Module ownership and order
| Order | File | Responsibility |
|---|---|---|
| 1 | `00-tokens.css` | Theme palettes, appearance tokens and custom properties shared with JavaScript preferences |
| 2 | `10-base.css` | Reset, typography, buttons, form fields, tables, shared cards, dialogs and overlays |
| 3 | `20-shell.css` | Startup, login, navigation, page structure and editor draft dock |
| 4 | `30-overview.css` | Shared charts/legends and Overview metrics, density, widget layout controls and dashboard presets |
| 5 | `40-editors.css` | Vhosts and backends, Web Server, Streams, redirects, 404 hosts and ACL controls |
| 6 | `50-settings.css` | Settings, certificates, integrations and Docker service/agent management |
| 7 | `60-monitoring.css` | Traffic, cache, transformations, live/stored logs and configuration history |
| 8 | `70-responsive.css` | All viewport/container queries and reduced-motion adjustments, in their required cascade order |
## Editing rules
- Update the rule in its owning module. Keep one occurrence of a selector in
each conditional scope; group selectors when they deliberately share one
declaration block and the move preserves cascade behavior.
- Reuse the existing theme and appearance variables. JavaScript also writes
live values such as chart geometry, widget layout and preferences; retain
those variable and class names when reorganizing CSS.
- Place a responsive adjustment inside the existing matching condition in
`70-responsive.css`. A container name needs whitespace before its condition,
for example `@container overview-canvas (max-width: 700px)`.
- Preserve the manifest order and the order of overlapping conditions. The
query order is part of the cascade; do not sort breakpoints mechanically.
In particular, the `760px` condition follows `620px` to preserve the later
Transform and file-log adjustments. Container queries follow viewport rules.
- Explicit dashboard spans set `--overview-widget-placement` on the widget;
one common rule applies it to `grid-column`. Container queries change that
variable for supported span values. Keep the generic no-span defaults
separate so the legacy row-start and custom-layout contracts remain intact.
- Add a new module only when it has a distinct owner and register it once in
the manifest. Missing, repeated and unlisted CSS files fail the build.
`@import`, `@charset`, path traversal and symlinked modules are rejected.
- Group identical declaration bodies within each compatible selector family.
Moving them across intervening rules requires checking specificity and source
order, including combinations of state classes. Keep vendor pseudo-elements
in separate compatible lists; a WebKit/Gecko selector mix can invalidate the
whole rule in another engine.
The builder removes formatting whitespace beside braces/semicolons and normal
comments. It preserves selector whitespace, escaped tokens, strings, functions
and declaration order. It does not sort, merge or otherwise optimize rules at
build time. No Node.js/npm or runtime stylesheet compiler is required.
## Verification
From the repository root:
```bash
python3 scripts/build/panel_css.py --check
python3 tools/dev/test_panel_css.py
python3 scripts/preflight.py
python3 scripts/audit_css_usage.py --strict
bash dev.sh check
bash dev.sh test
```
The first three commands require only Python 3.10+. The optional CSS audit
requires `tinycss2` and `cssselect2`; install them in a development environment,
not on the deployed server. `--structure-json PATH` records repeated selectors,
conditional blocks, properties and identical declaration blocks. `--json PATH`
records selector references. The audit follows JavaScript embedded by Rust and
flags scripts outside those entry points for review. Missing literal class
names still need manual review because JavaScript can construct them.
Strict mode also rejects identical declaration bodies within the same compatible
selector family. Its only body-repetition exception is a single rule per vendor
family when merging would create an incompatible selector list.
Use browser comparisons for layout changes: cover login and every workspace,
open dialogs and expanded rows, themes, corner styles, dashboard density/width,
hidden/disabled states, viewport breakpoints, container widths and reduced
motion. A source audit alone does not establish visual or runtime equivalence.
Rebuild the Rust binary after every stylesheet change.
When replacing a previously built checkout with a source ZIP, include the root
`build.rs`. If Cargo reports that `OUT_DIR/panel-style.css` is missing, follow
the [generated-CSS recovery steps](../../../../scripts/README.md#missing-generated-css-during-rust-compilation).
Source preflight does not generate Cargo outputs. The build tracks the generated
bundle as well as its inputs, and rejects a missing or empty output.
The optional [dashboard browser regression runner](../../../../tools/dev/README-dashboard-css.md)
assembles the actual HTML and JavaScript against synthetic API fixtures and
compares a supplied baseline stylesheet with the built candidate. It includes
breakpoint/container boundaries and dynamic editor states. Its dependencies are
development-only; they are not part of the application build or deployment.
+12
View File
@@ -0,0 +1,12 @@
{
"files": [
"00-tokens.css",
"10-base.css",
"20-shell.css",
"30-overview.css",
"40-editors.css",
"50-settings.css",
"60-monitoring.css",
"70-responsive.css"
]
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -423,7 +423,7 @@ mod tests {
#[test] #[test]
fn css_minifier_preserves_dashboard_corner_selector() { fn css_minifier_preserves_dashboard_corner_selector() {
let input = include_str!("../admin/web/style.css"); let input = include_str!(concat!(env!("OUT_DIR"), "/panel-style.css"));
let result = css::minify(input.as_bytes()).expect("minify panel stylesheet"); let result = css::minify(input.as_bytes()).expect("minify panel stylesheet");
let output = std::str::from_utf8(&result).expect("UTF-8 CSS"); let output = std::str::from_utf8(&result).expect("UTF-8 CSS");
assert!(output.contains("html:is([data-corner-style=square],[data-corner-style=subtle],[data-corner-style=rounded],[data-corner-style=extra]) :is("), "lost descendant pseudo-class combinator"); assert!(output.contains("html:is([data-corner-style=square],[data-corner-style=subtle],[data-corner-style=rounded],[data-corner-style=extra]) :is("), "lost descendant pseudo-class combinator");
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env python3
"""Read-only anonymous admin access probes against a running RustProxy.
No cookie, password or token is sent; response bodies are never printed.
Only HTTP loopback URLs are accepted. Exit 1 on an unexpected disclosure/status.
This is a focused regression probe, NOT a penetration test or full security audit.
"""
from __future__ import annotations
import argparse
import ipaddress
import json
from pathlib import Path
from urllib.parse import urlsplit
from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler
from urllib.error import HTTPError, URLError
PRIVATE = ('/api/status','/api/config','/api/backends','/api/traffic','/api/cache','/api/transform','/api/logs','/api/file-logs','/api/file-logs/query','/api/audit',
'/api/events','/api/certificates','/api/acme','/api/auth/session','/metrics','/api/metrics',
'/docs/openapi.json','/docs/init.js','/docs/style.css','/docs/swagger-ui-bundle.js',
'/docs/swagger-ui.css','/docs/licenses.txt','/assets/app.js','/assets/langs.json','/assets/vhosts.js',
'/assets/management.js','/assets/integrations.js','/assets/settings.js','/assets/file-logs.js','/assets/acme.js',
'/assets/traffic-panel.js','/assets/cache-panel.js','/assets/transform-panel.js','/api/does-not-exist')
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):
return None
def base_url(value: str) -> str:
url=urlsplit(value)
if url.scheme!='http' or url.username or url.password or url.path not in ('','/') or url.query or url.fragment:
raise argparse.ArgumentTypeError('Use the HTTP loopback administrator origin, e.g. http://127.0.0.1:9090')
try:
loopback=url.hostname=='localhost' or ipaddress.ip_address(url.hostname).is_loopback
if not loopback or not url.port:raise ValueError()
except (ValueError,TypeError):raise argparse.ArgumentTypeError('Explicit loopback address and port required')
return value.rstrip('/')
def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--url',type=base_url,default='http://127.0.0.1:9090');args=parser.parse_args()
opener=build_opener(ProxyHandler({}),NoRedirect());failed=0;count=0
def check(path,expected,validate):
nonlocal failed,count
count+=1
try:
request=Request(args.url+path,headers={'Accept':'application/json'})
try:response=opener.open(request,timeout=5)
except HTTPError as error:response=error
with response:
status=response.code;headers=response.headers;body=response.read(8388609 if path=='/assets/login-langs.json' else 131073)
ok=status==expected and headers.get('Cache-Control')=='no-store' and validate(body,headers)
except (OSError,ValueError,KeyError,TypeError,URLError):status='unavailable';ok=False
if not ok:failed+=1
print(f'{"PASS" if ok else "FAIL"} {path}: HTTP {status}; expected {expected}')
def denied(body,headers):return json.loads(body)=={'error':'auth.required'}
for path in PRIVATE:check(path,401,denied)
check('/docs',303,lambda b,h:h.get('Location')=='/?next=docs')
check('/docs/',303,lambda b,h:h.get('Location')=='/?next=docs')
check('/healthz',200,lambda b,h:json.loads(b)=={'status':'ok'})
check('/api/auth/status',200,lambda b,h:isinstance((v:=json.loads(b)).get('setup_required'),bool) and set(v)=={'setup_required'})
check('/',200,lambda b,h:b'/assets/login.js' in b and all(x not in b for x in [b'/assets/app.js',b'config-editor',b'loginLead']))
check('/assets/login-langs.json',200,lambda b,h:all(set(p['translations'])<=set(json.loads((Path(__file__).resolve().parents[2]/'src/admin/web/login-keys.json').read_text())) for p in json.loads(b)['languages']))
print(f'{count-failed}/{count} anonymous probes passed. No authenticated/mutation/TLS checks performed.')
raise SystemExit(bool(failed))
if __name__=='__main__':main()
-260
View File
@@ -1,260 +0,0 @@
#!/usr/bin/env python3
import argparse
import gzip
import hashlib
import io
import time
import zlib
import brotli
import requests
import zstandard
TESTS = {
"identity": "identity",
"gzip": "gzip",
"deflate": "deflate",
"brotli": "br",
"zstd": "zstd",
"all": "zstd, br, gzip, deflate",
}
def decompress(data, content_encoding):
encodings = [
e.strip().lower()
for e in content_encoding.split(",")
if e.strip() and e.strip().lower() != "identity"
]
# Content-Encoding usuwa się w odwrotnej kolejności.
for encoding in reversed(encodings):
if encoding == "gzip":
data = gzip.decompress(data)
elif encoding == "deflate":
try:
data = zlib.decompress(data)
except zlib.error:
# Niektóre serwery wysyłają raw DEFLATE.
data = zlib.decompress(data, -zlib.MAX_WBITS)
elif encoding == "br":
data = brotli.decompress(data)
elif encoding == "zstd":
dctx = zstandard.ZstdDecompressor()
with dctx.stream_reader(io.BytesIO(data)) as reader:
data = reader.read()
else:
raise RuntimeError(f"Nieznany Content-Encoding: {encoding}")
return data
def human_size(size):
if size < 1024:
return f"{size} B"
if size < 1024 * 1024:
return f"{size / 1024:.1f} KB"
return f"{size / 1024 / 1024:.2f} MB"
def request_test(url, accept_encoding, verify_ssl=True, timeout=15, revalidate=False):
headers = {
"Accept-Encoding": accept_encoding,
"User-Agent": "compression-tester/1.0",
}
if revalidate:
headers["Cache-Control"] = "no-cache"
start = time.perf_counter()
r = requests.get(
url,
headers=headers,
timeout=timeout,
verify=verify_ssl,
stream=True,
allow_redirects=True,
)
r.raw.decode_content = False
raw = r.raw.read()
elapsed = time.perf_counter() - start
content_encoding = r.headers.get("Content-Encoding", "identity")
try:
decoded = decompress(raw, content_encoding)
error = None
except Exception as exc:
decoded = b""
error = str(exc)
return {
"status": r.status_code,
"requested": accept_encoding,
"received": content_encoding,
"raw_size": len(raw),
"decoded_size": len(decoded),
"time": elapsed,
"decoded": decoded,
"error": error,
"headers": r.headers,
"final_url": r.url,
}
def main():
parser = argparse.ArgumentParser(
description="Tester HTTP Content-Encoding: zstd/br/gzip/deflate"
)
parser.add_argument("url", help="URL do przetestowania")
parser.add_argument(
"--insecure",
action="store_true",
help="Wyłącz weryfikację certyfikatu SSL",
)
parser.add_argument(
"--timeout",
type=int,
default=15,
help="Timeout HTTP w sekundach",
)
parser.add_argument(
"--revalidate",
action="store_true",
help="Wyslij Cache-Control: no-cache (wymusza rewalidacje i moze resetowac Age do 0)",
)
args = parser.parse_args()
if args.insecure:
requests.packages.urllib3.disable_warnings()
print()
print(f"URL: {args.url}")
print("=" * 100)
results = {}
for name, accept_encoding in TESTS.items():
try:
result = request_test(
args.url,
accept_encoding,
verify_ssl=not args.insecure,
timeout=args.timeout,
revalidate=args.revalidate,
)
results[name] = result
except Exception as exc:
results[name] = {
"error": f"REQUEST ERROR: {exc}"
}
identity_body = None
if "identity" in results:
identity_body = results["identity"].get("decoded")
print(
f"{'TEST':<10}"
f"{'HTTP':<7}"
f"{'REQUEST':<27}"
f"{'RESPONSE':<12}"
f"{'RAW':<12}"
f"{'DECODED':<12}"
f"{'RATIO':<10}"
f"{'TIME':<10}"
f"{'BODY'}"
)
print("-" * 120)
failed = False
for name, result in results.items():
if result.get("error"):
print(f"{name:<10} ERROR: {result['error']}")
failed = True
continue
raw_size = result["raw_size"]
decoded_size = result["decoded_size"]
if decoded_size:
ratio = raw_size / decoded_size
ratio_text = f"{ratio * 100:.1f}%"
else:
ratio_text = "-"
if identity_body is not None and result["decoded"]:
if result["decoded"] == identity_body:
body_status = "OK"
else:
body_status = "DIFF"
else:
body_status = "-"
print(
f"{name:<10}"
f"{result['status']:<7}"
f"{result['requested']:<27}"
f"{result['received']:<12}"
f"{human_size(raw_size):<12}"
f"{human_size(decoded_size):<12}"
f"{ratio_text:<10}"
f"{result['time']:.3f}s "
f"{body_status}"
)
print()
print("=" * 100)
print("Szczegóły nagłówków:")
print()
for name, result in results.items():
if result.get("error"):
continue
headers = result["headers"]
print(f"[{name}]")
print(f" URL finalny: {result['final_url']}")
print(f" HTTP status: {result['status']}")
print(f" Content-Encoding: {headers.get('Content-Encoding', '-')}")
print(f" Content-Type: {headers.get('Content-Type', '-')}")
print(f" Content-Length: {headers.get('Content-Length', '-')}")
print(f" Vary: {headers.get('Vary', '-')}")
print(f" Cache-Control: {headers.get('Cache-Control', '-')}")
print(f" Age: {headers.get('Age', '-')}")
print(f" X-Cache: {headers.get('X-Cache', '-')}")
print(f" X-Cache-Hits: {headers.get('X-Cache-Hits', '-')}")
print(f" Cache-Status: {headers.get('Cache-Status', '-')}")
print(f" ETag: {headers.get('ETag', '-')}")
print(f" Server: {headers.get('Server', '-')}")
print()
print("SHA256 po dekompresji:")
print()
for name, result in results.items():
body = result.get("decoded")
if body:
sha = hashlib.sha256(body).hexdigest()
print(f"{name:<10} {sha}")
if failed:
raise SystemExit(1)
if __name__ == "__main__":
main()