v0.34.9_css_refactor
This commit is contained in:
1 parent
2b7c67d755
commit
66ff9dfaa2
28 files changed
+11518
-13023
No files matched your search
@@ -4,6 +4,9 @@ Release history focused on **features, settings, default behavior, compatibility
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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
@@ -1,15 +1,15 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
18ea9db93171d23888334488151554d52d9a3af701581b89ccbe4150a5b81e7f .gitignore
|
||||
237ed6e2c0d4fa1f125180c349856749c13820b72593b5fc58e1b58cce47c840 AGENTS.md
|
||||
4f695dd4ded1ef0c6fbe421ae9bc01993140ddbb4f77cb7a46dd8191a2584ddb CHANGELOG.md
|
||||
608cc471556fa6e29fefae1d5ea18194d6ef94b5d32410965b0140a4fcf7018c CHANGELOG.md
|
||||
310d60da6c7f501ccdde86b93ee1d5b0102033ba71ac9bb42a4e3ce184692ef9 Cargo.lock
|
||||
c18c0ba1a22a8534a4cf3b6db907f41f04e0b6cb47cd305a21af139a06e303fb Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
fb75f6ff1fee8e5af4c89943c67b7e75cbac048254cecc3dc9fd0ea33d53ac25 README.md
|
||||
a288a9c873bfe2e866a41b116af8cf3dac7661db68164bbe6b57706e2fca7bc6 README.md
|
||||
ae6d14ada6fde7d0b8514840b821678418cf5b1816c18434e3b7fb2ae27a0b21 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
7309cfaa188c7a1fcf30be1b8313ee99c00b7fe5d6d626ddefcc7345fa066112 build.rs
|
||||
ac07b3bdd46c787989e93f55ebd6ea3d7f5b150ef36f2422e2b58df3f6b7543a build.rs
|
||||
2c6a0a5cb088399e2cc912073069eb334ec6615eee31734d2f0c161f1415fb6a deploy/README.md
|
||||
13d514de5042a230cacf8c10abe1ef183c94ac799a89b8e2c111152ac7c69b48 deploy/rustproxy.service
|
||||
c111f5b20484707b80efb7dd9176acbaa27b63ac78db497261ac234821e1fade dev.sh
|
||||
@@ -36,7 +36,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
|
||||
f24767c2f9d4db4db5005b700d03323dd20f0a1d180ae939be4fe54a126aaf32 docs/CONFIGURATION.md
|
||||
80bcc666736b10f6202386d0342e8d6feeba858d77aea310bf3ec6de01c2bfb3 docs/CONTAINER.md
|
||||
dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md
|
||||
9fb2d0bcac70d1883dd16557622851a1f0527b3d3569a4b5177f5e6dedd3c74d docs/DASHBOARD.md
|
||||
302a9db0a5a4e5f6c49684ef0ea9ad9f1796fdbcb5e28bb5a98c75fffd4374f4 docs/DASHBOARD.md
|
||||
3e2f7670d136911c1b7bfe8571c60590932f74a60bcac308add4ccb0446bf970 docs/DOCKER_DISCOVERY.md
|
||||
f1baabe34e8a1b204d7c78e2f558c019c156b03adfa6ddecb931b4b8a5501f78 docs/INSTALL.md
|
||||
37001cbf8ed248ca05ec40f12dd9876d33f6e0c97dcb21eb2e6f53c3d97a60bb docs/INTEGRATIONS.md
|
||||
@@ -44,7 +44,7 @@ f1baabe34e8a1b204d7c78e2f558c019c156b03adfa6ddecb931b4b8a5501f78 docs/INSTALL.m
|
||||
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
|
||||
140b95ad778261455f269f2144a1c61c2315bd3b85300fb613822eacddf3cfc2 docs/MONITORING.md
|
||||
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
||||
8b22a8f738f62cb27faf97683cc467de0fba02ab5999217b502f24a38f10ab4b docs/README.md
|
||||
bcd8c4eff9d57342d75441f5941a5828275d6b320317668e727467bc0d876f66 docs/README.md
|
||||
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
|
||||
e2b36152c13d65621a367c69dd400118dae1e4426a4d2b2139d2f63fa30442d6 docs/SECURITY.md
|
||||
bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md
|
||||
@@ -97,8 +97,11 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
|
||||
1465396cf1358067e9fe48b3d61f28fd4ac65c577b67c502950ce6b42ecbe3b3 nagios/check_rustproxy_latency
|
||||
6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues
|
||||
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
|
||||
bd313cacfc1b450a90368b30ae39ab48b877c8bed3c2db8bbd4e4a1f40865c9e scripts/README.md
|
||||
f550ffab868c5241ff5d43d9ae569e19f6842b324b80ecefdc7034642125ed9b scripts/audit_css_usage.py
|
||||
fd1c34599f725ad5f7c3c0cc89e08b11917baef02c83af054ac8528b2e01dd8e scripts/README.md
|
||||
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
|
||||
e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py
|
||||
618603239b000bbe4af36a6a6ee268758af632c761fc53fc6b7f44a4a818bdf1 scripts/deploy/deploy_core.py
|
||||
@@ -107,7 +110,7 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
||||
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
||||
3077405aae2dd0f32c0e168ef710745bd44e42283a4c566ab411e68d3bb745af scripts/preflight.py
|
||||
8f9f2dc7e6d6688b915a7dfbfc05643a80e5814819926cf43458a54965cfc83c scripts/preflight.py
|
||||
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
|
||||
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
|
||||
@@ -137,7 +140,7 @@ a402209141667fdce69698a60dc0b867248effa191494d0a0d93d468d7df9476 src/admin/cert
|
||||
159c3970d25fa29acf2483fc9422b003e5d63884713f62baa8375e4a4020730e src/admin/dashboard_presets.rs
|
||||
aae1ca0e70d987830bd63948d431f54302083148177c1b551565be12df7fe771 src/admin/documentation.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
|
||||
ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs
|
||||
d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file_logs_io.rs
|
||||
@@ -146,7 +149,7 @@ f66ff718f82a326f80808b0a5088bd05a48625be802e5ba305fff3c344b59948 src/admin/logg
|
||||
1ba0520c6348246cbb35b60e596e99dc600137c0696de86508cf06587bd86563 src/admin/mod.rs
|
||||
8e1012a8ec776c3525310b03a70f274955dce9de3528109070ea11de0ff5c16c src/admin/openapi.json
|
||||
7cfd6593ac9bcea3d1b9e61bbcb7ed98d65ed3190f54f94c1e104541f4fbb4f5 src/admin/operations.rs
|
||||
f14afb6ae141053d83d0dc59df835b20e38a37a9e83f86f0f2053f2bd5d9ce57 src/admin/panel_assets.rs
|
||||
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
|
||||
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
|
||||
730c559afaa8a50affc3eb6dc6fbde7005c0356237352fcf8c5423b65725a8fe src/admin/preferences.rs
|
||||
35a7f99c7448c90b8637f4b3a7becca138f1f378fe5dc3f87122151525bb20f9 src/admin/preferences_columns.rs
|
||||
@@ -160,7 +163,7 @@ e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/sess
|
||||
2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs
|
||||
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
666ef444c81e59a2fd47ace3be26e51a8630194d8ffd4db131eb228947fee8ca src/admin/tests.rs
|
||||
dacdf1b948991698f16640a375e3af90c3a18bb2247b0ad51aa97f763ee3f7bd src/admin/tests.rs
|
||||
c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js
|
||||
@@ -169,7 +172,6 @@ a085ee336e6669229f3ad2f74536f19bd088412ca79bcaff30dff293f1e1baf9 src/admin/web/
|
||||
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
|
||||
c8e1d31a7bf77d9e4f4be979bf6a7474369aae3e1e46e6b30d1a76677fa3d762 src/admin/web/app/core.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
|
||||
bfb3fbac62f9ff762a298d9c06808ae81ae55de7a41e0bb35ce5c167f333ffe2 src/admin/web/app/overview-extra-metrics.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
|
||||
849b2ca9d183bdf6cb1e04651cda52da55b8a631e11ab1d827a052c8436bbd76 src/admin/web/cache-panel.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
|
||||
14213011cfb6ac334c8711b76bbdde97588a7b7ba4eb2592a1e387f877d6cbe0 src/admin/web/dashboard-widgets.json
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
@@ -208,7 +220,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.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
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||
@@ -221,7 +233,6 @@ a8afc6e70950bc70b98725eb049d73cff35dc3a3ee659229a15a8e08d3eb4f12 src/admin/web/
|
||||
3ee4508cdda0b0b0e0f880ecac4b085368098b46240d24f2e6efed21c96bd5e9 src/admin/web/settings.js
|
||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||
6feb1996b1caa9d19e0dc56eae7ca5b48b7e7cdf0a93bb2e4b527fb8f13a2fe5 src/admin/web/streams.js
|
||||
38716fae968aeb8f6a8510f28bb9054a73a438a969c3b69f9c9d921acfed6e06 src/admin/web/style.css
|
||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||
dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js
|
||||
a55f45dad5b540def419f8487161fb506b7e52744c76a5e661c7a5fa0a98a426 src/admin/web/vhosts/core.js
|
||||
@@ -330,7 +341,7 @@ c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/upda
|
||||
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
|
||||
61a2b01db9010f9debfb38b43a26bf588438a5e470dff938e4e54f9d7ba46732 src/headers/mod.rs
|
||||
e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs
|
||||
b458510eb89ae8bf8fda84989073c6e64c69f2b12fde96c454b33f9df6a57df2 src/http3.rs
|
||||
1b291ef7b06647ce820111cc802874394e3600b4b2c86ee9a1fbf7eee6cb864a src/http3.rs
|
||||
0cc484bf0cc59a55a13e0e70435833a9a7a22ad7dfb5b9f86e059cf70a80cc2d src/http3/body.rs
|
||||
741972222414c280104acbcd8f8071bed9cbfcfa2f503c487c11c56f553f53cd src/http3/diagnostics.rs
|
||||
49ce4452ef7973cafa2e4071e2e14be0e16000e931a6501e4dc613652e49e230 src/http3/identity.rs
|
||||
@@ -451,8 +462,8 @@ df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 src/tls.rs
|
||||
2ff3c219f87bd82677045a4809d3d94921c5b230bbc2f3e7131b07548f73ea7e src/tls/external_paths.rs
|
||||
f88a6c8c2e35c3aa58b7763c71a5a2cec82a34f24478a98b0960b81659a1ec4c src/tls/external_paths_tests.rs
|
||||
7b4c0f58b3a1e1791bb3c682a001b78bfdbbdafdbc143db8063b3f22dc168616 src/tls/server_config.rs
|
||||
68a29d90f1675c7d97953e50ebccd4fa21c47fd0a4d8a0838b436ee84399afe0 src/tls/tests.rs
|
||||
bf0ccec8909e678aaed84d358058b1fa865b38e09d10471b02fca76eea38633d src/tls/types.rs
|
||||
7c0c06c3009c726043168ae47254a84642dbabbbce1b2e718544414312ca3e9b src/tls/tests.rs
|
||||
d625e2e3f669484432ffc0bf7c1aba94aa5bb709dc4d080daa3bfa8004dee9f2 src/tls/types.rs
|
||||
a535c8526b38de2990da7781e158b9ea70ef8c1f6fae0aac5ce6e9da21d3a309 src/traffic/mod.rs
|
||||
dc8a83fae47da0d86e6ce0a329cb5274233264df56eebdb56b9bf37cb7be2bb4 src/traffic/tests.rs
|
||||
65fcac840f8b5f1508213e3f374aa318b450b0f9f90ba827c5ea935b9c60bee7 src/transform/apply.rs
|
||||
@@ -468,7 +479,7 @@ d13fa462d6df516a4cfaee9f4e184044c5b6345e311aeb3d9058efafe54e4c5e src/transform/
|
||||
954b86300c3ea47281acb2f43b0771e838f83ac09eb55e882e42de26aee3128f src/transform/model.rs
|
||||
4b52257e1a3643910b5e7043852d7218230c0021c31215d90d0f8e384f2ae233 src/transform/svg/mod.rs
|
||||
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
|
||||
d1f3f53fedae0d8ef69eeee457f7d30ed5a670a1ed4ced60683400ee46680260 src/transform/tests.rs
|
||||
3c99de898e4c516a805fec1bc77007a211550b7d7a563933a81ae635d235a2e6 src/transform/tests.rs
|
||||
8d38f2f7d4ca4dd453cd0e2337614ec704fbd0a36d3e2374cda43f05dfcad663 src/update_check/mod.rs
|
||||
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
|
||||
b7ee3629a8fde0972bc1822b764f5fa2c813a760dd5dfb7f1ce643a7db8f9f75 src/upstream.rs
|
||||
@@ -511,11 +522,18 @@ b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server
|
||||
df888adf893d0b6b99348d19fa07bd9c2b8fc0ce7f411d06be7411afb4311fe0 src/web_server/tests.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md
|
||||
577302f167c9d76655f12852ce57a272712044b67a76823cf1ec625116d461e3 tools/audit/CSS_REFACTOR_REPORT.md
|
||||
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
|
||||
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
|
||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
|
||||
18f9d60dd78b6d7f6cbdc21d4cb579d4b48fbda2557c9c1f84370ac02af80a57 tools/dev/package.json
|
||||
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
|
||||
89d2b4e60a2f4cb93e8943b74b1b5ca84374b01fa74ea07bec07bdff315ac0e9 update.sh
|
||||
56c702111022e1ca2c8f6b72c635c749b05936ffe6387bdb0a202691614cbc96 upgrade.sh
|
||||
@@ -1,6 +1,53 @@
|
||||
use serde_json::{json, Value};
|
||||
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(
|
||||
root: &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")?,
|
||||
);
|
||||
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)?;
|
||||
let langs = root.join("langs");
|
||||
println!("cargo:rerun-if-changed={}", langs.display());
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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
@@ -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.
|
||||
- [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.
|
||||
- [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.
|
||||
- [Logs](LOGS.md) — live logs, stored logs, error logs and searches.
|
||||
- [Backups and recovery](BACKUP.md) — encrypted backups, restore and Secret Store.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
- `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.
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
"""Application-owned RustProxy scripts."""
|
||||
+122
-11
@@ -3,14 +3,14 @@
|
||||
|
||||
Requires: pip install tinycss2 cssselect2 (audit-only tooling, not runtime deps).
|
||||
Output is an inventory, NOT permission to remove dynamically generated styles.
|
||||
Selectors are considered dead only when a required class/ID token is absent
|
||||
from all application templates and scripts; :not() is not treated as a requirement.
|
||||
Absent class/ID tokens are candidates for review, because templates can assemble
|
||||
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.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -23,14 +23,106 @@ except ImportError as exc:
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WEB = ROOT / 'src/admin/web'
|
||||
STYLES = ('style.css', 'docs.css')
|
||||
SOURCE_SUFFIXES = {'.js', '.html', '.json', '.rs'}
|
||||
UNSUPPORTED_PSEUDO_ELEMENTS = re.compile(r'::(?:-webkit-[\w-]+|-moz-[\w-]+|backdrop)\b')
|
||||
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]:
|
||||
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':
|
||||
paths += list(WEB.rglob('*.json')) + list((ROOT / 'src').rglob('*.rs'))
|
||||
else:
|
||||
@@ -80,8 +172,8 @@ def token_refs(node) -> list[tuple[str, str]]:
|
||||
|
||||
|
||||
def possibly_matches(node, words: set[str]) -> bool:
|
||||
# Deliberately conservative: class/id absence proves a positive selector
|
||||
# impossible but does NOT make a selector in :not() impossible.
|
||||
# Only positive class/id requirements identify review candidates. Runtime
|
||||
# interpolation still requires manual inspection before removing a rule.
|
||||
if isinstance(node, css_parser.ClassSelector):
|
||||
return node.class_name in words
|
||||
if isinstance(node, css_parser.IDSelector):
|
||||
@@ -125,7 +217,9 @@ def audit_file(path: Path, words: set[str], scope: str) -> dict:
|
||||
if external:
|
||||
dead = False
|
||||
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})
|
||||
except css_parser.SelectorError as 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():
|
||||
cli = argparse.ArgumentParser(description=__doc__)
|
||||
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()
|
||||
results = [audit_file(WEB / filename, source_words('docs' if filename == 'docs.css' else 'panel'),
|
||||
'docs' if filename == 'docs.css' else 'panel') for filename in STYLES]
|
||||
panel = panel_sources()
|
||||
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:
|
||||
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"{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)')
|
||||
for e in r['entries']:
|
||||
for p in e['parts']:
|
||||
@@ -160,6 +262,15 @@ def main():
|
||||
print(f" {e['line']}: {p['selector'][:170]} ({', '.join(p['missing_tokens'])})")
|
||||
if args.json:
|
||||
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__':
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
"""RustProxy build helpers; keep local imports separate from external build packages."""
|
||||
Executable
+254
@@ -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
@@ -8,10 +8,74 @@ from pathlib import Path
|
||||
import sys
|
||||
|
||||
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]:
|
||||
messages: list[str] = []
|
||||
messages: list[str] = preserve_legacy_panel_css(root, repair_known)
|
||||
obsolete_split_files = (
|
||||
"src/admin/web/app.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"]:
|
||||
if not (root / path).is_file():
|
||||
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"
|
||||
if not registry.is_file():
|
||||
raise ValueError("Incomplete RustProxy 0.34.2 source tree: missing src/admin/documentation/documents.rs")
|
||||
|
||||
@@ -79,7 +79,11 @@ static ASSETS: LazyLock<HashMap<&'static str, Asset>> = LazyLock::new(|| {
|
||||
("/assets/streams.js", JS, include_str!("web/streams.js")),
|
||||
("/assets/redirects.js", JS, include_str!("web/redirects.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")),
|
||||
];
|
||||
entries
|
||||
|
||||
@@ -2966,6 +2966,69 @@ mod tests {
|
||||
}
|
||||
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]
|
||||
async fn public_document_and_discovery_contain_only_login_information() {
|
||||
let (router, store, _dir, _, _) = fixture().await;
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
-12666
File diff suppressed because it is too large.
Load diff
@@ -423,7 +423,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 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");
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user