diff --git a/AGENTS.md b/AGENTS.md index 45cf32a..9ec47b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # RustProxy source and documentation maintenance -Current source version: **0.36.2**. The Cargo package and binary are named `rust-proxy`. +Current source version: **0.36.3**. 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index b808a24..9036bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,28 @@ 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.3 — settings, cache and dashboard layout polish + +- **More chart styles:** Settings → Appearance → Chart style now offers eight additional presets: **Sunset**, **Aurora**, **Ocean**, **Forest**, **Pastel**, **Terminal** (green phosphor, square lines), **Blueprint** (thin technical lines on a blue tint) and **Colorblind safe** (Okabe–Ito palette with dashed grid). The `chart_style` preference accepts the new values (`sunset`, `aurora`, `ocean`, `forest`, `pastel`, `terminal`, `blueprint`, `colorblind`); existing values and the default are unchanged. +- **Cache → Domains and effectiveness:** numeric columns are right-aligned with tabular digits, HIT / MISS / BYPASS counts use the same colours as the Dashboard cache bar, the hit ratio has a small meter, domain names are emphasised and decision-counter chips are quieter. +- **Docker vhosts:** collapsed service rows are compact on desktop (publish switch, name with image/ports, and health with actions in one row instead of a stacked column). +- **Cache and Static response transformation pages:** the Cache → General tab is split into *Behavior* and *Memory and limits* groups (the fallback TTL and bypass options moved up next to the other behavior switches), switch descriptions sit directly under their checkbox, the memory/transform preset cards have a compact header, and numeric fields use a responsive grid (up to four per row) instead of two fixed columns. +- **Settings layout:** Settings → Appearance now uses one-setting-per-row layout (label and help on the left, control on the right, checkboxes on the right edge) instead of an uneven two-column grid, section headings no longer duplicate their first setting (`Charts`, `Tables and logs`) and the small eyebrow labels were dropped. Sub-tabs (Rules & headers, Cache, HTTP connections, Logging, …) are a horizontal tab bar on desktop instead of a vertical stack of full-width buttons, and the main settings menu has thin separators between its groups. +- **Compact stat cards:** Dashboard cards that consist of a title, a value and one secondary line (Requests, Active exchanges, Payload transferred, Errors, Process memory, HTTP/2 requests, backend and stream cards, …) now show that line to the right of the value, which makes them about 20 px lower. WebSocket tunnels shows a plain `H1 · H2 · H3` split (full names in the tooltip) HTTP/2 shows `H2 connections` and HTTP/3 now shows `H3 connections` (active HTTP/3 connections); the labels were shortened in `langs/en_US.json` and `langs/pl_pl.json`. +- **WebSocket tunnels card:** the *Established* count moved from its own text line to the right of the main value (it wraps below only when the card is too narrow), so the card is one line shorter. +- **Cache outcomes bar:** the HIT / MISS / BYPASS / OTHER segments of the Dashboard cache bar now show their percentage inside the bar (bar height 12 → 20 px with tighter margins, so the card keeps its height). A label that does not fit a narrow segment is hidden automatically and re-evaluated on resize; the legend below keeps the exact count and percentage. +- **CSS cleanup:** removed orphaned panel styles: the legacy `body[data-density=…]` rules (46 selectors across 7 modules; density is controlled only by `html[data-table-density]` and `html[data-dashboard-density]`), the unused `.badge` branch of the corner-style selector list, references to undefined custom properties (`--radius`, `--surface`, `--line-strong`; their fallbacks are now inlined) and duplicated `.path-permissions-*` blocks. A static audit found no other unused classes, ids, keyframes or custom properties. Computed styles of all panels at 402 px and 1280 px (several corner/density/icon settings) are identical before and after; the bundle is about 3 KB smaller. +- **Upgrade notes:** interface-only release; no configuration or database migration is required. New interface labels were added to `langs/en_US.json` and `langs/pl_pl.json` (`appearanceChartsTitle`, `appearanceTablesTitle`, `cacheSectionBehavior`, `cacheSectionMemory`, `h3ActiveConnections`, `chartStyleSunset` … `chartStyleColorblind`) and the WebSocket / HTTP/2 labels were shortened. + ## 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. +- **Mobile/Safari chart fixes:** with SVG icons enabled, the **Hide** / **Show** buttons of the Traffic-by-interface charts made the Overview jump and flicker on every refresh. Root causes: the chart lookup (`row.querySelector("svg")`) could return the button's icon instead of the chart (axis labels were then drawn inside the button); the periodic label update rebuilt the button and its icon each time; and descendant chart rules (`.network-interface-row svg`, `.stat-chart svg`, `.system-card svg` …) stretched icons to full chart size. Charts are now addressed with `svg:not(.ui-icon)`, the axis renderer ignores icons, refreshes update only the text node, chart rules exclude `.ui-icon`, and small buttons use an 11 px icon so their height never changes. A measured 28-refresh run kept the container height constant (previously a 13 px swing) with zero icon rebuilds. +- **Badge consistency:** the upstream protocol badges in the Vhost policy row (for example `origin · H2 compatible → H2`) now use the same geometry as the other policy pills (9 px mono, 1 px letter-spacing, 4×8 padding, transparent background, square corners in the original corner style, 20 px height); only the colour differs (muted = not observed, green = observed). The **Transformed** marker in the Cache objects table now uses pill typography (1 px letter-spacing, square corners). ACL picker tags (selected countries/IPs) now use the same chip look as host chips (square corners, 10 px mono, same padding and remove button) instead of a rounded pill with a different font. A style comparison of every pill/badge/chip family and of controls sharing a row found no further deviations besides intentional ones (ACL rule action labels that mimic form fields, quick enable/disable toggles, the sort-direction arrow). +- **Spacing review:** a rendered scan of every panel, settings page and dialog (402 px and 1280 px wide) found descriptive text and blocks that touched the next element. Added spacing for TLS policy (description → TLS 1.2/1.3 switches, PQC help → documentation link, which is now styled as a link), Rules & headers, HTTP connections and Logging sub-tabs, Cache and Transform live-activity cards, empty-state notes under tables, Logs footnote, Dashboard presets footer, GeoIP and cache-cleanup field groups, htaccess/PHP help dialogs and several settings help paragraphs. The rules add space only where the layout had none. +- **Mobile layout:** the mobile **Menu** button no longer shows two hamburger icons, and **System uptime** wraps inside the System load card instead of overflowing its right edge. - **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. diff --git a/Cargo.lock b/Cargo.lock index a0a6c82..9c74734 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1943,7 +1943,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.36.2" +version = "0.36.3" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index f5826a6..22de792 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.36.2" +version = "0.36.3" edition = "2021" rust-version = "1.99.1" description = "RustProxy - independent reverse proxy with local administration" diff --git a/SHA256SUMS b/SHA256SUMS index 6bc45a4..de974ff 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,10 +1,10 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 .gitignore -eb2e5fcc5cd2ab599f6bd898c99d5ce21d417b5e67b60755809224313b8287c9 AGENTS.md -9c209f4fa88b9d2f61e85fdefadcdfa156099c3b23f767f17b07e6ff4a89e556 CHANGELOG.md -1b57c672088350d2ce0ce81980313b8f10615cb1a235e341c2c64ae258c23a46 Cargo.lock -2a37b391ded04b9a675e8befc6e82c06db2a05bf1ab6f4cb2e8defec193e5d81 Cargo.toml +a68d2e8a774443f5ba359ba1362144162573e895829297f55e924dd6debd98a7 AGENTS.md +10eb102a2a819deb15f46dca91fb2c1cc5c6354ad757da34ebcfca8a6f325b10 CHANGELOG.md +5d74dab9713870cfdf2d0220736a47e07f322529cf7321b06599953cbc7930c3 Cargo.lock +ca01b36019447f539eb4f6fa2abd4125db2593c03989f46efacf5f3bc2f05928 Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE 82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.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 -1effe1bc8b04c5469401b8a2e8174e0aa0ba73f0c2db10f6a21e5ef7d5a8bc65 integrations/routeros-oci-build/README.md +06a2f64af5372b12eb9421ab2b42fe7d7ce1ea39dc076f85d5af24fe38e7cc74 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 -f28ec357bafa539be82be86340651458b1002f8865411fd95a83c186cbc80a8a langs/en_US.json -03d219fbbb4cc7902b4f3979ea91a46bc8ee3c8b1e68b59b94303cbba087dc25 langs/pl_pl.json +052717c1d67c2b33084dae9d0e6f24c3a3ec4f16f0b7871f2a58773529961415 langs/en_US.json +7bb35391fb52cdcce6f03c0610f67e08cfa1798fee345ee7b6edd64b041d3f9f langs/pl_pl.json 9664543e4d03422788a65e42f968c1f026c2235cb5be6fc3e0ad318997077538 make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql @@ -165,11 +165,11 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file 77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs 743bb7436f77e04015328a0e7e7d425e43ef8aa8d61467bde157e462c1dcdc2e src/admin/logging_api.rs 243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs -cf588dd17399a752b23c3e87ba3cb6cdce02b059a6479528da7402ff517ddcdc src/admin/openapi.json +4ffdef6e47ce31633b08e090477c289e719cd829eb4991d598824dcb909dc389 src/admin/openapi.json f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 src/admin/operations.rs 257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs -35dd127a37db8f18a0ddfcacc342a3028ef4ddc0cb619fd9b13b12b2ff047e86 src/admin/preferences.rs +229efd2691e51755f402ee04bcd9b806e3d3176c39526b9f7c2381ce779a7ba5 src/admin/preferences.rs 35a7f99c7448c90b8637f4b3a7becca138f1f378fe5dc3f87122151525bb20f9 src/admin/preferences_columns.rs a238feee9afce314b8f6f7257e5488a53c4d680f33450a4e3e460fccbe54de88 src/admin/preferences_tests.rs 62ecbae5dee00aeeca3eb806e2605372471dfab971cde8f66f11e711df8fcea1 src/admin/public/mod.rs @@ -187,37 +187,37 @@ c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/ 86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js faa9735c2a02746b2954bc4955eb609f94afebb685cd5521d2629640cea2777a src/admin/web/app/backend-dashboard.js aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 src/admin/web/app/certificate-inspection.js -1a4d40a86067493e75b47f2d735d50fa4d161344ce145d4e502b0225144ed45a src/admin/web/app/chart-scales.js +f6b0533ffd6d0e6471519120c440619a7fb4b255854ba3dd23d81cb35cd21afb src/admin/web/app/chart-scales.js e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 src/admin/web/app/config-history.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 -bd120d5ff23f9893ab666e0ef07f08dfb3348053351bc4f7d950647871b2f7f4 src/admin/web/app/icons.js +0be1024d9ff657044e04ed71f275cff3b432406d5fdcdeac726daaee54444abd 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 -0c23e28d7799582d3a3a5835ed6c8e5868a3cac678b5be7e902652d0901404f3 src/admin/web/app/preferences.js +e540119edc555cefbe5147d97e06527adf49c984ba7a4dd51dc67603d3a46041 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 +5d1e805787530644b8853bd7f48d0efb1dc3f4829ca8b11eaa80c57434b8841d src/admin/web/app/runtime.js 31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/app/stream-dashboard.js 7de070cc8cdde45d7e0932259299e341e25b32c9ffbd87e4fcce934d0f090de4 src/admin/web/app/web-overview.js -f5855db3327f51170a28114e07d207745b4d6c8822d88d24c1a81eb92f345b14 src/admin/web/cache-panel.js +d330e3351a55561ceb36573b0df91a663ca4765c3165ea340b6ba95c28f23a9b src/admin/web/cache-panel.js 9f323507649910561518b63cf6632e81b3ecc4b09501ae53b635705f34ea6587 src/admin/web/certificate-api.js -a09f060b47771c56e9b9e57f1b896a32a2f87f1fb33401ac76932a91d247c67e src/admin/web/css/00-tokens.css -422a8c04d6499f806516be1694f316a4881d7cc191072218ea851445e2493506 src/admin/web/css/10-base.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 -4a3d733d76d5f707d58c301f4dd5416c2401df8b8013f8c8836d07de99697f61 src/admin/web/css/50-settings.css -f8b515c63080c71ee647cfd8c36628271e8357b31cd84f00cf591e9b752b44fe src/admin/web/css/60-monitoring.css -fb7cc4d733cb9a3e320a8cb95269ca047394dded8846120ffb50aaeaaab4d749 src/admin/web/css/70-responsive.css +4810137aa4217a86770617d442cf5dc6bec0d9f19e20bb2155e932a9a111cedf src/admin/web/css/00-tokens.css +ec3ff1b7824a7f03ee6d6b7ae9b6e0cbdb300e4891d44f982b6c04b3870f5c2b src/admin/web/css/10-base.css +8a1996d37f138790c12a704deedde9de74f733e7dcbb6be816fd68c281e8e0a9 src/admin/web/css/20-shell.css +9495c1b9f6a2cc72c1d99b4ea8c3cf3706835cb4670b53d5d7d71ef41ad62006 src/admin/web/css/30-overview.css +2d703473f7b751b587c32af6954c838a3160e9efa448236b6abe291832486b72 src/admin/web/css/40-editors.css +87df062d7478176641364c07de396ca988a37b1a8bcb892b53cc578fe8471850 src/admin/web/css/50-settings.css +606d460ec901a095c9537dca6a0e56ece76142fdb25c59490703e62691d60356 src/admin/web/css/60-monitoring.css +8ea9eb91ca422699e8b4a0cba8b3dbf71be57fc99b10cedc48fb5ec04d6dd609 src/admin/web/css/70-responsive.css 8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md 8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 src/admin/web/css/manifest.json ee51ab0912e0ade6a3e1c2ea6f533bb2f65a6d3e6ec9087e25b9f0df009c19b2 src/admin/web/dashboard-defaults.json @@ -244,14 +244,14 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/ 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js 3f0525ca1d4e088ccd4988b8a117b26a8cf8c12213b24c37081d41b68d2b0ff2 src/admin/web/host404.js -dbb063615c93ae0fd247b4e9dfbc92c772bb7bdf182fa1080c0054c8f22c6b97 src/admin/web/index.html +feca752133d6a1300c5161af91124d5841e803a77f62231159b4b82b22db9dd9 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 16d848a25f744e55d18757ce82cfd80e85bcc5b1777342c41e7bd732e13d8913 src/admin/web/management.js 1f3d10e389c9c20e6eb019509fa6e82b50c6b43bbf988c1210a4d42bb73985d0 src/admin/web/partials/overview-charts.html -95ba5fa72c04628f211ca216931a895ddf2059a791fa9d17c74812acd6c874d0 src/admin/web/partials/overview-metrics.html +32b3f9b3ce9d1de928d2f23265d09b65a135fd045d1d24b98e830922be21432f 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 @@ -559,9 +559,9 @@ b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server 714418c86ab241020a561fcf662a8fe324a6a70cc2fd3a313c56d7939c14b9e8 src/web_server/tests.rs f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json 102b83171a6429b6375d8001d6c5294702385cd58ee55727cb657b1f3566e877 tests/panel_icons_browser.py -c968beeda4a1bf626991deeb9134e43d36d44fc922cb1c1c27221cf2d5a5c2c3 tests/panel_icons_contract.js +52e61c389a60d147184ee76925cac76e454f8f2e3cb91039bb06e17787ab900f tests/panel_icons_contract.js 01af3008be27fabb486c141c67099af104f2725cc56f4c656aea9741a3c150cd tests/panel_interface_contract.py -40d7fa6e0b8111c0c9815cff43f235984c49b12ffd3947eb2542400fc807b621 tests/panel_refresh_contract.js +d6731b46a5a22605cd5bcf996d9f5dbb921aaa455395ed0098bc808c6428c514 tests/panel_refresh_contract.js e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js eca68a07c2b1e45fdb8f512294f5a432db3ae5a6aec8fb200b47f155cd9a31d6 tools/README.md 7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py diff --git a/integrations/routeros-oci-build/README.md b/integrations/routeros-oci-build/README.md index f58b45c..935c5cf 100644 --- a/integrations/routeros-oci-build/README.md +++ b/integrations/routeros-oci-build/README.md @@ -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.2` and the defaults this publishes: +For example, with RustProxy version `0.36.3` and the defaults this publishes: ```text -zot.example.net/network/rustproxy:0.36.2 +zot.example.net/network/rustproxy:0.36.3 zot.example.net/network/rustproxy:latest zot.example.net/network/rustproxy:routeros ``` diff --git a/langs/en_US.json b/langs/en_US.json index 358c68e..f98b98f 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -253,7 +253,8 @@ "certificateSafety": "Certificates secure TLS connections, not routing. Assign a certificate to a vhost. Changes affect new handshakes.", "wsActive": "WebSocket tunnels", "h2Requests": "HTTP/2 requests", - "h2ActiveConnections": "Active H2 connections", + "h2ActiveConnections": "H2 connections", + "h3ActiveConnections": "H3 connections", "tlsFailures": "TLS handshake errors", "tlsListener": "TLS listener", "notEnabled": "Disabled", @@ -1877,9 +1878,9 @@ "tunnelKillReasonDefault": "admin panel", "listenerH2c": "h2c cleartext HTTP/2 (prior knowledge + HTTP/1.1 Upgrade)", "vhWebsocketRfc8441": "RFC 8441 Extended CONNECT over HTTP/2", - "wsH1": "H1 Upgrade", - "wsH2Rfc8441": "H2 RFC 8441", - "wsH3Rfc9220": "H3 RFC 9220", + "wsH1": "H1", + "wsH2Rfc8441": "H2", + "wsH3Rfc9220": "H3", "wsEstablished": "Established", "httpProtocolMix": "HTTP protocol mix", "wsProtocolMix": "WebSocket protocol mix", @@ -2360,6 +2361,14 @@ "chartStyleMonochrome": "Monochrome", "chartStyleWarm": "Warm", "chartStyleCool": "Cool", + "chartStyleSunset": "Sunset", + "chartStyleAurora": "Aurora", + "chartStyleOcean": "Ocean", + "chartStyleForest": "Forest", + "chartStylePastel": "Pastel", + "chartStyleTerminal": "Terminal", + "chartStyleBlueprint": "Blueprint", + "chartStyleColorblind": "Colorblind safe", "settingsSearch": "Find settings", "settingsSearchPlaceholder": "Search settings, sections and options...", "settingsSearchResults": "Matching sections", @@ -2368,6 +2377,10 @@ "appearanceInterfaceSection": "INTERFACE", "appearanceInterfaceTitle": "Interface", "appearanceChartsSection": "CHARTS", + "cacheSectionBehavior": "Behavior", + "cacheSectionMemory": "Memory and limits", + "appearanceChartsTitle": "Charts", + "appearanceTablesTitle": "Tables and logs", "appearanceDataSection": "TABLES / LOGS", "appearanceDashboardSection": "DASHBOARD", "resetSection": "Reset section", diff --git a/langs/pl_pl.json b/langs/pl_pl.json index dafabae..9bcf660 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -253,7 +253,8 @@ "certificateSafety": "Certyfikaty samopodpisane służą do testów. Certyfikat nie tworzy trasy. Zmiana dotyczy nowych połączeń TLS.", "wsActive": "Tunele WebSocket", "h2Requests": "Żądania HTTP/2", - "h2ActiveConnections": "Aktywne połączenia H2", + "h2ActiveConnections": "Połączenia H2", + "h3ActiveConnections": "Połączenia H3", "tlsFailures": "Błędy handshake TLS", "tlsListener": "Listener TLS", "notEnabled": "Wyłączony", @@ -1877,9 +1878,9 @@ "tunnelKillReasonDefault": "panel administracyjny", "listenerH2c": "h2c HTTP/2 bez TLS (prior knowledge + Upgrade z HTTP/1.1)", "vhWebsocketRfc8441": "RFC 8441 Extended CONNECT przez HTTP/2", - "wsH1": "H1 Upgrade", - "wsH2Rfc8441": "H2 RFC 8441", - "wsH3Rfc9220": "H3 RFC 9220", + "wsH1": "H1", + "wsH2Rfc8441": "H2", + "wsH3Rfc9220": "H3", "wsEstablished": "Utworzone", "httpProtocolMix": "Miks protokołów HTTP", "wsProtocolMix": "Miks protokołów WebSocket", @@ -2360,6 +2361,14 @@ "chartStyleMonochrome": "Monochromatyczny", "chartStyleWarm": "Ciepły", "chartStyleCool": "Chłodny", + "chartStyleSunset": "Zachód słońca", + "chartStyleAurora": "Zorza", + "chartStyleOcean": "Ocean", + "chartStyleForest": "Las", + "chartStylePastel": "Pastelowy", + "chartStyleTerminal": "Terminal", + "chartStyleBlueprint": "Rysunek techniczny", + "chartStyleColorblind": "Przyjazny daltonistom", "settingsSearch": "Znajdź ustawienia", "settingsSearchPlaceholder": "Szukaj ustawień, sekcji i opcji...", "settingsSearchResults": "Pasujące sekcje", @@ -2368,6 +2377,10 @@ "appearanceInterfaceSection": "INTERFEJS", "appearanceInterfaceTitle": "Interfejs", "appearanceChartsSection": "WYKRESY", + "cacheSectionBehavior": "Działanie", + "cacheSectionMemory": "Pamięć i limity", + "appearanceChartsTitle": "Wykresy", + "appearanceTablesTitle": "Tabele i logi", "appearanceDataSection": "TABELE / LOGI", "appearanceDashboardSection": "DASHBOARD", "resetSection": "Resetuj sekcję", @@ -2645,7 +2658,7 @@ "backendWidgetInFlight": "W toku", "backendWidgetErrorsRate": "Błędy/s", "backendWidgetErrorPercent": "Błędne żądania w ostatnim okresie", - "backendWidgetProbeCount": "Zdrowe źródła", + "backendWidgetProbeCount": "Zdrowe backendy", "backendWidgetH2c": "Backendy h2c", "backendWidgetModeUnknown": "Nieznane", "vhProtocolCompatibilityWarning": "Tryb zgodności: zapisane wcześniej backendy HTTPS HTTP/2 zachowują fallback TLS H2/H1. Wybierz „Tylko H2”, jeżeli HTTP/1.1 ma być odrzucony.", @@ -3001,4 +3014,4 @@ "overviewLayoutInlineCompact": "Uzupełnij luki", "overviewLayoutInlineCompactHint": "Dosuń bloki w puste miejsca siatki (jednorazowo, przed zapisaniem możesz to anulować)" } -} +} \ No newline at end of file diff --git a/src/admin/openapi.json b/src/admin/openapi.json index b05039d..4be3bb8 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.36.2", + "version": "0.36.3", "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": [ @@ -3812,7 +3812,15 @@ "neon", "monochrome", "warm", - "cool" + "cool", + "sunset", + "aurora", + "ocean", + "forest", + "pastel", + "terminal", + "blueprint", + "colorblind" ], "default": "default", "description": "Account-scoped visual presentation preset applied consistently to administration charts." diff --git a/src/admin/preferences.rs b/src/admin/preferences.rs index 8347122..ad3c6a7 100644 --- a/src/admin/preferences.rs +++ b/src/admin/preferences.rs @@ -303,6 +303,14 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE | "monochrome" | "warm" | "cool" + | "sunset" + | "aurora" + | "ocean" + | "forest" + | "pastel" + | "terminal" + | "blueprint" + | "colorblind" ) { return Err(ApiError( StatusCode::UNPROCESSABLE_ENTITY, diff --git a/src/admin/web/app/chart-scales.js b/src/admin/web/app/chart-scales.js index 9ca1c05..8eddbed 100644 --- a/src/admin/web/app/chart-scales.js +++ b/src/admin/web/app/chart-scales.js @@ -115,7 +115,7 @@ window.rustProxyChartScale = (() => { { top = 0.08, bottom = 0.92, clipped = false, seriesScales = null } = {}) { const node = typeof target === "string" ? document.getElementById(target) : target; const svg = node?.tagName?.toLowerCase() === "svg" ? node : node?.ownerSVGElement; - if (!svg) return; + if (!svg || svg.classList.contains("ui-icon")) return; svg.rustProxyAxisData = { peak: Number.isFinite(peakValue) && peakValue > 0 ? peakValue : 1, unit, count: Number.isInteger(count) && count > 0 ? count : 0, @@ -127,7 +127,7 @@ window.rustProxyChartScale = (() => { } function refreshAxes() { const show = document.documentElement.dataset.chartAxes === "true"; - for (const svg of document.querySelectorAll("svg")) { + for (const svg of document.querySelectorAll("svg:not(.ui-icon)")) { if (show && svg.rustProxyAxisData) renderAxes(svg); else if (!show && svg.parentElement?.classList.contains("rp-chart-axis-host")) { const host = svg.parentElement; diff --git a/src/admin/web/app/icons.js b/src/admin/web/app/icons.js index e53b22a..7ef652a 100644 --- a/src/admin/web/app/icons.js +++ b/src/admin/web/app/icons.js @@ -168,7 +168,7 @@ function uiIconForTarget(target) { 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, .vhost-backend-protocol-badge, .config-history-day, .draft-dock-toggle")) return null; + if (target.matches("#tunnels-open, .vhost-backend-protocol-badge, .config-history-day, .draft-dock-toggle, .mobile-menu-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"; diff --git a/src/admin/web/app/preferences.js b/src/admin/web/app/preferences.js index ba3292b..e0fbafc 100644 --- a/src/admin/web/app/preferences.js +++ b/src/admin/web/app/preferences.js @@ -1,6 +1,6 @@ const ADMIN_THEMES = new Set(["dark", "ultra_dark", "light", "cream"]); const ADMIN_TIME_FORMATS = new Set(["24h", "12h"]); -const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool"]); +const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool", "sunset", "aurora", "ocean", "forest", "pastel", "terminal", "blueprint", "colorblind"]); const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]); const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]); const ADMIN_CORNER_STYLES = new Set(["default", "square", "subtle", "rounded", "extra"]); @@ -16,7 +16,7 @@ const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({ 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" }); +const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool", sunset: "chartStyleSunset", aurora: "chartStyleAurora", ocean: "chartStyleOcean", forest: "chartStyleForest", pastel: "chartStylePastel", terminal: "chartStyleTerminal", blueprint: "chartStyleBlueprint", colorblind: "chartStyleColorblind" }); let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false; function normalizeAdminTheme(value) { return ADMIN_THEMES.has(value) ? value : "dark"; } diff --git a/src/admin/web/app/runtime.js b/src/admin/web/app/runtime.js index a5620fc..19053cd 100644 --- a/src/admin/web/app/runtime.js +++ b/src/admin/web/app/runtime.js @@ -282,6 +282,15 @@ function svgNode(tag, attrs = {}) { function setChartText(node, value) { const text = String(value); + // Controls decorated by the optional SVG icons hold [svg.ui-icon, text]; keep the icon + // and update only the text node so periodic refreshes do not rebuild the button. + const icon = node.querySelector(":scope > svg.ui-icon"); + if (icon) { + const last = node.lastChild; + if (last?.nodeType === 3) { if (last.data !== text) last.data = text; } + else node.append(text); + return; + } if (node.firstChild?.nodeType === 3 && node.firstChild === node.lastChild) { if (node.firstChild.data !== text) node.firstChild.data = text; } else if (node.textContent !== text) { @@ -346,9 +355,12 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) { setChartText(row.querySelector(".network-interface-hide"), t("hideMetric")); row.querySelector(".network-rx-line").setAttribute("points", sparklinePoints(history.rx, 300, 60, 5, peak) || "0,55 300,55"); row.querySelector(".network-tx-line").setAttribute("points", sparklinePoints(history.tx, 300, 60, 5, peak) || "0,55 300,55"); - window.rustProxyChartScale.axes(row.querySelector("svg"), peak, "bytes/s", history.rx.length, + // The Hide button precedes the chart and may carry an injected svg.ui-icon: + // address the chart explicitly so axes/labels never attach to the icon. + const chart = row.querySelector("svg:not(.ui-icon)"); + window.rustProxyChartScale.axes(chart, peak, "bytes/s", history.rx.length, { top: 5 / 60, bottom: 55 / 60 }); - row.querySelector("svg").setAttribute("aria-label", `${name} ${t("networkInterfacesHistory")}`); + chart.setAttribute("aria-label", `${name} ${t("networkInterfacesHistory")}`); children.push(row); } const hidden = current.filter(item => item?.name && hiddenNetworkInterfaces.has(item.name)); @@ -395,15 +407,32 @@ function cacheOutcomeMix(totals) { const counts = [hits, misses, bypasses, requests - cacheTotal]; return { counts, percentages: counts.map(value => value / requests * 100), requests }; } +// Percent labels live inside the bar segments; a label that does not fit is hidden +// (the legend below always carries the exact value). +function fitCacheMixLabels() { + const bar = $("cache-mix-bar"); + if (!bar) return; + bar.querySelectorAll(":scope > span").forEach(segment => { + segment.classList.remove("is-tight"); + segment.classList.toggle("is-tight", segment.textContent !== "" && segment.scrollWidth > segment.clientWidth); + }); + if (!bar.dataset.fitObserved && typeof ResizeObserver === "function") { + bar.dataset.fitObserved = "1"; + new ResizeObserver(() => fitCacheMixLabels()).observe(bar); + } +} function renderCacheMix(mix, totalRequests) { const names = ["hit", "miss", "bypass", "other"]; const unavailable = t(totalRequests === 0 ? "cacheMixWaiting" : "cacheMixInvalid"); names.forEach((name, i) => { const pct = mix ? mix.percentages[i] : 0; - $(`cache-mix-${name}`).style.width = `${pct}%`; + const segment = $(`cache-mix-${name}`); + segment.style.width = `${pct}%`; + segment.textContent = mix && pct > 0 ? `${fmt(pct, pct < 10 ? 1 : 0)}%` : ""; $(`cache-mix-${name}-count`).textContent = mix ? fmt(mix.counts[i]) : "-"; $(`cache-mix-${name}-percent`).textContent = mix ? `${fmt(pct, 1)}%` : "-"; }); + fitCacheMixLabels(); $("cache-mix-headline").textContent = mix ? `${fmt(mix.percentages[0], 1)}% HIT` : "-"; const caption = $("cache-mix-caption"); const validTotal = Number.isSafeInteger(totalRequests) && totalRequests >= 0; @@ -424,6 +453,7 @@ function renderCacheMix(mix, totalRequests) { function renderDiagnostics(status, appendSample = true, systemSample = appendSample) { const d = status.system || {}, live = status.live_system || d, c = status.cache || {}, m = status.metrics || {}; $("metric-h3").textContent = fmt(m.http3_requests); + $("metric-h3-active").textContent = fmt(m.http3_active_connections || 0); const cacheHits = c.hits == null ? null : Number(c.hits), cacheMisses = c.misses == null ? null : Number(c.misses); const cacheEvents = cacheHits !== null && cacheMisses !== null ? cacheHits + cacheMisses : 0; $("metric-cache-ratio").textContent = cacheEvents > 0 ? `${fmt(cacheHits / cacheEvents * 100, 1)}%` : "-"; diff --git a/src/admin/web/cache-panel.js b/src/admin/web/cache-panel.js index 2b07f63..ac0fa44 100644 --- a/src/admin/web/cache-panel.js +++ b/src/admin/web/cache-panel.js @@ -162,7 +162,12 @@ $("cp-expired").textContent = `${number(m.evictions)} / ${number(m.expired)}`; $("cp-rejected").textContent = `${t("cacheRejected")}: ${number(m.rejected_fills)} · ${t("cacheMemoryPressureShort")}: ${number(m.memory_pressure_rejections)} · ${t("cacheNukeLimitShort")}: ${number(m.hot_eviction_limited)}`; $("cache-domain-rows").replaceChildren(...shownGroups.map(row => { - const tr = el("tr"); for (const value of [row.vhost || t("cacheLegacy"), row.domain, number(row.hot_entries), number(row.cold_entries), fmtBytes(row.resident_bytes), fmtBytes(row.cold_bytes), `${number(row.hits)} / ${number(row.misses)} / ${number(row.bypasses)}`, `${fmt((row.hit_ratio || 0) * 100, 1)}%`]) tr.append(el("td", value)); + const tr = el("tr"); for (const value of [row.vhost || t("cacheLegacy"), row.domain, number(row.hot_entries), number(row.cold_entries), fmtBytes(row.resident_bytes), fmtBytes(row.cold_bytes)]) tr.append(el("td", value)); + // HIT / MISS / BYPASS: coloured counts that match the dashboard cache bar, plus a hit-ratio meter. + const outcomes = el("td", undefined, "cache-outcomes-cell"); + [[row.hits, "cache-outcome-hit"], [row.misses, "cache-outcome-miss"], [row.bypasses, "cache-outcome-bypass"]].forEach(([count, cls], index) => { if (index) outcomes.append(el("span", "/", "cache-outcome-sep")); outcomes.append(el("span", number(count), cls)); }); + const ratioPercent = Math.max(0, Math.min(100, (row.hit_ratio || 0) * 100)), ratio = el("td", undefined, "cache-ratio-cell"), meter = el("span", undefined, "cache-ratio-meter"), fill = el("i"); fill.style.width = `${ratioPercent}%`; meter.append(fill); ratio.append(el("span", `${fmt(ratioPercent, 1)}%`, "cache-ratio-value"), meter); + tr.append(outcomes, ratio); const td = el("td", undefined, "cache-reasons"); for (const [key, count] of Object.entries(row.reasons || {}).sort((a, b) => b[1] - a[1])) { const tag = el("span", `${reason(key)}: ${number(count)}`, "cache-reason"); tag.title = key; td.append(tag); } tr.append(td); return tr; })); const domainEmpty = $("cache-domains-empty"); diff --git a/src/admin/web/css/00-tokens.css b/src/admin/web/css/00-tokens.css index 2f4f79c..4b2c32b 100644 --- a/src/admin/web/css/00-tokens.css +++ b/src/admin/web/css/00-tokens.css @@ -257,20 +257,6 @@ html[data-theme=cream] { --scrollbar-thumb-hover: #978a76; } -body[data-density=dense] { - --ui-gap: 4px; - --ui-card: 7px; - --ui-font: 11.5px; - --ui-control: 25px; -} - -body[data-density=comfortable] { - --ui-gap: 13px; - --ui-card: 17px; - --ui-font: 14px; - --ui-control: 36px; -} - html[data-button-style=compact] body { --button-padding: 3px 7px; --button-font-size: 10px; @@ -333,6 +319,62 @@ html[data-button-style=compact] body { --sw4: #70d7ff; } +.chart-style-choice[data-chart-preset=sunset] { + --sw1: #ff7a59; + --sw2: #ffb347; + --sw3: #e8509b; + --sw4: #9b6bff; +} + +.chart-style-choice[data-chart-preset=aurora] { + --sw1: #4ff0b0; + --sw2: #6ec8ff; + --sw3: #b78cff; + --sw4: #ff7ad9; +} + +.chart-style-choice[data-chart-preset=ocean] { + --sw1: #3fa7ff; + --sw2: #2fd0c0; + --sw3: #6f8cff; + --sw4: #9be7ff; +} + +.chart-style-choice[data-chart-preset=forest] { + --sw1: #7bc96f; + --sw2: #d6c45a; + --sw3: #4fae9a; + --sw4: #c98f5c; +} + +.chart-style-choice[data-chart-preset=pastel] { + --sw1: #f4b6c2; + --sw2: #b5e0c2; + --sw3: #b4c8f5; + --sw4: #e3c8f0; +} + +.chart-style-choice[data-chart-preset=terminal] { + --sw1: #6bff7a; + --sw2: #e5ff6b; + --sw3: #4fd0a0; + --sw4: #ffb347; +} + +.chart-style-choice[data-chart-preset=blueprint] { + --sw1: #cfe6ff; + --sw2: #8fc4ff; + --sw3: #ffffff; + --sw4: #7aa8e0; +} + +.chart-style-choice[data-chart-preset=colorblind] { + --sw1: #e69f00; + --sw2: #56b4e9; + --sw3: #009e73; + --sw4: #cc79a7; +} + html[data-chart-style=default] { --chart-accent: var(--accent); --chart-secondary: var(--green); @@ -485,6 +527,158 @@ html[data-chart-style=warm] { --chart-svg-filter: saturate(1.1) brightness(1.04); } +html[data-chart-style=sunset] { + --chart-linecap: round; + --chart-linejoin: round; + --chart-accent: #ff7a59; + --chart-secondary: #ffb347; + --chart-tertiary: #e8509b; + --chart-quaternary: #9b6bff; + --chart-quinary: #ffd479; + --chart-senary: #ff9fb2; + --chart-septenary: #c08ad8; + --chart-octonary: #8f6aa8; + --chart-surface: color-mix(in srgb, var(--panel2) 90%, #3a1428 10%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 92%, #2e1022 8%); + --chart-line-width: 2.5; + --chart-grid-opacity: .36; + --chart-grid-dash: 1 0; + --chart-svg-filter: saturate(1.12) brightness(1.04); +} + +html[data-chart-style=aurora] { + --chart-linecap: round; + --chart-linejoin: round; + --chart-accent: #4ff0b0; + --chart-secondary: #6ec8ff; + --chart-tertiary: #b78cff; + --chart-quaternary: #ff7ad9; + --chart-quinary: #c8ff7a; + --chart-senary: #7affea; + --chart-septenary: #8fa0ff; + --chart-octonary: #d0a8ff; + --chart-surface: color-mix(in srgb, var(--panel2) 91%, #0a2630 9%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 92%, #082028 8%); + --chart-line-width: 2.4; + --chart-grid-opacity: .4; + --chart-grid-dash: 2 5; + --chart-svg-filter: saturate(1.2) brightness(1.08); +} + +html[data-chart-style=ocean] { + --chart-linecap: round; + --chart-linejoin: round; + --chart-accent: #3fa7ff; + --chart-secondary: #2fd0c0; + --chart-tertiary: #6f8cff; + --chart-quaternary: #9be7ff; + --chart-quinary: #52e0a8; + --chart-senary: #78b8ff; + --chart-septenary: #4a7fb5; + --chart-octonary: #5f9ea8; + --chart-surface: color-mix(in srgb, var(--panel2) 90%, #061c33 10%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 92%, #05182b 8%); + --chart-line-width: 2.2; + --chart-grid-opacity: .4; + --chart-grid-dash: 2 5; + --chart-svg-filter: saturate(1.05); +} + +html[data-chart-style=forest] { + --chart-linecap: round; + --chart-linejoin: round; + --chart-accent: #7bc96f; + --chart-secondary: #d6c45a; + --chart-tertiary: #4fae9a; + --chart-quaternary: #c98f5c; + --chart-quinary: #a4d86a; + --chart-senary: #e0a84f; + --chart-septenary: #7aa86a; + --chart-octonary: #8c7a5a; + --chart-surface: color-mix(in srgb, var(--panel2) 91%, #11230f 9%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 92%, #0e1d0d 8%); + --chart-line-width: 2.3; + --chart-grid-opacity: .34; + --chart-grid-dash: 1 0; + --chart-svg-filter: saturate(.96); +} + +html[data-chart-style=pastel] { + --chart-linecap: round; + --chart-linejoin: round; + --chart-accent: #f4b6c2; + --chart-secondary: #b5e0c2; + --chart-tertiary: #b4c8f5; + --chart-quaternary: #e3c8f0; + --chart-quinary: #f6dba4; + --chart-senary: #a8dcdc; + --chart-septenary: #d2b8e8; + --chart-octonary: #b9c2cc; + --chart-surface: color-mix(in srgb, var(--panel2) 90%, #fff 10%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 93%, #fff 7%); + --chart-line-width: 3; + --chart-grid-opacity: .44; + --chart-grid-dash: 1 0; + --chart-svg-filter: saturate(.82) brightness(1.08); +} + +html[data-chart-style=terminal] { + --chart-linecap: square; + --chart-linejoin: miter; + --chart-accent: #6bff7a; + --chart-secondary: #e5ff6b; + --chart-tertiary: #4fd0a0; + --chart-quaternary: #ffb347; + --chart-quinary: #a6ff9a; + --chart-senary: #d4ff9a; + --chart-septenary: #58b868; + --chart-octonary: #7a8f58; + --chart-surface: color-mix(in srgb, var(--panel2) 88%, #031a07 12%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 90%, #021405 10%); + --chart-line-width: 1.6; + --chart-grid-opacity: .7; + --chart-grid-dash: 1 3; + --chart-svg-filter: saturate(1.1) contrast(1.1); +} + +html[data-chart-style=blueprint] { + --chart-linecap: square; + --chart-linejoin: miter; + --chart-accent: #cfe6ff; + --chart-secondary: #8fc4ff; + --chart-tertiary: #ffffff; + --chart-quaternary: #7aa8e0; + --chart-quinary: #a8d0ff; + --chart-senary: #e8f2ff; + --chart-septenary: #6a90c0; + --chart-octonary: #506c94; + --chart-surface: color-mix(in srgb, var(--panel2) 84%, #0a2a5c 16%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 88%, #08224a 12%); + --chart-line-width: 1.3; + --chart-grid-opacity: .78; + --chart-grid-dash: 4 4; + --chart-svg-filter: saturate(.9) contrast(1.06); +} + +html[data-chart-style=colorblind] { + --chart-linecap: round; + --chart-linejoin: round; + --chart-accent: #e69f00; + --chart-secondary: #56b4e9; + --chart-tertiary: #009e73; + --chart-quaternary: #cc79a7; + --chart-quinary: #f0e442; + --chart-senary: #0072b2; + --chart-septenary: #d55e00; + --chart-octonary: #bdbdbd; + --chart-surface: color-mix(in srgb, var(--panel2) 94%, #000 6%); + --chart-surface-alt: color-mix(in srgb, var(--panel) 94%, #000 6%); + --chart-line-width: 2.6; + --chart-grid-opacity: .7; + --chart-grid-dash: 3 4; + --chart-svg-filter: contrast(1.1); +} + html[data-chart-style] { --traffic-h1: var(--chart-accent); --traffic-h2: var(--chart-secondary); diff --git a/src/admin/web/css/10-base.css b/src/admin/web/css/10-base.css index 8c9bc27..be57d41 100644 --- a/src/admin/web/css/10-base.css +++ b/src/admin/web/css/10-base.css @@ -1067,43 +1067,10 @@ dialog .vh-form, align-items: start; } -body[data-density=compact] { - --ui-gap: 2px; - --ui-card: 5px; - --ui-font: 10.75px; - --ui-control: 22px; - line-height: 1.32; -} - -body[data-density=compact] main { - padding: 0 10px; -} - -body[data-density=compact] .card-heading { - margin-bottom: 5px; -} - -body[data-density=compact] .card-heading h2, html[data-dashboard-density=compact] #panel-overview .dashboard-overview-grid>.card .card-heading h2 { font-size: 14px; } -body[data-density=compact] .outline, -body[data-density=compact] .primary, -body[data-density=compact] .quiet, -body[data-density=compact] button, -body[data-density=compact] input:not([type=checkbox]):not([type=radio]), -body[data-density=compact] select, -body[data-density=compact] textarea { - padding: 3px 7px; - font-size: 10px; -} - -body[data-density=compact] .page-footer { - padding: 7px 0; - margin-top: 4px; -} - .network-interface-row-head { display: flex; justify-content: space-between; @@ -1151,7 +1118,7 @@ body[data-density=compact] .page-footer { font: 10px var(--mono); } -.network-interface-row svg { +.network-interface-row svg:not(.ui-icon) { display: block; width: 100%; height: 60px; @@ -1251,6 +1218,29 @@ body[data-density=compact] .page-footer { min-height: 24px; padding: 3px 7px; font: 9px var(--mono); + display: inline-flex; + align-items: center; + white-space: nowrap; +} + +/* Small controls: a smaller icon must not change the button height when the + optional SVG icons are toggled or re-applied during a refresh (Safari jumped). */ +.network-interface-hide>.ui-icon, +.network-interface-hidden .quiet>.ui-icon { + width: 11px; + height: 11px; + margin-inline-end: 4px; + vertical-align: 0; +} + +html[data-interface-icons="true"] .network-interface-hide>svg.ui-icon, +html[data-interface-icons="true"] .network-interface-hidden .quiet>svg.ui-icon { + width: 11px; + height: 11px; +} + +.network-interface-charts { + overflow-anchor: none; } .network-interface-row+.network-interface-row, @@ -1570,14 +1560,6 @@ html[data-table-density=ultra] :is(table th, table td) { min-width: 0; } -.path-permissions-commands { - max-height: 180px; -} - -.path-permissions-actions { - margin-top: 0; -} - .path-permissions-options>summary { cursor: pointer; } @@ -1599,6 +1581,7 @@ html[data-table-density=ultra] :is(table th, table td) { } .path-permissions-commands { + max-height: 180px; margin-bottom: 0; } @@ -1608,4 +1591,13 @@ html[data-table-density=ultra] :is(table th, table td) { .path-permissions-actions { margin-top: 10px; -} \ No newline at end of file +} +/* Safety net: whatever chart/card rule is added later, injected UI icons stay inline, + fixed-size and out of layout flow (they used to stretch inside chart cards on mobile Safari). */ +html[data-interface-icons="true"] :is(button, a, summary)>svg.ui-icon { + display: inline-block; + position: static; + flex: 0 0 auto; + max-width: none; + overflow: visible; +} diff --git a/src/admin/web/css/20-shell.css b/src/admin/web/css/20-shell.css index ab13c87..5509577 100644 --- a/src/admin/web/css/20-shell.css +++ b/src/admin/web/css/20-shell.css @@ -604,37 +604,6 @@ main { padding-top: 4px; } -body[data-density=compact] .shell { - grid-template-columns: 164px minmax(0, 1fr); -} - -body[data-density=compact] .sidebar { - padding: 14px 9px; -} - -body[data-density=compact] .nav, -body[data-density=compact] .stat { - padding: 6px 7px; -} - -body[data-density=compact] .topbar { - height: 42px; -} - -body[data-density=compact] .page-intro { - padding: 7px 0 6px; - gap: 7px; -} - -body[data-density=compact] .page-intro h1 { - font-size: 20px; -} - -body[data-density=compact] .page-intro .lead { - font-size: 10px; - margin-top: 2px; -} - .top-log-menu>summary, .backend-column-picker>summary, .file-log-search-help>summary { diff --git a/src/admin/web/css/30-overview.css b/src/admin/web/css/30-overview.css index 245a032..8c7a360 100644 --- a/src/admin/web/css/30-overview.css +++ b/src/admin/web/css/30-overview.css @@ -280,7 +280,7 @@ html[data-theme=cream] :is(.card, .stat, dialog) { text-align: right; } -.mini-live-chart svg { +.mini-live-chart svg:not(.ui-icon) { display: block; width: 100%; height: 60px; @@ -361,11 +361,11 @@ html[data-theme=cream] :is(.card, .stat, dialog) { font-size: 10px; } -.activity-card svg { +.activity-card svg:not(.ui-icon) { height: 105px; } -.system-card svg { +.system-card svg:not(.ui-icon) { height: 80px; } @@ -383,11 +383,6 @@ html[data-theme=cream] :is(.card, .stat, dialog) { max-height: 205px; } -body[data-density=dense] .stat strong { - font-size: 22px; - margin: 2px 0; -} - .traffic-chart-grid, .logging-grid { display: grid; @@ -468,7 +463,7 @@ body[data-density=dense] .stat strong { padding: 7px 10px 0; } -.vhost-chart svg { +.vhost-chart svg:not(.ui-icon) { display: block; width: 100%; height: 54px; @@ -550,37 +545,12 @@ body[data-density=dense] .stat strong { font-weight: 600; } -body[data-density=compact] .stat strong { - font-size: 18px; - margin: 1px 0; -} - -body[data-density=compact] .stat p, -body[data-density=compact] .stat small, html[data-dashboard-density=normal] #panel-overview .stat small, html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-mix-header>span, html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-mix-caption { font-size: 9px; } -body[data-density=compact] .activity-card svg { - height: 84px; -} - -body[data-density=compact] .system-card svg { - height: 66px; -} - -body[data-density=compact] .pool-table td, -body[data-density=compact] .pool-table th { - padding: 3px 5px; - font-size: 10px; -} - -body[data-density=compact] .pool-card .table-scroll { - max-height: 170px; -} - .network-interface-charts { display: grid; max-height: 260px; @@ -664,7 +634,7 @@ body[data-density=compact] .pool-card .table-scroll { margin-bottom: 3px; } -.stat-chart svg { +.stat-chart svg:not(.ui-icon) { display: block; width: 100%; height: 26px; @@ -769,7 +739,7 @@ body[data-density=compact] .pool-card .table-scroll { padding: 14px 15px 12px; } -.function-live-chart svg { +.function-live-chart svg:not(.ui-icon) { display: block; width: 100%; height: 180px; @@ -851,7 +821,7 @@ body[data-density=compact] .pool-card .table-scroll { font-weight: 400; } -.connection-card svg { +.connection-card svg:not(.ui-icon) { display: block; width: 100%; height: 100px; @@ -1052,7 +1022,7 @@ body[data-density=compact] .pool-card .table-scroll { border-radius: 0; } -:is(.stat-chart, .activity-card, .system-card, .function-live-chart) svg, +:is(.stat-chart, .activity-card, .system-card, .function-live-chart) svg:not(.ui-icon), :is(.traffic-donut, .traffic-bar-chart span) { filter: var(--chart-svg-filter); } @@ -1127,15 +1097,15 @@ html[data-dashboard-density=ultra] #panel-overview .stat small { line-height: 1.12; } -html[data-dashboard-density=ultra] #panel-overview .stat-chart svg { +html[data-dashboard-density=ultra] #panel-overview .stat-chart svg:not(.ui-icon) { height: 24px; } -html[data-dashboard-density=normal] #panel-overview .stat-chart svg { +html[data-dashboard-density=normal] #panel-overview .stat-chart svg:not(.ui-icon) { height: 34px; } -html[data-dashboard-density=compact] #panel-overview .stat-chart svg { +html[data-dashboard-density=compact] #panel-overview .stat-chart svg:not(.ui-icon) { height: 28px; } @@ -1737,7 +1707,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty border-radius: var(--rp-radius-surface) !important; } -html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-style=rounded], [data-corner-style=extra]) :is(button:not(.nav):not(.file-log-tab):not(.vhost-backend-trigger), input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea, .pill, .vhost-backend-protocol-badge, .overview-widget-type, .badge, .quiet, .outline, .primary, .code, .command, .dashboard-widget-toggle, .settings-tabs button, .overview-inline-restore), +html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-style=rounded], [data-corner-style=extra]) :is(button:not(.nav):not(.file-log-tab):not(.vhost-backend-trigger), input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea, .pill, .vhost-backend-protocol-badge, .overview-widget-type, .quiet, .outline, .primary, .code, .command, .dashboard-widget-toggle, .settings-tabs button, .overview-inline-restore), html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-style=rounded], [data-corner-style=extra]) :is(.nav, .file-log-tab, .vhost-backend-trigger, .backend-column-options, .docker-service-quick-actions a, .docker-service-quick-actions button, .docker-view-toggle, .provider-picker, .renewal-meta-item) { border-radius: var(--rp-radius-control) !important; } @@ -2074,7 +2044,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty gap: 8px; padding: 9px; border: 1px solid var(--line); - border-radius: var(--rp-radius-surface, var(--radius, 8px)); + border-radius: var(--rp-radius-surface, 8px); background: var(--panel); } @@ -2213,8 +2183,8 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over #panel-overview .stat-cache-outcomes .cache-mix-bar { display: flex; width: 100%; - height: 12px; - margin: 7px 0 9px; + height: 20px; + margin: 3px 0 4px; border-radius: var(--rp-progress-radius); background: var(--rp-progress-track); overflow: hidden; @@ -2224,6 +2194,17 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over display: block; height: 100%; min-width: 0; + padding: 0 2px; + overflow: hidden; + color: #0c0e11; + font: 600 10px/20px var(--mono, ui-monospace, monospace); + text-align: center; + white-space: nowrap; + user-select: none; +} + +#panel-overview .cache-mix-bar>span.is-tight { + color: transparent; } #panel-overview .cache-mix-other { @@ -2322,4 +2303,61 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over overflow: hidden; text-overflow: ellipsis; white-space: nowrap; -} \ No newline at end of file +} +/* WebSocket tunnels: "Established" sits to the right of the main value instead of a third text line. */ +#panel-overview .stat .stat-value-row { + display: flex; + align-items: baseline; + flex-wrap: wrap; + justify-content: space-between; + column-gap: 8px; + min-width: 0; +} + +#panel-overview .stat .stat-value-row>strong { + flex: 0 0 auto; +} + +#panel-overview .stat .stat-value-row>.stat-side-value { + display: block; + margin: 0 0 0 auto; + color: var(--muted); + font-size: 10px; + line-height: 1.3; + text-align: right; + white-space: nowrap; +} + +#panel-overview .stat .stat-value-row>.stat-side-value b { + color: var(--text); + font-family: var(--mono); + font-size: 13px; + font-weight: 450; +} + +/* Compact stat cards: a card made only of title, value and one secondary line shows that line + beside the value (right-aligned) instead of on a line of its own. */ +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small)) { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + column-gap: 10px; + align-items: baseline; + align-content: start; +} + +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small))>span:first-child { + grid-column: 1 / -1; +} + +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small))>strong { + grid-column: 1; + min-width: 0; +} + +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small))>small { + grid-column: 2; + max-width: 14em; + margin: 0; + text-align: right; + overflow-wrap: anywhere; +} diff --git a/src/admin/web/css/40-editors.css b/src/admin/web/css/40-editors.css index f10ac8a..8888446 100644 --- a/src/admin/web/css/40-editors.css +++ b/src/admin/web/css/40-editors.css @@ -29,7 +29,7 @@ .traffic-chart-card, #panel-overview .backend-mode-bar span, #panel-overview .stats-grid>.stat, -#panel-overview .dashboard-overview-grid>.activity-card svg, +#panel-overview .dashboard-overview-grid>.activity-card svg:not(.ui-icon), #panel-overview .dashboard-overview-grid>[data-dashboard-widget], .dashboard-density-setting>.fine, .dashboard-density-setting>select, @@ -1140,7 +1140,7 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car display: grid; grid-template-columns: minmax(0, 1fr); gap: 11px; - background: var(--surface, transparent); + background: transparent; } .vh-backend-health-editor h3 { @@ -1483,18 +1483,22 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car grid-row: 1; } +/* Same geometry and typography as .pill so protocol indicators sit evenly in the + Vhost policy row; only the colour (muted = not observed, green = observed) differs. */ .vhost-backend-protocol-badge { display: inline-flex; gap: 4px; - align-items: baseline; + align-items: center; max-width: 100%; min-width: 0; border: 1px solid var(--pill-line); - border-radius: var(--rp-radius-control, 4px); - background: var(--panel2); + border-radius: var(--rp-radius-control, 0); + background: transparent; color: var(--muted); - padding: 3px 7px; - font: 10px var(--mono); + min-height: 0; + padding: 4px 8px; + font: 9px/10px var(--mono); + letter-spacing: 1px; overflow-wrap: anywhere; text-align: left; } @@ -1504,6 +1508,10 @@ html.app-loading, background: var(--bg); } +.vhost-policy-pills>.vhost-backend-protocol-badge { + align-self: center; +} + .vhost-backend-protocol-badge[data-observed=true] { color: var(--green); border-color: var(--pill-live-line); @@ -1788,12 +1796,12 @@ html.app-loading, display: inline-flex; align-items: center; max-width: 100%; - gap: 7px; - padding: 2px 4px 2px 9px; - border-radius: var(--rp-radius-control, 15px); + gap: 5px; + padding: 4px 5px 4px 8px; + border-radius: var(--rp-radius-control, 0); border: 1px solid var(--pill-live-line); - background: var(--nav-selected-bg); - font-size: 11px; + background: var(--panel); + font: 10px var(--mono); } .acl-picker-tag>span { @@ -1803,13 +1811,12 @@ html.app-loading, .acl-picker-tag .acl-picker-remove { border: none; - border-radius: var(--rp-radius-control, 50%); + border-radius: var(--rp-radius-control, 0); background: transparent; color: var(--muted); - min-width: 20px; - height: 20px; - font-size: 16px; - padding: 0; + font-size: 15px; + line-height: 1; + padding: 0 3px; cursor: pointer; } @@ -1940,4 +1947,76 @@ html.app-loading, .web-service-preset-toggles>.check { margin: 0 !important; min-width: 0; -} \ No newline at end of file +} +/* Spacing review (0.36.2): descriptive text and blocks that used to touch the next + element. Rules only add space where the layout had none; grid/flex gaps are untouched. */ +.fine+:is(.switch-grid, .settings-tabs, .table-scroll, .soft-card, .appearance-settings-grid, .runtime-information-grid, details), +.form-actions+.fine, +.table-scroll+:is(.empty, .fine), +.empty+.file-logs-pager { + margin-top: 10px; +} + +:is(.card-heading, .window-heading)+.fine, +h2+.fine { + margin-top: 6px; +} + +.location-help-content :is(p, .fine)+:is(p, .fine) { + margin-top: 8px; +} + +.location-help-content :is(p, .fine, ul, ol, table, pre)+:is(h3, h4) { + margin-top: 16px; +} + +.cache-toolbar+.function-live-chart, +.function-live-chart+.stats-grid, +.soft-card+.soft-card, +.field-grid+.field-grid { + margin-top: 12px; +} + +.function-live-chart-summary+.function-live-chart-body { + margin-top: 8px; +} + +.tls-pqc-control>:is(.fine, a) { + margin-block: 0; +} + +.tls-pqc-control { + display: grid; + gap: 6px; + margin-top: 6px; +} + +.tls-pqc-control>a { + justify-self: start; + color: var(--accent); + text-decoration: underline; + text-underline-offset: 3px; + font-size: 12px; +} + +#settings-page-tls-policy .switch-grid { + margin-bottom: 12px; +} + +#panel-settings .fine+.settings-tabs.sub-tabs, +.fine+.field-grid, +.vh-grid+.fine, +.logs-card+.log-footnote, +.overview-presets-content+.overview-layout-footer { + margin-top: 10px; +} + +#settings-page-policies>.edit-form+div { + margin-top: 12px; +} + +@media (min-width: 761px) { + #panel-settings .settings-tabs.sub-tabs+:is(div, form) { + margin-top: 12px; + } +} diff --git a/src/admin/web/css/50-settings.css b/src/admin/web/css/50-settings.css index f383b26..21f0e7a 100644 --- a/src/admin/web/css/50-settings.css +++ b/src/admin/web/css/50-settings.css @@ -130,7 +130,6 @@ align-items: center; } -body[data-density=compact] .sidebar nav, .certificate-coverage { margin-top: 14px; } @@ -688,40 +687,6 @@ body[data-density=compact] .sidebar nav, margin-bottom: 12px; } -body[data-density=compact] .docker-service-row { - padding: 6px 8px; - gap: 4px; -} - -body[data-density=compact] .docker-service-head, -body[data-density=compact] .security-stack { - gap: 7px; -} - -body[data-density=compact] .docker-agent-overview { - gap: 4px 12px; - margin: 4px 0 6px; - font-size: 10px; -} - -body[data-density=compact] .docker-agent-details { - margin-bottom: 6px; - padding-top: 5px; -} - -body[data-density=compact] .docker-service-editor { - padding-top: 5px; -} - -body[data-density=compact] .docker-service-editor[open]>summary { - margin-bottom: 7px; -} - -body[data-density=compact] .security-editor { - margin-top: 8px; - padding-top: 6px; -} - .docker-agent-grid>label { display: grid; align-content: start; @@ -1294,4 +1259,224 @@ html[data-dashboard-density=normal] #panel-overview .stat>span, .global-rate-fields { grid-template-columns: 1fr; } -} \ No newline at end of file +} +/* Settings rows (0.36.3): label and help on the left, control on the right, one setting per row. */ +#settings-page-appearance .appearance-section-heading .small-label { + display: none; +} + +#settings-page-appearance .appearance-section-heading h3 { + margin: 0; + font-size: 14px; +} + +#settings-page-appearance .appearance-settings-grid { + grid-template-columns: minmax(0, 1fr); + gap: 0; +} + +#settings-page-appearance .appearance-settings-grid>label { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(170px, 260px); + grid-template-areas: "name control" "help control"; + align-content: center; + align-items: center; + column-gap: 24px; + row-gap: 3px; + margin: 0; + padding: 11px 0; + border-top: 1px solid var(--line); +} + +#settings-page-appearance .appearance-settings-grid>label:first-child { + border-top: 0; + padding-top: 0; +} + +#settings-page-appearance .appearance-settings-grid>label>span:first-child { + grid-area: name; + align-self: end; + font-size: 12px; + font-weight: 600; + color: var(--text); +} + +#settings-page-appearance .appearance-settings-grid>label>:is(select, input[type=range]) { + grid-area: control; + width: 100%; + min-width: 0; +} + +#settings-page-appearance .appearance-settings-grid>label>small { + grid-area: help; + align-self: start; + margin: 0; + color: var(--muted); + font-size: 11px; + line-height: 1.45; +} + +#settings-page-appearance .appearance-settings-grid>label:not(:has(>small), .check) { + grid-template-areas: "name control"; +} + +#settings-page-appearance .appearance-settings-grid>label:not(:has(>small), .check)>span:first-child { + align-self: center; +} + +#settings-page-appearance .refresh-settings-grid>label.check>span, +#settings-page-appearance .dashboard-widget-settings-intro>span { + color: var(--text); + font-size: 12px; + font-weight: 600; +} + +#settings-page-appearance .appearance-scale-control>span:first-child { + justify-content: flex-start; +} + +/* Checkbox settings: text on the left, the checkbox on the right edge of the row + (.check is display:flex !important, so the row is reversed instead of using grid areas). */ +#settings-page-appearance .appearance-settings-grid>label.check { + flex-direction: row-reverse; + justify-content: space-between; + align-items: center; + gap: 24px; + align-self: auto; +} + +#settings-page-appearance .appearance-settings-grid>label.check>input { + flex: 0 0 auto; + margin: 0; +} + +#settings-page-appearance .appearance-settings-grid>label.check>span { + grid-area: text; + display: grid; + gap: 3px; + flex: 1 1 auto; + min-width: 0; + align-self: center; + font-size: 12px; + font-weight: 400; +} + +#settings-page-appearance .appearance-settings-grid>label.check>span b { + font-weight: 600; + color: var(--text); +} + +#settings-page-appearance .appearance-settings-grid>label.check>span small { + color: var(--muted); + font-size: 11px; + line-height: 1.45; +} + +#settings-page-appearance .appearance-chart-preview { + margin-top: 12px; +} + +#settings-page-appearance .dashboard-appearance-grid>.dashboard-widget-settings { + margin-top: 4px; + padding-top: 12px; + border-top: 1px solid var(--line); +} + +#settings-page-appearance .refresh-settings-grid>label.check { + grid-column: auto; +} + +@media (max-width: 760px) { + #settings-page-appearance .appearance-settings-grid>label { + grid-template-columns: minmax(0, 1fr); + grid-template-areas: "name" "help" "control"; + row-gap: 5px; + } + + #settings-page-appearance .appearance-settings-grid>label>span:first-child { + align-self: start; + } +} + +/* Settings navigation (0.36.3) */ +@media (min-width: 761px) { + /* Sub-tabs are a horizontal tab bar; the vertical rail layout is for the main settings menu only. */ + #panel-settings .settings-tabs.sub-tabs { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 4px; + padding: 5px; + margin-bottom: 14px; + } + + #panel-settings .settings-tabs.sub-tabs button { + width: auto; + min-width: 0; + padding: 7px 14px; + text-align: center; + white-space: nowrap; + } + + /* Visual groups in the main menu: appearance / traffic / rules / observability / system / security. */ + #panel-settings #settings-tabs button:is([data-settings-tab=listeners], [data-settings-tab=policies], [data-settings-tab=logging], [data-settings-tab=administration], [data-settings-tab=certificate-api]) { + margin-top: 7px; + position: relative; + } + + #panel-settings #settings-tabs button:is([data-settings-tab=listeners], [data-settings-tab=policies], [data-settings-tab=logging], [data-settings-tab=administration], [data-settings-tab=certificate-api])::before { + content: ""; + position: absolute; + top: -5px; + right: 8px; + left: 8px; + border-top: 1px solid var(--line); + } +} + +/* Cache and static transformation pages (0.36.3): grouped sections, help attached to its switch. */ +#cache-form .cache-section-title { + margin-top: 20px; + padding-top: 14px; + border-top: 1px solid var(--line); + font-size: 13px; +} + +#cache-form [data-cache-settings-page=general]>.cache-section-title:first-child { + margin-top: 2px; + padding-top: 0; + border-top: 0; +} + +:is(#cache-form, #transform-form)>.check:first-of-type, +#cache-form [data-cache-settings-page=general]>.check:first-of-type { + font-weight: 600; +} + +:is(#cache-form, #transform-form) .check+.fine { + margin: 3px 0 12px 24px; +} + +:is(#cache-form, #transform-form) .cache-memory-sizing .card-heading { + justify-content: flex-start; + gap: 8px; +} + +:is(#cache-form, #transform-form) .cache-memory-sizing .card-heading>h3 { + margin-right: auto; +} + +:is(#cache-form, #transform-form) .field-grid { + grid-template-columns: repeat(auto-fill, minmax(min(100%, 210px), 1fr)); + column-gap: 14px; +} + +#transform-form .switch-grid { + grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr)); +} + +#cache-form #cache-default-ttl { + display: block; + max-width: 320px; +} diff --git a/src/admin/web/css/60-monitoring.css b/src/admin/web/css/60-monitoring.css index 14f284b..9e4c214 100644 --- a/src/admin/web/css/60-monitoring.css +++ b/src/admin/web/css/60-monitoring.css @@ -430,13 +430,12 @@ html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.pool display: inline-flex; align-items: center; margin-left: 6px; - padding: 1px 5px; + padding: 2px 6px; border: 1px solid var(--pill-line); - border-radius: var(--rp-radius-control, 3px); + border-radius: var(--rp-radius-control, 0); color: var(--accent); font: 9px var(--mono); - text-transform: uppercase; - letter-spacing: .35px; + letter-spacing: 1px; } .cache-toolbar-message { @@ -574,8 +573,8 @@ html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.pool button.config-history-day.has-revisions { cursor: pointer; color: var(--text); - border-color: var(--line-strong, var(--line)); - background: var(--surface, var(--bg)); + border-color: var(--line); + background: var(--bg); } button.config-history-day.has-revisions:hover, @@ -834,7 +833,6 @@ html[data-table-density=ultra] .file-log-inline-detail { padding: 5px 7px; } -body[data-density=compact] .mini-metrics div, .acl-policy-mode-card input, .docker-service-certificate-bar>.check>input, html[data-table-density=ultra] .file-log-field-grid { @@ -1006,7 +1004,7 @@ html[data-table-density=ultra] .file-log-inline-detail pre { .file-log-advanced-parser { margin: 0 0 1rem; padding: .75rem 1rem; - border-radius: var(--rp-radius-surface, var(--radius, 12px)); + border-radius: var(--rp-radius-surface, 12px); } .file-log-aggregate-form { @@ -1074,3 +1072,78 @@ html[data-table-density=ultra] .file-log-inline-detail pre { max-height: 68dvh; font: 11px/1.5 var(--mono); } + +/* Cache → Domains and effectiveness (0.36.3): numeric columns aligned, outcomes coloured like the dashboard bar. */ +#cache-domain-scroll .cache-table :is(th, td):nth-child(n+3):nth-child(-n+8) { + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +#cache-domain-scroll .cache-table td:first-child { + color: var(--muted); +} + +#cache-domain-scroll .cache-table td:nth-child(2) { + color: var(--text); + font-weight: 600; +} + +#cache-domain-scroll .cache-table tbody tr:hover { + background: var(--panel2); +} + +#cache-domain-scroll .cache-table td { + vertical-align: middle; +} + +.cache-outcomes-cell { + letter-spacing: 0; +} + +.cache-outcome-hit { + color: var(--chart-accent); +} + +.cache-outcome-miss { + color: var(--chart-secondary); +} + +.cache-outcome-bypass { + color: var(--chart-tertiary); +} + +.cache-outcome-sep { + margin: 0 5px; + color: var(--muted); + opacity: .6; +} + +.cache-ratio-cell { + min-width: 96px; +} + +.cache-ratio-value { + display: block; + font-weight: 600; +} + +.cache-ratio-meter { + display: block; + height: 3px; + margin-top: 4px; + overflow: hidden; + background: var(--line); +} + +.cache-ratio-meter>i { + display: block; + height: 100%; + background: var(--chart-accent); +} + +#cache-domain-scroll .cache-reason { + border-color: transparent; + background: var(--panel2); + color: var(--muted); +} diff --git a/src/admin/web/css/70-responsive.css b/src/admin/web/css/70-responsive.css index 825c5b5..f68d039 100644 --- a/src/admin/web/css/70-responsive.css +++ b/src/admin/web/css/70-responsive.css @@ -302,18 +302,6 @@ max-width: calc(100vw - 12px); } - body[data-density=compact] .sidebar nav { - margin-top: 0; - } - - body[data-density=compact] .sidebar { - padding: 8px 9px; - } - - body[data-density=compact] .topbar { - height: 42px; - } - .sidebar .brand { min-width: 0; } @@ -1293,16 +1281,6 @@ align-self: flex-start; } - body[data-density=compact] .docker-service-row { - padding: 8px; - gap: 5px; - } - - body[data-density=compact] .docker-service-head-actions .docker-runtime .pill { - top: 8px; - right: 8px; - } - .backend-column-options { width: min(330px, calc(100vw - 32px)); min-width: 0; @@ -1703,3 +1681,53 @@ --overview-widget-placement: 1/-1; } } + +@media (max-width: 760px) { + #panel-overview .system-card .card-heading { + flex-wrap: wrap; + gap: 4px 12px; + } + + #panel-overview .system-card .system-uptime-meta { + white-space: normal; + max-width: 100%; + margin-left: auto; + text-align: right; + } +} + +/* Docker vhosts (0.36.3): compact service rows - switch | name and details | health and actions. */ +@media (min-width:900px) { + .docker-service-head { + grid-template-columns: auto minmax(220px, 1fr) auto; + align-items: center; + gap: 6px 18px; + } + + .docker-service-head-actions { + grid-column: auto; + justify-content: flex-end; + gap: 6px 10px; + } + + .docker-service-head-actions .docker-runtime { + flex: 1 0 100%; + justify-content: flex-end; + } + + .docker-service-meta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 1px 14px; + } + + .docker-service-meta>b { + flex: 0 0 100%; + font-size: 13px; + } + + .docker-service-meta>span { + font-size: 11px; + } +} diff --git a/src/admin/web/index.html b/src/admin/web/index.html index e5de0c4..9289e69 100644 --- a/src/admin/web/index.html +++ b/src/admin/web/index.html @@ -1821,7 +1821,7 @@
CHARTS -

Chart style

+

Charts

@@ -1840,6 +1840,14 @@ + + + + + + + +