Compare commits
2
Commits
6d7f152a0b
...
5cb63f542c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cb63f542c | ||
|
|
064772765b |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# RustProxy source and documentation maintenance
|
||||
|
||||
Current source version: **0.36.1**. The Cargo package and binary are named `rust-proxy`.
|
||||
Current source version: **0.36.2**. 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,6 +2,14 @@
|
||||
|
||||
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.2 — icon semantics review and optional logo animation
|
||||
|
||||
- **SVG icon review:** every panel control was re-checked against its function. Fixes include **Unlock layout** (was the sign-out icon, now an open padlock), **Save and lock** (padlock), **Dashboard presets** (layers instead of edit), **Docker agents settings** (was a pin because "docker" matched "dock"), cache/transform memory presets (were the undo icon because "preset" matched "reset"), **Previous/Next** in Cache and Stored logs (chevrons instead of database/list), **Close** in the health and history dialogs (close instead of activity/history), revision history **Reload/Restore/Compare** (refresh/undo/compare), **Sign in** (sign-in instead of check), **Sign out all sessions** (sign-out instead of trash), **Run operation** (play), **Purge / Ban** and **Active BANs** (ban), **Clean cold cache** and **Clear** (eraser), **Download now** (download), token generation/rotation (key), **Repeated values / Count repeats** (hash), **Open Logs** (list), **Configure fields** (sliders), **About** (info), automatic renewal enable/disable (toggles) and Live logs **Pause/Resume** (pause/play). Expand/collapse toggles now follow their current state.
|
||||
- **Distinct section icons:** Streams (transfer arrows), Listeners (plug), GeoIP updates (map pin) and Administration (users) no longer share icons with Vhosts, HTTP connections and Settings. The settings cog, pin, undo and sign-in/sign-out glyphs were redrawn to be more recognizable.
|
||||
- **Icon fixes:** English labels whose translation equals the key (for example **Edit**) no longer fall back to the generic placeholder icon; buttons labelled with a single symbol (`×`, `−`, `↑`, `←` …) keep the native glyph instead of showing a duplicate SVG; protocol badges and history calendar days no longer receive icons.
|
||||
- **Logo animation:** new **Settings → Appearance → Interface → Animate logo** option (account-scoped `logo_animation`, **off by default**) plays short, subtle random effects on the sidebar logo; it pauses in hidden tabs and respects reduced-motion preferences.
|
||||
- **Upgrade notes:** no configuration or database migration is required; `logo_animation` is an additive preference field that defaults to `false` for existing accounts.
|
||||
|
||||
## 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.
|
||||
|
||||
Generated
+1
-1
@@ -1943,7 +1943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.36.1"
|
||||
version = "0.36.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.36.1"
|
||||
version = "0.36.2"
|
||||
edition = "2021"
|
||||
rust-version = "1.99.1"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
+23
-22
@@ -1,10 +1,10 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 .gitignore
|
||||
fded6c62b3bbf31e80efa6a5411431625fb06a3be044eb03399de2e09d02c1d6 AGENTS.md
|
||||
9e2912307564ea75dafd974af97d681208ff1a6babeab1955f22cee958535cb0 CHANGELOG.md
|
||||
a9309ef711e6daa6ca9bf7f3d7d9bfffe2411a5557be90d7eceeea06e3f5bd1d Cargo.lock
|
||||
a27d4c4f02f92ef1d41a96800ec46a9c835d4fe5edc2184f0babfa28e6c053a6 Cargo.toml
|
||||
eb2e5fcc5cd2ab599f6bd898c99d5ce21d417b5e67b60755809224313b8287c9 AGENTS.md
|
||||
9c209f4fa88b9d2f61e85fdefadcdfa156099c3b23f767f17b07e6ff4a89e556 CHANGELOG.md
|
||||
1b57c672088350d2ce0ce81980313b8f10615cb1a235e341c2c64ae258c23a46 Cargo.lock
|
||||
2a37b391ded04b9a675e8befc6e82c06db2a05bf1ab6f4cb2e8defec193e5d81 Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE
|
||||
82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md
|
||||
@@ -28,13 +28,13 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||
60d108fd168064e4d6980ed7499f3219017f9878c743697e77f2efc592e4d629 docs/ACCESS.md
|
||||
19a4e028350f6ff786dd799a73fe7f3ec99cb5cf51cc291d359d35452df84862 docs/API.md
|
||||
5a8821f28a1f529263db462a0ed1f7c082ad91b5936491806913e5cb5b274fd9 docs/API.md
|
||||
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
||||
4fb55a549535f37467f14536cbf0db7be027ca8a019c2919a384af4f23655300 docs/CACHE.md
|
||||
32599d67a11fd0ebbc612e82b9f602789f4ed4103cda482c6870d8741100c8b4 docs/CERTIFICATES.md
|
||||
02bb383b5c3bce22616c3cc3db30a9469806dc16decb509973e84c60aa30fc5b docs/CONFIGURATION.md
|
||||
fec29ce846d6705eb45819d85d382f28e28a146b265feab2e4c7cce2a35e5883 docs/CUSTOM_LOCATIONS.md
|
||||
91ce42f259e833b6b5bd351519d27727b81dacf7879516bd024bb11dc728d7bd docs/DASHBOARD.md
|
||||
9c54b8da03adb1bc1a3db96a72fb99924dca25eed5ac662989aba18ca226d066 docs/DASHBOARD.md
|
||||
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
|
||||
3e7e63d1d8f5d5019d5e4fdbabfe44fb39c2b75e7141d9c94846d946f34849f2 docs/FEATURES.md
|
||||
3939c14e83ef6856a421f358a3f7fce3d50ea375bc1b9f0419f27a3eed658f6f docs/INSTALL.md
|
||||
@@ -88,12 +88,12 @@ de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 integrations/o
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example
|
||||
11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 integrations/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
e655bb17583e46f182226d7d2e1da45e6dffd0a8fd457e0cfb776778445e4358 integrations/routeros-oci-build/README.md
|
||||
1effe1bc8b04c5469401b8a2e8174e0aa0ba73f0c2db10f6a21e5ef7d5a8bc65 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
|
||||
9aee5697423f618e2d913f071240d288134ad1e6d9aeedc37636a5185ae33fb2 langs/en_US.json
|
||||
1a0b5a24daf53fb04f92bb4fbd5dfa501e0a5a7c0bb0dad7f58668c3be1b5110 langs/pl_pl.json
|
||||
f28ec357bafa539be82be86340651458b1002f8865411fd95a83c186cbc80a8a langs/en_US.json
|
||||
03d219fbbb4cc7902b4f3979ea91a46bc8ee3c8b1e68b59b94303cbba087dc25 langs/pl_pl.json
|
||||
9664543e4d03422788a65e42f968c1f026c2235cb5be6fc3e0ad318997077538 make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -165,23 +165,23 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file
|
||||
77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs
|
||||
743bb7436f77e04015328a0e7e7d425e43ef8aa8d61467bde157e462c1dcdc2e src/admin/logging_api.rs
|
||||
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
|
||||
500e87bb88ccc45806c0967262e76e3ee77ec5c2fbd9881d8aa26ff043f3377d src/admin/openapi.json
|
||||
cf588dd17399a752b23c3e87ba3cb6cdce02b059a6479528da7402ff517ddcdc src/admin/openapi.json
|
||||
f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 src/admin/operations.rs
|
||||
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
|
||||
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
|
||||
9080babb5094d24c2cd4b8e6efb6a516142263ff9f45219a13857dfc55f33fa4 src/admin/preferences.rs
|
||||
35dd127a37db8f18a0ddfcacc342a3028ef4ddc0cb619fd9b13b12b2ff047e86 src/admin/preferences.rs
|
||||
35a7f99c7448c90b8637f4b3a7becca138f1f378fe5dc3f87122151525bb20f9 src/admin/preferences_columns.rs
|
||||
1a7b2c2ab9c9cb00f48be34640c4f430ec0a3bc4ee3202db63b804f64e32f7b6 src/admin/preferences_tests.rs
|
||||
a238feee9afce314b8f6f7257e5488a53c4d680f33450a4e3e460fccbe54de88 src/admin/preferences_tests.rs
|
||||
62ecbae5dee00aeeca3eb806e2605372471dfab971cde8f66f11e711df8fcea1 src/admin/public/mod.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
6930b4f4b45257772b321b20a9becdf9bc4610274852948ba38c2d0cdf02dfaf src/admin/refresh_preferences.rs
|
||||
e399ff25c8f26aab429b329609beb9633a61ed6e32ca82d14cfa59978585dd35 src/admin/router.rs
|
||||
fb4ae8f958e136f46c50a3db8347ea10123c2a3a775dbea7e261aa0b3c7e7994 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
|
||||
eafb1afb4bf3dd967c18313d4725cc69edf7ad9d351b89f4e1da545f08e7e941 src/admin/tests.rs
|
||||
d8693d5341ff5be2dcda5a12bf55f173773cb9eb642862d9f4b03c4760ebf8fc src/admin/tests.rs
|
||||
c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js
|
||||
@@ -193,13 +193,14 @@ fcce3b0204ca2c679f5d8049d020619664a3c0a45889975681258d82ae329988 src/admin/web/
|
||||
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
|
||||
2bfc071e4d50b01f39650ac74380b355daf847a9b3c015cb5b3f17d8771114c6 src/admin/web/app/icons.js
|
||||
bd120d5ff23f9893ab666e0ef07f08dfb3348053351bc4f7d950647871b2f7f4 src/admin/web/app/icons.js
|
||||
e5a92fa1cb7e049082f0764b245560c41a9a34bcd8a9536fd872d9169fa5465d src/admin/web/app/logo-animation.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
|
||||
be276b8c55dc2c48770de200ce3f6e669e4c4146bcd1b4d03c4af32f025c4709 src/admin/web/app/preferences.js
|
||||
0c23e28d7799582d3a3a5835ed6c8e5868a3cac678b5be7e902652d0901404f3 src/admin/web/app/preferences.js
|
||||
9e2b12d34ab1fb29716645720577ad49b16c03b46c482a03695e8f645d170e79 src/admin/web/app/rate-sampling.js
|
||||
3b8e566d81a5b94ef21c73e7805ae128454f085554de9358040de266bcbdb0e0 src/admin/web/app/refresh.js
|
||||
c6ae53c0e95e67b9165822813efc622001be5354c19bb221a52514221d1df23e src/admin/web/app/runtime.js
|
||||
@@ -211,10 +212,10 @@ f5855db3327f51170a28114e07d207745b4d6c8822d88d24c1a81eb92f345b14 src/admin/web/
|
||||
9f323507649910561518b63cf6632e81b3ecc4b09501ae53b635705f34ea6587 src/admin/web/certificate-api.js
|
||||
a09f060b47771c56e9b9e57f1b896a32a2f87f1fb33401ac76932a91d247c67e src/admin/web/css/00-tokens.css
|
||||
422a8c04d6499f806516be1694f316a4881d7cc191072218ea851445e2493506 src/admin/web/css/10-base.css
|
||||
6ed54b27a4a81b073f1d30cf61e366f3d5c1f6f8495f0d0a8af48e061937eb4e src/admin/web/css/20-shell.css
|
||||
b8cf033180211969912cbdf1190f52129b3bf40cda4522f740d7630c8c081224 src/admin/web/css/20-shell.css
|
||||
feca0078ca889900a75d08a4a241e1a8ec787c8f4de25933d4883aac955e8023 src/admin/web/css/30-overview.css
|
||||
dd82b5e3ead91c20bb08cf8d71bbba31ea8e3d108c417913c63409f3e34da7e1 src/admin/web/css/40-editors.css
|
||||
24defe4c76df881da43498e2fc9ebd2927178f4a6f4dc964dbbae03353634008 src/admin/web/css/50-settings.css
|
||||
4a3d733d76d5f707d58c301f4dd5416c2401df8b8013f8c8836d07de99697f61 src/admin/web/css/50-settings.css
|
||||
f8b515c63080c71ee647cfd8c36628271e8357b31cd84f00cf591e9b752b44fe src/admin/web/css/60-monitoring.css
|
||||
fb7cc4d733cb9a3e320a8cb95269ca047394dded8846120ffb50aaeaaab4d749 src/admin/web/css/70-responsive.css
|
||||
8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md
|
||||
@@ -243,7 +244,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
||||
3f0525ca1d4e088ccd4988b8a117b26a8cf8c12213b24c37081d41b68d2b0ff2 src/admin/web/host404.js
|
||||
d9b226ebdd93b3e829f20e84dddb34f9bdba1ab01aefb33455102077ef0c94dd src/admin/web/index.html
|
||||
dbb063615c93ae0fd247b4e9dfbc92c772bb7bdf182fa1080c0054c8f22c6b97 src/admin/web/index.html
|
||||
7979c490b7fcfcd8f5e52e99fcf7757d78509337a625341c98f62c6da50da374 src/admin/web/integrations.js
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
ac9f667273918e18406df06cb2613c923f50a88897693a03a18f500e22dc6ec2 src/admin/web/login.html
|
||||
@@ -557,10 +558,10 @@ e5f5fbe702f66bfcbcbc1d1f3cd3d89bdbb929bb86aed94868a2a06a4b9e59e3 src/web_server
|
||||
b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server/serve.rs
|
||||
714418c86ab241020a561fcf662a8fe324a6a70cc2fd3a313c56d7939c14b9e8 src/web_server/tests.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
2f7f0453c817dc9e3d4ab61296af5821ff3e0826ad70511714afd7486c6105bc tests/panel_icons_browser.py
|
||||
5db3215d2e43e9dc9348a42b2351b7c605991ed8757f598de539f8f84e3f9593 tests/panel_icons_contract.js
|
||||
102b83171a6429b6375d8001d6c5294702385cd58ee55727cb657b1f3566e877 tests/panel_icons_browser.py
|
||||
c968beeda4a1bf626991deeb9134e43d36d44fc922cb1c1c27221cf2d5a5c2c3 tests/panel_icons_contract.js
|
||||
01af3008be27fabb486c141c67099af104f2725cc56f4c656aea9741a3c150cd tests/panel_interface_contract.py
|
||||
24a815a280de87a7ccec6f4d212d1468022c844e9e7d0b113349ff26bde5fc52 tests/panel_refresh_contract.js
|
||||
40d7fa6e0b8111c0c9815cff43f235984c49b12ffd3947eb2542400fc807b621 tests/panel_refresh_contract.js
|
||||
e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js
|
||||
eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md
|
||||
7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py
|
||||
|
||||
Regular → Executable
File mode changed.
+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, including theme/layout and optional `interface_icons` (default `false`). |
|
||||
| `GET/PUT /api/preferences` | Personal UI settings, including theme/layout and optional `interface_icons` and `logo_animation` (both default `false`). |
|
||||
| `GET /api/dashboard-presets` and named preset operations | Saved Overview presets. |
|
||||
| `GET /docs/openapi.json` | Protected machine-readable API specification. |
|
||||
|
||||
|
||||
@@ -47,6 +47,12 @@ Under **Settings → Appearance → Interface**, enable **Show SVG interface ico
|
||||
|
||||
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.
|
||||
|
||||
Controls whose label is already a single symbol (`×`, `−`, `+`, `←`, `→`, `↑`, `↓`) keep that glyph and do not receive a second SVG. Verbs take precedence over the surrounding feature: a **Close** button in the health or history dialog shows a close icon, **Previous/Next** use chevrons, and **Unlock layout** uses an open padlock.
|
||||
|
||||
### Logo animation
|
||||
|
||||
Under **Settings → Appearance → Interface**, enable **Animate logo** to let the sidebar `r/p` logo play a short, subtle effect (rotating slash, letter flip, wave, glow, sheen, glitch or wobble) at random intervals of roughly 10–25 seconds and when the pointer moves over it. The preference is stored per administrator as `logo_animation: true|false` in `/api/preferences` and is **off by default**. Animations pause while the browser tab is hidden and are suppressed entirely when the operating system or browser requests reduced motion.
|
||||
|
||||
## Editing configuration safely
|
||||
|
||||
1. Open the relevant workspace and inspect existing state before editing.
|
||||
|
||||
@@ -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.1` and the defaults this publishes:
|
||||
For example, with RustProxy version `0.36.2` and the defaults this publishes:
|
||||
|
||||
```text
|
||||
zot.example.net/network/rustproxy:0.36.1
|
||||
zot.example.net/network/rustproxy:0.36.2
|
||||
zot.example.net/network/rustproxy:latest
|
||||
zot.example.net/network/rustproxy:routeros
|
||||
```
|
||||
|
||||
@@ -2404,6 +2404,8 @@
|
||||
"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.",
|
||||
"logoAnimation": "Animate logo",
|
||||
"logoAnimationHelp": "Plays short, subtle animations of the sidebar logo from time to time and on hover.",
|
||||
"entityEnableHint": "Controls whether this item is active.",
|
||||
"setupCommandExample": "Standard service installation example:",
|
||||
"certificateRuntimeTitle": "ACME runtime",
|
||||
|
||||
@@ -2404,6 +2404,8 @@
|
||||
"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.",
|
||||
"logoAnimation": "Animuj logo",
|
||||
"logoAnimationHelp": "Od czasu do czasu (i po najechaniu kursorem) odtwarza krótkie, subtelne animacje logo w panelu bocznym.",
|
||||
"entityEnableHint": "Określa, czy ten element jest aktywny.",
|
||||
"setupCommandExample": "Przykład dla standardowej instalacji usługi:",
|
||||
"certificateRuntimeTitle": "Środowisko ACME",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.36.1",
|
||||
"version": "0.36.2",
|
||||
"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": [
|
||||
@@ -3827,6 +3827,11 @@
|
||||
"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."
|
||||
},
|
||||
"logo_animation": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped administration UI preference. When enabled, the sidebar logo plays short, subtle animations at random intervals and on hover. Paused while the tab is hidden and suppressed when the browser requests reduced motion."
|
||||
},
|
||||
"button_style": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -120,6 +120,8 @@ struct AdminPreferences {
|
||||
chart_axes: bool,
|
||||
#[serde(default)]
|
||||
interface_icons: bool,
|
||||
#[serde(default)]
|
||||
logo_animation: bool,
|
||||
#[serde(default = "default_admin_button_style")]
|
||||
button_style: String,
|
||||
#[serde(default = "default_admin_corner_style")]
|
||||
@@ -183,6 +185,7 @@ impl Default for AdminPreferences {
|
||||
chart_style: default_admin_chart_style(),
|
||||
chart_axes: false,
|
||||
interface_icons: false,
|
||||
logo_animation: false,
|
||||
button_style: default_admin_button_style(),
|
||||
corner_style: default_admin_corner_style(),
|
||||
table_density: default_admin_table_density(),
|
||||
|
||||
@@ -42,6 +42,20 @@ fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
assert!(validate_admin_preferences(&AdminPreferences::default()).is_ok());
|
||||
assert!(!AdminPreferences::default().chart_axes);
|
||||
assert!(!AdminPreferences::default().interface_icons);
|
||||
assert!(!AdminPreferences::default().logo_animation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_logo_animation_preference_is_backward_compatible_and_off_by_default() {
|
||||
let legacy: AdminPreferences = serde_json::from_str(r#"{"theme":"light"}"#).unwrap();
|
||||
assert!(!legacy.logo_animation);
|
||||
let mut enabled = legacy.clone();
|
||||
enabled.logo_animation = true;
|
||||
assert!(validate_admin_preferences(&enabled).is_ok());
|
||||
let stored = serde_json::to_string(&enabled).unwrap();
|
||||
assert!(stored.contains("\"logo_animation\":true"));
|
||||
let recovered: AdminPreferences = serde_json::from_str(&stored).unwrap();
|
||||
assert!(recovered.logo_animation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -58,6 +58,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/logo-animation.js"),
|
||||
include_str!("web/app/path-permissions.js"),
|
||||
include_str!("web/app/session-resume.js"),
|
||||
include_str!("web/app/refresh.js"),
|
||||
|
||||
@@ -2147,6 +2147,7 @@ mod tests {
|
||||
assert_eq!(initial["chart_style"], "default");
|
||||
assert_eq!(initial["chart_axes"], false);
|
||||
assert_eq!(initial["interface_icons"], false);
|
||||
assert_eq!(initial["logo_animation"], false);
|
||||
assert_eq!(initial["button_style"], "normal");
|
||||
assert_eq!(initial["corner_style"], "default");
|
||||
assert_eq!(
|
||||
@@ -2185,6 +2186,7 @@ mod tests {
|
||||
assert_eq!(legacy["chart_style"], "default");
|
||||
assert_eq!(legacy["chart_axes"], false);
|
||||
assert_eq!(legacy["interface_icons"], false);
|
||||
assert_eq!(legacy["logo_animation"], false);
|
||||
assert_eq!(legacy["button_style"], "normal");
|
||||
assert_eq!(legacy["corner_style"], "default");
|
||||
assert_eq!(
|
||||
@@ -2208,6 +2210,7 @@ mod tests {
|
||||
"chart_style":"neon",
|
||||
"chart_axes":true,
|
||||
"interface_icons":true,
|
||||
"logo_animation":true,
|
||||
"button_style":"compact",
|
||||
"corner_style":"rounded",
|
||||
"table_density":"ultra",
|
||||
|
||||
+84
-29
@@ -3,6 +3,7 @@ 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" }]],
|
||||
ban: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "m5.6 5.6 12.8 12.8" }]],
|
||||
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" }]],
|
||||
@@ -13,11 +14,15 @@ const UI_ICON_SHAPES = Object.freeze({
|
||||
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" }]],
|
||||
collapse: [["path", { d: "M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7" }]],
|
||||
compare: [["rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }], ["path", { d: "M12 4v16M6 9h3M6 13h3M15 11h3M15 15h3" }]],
|
||||
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" }]],
|
||||
eraser: [["path", { d: "m7 20-3.3-3.3a1 1 0 0 1 0-1.4l9.6-9.6a1 1 0 0 1 1.4 0l5.6 5.6a1 1 0 0 1 0 1.4L13 20H7ZM21 20H7M5.5 11.5l7 7" }]],
|
||||
expand: [["path", { d: "M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" }]],
|
||||
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" }]],
|
||||
@@ -26,33 +31,40 @@ const UI_ICON_SHAPES = Object.freeze({
|
||||
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" }]],
|
||||
hash: [["path", { d: "M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" }]],
|
||||
history: [["path", { d: "M4 8V4m0 0h4M4 4l3 3a8 8 0 1 1-2.2 7" }], ["path", { d: "M12 8v5l3 2" }]],
|
||||
info: [["circle", { cx: "12", cy: "12", r: "9" }], ["path", { d: "M12 11v5" }], ["circle", { cx: "12", cy: "7.8", r: ".8", fill: "currentColor", stroke: "none" }]],
|
||||
key: [["circle", { cx: "8", cy: "12", r: "4" }], ["path", { d: "M12 12h9M18 12v3M15 12v2" }]],
|
||||
layers: [["path", { d: "m12 3 9 5-9 5-9-5 9-5Z" }], ["path", { d: "m3 12.5 9 5 9-5M3 16.5l9 5 9-5" }]],
|
||||
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" }]],
|
||||
login: [["path", { d: "M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 8l4 4-4 4M14 12H3" }]],
|
||||
logout: [["path", { d: "M9 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h3M16 8l4 4-4 4M20 12H9" }]],
|
||||
mapPin: [["path", { d: "M12 21s-7-6.2-7-11a7 7 0 0 1 14 0c0 4.8-7 11-7 11Z" }], ["circle", { cx: "12", cy: "10", r: "2.5" }]],
|
||||
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" }]],
|
||||
pin: [["path", { d: "M8 3h8M10 3v5l-3 4h10l-3-4V3M12 12v9" }]],
|
||||
play: [["path", { d: "m8 5 11 7-11 7V5Z" }]],
|
||||
plug: [["path", { d: "M9 3v5M15 3v5M6 8h12v3a6 6 0 0 1-12 0V8ZM12 17v4" }]],
|
||||
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" }]],
|
||||
settings: [["path", { d: "M18.9 9.9L21.3 10.3L21.3 13.7L18.9 14.1L18.4 15.4L19.8 17.4L17.4 19.8L15.4 18.4L14.1 18.9L13.7 21.3L10.3 21.3L9.9 18.9L8.6 18.4L6.6 19.8L4.2 17.4L5.6 15.4L5.1 14.1L2.7 13.7L2.7 10.3L5.1 9.9L5.6 8.6L4.2 6.6L6.6 4.2L8.6 5.6L9.9 5.1L10.3 2.7L13.7 2.7L14.1 5.1L15.4 5.6L17.4 4.2L19.8 6.6L18.4 8.6Z" }], ["circle", { cx: "12", cy: "12", r: "3" }]],
|
||||
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" }]],
|
||||
transfer: [["path", { d: "M4 8h15m0 0-4-4m4 4-4 4M20 16H5m0 0 4-4m-4 4 4 4" }]],
|
||||
trash: [["path", { d: "M4 7h16M9 3h6l1 4H8l1-4ZM7 7l1 14h8l1-14M10 11v6M14 11v6" }]],
|
||||
toggleOff: [["rect", { x: "3", y: "7", width: "18", height: "10", rx: "5" }], ["circle", { cx: "8", cy: "12", r: "3", fill: "currentColor", stroke: "none" }]],
|
||||
toggleOn: [["rect", { x: "3", y: "7", width: "18", height: "10", rx: "5" }], ["circle", { cx: "16", cy: "12", r: "3", fill: "currentColor", stroke: "none" }]],
|
||||
undo: [["path", { d: "M9 7H4v-5M4 7l4-4M4.5 8A8 8 0 1 1 5 17" }]],
|
||||
undo: [["path", { d: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11" }]],
|
||||
unlock: [["rect", { x: "5", y: "10", width: "14", height: "10", rx: "2" }], ["path", { d: "M8 10V7a4 4 0 0 1 7.8-1.2" }]],
|
||||
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" }]],
|
||||
@@ -61,13 +73,13 @@ const UI_ICON_SHAPES = Object.freeze({
|
||||
|
||||
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",
|
||||
redirects: "route", notfound: "fileX", streams: "transfer", "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",
|
||||
appearance: "palette", listeners: "plug", connections: "globe", "docker-agents": "box",
|
||||
policies: "shield", cache: "database", transform: "wand", forwarding: "route", pages: "file",
|
||||
logging: "list", geoip: "globe", prometheus: "activity", administration: "settings",
|
||||
logging: "list", geoip: "mapPin", prometheus: "activity", administration: "users",
|
||||
"system-info": "cpu", "certificate-api": "key", "tls-policy": "lock", "manual-config": "code", security: "shield",
|
||||
});
|
||||
const UI_VHOST_TAB_ICONS = Object.freeze({
|
||||
@@ -75,17 +87,19 @@ const UI_VHOST_TAB_ICONS = Object.freeze({
|
||||
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"]],
|
||||
["trash", ["remove", "delete", "draftDiscard", "cacheRemoveBan", "cachePurgeUrl", "certificateApiRevoke", "certificateApiRevokeAll", "apiTokenRevoke", "revokeAll"]],
|
||||
["x", ["close", "cancelAction", "overviewLayoutInlineCancel"]],
|
||||
["save", ["saveCertificate", "savePassword", "dashboardPresetsSave", "overviewLayoutInlineSave"]],
|
||||
["check", ["apply", "applyFilters", "savePolicy", "continueAction", "webServicePresetApply", "dockerApplyService"]],
|
||||
["save", ["certAssignApply", "saveCertificate", "savePassword", "dashboardPresetsSave", "overviewLayoutInlineSave"]],
|
||||
["check", ["apply", "applyFilters", "savePolicy", "continueAction", "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"]],
|
||||
["eyeOff", ["hideMetric", "overviewLayoutEditorHide"]],
|
||||
["edit", ["edit", "certificateApiEdit"]],
|
||||
["key", ["certificateApiRegenerate"]],
|
||||
["download", ["export"]],
|
||||
["upload", ["acmeImport", "aclProfileImportRules"]],
|
||||
["eye", ["certificateReadNames", "dockerShowHidden", "dashboardWidgetsShowAll"]],
|
||||
["eye", ["certificateReadNames", "dockerShowHidden", "overviewLayoutEditorShow"]],
|
||||
["settings", ["settings", "vhManage", "dockerAgentsSettings"]],
|
||||
["book", ["webServiceHtaccessDocs", "webServicePhpDocsButton"]],
|
||||
["pause", ["logPause"]],
|
||||
@@ -116,20 +130,52 @@ 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;
|
||||
const raw = String(t(key) || "").trim(), translated = raw.toLocaleLowerCase();
|
||||
// t() returns the key itself when a translation is missing; compare case-sensitively
|
||||
// so an English label such as "Edit" for the key "edit" still counts as translated.
|
||||
if (!translated || raw === key) continue;
|
||||
if (strings.some(value => value === translated || value.startsWith(`${translated} `) || value.startsWith(`${translated}:`))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Explicit choices for controls whose ids/i18n keys would otherwise collide with
|
||||
// broader keyword rules (for example "unlock" vs "lock", "presets" vs "reset",
|
||||
// "docker" vs "dock", or a domain noun such as "cache" before a verb such as "next").
|
||||
const UI_ID_ICONS = Object.freeze({
|
||||
"about-open": "info", "login-submit": "login",
|
||||
"overview-layout-unlock": "unlock", "overview-inline-save": "lock", "overview-inline-compact": "grid",
|
||||
"overview-layout-open": "layers", "overview-layout-open-settings": "layers",
|
||||
"overview-presets-close": "x", "overview-presets-save": "save", "overview-presets-factory": "undo",
|
||||
"docker-settings-open": "settings", "settings-vhost-open": "network", "settings-file-logs-open": "list",
|
||||
"log-format-open": "sliders", "file-log-tab-counts": "hash", "refresh-reset": "undo",
|
||||
"acme-submit": "certificate", "geoip-update-now": "download",
|
||||
"cache-panel-cleanup": "eraser", "cache-invalidation-open": "ban", "cache-bans-open": "ban",
|
||||
"prom-token-generate": "key", "certificate-api-save": "key", "revoke-sessions": "logout",
|
||||
"acl-profile-reset": "eraser", "reset-certificate": "eraser",
|
||||
"config-history-compare-open": "compare", "logs-load-history": "history",
|
||||
});
|
||||
const UI_I18N_ICONS = Object.freeze({
|
||||
apply: "check", fileLogsCountRepeats: "hash", cacheRunInvalidation: "play",
|
||||
previous: "chevronLeft", next: "chevronRight", cachePrevious: "chevronLeft", cacheNext: "chevronRight",
|
||||
renewalEnable: "toggleOn", renewalDisable: "toggleOff", tunnelKill: "ban", certAssignVhosts: "network",
|
||||
});
|
||||
// Buttons whose visible label is already a single symbol (×, −, +, ←, →) keep the
|
||||
// native glyph instead of receiving a second, competing SVG.
|
||||
const UI_GLYPH_LABEL = /^[×✕−+←→↑↓⠿]$/;
|
||||
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("#tunnels-open, .vhost-backend-protocol-badge, .config-history-day, .draft-dock-toggle")) return null;
|
||||
if (UI_GLYPH_LABEL.test(String(target.textContent || "").trim())) return null;
|
||||
if (UI_ID_ICONS[target.id]) return UI_ID_ICONS[target.id];
|
||||
if (target.matches("#web-service-preset-apply, [data-cache-memory-preset], [data-cache-cold-preset], [data-transform-preset]")) return "wand";
|
||||
if (target.matches("#logs-pause")) return uiIconMatchesTranslation(target, ["logResume"]) ? "play" : "pause";
|
||||
if (target.matches("#docker-metrics-toggle-all, #dashboard-widget-expand")) return target.getAttribute("aria-expanded") === "true" ? "collapse" : "expand";
|
||||
if (target.matches(".docker-open-logs")) return "list";
|
||||
if (target.matches(".docker-view-toggle")) return uiIconMatchesTranslation(target, ["dockerUnhideVhost"]) ? "eye" : "eyeOff";
|
||||
if (target.matches(".vhost-quick-toggle.is-enable")) return "toggleOn";
|
||||
if (target.matches(".vhost-quick-toggle.is-disable")) return "toggleOff";
|
||||
if (target.matches(".listener-h2-settings-link, .listener-h3-settings-link")) return "settings";
|
||||
@@ -145,6 +191,9 @@ function uiIconForTarget(target) {
|
||||
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 i18nKey = target.dataset.i18n || target.dataset.action || target.dataset.presetAction || "";
|
||||
if (UI_I18N_ICONS[i18nKey]) return UI_I18N_ICONS[i18nKey];
|
||||
for (const [key, icon] of Object.entries(UI_I18N_ICONS)) if (uiIconMatchesTranslation(target, [key]) && !["previous", "next", "apply"].includes(key)) return icon;
|
||||
|
||||
const signature = [target.id, target.dataset.i18n, target.dataset.i18nAria, target.dataset.action, target.className]
|
||||
.filter(Boolean).join(" ").toLocaleLowerCase();
|
||||
@@ -152,10 +201,17 @@ function uiIconForTarget(target) {
|
||||
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 (/scroll-prev|move-earlier|layout-up|-prev\b/.test(signature)) return "chevronLeft";
|
||||
if (/scroll-next|move-later|layout-down|-next\b/.test(signature)) return "chevronRight";
|
||||
// Verbs win over domain nouns: a "Close" button inside the health or history
|
||||
// dialog is still a close action.
|
||||
if (/close|cancel/.test(signature)) return "x";
|
||||
if (/sign-in|login/.test(signature)) return "login";
|
||||
if (/logout|sign-out/.test(signature)) return "logout";
|
||||
if (/unlock/.test(signature)) return "unlock";
|
||||
if (/collapse|narrow/.test(signature)) return "collapse";
|
||||
if (/expand|widen/.test(signature)) return "expand";
|
||||
if (/compare|diff/.test(signature)) return "compare";
|
||||
if (/health/.test(signature) && !/certificate/.test(signature)) return "activity";
|
||||
if (/copy/.test(signature)) return "copy";
|
||||
if (/export|download/.test(signature)) return "download";
|
||||
@@ -163,21 +219,20 @@ function uiIconForTarget(target) {
|
||||
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 (/(^|[^p])reset/.test(signature)) return "undo";
|
||||
if (/refresh|reload|retry|renew|update-now|updatenow/.test(signature)) return "refresh";
|
||||
if (/reset/.test(signature)) return "undo";
|
||||
if (/history|revision/.test(signature)) return "history";
|
||||
if (/revoke|remove|delete|discard|purge|kill/.test(signature)) return "trash";
|
||||
if (/close|cancel/.test(signature)) return "x";
|
||||
if (/preset/.test(signature)) return "layers";
|
||||
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 (/logs-pin|logpin|(^|[\s-])pin($|[\s-])|(^|[\s-])dock($|[\s-])/.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 (/(^|[\s-])lock($|[\s-])/.test(signature)) return "lock";
|
||||
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";
|
||||
@@ -192,8 +247,8 @@ function uiIconForTarget(target) {
|
||||
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 (/layout|dashboard|overview/.test(signature)) return "grid";
|
||||
if (/about|info/.test(signature)) return "info";
|
||||
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") {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Optional, account-scoped subtle animation of the sidebar r/p logo
|
||||
// (Settings → Appearance → Interface → Animate logo). Off by default.
|
||||
// Effects are short (≤1.2 s), run at random intervals, pause while the tab is
|
||||
// hidden and are skipped entirely when the system asks for reduced motion.
|
||||
const LOGO_ANIMATION_EFFECTS = Object.freeze(["spin-slash", "flip", "wave", "glow", "sheen", "glitch", "wobble"]);
|
||||
const LOGO_ANIMATION_DURATION_MS = 1400;
|
||||
let logoAnimationTimer = null, logoAnimationReset = null, logoAnimationLast = "", logoAnimationHoverBound = false;
|
||||
function logoAnimationMark() { return document.querySelector(".sidebar a.brand > .brand-mark") || document.querySelector("a.brand > .brand-mark"); }
|
||||
function logoAnimationReducedMotion() { return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true; }
|
||||
function prepareLogoAnimationMark(mark) {
|
||||
if (mark.dataset.logoPrepared === "true") return;
|
||||
// Wrap the bare "r" and "p" text nodes so each letter can be transformed on its own.
|
||||
for (const node of [...mark.childNodes]) {
|
||||
if (node.nodeType !== Node.TEXT_NODE || !node.textContent.trim()) continue;
|
||||
const letter = document.createElement("b"); letter.className = "brand-letter"; letter.textContent = node.textContent.trim();
|
||||
node.replaceWith(letter);
|
||||
}
|
||||
mark.dataset.logoPrepared = "true";
|
||||
}
|
||||
function playLogoAnimation(effect) {
|
||||
const mark = logoAnimationMark();
|
||||
if (!mark || document.documentElement.dataset.logoAnimation !== "true" || document.hidden || logoAnimationReducedMotion() || mark.dataset.logoEffect) return;
|
||||
prepareLogoAnimationMark(mark);
|
||||
const choices = LOGO_ANIMATION_EFFECTS.filter(name => name !== logoAnimationLast);
|
||||
const next = LOGO_ANIMATION_EFFECTS.includes(effect) ? effect : choices[Math.floor(Math.random() * choices.length)];
|
||||
logoAnimationLast = next; mark.dataset.logoEffect = next;
|
||||
clearTimeout(logoAnimationReset);
|
||||
logoAnimationReset = setTimeout(() => { delete mark.dataset.logoEffect; }, LOGO_ANIMATION_DURATION_MS);
|
||||
}
|
||||
function scheduleLogoAnimation() {
|
||||
clearTimeout(logoAnimationTimer);
|
||||
if (document.documentElement.dataset.logoAnimation !== "true") return;
|
||||
const delay = 9000 + Math.random() * 14000;
|
||||
logoAnimationTimer = setTimeout(() => { playLogoAnimation(); scheduleLogoAnimation(); }, delay);
|
||||
}
|
||||
function setLogoAnimationEnabled(value) {
|
||||
const enabled = value === true;
|
||||
document.documentElement.dataset.logoAnimation = String(enabled);
|
||||
const input = $("ui-logo-animation"); if (input) input.checked = enabled;
|
||||
const mark = logoAnimationMark();
|
||||
if (!enabled) {
|
||||
clearTimeout(logoAnimationTimer); clearTimeout(logoAnimationReset); logoAnimationTimer = null;
|
||||
if (mark) delete mark.dataset.logoEffect;
|
||||
return false;
|
||||
}
|
||||
if (mark) prepareLogoAnimationMark(mark);
|
||||
if (!logoAnimationHoverBound) {
|
||||
mark?.closest("a.brand")?.addEventListener("mouseenter", () => playLogoAnimation());
|
||||
logoAnimationHoverBound = !!mark;
|
||||
}
|
||||
if (!logoAnimationTimer) { scheduleLogoAnimation(); setTimeout(() => playLogoAnimation(), 600); }
|
||||
return true;
|
||||
}
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden && document.documentElement.dataset.logoAnimation === "true") scheduleLogoAnimation(); });
|
||||
window.rustProxyLogoAnimation = { setEnabled: setLogoAnimationEnabled, play: playLogoAnimation, effects: LOGO_ANIMATION_EFFECTS };
|
||||
@@ -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, interface_icons: false,
|
||||
chart_axes: false, interface_icons: false, logo_animation: 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" });
|
||||
@@ -124,6 +124,7 @@ function applyAdminChartAxes(value) {
|
||||
return enabled;
|
||||
}
|
||||
function applyAdminInterfaceIcons(value) { return window.rustProxyIcons?.setEnabled?.(value === true) ?? false; }
|
||||
function applyAdminLogoAnimation(value) { return window.rustProxyLogoAnimation?.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;
|
||||
@@ -288,7 +289,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), 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),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), interface_icons: applyAdminInterfaceIcons(value.interface_icons), logo_animation: applyAdminLogoAnimation(value.logo_animation), 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;
|
||||
@@ -336,7 +337,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.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.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.logo_animation = next.logo_animation === 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;
|
||||
@@ -344,7 +345,7 @@ function updateAdminPreferences(patch) {
|
||||
});
|
||||
adminPreferencesWrite = task.catch(() => undefined); return task;
|
||||
}
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); applyAdminInterfaceIcons(false); window.rustProxyRefresh?.update(REFRESH_DEFAULTS); }
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); applyAdminInterfaceIcons(false); applyAdminLogoAnimation(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;
|
||||
@@ -428,6 +429,14 @@ $("ui-interface-icons")?.addEventListener("change", () => {
|
||||
applyAdminInterfaceIcons(previous);
|
||||
});
|
||||
});
|
||||
$("ui-logo-animation")?.addEventListener("change", () => {
|
||||
const control = $("ui-logo-animation"), previous = adminPreferences?.logo_animation === true, next = control.checked;
|
||||
applyAdminLogoAnimation(next); if (adminPreferences) adminPreferences.logo_animation = next;
|
||||
void saveAppearancePreference({ logo_animation: next }, control, () => {
|
||||
if (adminPreferences) adminPreferences.logo_animation = previous;
|
||||
applyAdminLogoAnimation(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); });
|
||||
@@ -459,7 +468,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", interface_icons: false, 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, logo_animation: 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);
|
||||
|
||||
@@ -869,4 +869,160 @@ body[data-density=compact] .page-intro .lead {
|
||||
to {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Optional subtle logo animation (Settings → Appearance → Animate logo). */
|
||||
.brand-mark .brand-letter {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
html[data-logo-animation="true"] a.brand>.brand-mark {
|
||||
overflow: hidden;
|
||||
transition: box-shadow .4s ease;
|
||||
}
|
||||
|
||||
html[data-logo-animation="true"] a.brand>.brand-mark>.brand-letter,
|
||||
html[data-logo-animation="true"] a.brand>.brand-mark>span {
|
||||
display: inline-block;
|
||||
transform-origin: 50% 55%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="spin-slash"]>span {
|
||||
animation: brand-logo-spin 1.1s cubic-bezier(.55, 0, .25, 1);
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="flip"]>.brand-letter {
|
||||
animation: brand-logo-flip .9s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="flip"]>.brand-letter:last-child {
|
||||
animation-delay: .18s;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wave"]>* {
|
||||
animation: brand-logo-wave .7s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wave"]>:nth-child(2) {
|
||||
animation-delay: .1s;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wave"]>:nth-child(3) {
|
||||
animation-delay: .2s;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="glow"] {
|
||||
animation: brand-logo-glow 1.3s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="sheen"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(105deg, transparent 30%, color-mix(in srgb, var(--logo-accent, var(--accent)) 28%, transparent) 50%, transparent 70%);
|
||||
animation: brand-logo-sheen 1s ease-in-out;
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="glitch"]>* {
|
||||
animation: brand-logo-glitch .45s steps(2, end);
|
||||
}
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect="wobble"]>span {
|
||||
animation: brand-logo-wobble .9s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes brand-logo-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-flip {
|
||||
to {
|
||||
transform: rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-wave {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-glow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 12px 1px color-mix(in srgb, var(--logo-accent, var(--accent)) 45%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-sheen {
|
||||
from {
|
||||
transform: translateX(-110%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(110%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-glitch {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-1px);
|
||||
text-shadow: 1px 0 var(--logo-secondary, var(--brand-secondary)), -1px 0 var(--logo-accent, var(--accent));
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateX(1px);
|
||||
text-shadow: -1px 0 var(--logo-secondary, var(--brand-secondary)), 1px 0 var(--logo-accent, var(--accent));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes brand-logo-wobble {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(-16deg);
|
||||
}
|
||||
|
||||
55% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
a.brand>.brand-mark[data-logo-effect],
|
||||
a.brand>.brand-mark[data-logo-effect]>*,
|
||||
a.brand>.brand-mark[data-logo-effect]::after {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -82,12 +82,14 @@
|
||||
}
|
||||
|
||||
.appearance-interface-icons-setting,
|
||||
.appearance-logo-animation-setting,
|
||||
.appearance-chart-axes-setting {
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.appearance-interface-icons-setting>span,
|
||||
.appearance-logo-animation-setting>span,
|
||||
.appearance-chart-axes-setting>span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
@@ -1799,6 +1799,11 @@
|
||||
data-i18n="interfaceIconsHelp">Adds semantic SVG icons to
|
||||
navigation,
|
||||
actions, tabs and dialogs.</small></span></label>
|
||||
<label class="check appearance-logo-animation-setting"><input
|
||||
id="ui-logo-animation" type="checkbox" /><span><b
|
||||
data-i18n="logoAnimation">Animate logo</b><small
|
||||
data-i18n="logoAnimationHelp">Plays short, subtle animations of
|
||||
the sidebar logo from time to time and on hover.</small></span></label>
|
||||
<label><span data-i18n="cornerStyle">Corner rounding</span><select
|
||||
id="ui-corner-style">
|
||||
<option data-i18n="cornerStyleDefault" value="default">Original
|
||||
|
||||
@@ -32,13 +32,13 @@ function t(key) { return translations[key] || key; }
|
||||
(() => {
|
||||
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"));
|
||||
const eligible = [...document.querySelectorAll("button, a.primary, a.outline, a.quiet")].filter(node => window.rustProxyIcons.iconFor(node) !== null);
|
||||
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" };
|
||||
const expectedPanels = { overview: "grid", vhosts: "network", "web-server": "globe", "docker-vhosts": "box", redirects: "route", notfound: "fileX", streams: "transfer", "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}`);
|
||||
|
||||
@@ -47,4 +47,28 @@ for (const icon of ["plus", "save", "check", "trash", "edit", "refresh", "settin
|
||||
assert.ok(new RegExp(`\\b${icon}:`).test(icons), `missing semantic SVG icon definition: ${icon}`);
|
||||
}
|
||||
|
||||
// 0.36.2 semantic review: verbs/explicit ids must win over colliding keywords.
|
||||
const explicitIcons = { "overview-layout-unlock": "unlock", "overview-inline-save": "lock", "overview-layout-open": "layers", "docker-settings-open": "settings", "login-submit": "login", "revoke-sessions": "logout", "about-open": "info" };
|
||||
for (const [id, icon] of Object.entries(explicitIcons)) assert.match(icons, new RegExp(`"${id}": "${icon}"`), `${id} must use ${icon}`);
|
||||
assert.match(icons, /\(\^\|\[\^p\]\)reset/, "preset buttons must not match the reset rule");
|
||||
assert.match(icons, /UI_GLYPH_LABEL/);
|
||||
assert.match(icons, /raw === key/, "English labels equal to their key must still count as translated");
|
||||
for (const icon of ["unlock", "layers", "expand", "collapse", "ban", "eraser", "hash", "info", "plug", "transfer", "mapPin", "compare"]) {
|
||||
assert.ok(new RegExp(`\\b${icon}:`).test(icons), `missing semantic SVG icon definition: ${icon}`);
|
||||
}
|
||||
|
||||
const logo = read("src/admin/web/app/logo-animation.js");
|
||||
const router = read("src/admin/router.rs");
|
||||
const shellCss = read("src/admin/web/css/20-shell.css");
|
||||
assert.match(html, /id="ui-logo-animation"[^>]*type="checkbox"/);
|
||||
assert.match(router, /web\/app\/logo-animation\.js/);
|
||||
assert.match(preferences, /logo_animation:\s*false/);
|
||||
assert.match(preferences, /applyAdminLogoAnimation\(value\.logo_animation\)/);
|
||||
assert.match(preferences, /saveAppearancePreference\(\{ logo_animation: next \}/);
|
||||
assert.match(logo, /prefers-reduced-motion: reduce/);
|
||||
assert.match(logo, /document\.hidden/);
|
||||
assert.match(shellCss, /prefers-reduced-motion: reduce[\s\S]*data-logo-effect/);
|
||||
assert.equal(openapi.components.schemas.AdminPreferences.properties.logo_animation.default, false);
|
||||
assert.ok(pl.translations.logoAnimation && pl.translations.logoAnimationHelp);
|
||||
|
||||
console.log("Panel SVG icon preference contract: OK");
|
||||
@@ -51,5 +51,5 @@ 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");
|
||||
assert.equal(openapi.info.version, "0.36.2");
|
||||
console.log("Panel refresh/API contract checks: OK");
|
||||
Reference in new issue
Block a user