v0.36.1
This commit is contained in:
1 parent
672c84bc13
commit
6f52ac3d00
38 files changed
+1177
-215
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# RustProxy source and documentation maintenance
|
||||
|
||||
Current source version: **0.36.0**. The Cargo package and binary are named `rust-proxy`.
|
||||
Current source version: **0.36.1**. The Cargo package and binary are named `rust-proxy`.
|
||||
|
||||
Before editing, read [README.md](README.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
|
||||
|
||||
|
||||
@@ -2,8 +2,19 @@
|
||||
|
||||
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in the latest release.
|
||||
|
||||
## 0.36.1 — interface icon polish and on-demand live logs
|
||||
|
||||
- **SVG icon polish:** HTTP/2 and HTTP/3 tabs now use the same protocol icon; listener HTTP/2/HTTP/3 limit links use the settings cog; **Apply preset** uses a preset/wand icon; the top **Logs** menu receives its missing icon. Compact Overview layout controls keep their native `+ / − / ← / → / ×` glyphs instead of receiving duplicate SVGs, and UI icon sizing is protected from chart SVG rules.
|
||||
- **WebSocket tunnels:** **Active tunnels** is now a compact clickable badge without an SVG icon.
|
||||
- **Overview metric cards:** removed the redundant **Recent samples / Ostatnie próbki** footer from the lower metric tiles; the footer is shown only when a metric has real detail text to present.
|
||||
- **Appearance copy:** removed the experimental/default-state sentence from the SVG icon option help text. The option remains account-scoped and off by default.
|
||||
- **On-demand live logs:** the panel status SSE uses `/api/events?...&logs=false`; `/api/logs` refreshes only while the Live Logs window is open or docked and actively following the bounded buffer. Closing, pausing or expanding the history view stops periodic log reads; reopening/resuming triggers an immediate refresh. Existing `/api/events` API consumers remain compatible because the new `logs` query parameter defaults to `true`.
|
||||
- **OPNsense certificate deploy:** the source release now includes the previously referenced `rustproxy-acme.conf.example` and `actions_rustproxy_cert.conf`; the integration guide explicitly requires certificate scope plus `allow_deploy` for the deployment credential.
|
||||
- **Upgrade notes:** no configuration or database changes are required.
|
||||
|
||||
## 0.36.0 — leaner administration polling
|
||||
|
||||
- **Experimental interface icons:** **Settings → Appearance → Interface → Show SVG interface icons** adds semantic inline SVG icons to navigation, action buttons, tabs and dialogs. The account-scoped option is **off by default**, keeps text labels as the accessible source of meaning, follows the current theme via `currentColor`, and can be disabled without changing the original control layout or behavior.
|
||||
- **Polling API:** `GET /api/cache`, `GET /api/transform` and `GET /api/traffic` now accept `view=live` for lightweight periodic refreshes. The default response remains the complete backward-compatible payload. Live responses carry the configuration `revision`, and the panel automatically reloads static data when that revision changes.
|
||||
- **Panel network load:** Cache and Transform polling no longer retransmit static settings/Vhost configuration on every refresh; Traffic polling omits the static Vhost list. Certificate API credentials are fetched only when the corresponding Settings tab is opened.
|
||||
- **Refresh behavior:** configuration writes and operational actions refresh status without also forcing a full `/api/logs` snapshot. Initial load, explicit log refresh and the existing live-log/fallback path retain log coverage.
|
||||
|
||||
Generated
+1
-1
@@ -1943,7 +1943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.36.0"
|
||||
version = "0.36.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.36.0"
|
||||
version = "0.36.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.99.1"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
+37
-32
@@ -1,10 +1,10 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 .gitignore
|
||||
eb7cb7121e65b91809f94b8994983635e0086bfeff30eef42e1a450de5e98517 AGENTS.md
|
||||
a21c3c493427f528178010fa4670d57f40db350890ff3c204d5917bf3f6d1bf3 CHANGELOG.md
|
||||
57cdec24db9391f2427445e6585b075219a57c215e054b20b5d9e8fa2a9ccb4e Cargo.lock
|
||||
ba5cf6364e861c263c8b8edf2243e5b45ee417ea873863904df3fb37aa8b5636 Cargo.toml
|
||||
fded6c62b3bbf31e80efa6a5411431625fb06a3be044eb03399de2e09d02c1d6 AGENTS.md
|
||||
ad23fed76aa9795ce6a34272504607454a8b60aa4293faf1b5be32526e9313f3 CHANGELOG.md
|
||||
a9309ef711e6daa6ca9bf7f3d7d9bfffe2411a5557be90d7eceeea06e3f5bd1d Cargo.lock
|
||||
a27d4c4f02f92ef1d41a96800ec46a9c835d4fe5edc2184f0babfa28e6c053a6 Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE
|
||||
82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md
|
||||
@@ -28,17 +28,17 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||
60d108fd168064e4d6980ed7499f3219017f9878c743697e77f2efc592e4d629 docs/ACCESS.md
|
||||
7d9fc7238ed04fe0e7258f7b9fced737ce7b85b578b32e7405cd5bea7983b5a3 docs/API.md
|
||||
19a4e028350f6ff786dd799a73fe7f3ec99cb5cf51cc291d359d35452df84862 docs/API.md
|
||||
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
||||
4fb55a549535f37467f14536cbf0db7be027ca8a019c2919a384af4f23655300 docs/CACHE.md
|
||||
32599d67a11fd0ebbc612e82b9f602789f4ed4103cda482c6870d8741100c8b4 docs/CERTIFICATES.md
|
||||
02bb383b5c3bce22616c3cc3db30a9469806dc16decb509973e84c60aa30fc5b docs/CONFIGURATION.md
|
||||
fec29ce846d6705eb45819d85d382f28e28a146b265feab2e4c7cce2a35e5883 docs/CUSTOM_LOCATIONS.md
|
||||
57db0ce916e01af49d41910ede6885e1a77744484ceb084606d34f8a72362c53 docs/DASHBOARD.md
|
||||
91ce42f259e833b6b5bd351519d27727b81dacf7879516bd024bb11dc728d7bd docs/DASHBOARD.md
|
||||
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
|
||||
54a9b66016457e104aae7de30121fad87c887e98fd3834b60051afbcc81e014a docs/FEATURES.md
|
||||
3e7e63d1d8f5d5019d5e4fdbabfe44fb39c2b75e7141d9c94846d946f34849f2 docs/FEATURES.md
|
||||
3939c14e83ef6856a421f358a3f7fce3d50ea375bc1b9f0419f27a3eed658f6f docs/INSTALL.md
|
||||
694cb7b15c5bbdc94cecd15dc516c05dcb3467849322cdd125b872d36dee4699 docs/LOGS.md
|
||||
6e37859bf0dced99d4a7db93790cb38fcd90191932827946d5223eaa4c784fce docs/LOGS.md
|
||||
0e9ae9509a78d00951d2c7c3f13eaf518d1f3448a29b048f5933e1f5f8440275 docs/MIGRATION.md
|
||||
0972248b7062b37e61edcc34dab5fe73ee9bb50195a0aa475c72efa0dd311aa9 docs/MONITORING.md
|
||||
e2e8fc307885bd02c177aea5740b2ac767fc7bdcfe7034dd75b8646f3014b39f docs/PERFORMANCE.md
|
||||
@@ -81,17 +81,19 @@ af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhost
|
||||
1a041c505669625a7f0f8011484cf5b6d2ec69a7de859801312e4fcf4c8d3555 install-debian13.sh
|
||||
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
||||
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md
|
||||
ba753f69557bd9fa990e56beabebccf39f93b5f6fb7e66a2e241c8c461dfd763 integrations/opnsense-acme/README.md
|
||||
3a1f8c326618a4613bb35f9e83428f6d1cf668b9e37b4731e5d38c3081fc94d8 integrations/opnsense-acme/actions_rustproxy_cert.conf
|
||||
de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh
|
||||
73784ac652708b04ca6c91aeed4524f51f50d63715bbeefe7f5590b353e78943 integrations/opnsense-acme/rustproxy-acme.conf.example
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example
|
||||
11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 integrations/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
a10d61ad26d111358dbe8b4bcf5b0df1eb01e52a9c632aab8f2a1dc7af4f253a integrations/routeros-oci-build/README.md
|
||||
e655bb17583e46f182226d7d2e1da45e6dffd0a8fd457e0cfb776778445e4358 integrations/routeros-oci-build/README.md
|
||||
3f092a9ab80221eddd67be4d1a776acbc6e04c1eb6011b588280c28e83b2582f integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||
e1c3eca78541682f98bf1449ce5b0c5e5c69228a608250e6d879275380cde25c langs/en_US.json
|
||||
34628bddc76c02eff482aa8300e0523e0edad87dfa4ebc3ef3285d68b6c46cfb langs/pl_pl.json
|
||||
a83dd54e685cdf96c2f3c8dc82c8df2ccc4c3e46268d9c1dbc68d293cbebf8bb langs/en_US.json
|
||||
8b9b4ea09b9f946167805173f27fab9b17a6579dc28811cbc0aee619a13c5166 langs/pl_pl.json
|
||||
9664543e4d03422788a65e42f968c1f026c2235cb5be6fc3e0ad318997077538 make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -123,7 +125,7 @@ a59587ce8ad41b1bbb431356b58398f9c8ff7cb96a613a91752e907a70e10c2e scripts/deploy
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
||||
4c905a387f210c30fbf12a7738676fd39819bc96bc4a272da1db5f18a2d3013e scripts/migration/migrate_npm_core.py
|
||||
2ae780e30c1212ffe134f1785b2767a4a6591d5f74baef6c2f47cc8df35afbdb scripts/preflight.py
|
||||
960fdf97ec899107a567405e9b9c2b70ee16951d5ea26c37d885a7920f7b3aa1 scripts/preflight.py
|
||||
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
|
||||
d759315babd9971b354d6f702d4c861eb1cd5c41b1e2e0f1a94ad4d2c2db49a3 scripts/runtime/acme_provider.py
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
|
||||
@@ -160,25 +162,25 @@ b9c2952e3fcfca540d0ad64f2d3acc5c1f5240e707f4f23ef4d452f46fcb520f src/admin/docu
|
||||
059218501907355b22bc84e7e0c41ec72565af283f5e97170ce7707ea5874009 src/admin/file_logs.rs
|
||||
d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file_logs_io.rs
|
||||
77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs
|
||||
77645eb8a845e64b76726ffcfd31f2e009667c6285a80915c60c2a02ecacf236 src/admin/logging_api.rs
|
||||
743bb7436f77e04015328a0e7e7d425e43ef8aa8d61467bde157e462c1dcdc2e src/admin/logging_api.rs
|
||||
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
|
||||
192350fb8b6557cf05e49dce9597228a00b21566cd6da5129871bea026983669 src/admin/openapi.json
|
||||
500e87bb88ccc45806c0967262e76e3ee77ec5c2fbd9881d8aa26ff043f3377d src/admin/openapi.json
|
||||
f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 src/admin/operations.rs
|
||||
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
|
||||
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
|
||||
b3c3c2d33c150eb42ce210c6074e5eff6a8af9391118012e413bc9323d3ae50c src/admin/preferences.rs
|
||||
9080babb5094d24c2cd4b8e6efb6a516142263ff9f45219a13857dfc55f33fa4 src/admin/preferences.rs
|
||||
35a7f99c7448c90b8637f4b3a7becca138f1f378fe5dc3f87122151525bb20f9 src/admin/preferences_columns.rs
|
||||
b31595d9522962ae16dff83dd8bc063d31fb027cbf357e9953285d2e622daa26 src/admin/preferences_tests.rs
|
||||
4bc49f367ea0d7e387151434498366e61836d3132e12657cd2b28073f668f80c src/admin/preferences_tests.rs
|
||||
62ecbae5dee00aeeca3eb806e2605372471dfab971cde8f66f11e711df8fcea1 src/admin/public/mod.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
6930b4f4b45257772b321b20a9becdf9bc4610274852948ba38c2d0cdf02dfaf src/admin/refresh_preferences.rs
|
||||
c8454f3092145c5fefdc8d93951ae46f192a6eb8139dbe288284b3a1c803b192 src/admin/router.rs
|
||||
e399ff25c8f26aab429b329609beb9633a61ed6e32ca82d14cfa59978585dd35 src/admin/router.rs
|
||||
e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/session.rs
|
||||
7aeb784a3ef58795c8fe26467bb4e1a00e61420911f798a584ab09c12f73a754 src/admin/status.rs
|
||||
2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs
|
||||
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
e427a72973381c47f140c20d0d23ac88fb57e4acc2c44979b53503b6944e7bbb src/admin/tests.rs
|
||||
0d5bd0e7fe4428959a90256ae1cbdb8ec6ffbc829214907670779564e1b1b2f8 src/admin/tests.rs
|
||||
c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js
|
||||
@@ -186,18 +188,19 @@ c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/
|
||||
aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 src/admin/web/app/certificate-inspection.js
|
||||
1a4d40a86067493e75b47f2d735d50fa4d161344ce145d4e502b0225144ed45a src/admin/web/app/chart-scales.js
|
||||
e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 src/admin/web/app/config-history.js
|
||||
892833d21c58a8b0d4e4efc3752391cf3d3ec5f24fceae8fd4f180849fe9187c src/admin/web/app/core.js
|
||||
fcce3b0204ca2c679f5d8049d020619664a3c0a45889975681258d82ae329988 src/admin/web/app/core.js
|
||||
d695ec90fb5c98c27d7ba1d2bebf54f9840985fc530efcec3fcaf48d74b36c6b src/admin/web/app/dashboard-inline-layout.js
|
||||
e4238322a985d3e8b7f2de94b7af31e1c718775590b2c7b3428b878cd43337b7 src/admin/web/app/dashboard-layout-editor.js
|
||||
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
|
||||
47eaf9486099468b3d686bdca5c6b8d59aa5f5661baa03b3698320cf618db4e2 src/admin/web/app/overview-extra-metrics.js
|
||||
6c1321059662a15998bba60190419469c04b71be346fb9c2adfd8b83a4ea696b src/admin/web/app/icons.js
|
||||
32aa11f041eafdf16e66adef6f5847a67413785224da2c19ecbf825996622aff src/admin/web/app/overview-extra-metrics.js
|
||||
5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js
|
||||
4d1d4760cdff44771c9d880a7c0da81d7233ef6e3a4868111a522703831a7a54 src/admin/web/app/overview-samples.js
|
||||
db9012546cae8c58fc2bcbee71c9a95956a13a6db9e6d126c6a3770d45a4e4d2 src/admin/web/app/overview-wide-metrics.js
|
||||
bfd9fafb3112197e8f875dbf87a9a66c81aef0fe83687601a84a66f5e13f69bc src/admin/web/app/path-permissions.js
|
||||
46c7cd36397375eb7e8c230c8aa97b1dbea0a63800beb977efe1ec67a14b237e src/admin/web/app/preferences.js
|
||||
be276b8c55dc2c48770de200ce3f6e669e4c4146bcd1b4d03c4af32f025c4709 src/admin/web/app/preferences.js
|
||||
9e2b12d34ab1fb29716645720577ad49b16c03b46c482a03695e8f645d170e79 src/admin/web/app/rate-sampling.js
|
||||
d34f9e25a04cc4eafdbc48daa4e69aab18944e8d34e555a50d9c122c680595f8 src/admin/web/app/refresh.js
|
||||
3b8e566d81a5b94ef21c73e7805ae128454f085554de9358040de266bcbdb0e0 src/admin/web/app/refresh.js
|
||||
c6ae53c0e95e67b9165822813efc622001be5354c19bb221a52514221d1df23e src/admin/web/app/runtime.js
|
||||
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
||||
c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js
|
||||
@@ -206,11 +209,11 @@ b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/
|
||||
f5855db3327f51170a28114e07d207745b4d6c8822d88d24c1a81eb92f345b14 src/admin/web/cache-panel.js
|
||||
9f323507649910561518b63cf6632e81b3ecc4b09501ae53b635705f34ea6587 src/admin/web/certificate-api.js
|
||||
a09f060b47771c56e9b9e57f1b896a32a2f87f1fb33401ac76932a91d247c67e src/admin/web/css/00-tokens.css
|
||||
77b8b748f31ef383c64ba5ca1740e9b6073f0de1e77c2c8c62d0f38bb52dd296 src/admin/web/css/10-base.css
|
||||
922e1bc7631f9e942c31cb8b130bb377791cdc0a3248513c80698af9ed4c8b7b src/admin/web/css/10-base.css
|
||||
6ed54b27a4a81b073f1d30cf61e366f3d5c1f6f8495f0d0a8af48e061937eb4e src/admin/web/css/20-shell.css
|
||||
c6d5696f0cdf4149e3341d18f7fd955f2411d50aac49a307c3eb0de2db9f92cb src/admin/web/css/30-overview.css
|
||||
321f8c767f1bb82d2b684ed72506b753fc01d8d9a81676f28a84dcaa580d5fc8 src/admin/web/css/30-overview.css
|
||||
dd82b5e3ead91c20bb08cf8d71bbba31ea8e3d108c417913c63409f3e34da7e1 src/admin/web/css/40-editors.css
|
||||
6af9fcb439823ec63abe2c1dd86c9ecba17c233262fd8ab80d321d64ae2758ab src/admin/web/css/50-settings.css
|
||||
89e33ec7004dda531a15b0b210a59aa125f5954a382e6f7def429293cbdf0f4b src/admin/web/css/50-settings.css
|
||||
f8b515c63080c71ee647cfd8c36628271e8357b31cd84f00cf591e9b752b44fe src/admin/web/css/60-monitoring.css
|
||||
33c556cd4be0310117ea5324d27e622b0b5ca2190c8d6f70aa551d94fb411d38 src/admin/web/css/70-responsive.css
|
||||
8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md
|
||||
@@ -239,14 +242,14 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
||||
3f0525ca1d4e088ccd4988b8a117b26a8cf8c12213b24c37081d41b68d2b0ff2 src/admin/web/host404.js
|
||||
cdd390e69ca4cd010262e8fddb653e4097fbcfdc678b081c422edcd68e974f85 src/admin/web/index.html
|
||||
7915a06cebb3dfa2c51833c326586920691ba4431b1f984429b039333f9c2a93 src/admin/web/integrations.js
|
||||
c6e8e98a17126b24da815f0d8d55cbc47b451b046aea922638dff9cc9f4397d7 src/admin/web/index.html
|
||||
7979c490b7fcfcd8f5e52e99fcf7757d78509337a625341c98f62c6da50da374 src/admin/web/integrations.js
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
ac9f667273918e18406df06cb2613c923f50a88897693a03a18f500e22dc6ec2 src/admin/web/login.html
|
||||
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js
|
||||
734aeac4855f0913a84a82d7b5c17b37de4504b50a8701edeb775d6926929bce src/admin/web/management.js
|
||||
16d848a25f744e55d18757ce82cfd80e85bcc5b1777342c41e7bd732e13d8913 src/admin/web/management.js
|
||||
1f3d10e389c9c20e6eb019509fa6e82b50c6b43bbf988c1210a4d42bb73985d0 src/admin/web/partials/overview-charts.html
|
||||
2842346eb66569b0aba2938ff0f724b50d21254b2aa0d2c00f6380ad03449b61 src/admin/web/partials/overview-metrics.html
|
||||
5b98f30f2d0da378221b029e7f7fa81d81fc725424a2242a336f1cf5025e6f87 src/admin/web/partials/overview-metrics.html
|
||||
3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js
|
||||
2d491fe9a547fe109dc5db6a74806d50b08c3daca15a6622c79c9fea33b10c63 src/admin/web/redirects.js
|
||||
76a01f590d49649dcb1f554c221290919b2a8438e6001737f3c096fa6daec74d src/admin/web/settings.js
|
||||
@@ -553,8 +556,10 @@ e5f5fbe702f66bfcbcbc1d1f3cd3d89bdbb929bb86aed94868a2a06a4b9e59e3 src/web_server
|
||||
b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server/serve.rs
|
||||
714418c86ab241020a561fcf662a8fe324a6a70cc2fd3a313c56d7939c14b9e8 src/web_server/tests.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
d1b019e702f1b0f1fc2fd03eed3893b7f966e7ff3025d76c17452b02a563a231 tests/panel_interface_contract.py
|
||||
b2c39608573fef61e2a50e17dff3868c2ada770abea691872ec261c5cb3c554b tests/panel_refresh_contract.js
|
||||
2f7f0453c817dc9e3d4ab61296af5821ff3e0826ad70511714afd7486c6105bc tests/panel_icons_browser.py
|
||||
d136ebe964e83aa7194d70ec7e9dda36cf0e9832e7cd0d357d199440a705e4f2 tests/panel_icons_contract.js
|
||||
01af3008be27fabb486c141c67099af104f2725cc56f4c656aea9741a3c150cd tests/panel_interface_contract.py
|
||||
24a815a280de87a7ccec6f4d212d1468022c844e9e7d0b113349ff26bde5fc52 tests/panel_refresh_contract.js
|
||||
e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js
|
||||
eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md
|
||||
7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ The current bundled `src/admin/openapi.json` declares these operations (among ot
|
||||
| `GET /api/certificates` · `POST /api/certificates/upload` | Certificate inventory/import. |
|
||||
| `POST /api/certificates/inspect` | Read DNS SAN coverage from a managed/external public PEM path for the editor; admin browser session only. |
|
||||
| `GET /api/acme` · `POST /api/acme/issue` · `POST /api/acme/renew` | Managed certificate jobs. |
|
||||
| `GET/PUT /api/preferences` | Personal UI settings. |
|
||||
| `GET/PUT /api/preferences` | Personal UI settings, including theme/layout and optional `interface_icons` (default `false`). |
|
||||
| `GET /api/dashboard-presets` and named preset operations | Saved Overview presets. |
|
||||
| `GET /docs/openapi.json` | Protected machine-readable API specification. |
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ After **Unlock layout**, every block shows a compact toolbar: the **⠿** handle
|
||||
|
||||
**Dashboard Presets** supports up to **16 named layouts per administrator**. Save, load, rename, overwrite and delete presets; a factory layout can be restored. Presets preserve the dashboard arrangement, not the live traffic values. Interface theme, scale and other display settings are also account-scoped and do not alter proxy routing or rewrite shared configuration.
|
||||
|
||||
### SVG interface icons
|
||||
|
||||
Under **Settings → Appearance → Interface**, enable **Show SVG interface icons** to add semantic line icons to the main navigation, action buttons, tabs and dialogs. The preference is stored per administrator as `interface_icons: true|false` in `/api/preferences` and is **off by default** for both new and upgraded accounts.
|
||||
|
||||
The icons are inline SVGs bundled with RustProxy; no web font, CDN or external icon library is loaded. They use `currentColor`, so the active theme, danger/primary states and disabled-state colors remain authoritative. Text labels and existing ARIA labels remain the source of accessible meaning; the SVGs are `aria-hidden`. Dynamic controls are reclassified when their state changes, so actions such as sort direction, save/loading, show/hide and refresh keep a matching icon. Turning the option off removes the injected icons and restores the original text-only presentation without restarting the panel.
|
||||
|
||||
## Editing configuration safely
|
||||
|
||||
1. Open the relevant workspace and inspect existing state before editing.
|
||||
|
||||
@@ -95,6 +95,7 @@ RustProxy is a single service that combines a reverse proxy, a static/PHP web se
|
||||
- Nginx Proxy Manager import with a dry-run plan.
|
||||
- Managed install/update scripts with rollback, DEB/RPM packages, Docker image and an OPNsense plugin.
|
||||
- Panel in English and Polish, light and dark themes, adjustable density and corner style.
|
||||
- Optional account-scoped semantic SVG icons for navigation, actions, tabs and dialogs; off by default.
|
||||
|
||||
## Administration and API
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ RustProxy has several independent logging paths. Choose the one matching the que
|
||||
|
||||
## Live Logs
|
||||
|
||||
The administration panel does not fetch `/api/logs` while Live Logs is closed. Opening or docking the Live Logs window starts the bounded live snapshot refresh; closing it stops that polling. Pausing or expanding the history buffer also suspends periodic `/api/logs` reads until live viewing resumes. The main `/api/events` status stream is kept independent by using `logs=false` in the panel.
|
||||
|
||||
Use **Logs → Live Logs** in the authenticated panel, or locally:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -5,7 +5,7 @@ A simple mechanism for automatically sending a certificate generated by `os-acme
|
||||
## Files
|
||||
|
||||
- `opnsense-rustproxy-cert-deploy.sh` - sends the certificate and private key to RustProxy.
|
||||
- `rustproxy-acme.conf` - connection settings, API token, and certificate file paths.
|
||||
- `rustproxy-acme.conf.example` - example connection settings, API token, and certificate file paths.
|
||||
- `actions_rustproxy_cert.conf` - optional `configd` action that allows the script to be executed from Cron in OPNsense.
|
||||
|
||||
## 1. Installation
|
||||
@@ -50,13 +50,15 @@ KEY_FILE='/path/to/private.key'
|
||||
|
||||
## 3. RustProxy API
|
||||
|
||||
Create a certificate API key in RustProxy and enter its values in the configuration:
|
||||
Create a certificate API key in **Settings -> Certificate API** and enter its values in the configuration:
|
||||
|
||||
```text
|
||||
RUSTPROXY_KEY_ID
|
||||
RUSTPROXY_TOKEN
|
||||
```
|
||||
|
||||
The credential must explicitly allow `CERTIFICATE_ID` and **Also allow deploy** (`allow_deploy`) must be enabled. A reload-only credential will be rejected with HTTP 403.
|
||||
|
||||
The script sends the certificate to:
|
||||
|
||||
```text
|
||||
@@ -88,7 +90,7 @@ cp actions_rustproxy_cert.conf /usr/local/opnsense/service/conf/actions.d/
|
||||
service configd restart
|
||||
```
|
||||
|
||||
Then go to **System -> Settings -> Cron** and add a scheduled execution of the `Deploy ACME certificate to RustProxy` action.
|
||||
Verify the action first with `configctl rustproxy_cert deploy`. Then go to **System -> Settings -> Cron** and add a scheduled execution of the `Deploy ACME certificate to RustProxy` action.
|
||||
|
||||
The script deliberately reconciles on every run instead of trusting a local cache. The server is the source of truth: an identical RustProxy-owned certificate generation is a no-op, a missing id is created, and changed material is rotated atomically. This also repairs drift when a certificate entry or generation was removed outside OPNsense.
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[deploy]
|
||||
command:/usr/local/sbin/opnsense-rustproxy-cert-deploy.sh
|
||||
parameters:
|
||||
type:script_output
|
||||
message:deploy ACME certificate to RustProxy
|
||||
description:Deploy ACME certificate to RustProxy
|
||||
@@ -0,0 +1,21 @@
|
||||
# RustProxy administration endpoint reachable from OPNsense.
|
||||
RUSTPROXY_URL='https://rustproxy.example.lan:9090'
|
||||
|
||||
# Certificate API credential created in RustProxy Settings -> Certificate API.
|
||||
# The credential must allow this CERTIFICATE_ID and have deploy permission enabled.
|
||||
RUSTPROXY_KEY_ID='opnsense-acme'
|
||||
RUSTPROXY_TOKEN='REPLACE_WITH_CERTIFICATE_API_TOKEN'
|
||||
|
||||
# Stable RustProxy certificate id to create/update.
|
||||
CERTIFICATE_ID='shared-cert'
|
||||
|
||||
# os-acme-client certificate material. Replace CERT_UUID with the actual UUID.
|
||||
CERT_FILE='/var/etc/acme-client/certs/CERT_UUID/fullchain.pem'
|
||||
KEY_FILE='/var/etc/acme-client/keys/CERT_UUID/private.key'
|
||||
|
||||
# Optional: trust a private CA used by the RustProxy administration listener.
|
||||
# RUSTPROXY_CACERT='/usr/local/etc/ssl/rustproxy-ca.pem'
|
||||
|
||||
# Optional: connect to a fixed address while keeping TLS verification for the URL hostname.
|
||||
# Format: hostname:port:address
|
||||
# RUSTPROXY_RESOLVE='rustproxy.example.lan:9090:192.0.2.20'
|
||||
@@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \
|
||||
bash integrations/routeros-oci-build/build-routeros-oci.sh push
|
||||
```
|
||||
|
||||
For example, with RustProxy version `0.36.0` and the defaults this publishes:
|
||||
For example, with RustProxy version `0.36.1` and the defaults this publishes:
|
||||
|
||||
```text
|
||||
zot.example.net/network/rustproxy:0.36.0
|
||||
zot.example.net/network/rustproxy:0.36.1
|
||||
zot.example.net/network/rustproxy:latest
|
||||
zot.example.net/network/rustproxy:routeros
|
||||
```
|
||||
|
||||
@@ -2402,6 +2402,8 @@
|
||||
"buttonStyleNormal": "Normal",
|
||||
"buttonStyleCompact": "Compact",
|
||||
"buttonStyleHelp": "Normal uses the refreshed controls; Compact restores the smaller source-style buttons across all actions and dialog tabs.",
|
||||
"interfaceIcons": "Show SVG interface icons",
|
||||
"interfaceIconsHelp": "Adds semantic SVG icons to navigation, actions, tabs and dialogs.",
|
||||
"entityEnableHint": "Controls whether this item is active.",
|
||||
"setupCommandExample": "Standard service installation example:",
|
||||
"certificateRuntimeTitle": "ACME runtime",
|
||||
|
||||
@@ -2402,6 +2402,8 @@
|
||||
"buttonStyleNormal": "Normalny",
|
||||
"buttonStyleCompact": "Kompaktowy",
|
||||
"buttonStyleHelp": "Normalny używa odświeżonych przycisków; Kompaktowy przywraca mniejsze przyciski ze źródłowego interfejsu we wszystkich akcjach i zakładkach modali.",
|
||||
"interfaceIcons": "Pokaż ikony SVG interfejsu",
|
||||
"interfaceIconsHelp": "Dodaje semantyczne ikony SVG do nawigacji, akcji, zakładek i okien dialogowych.",
|
||||
"entityEnableHint": "Określa, czy ten element jest aktywny.",
|
||||
"setupCommandExample": "Przykład dla standardowej instalacji usługi:",
|
||||
"certificateRuntimeTitle": "Środowisko ACME",
|
||||
|
||||
@@ -132,7 +132,7 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
flat = module.parent.with_suffix(".rs")
|
||||
if flat.is_file():
|
||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/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", "LICENSE", "docs/FEATURES.md", "docs/PERFORMANCE.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/INSTALL.md", "docs/MONITORING.md", "docs/CUSTOM_LOCATIONS.md", "grafana/rustproxy-overview.json"]:
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression/mod.rs", "src/diagnostics.rs", "src/traffic/mod.rs", "src/system_stats/mod.rs", "src/system_stats/linux/mod.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security/mod.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/rates.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/icons.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", "LICENSE", "docs/FEATURES.md", "docs/PERFORMANCE.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/INSTALL.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"):
|
||||
|
||||
@@ -171,6 +171,7 @@ async fn metrics_summary(State(state): State<Arc<App>>) -> Response {
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct EventsQuery {
|
||||
interval: Option<u64>,
|
||||
logs: Option<bool>,
|
||||
}
|
||||
|
||||
async fn events(
|
||||
@@ -179,6 +180,7 @@ async fn events(
|
||||
Query(query): Query<EventsQuery>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let interval = query.interval.unwrap_or(3);
|
||||
let include_logs = query.logs.unwrap_or(true);
|
||||
if ![1, 2, 3, 5, 10].contains(&interval) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
@@ -198,8 +200,8 @@ async fn events(
|
||||
)
|
||||
})?;
|
||||
let stream = futures_util::stream::unfold(
|
||||
(state, secret, permit, false, interval),
|
||||
|(state, secret, permit, delayed, interval)| async move {
|
||||
(state, secret, permit, false, interval, include_logs),
|
||||
|(state, secret, permit, delayed, interval, include_logs)| async move {
|
||||
if delayed {
|
||||
tokio::time::sleep(Duration::from_secs(interval)).await;
|
||||
}
|
||||
@@ -218,13 +220,16 @@ async fn events(
|
||||
Ok(Some(_)) => {}
|
||||
_ => return None,
|
||||
}
|
||||
let payload =
|
||||
let payload = if include_logs {
|
||||
json!({"status":live_status_payload(&state), "logs":logs_payload(&state)})
|
||||
} else {
|
||||
json!({"status":live_status_payload(&state)})
|
||||
}
|
||||
.to_string();
|
||||
let event = Event::default().event("snapshot").data(payload);
|
||||
Some((
|
||||
Ok::<Event, Infallible>(event),
|
||||
(state, secret, permit, true, interval),
|
||||
(state, secret, permit, true, interval, include_logs),
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
+183
-33
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.36.0",
|
||||
"version": "0.36.1",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -443,10 +443,31 @@
|
||||
"type": "object",
|
||||
"description": "Optional explicit provider and integration method. Absent means infer the existing active Basic/Tinyauth/Authelia/authentik/OIDC configuration for backward compatibility. The selected method must match the one enabled legacy settings object; unknown, inconsistent or unsupported pairs are rejected.",
|
||||
"additionalProperties": false,
|
||||
"required": ["provider", "method"],
|
||||
"required": [
|
||||
"provider",
|
||||
"method"
|
||||
],
|
||||
"properties": {
|
||||
"provider": {"type": "string", "enum": ["basic", "tinyauth", "authelia", "authentik", "pocket_id", "custom"]},
|
||||
"method": {"type": "string", "enum": ["basic", "forward_auth", "oidc"], "description": "Basic supports basic only; Tinyauth/Authelia/authentik support forward_auth or oidc; Pocket ID/custom support oidc only."}
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"basic",
|
||||
"tinyauth",
|
||||
"authelia",
|
||||
"authentik",
|
||||
"pocket_id",
|
||||
"custom"
|
||||
]
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"basic",
|
||||
"forward_auth",
|
||||
"oidc"
|
||||
],
|
||||
"description": "Basic supports basic only; Tinyauth/Authelia/authentik support forward_auth or oidc; Pocket ID/custom support oidc only."
|
||||
}
|
||||
}
|
||||
},
|
||||
"OidcConfig": {
|
||||
@@ -454,20 +475,76 @@
|
||||
"description": "Opt-in OpenID Connect Authorization Code integration method for an identity provider selected by authentication.provider. Uses issuer discovery, S256 PKCE, nonce/state cookies, JWKS RS256/ES256 ID token verification and encrypted host-only application sessions. Existing standalone oidc.enabled configurations remain supported.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "default": false},
|
||||
"issuer_url": {"type": "string", "format": "uri", "description": "Exact HTTPS OIDC issuer; /.well-known/openid-configuration is discovered at this URL"},
|
||||
"backend_url": {"type": "string", "format": "uri", "description": "Optional internal HTTP(S) origin for server-side discovery/token/JWKS/UserInfo requests; public issuer and JWT validation remain unchanged"},
|
||||
"redirect_url": {"type": "string", "format": "uri", "description": "HTTPS URL for an exact hostname in this Vhost and path /_rustproxy/oidc/callback"},
|
||||
"client_id": {"type": "string", "maxLength": 256},
|
||||
"client_secret": {"type": "string", "maxLength": 2048, "description": "Confidential OIDC client secret; stored in Vhost configuration"},
|
||||
"session_secret": {"type": "string", "minLength": 32, "maxLength": 4096, "description": "High-entropy secret for encrypted session and temporary state cookies"},
|
||||
"scopes": {"type": "string", "default": "openid profile email"},
|
||||
"allow_private": {"type": "boolean", "default": false},
|
||||
"address_allowlist": {"type": "array", "items": {"type": "string"}},
|
||||
"tls_ca_pem": {"type": "string", "nullable": true},
|
||||
"connect_timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 30, "default": 5},
|
||||
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 60, "default": 10},
|
||||
"session_ttl_seconds": {"type": "integer", "minimum": 60, "maximum": 86400, "default": 3600}
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"issuer_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Exact HTTPS OIDC issuer; /.well-known/openid-configuration is discovered at this URL"
|
||||
},
|
||||
"backend_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Optional internal HTTP(S) origin for server-side discovery/token/JWKS/UserInfo requests; public issuer and JWT validation remain unchanged"
|
||||
},
|
||||
"redirect_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "HTTPS URL for an exact hostname in this Vhost and path /_rustproxy/oidc/callback"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string",
|
||||
"maxLength": 256
|
||||
},
|
||||
"client_secret": {
|
||||
"type": "string",
|
||||
"maxLength": 2048,
|
||||
"description": "Confidential OIDC client secret; stored in Vhost configuration"
|
||||
},
|
||||
"session_secret": {
|
||||
"type": "string",
|
||||
"minLength": 32,
|
||||
"maxLength": 4096,
|
||||
"description": "High-entropy secret for encrypted session and temporary state cookies"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "string",
|
||||
"default": "openid profile email"
|
||||
},
|
||||
"allow_private": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"address_allowlist": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tls_ca_pem": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"connect_timeout_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 30,
|
||||
"default": 5
|
||||
},
|
||||
"timeout_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 60,
|
||||
"default": 10
|
||||
},
|
||||
"session_ttl_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 60,
|
||||
"maximum": 86400,
|
||||
"default": 3600
|
||||
}
|
||||
}
|
||||
},
|
||||
"ForwardAuthConfig": {
|
||||
@@ -475,17 +552,70 @@
|
||||
"description": "Per-Vhost Authelia or authentik verification. Disabled by default. All other settings are optional and use RustProxy defaults; when enabled, url must identify an internal HTTP(S) origin (or expected verifier path). Authelia additionally requires a public HTTPS login_url. External addresses retain normal safe upstream validation, with TLS verification always enabled for HTTPS.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "default": false},
|
||||
"url": {"type": "string", "maxLength": 2048, "description": "Internal service/outpost origin or the provider's verifier endpoint"},
|
||||
"login_url": {"type": "string", "maxLength": 2048, "description": "Required public HTTPS portal URL for Authelia; ignored for authentik"},
|
||||
"allow_private": {"type": "boolean", "default": false},
|
||||
"address_allowlist": {"type": "array", "maxItems": 32, "items": {"type": "string"}},
|
||||
"tls_ca_pem": {"type": "string", "nullable": true},
|
||||
"connect_timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 30, "default": 5},
|
||||
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 60, "default": 10},
|
||||
"request_headers": {"type": "array", "maxItems": 32, "items": {"type": "string", "maxLength": 128}, "description": "Only cookie, authorization, user-agent, accept, accept-language and x-api-key are allowed"},
|
||||
"response_headers": {"type": "array", "maxItems": 32, "items": {"type": "string", "maxLength": 128}, "description": "Allowed Remote-*, X-Auth-Request-* and X-Authentik-* identity headers; total injected identity data capped at 8 KiB"},
|
||||
"redirect_forbidden": {"type": "boolean", "default": false, "description": "Authelia only: accept trusted portal Location on explicit 403 if provided"}
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"maxLength": 2048,
|
||||
"description": "Internal service/outpost origin or the provider's verifier endpoint"
|
||||
},
|
||||
"login_url": {
|
||||
"type": "string",
|
||||
"maxLength": 2048,
|
||||
"description": "Required public HTTPS portal URL for Authelia; ignored for authentik"
|
||||
},
|
||||
"allow_private": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"address_allowlist": {
|
||||
"type": "array",
|
||||
"maxItems": 32,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tls_ca_pem": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"connect_timeout_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 30,
|
||||
"default": 5
|
||||
},
|
||||
"timeout_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 60,
|
||||
"default": 10
|
||||
},
|
||||
"request_headers": {
|
||||
"type": "array",
|
||||
"maxItems": 32,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"description": "Only cookie, authorization, user-agent, accept, accept-language and x-api-key are allowed"
|
||||
},
|
||||
"response_headers": {
|
||||
"type": "array",
|
||||
"maxItems": 32,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"description": "Allowed Remote-*, X-Auth-Request-* and X-Authentik-* identity headers; total injected identity data capped at 8 KiB"
|
||||
},
|
||||
"redirect_forbidden": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Authelia only: accept trusted portal Location on explicit 403 if provided"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Vhost": {
|
||||
@@ -591,8 +721,13 @@
|
||||
"$ref": "#/components/schemas/ForwardAuthConfig",
|
||||
"description": "authentik single-application forward auth. The URL is the internal outpost origin; RustProxy proxies its reserved path on the protected host. Disabled by default; mutually exclusive with basic_auth, tinyauth and authelia."
|
||||
},
|
||||
"oidc": {"$ref": "#/components/schemas/OidcConfig"},
|
||||
"authentication": {"$ref": "#/components/schemas/AuthenticationChoice", "description": "Optional provider/method selector. Set to null or omit for legacy Vhost data. It does not replace the existing provider-specific settings objects."},
|
||||
"oidc": {
|
||||
"$ref": "#/components/schemas/OidcConfig"
|
||||
},
|
||||
"authentication": {
|
||||
"$ref": "#/components/schemas/AuthenticationChoice",
|
||||
"description": "Optional provider/method selector. Set to null or omit for legacy Vhost data. It does not replace the existing provider-specific settings objects."
|
||||
},
|
||||
"cache": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3687,6 +3822,11 @@
|
||||
"default": false,
|
||||
"description": "Account-scoped optional X/Y scales on sampled time-series charts. Shows the same Y range used to draw each SVG and X sample indices rather than inferred elapsed time; does not change adaptive scaling."
|
||||
},
|
||||
"interface_icons": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped administration UI preference. When enabled, semantic inline SVG icons are added to navigation, actions, tabs and dialogs while preserving text labels and accessibility."
|
||||
},
|
||||
"button_style": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -10414,7 +10554,7 @@
|
||||
},
|
||||
"/api/events": {
|
||||
"get": {
|
||||
"summary": "Live SSE status and log snapshots",
|
||||
"summary": "Live SSE status snapshots with optional logs",
|
||||
"tags": [
|
||||
"Monitoring"
|
||||
],
|
||||
@@ -10471,7 +10611,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "SSE snapshot events contain compact volatile status and bounded logs. Session validity is checked before each event. Native reconnect and keepalive apply. Each viewer selects 1/2/3/5/10 second pacing; invalid values return 422.",
|
||||
"description": "SSE snapshot events always contain compact volatile status. Bounded recent logs are included by default for backward compatibility and can be disabled per viewer with logs=false; the administration panel disables them until Live Logs is open/docked and polls /api/logs only while that view is active. Session validity is checked before each event. Native reconnect and keepalive apply. Each viewer selects 1/2/3/5/10 second pacing; invalid values return 422.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "interval",
|
||||
@@ -10489,6 +10629,16 @@
|
||||
],
|
||||
"default": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logs",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Include the bounded live log snapshot in each SSE event. Defaults to true for API compatibility; the panel uses false and fetches logs only while Live Logs is active.",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ struct AdminPreferences {
|
||||
chart_style: String,
|
||||
#[serde(default)]
|
||||
chart_axes: bool,
|
||||
#[serde(default)]
|
||||
interface_icons: bool,
|
||||
#[serde(default = "default_admin_button_style")]
|
||||
button_style: String,
|
||||
#[serde(default = "default_admin_corner_style")]
|
||||
@@ -180,6 +182,7 @@ impl Default for AdminPreferences {
|
||||
time_format: default_admin_time_format(),
|
||||
chart_style: default_admin_chart_style(),
|
||||
chart_axes: false,
|
||||
interface_icons: false,
|
||||
button_style: default_admin_button_style(),
|
||||
corner_style: default_admin_corner_style(),
|
||||
table_density: default_admin_table_density(),
|
||||
|
||||
@@ -41,6 +41,20 @@ fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
assert!(hidden.iter().all(|id| order.contains(id)));
|
||||
assert!(validate_admin_preferences(&AdminPreferences::default()).is_ok());
|
||||
assert!(!AdminPreferences::default().chart_axes);
|
||||
assert!(!AdminPreferences::default().interface_icons);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_interface_icons_preference_is_backward_compatible_and_off_by_default() {
|
||||
let legacy: AdminPreferences = serde_json::from_str(r#"{"theme":"light"}"#).unwrap();
|
||||
assert!(!legacy.interface_icons);
|
||||
let mut enabled = legacy.clone();
|
||||
enabled.interface_icons = true;
|
||||
assert!(validate_admin_preferences(&enabled).is_ok());
|
||||
let stored = serde_json::to_string(&enabled).unwrap();
|
||||
assert!(stored.contains("\"interface_icons\":true"));
|
||||
let recovered: AdminPreferences = serde_json::from_str(&stored).unwrap();
|
||||
assert!(recovered.interface_icons);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -57,6 +57,7 @@ fn admin_app_javascript() -> &'static str {
|
||||
");",
|
||||
include_str!("web/app/overview-samples.js"),
|
||||
include_str!("web/app/core.js"),
|
||||
include_str!("web/app/icons.js"),
|
||||
include_str!("web/app/path-permissions.js"),
|
||||
include_str!("web/app/session-resume.js"),
|
||||
include_str!("web/app/refresh.js"),
|
||||
|
||||
+17
-1
@@ -2146,6 +2146,7 @@ mod tests {
|
||||
assert_eq!(initial["time_format"], "24h");
|
||||
assert_eq!(initial["chart_style"], "default");
|
||||
assert_eq!(initial["chart_axes"], false);
|
||||
assert_eq!(initial["interface_icons"], false);
|
||||
assert_eq!(initial["button_style"], "normal");
|
||||
assert_eq!(initial["corner_style"], "default");
|
||||
assert_eq!(
|
||||
@@ -2183,6 +2184,7 @@ mod tests {
|
||||
assert_eq!(legacy["time_format"], "24h");
|
||||
assert_eq!(legacy["chart_style"], "default");
|
||||
assert_eq!(legacy["chart_axes"], false);
|
||||
assert_eq!(legacy["interface_icons"], false);
|
||||
assert_eq!(legacy["button_style"], "normal");
|
||||
assert_eq!(legacy["corner_style"], "default");
|
||||
assert_eq!(
|
||||
@@ -2205,6 +2207,7 @@ mod tests {
|
||||
"time_format":"12h",
|
||||
"chart_style":"neon",
|
||||
"chart_axes":true,
|
||||
"interface_icons":true,
|
||||
"button_style":"compact",
|
||||
"corner_style":"rounded",
|
||||
"table_density":"ultra",
|
||||
@@ -3524,6 +3527,16 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(bad_sse.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
let bad_sse_logs = router
|
||||
.oneshot(
|
||||
request("/api/events?logs=maybe")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(bad_sse_logs.status(), StatusCode::BAD_REQUEST);
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
@@ -3562,7 +3575,10 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::OK, "GET {path}");
|
||||
let value = json_body(response).await;
|
||||
for &field in omitted {
|
||||
assert!(value.get(field).is_none(), "{path} unexpectedly contains {field}");
|
||||
assert!(
|
||||
value.get(field).is_none(),
|
||||
"{path} unexpectedly contains {field}"
|
||||
);
|
||||
}
|
||||
for &field in retained {
|
||||
assert!(value.get(field).is_some(), "{path} is missing {field}");
|
||||
|
||||
@@ -59,6 +59,7 @@ function translate() {
|
||||
$("connection-label").textContent = t(connected ? "connected" : "disconnected");
|
||||
if (envelope) renderConfig(); renderConfigStorage(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyDocker?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); window.rustProxyBackendDashboard?.render(latestStatus, null, false); window.rustProxyExtraOverview?.render(latestStatus, false); window.rustProxyWideOverview?.render(latestStatus, false); window.rustProxyWebOverview?.render(latestStatus, false); } if (lastLogs) renderLogs(lastLogs);
|
||||
syncSettingsDirtyIndicators();
|
||||
window.rustProxyIcons?.refresh?.();
|
||||
}
|
||||
function hideToast() { const node = $("toast"); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
|
||||
function raiseToast() {
|
||||
@@ -379,13 +380,9 @@ function renderFooter(s) {
|
||||
}
|
||||
let lastLogs = null;
|
||||
function renderLogs(data) { lastLogs = data; window.rustProxyLogs?.render(data); }
|
||||
async function refresh(force = false, includeLogs = false) {
|
||||
async function refresh(force = false) {
|
||||
if (!authenticated) return;
|
||||
if (force) {
|
||||
const tasks = [rustProxyRefresh.manual("status-full")];
|
||||
if (includeLogs) tasks.push(rustProxyRefresh.manual("logs-fallback"));
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
if (force) return rustProxyRefresh.manual("status-full");
|
||||
return rustProxyRefresh.manual("status-fallback");
|
||||
}
|
||||
function renderLiveStatus(value) { renderStatus(value); }
|
||||
@@ -399,15 +396,16 @@ function startLive() {
|
||||
if (liveEvents && liveInterval === seconds) return;
|
||||
stopLive();
|
||||
liveInterval = seconds;
|
||||
const epoch = authEpoch, stream = new EventSource(`/api/events?interval=${seconds}`);
|
||||
const epoch = authEpoch, stream = new EventSource(`/api/events?interval=${seconds}&logs=false`);
|
||||
liveEvents = stream;
|
||||
stream.addEventListener("snapshot", event => {
|
||||
if (!authenticated || epoch !== authEpoch || stream !== liveEvents) return;
|
||||
try {
|
||||
const payload = JSON.parse(event.data);
|
||||
if (!payload.status || !payload.logs) throw new Error("Invalid SSE payload");
|
||||
if (!payload.status) throw new Error("Invalid SSE payload");
|
||||
liveConnected = true; liveFallback = false;
|
||||
renderLiveStatus(payload.status); renderLogs(payload.logs);
|
||||
renderLiveStatus(payload.status);
|
||||
if (payload.logs) renderLogs(payload.logs);
|
||||
window.rustProxyRefresh?.sync();
|
||||
} catch (error) { liveConnected = false; liveFallback = true; window.rustProxyRefresh?.sync(); }
|
||||
});
|
||||
@@ -529,7 +527,7 @@ async function activate(info) {
|
||||
void window.rustProxyApiTokens?.refresh?.();
|
||||
window.rustProxySettings?.scrollToApiTokens?.();
|
||||
}
|
||||
void refresh(true, true).catch(error => {
|
||||
void refresh(true).catch(error => {
|
||||
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
||||
});
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
const UI_ICON_NS = "http://www.w3.org/2000/svg";
|
||||
const UI_ICON_SHAPES = Object.freeze({
|
||||
activity: [["path", { d: "M3 12h4l2-6 4 12 2-6h6" }]],
|
||||
alert: [["path", { d: "M12 3 2.8 20h18.4L12 3Z" }], ["path", { d: "M12 9v4" }], ["circle", { cx: "12", cy: "17", r: ".7", fill: "currentColor", stroke: "none" }]],
|
||||
archive: [["rect", { x: "3", y: "5", width: "18", height: "4", rx: "1" }], ["path", { d: "M5 9v10h14V9M9 13h6" }]],
|
||||
book: [["path", { d: "M4 4.5h5.5A2.5 2.5 0 0 1 12 7v13a3 3 0 0 0-3-3H4V4.5ZM20 4.5h-5.5A2.5 2.5 0 0 0 12 7v13a3 3 0 0 1 3-3h5V4.5Z" }]],
|
||||
box: [["path", { d: "m12 3 8 4.5v9L12 21l-8-4.5v-9L12 3Z" }], ["path", { d: "m4 7.5 8 4.5 8-4.5M12 12v9" }]],
|
||||
certificate: [["rect", { x: "4", y: "3", width: "16", height: "14", rx: "2" }], ["path", { d: "M8 7h8M8 10h5M9 17l-1 4 4-2 4 2-1-4" }]],
|
||||
check: [["path", { d: "m5 12 4 4L19 6" }]],
|
||||
chevronDown: [["path", { d: "m6 9 6 6 6-6" }]],
|
||||
chevronLeft: [["path", { d: "m15 6-6 6 6 6" }]],
|
||||
chevronRight: [["path", { d: "m9 6 6 6-6 6" }]],
|
||||
chevronUp: [["path", { d: "m6 15 6-6 6 6" }]],
|
||||
code: [["path", { d: "m9 7-5 5 5 5M15 7l5 5-5 5M13 5l-2 14" }]],
|
||||
command: [["rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" }], ["path", { d: "M8 12h8M12 8v8" }]],
|
||||
copy: [["rect", { x: "8", y: "8", width: "11", height: "11", rx: "2" }], ["path", { d: "M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" }]],
|
||||
cpu: [["rect", { x: "7", y: "7", width: "10", height: "10", rx: "2" }], ["path", { d: "M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4M10 10h4v4h-4z" }]],
|
||||
database: [["ellipse", { cx: "12", cy: "5", rx: "7", ry: "3" }], ["path", { d: "M5 5v7c0 1.7 3.1 3 7 3s7-1.3 7-3V5M5 12v7c0 1.7 3.1 3 7 3s7-1.3 7-3v-7" }]],
|
||||
download: [["path", { d: "M12 3v12m0 0-4-4m4 4 4-4M4 19h16" }]],
|
||||
edit: [["path", { d: "M4 20h4l11-11-4-4L4 16v4ZM13.5 6.5l4 4" }]],
|
||||
external: [["path", { d: "M14 4h6v6M20 4l-9 9" }], ["path", { d: "M18 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6" }]],
|
||||
eye: [["path", { d: "M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6S2.5 12 2.5 12Z" }], ["circle", { cx: "12", cy: "12", r: "2.5" }]],
|
||||
eyeOff: [["path", { d: "M3 3l18 18M10.6 6.2A10 10 0 0 1 12 6c6 0 9.5 6 9.5 6a15 15 0 0 1-3 3.6M6.1 6.2C3.8 8 2.5 12 2.5 12s3.5 6 9.5 6a10 10 0 0 0 3.2-.5M9.9 9.9a3 3 0 0 0 4.2 4.2" }]],
|
||||
file: [["path", { d: "M6 3h8l4 4v14H6V3Z" }], ["path", { d: "M14 3v5h5" }]],
|
||||
fileX: [["path", { d: "M6 3h8l4 4v14H6V3ZM14 3v5h5M9 13l6 6M15 13l-6 6" }]],
|
||||
filter: [["path", { d: "M3 5h18l-7 8v6l-4 2v-8L3 5Z" }]],
|
||||
globe: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "M3 12h18M12 3c2.5 2.7 3.5 5.7 3.5 9S14.5 18.3 12 21M12 3C9.5 5.7 8.5 8.7 8.5 12S9.5 18.3 12 21" }]],
|
||||
grid: [["rect", { x: "4", y: "4", width: "6", height: "6", rx: "1" }], ["rect", { x: "14", y: "4", width: "6", height: "6", rx: "1" }], ["rect", { x: "4", y: "14", width: "6", height: "6", rx: "1" }], ["rect", { x: "14", y: "14", width: "6", height: "6", rx: "1" }]],
|
||||
history: [["path", { d: "M4 8V4m0 0h4M4 4l3 3a8 8 0 1 1-2.2 7" }], ["path", { d: "M12 8v5l3 2" }]],
|
||||
key: [["circle", { cx: "8", cy: "12", r: "4" }], ["path", { d: "M12 12h9M18 12v3M15 12v2" }]],
|
||||
list: [["path", { d: "M9 6h11M9 12h11M9 18h11" }], ["circle", { cx: "4.5", cy: "6", r: ".8", fill: "currentColor", stroke: "none" }], ["circle", { cx: "4.5", cy: "12", r: ".8", fill: "currentColor", stroke: "none" }], ["circle", { cx: "4.5", cy: "18", r: ".8", fill: "currentColor", stroke: "none" }]],
|
||||
loader: [["path", { d: "M20 12a8 8 0 1 1-2.3-5.7" }]],
|
||||
lock: [["rect", { x: "5", y: "10", width: "14", height: "10", rx: "2" }], ["path", { d: "M8 10V7a4 4 0 0 1 8 0v3" }]],
|
||||
login: [["path", { d: "M10 5H5v14h5M14 8l4 4-4 4M8 12h10" }]],
|
||||
logout: [["path", { d: "M14 5h5v14h-5M10 8l-4 4 4 4M16 12H6" }]],
|
||||
menu: [["path", { d: "M4 7h16M4 12h16M4 17h16" }]],
|
||||
network: [["rect", { x: "9", y: "3", width: "6", height: "5", rx: "1" }], ["rect", { x: "3", y: "16", width: "6", height: "5", rx: "1" }], ["rect", { x: "15", y: "16", width: "6", height: "5", rx: "1" }], ["path", { d: "M12 8v4M6 16v-4h12v4" }]],
|
||||
palette: [["path", { d: "M12 3a9 9 0 1 0 0 18h1.5a2 2 0 0 0 0-4H12a2 2 0 0 1 0-4h4a5 5 0 0 0 0-10h-4Z" }], ["circle", { cx: "7", cy: "10", r: "1", fill: "currentColor", stroke: "none" }], ["circle", { cx: "9", cy: "6.5", r: "1", fill: "currentColor", stroke: "none" }], ["circle", { cx: "14", cy: "6", r: "1", fill: "currentColor", stroke: "none" }]],
|
||||
pause: [["path", { d: "M8 5v14M16 5v14" }]],
|
||||
pin: [["path", { d: "m9 3 6 6-2 2 4 4-2 2-4-4-2 2-6-6 2-2 4 4 2-2-4-4 2-2ZM5 19l4-4" }]],
|
||||
play: [["path", { d: "m8 5 11 7-11 7V5Z" }]],
|
||||
plus: [["path", { d: "M12 5v14M5 12h14" }]],
|
||||
refresh: [["path", { d: "M20 7v5h-5M4 17v-5h5" }], ["path", { d: "M18.5 9A7 7 0 0 0 6.2 6.2L4 8M5.5 15A7 7 0 0 0 17.8 17.8L20 16" }]],
|
||||
route: [["circle", { cx: "6", cy: "6", r: "2" }], ["circle", { cx: "18", cy: "18", r: "2" }], ["path", { d: "M8 6h4a4 4 0 0 1 4 4v0a4 4 0 0 1-4 4H8a2 2 0 0 0-2 2v0" }]],
|
||||
save: [["path", { d: "M5 4h12l2 2v14H5V4Z" }], ["path", { d: "M8 4v6h8V4M8 20v-6h8v6" }]],
|
||||
search: [["circle", { cx: "10.5", cy: "10.5", r: "6.5" }], ["path", { d: "m15.5 15.5 5 5" }]],
|
||||
settings: [["circle", { cx: "12", cy: "12", r: "3" }], ["path", { d: "M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6 7 7M17 17l1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4" }]],
|
||||
shield: [["path", { d: "M12 3 20 6v6c0 5-3.3 8-8 9-4.7-1-8-4-8-9V6l8-3Z" }], ["path", { d: "m8.5 12 2.2 2.2 4.8-5" }]],
|
||||
sliders: [["path", { d: "M4 7h6M14 7h6M4 17h10M18 17h2M10 4v6M14 14v6" }]],
|
||||
sortDown: [["path", { d: "M8 5v14M5 16l3 3 3-3M14 8h5M14 12h4M14 16h3" }]],
|
||||
sortUp: [["path", { d: "M8 19V5M5 8l3-3 3 3M14 8h3M14 12h4M14 16h5" }]],
|
||||
trash: [["path", { d: "M4 7h16M9 3h6l1 4H8l1-4ZM7 7l1 14h8l1-14M10 11v6M14 11v6" }]],
|
||||
undo: [["path", { d: "M9 7H4v-5M4 7l4-4M4.5 8A8 8 0 1 1 5 17" }]],
|
||||
upload: [["path", { d: "M12 16V4m0 0-4 4m4-4 4 4M4 20h16" }]],
|
||||
users: [["circle", { cx: "9", cy: "9", r: "3" }], ["path", { d: "M3 20v-2a5 5 0 0 1 10 0v2M16 8a3 3 0 0 1 0 6M16 15a5 5 0 0 1 5 5" }]],
|
||||
wand: [["path", { d: "m4 20 12-12 3 3L7 23 4 20ZM14 4l1-3 1 3 3 1-3 1-1 3-1-3-3-1 3-1ZM19 14l.7-2 .8 2 2 .8-2 .7-.8 2-.7-2-2-.7 2-.8Z" }]],
|
||||
x: [["path", { d: "M6 6l12 12M18 6 6 18" }]],
|
||||
});
|
||||
|
||||
const UI_PANEL_ICONS = Object.freeze({
|
||||
overview: "grid", vhosts: "network", "web-server": "globe", "docker-vhosts": "box",
|
||||
redirects: "route", notfound: "fileX", streams: "network", "acl-profiles": "shield",
|
||||
tls: "certificate", traffic: "activity", cache: "database", transform: "wand", settings: "settings",
|
||||
});
|
||||
const UI_SETTINGS_TAB_ICONS = Object.freeze({
|
||||
appearance: "palette", listeners: "network", connections: "globe", "docker-agents": "box",
|
||||
policies: "shield", cache: "database", transform: "wand", forwarding: "route", pages: "file",
|
||||
logging: "list", geoip: "globe", prometheus: "activity", administration: "settings",
|
||||
"system-info": "cpu", "certificate-api": "key", "tls-policy": "lock", "manual-config": "code", security: "shield",
|
||||
});
|
||||
const UI_VHOST_TAB_ICONS = Object.freeze({
|
||||
details: "sliders", backends: "network", health: "activity", locations: "route", auth: "key", ssl: "lock",
|
||||
cache: "database", compression: "archive", transform: "wand", security: "shield", pages: "file", advanced: "code",
|
||||
});
|
||||
const UI_TRANSLATED_ICON_KEYS = Object.freeze([
|
||||
["trash", ["remove", "delete", "draftDiscard", "cacheRemoveBan", "certificateApiRevokeAll", "revokeAll"]],
|
||||
["x", ["close", "cancelAction", "overviewLayoutInlineCancel"]],
|
||||
["save", ["saveCertificate", "savePassword", "dashboardPresetsSave", "overviewLayoutInlineSave"]],
|
||||
["check", ["apply", "applyFilters", "savePolicy", "continueAction", "webServicePresetApply", "dockerApplyService"]],
|
||||
["plus", ["addCertificate", "vhNew", "webServiceNew", "redirectNew", "notfoundNew", "streamNew", "aclProfileNew", "phpProfileAdd", "redirectAddRule", "streamAddListener", "aclProfileAddIpException", "apiTokenCreate"]],
|
||||
["refresh", ["refresh", "refreshNow", "reload", "reloadAudit", "reloadCertificates", "tunnelsReload", "dockerRefresh", "cacheRefresh", "sessionRetry"]],
|
||||
["undo", ["configHistoryRestoreAction", "dashboardPresetsFactory", "draftRestore"]],
|
||||
["edit", ["edit"]],
|
||||
["download", ["export"]],
|
||||
["upload", ["acmeImport", "aclProfileImportRules"]],
|
||||
["eye", ["certificateReadNames", "dockerShowHidden", "dashboardWidgetsShowAll"]],
|
||||
["settings", ["settings", "vhManage", "dockerAgentsSettings"]],
|
||||
["book", ["webServiceHtaccessDocs", "webServicePhpDocsButton"]],
|
||||
["pause", ["logPause"]],
|
||||
["pin", ["logPin"]],
|
||||
["history", ["configHistory"]],
|
||||
["chevronLeft", ["previous"]],
|
||||
["chevronRight", ["next"]],
|
||||
]);
|
||||
|
||||
function uiIconSvg(name) {
|
||||
const definition = UI_ICON_SHAPES[name] || UI_ICON_SHAPES.command;
|
||||
const svg = document.createElementNS(UI_ICON_NS, "svg");
|
||||
svg.classList.add("ui-icon"); svg.dataset.uiIcon = name; svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false");
|
||||
svg.setAttribute("fill", "none"); svg.setAttribute("stroke", "currentColor"); svg.setAttribute("stroke-width", "1.8");
|
||||
svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round");
|
||||
for (const [tag, attrs] of definition) {
|
||||
const node = document.createElementNS(UI_ICON_NS, tag);
|
||||
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
||||
svg.append(node);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
function uiIconStrings(target) {
|
||||
return [target.textContent, target.getAttribute("aria-label"), target.title].filter(Boolean).map(value => String(value).trim().toLocaleLowerCase());
|
||||
}
|
||||
function uiIconMatchesTranslation(target, keys) {
|
||||
if (typeof t !== "function") return false;
|
||||
const strings = uiIconStrings(target); if (!strings.length) return false;
|
||||
for (const key of keys) {
|
||||
const translated = String(t(key) || "").trim().toLocaleLowerCase();
|
||||
if (!translated || translated === key.toLocaleLowerCase()) continue;
|
||||
if (strings.some(value => value === translated || value.startsWith(`${translated} `) || value.startsWith(`${translated}:`))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function uiIconForTarget(target) {
|
||||
const manual = target.dataset.uiIconName;
|
||||
if (manual === "none") return null;
|
||||
if (manual && UI_ICON_SHAPES[manual]) return manual;
|
||||
if (target.matches(".chart-style-choice")) return null;
|
||||
if (target.matches(".overview-inline-grip, .overview-inline-narrow, .overview-inline-widen, .overview-inline-up, .overview-inline-down, .overview-inline-hide, .overview-layout-narrow, .overview-layout-widen, .overview-layout-up, .overview-layout-down, .overview-layout-hide")) return null;
|
||||
if (target.matches("#tunnels-open")) return null;
|
||||
if (target.matches("#web-service-preset-apply")) return "wand";
|
||||
if (target.matches(".listener-h2-settings-link, .listener-h3-settings-link")) return "settings";
|
||||
if (target.dataset.panel && UI_PANEL_ICONS[target.dataset.panel]) return UI_PANEL_ICONS[target.dataset.panel];
|
||||
if (target.dataset.settingsTab && UI_SETTINGS_TAB_ICONS[target.dataset.settingsTab]) return UI_SETTINGS_TAB_ICONS[target.dataset.settingsTab];
|
||||
if (target.dataset.tab && UI_VHOST_TAB_ICONS[target.dataset.tab]) return UI_VHOST_TAB_ICONS[target.dataset.tab];
|
||||
if (target.dataset.certificateTab) return ({ installed: "certificate", renewal: "refresh", generate: "plus" })[target.dataset.certificateTab] || "certificate";
|
||||
if (target.dataset.trafficTab) return ({ overview: "grid", clients: "users", requests: "list", delivery: "activity" })[target.dataset.trafficTab] || "activity";
|
||||
if (target.dataset.httpSettingsTab) return "network";
|
||||
if (target.dataset.policySettingsTab) return target.dataset.policySettingsTab === "headers" || target.dataset.policySettingsTab === "response" ? "list" : "shield";
|
||||
if (target.dataset.cacheSettingsTab) return ({ general: "database", resilience: "shield", cold: "archive", diagnostics: "activity" })[target.dataset.cacheSettingsTab] || "database";
|
||||
if (target.dataset.loggingSettingsTab) return ({ outputs: "list", format: "code", errors: "alert" })[target.dataset.loggingSettingsTab] || "list";
|
||||
if (target.dataset.dashboardWidgetAction === "toggle") return target.classList.contains("is-hidden") ? "eyeOff" : "eye";
|
||||
if (target.dataset.dashboardWidgetAction === "up") return "chevronLeft";
|
||||
if (target.dataset.dashboardWidgetAction === "down") return "chevronRight";
|
||||
|
||||
const signature = [target.id, target.dataset.i18n, target.dataset.i18nAria, target.dataset.action, target.className]
|
||||
.filter(Boolean).join(" ").toLocaleLowerCase();
|
||||
const aria = String(target.getAttribute("aria-label") || "").toLocaleLowerCase();
|
||||
if (/mobile-menu|menu-toggle/.test(signature)) return "menu";
|
||||
if (/is-saving|saving/.test(signature)) return "loader";
|
||||
if (/sort-direction|sort-button/.test(signature)) return /descending|desc/.test(aria) ? "sortDown" : "sortUp";
|
||||
if (/scroll-prev|move-earlier|layout-up/.test(signature)) return "chevronLeft";
|
||||
if (/scroll-next|move-later|layout-down/.test(signature)) return "chevronRight";
|
||||
if (/collapse|narrow/.test(signature)) return "chevronDown";
|
||||
if (/expand|widen/.test(signature)) return "chevronUp";
|
||||
if (/health/.test(signature) && !/certificate/.test(signature)) return "activity";
|
||||
if (/copy/.test(signature)) return "copy";
|
||||
if (/export|download/.test(signature)) return "download";
|
||||
if (/import|upload/.test(signature)) return "upload";
|
||||
if (/search|find/.test(signature)) return "search";
|
||||
if (/filter/.test(signature)) return "filter";
|
||||
if (/inspect|preview|read-names|readnames|show-hidden|visibility|view-toggle/.test(signature)) return "eye";
|
||||
if (/history|revision/.test(signature)) return "history";
|
||||
if (/restore|factory/.test(signature)) return "undo";
|
||||
if (/refresh|reload|retry|renew|update-now|updatenow/.test(signature)) return "refresh";
|
||||
if (/reset/.test(signature)) return "undo";
|
||||
if (/revoke|remove|delete|discard|purge|kill/.test(signature)) return "trash";
|
||||
if (/close|cancel/.test(signature)) return "x";
|
||||
if (/new|add|create|generate|issue/.test(signature)) return "plus";
|
||||
if (/save/.test(signature)) return "save";
|
||||
if (/apply|submit|continue/.test(signature)) return "check";
|
||||
if (/edit/.test(signature)) return "edit";
|
||||
if (/pause/.test(signature)) return "pause";
|
||||
if (/pin|dock/.test(signature)) return "pin";
|
||||
if (/start|run-operation|cleanup-start/.test(signature)) return "play";
|
||||
if (/logout|sign-out|lock/.test(signature)) return "logout";
|
||||
if (/login|sign-in/.test(signature)) return "login";
|
||||
if (/certificate-api|api-token|token|secret|key/.test(signature)) return "key";
|
||||
if (/certificate|acme|tls/.test(signature)) return "certificate";
|
||||
if (/security|acl|policy|audit/.test(signature)) return "shield";
|
||||
if (/settings|manage|configuration/.test(signature)) return "settings";
|
||||
if (/docker/.test(signature)) return "box";
|
||||
if (/vhost|backend|stream|tunnel|listener/.test(signature)) return "network";
|
||||
if (/redirect|forward/.test(signature)) return "route";
|
||||
if (/notfound|404/.test(signature)) return "fileX";
|
||||
if (/cache|cold/.test(signature)) return "database";
|
||||
if (/transform|minif/.test(signature)) return "wand";
|
||||
if (/traffic|metric|prometheus/.test(signature)) return "activity";
|
||||
if (/log/.test(signature)) return "list";
|
||||
if (/docs|help|documentation/.test(signature)) return "book";
|
||||
if (/php|json|advanced|code/.test(signature)) return "code";
|
||||
if (/layout|preset|dashboard|overview/.test(signature)) return "grid";
|
||||
if (/about|info/.test(signature)) return "book";
|
||||
if (target.matches("a[href]")) return target.href?.startsWith(location.origin) ? "file" : "external";
|
||||
for (const [icon, keys] of UI_TRANSLATED_ICON_KEYS) if (uiIconMatchesTranslation(target, keys)) return icon;
|
||||
if (aria && typeof t === "function") {
|
||||
if (aria.startsWith(String(t("remove")).toLocaleLowerCase())) return "trash";
|
||||
if (aria.startsWith(String(t("copyValue")).toLocaleLowerCase())) return "copy";
|
||||
if (aria === String(t("dashboardWidgetsMoveEarlier")).toLocaleLowerCase()) return "chevronLeft";
|
||||
if (aria === String(t("dashboardWidgetsMoveLater")).toLocaleLowerCase()) return "chevronRight";
|
||||
}
|
||||
return "command";
|
||||
}
|
||||
const UI_ICON_TARGET_SELECTOR = "button, a.primary, a.outline, a.quiet, #logs-menu > summary";
|
||||
function uiIconTargetIsEligible(target) {
|
||||
return target instanceof Element && target.matches(UI_ICON_TARGET_SELECTOR);
|
||||
}
|
||||
function uiDecorateIconTarget(target) {
|
||||
if (!uiIconTargetIsEligible(target)) return;
|
||||
const existing = target.querySelector(":scope > svg.ui-icon");
|
||||
if (document.documentElement.dataset.interfaceIcons !== "true") { existing?.remove(); return; }
|
||||
const name = uiIconForTarget(target);
|
||||
if (!name) { existing?.remove(); return; }
|
||||
if (existing?.dataset.uiIcon === name) return;
|
||||
existing?.remove(); target.prepend(uiIconSvg(name));
|
||||
}
|
||||
function refreshUiIcons(root = document) {
|
||||
if (root instanceof Element && uiIconTargetIsEligible(root)) uiDecorateIconTarget(root);
|
||||
root.querySelectorAll?.(UI_ICON_TARGET_SELECTOR).forEach(uiDecorateIconTarget);
|
||||
}
|
||||
let uiIconObserverActive = false;
|
||||
function setUiIconsEnabled(value) {
|
||||
const enabled = value === true;
|
||||
document.documentElement.dataset.interfaceIcons = String(enabled);
|
||||
if (enabled) {
|
||||
if (!uiIconObserverActive) {
|
||||
uiIconObserver.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "aria-label", "aria-expanded", "aria-pressed", "data-i18n", "data-action"] });
|
||||
uiIconObserverActive = true;
|
||||
}
|
||||
refreshUiIcons();
|
||||
} else {
|
||||
if (uiIconObserverActive) { uiIconObserver.disconnect(); uiIconObserverActive = false; }
|
||||
document.querySelectorAll("svg.ui-icon").forEach(node => node.remove());
|
||||
}
|
||||
const input = $("ui-interface-icons"); if (input) input.checked = enabled;
|
||||
return enabled;
|
||||
}
|
||||
const uiIconObserver = new MutationObserver(records => {
|
||||
if (document.documentElement.dataset.interfaceIcons !== "true") return;
|
||||
const targets = new Set();
|
||||
for (const record of records) {
|
||||
const element = record.target instanceof Element ? record.target : record.target.parentElement;
|
||||
const action = element?.closest?.(UI_ICON_TARGET_SELECTOR); if (action) targets.add(action);
|
||||
for (const node of record.addedNodes) {
|
||||
if (!(node instanceof Element)) continue;
|
||||
if (uiIconTargetIsEligible(node)) targets.add(node);
|
||||
node.querySelectorAll?.(UI_ICON_TARGET_SELECTOR).forEach(item => targets.add(item));
|
||||
}
|
||||
}
|
||||
targets.forEach(uiDecorateIconTarget);
|
||||
});
|
||||
window.rustProxyIcons = { refresh: refreshUiIcons, setEnabled: setUiIconsEnabled, iconFor: uiIconForTarget };
|
||||
@@ -219,7 +219,11 @@ window.rustProxyExtraOverview = (() => {
|
||||
donut("extra-cache-ratio", slices?.[0], slices?.[1]);
|
||||
}
|
||||
const detail = $(`overview-extra-${id}-detail`);
|
||||
if (detail && id !== "extra_cache_hit_rate") detail.textContent = metric.detail || t("overviewExtraRecentSamples");
|
||||
if (detail && id !== "extra_cache_hit_rate") {
|
||||
const detailText = metric.detail || "";
|
||||
detail.textContent = detailText;
|
||||
detail.hidden = !detailText;
|
||||
}
|
||||
}
|
||||
}
|
||||
function reset() { histories.clear(); lastCounter.clear(); lastValue.clear(); lastUnits.clear(); lastSegments.clear(); window.rustProxyChartScale.reset("extra:"); }
|
||||
|
||||
@@ -13,7 +13,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
||||
])]);
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
||||
chart_axes: false,
|
||||
chart_axes: false, interface_icons: false,
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null,
|
||||
});
|
||||
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
|
||||
@@ -123,6 +123,7 @@ function applyAdminChartAxes(value) {
|
||||
window.rustProxyChartScale?.refreshAxes?.();
|
||||
return enabled;
|
||||
}
|
||||
function applyAdminInterfaceIcons(value) { return window.rustProxyIcons?.setEnabled?.(value === true) ?? false; }
|
||||
function applyAdminButtonStyle(value) {
|
||||
const style = normalizeAdminButtonStyle(value); document.documentElement.dataset.buttonStyle = style;
|
||||
const select = $("ui-button-style"); if (select) select.value = style; return style;
|
||||
@@ -287,7 +288,7 @@ function applyAdminPreferences(value, cache = true) {
|
||||
...value,
|
||||
refresh: normalizeRefreshPreferences(value.refresh),
|
||||
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), interface_icons: applyAdminInterfaceIcons(value.interface_icons), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
@@ -335,7 +336,7 @@ function updateAdminPreferences(patch) {
|
||||
const next = { ...current, ...patch };
|
||||
next.refresh = normalizeRefreshPreferences(next.refresh);
|
||||
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.interface_icons = next.interface_icons === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes);
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
@@ -343,7 +344,7 @@ function updateAdminPreferences(patch) {
|
||||
});
|
||||
adminPreferencesWrite = task.catch(() => undefined); return task;
|
||||
}
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); window.rustProxyRefresh?.update(REFRESH_DEFAULTS); }
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); applyAdminInterfaceIcons(false); window.rustProxyRefresh?.update(REFRESH_DEFAULTS); }
|
||||
function renderRefreshPreferences(value = REFRESH_DEFAULTS) {
|
||||
const refresh = normalizeRefreshPreferences(value);
|
||||
const switcher = $("refresh-live-enabled"); if (switcher) switcher.checked = refresh.live_enabled;
|
||||
@@ -419,6 +420,14 @@ $("ui-chart-axes")?.addEventListener("change", () => {
|
||||
applyAdminChartAxes(previous);
|
||||
});
|
||||
});
|
||||
$("ui-interface-icons")?.addEventListener("change", () => {
|
||||
const control = $("ui-interface-icons"), previous = adminPreferences?.interface_icons === true, next = control.checked;
|
||||
applyAdminInterfaceIcons(next); if (adminPreferences) adminPreferences.interface_icons = next;
|
||||
void saveAppearancePreference({ interface_icons: next }, control, () => {
|
||||
if (adminPreferences) adminPreferences.interface_icons = previous;
|
||||
applyAdminInterfaceIcons(previous);
|
||||
});
|
||||
});
|
||||
$("ui-button-style")?.addEventListener("change", () => {
|
||||
const control = $("ui-button-style"), previous = adminPreferences?.button_style || "normal", next = normalizeAdminButtonStyle(control.value);
|
||||
applyAdminButtonStyle(next); if (adminPreferences) adminPreferences.button_style = next; void saveAppearancePreference({ button_style: next }, control, () => { if (adminPreferences) adminPreferences.button_style = previous; applyAdminButtonStyle(previous); });
|
||||
@@ -450,7 +459,7 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
|
||||
}
|
||||
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
|
||||
const patches = {
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", interface_icons: false, button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
||||
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
|
||||
@@ -170,7 +170,7 @@ const rustProxyRefresh = (() => {
|
||||
}, onError: error => { if (error?.status !== 401) { connected = false; $("connection-label").textContent = t("disconnected"); $("signal").classList.add("status-error"); $("app-health-slot").hidden = false; renderAppHealth(null); } }
|
||||
});
|
||||
register("logs-fallback", {
|
||||
interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
|
||||
interval: () => options.overview_seconds, active: () => !!window.rustProxyLogs?.active?.(), run: async ({ signal, isCurrent }) => {
|
||||
const value = await api("/api/logs", { signal });
|
||||
if (isCurrent()) renderLogs(value);
|
||||
}
|
||||
@@ -199,6 +199,6 @@ const rustProxyRefresh = (() => {
|
||||
window.rustProxyRefresh = rustProxyRefresh;
|
||||
|
||||
$("overview-refresh")?.addEventListener("click", () => {
|
||||
void Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("logs-fallback"), rustProxyRefresh.manual("system-info")]);
|
||||
void Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("system-info")]);
|
||||
});
|
||||
$("system-info-refresh")?.addEventListener("click", () => void rustProxyRefresh.manual("system-info"));
|
||||
@@ -87,6 +87,53 @@ button:disabled {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.ui-icon {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-inline-end: 6px;
|
||||
vertical-align: -2px;
|
||||
flex: 0 0 auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html[data-interface-icons="true"] svg.ui-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.nav>.ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
html[data-interface-icons="true"] .nav>svg.ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.backend-sort-direction>.ui-icon,
|
||||
.dashboard-widget-scroll-button>.ui-icon,
|
||||
.host-chip button>.ui-icon,
|
||||
.acl-picker-remove>.ui-icon,
|
||||
.draft-dock-remove>.ui-icon,
|
||||
.vhost-backend-popup-close>.ui-icon,
|
||||
.dashboard-widget-toggle>.ui-icon {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
button.is-saving>.ui-icon[data-ui-icon="loader"] {
|
||||
animation: ui-icon-spin .8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ui-icon-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
html[data-interface-icons="true"] .nav>span:first-of-type,
|
||||
[hidden],
|
||||
#file-log-view-counts[hidden],
|
||||
#file-log-view-entries[hidden],
|
||||
|
||||
@@ -15,8 +15,6 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
margin-bottom: var(--ui-gap);
|
||||
}
|
||||
|
||||
/* Summary tiles outside the Overview dashboard (Cache, Traffic, Transform) fill
|
||||
every row instead of leaving empty grid columns after the last tile. */
|
||||
.stats-grid.cache-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -86,7 +84,6 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
stroke-dasharray: var(--chart-grid-dash, 2 5);
|
||||
}
|
||||
|
||||
/* Axis labels are HTML overlays, not stretched SVG text. */
|
||||
.rp-chart-axis-host {
|
||||
position: relative;
|
||||
display: flow-root;
|
||||
@@ -94,7 +91,10 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rp-chart-axis-host>svg { display: block; vertical-align: top; }
|
||||
.rp-chart-axis-host>svg {
|
||||
display: block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.rp-chart-axis-y {
|
||||
position: absolute;
|
||||
@@ -115,9 +115,20 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
background: color-mix(in srgb, var(--panel) 82%, transparent);
|
||||
}
|
||||
|
||||
.rp-chart-axis-top { top: var(--rp-axis-top, 8%); transform: translateY(-30%); }
|
||||
.rp-chart-axis-middle { top: 50%; transform: translateY(-50%); }
|
||||
.rp-chart-axis-bottom { top: var(--rp-axis-bottom, 92%); transform: translateY(-70%); }
|
||||
.rp-chart-axis-top {
|
||||
top: var(--rp-axis-top, 8%);
|
||||
transform: translateY(-30%);
|
||||
}
|
||||
|
||||
.rp-chart-axis-middle {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.rp-chart-axis-bottom {
|
||||
top: var(--rp-axis-bottom, 92%);
|
||||
transform: translateY(-70%);
|
||||
}
|
||||
|
||||
.rp-chart-axis-x {
|
||||
display: grid;
|
||||
@@ -133,12 +144,29 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rp-chart-axis-x .rp-chart-axis-start { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.rp-chart-axis-x .rp-chart-axis-end { min-width: 0; text-align: right; }
|
||||
.rp-chart-axis-x .rp-chart-axis-start {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rp-chart-axis-x .rp-chart-axis-end {
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rp-chart-axis-compact .rp-chart-axis-y,
|
||||
.rp-chart-axis-multiple .rp-chart-axis-y,
|
||||
.rp-chart-axis-series[hidden] { display: none; visibility: hidden; }
|
||||
.rp-chart-axis-compact .rp-chart-axis-x { font-size: 8px; gap: 2px; }
|
||||
.rp-chart-axis-series[hidden] {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.rp-chart-axis-compact .rp-chart-axis-x {
|
||||
font-size: 8px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.rp-chart-axis-series {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -150,8 +178,13 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.rp-chart-axis-y .rp-chart-axis-middle { display: none; }
|
||||
.rp-chart-axis-x { font-size: 8px; }
|
||||
.rp-chart-axis-y .rp-chart-axis-middle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rp-chart-axis-x {
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-footer {
|
||||
@@ -291,13 +324,19 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
min-width: 640px;
|
||||
}
|
||||
|
||||
.pool-table td:nth-child(n/**/+4) {
|
||||
.pool-table td:nth-child(n
|
||||
|
||||
/**/
|
||||
+4) {
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir=rtl] table,
|
||||
.pool-table th:nth-child(n/**/+4),
|
||||
.pool-table th:nth-child(n
|
||||
|
||||
/**/
|
||||
+4),
|
||||
.logging-builder-action .small-label {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -561,7 +600,6 @@ body[data-density=compact] .pool-card .table-scroll {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Keep the block title clear of the corner button whatever padding the grid applies. */
|
||||
#panel-overview .stat-with-action>span:first-child {
|
||||
display: block;
|
||||
padding-right: 104px;
|
||||
@@ -576,6 +614,20 @@ body[data-density=compact] .pool-card .table-scroll {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#panel-overview .stat-with-action .stat-corner-badge {
|
||||
min-height: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#panel-overview .stat-with-action .stat-corner-badge:hover,
|
||||
#panel-overview .stat-with-action .stat-corner-badge:focus-visible {
|
||||
border-color: var(--accent);
|
||||
background: var(--notice-bg);
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked .stat-with-action .stat-corner-action {
|
||||
top: 44px;
|
||||
}
|
||||
@@ -1201,7 +1253,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
/* While sorting, the grid keeps strict DOM order: the placeholder is exactly where the block lands. */
|
||||
#panel-overview .dashboard-overview-grid.is-overview-sorting {
|
||||
grid-auto-flow: row !important;
|
||||
user-select: none;
|
||||
@@ -1216,10 +1267,21 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="3"] { grid-column: auto/span 3; }
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="4"] { grid-column: auto/span 4; }
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="6"] { grid-column: auto/span 6; }
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="12"] { grid-column: 1/-1; }
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="3"] {
|
||||
grid-column: auto/span 3;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="4"] {
|
||||
grid-column: auto/span 4;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="6"] {
|
||||
grid-column: auto/span 6;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>.overview-drop-placeholder[data-dashboard-span="12"] {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>.is-overview-lifted,
|
||||
#panel-overview .dashboard-overview-grid>.is-overview-lifted * {
|
||||
@@ -1256,7 +1318,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Narrow blocks keep only the essentials; arrow keys on the grip still move them. */
|
||||
#panel-overview .dashboard-overview-grid>[data-dashboard-span="2"] .overview-inline-toolbar :is(.overview-widget-type, .overview-inline-up, .overview-inline-down),
|
||||
#panel-overview .dashboard-overview-grid>[data-dashboard-span="3"] .overview-inline-toolbar :is(.overview-inline-up, .overview-inline-down) {
|
||||
display: none;
|
||||
@@ -1886,8 +1947,6 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
margin: 4px 10px 8px;
|
||||
}
|
||||
|
||||
/* Custom layouts follow the saved order exactly (no implicit back-filling of gaps);
|
||||
"Fill gaps" in the layout editor compacts explicitly. */
|
||||
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom] {
|
||||
grid-auto-flow: row;
|
||||
}
|
||||
@@ -2224,7 +2283,6 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Live values stay on one line so refreshes do not change card heights. */
|
||||
.mini-live-chart-head> :last-child,
|
||||
.network-interface-row-head .network-interface-head-actions {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -81,11 +81,13 @@
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.appearance-interface-icons-setting,
|
||||
.appearance-chart-axes-setting {
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.appearance-interface-icons-setting>span,
|
||||
.appearance-chart-axes-setting>span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
@@ -1203,6 +1205,7 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Global security policy forms */
|
||||
.global-policy-form .global-policy-section {
|
||||
display: grid;
|
||||
@@ -1212,18 +1215,81 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
padding-top: 17px;
|
||||
min-width: 0;
|
||||
}
|
||||
.global-policy-form .global-policy-section h3 { margin: 0; font-size: 14px; }
|
||||
.global-policy-heading { display: grid; gap: 5px; }
|
||||
.global-policy-heading p { margin: 0; max-width: 85ch; }
|
||||
.global-policy-fields { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(260px, 2fr); gap: 14px; align-items: start; }
|
||||
.global-rate-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; align-items: end; }
|
||||
.global-policy-fields label, .global-rate-fields label, .global-policy-section > label:not(.check) { display: grid; gap: 6px; min-width: 0; }
|
||||
.global-policy-section textarea { width: 100%; box-sizing: border-box; }
|
||||
.global-policy-form .form-actions { margin-top: 20px; }
|
||||
.cache-static-guidance { margin: 9px 0 0; max-width: 88ch; }
|
||||
.cache-header-settings-heading { border-top: 1px solid var(--line); padding-top: 18px; margin-top: 24px; margin-bottom: 13px; }
|
||||
.transform-operations-title { border-top: 1px solid var(--line); padding-top: 18px; margin-top: 22px; margin-bottom: 14px; }
|
||||
.transform-operations-grid { margin-top: 3px; row-gap: 12px; }
|
||||
@media (max-width: 760px) {
|
||||
.global-policy-fields, .global-rate-fields { grid-template-columns: 1fr; }
|
||||
|
||||
.global-policy-form .global-policy-section h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.global-policy-heading {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.global-policy-heading p {
|
||||
margin: 0;
|
||||
max-width: 85ch;
|
||||
}
|
||||
|
||||
.global-policy-fields {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1fr) minmax(260px, 2fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.global-rate-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.global-policy-fields label,
|
||||
.global-rate-fields label,
|
||||
.global-policy-section>label:not(.check) {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.global-policy-section textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.global-policy-form .form-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.cache-static-guidance {
|
||||
margin: 9px 0 0;
|
||||
max-width: 88ch;
|
||||
}
|
||||
|
||||
.cache-header-settings-heading {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 18px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.transform-operations-title {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 18px;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.transform-operations-grid {
|
||||
margin-top: 3px;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
|
||||
.global-policy-fields,
|
||||
.global-rate-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+163
-47
@@ -61,7 +61,8 @@
|
||||
<div class="sidebar-bottom" id="app-health-slot" hidden>
|
||||
<button class="local-tag app-health-button is-checking" id="app-health-open" type="button"
|
||||
aria-haspopup="dialog" aria-controls="app-health-dialog">
|
||||
<i aria-hidden="true"></i><span id="app-health-label" data-i18n="appHealthSidebarChecking">CHECKING</span>
|
||||
<i aria-hidden="true"></i><span id="app-health-label"
|
||||
data-i18n="appHealthSidebarChecking">CHECKING</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -1792,6 +1793,12 @@
|
||||
</select><small class="fine" data-i18n="buttonStyleHelp">Normal uses the
|
||||
refreshed controls; Compact restores the smaller source-style buttons
|
||||
across all actions and dialog tabs.</small></label>
|
||||
<label class="check appearance-interface-icons-setting"><input
|
||||
id="ui-interface-icons" type="checkbox" /><span><b
|
||||
data-i18n="interfaceIcons">Show SVG interface icons</b><small
|
||||
data-i18n="interfaceIconsHelp">Adds semantic SVG icons to
|
||||
navigation,
|
||||
actions, tabs and dialogs.</small></span></label>
|
||||
<label><span data-i18n="cornerStyle">Corner rounding</span><select
|
||||
id="ui-corner-style">
|
||||
<option data-i18n="cornerStyleDefault" value="default">Original
|
||||
@@ -2102,79 +2109,184 @@
|
||||
<div data-policy-settings-page="block-allow">
|
||||
<form class="card edit-form global-policy-form" id="global-security-form">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="globalPolicyScope">PUBLIC HTTP LISTENERS</span>
|
||||
<div><span class="small-label" data-i18n="globalPolicyScope">PUBLIC HTTP
|
||||
LISTENERS</span>
|
||||
<h2 data-i18n="globalBlockAllow">Global block / allow</h2>
|
||||
</div><span class="pill live" data-i18n="hotApply">LIVE APPLY</span>
|
||||
</div>
|
||||
<p class="fine" data-i18n="globalPolicyHelp">These rules apply before routing to the backend on all public HTTP vhosts, redirects and 404 hosts. Per-vhost restrictions remain active; a local allow cannot bypass a global block. Administration and TCP/UDP Streams are excluded.</p>
|
||||
<p class="fine" data-i18n="globalPolicyHelp">These rules apply before routing to the
|
||||
backend on all public HTTP vhosts, redirects and 404 hosts. Per-vhost
|
||||
restrictions remain active; a local allow cannot bypass a global block.
|
||||
Administration and TCP/UDP Streams are excluded.</p>
|
||||
<section class="global-policy-section">
|
||||
<div class="global-policy-heading"><h3 data-i18n="globalCountryTitle">Country access</h3><p class="fine" data-i18n="globalCountryHelp">Uses the verified client IP and GeoIP database. Unknown countries are denied in allow mode, but allowed in block mode.</p></div>
|
||||
<div class="global-policy-heading">
|
||||
<h3 data-i18n="globalCountryTitle">Country access</h3>
|
||||
<p class="fine" data-i18n="globalCountryHelp">Uses the verified client IP
|
||||
and GeoIP database. Unknown countries are denied in allow mode, but
|
||||
allowed in block mode.</p>
|
||||
</div>
|
||||
<div class="global-policy-fields">
|
||||
<label><span data-i18n="globalCountryMode">Country policy</span><select id="global-country-mode">
|
||||
<label><span data-i18n="globalCountryMode">Country policy</span><select
|
||||
id="global-country-mode">
|
||||
<option value="off" data-i18n="vhOff">Off</option>
|
||||
<option value="block" data-i18n="vhCountryBlock">Block listed</option>
|
||||
<option value="allow" data-i18n="vhCountryAllow">Allow listed only</option>
|
||||
<option value="block" data-i18n="vhCountryBlock">Block listed
|
||||
</option>
|
||||
<option value="allow" data-i18n="vhCountryAllow">Allow listed only
|
||||
</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="globalCountryList">ISO country codes (one per line)</span><textarea id="global-country-list" rows="3" maxlength="1024" spellcheck="false" placeholder="RU CN"></textarea></label>
|
||||
<label><span data-i18n="globalCountryList">ISO country codes (one per
|
||||
line)</span><textarea id="global-country-list" rows="3"
|
||||
maxlength="1024" spellcheck="false"
|
||||
placeholder="RU CN"></textarea></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="global-policy-section">
|
||||
<div class="global-policy-heading"><h3 data-i18n="globalUaTitle">User-Agent blocklist</h3><p class="fine" data-i18n="uaBlockPatternHelp">Case-insensitive substring matching; * is a wildcard. A missing User-Agent is not blocked.</p></div>
|
||||
<label class="check"><input id="global-ua-block-enabled" type="checkbox"/><span data-i18n="globalUaEnabled">Block matching User-Agents</span></label>
|
||||
<label><span data-i18n="uaBlockPatterns">Blocked patterns, one per line</span><textarea id="global-ua-block-patterns" rows="4" maxlength="32768" spellcheck="false" placeholder="BadBot curl/* *scanner*"></textarea></label>
|
||||
<div class="global-policy-heading">
|
||||
<h3 data-i18n="globalUaTitle">User-Agent blocklist</h3>
|
||||
<p class="fine" data-i18n="uaBlockPatternHelp">Case-insensitive substring
|
||||
matching; * is a wildcard. A missing User-Agent is not blocked.</p>
|
||||
</div>
|
||||
<label class="check"><input id="global-ua-block-enabled" type="checkbox" /><span
|
||||
data-i18n="globalUaEnabled">Block matching User-Agents</span></label>
|
||||
<label><span data-i18n="uaBlockPatterns">Blocked patterns, one per
|
||||
line</span><textarea id="global-ua-block-patterns" rows="4"
|
||||
maxlength="32768" spellcheck="false"
|
||||
placeholder="BadBot curl/* *scanner*"></textarea></label>
|
||||
</section>
|
||||
<section class="global-policy-section">
|
||||
<div class="global-policy-heading"><h3 data-i18n="globalIpTitle">IP / CIDR access</h3><p class="fine" data-i18n="globalIpHelp">Each line: allow or deny followed by IPv4/IPv6 address, CIDR or allow-only DNS name. Default allow with no rules leaves access unrestricted.</p></div>
|
||||
<label class="check"><input id="global-ip-enabled" type="checkbox"/><span data-i18n="globalIpEnabled">Enable global IP rules</span></label>
|
||||
<div class="global-policy-heading">
|
||||
<h3 data-i18n="globalIpTitle">IP / CIDR access</h3>
|
||||
<p class="fine" data-i18n="globalIpHelp">Each line: allow or deny followed
|
||||
by IPv4/IPv6 address, CIDR or allow-only DNS name. Default allow with no
|
||||
rules leaves access unrestricted.</p>
|
||||
</div>
|
||||
<label class="check"><input id="global-ip-enabled" type="checkbox" /><span
|
||||
data-i18n="globalIpEnabled">Enable global IP rules</span></label>
|
||||
<div class="global-policy-fields">
|
||||
<label><span data-i18n="aclDefault">Default action</span><select id="global-ip-default"><option value="allow" data-i18n="allow">Allow</option><option value="deny" data-i18n="deny">Deny</option></select></label>
|
||||
<label><span data-i18n="globalIpRules">Rules (one per line)</span><textarea id="global-ip-rules" rows="3" maxlength="32768" placeholder="deny 192.0.2.0/24 allow 10.0.0.0/8" spellcheck="false"></textarea></label>
|
||||
<label><span data-i18n="aclDefault">Default action</span><select
|
||||
id="global-ip-default">
|
||||
<option value="allow" data-i18n="allow">Allow</option>
|
||||
<option value="deny" data-i18n="deny">Deny</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="globalIpRules">Rules (one per line)</span><textarea
|
||||
id="global-ip-rules" rows="3" maxlength="32768"
|
||||
placeholder="deny 192.0.2.0/24 allow 10.0.0.0/8"
|
||||
spellcheck="false"></textarea></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="global-policy-section">
|
||||
<div class="global-policy-heading"><h3 data-i18n="globalMethodTitle">HTTP methods</h3><p class="fine" data-i18n="globalMethodHelp">Block selected methods or allow only those listed. Applies before redirects and cache invalidation; denied requests receive 405.</p></div>
|
||||
<div class="global-policy-heading">
|
||||
<h3 data-i18n="globalMethodTitle">HTTP methods</h3>
|
||||
<p class="fine" data-i18n="globalMethodHelp">Block selected methods or allow
|
||||
only those listed. Applies before redirects and cache invalidation;
|
||||
denied requests receive 405.</p>
|
||||
</div>
|
||||
<div class="global-policy-fields">
|
||||
<label><span data-i18n="globalMethodMode">Method policy</span><select id="global-method-mode"><option value="off" data-i18n="vhOff">Off</option><option value="block" data-i18n="locationMethodBlock">Block listed</option><option value="allow" data-i18n="locationMethodAllow">Allow listed only</option></select></label>
|
||||
<label><span data-i18n="globalMethodList">Methods (comma, space or newline separated)</span><textarea id="global-method-list" rows="2" maxlength="1024" spellcheck="false" placeholder="TRACE, TRACK"></textarea></label>
|
||||
<label><span data-i18n="globalMethodMode">Method policy</span><select
|
||||
id="global-method-mode">
|
||||
<option value="off" data-i18n="vhOff">Off</option>
|
||||
<option value="block" data-i18n="locationMethodBlock">Block listed
|
||||
</option>
|
||||
<option value="allow" data-i18n="locationMethodAllow">Allow listed
|
||||
only</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="globalMethodList">Methods (comma, space or newline
|
||||
separated)</span><textarea id="global-method-list" rows="2"
|
||||
maxlength="1024" spellcheck="false"
|
||||
placeholder="TRACE, TRACK"></textarea></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="global-policy-section">
|
||||
<div class="global-policy-heading"><h3 data-i18n="globalContentTypeTitle">Request Content-Type</h3><p class="fine" data-i18n="globalContentTypeHelp">Reject declared MIME types (exact or type/*) with 403. This is a header rule, not body inspection.</p></div>
|
||||
<label class="check"><input id="global-content-type-enabled" type="checkbox"/><span data-i18n="globalContentTypeEnabled">Enable Content-Type denylist</span></label>
|
||||
<label><span data-i18n="globalContentTypeList">Blocked MIME types, one per line</span><textarea id="global-content-type-list" rows="3" maxlength="16384" spellcheck="false" placeholder="application/x-msdownload application/x-sh"></textarea></label>
|
||||
<div class="global-policy-heading">
|
||||
<h3 data-i18n="globalContentTypeTitle">Request Content-Type</h3>
|
||||
<p class="fine" data-i18n="globalContentTypeHelp">Reject declared MIME types
|
||||
(exact or type/*) with 403. This is a header rule, not body inspection.
|
||||
</p>
|
||||
</div>
|
||||
<label class="check"><input id="global-content-type-enabled"
|
||||
type="checkbox" /><span data-i18n="globalContentTypeEnabled">Enable
|
||||
Content-Type denylist</span></label>
|
||||
<label><span data-i18n="globalContentTypeList">Blocked MIME types, one per
|
||||
line</span><textarea id="global-content-type-list" rows="3"
|
||||
maxlength="16384" spellcheck="false"
|
||||
placeholder="application/x-msdownload application/x-sh"></textarea></label>
|
||||
</section>
|
||||
<section class="global-policy-section">
|
||||
<div class="global-policy-heading"><h3 data-i18n="globalRateTitle">Shared rate limit</h3><p class="fine" data-i18n="globalRateHelp">Optional per-client-IP limit shared by all public vhosts. Runs in addition to per-vhost limits; use conservative values to avoid blocking legitimate shared clients.</p></div>
|
||||
<label class="check"><input id="global-rate-enabled" type="checkbox"/><span data-i18n="globalRateEnabled">Enable global request rate limit</span></label>
|
||||
<div class="global-policy-heading">
|
||||
<h3 data-i18n="globalRateTitle">Shared rate limit</h3>
|
||||
<p class="fine" data-i18n="globalRateHelp">Optional per-client-IP limit
|
||||
shared by all public vhosts. Runs in addition to per-vhost limits; use
|
||||
conservative values to avoid blocking legitimate shared clients.</p>
|
||||
</div>
|
||||
<label class="check"><input id="global-rate-enabled" type="checkbox" /><span
|
||||
data-i18n="globalRateEnabled">Enable global request rate
|
||||
limit</span></label>
|
||||
<div class="global-rate-fields">
|
||||
<label><span data-i18n="globalRateRequests">Requests per window</span><input id="global-rate-requests" type="number" min="1" max="1000000" required value="120"/></label>
|
||||
<label><span data-i18n="globalRateWindow">Window (seconds)</span><input id="global-rate-window" type="number" min="1" max="3600" required value="60"/></label>
|
||||
<label><span data-i18n="globalRateBurst">Burst allowance</span><input id="global-rate-burst" type="number" min="0" max="1000000" required value="30"/></label>
|
||||
<label><span data-i18n="globalRateRequests">Requests per window</span><input
|
||||
id="global-rate-requests" type="number" min="1" max="1000000"
|
||||
required value="120" /></label>
|
||||
<label><span data-i18n="globalRateWindow">Window (seconds)</span><input
|
||||
id="global-rate-window" type="number" min="1" max="3600" required
|
||||
value="60" /></label>
|
||||
<label><span data-i18n="globalRateBurst">Burst allowance</span><input
|
||||
id="global-rate-burst" type="number" min="0" max="1000000" required
|
||||
value="30" /></label>
|
||||
</div>
|
||||
</section>
|
||||
<div class="form-actions"><button class="primary" data-i18n="globalPolicySave" type="submit">Apply global block / allow rules</button></div>
|
||||
<div class="form-actions"><button class="primary" data-i18n="globalPolicySave"
|
||||
type="submit">Apply global block / allow rules</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div data-policy-settings-page="security-headers" hidden="">
|
||||
<form class="card edit-form global-policy-form" id="global-security-headers-form">
|
||||
<div class="card-heading"><div><span class="small-label" data-i18n="globalPolicyScope">PUBLIC HTTP LISTENERS</span><h2 data-i18n="globalSecurityHeaders">Security headers</h2></div><span class="pill live" data-i18n="hotApply">LIVE APPLY</span></div>
|
||||
<p class="fine" data-i18n="globalHeadersSecurityHelp">Global response-header baseline for public HTTP vhosts, redirects and 404 hosts. Explicit per-vhost values override matching global headers. HSTS is sent only over HTTPS; test CSP carefully before rollout.</p>
|
||||
<section class="global-policy-section"><h3 data-i18n="vhSecurityHstsSection">HTTP Strict Transport Security (HSTS)</h3>
|
||||
<label class="check"><input id="global-hsts-enabled" type="checkbox"/><span data-i18n="vhHsts">Enable HSTS</span></label>
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="globalPolicyScope">PUBLIC HTTP
|
||||
LISTENERS</span>
|
||||
<h2 data-i18n="globalSecurityHeaders">Security headers</h2>
|
||||
</div><span class="pill live" data-i18n="hotApply">LIVE APPLY</span>
|
||||
</div>
|
||||
<p class="fine" data-i18n="globalHeadersSecurityHelp">Global response-header
|
||||
baseline for public HTTP vhosts, redirects and 404 hosts. Explicit per-vhost
|
||||
values override matching global headers. HSTS is sent only over HTTPS; test CSP
|
||||
carefully before rollout.</p>
|
||||
<section class="global-policy-section">
|
||||
<h3 data-i18n="vhSecurityHstsSection">HTTP Strict Transport Security (HSTS)</h3>
|
||||
<label class="check"><input id="global-hsts-enabled" type="checkbox" /><span
|
||||
data-i18n="vhHsts">Enable HSTS</span></label>
|
||||
<div class="global-rate-fields">
|
||||
<label><span data-i18n="vhHstsAge">Max age (seconds)</span><input id="global-hsts-age" type="number" min="0" max="63072000" required value="31536000"/></label>
|
||||
<label class="check"><input id="global-hsts-sub" type="checkbox"/><span data-i18n="vhHstsSub">Include subdomains</span></label>
|
||||
<label class="check"><input id="global-hsts-preload" type="checkbox"/><span data-i18n="vhHstsPreload">Preload</span></label>
|
||||
<label><span data-i18n="vhHstsAge">Max age (seconds)</span><input
|
||||
id="global-hsts-age" type="number" min="0" max="63072000" required
|
||||
value="31536000" /></label>
|
||||
<label class="check"><input id="global-hsts-sub" type="checkbox" /><span
|
||||
data-i18n="vhHstsSub">Include subdomains</span></label>
|
||||
<label class="check"><input id="global-hsts-preload" type="checkbox" /><span
|
||||
data-i18n="vhHstsPreload">Preload</span></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="global-policy-section"><h3 data-i18n="vhSecurityHeadersSection">Response security headers</h3>
|
||||
<label class="check"><input id="global-nosniff" type="checkbox"/><span data-i18n="vhNosniff">X-Content-Type-Options: nosniff</span></label>
|
||||
<label><span data-i18n="vhFrame">X-Frame-Options</span><select id="global-frame"><option value="preserve" data-i18n="vhPreserve">Preserve origin value</option><option value="deny" data-i18n="vhDeny">DENY</option><option value="sameorigin" data-i18n="vhSameorigin">SAMEORIGIN</option></select></label>
|
||||
<label><span data-i18n="vhReferrer">Referrer-Policy</span><input id="global-referrer" maxlength="8192" spellcheck="false" placeholder="strict-origin-when-cross-origin"/></label>
|
||||
<label><span data-i18n="vhCsp">Content-Security-Policy</span><textarea id="global-csp" rows="3" maxlength="8192" spellcheck="false" placeholder="default-src 'self'"></textarea></label>
|
||||
<label><span data-i18n="vhPermissions">Permissions-Policy</span><input id="global-permissions" maxlength="8192" spellcheck="false" placeholder="camera=(), microphone=()"/></label>
|
||||
<section class="global-policy-section">
|
||||
<h3 data-i18n="vhSecurityHeadersSection">Response security headers</h3>
|
||||
<label class="check"><input id="global-nosniff" type="checkbox" /><span
|
||||
data-i18n="vhNosniff">X-Content-Type-Options: nosniff</span></label>
|
||||
<label><span data-i18n="vhFrame">X-Frame-Options</span><select
|
||||
id="global-frame">
|
||||
<option value="preserve" data-i18n="vhPreserve">Preserve origin value
|
||||
</option>
|
||||
<option value="deny" data-i18n="vhDeny">DENY</option>
|
||||
<option value="sameorigin" data-i18n="vhSameorigin">SAMEORIGIN</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="vhReferrer">Referrer-Policy</span><input
|
||||
id="global-referrer" maxlength="8192" spellcheck="false"
|
||||
placeholder="strict-origin-when-cross-origin" /></label>
|
||||
<label><span data-i18n="vhCsp">Content-Security-Policy</span><textarea
|
||||
id="global-csp" rows="3" maxlength="8192" spellcheck="false"
|
||||
placeholder="default-src 'self'"></textarea></label>
|
||||
<label><span data-i18n="vhPermissions">Permissions-Policy</span><input
|
||||
id="global-permissions" maxlength="8192" spellcheck="false"
|
||||
placeholder="camera=(), microphone=()" /></label>
|
||||
</section>
|
||||
<div class="form-actions"><button class="primary" data-i18n="globalSecurityHeadersSave" type="submit">Apply security headers</button></div>
|
||||
<div class="form-actions"><button class="primary"
|
||||
data-i18n="globalSecurityHeadersSave" type="submit">Apply security
|
||||
headers</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div data-policy-settings-page="headers" hidden="">
|
||||
@@ -2501,9 +2613,11 @@
|
||||
<label data-i18n="cacheStaticExtensions" for="cache-static-extensions">Global static
|
||||
extensions</label><textarea id="cache-static-extensions" rows="5"
|
||||
spellcheck="false"></textarea>
|
||||
<p class="fine cache-static-guidance" data-i18n="cacheStaticHelp">Cookie bypass is limited to these file
|
||||
<p class="fine cache-static-guidance" data-i18n="cacheStaticHelp">Cookie bypass is
|
||||
limited to these file
|
||||
extensions. Response privacy rules still apply.</p>
|
||||
<h3 class="subheading cache-header-settings-heading" data-i18n="cacheHeadersSection">Cache response headers</h3>
|
||||
<h3 class="subheading cache-header-settings-heading"
|
||||
data-i18n="cacheHeadersSection">Cache response headers</h3>
|
||||
<div class="switch-grid"><label class="check"><input id="cache-status-header"
|
||||
type="checkbox" /><span>X-Cache: HIT / MISS /
|
||||
BYPASS</span></label><label class="check"><input id="cache-hits-header"
|
||||
@@ -2609,7 +2723,8 @@
|
||||
concurrent transformations</label><input id="transform-max-inflight" min="1"
|
||||
max="1024" required="" type="number" /></div>
|
||||
</div>
|
||||
<h3 class="subheading transform-operations-title" data-i18n="transformOperationsTitle">Content transformations</h3>
|
||||
<h3 class="subheading transform-operations-title" data-i18n="transformOperationsTitle">
|
||||
Content transformations</h3>
|
||||
<div class="switch-grid transform-operations-grid">
|
||||
<label class="check"><input id="transform-javascript" type="checkbox" /><span
|
||||
data-i18n="transformJavascript">JavaScript minification</span></label>
|
||||
@@ -3144,8 +3259,8 @@
|
||||
<p class="fine" data-i18n="tlsPqcHelp">TLS 1.3 only: prioritize hybrid
|
||||
X25519MLKEM768 with classic key exchanges as fallback. This is a key exchange
|
||||
group, not a cipher suite. Disabled by default.</p>
|
||||
<a href="/docs#doc=docs%2FTRANSPORT.md" target="_blank"
|
||||
rel="noopener noreferrer" data-i18n="tlsPqcDocs">Learn about PQC, compatibility
|
||||
<a href="/docs#doc=docs%2FTRANSPORT.md" target="_blank" rel="noopener noreferrer"
|
||||
data-i18n="tlsPqcDocs">Learn about PQC, compatibility
|
||||
and testing (documentation)</a>
|
||||
</div>
|
||||
<p class="fine" id="tls-policy-key-exchange"></p>
|
||||
@@ -4353,7 +4468,8 @@ log_errors = On</code></pre>
|
||||
</section>
|
||||
<div class="about-links">
|
||||
<a class="outline about-repository" href="https://rustproxy.linuxiarz.pl/" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutPackagesRepository">Website and packages</span><span>rustproxy.linuxiarz.pl</span></a>
|
||||
target="_blank"><span data-i18n="aboutPackagesRepository">Website and
|
||||
packages</span><span>rustproxy.linuxiarz.pl</span></a>
|
||||
<a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy"
|
||||
rel="noopener noreferrer" target="_blank"><span data-i18n="aboutRepository">Source
|
||||
code</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
window.rustProxySettings?.selectHttpTab?.("http2");
|
||||
document.querySelector('[data-http-settings-page="http2"]')?.scrollIntoView({ block: "start" });
|
||||
});
|
||||
h2Settings.classList.add("listener-h2-settings-link");
|
||||
h2Settings.classList.add("listener-h2-settings-link"); h2Settings.dataset.uiIconName = "settings";
|
||||
const preservedHttp3 = Object.hasOwn(b, "http3") ? b.http3 : (b.tls ? { ...h3Defaults } : null);
|
||||
const h3 = field(grid, "listener-h3", "h3Enable", b.tls && !!preservedHttp3, "checkbox");
|
||||
const h3Settings = button(protocolActions, "h3Configure", () => {
|
||||
@@ -141,7 +141,7 @@
|
||||
window.rustProxySettings?.selectHttpTab?.("http3");
|
||||
document.querySelector('[data-http-settings-page="http3"]')?.scrollIntoView({ block: "start" });
|
||||
});
|
||||
h3Settings.classList.add("listener-h3-settings-link");
|
||||
h3Settings.classList.add("listener-h3-settings-link"); h3Settings.dataset.uiIconName = "settings";
|
||||
const sync = () => {
|
||||
h2.disabled = !tls.checked; h2c.disabled = tls.checked; h3.disabled = !tls.checked;
|
||||
h2Settings.hidden = !((tls.checked && h2.checked) || (!tls.checked && h2c.checked));
|
||||
|
||||
@@ -85,6 +85,11 @@
|
||||
];
|
||||
const defaultColumns = () => new Set(columns.map(column => column.id));
|
||||
let visibleColumns = defaultColumns();
|
||||
function active() { return dialog.open && !paused && !expanded; }
|
||||
function syncLivePolling(fetchNow = false) {
|
||||
window.rustProxyRefresh?.sync?.();
|
||||
if (fetchNow && active()) void window.rustProxyRefresh?.manual?.("logs-fallback");
|
||||
}
|
||||
function source() { return paused || expanded || lastLogs || { entries: [] }; }
|
||||
function mergeExpanded(data) {
|
||||
if (!expanded || paused || !data?.entries) return;
|
||||
@@ -189,7 +194,7 @@
|
||||
if (!preferencesLoaded) void loadPreferences();
|
||||
if (vhost) { const opt = el("option", vhost); opt.value = vhost; $("log-vhost").append(opt); $("log-vhost").value = vhost; }
|
||||
if (!dialog.open) { if (docked) dialog.show(); else dialog.showModal(); }
|
||||
render();
|
||||
render(); syncLivePolling(true);
|
||||
}
|
||||
syncColumnPicker(); syncHeaders();
|
||||
$("logs-open").addEventListener("click", () => { $("logs-menu").open = false; open(); }); $("logs-close").addEventListener("click", () => dialog.close());
|
||||
@@ -200,14 +205,14 @@
|
||||
expanded = { ...snapshot, capacity: 4096, window: 4096 }; render();
|
||||
toast(t("logBufferExpanded").replace("{count}", String(snapshot.entries?.length || 0)));
|
||||
} catch (error) { toast(error.message, true); }
|
||||
finally { button.disabled = Boolean(expanded); }
|
||||
finally { button.disabled = Boolean(expanded); syncLivePolling(); }
|
||||
});
|
||||
$("logs-pause").addEventListener("click", () => { paused = paused ? null : structuredClone(source()); render(); });
|
||||
$("logs-pin").addEventListener("click", () => { pinning = true; dialog.close(); docked = !docked; dialog.classList.toggle("docked", docked); if (docked) dialog.show(); else dialog.showModal(); pinning = false; render(); });
|
||||
dialog.addEventListener("close", () => { if (pinning) return; expanded = null; paused = null; syncExpandedButton(); });
|
||||
$("logs-pause").addEventListener("click", () => { paused = paused ? null : structuredClone(source()); render(); syncLivePolling(!paused); });
|
||||
$("logs-pin").addEventListener("click", () => { pinning = true; dialog.close(); docked = !docked; dialog.classList.toggle("docked", docked); if (docked) dialog.show(); else dialog.showModal(); pinning = false; render(); syncLivePolling(true); });
|
||||
dialog.addEventListener("close", () => { if (pinning) return; expanded = null; paused = null; syncExpandedButton(); syncLivePolling(); });
|
||||
for (const id of filters) $(id).addEventListener(id === "log-filter" ? "input" : "change", render);
|
||||
$("export-logs").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify({ entries: filtered(), capacity: source().capacity, filtered: true }, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = "RustProxy-access-log.json"; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
|
||||
window.rustProxyLogs = { render, open, loadPreferences, applyPreferences, reset() { dialog.close(); paused = null; expanded = null; preferencesLoaded = false; preferencesLoading = null; preferenceSaveQueued = false; visibleColumns = defaultColumns(); syncColumnPicker(); syncHeaders(); for (const id of filters) $(id).value = id === "log-outcome" ? "all" : ""; syncExpandedButton(); } };
|
||||
window.rustProxyLogs = { render, open, active, loadPreferences, applyPreferences, reset() { dialog.close(); paused = null; expanded = null; preferencesLoaded = false; preferencesLoading = null; preferenceSaveQueued = false; visibleColumns = defaultColumns(); syncColumnPicker(); syncHeaders(); for (const id of filters) $(id).value = id === "log-outcome" ? "all" : ""; syncExpandedButton(); syncLivePolling(); } };
|
||||
})();
|
||||
|
||||
(() => {
|
||||
|
||||
@@ -34,20 +34,26 @@
|
||||
<article class="stat stat-cache-outcomes" data-dashboard-widget="cache_outcomes">
|
||||
<div class="cache-mix-header">
|
||||
<span data-i18n="cacheMixTitle">Cache outcomes</span>
|
||||
<small class="cache-mix-caption" id="cache-mix-caption" aria-label="HTTP requests since service start" title="HTTP requests since service start">-</small>
|
||||
<small class="cache-mix-caption" id="cache-mix-caption" aria-label="HTTP requests since service start"
|
||||
title="HTTP requests since service start">-</small>
|
||||
</div>
|
||||
<strong id="cache-mix-headline">-</strong>
|
||||
<div class="cache-mix-bar" id="cache-mix-bar" role="img" aria-label="Cumulative HTTP cache outcomes since service start">
|
||||
<div class="cache-mix-bar" id="cache-mix-bar" role="img"
|
||||
aria-label="Cumulative HTTP cache outcomes since service start">
|
||||
<span class="cache-mix-hit" id="cache-mix-hit" style="width:0%"></span>
|
||||
<span class="cache-mix-miss" id="cache-mix-miss" style="width:0%"></span>
|
||||
<span class="cache-mix-bypass" id="cache-mix-bypass" style="width:0%"></span>
|
||||
<span class="cache-mix-other" id="cache-mix-other" style="width:0%"></span>
|
||||
</div>
|
||||
<small class="cache-mix-legend">
|
||||
<span class="cache-mix-hit-key"><i></i>HIT <b><span id="cache-mix-hit-count">-</span><span class="cache-mix-percent" id="cache-mix-hit-percent">-</span></b></span>
|
||||
<span class="cache-mix-miss-key"><i></i>MISS <b><span id="cache-mix-miss-count">-</span><span class="cache-mix-percent" id="cache-mix-miss-percent">-</span></b></span>
|
||||
<span class="cache-mix-bypass-key"><i></i>BYPASS <b><span id="cache-mix-bypass-count">-</span><span class="cache-mix-percent" id="cache-mix-bypass-percent">-</span></b></span>
|
||||
<span class="cache-mix-other-key"><i></i><span data-i18n="cacheMixOther">OTHER</span> <b><span id="cache-mix-other-count">-</span><span class="cache-mix-percent"
|
||||
<span class="cache-mix-hit-key"><i></i>HIT <b><span id="cache-mix-hit-count">-</span><span
|
||||
class="cache-mix-percent" id="cache-mix-hit-percent">-</span></b></span>
|
||||
<span class="cache-mix-miss-key"><i></i>MISS <b><span id="cache-mix-miss-count">-</span><span
|
||||
class="cache-mix-percent" id="cache-mix-miss-percent">-</span></b></span>
|
||||
<span class="cache-mix-bypass-key"><i></i>BYPASS <b><span id="cache-mix-bypass-count">-</span><span
|
||||
class="cache-mix-percent" id="cache-mix-bypass-percent">-</span></b></span>
|
||||
<span class="cache-mix-other-key"><i></i><span data-i18n="cacheMixOther">OTHER</span> <b><span
|
||||
id="cache-mix-other-count">-</span><span class="cache-mix-percent"
|
||||
id="cache-mix-other-percent">-</span></b></span>
|
||||
</small>
|
||||
</article>
|
||||
@@ -55,9 +61,10 @@
|
||||
memory</span><strong id="metric-rss">-</strong><small id="process-summary">-</small>
|
||||
</article>
|
||||
<article class="stat stat-with-action" data-dashboard-widget="websocket"><span data-i18n="wsActive">WebSocket
|
||||
tunnels</span><button class="quiet stat-corner-action" data-i18n="tunnelsOpen" id="tunnels-open"
|
||||
type="button">Active tunnels</button><strong id="metric-ws">0</strong><small
|
||||
aria-label="Active WebSocket tunnels by frontend protocol"><span data-i18n="wsH1">H1
|
||||
tunnels</span><button class="pill stat-corner-action stat-corner-badge" data-i18n="tunnelsOpen"
|
||||
data-ui-icon-name="none" id="tunnels-open" type="button">Active tunnels</button><strong
|
||||
id="metric-ws">0</strong><small aria-label="Active WebSocket tunnels by frontend protocol"><span
|
||||
data-i18n="wsH1">H1
|
||||
Upgrade</span> <span id="metric-ws-h1">0</span> · <span data-i18n="wsH2Rfc8441">H2
|
||||
RFC 8441</span> <span id="metric-ws-h2">0</span> · <span data-i18n="wsH3Rfc9220">H3
|
||||
RFC 9220</span> <span id="metric-ws-h3">0</span><br><span data-i18n="wsEstablished">Established</span>:
|
||||
@@ -366,14 +373,16 @@
|
||||
</svg><small id="overview-extra-extra_backend_unhealthy-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_backends_healthy" hidden>
|
||||
<span data-i18n="overviewExtraBackendsHealthy">overviewExtraBackendsHealthy</span><strong id="overview-extra-extra_backends_healthy">-</strong>
|
||||
<span data-i18n="overviewExtraBackendsHealthy">overviewExtraBackendsHealthy</span><strong
|
||||
id="overview-extra-extra_backends_healthy">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_backends_healthy-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraBackendsHealthy" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_backends_healthy-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_backends_healthy-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_error_share" hidden>
|
||||
<span data-i18n="overviewExtraErrorShare">overviewExtraErrorShare</span><strong id="overview-extra-extra_error_share">-</strong>
|
||||
<span data-i18n="overviewExtraErrorShare">overviewExtraErrorShare</span><strong
|
||||
id="overview-extra-extra_error_share">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraErrorShare" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_error_share-line" points="0,55 300,55">
|
||||
@@ -381,7 +390,8 @@
|
||||
</svg><small id="overview-extra-extra_error_share-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_rejected_rate" hidden>
|
||||
<span data-i18n="overviewExtraRejectedRate">overviewExtraRejectedRate</span><strong id="overview-extra-extra_rejected_rate">-</strong>
|
||||
<span data-i18n="overviewExtraRejectedRate">overviewExtraRejectedRate</span><strong
|
||||
id="overview-extra-extra_rejected_rate">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraRejectedRate" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_rejected_rate-line" points="0,55 300,55">
|
||||
@@ -389,7 +399,8 @@
|
||||
</svg><small id="overview-extra-extra_rejected_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cert_expiry" hidden>
|
||||
<span data-i18n="overviewExtraCertExpiry">overviewExtraCertExpiry</span><strong id="overview-extra-extra_cert_expiry">-</strong>
|
||||
<span data-i18n="overviewExtraCertExpiry">overviewExtraCertExpiry</span><strong
|
||||
id="overview-extra-extra_cert_expiry">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cert_expiry-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraCertExpiry" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_cert_expiry-meter-fill"></span>
|
||||
@@ -401,7 +412,7 @@
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img" aria-label="HTTP requests / s">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_total_rps-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_total_rps-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</svg><small id="overview-extra-extra_total_rps-detail" hidden></small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_active_exchanges" hidden>
|
||||
<span data-i18n="overviewExtraActiveExchanges">Active HTTP exchanges</span><strong
|
||||
@@ -410,21 +421,21 @@
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_active_exchanges-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_active_exchanges-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</svg><small id="overview-extra-extra_active_exchanges-detail" hidden></small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h1_share" hidden>
|
||||
<span data-i18n="overviewExtraH1Share">HTTP/1.1 share</span><strong id="overview-extra-extra_h1_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h1_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH1Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h1_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h1_share-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</div><small id="overview-extra-extra_h1_share-detail" hidden></small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h3_share" hidden>
|
||||
<span data-i18n="overviewExtraH3Share">HTTP/3 share</span><strong id="overview-extra-extra_h3_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h3_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH3Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h3_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h3_share-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</div><small id="overview-extra-extra_h3_share-detail" hidden></small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheMissRate">Cache misses / s</span><strong
|
||||
@@ -433,7 +444,7 @@
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_cache_miss_rate-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_cache_miss_rate-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</svg><small id="overview-extra-extra_cache_miss_rate-detail" hidden></small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_backend_active" hidden>
|
||||
<span data-i18n="overviewExtraBackendActive">In-flight backend requests</span><strong
|
||||
@@ -441,7 +452,7 @@
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img" aria-label="In-flight backend requests">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_backend_active-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_backend_active-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</svg><small id="overview-extra-extra_backend_active-detail" hidden></small>
|
||||
</article>
|
||||
<!-- RP_METRICS_SPLIT -->
|
||||
<article class="stat stat-donut" data-dashboard-widget="www_status" hidden>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Browser smoke test for the optional semantic SVG interface icons.
|
||||
|
||||
Uses a locally installed Chromium when available and otherwise exits successfully
|
||||
with a skip message. It exercises the real index.html plus the embedded icon
|
||||
module without requiring a running RustProxy backend.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import html
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WEB = ROOT / "src/admin/web"
|
||||
chromium = next((shutil.which(name) for name in ("chromium", "chromium-browser", "google-chrome") if shutil.which(name)), None)
|
||||
if not chromium:
|
||||
print("Panel SVG icon browser smoke: SKIP (Chromium not installed)")
|
||||
raise SystemExit(0)
|
||||
|
||||
page = (WEB / "index.html").read_text(encoding="utf-8")
|
||||
page = re.sub(r'<script\s+defer=""\s+src="/assets/app___RUSTPROXY_ASSET_ID__\.js"></script>', "", page, count=1)
|
||||
icons = (WEB / "app/icons.js").read_text(encoding="utf-8")
|
||||
script = r'''
|
||||
const $ = id => document.getElementById(id);
|
||||
const translations = { edit: "Edit", remove: "Remove", copyValue: "Copy value", previous: "Previous", next: "Next" };
|
||||
function t(key) { return translations[key] || key; }
|
||||
''' + icons + r'''
|
||||
(() => {
|
||||
const fail = message => { document.getElementById("browser-icon-test").textContent = `FAIL: ${message}`; throw new Error(message); };
|
||||
const result = document.getElementById("browser-icon-test");
|
||||
const eligible = [...document.querySelectorAll("button, a.primary, a.outline, a.quiet")].filter(node => !node.matches(".chart-style-choice"));
|
||||
if (document.querySelectorAll("svg.ui-icon").length !== 0) fail("icons must be absent before enabling the preference");
|
||||
window.rustProxyIcons.setEnabled(true);
|
||||
const decorated = eligible.filter(node => node.querySelector(":scope > svg.ui-icon"));
|
||||
if (decorated.length !== eligible.length) fail(`expected ${eligible.length} decorated controls, got ${decorated.length}`);
|
||||
if ([...document.querySelectorAll("svg.ui-icon")].some(icon => icon.getAttribute("aria-hidden") !== "true")) fail("SVG icons must be aria-hidden");
|
||||
const expectedPanels = { overview: "grid", vhosts: "network", "web-server": "globe", "docker-vhosts": "box", redirects: "route", notfound: "fileX", streams: "network", "acl-profiles": "shield", tls: "certificate", traffic: "activity", cache: "database", transform: "wand", settings: "settings" };
|
||||
for (const [panel, icon] of Object.entries(expectedPanels)) {
|
||||
const button = document.querySelector(`[data-panel="${panel}"]`);
|
||||
if (button?.querySelector(":scope > svg.ui-icon")?.dataset.uiIcon !== icon) fail(`panel ${panel} must use ${icon}`);
|
||||
}
|
||||
const dynamic = document.createElement("button"); dynamic.type = "button"; dynamic.className = "quiet"; dynamic.textContent = "Edit"; document.body.append(dynamic);
|
||||
window.rustProxyIcons.refresh(dynamic);
|
||||
if (dynamic.querySelector("svg.ui-icon")?.dataset.uiIcon !== "edit") fail("translated dynamic Edit action must use edit icon");
|
||||
dynamic.textContent = "Remove item"; dynamic.setAttribute("aria-label", "Remove item"); window.rustProxyIcons.refresh(dynamic);
|
||||
if (dynamic.querySelector("svg.ui-icon")?.dataset.uiIcon !== "trash") fail("dynamic Remove action must use trash icon");
|
||||
const visibility = document.createElement("button"); visibility.dataset.dashboardWidgetAction = "toggle"; visibility.className = "dashboard-widget-toggle is-hidden"; document.body.append(visibility);
|
||||
window.rustProxyIcons.refresh(visibility);
|
||||
if (visibility.querySelector("svg.ui-icon")?.dataset.uiIcon !== "eyeOff") fail("hidden widget action must use eyeOff");
|
||||
visibility.classList.remove("is-hidden"); window.rustProxyIcons.refresh(visibility);
|
||||
if (visibility.querySelector("svg.ui-icon")?.dataset.uiIcon !== "eye") fail("visible widget action must use eye");
|
||||
window.rustProxyIcons.setEnabled(false);
|
||||
if (document.querySelectorAll("svg.ui-icon").length !== 0) fail("disabling preference must remove all injected icons");
|
||||
result.textContent = `OK ${eligible.length} controls`;
|
||||
})();
|
||||
'''
|
||||
page = page.replace("</body>", '<pre id="browser-icon-test">PENDING</pre><script>' + script + "</script></body>", 1)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="rustproxy-icons-") as tmp:
|
||||
target = Path(tmp) / "icons-test.html"
|
||||
target.write_text(page, encoding="utf-8")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[chromium, "--headless", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage", "--virtual-time-budget=1000", "--dump-dom", target.as_uri()],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=8,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Panel SVG icon browser smoke: SKIP (Chromium cannot start in this environment)")
|
||||
raise SystemExit(0)
|
||||
match = re.search(r'<pre id="browser-icon-test">([^<]+)</pre>', proc.stdout)
|
||||
status = html.unescape(match.group(1)) if match else "missing result marker"
|
||||
if proc.returncode != 0 or not status.startswith("OK "):
|
||||
raise SystemExit(f"Panel SVG icon browser smoke failed: {status}\n{proc.stderr[-4000:]}")
|
||||
print(f"Panel SVG icon browser smoke: {status}")
|
||||
@@ -0,0 +1,45 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const read = relative => fs.readFileSync(path.join(root, relative), "utf8");
|
||||
const preferences = read("src/admin/web/app/preferences.js");
|
||||
const icons = read("src/admin/web/app/icons.js");
|
||||
const core = read("src/admin/web/app/core.js");
|
||||
const html = read("src/admin/web/index.html");
|
||||
const css = read("src/admin/web/css/10-base.css");
|
||||
const overviewCss = read("src/admin/web/css/30-overview.css");
|
||||
const integrations = read("src/admin/web/integrations.js");
|
||||
const metricsPartial = read("src/admin/web/partials/overview-metrics.html");
|
||||
const pl = JSON.parse(read("langs/pl_pl.json"));
|
||||
const openapi = JSON.parse(read("src/admin/openapi.json"));
|
||||
|
||||
assert.match(html, /id="ui-interface-icons"[^>]*type="checkbox"/);
|
||||
assert.match(preferences, /interface_icons:\s*false/);
|
||||
assert.match(preferences, /applyAdminInterfaceIcons\(value\.interface_icons\)/);
|
||||
assert.match(preferences, /saveAppearancePreference\(\{ interface_icons: next \}/);
|
||||
assert.match(preferences, /interface:\s*\{[^}]*interface_icons:\s*false/);
|
||||
assert.match(core, /rustProxyIcons\?\.refresh\?\.\(\)/);
|
||||
assert.match(icons, /dataset\.interfaceIcons !== "true"/);
|
||||
assert.match(icons, /aria-hidden", "true"/);
|
||||
assert.match(icons, /data\.panel|dataset\.panel/);
|
||||
assert.match(icons, /MutationObserver/);
|
||||
assert.match(icons, /if \(target\.dataset\.httpSettingsTab\) return "network"/);
|
||||
assert.match(icons, /#web-service-preset-apply.*return "wand"/);
|
||||
assert.match(icons, /#tunnels-open.*return null/);
|
||||
assert.match(icons, /#logs-menu > summary/);
|
||||
assert.match(integrations, /listener-h2-settings-link.*uiIconName = "settings"/);
|
||||
assert.match(integrations, /listener-h3-settings-link.*uiIconName = "settings"/);
|
||||
assert.match(metricsPartial, /stat-corner-badge[^>]+data-ui-icon-name="none"[^>]+id="tunnels-open"/);
|
||||
assert.match(overviewCss, /\.stat-corner-badge/);
|
||||
assert.equal(pl.translations.interfaceIconsHelp, "Dodaje semantyczne ikony SVG do nawigacji, akcji, zakładek i okien dialogowych.");
|
||||
assert.match(css, /html\[data-interface-icons="true"\]/);
|
||||
assert.equal(openapi.components.schemas.AdminPreferences.properties.interface_icons.default, false);
|
||||
assert.equal(openapi.components.schemas.AdminPreferences.properties.interface_icons.type, "boolean");
|
||||
|
||||
for (const icon of ["plus", "save", "check", "trash", "edit", "refresh", "settings", "search", "key", "certificate", "shield", "network", "database", "wand", "grid", "activity", "book"]) {
|
||||
assert.ok(new RegExp(`\\b${icon}:`).test(icons), `missing semantic SVG icon definition: ${icon}`);
|
||||
}
|
||||
|
||||
console.log("Panel SVG icon preference contract: OK");
|
||||
@@ -76,6 +76,13 @@ for path in sources:
|
||||
missing_refs = {dom_id: sorted(paths) for dom_id, paths in literal_refs.items() if dom_id not in id_set}
|
||||
assert not missing_refs, f"embedded panel JS references missing DOM ids: {missing_refs}"
|
||||
|
||||
assert "ui-interface-icons" in id_set, "Appearance must expose the SVG icon toggle"
|
||||
assert any(path.name == "icons.js" for path in sources), "semantic icon module must be embedded in app.js"
|
||||
|
||||
assert 'data-i18n="overviewExtraRecentSamples"' not in html, "Overview metric tiles must not show the redundant Recent samples footer"
|
||||
overview_extra = (WEB / "app/overview-extra-metrics.js").read_text(encoding="utf-8")
|
||||
assert 't("overviewExtraRecentSamples")' not in overview_extra, "Overview metric details must not fall back to the generic Recent samples label"
|
||||
|
||||
# Every visible primary navigation target must have a panel section. filelogs is
|
||||
# intentionally opened from the Logs menu rather than the main navigation.
|
||||
panel_targets = attribute_values(html, "data-panel")
|
||||
|
||||
@@ -11,6 +11,9 @@ const core = read("src/admin/web/app/core.js");
|
||||
const cache = read("src/admin/web/cache-panel.js");
|
||||
const transform = read("src/admin/web/transform-panel.js");
|
||||
const traffic = read("src/admin/web/traffic-panel.js");
|
||||
const refresh = read("src/admin/web/app/refresh.js");
|
||||
const management = read("src/admin/web/management.js");
|
||||
const loggingApi = read("src/admin/logging_api.rs");
|
||||
const openapi = JSON.parse(read("src/admin/openapi.json"));
|
||||
|
||||
assert.match(settings, /authenticated && name === "certificate-api"[^\n]+rustProxyCertificateApi\?\.load/);
|
||||
@@ -18,9 +21,18 @@ assert.match(settings, /authenticated && active === "certificate-api"[^\n]+rustP
|
||||
assert.ok(!/renderCertificateApi\(\);\s*loadCertificateApi\(\);\s*\}\)\(\);\s*$/.test(certificateApi),
|
||||
"Certificate API must not fetch credentials when its module is initialized");
|
||||
|
||||
assert.match(core, /async function refresh\(force = false, includeLogs = false\)/);
|
||||
assert.match(core, /if \(includeLogs\) tasks\.push\(rustProxyRefresh\.manual\("logs-fallback"\)\)/);
|
||||
assert.match(core, /refresh\(true, true\)\.catch/);
|
||||
assert.match(core, /async function refresh\(force = false\)/);
|
||||
assert.doesNotMatch(core, /includeLogs/);
|
||||
assert.match(core, /new EventSource\(`\/api\/events\?interval=\$\{seconds\}&logs=false`\)/);
|
||||
assert.match(loggingApi, /logs: Option<bool>/);
|
||||
assert.match(loggingApi, /let include_logs = query\.logs\.unwrap_or\(true\)/);
|
||||
assert.match(loggingApi, /if include_logs[\s\S]+logs_payload\(&state\)[\s\S]+else[\s\S]+live_status_payload/);
|
||||
assert.match(core, /if \(payload\.logs\) renderLogs\(payload\.logs\)/);
|
||||
|
||||
assert.match(refresh, /active: \(\) => !!window\.rustProxyLogs\?\.active\?\.\(\)/);
|
||||
assert.doesNotMatch(refresh, /manual\("logs-fallback"\).*manual\("system-info"\)/);
|
||||
assert.match(management, /function active\(\) \{ return dialog\.open && !paused && !expanded; \}/);
|
||||
assert.match(management, /syncLivePolling\(true\)/);
|
||||
|
||||
for (const [name, source, endpoint] of [
|
||||
["cache", cache, "/api/cache"],
|
||||
@@ -35,5 +47,9 @@ for (const [name, source, endpoint] of [
|
||||
assert.deepEqual(view?.schema?.enum, ["live"], `${endpoint} must document view=live`);
|
||||
}
|
||||
|
||||
assert.equal(openapi.info.version, "0.36.0");
|
||||
const events = openapi.paths["/api/events"].get;
|
||||
const logsFlag = (events.parameters || []).find(parameter => parameter.name === "logs");
|
||||
assert.equal(logsFlag?.schema?.type, "boolean");
|
||||
assert.equal(logsFlag?.schema?.default, true);
|
||||
assert.equal(openapi.info.version, "0.36.1");
|
||||
console.log("Panel refresh/API contract checks: OK");
|
||||
Reference in new issue
Block a user