diff --git a/AGENTS.md b/AGENTS.md
index 298abef..e183624 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
-compatibility. Current source version: 0.33.6.
+compatibility. Current source version: 0.33.7.
## Delivery
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65f12a6..7f292c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,53 @@
> Operator-facing release history. Historical engineering notes are kept in
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
+## 0.33.7
+
+- Apply the same theme-aware subdued `--muted` color used on small metric
+ tiles to every Overview heading and the chart/metric headings in Cache,
+ Transform, Traffic and Vhosts. Keep numerical readings, legends, font sizes
+ and established sentence/uppercase title rules unchanged.
+- Make the shared Vhosts Backend/H2 popover (including rows and its close
+ control) follow the selected interface corner style: default/square,
+ subtle, rounded and extra-rounded, updating without dismissing the popup.
+
+- Unify all 78 Overview card headings at 12px in sentence case, with the
+ same proportional font across metrics, Process, RAM cache, backend tiles
+ and wide charts. Use one monospaced font for compact descriptions and
+ numeric readings; preserve HTTP, H1/H2 and RAM acronym capitalization.
+ Keep the 9px chart legends and other panels' typography unchanged.
+- Add 10px separation between the Cache "Domains and effectiveness" and
+ "Stored objects" cards, leaving other Cache section spacing unchanged.
+- Replace the native Vhosts H1/H2 upstream badge tooltip with a click-to-open
+ panel sharing the backend quick-view's anchored popover, dismissal and
+ positioning. Keep the panel and its content nodes stable across status
+ refreshes; show current observations and H1/H2 response counters without
+ a blinking browser tooltip. Ensure clickable Backend count and Health tiles
+ keep the same square geometry and typography as adjacent Vhost detail and
+ live-metric tiles, regardless of the selected interface corner style.
+- Align every chart legend and its value labels with the 9px monospace styling
+ used by **Requests per second**, including compact/ultra dashboard densities.
+- Add a clickable Backend count / Health tile to Vhosts: a compact,
+ keyboard-dismissible backend list shows name, configured IP/hostname and
+ port, upstream scheme/HTTP mode, health and the last real HTTP response
+ code. The live status feed refreshes the open list without extra API calls.
+
+- Replace the legacy Arrange Overview modal with authenticated SQLite-backed,
+ administrator-scoped named layout presets (16 maximum). Support save,
+ overwrite, load, rename, delete and current-view factory reset. Include
+ Overview history sample limits, density, card widths, order and visibility.
+- Add SQLite migration `0010_dashboard_presets.sql` (schema 10), protected
+ preset endpoints and English/Polish UI translations. Existing preferences
+ and proxy configuration documents remain unchanged.
+- Extract only Overview/WWW metric and chart elements into compile-time HTML
+ partials. Preserve card IDs, DOM order, dashboard controls and API polling.
+- Harmonize chart headings at 12px, using the existing proportional heading
+ font (sentence case on Overview, uppercase in other panels). Use the existing
+ monospaced chart font for **all** legends and data labels, not only their
+ numeric values. Covers Overview, Compression activity, Web Server, Cache,
+ Transform, Traffic, Vhosts and Streams, including dynamically rendered charts
+ and compact/ultra dashboard densities.
+
## 0.33.6
- Normalize the height of all 14 large Overview/WWW plots to 125px, avoiding
diff --git a/Cargo.lock b/Cargo.lock
index ce40dfc..1616cc9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
-version = "0.33.6"
+version = "0.33.7"
dependencies = [
"anyhow",
"arc-swap",
diff --git a/Cargo.toml b/Cargo.toml
index 0ffa85f..889f186 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
-version = "0.33.6"
+version = "0.33.7"
edition = "2021"
rust-version = "1.99"
description = "RustProxy - independent reverse proxy with local administration"
diff --git a/README.md b/README.md
index ea55664..a1da598 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,59 @@
-# RustProxy 0.33.6
+# RustProxy 0.33.7
+
+## 0.33.7: named dashboard presets, panel components and chart typography
+
+Titles across metric tiles and charts now use the same understated text tone
+(`--muted`) as the small Overview counters. The shade follows the active UI
+color theme; numerical readings keep their original contrast and fonts.
+The shared Vhosts Backend/H2 quick-view popover follows Corner rounding
+settings (including square, subtle, rounded and extra-rounded) for its panel,
+rows and close button, immediately and without losing the open popover.
+
+Every Overview card title is now set at 12px in sentence case: counters,
+Process, RAM cache, backend tiles, charts and diagnostics use one proportional
+heading font. Secondary labels and small numeric readings use consistent
+monospace, while established HTTP/RAM/H1/H2 abbreviations retain their case.
+This Overview-only refinement does not change the uppercase styling of chart
+headings elsewhere in the interface.
+
+The Cache page separates **Domains and effectiveness** from **Stored objects**
+with a consistent 10px gap. Vhosts upstream H1/H2 protocol badges now open
+the same anchored click-to-open popover used by the backend quick-view. It
+shows protocol observations and H1/H2 totals without a native hover tooltip;
+live status updates change its contents in place, without closing it.
+
+Chart legends use the same 9px monospaced size as **Requests per second**
+throughout the interface, including numeric values and Overview density modes.
+In the Vhosts list, click the **Backends** count or **Status** metric to show a
+compact per-origin overview (ID, configured address and port, HTTP/HTTPS and
+HTTP version policy, health and last HTTP status from a real proxied response).
+Missing HTTP responses remain explicitly unknown; the configured hostname is
+shown rather than inventing a DNS-resolved IP. No additional probe or request
+is made to open the panel. Click outside or press Escape to dismiss.
+The H1/H2 policy badges use that same click-to-open popover instead of a
+transient hover tooltip. Its text and response counters update in place on
+live status ticks. Clickable Backend and Health tiles retain the square shape
+of adjacent Vhost metrics at every corner-rounding setting. Cache's two
+adjacent domain/object tables have a 10px gap.
+
+- **Dashboard Presets** replaces the former Arrange Overview modal. Save up to
+ 16 named layouts per administrator, including widget order, visibility, card
+ widths, density and Overview history sample count. Load, overwrite, rename,
+ delete or restore factory defaults from one dialog. **Unlock layout** remains
+ the interactive editor; existing saved preferences are retained.
+- Presets are persisted in a separate SQLite `dashboard_presets` table (schema
+ 10), outside the 4096-byte administrator appearance document. They do not
+ change the active proxy configuration revision. A factory reset changes the
+ current dashboard, not existing named presets.
+- Chart headings throughout Overview, Web Server, diagnostics, Cache,
+ Transform, Traffic, Vhosts and Streams use the established proportional font
+ at **12px** (Overview sentence case, other sections uppercase). Chart labels, values and
+ entire legends (including Compression activity series names) use the existing
+ monospaced font. Chart behavior is unchanged; two Overview titles now
+ use sentence-case English/Polish translations.
+ Proxy forwarding, trusted-client IP headers and WAF behavior were not
+ modified. See [API](docs/API.md) and [verification](docs/TEST_REPORT.md).
+ Rust compilation and live server acceptance remain outstanding.
## 0.33.5: fixed CPU chart scale on Overview
diff --git a/ROADMAP.md b/ROADMAP.md
index 3e1cb7f..94c59c5 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,5 +1,34 @@
# RustProxy roadmap
+## 0.33.7 - named dashboard presets, focused HTML extraction and chart typography
+
+- [x] Apply uniform 12px sentence-case labels to all Overview widgets,
+ including Process, RAM cache and backend tiles. Normalize secondary
+ captions and small values to monospace without changing other panels.
+- [x] Space the two Cache table cards by 10px without altering other sections.
+- [x] Reuse the backend quick-view popover for clickable Vhost upstream H1/H2
+ protocol details; keep it stable during repeated live status refreshes.
+- [x] Keep the clickable Backend and Health fields square, visually matching
+ adjacent Vhost tiles under every interface corner style.
+- [x] Standardize the full legend text (including every value) at 9px monospace,
+ matching Requests per second across desktop, mobile and density modes.
+- [x] Add a compact, nonmodal Vhost backend quick-view through both backend
+ count and health metric; use existing configuration and status fields, with
+ accurate unknown/last HTTP status handling and accessible dismissal.
+
+- [x] Introduce account-scoped, bounded SQLite storage for 16 named Overview
+ layout snapshots without changing proxy configuration or existing preferences.
+- [x] Replace Arrange Overview modal with save/load/overwrite/rename/delete,
+ and factory reset; keep Unlock layout editing intact.
+- [x] Extract metrics and charts only into compile-time embedded HTML partials
+ without changing existing card DOM order, IDs or polling.
+- [x] Standardize chart headings on 12px proportional type (Overview in
+ sentence case, all other panels uppercase); use mono for full legends,
+ captions and chart data across all chart families.
+- [x] Validate SQL migration and run available simulated-browser/JS tests.
+- [ ] Build/test Rust and run the new endpoint/UI against a real server with
+ representative persisted data. The production acceptance gate remains open.
+
## 0.33.6 - Stable dashboard chart ranges and plot heights
- [x] Fix all 14 Overview/WWW full-width SVG heights to 125px and align their
diff --git a/SHA256SUMS b/SHA256SUMS
index e141187..278e20e 100644
--- a/SHA256SUMS
+++ b/SHA256SUMS
@@ -1,13 +1,13 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
-ffae7ce20cc0fb1990394822515c933843ba39304aa44134403f0d25a6a66d9f AGENTS.md
-8b72ed7b17505c129b60faf97ee6a838b855e4303dc390ea7196b0d97ee79134 CHANGELOG.md
-a35a5d557bb57df37d130a03752ffc092f005cb4849733d3d81485fb9450a931 Cargo.lock
-8b77b2beffb31dd14ff62e225ff5384084d2b4e66faa100aaf38470883fca879 Cargo.toml
+c793a8be7f4b76350e04ab36dbeec3792bb2dcf33c87a86f76fa2f330e26dd9f AGENTS.md
+91edfded868ff72e97fbb34d08e1be2df5d5b8fa6582b534a8e4d109cd312acd CHANGELOG.md
+d5dc2b778af98f52ad52a7515e2d1b4ba8c2e01a04cc6e02564377075b01c501 Cargo.lock
+7fd5dd1ccae8fac77bbbea4bbf960f5cf762f712ff17a2e629a5dd8629eb7ff9 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
-dfe12a6a0d360e0febec982f30098123a8974007c7c3fba39509be5c45d3ebbd README.md
-65340bb1f93da256043cb6bd75b6c9de8539e31f0417f24c6f05093a27f8319f ROADMAP.md
+30708454b96c0dd57c9c621050ace58b92dbdbd85591c7b8199ebb54937dc372 README.md
+e796ab6e379b3d71cf5d77ca3eb30bc6a320080af0c9e35f07c82737e09cbc2e ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
@@ -27,7 +27,7 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
-627f1d8727321ef8961b153aabab7448f239fa383177bb9ef115cfeef48452da docs/API.md
+e21a5c3cea78386fcb3ebd6516a66eed2ecd1966481ae0d3b2f57fa01010542e docs/API.md
f27053f8a93d7883025a04922d015e0b697cad653e714be632b757e167297de1 docs/API_ACCESS.md
d30551a52ffd34c5963094d2826ba39795210a4e6a4e4d7333066dae7456fc50 docs/ARCHITECTURE.md
fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md
@@ -64,11 +64,11 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
-72baf37ed0989de65123eba73f83d8c31cba7415382dd00e639d8ca412f0ef0d docs/TEST_REPORT.md
+e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
-fd1c187ea8eef101764670d4ebb0c554173412514e37b909c81ea3f22ba8e7ee docs/VHOSTS.md
+711c69c9e05761ba5cb96dadc3c368eb2961e2d2287ca039eb49c7fbca0dd63c docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
4257729a4c1b1f3949538410af78a12067bd3417c70525a15bd01411155d25fd docs/WEB_SERVER.md
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
@@ -96,8 +96,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
-7783e629ca4b2c1614e776838bf90f3692d521e50ee580aa5e92236c92ada844 langs/en_US.json
-58f2fdaf0aa9d3a3ce25d47659501a8b3df622e0c08eee0a4baaad4d5f1ac660 langs/pl_pl.json
+1a69ed38b730c3b379b8b14b5707d8ae923320a0c66d084c7ee478e6a01eb53c langs/en_US.json
+6513435292eb947935f50eeadf9ef2ee0995f4b57e989699dfc9cda686f21685 langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -107,6 +107,7 @@ faa7e50922bf180c363f7627fbcfe8079831e36282220d17481cf69bc64b57f1 migrations/000
f093aef9639a959a2522946995a02d2e67c464e794c3ef665261a9a5b348c968 migrations/0007_secret_store.sql
90f6dce865496e018c047133d3bc1721fc2a19b2f4ced22e2a3625d9e9ff18ac migrations/0008_config_history.sql
fc7585fdcbe99fe3496f0e62f4c4e2161f4a0173222ba4950331aec6c44e50f8 migrations/0009_certificate_api_grants.sql
+a8b1e5ec3236a0c2681dfa1746c526eeef77e8bcb5ea8634d954ac398b4642bb migrations/0010_dashboard_presets.sql
478220552ffeea3de00e2866d427d9f87f4cfc4ae5232e5c3a774e62425b1e0c nagios/README.md
91233910aa4dab07ae917fef93d018a7a0beaab697f9a0bb889ac613e595cca4 nagios/check_rustproxy.py
a42e01b341725c9da0928edf40c8c3b9c814ef90592f208c52b1f2c56a2ad383 nagios/check_rustproxy_api
@@ -123,31 +124,40 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
-63ed91f4cd77450cfe04f4bdb6e5f866b7868afa10207f78041e6fcd86d96fb0 scripts/preflight.py
+b5b3135449f6be4e97088b76ef7cf2428e2de3c7944c9cbbcb1edd2016c0fd4a scripts/preflight.py
3d6a0503afc25beab79ddc86864bcbb15248ded7368d33aa64321b8cdff59933 scripts/resolve_dependencies_0312.sh
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
-cc7026ec9c5b69850a815816df093de0ffdf4aeadabb3af702783483122ede55 scripts/tests/dashboard_chart_headers_0335_browser_test.py
-aa3ca7419d6d7d506c5aaf4d624d20cc7e8ee66bec3cb0cf9b5e7d164bcbab57 scripts/tests/dashboard_chart_width_0333_browser_test.py
-fdcea6dd1e3bc160abe1139bf94b45c9a28f6aaf7afbe7e38fb452ee4892b8f1 scripts/tests/dashboard_cpu_fixed_scale_0335_browser_test.py
-52eacb467df123dc4a35f14933e006b090c9238df1b14dcff856a16b37c8063f scripts/tests/dashboard_layout_0331_browser_test.py
-276e732fc2dfb0d39badb159e4fdcba0d7d73509c60ad5c9df49621c03046f1b scripts/tests/dashboard_metric_visuals_ui_test.mjs
-61a0fb2c4de86f25debec9662aedd923bce8c2ccd51e75593d485b2e0c9b8551 scripts/tests/dashboard_modal_0333_browser_test.py
-0c8a566979b51cd62601d138697ccd5c4ed47b8fa33e8faa28fa3a085a72ad97 scripts/tests/dashboard_resizing_0333_browser_test.py
-f82d57d3c01858eb60aa6882f7d3470e6587d7fc60e7ee6dbd8c5bec1f223c66 scripts/tests/dashboard_resume_rate_charts_ui_test.mjs
-aa12d00acdd226602b3efc90221fbc936f69f73bcabbaa6c01dfb0e05d08c505 scripts/tests/overview_samples_0330_ui_test.mjs
-0ec69b7811db3dcd51cba60387900ca8df50eb836b22cc2a3ecfcc22b40a2edd scripts/tests/progressbar_0332_browser_test.py
-222077dab4d4928ae9110a9f2d64c07cc8386ea14099819b43304bbd2364e5b5 scripts/tests/sidebar_nav_0333_browser_test.py
-d5f675b97a85220043a9475347412972db904e559d863ce5058aa18f649f9e16 scripts/tests/system_ram_meter_0332_browser_test.py
+efb7a1fde2c74f30cd4a1abb88ae07c1cac7378bb33685dd09d52bb30849daac scripts/tests/dashboard_chart_headers_0335_browser_test.py
+eb2c9e5c968938b6fbe4675b547ba1f537519878b9095e1fea133f4d83f96fc8 scripts/tests/dashboard_chart_typography_0337_browser_test.py
+6a908fd434d2a8a80adfd06a66d4b83c4174e448d453a8c10b1812475d036677 scripts/tests/dashboard_chart_width_0333_browser_test.py
+252b401c29e2db50e105d7fb044bcae14752748261a67da1a4025d71bfa2608c scripts/tests/dashboard_cpu_autoscale_0335_browser_test.py
+41a2b3765006c138324ae1312f5c1edfed6ce48051230f85779e8d2aad34d2a3 scripts/tests/dashboard_cpu_fixed_scale_0335_browser_test.py
+de583adde771e0d6f64ea0d5aed45b1c942a7c78b7193598eeeb77fff00c2803 scripts/tests/dashboard_layout_0331_browser_test.py
+1c813c78dfe96ba4d2c9470c93dd24e81be06e6485ebdc39cbfa30f8d32aa0fe scripts/tests/dashboard_metric_visuals_ui_test.mjs
+f14626c5ec006477062b496bdc0025caa3366c7037ee598e15cd55e29041d58f scripts/tests/dashboard_modal_0333_browser_test.py
+f934f5ce66105a1394b93761a6081a51133837ac038f5faf67a236f676b15c1b scripts/tests/dashboard_overview_labels_0337_browser_test.py
+df87420f16c418aa17a8ba8808e506095681446d32a7d76ae0244222ed37928a scripts/tests/dashboard_presets_sql_test.py
+df1457797241aab114a5671ce3f0229815dd29d06ab6f6254c0a1048576af6e1 scripts/tests/dashboard_resizing_0333_browser_test.py
+149910e6e8d569443a724fd21701e2fd8f6b5e360991d4f710f5d21364402bce scripts/tests/dashboard_resume_rate_charts_ui_test.mjs
+3868dc7b241f1cf15bc297104e29449ec3e35f75cb66078439bc4b5c4ee11040 scripts/tests/dashboard_stable_scales_0336_browser_test.py
+fa42ec0475e4398d647b3c094309b4332cd2b5649db223cae0b8e821d6b44a28 scripts/tests/dashboard_title_color_0337_browser_test.py
+3c1b2bda6410d3348b0f5a05c70494cca9636339bed8217fc4abc0a30981faae scripts/tests/overview_samples_0330_ui_test.mjs
+72185f59f31bf04a4e88d53fe2fca52509f3663cb5f40b1d3bc319a5f945ef23 scripts/tests/panel_html.mjs
+b0136544af717b75df2cb107900ea65c3f945fdd0040369dbb93919008fb954c scripts/tests/panel_html.py
+8d1903e2b17b3d59cd0a5bec3e298e468b3d88ed2dda8b724239cc45d8353c59 scripts/tests/progressbar_0332_browser_test.py
+1bb99b148d0297235b707f16865ca077329246fc19ecbb20a143bdeb56a23f37 scripts/tests/sidebar_nav_0333_browser_test.py
+12f2b18b0ed2171c49eb8cde2c2b88fc627054c2f574c04bd9847821b00cd4c1 scripts/tests/system_ram_meter_0332_browser_test.py
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py
-2da8b8d56b6e53b061664c352df0e7a351602c4e460e5f35ce61b738467a5d23 scripts/tests/web_observability_0330_browser_test.py
-fc982558e5c27281d71c2b3cbc926c415ab9134cf595e7ac12b7214f8a180447 scripts/tests/web_observability_0330_source_test.py
-651330e4bd38d1f2c45d304e5f7461848f414e80de5089358775af4e5eba5b97 scripts/tests/web_server_0330_ui_test.py
-17839b4048e055c55b15c46f95e6745d8d87bde17ea0d04e557a85411526e83e scripts/tests/web_server_0334_browser_test.py
+f0a2e3daa0a49aac54723cb5a281360e49a97c076281372c5cca2a85e096c02c scripts/tests/vhost_backend_popup_0337_browser_test.py
+23cb1f790cd5cee1b76ecd85f6a2a44ef2e16b0840bf06beaa2ae13048063b2f scripts/tests/web_observability_0330_browser_test.py
+3babdd0fd6e2998755ae88615112f6ea056f5c808245abb6de60fbda555594d1 scripts/tests/web_observability_0330_source_test.py
+f55efa99245bbdbc98fe3202e52952d224ac3200612561580d52f6e251810049 scripts/tests/web_server_0330_ui_test.py
+0dd1cef06a1c3e1546e2fb9b085bf369dfbed02107b9ab01b4fb4f3b894099c0 scripts/tests/web_server_0334_browser_test.py
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs
63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs
@@ -165,6 +175,7 @@ e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
+b5f6d49f1add583cfc8cd4aa36ffe4db56dac4de3b151fe8239fe42f507019cc src/admin/dashboard_presets.rs
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
958546c43c05bf4bed8121b24db3b3096f9243f3c56a71e1762592b755684013 src/admin/documentation/documents.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
@@ -173,34 +184,36 @@ ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file
c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
-33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
-6884210022056d7f2a92160b6d37026e420483c2285368b7ae3c01b1c981df8b src/admin/openapi.json
+e552c4b565e3198cc2224d1efa3bef21db02a9872cb80cd96f446181035f19fb src/admin/mod.rs
+28dd33ef19d7eab08cfa243b253785012d6c731bc4c56b4f69f28edd3d9e07a7 src/admin/openapi.json
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs
4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs
9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs
-055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
+4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
-6d12bb97819ff05773d017d6b73bd6659dc9f2345f49d449fbaf9e34b936aab8 src/admin/router.rs
+daef5e663c1bc99a22b3b8fa4481ae5e4916b91db132c104bc050f51b1fdf77f src/admin/router.rs
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
77fe317c15bf2e03301b22e0552518fe2d7e1ad8d80afaba4e16cd2bf8ddf10c src/admin/status.rs
4b8a0483ede651d08489a4d6e5bb616f06423094bbbefcee70b8755d65c04398 src/admin/status_live.rs
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
-18b778c2c4e723e2fe5cbaaef20effa6ea50fcc35d507ebde2361c9b6eda62b2 src/admin/tests.rs
+3d9dcf96d8f05f1f03ad41b6f53d2b743ce6f9fadd608745d04fced0efabb9af src/admin/tests.rs
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
c0d08c5b8fc13dec9bf53fd842d224b9df3874fc3bdf31413e3ab182a8cec2aa src/admin/web/app/backend-dashboard.js
+a085ee336e6669229f3ad2f74536f19bd088412ca79bcaff30dff293f1e1baf9 src/admin/web/app/chart-scales.js
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js
-f3b566d79ba47848021a74653dbd7fec72f0fb819c6b2a07259e047bc8c427fd src/admin/web/app/dashboard-inline-layout.js
+afe4e0707fdcdfbce9134f3998e21ef05066da7cb29fe22136a67d288d5a92d6 src/admin/web/app/dashboard-inline-layout.js
851a4337127f332db91f6954f689363ee227fcddde2dda82e55f645940d0ce3b src/admin/web/app/dashboard-layout-editor.js
-2f1669cbeb9f49aef43a548e2096825aa14c64c7097ffad520822bbbaefb2b4c src/admin/web/app/overview-extra-metrics.js
+a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
+bfb3fbac62f9ff762a298d9c06808ae81ae55de7a41e0bb35ce5c167f333ffe2 src/admin/web/app/overview-extra-metrics.js
5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js
b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js
-41d9d872ba2f8e1871564fa3f72e88bcb8f118c0ddd5255f256ac7230de945b6 src/admin/web/app/overview-wide-metrics.js
+d997f62e8bd7ac9d4f16bb302800c98ad943afab663bb0b212ea26c35b58de11 src/admin/web/app/overview-wide-metrics.js
5bde5f0881320464eb133cd44de9eff5211e6816e79f7f7e9574b16784fab126 src/admin/web/app/preferences.js
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
@@ -208,8 +221,8 @@ daa20d634f921191cb4ac37889504f203ab39ad7e8fd8f33e1b182fc4206c24d src/admin/web/
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js
-655db0b4d5e7ee14dbe69fa22bdb9d99eecc32abcf3b1b76a5819a8015f72309 src/admin/web/app/web-overview.js
-0603dc8793a731a0e582cbbec53aa6d1cc6e007faafcec5d8843730b2cdc39e1 src/admin/web/cache-panel.js
+7f89bfae66e2a08a05a112733864e908f036bb1ee3d8b5b9ff23783b870acd96 src/admin/web/app/web-overview.js
+bb94697a1cbeccaf80069dd47ab84efe14b052d81c472f8309478bbeb4e05f06 src/admin/web/cache-panel.js
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
aec5755244242424e5091ff57c0258d2618588618fe1d52d36f081d03879f686 src/admin/web/dashboard-defaults.json
797e2935a34b974cb0e07a88056c31e6948cc11385bf2a076d902a332882d0a0 src/admin/web/dashboard-widgets.json
@@ -234,25 +247,27 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
-0ebfde7081385aa7158f3b0f8f2b9355836e087c3d91a7c98d5187afbd47abd0 src/admin/web/index.html
+6684e7de2540499f9129b78c790f9a820b0e6c5de33e3b60c318d9a1afc931a5 src/admin/web/index.html
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js
097fdb41aa1cd526c4d9bc65e502b05a62e664ef9fff8e69f706b93b0b124dc2 src/admin/web/management.js
+4481a592fcce9a2aa1db5e4a25619e19fd459c39039b86c0549dfc427eec1b45 src/admin/web/partials/overview-charts.html
+c1b831617bf3cd03dfa416b43791777c8e71f0711c78f9d467fd0f1930ca2a1e src/admin/web/partials/overview-metrics.html
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
-cc0fdbed4a344487fef7593375177043ee4524b98f58842f37a97e2d2a414183 src/admin/web/style.css
+0109870251482b02f5adc63a8359a790bd793e21c827951e9b39a4fcd5fc9451 src/admin/web/style.css
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
-69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js
+dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
b31782c61da3623389b57fdbba5bd41d2287d45fc172e9c14360a9dd7accf061 src/admin/web/vhosts/editor.js
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
-b991e81a98dd604a1d7c30158c0cd8cd825479bd2cd94b082b3550de1071bb58 src/admin/web/vhosts/render.js
+bb93e2017f40e3a45ff7f4ca92e4188bb464f432704b6d2c39da9d9621b7984c src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
0049e5c2a35e3ec25f3c1590879684a7b15165584e96eb1b022fb60d3c548f2c src/admin/web/web-server.js
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
@@ -323,8 +338,9 @@ a19fc675720c7c2dbf4bba706e1b1c30c83c6df1da3f29e3cbb3b82442e7246a src/config/mod
f66c2388f31b19142c12469df23d43451e0f7c48b158d4996920282c06d32076 src/config/routing.rs
818545ad36af3ae8d56976f318349dd1f5264ef5665fbf94681a81b79003f8cb src/config/snapshot.rs
76cbb4f1339de9f13d441561a3c4ee6bc5ff06e90ff0f019b7f0239c74f9542d src/config/tests.rs
-c6db84438878accb8a0c96be13fa01a83da824af39c929dc3f6df498f393b7e0 src/db.rs
+23541660151d9005555a07db6d491455d61b185f0cc70c3ec3a0bee2aef08dc3 src/db.rs
30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs
+20ba52d3755c359d670968efa53b0604ff7cbed63a416f9f765b50a1f45074ac src/db/dashboard_presets.rs
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
diff --git a/docs/API.md b/docs/API.md
index 7e991cf..fa0afa7 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -76,6 +76,37 @@ rejection, 409 for already-completed setup, 422 for password policy and 429 for
throttling/busy workers. Error objects usually contain an `error` code/string;
framework extraction failures are not guaranteed to have that JSON shape.
+## Administrator Overview layout presets (0.33.7)
+
+The dashboard preset endpoints are private control-plane routes and use the
+existing administrator session cookie. The same Origin, request-marker and
+CSRF rules as other settings writes apply. They do **not** change the proxy
+configuration revision and cannot be called from public vhosts.
+
+| Method / path | Request or result |
+|---|---|
+| `GET /api/dashboard-presets` | Array of named layouts for the authenticated administrator |
+| `PUT /api/dashboard-presets/{name}` | Create/overwrite a named layout; JSON body described below; returns 204 |
+| `PATCH /api/dashboard-presets/{name}` | Rename using `{ "name": "new name" }`; returns 204 |
+| `DELETE /api/dashboard-presets/{name}` | Delete the named layout; returns 204 |
+
+A layout is `{ "dashboard_widgets": ["..."], "dashboard_hidden_widgets": ["..."],
+"dashboard_widget_sizes": null, "dashboard_density": "compact",
+"overview_samples": 600 }`, using registered widget IDs and the same
+width/density validation as administrator view preferences. History accepts
+60, 120, 300 or 600 samples. For sizes, `null` preserves legacy grouped
+placement; an object maps widget IDs to valid 12-column spans. The account
+may store at most 16 named layouts; names are 1-64 UTF-8 bytes with no
+control characters, surrounding whitespace or slash. Invalid values receive
+422; full capacity or a conflicting rename receives 409. Unknown delete
+returns 404. All write endpoints require a session-bound CSRF token.
+
+The data resides in SQLite schema 10 (`dashboard_presets`), separate from
+`admin_preferences`; the factory preset is not stored as a mutable row.
+The browser saves the chosen history sample count using its existing local
+preference when loading a preset. The client does not use JSON configuration
+files to store named presets.
+
## Configuration and observations
| Method / path | Result |
@@ -107,7 +138,7 @@ and rechecks session validity. It does not queue an unlimited per-viewer event l
or guarantee replay. The browser falls back to compact status/log polling only after SSE fails and stops polling after its recovery. Invalid SSE intervals are rejected with 422. An active dashboard renews
idle activity, not the absolute session deadline.
-Configuration history, introduced in SQLite schema 8 and retained in current schema 9, keeps the newest 512 successful manual/configuration revisions. `GET /api/config/history` is session-protected like the rest of the administration API; `limit` is bounded to 512 and `revision` selects one retained revision for diff/inspection. `POST /api/config/history/restore` also requires the normal CSRF/request-marker protections, rejects stale `expected_revision`, and never rewinds the revision counter: restoring revision 85 while current revision is 120 creates revision 121.
+Configuration history, introduced in SQLite schema 8 and retained in current schema 10, keeps the newest 512 successful manual/configuration revisions. `GET /api/config/history` is session-protected like the rest of the administration API; `limit` is bounded to 512 and `revision` selects one retained revision for diff/inspection. `POST /api/config/history/restore` also requires the normal CSRF/request-marker protections, rejects stale `expected_revision`, and never rewinds the revision counter: restoring revision 85 while current revision is 120 creates revision 121.
Configuration updates return 409 for a stale revision, 422 for validation failure
and 429 when the eight-operation update admission bound is reached. Persistence and
diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md
index 5d85606..e69de29 100644
--- a/docs/TEST_REPORT.md
+++ b/docs/TEST_REPORT.md
@@ -1,44 +0,0 @@
-# RustProxy 0.33.6 - verification report
-
-## Scope (2026-10-15)
-
-Additional 0.33.6 chart/UI changes:
-- All 14 full-width Overview and Web Server plots retain a fixed 125px SVG
- height, regardless of viewport width and dashboard density.
-- Grid lines are mapped to the actual plotting range rather than approximate
- horizontal positions.
-- Transform and Compression activity (plus other unbounded unit series) use
- sane zero-based minimum ranges and monotonic, rounded range expansion, so
- low values do not look like maximum utilization and spikes remain visible.
-- Full Cache and Transform live charts keep an 180px plot height and the same
- stable per-second range policy. Compact percent sparklines stay 0-100%.
-
-Previous 0.33.5 regression scope:
-
-Correct the Overview **System and process CPU** widget:
-- Use a fixed 0-100% vertical range, not the measured-history peak.
-- Map 25%, 50% and 75% to the horizontal grid lines.
-- Clip plotted CPU values above 100% at the top without modifying their measured values in the legend or summary.
-- Fix only this plot's CSS height at 125px, independent of viewport width, dashboard density and CPU samples.
-- Leave other chart scales, plot dimensions, status sampling and dashboard preferences unchanged.
-
-## Checks completed
-
-- **PASS:** `python scripts/tests/dashboard_stable_scales_0336_browser_test.py`
- with real bundled HTML/CSS/JS in simulated Chromium at 320, 375, 768,
- 1440 and 1920px and all three density modes. Covered 14 large plots,
- 125/180px plot heights, quarter-grid alignment, 0-100% CPU mapping,
- steady low/high traffic plotting, minimum count/rate/latency/memory scales,
- sticky expanded scales, reset and non-clipped live operation-rate labels.
-
-- **PASS:** `python scripts/tests/dashboard_cpu_fixed_scale_0335_browser_test.py` against the actual bundled HTML/CSS/JS in simulated Chromium, at 320, 375, 768, 1440 and 1920px, in normal/compact/ultra densities. Checked low and high CPU histories, missing samples, new status revisions, clipping above 100%, actual percentage labels, consistent grid positions, fixed plot height and no sample-dependent changes to the CPU/adjacent Connection Activity cards' heights.
-- **PASS:** All 11 prior `scripts/tests/*browser_test.py` simulated-browser regressions (including the 0.33.5 CPU test) and the new 0.33.6 browser regression: chart headers, resized widths, dashboard grid/modal resizing, sidebar, progress bars, System RAM, Web observability and Web Server metrics. No live RustProxy server was used.
-- **PASS:** `node scripts/tests/overview_samples_0330_ui_test.mjs`, `node scripts/tests/dashboard_resume_rate_charts_ui_test.mjs` and `node scripts/tests/dashboard_metric_visuals_ui_test.mjs`.
-- **PASS:** `./dev.sh doctor` and `python scripts/preflight.py` (source layout).
-- **PASS:** All eight `migrations/*.sql` applied successfully in two successive cycles to a disposable, in-memory SQLite database. This verifies SQL script execution/idempotency in this context, not an upgrade of production data.
-- **PASS:** Parsed `scripts/deploy/upgrade.py --help` and `scripts/migration/migrate_npm.py --help`; did not run a live upgrade or migration against an external database or service.
-
-## Not verified in this environment
-
-- `./dev.sh check` and `./dev.sh test` both exit 127: Cargo/rustc are unavailable. Therefore no Rust compilation, unit/integration suite, actual proxy runtime, real SSE feed, production database upgrade or live load test has been verified.
-- The real-runtime acceptance gate remains open until Rust builds and runs against representative test traffic.
diff --git a/docs/VHOSTS.md b/docs/VHOSTS.md
index bf5e75f..117279e 100644
--- a/docs/VHOSTS.md
+++ b/docs/VHOSTS.md
@@ -6,6 +6,17 @@
This is the source implementation contract. Rust compilation and runtime acceptance
were not performed in the packaging environment; see TEST_REPORT.md.
+## Backend quick-view in Vhosts
+
+Click the **Backends** count or the **Status** tile of a Vhost to open its
+backend list. Each member displays the configured ID, origin host/IP and port,
+HTTP/HTTPS scheme and HTTP mode, current reported health and `last_status`.
+`last_status` is the most recent HTTP code from a proxied request; zero/no
+member means no known HTTP response, not a failed probe. DNS origins show the
+configured hostname, as the status API does not report the resolved IP.
+The panel updates from the existing status feed without initiating requests,
+and supports Escape and click-outside dismissal.
+
## One vhost, one backend pool
A `vhosts` entry owns an `id`, `enabled`, `frontend`, `source`, `backends`, `load_balancing`,
diff --git a/langs/en_US.json b/langs/en_US.json
index 0f7e984..68c8093 100644
--- a/langs/en_US.json
+++ b/langs/en_US.json
@@ -209,7 +209,7 @@
"settingsDescription": "Switch between function tabs. Unsaved changes stay in their forms.",
"cacheHitRatio": "Cache hit ratio",
"processMemory": "Process memory",
- "systemLoad": "SYSTEM LOAD",
+ "systemLoad": "System load",
"systemChart": "Recent system CPU usage",
"systemMemory": "RAM used / total",
"systemSwap": "System swap",
@@ -323,7 +323,6 @@
"vhGenerationRequests": "Gen. requests",
"vhGenerationAge": "Gen. age",
"vhClientRotations": "Rotations",
- "vhIntro": "Each vhost owns its bound IPs, paths and backends. Unlisted addresses cannot reach it. Listener changes require restart.",
"vhList": "Virtual hosts",
"vhSearch": "Search vhosts",
"vhSearchPlaceholder": "Search hostname, vhost or backend",
@@ -343,6 +342,14 @@
"vhAddress": "IP:port (IPv6: [address]:port)",
"vhTls": "TLS enabled",
"vhHttp2": "Prefer HTTP/2 over TLS; retain HTTP/1.1",
+ "vhBackendPopupOpen": "Show backend details",
+ "vhBackendPopupTitle": "Backends",
+ "vhBackendPopupAddress": "IP / host",
+ "vhBackendPopupProtocol": "Protocol",
+ "vhBackendPopupLastHttp": "Last HTTP response",
+ "vhBackendPopupNoResponse": "No HTTP response yet",
+ "vhBackendPopupEmpty": "No configured backends.",
+ "vhBackendPopupHint": "The last HTTP response comes from an actual proxied request, not a health probe. DNS names show the configured host, not a resolved IP.",
"vhBackendPool": "Backends",
"vhAddBackend": "Add backend",
"vhMemberId": "Backend ID",
@@ -1795,7 +1802,7 @@
"statisticsSource": "Statistics source",
"dataDirectory": "Data directory",
"databaseLocation": "Database",
- "connectionActivity": "CONNECTION ACTIVITY",
+ "connectionActivity": "Connection activity",
"reloadCertificate": "Reload certificate",
"certificateApiComment": "Comment",
"certificateApiCertificateIds": "Allowed certificate IDs",
@@ -2510,7 +2517,7 @@
"backendWidgetNetworkBreakdown": "Connect / DNS / TLS / I/O",
"backendWidgetPoolGeneration": "Maximum active pool generation",
"backendWidgetPoolAge": "Oldest generation age",
- "overviewLayoutEditorOpen": "Arrange Overview",
+ "overviewLayoutEditorOpen": "Dashboard presets",
"overviewLayoutEditorTitle": "Arrange entire Overview",
"overviewLayoutEditorHelp": "Move any tile or panel by dragging or using the arrows. Hidden items can be enabled below. Default view previews the standard layout; Reset layout asks for confirmation before saving.",
"overviewLayoutEditorVisible": "Visible cards",
@@ -2524,7 +2531,7 @@
"overviewLayoutEditorHiddenEmpty": "All cards are visible.",
"overviewLayoutEditorMoveBefore": "Move card before",
"overviewLayoutEditorSaved": "Dashboard layout saved",
- "overviewLayoutEditorOpenSettings": "Visual Overview editor",
+ "overviewLayoutEditorOpenSettings": "Dashboard presets",
"overviewLayoutEditorSearch": "Find hidden card",
"overviewLayoutDefaultPreview": "Default view",
"overviewLayoutReset": "Reset layout",
@@ -2703,10 +2710,33 @@
"webLivePhpCapacity": "PHP capacity",
"webLiveKinds": "Static / PHP / other",
"webLiveBytes": "Body delivered",
- "webLiveNoPhp": "PHP disabled"
+ "webLiveNoPhp": "PHP disabled",
+ "dashboardPresetsTitle": "Dashboard presets",
+ "dashboardPresetsHelp": "Save named arrangements and restore them from any browser. Use Unlock layout to rearrange cards.",
+ "dashboardPresetsName": "Preset name",
+ "dashboardPresetsNamePlaceholder": "e.g. Monitoring",
+ "dashboardPresetsSave": "Save current view",
+ "dashboardPresetsEmpty": "No presets saved yet.",
+ "dashboardPresetsRenameHint": "To rename a preset, enter a new name above and click Rename beside it.",
+ "dashboardPresetsLoad": "Load",
+ "dashboardPresetsOverwrite": "Overwrite",
+ "dashboardPresetsRename": "Rename",
+ "dashboardPresetsDelete": "Delete",
+ "dashboardPresetsFactory": "Restore factory layout",
+ "dashboardPresetsResetConfirm": "Restore factory order, visibility, widths, density and history length? Your saved presets will not be deleted.",
+ "dashboardPresetsDeleteConfirm": "Delete saved preset \"{name}\"?",
+ "dashboardPresetsOverwriteConfirm": "Overwrite saved preset \"{name}\" with the current view?",
+ "dashboardPresetsSaved": "Preset saved.",
+ "dashboardPresetsLoaded": "Preset loaded.",
+ "dashboardPresetsRenamed": "Preset renamed.",
+ "dashboardPresetsDeleted": "Preset deleted.",
+ "dashboardPresetsRestored": "Factory dashboard layout restored.",
+ "dashboardPresetsNameRequired": "Enter a preset name (up to 64 characters).",
+ "dashboardPresetsLimit": "You can save up to 16 presets."
},
"overviewLayoutNarrow": "Decrease card width",
"overviewLayoutWiden": "Increase card width",
"overviewLayoutWidth": "Card width in 12-column grid",
- "overviewLayoutInlineHint": "Layout unlocked: move any card anywhere; use − / + to resize (2, 3, 4, 6 or 12 columns). Save and lock or cancel to revert."
+ "overviewLayoutInlineHint": "Layout unlocked: move any card anywhere; use − / + to resize (2, 3, 4, 6 or 12 columns). Save and lock or cancel to revert.",
+ "dashboardPresetsConflict": "Preset already exists or was removed. Refresh and try again."
}
\ No newline at end of file
diff --git a/langs/pl_pl.json b/langs/pl_pl.json
index 173327f..af4a849 100644
--- a/langs/pl_pl.json
+++ b/langs/pl_pl.json
@@ -209,7 +209,7 @@
"settingsDescription": "Przelaczaj karty funkcji. Niezapisane zmiany pozostaja w formularzach.",
"cacheHitRatio": "Trafienia cache",
"processMemory": "Pamięć procesu",
- "systemLoad": "OBCIĄŻENIE SYSTEMU",
+ "systemLoad": "Obciążenie systemu",
"systemChart": "Ostatnie próbki obciążenia CPU",
"systemMemory": "RAM użyta / łącznie",
"systemSwap": "Swap systemu",
@@ -323,7 +323,6 @@
"vhGenerationRequests": "Żądania gen.",
"vhGenerationAge": "Wiek gen.",
"vhClientRotations": "Rotacje",
- "vhIntro": "Każdy vhost ma własne adresy IP, ścieżki i backendy. Inne adresy go nie obsługują. Zmiana nasłuchu wymaga restartu.",
"vhList": "Hosty wirtualne",
"vhSearch": "Szukaj vhostów",
"vhSearchPlaceholder": "Szukaj domeny, vhosta lub backendu",
@@ -343,6 +342,14 @@
"vhAddress": "IP:port (IPv6: [adres]:port)",
"vhTls": "TLS włączony",
"vhHttp2": "Preferuj HTTP/2 przez TLS; zachowaj HTTP/1.1",
+ "vhBackendPopupOpen": "Pokaż szczegóły backendów",
+ "vhBackendPopupTitle": "Backendy",
+ "vhBackendPopupAddress": "IP / host",
+ "vhBackendPopupProtocol": "Protokół",
+ "vhBackendPopupLastHttp": "Ostatnia odpowiedź HTTP",
+ "vhBackendPopupNoResponse": "Brak odpowiedzi HTTP",
+ "vhBackendPopupEmpty": "Brak skonfigurowanych backendów.",
+ "vhBackendPopupHint": "Ostatni kod HTTP pochodzi z rzeczywistego żądania proxy, a nie sondy. Dla DNS wyświetlany jest skonfigurowany host, nie rozwiązany adres IP.",
"vhBackendPool": "Backendy",
"vhAddBackend": "Dodaj backend",
"vhMemberId": "Identyfikator backendu",
@@ -1795,7 +1802,7 @@
"statisticsSource": "Źródło statystyk",
"dataDirectory": "Katalog danych",
"databaseLocation": "Baza danych",
- "connectionActivity": "AKTYWNOŚĆ POŁĄCZEŃ",
+ "connectionActivity": "Aktywność połączeń",
"reloadCertificate": "Przeładuj certyfikat",
"certificateApiComment": "Komentarz",
"certificateApiCertificateIds": "Dozwolone ID certyfikatów",
@@ -2510,7 +2517,7 @@
"backendWidgetNetworkBreakdown": "TCP / DNS / TLS / I/O",
"backendWidgetPoolGeneration": "Najwyższa generacja puli",
"backendWidgetPoolAge": "Najstarsza generacja",
- "overviewLayoutEditorOpen": "Ułóż Dashboard",
+ "overviewLayoutEditorOpen": "Presety dashboardu",
"overviewLayoutEditorTitle": "Ułóż cały Dashboard",
"overviewLayoutEditorHelp": "Przeciągaj kafelki i panele lub używaj strzałek. Ukryte elementy włączysz poniżej. Widok domyślny pokazuje standardowy układ, a Resetuj układ prosi o potwierdzenie przed zapisem.",
"overviewLayoutEditorVisible": "Widoczne kafelki",
@@ -2524,7 +2531,7 @@
"overviewLayoutEditorHiddenEmpty": "Wszystkie kafelki są widoczne.",
"overviewLayoutEditorMoveBefore": "Przesuń kafelek przed",
"overviewLayoutEditorSaved": "Układ Dashboardu zapisany",
- "overviewLayoutEditorOpenSettings": "Wizualny edytor Overview",
+ "overviewLayoutEditorOpenSettings": "Presety dashboardu",
"overviewLayoutEditorSearch": "Znajdź ukryty kafelek",
"overviewLayoutDefaultPreview": "Widok domyślny",
"overviewLayoutReset": "Resetuj układ",
@@ -2703,10 +2710,33 @@
"webLivePhpCapacity": "Limit PHP",
"webLiveKinds": "Statyczne / PHP / inne",
"webLiveBytes": "Wysłana treść",
- "webLiveNoPhp": "PHP wyłączone"
+ "webLiveNoPhp": "PHP wyłączone",
+ "dashboardPresetsTitle": "Presety dashboardu",
+ "dashboardPresetsHelp": "Zapisuj układy pod nazwą i wczytuj je w dowolnej przeglądarce. Karty możesz zmieniać przez Odblokuj układ.",
+ "dashboardPresetsName": "Nazwa presetu",
+ "dashboardPresetsNamePlaceholder": "np. Monitoring",
+ "dashboardPresetsSave": "Zapisz bieżący widok",
+ "dashboardPresetsEmpty": "Brak zapisanych presetów.",
+ "dashboardPresetsRenameHint": "Aby zmienić nazwę presetu, wpisz nową nazwę powyżej i kliknij Zmień nazwę przy wybranym presecie.",
+ "dashboardPresetsLoad": "Wczytaj",
+ "dashboardPresetsOverwrite": "Nadpisz",
+ "dashboardPresetsRename": "Zmień nazwę",
+ "dashboardPresetsDelete": "Usuń",
+ "dashboardPresetsFactory": "Przywróć układ fabryczny",
+ "dashboardPresetsResetConfirm": "Przywrócić fabryczną kolejność, widoczność, szerokości, gęstość i liczbę próbek? Zapisane presety pozostaną.",
+ "dashboardPresetsDeleteConfirm": "Usunąć zapisany preset „{name}”?",
+ "dashboardPresetsOverwriteConfirm": "Nadpisać preset „{name}” bieżącym widokiem?",
+ "dashboardPresetsSaved": "Preset zapisany.",
+ "dashboardPresetsLoaded": "Preset wczytany.",
+ "dashboardPresetsRenamed": "Zmieniono nazwę presetu.",
+ "dashboardPresetsDeleted": "Preset usunięty.",
+ "dashboardPresetsRestored": "Przywrócono fabryczny układ dashboardu.",
+ "dashboardPresetsNameRequired": "Wpisz nazwę presetu (maks. 64 znaki).",
+ "dashboardPresetsLimit": "Możesz zapisać maksymalnie 16 presetów."
},
"overviewLayoutNarrow": "Zmniejsz szerokość karty",
"overviewLayoutWiden": "Zwiększ szerokość karty",
"overviewLayoutWidth": "Szerokość karty w siatce 12 kolumn",
- "overviewLayoutInlineHint": "Układ odblokowany: przenoś dowolne karty i zmieniaj ich szerokość przyciskami − / + (2, 3, 4, 6 lub 12 kolumn). Zapisz lub anuluj zmiany."
+ "overviewLayoutInlineHint": "Układ odblokowany: przenoś dowolne karty i zmieniaj ich szerokość przyciskami − / + (2, 3, 4, 6 lub 12 kolumn). Zapisz lub anuluj zmiany.",
+ "dashboardPresetsConflict": "Preset już istnieje lub został usunięty. Odśwież listę i spróbuj ponownie."
}
\ No newline at end of file
diff --git a/migrations/0010_dashboard_presets.sql b/migrations/0010_dashboard_presets.sql
new file mode 100644
index 0000000..0ffa73a
--- /dev/null
+++ b/migrations/0010_dashboard_presets.sql
@@ -0,0 +1,10 @@
+-- Named, administrator-scoped Overview presets. Keep this separate from the
+-- existing 4096-byte appearance preferences document and proxy configuration.
+CREATE TABLE IF NOT EXISTS dashboard_presets (
+ administrator_id INTEGER NOT NULL CHECK (administrator_id = 1),
+ name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 64),
+ document TEXT NOT NULL CHECK (length(document) BETWEEN 2 AND 8192),
+ updated_at INTEGER NOT NULL,
+ PRIMARY KEY (administrator_id, name),
+ FOREIGN KEY (administrator_id) REFERENCES administrator(id) ON DELETE CASCADE
+);
diff --git a/scripts/preflight.py b/scripts/preflight.py
index 523f839..34245dc 100755
--- a/scripts/preflight.py
+++ b/scripts/preflight.py
@@ -89,6 +89,16 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
for doc_path in sorted(embedded_docs):
if not (root / doc_path).is_file():
raise ValueError(f"Missing embedded Reader document: {doc_path}; restore the file or remove its Reader registration")
+ # An included Rust source fragment cannot begin with an inner doc comment.
+ # Catch E0753 here so a source-only release does not ship an unbuildable
+ # admin module when a compiler is unavailable during packaging.
+ admin_mod = (root / "src/admin/mod.rs").read_text(encoding="utf-8")
+ if 'include!("dashboard_presets.rs");' in admin_mod:
+ fragment = root / "src/admin/dashboard_presets.rs"
+ if not fragment.is_file():
+ raise ValueError(f"Missing included module: {fragment.relative_to(root)}")
+ if fragment.read_text(encoding="utf-8").lstrip().startswith(("//!", "/*!")):
+ raise ValueError("src/admin/dashboard_presets.rs starts with an inner doc comment; use /// instead (Rust E0753)")
text = (root / "src/lib.rs").read_text(encoding="utf-8")
if '#[path = "auth/mod.rs"]' not in text:
raise ValueError("src/lib.rs is not the 0.31.9 entry point; extract the complete package, not individual files.")
diff --git a/scripts/tests/dashboard_chart_headers_0335_browser_test.py b/scripts/tests/dashboard_chart_headers_0335_browser_test.py
index 0a762e5..b824583 100755
--- a/scripts/tests/dashboard_chart_headers_0335_browser_test.py
+++ b/scripts/tests/dashboard_chart_headers_0335_browser_test.py
@@ -4,12 +4,13 @@
Runs a simulated authenticated DOM rather than a live RustProxy service.
"""
from pathlib import Path
+from panel_html import render_admin_index
import json
import re
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[2]
-HTML = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
+HTML = render_admin_index(ROOT)
CSS = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
ROUTER = (ROOT / 'src/admin/router.rs').read_text(encoding='utf-8')
@@ -76,7 +77,7 @@ with sync_playwright() as p:
noTrafficEyebrow: !traffic.querySelector('.small-label'),
noTrafficLiveBadge: !traffic.querySelector('.pill.live'),
noConnectionLiveBadge: !connection.querySelector('.pill.live'),
- connectionTitleIntact: !!connection.querySelector('.card-heading .small-label[data-i18n="connectionActivity"]'),
+ connectionTitleIntact: !!connection.querySelector('.card-heading h3.overview-chart-heading[data-i18n="connectionActivity"]'),
peakInsideCard: peakRect.left >= cardRect.left - 1 && peakRect.right <= cardRect.right + 1,
peakInsideHeading: peakRect.top >= headingRect.top - 1 && peakRect.bottom <= headingRect.bottom + 1,
browserOverflow: document.documentElement.scrollWidth - innerWidth,
diff --git a/scripts/tests/dashboard_chart_typography_0337_browser_test.py b/scripts/tests/dashboard_chart_typography_0337_browser_test.py
new file mode 100755
index 0000000..d68a4ad
--- /dev/null
+++ b/scripts/tests/dashboard_chart_typography_0337_browser_test.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+"""All chart titles and series labels in simulated Chromium (no live Rust server).
+
+Covers Overview, diagnostics, Web Server, Traffic, Cache/Transform, Vhosts,
+Streams, and DOM-created chart/legend variants. Preserve localized DOM text.
+"""
+from pathlib import Path
+from panel_html import render_admin_index
+from playwright.sync_api import sync_playwright
+
+ROOT = Path(__file__).resolve().parents[2]
+HTML = render_admin_index(ROOT)
+CSS = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
+
+CHECK = r'''(settings) => {
+ const { density, compactBody, theme } = settings;
+ document.documentElement.dataset.dashboardDensity = density;
+ document.documentElement.dataset.theme = theme;
+ document.body.dataset.density = compactBody ? 'compact' : 'comfortable';
+
+ const titleSelectors = [
+ '#panel-overview .overview-wide-chart .card-heading h2',
+ '#panel-overview .activity-card .card-heading h2',
+ '#panel-overview :is(.system-card, .connection-card) .overview-chart-heading',
+ '#panel-overview :is(.stat-chart, .stat-donut) > span:first-child',
+ '.mini-live-chart > .mini-live-chart-head > span:first-child',
+ '.traffic-chart-card > h3.subheading',
+ '.function-live-chart-heading > strong',
+ '.vhost-chart-head > span:first-child',
+ ];
+ const monoSelectors = [
+ '.overview-wide-summary', '.overview-wide-legend',
+ '.overview-donut-legend', '.www-method-legend', '.stat-chart-legend',
+ '.mini-chart-legend', '.connection-legend',
+ '.traffic-protocol-legend', '.traffic-chart-legend',
+ '.function-live-chart-legend', '.stream-chart-legend',
+ '.chart-footer', '.vhost-chart-head > :not(:first-child)',
+ '.mini-live-chart-head > b', '.mini-live-chart-head > .mini-live-chart-actions',
+ '.network-interface-rate',
+ '.function-live-chart-summary > b', '.function-live-chart-heading small',
+ '.function-live-chart-samples',
+ '#panel-overview .stat-chart > small',
+ '#panel-overview .overview-wide-chart > .fine',
+ '#panel-overview .www-top-row', '.traffic-bar-row',
+ '#panel-overview .system-card .card-heading h2',
+ '#panel-overview .system-card .card-heading .meta',
+ '#panel-overview .system-card .memory-line',
+ '#panel-overview .connection-card .card-heading h2',
+ ];
+ const legendSelectors = [
+ '.overview-wide-legend', '.overview-donut-legend', '.www-ring-legend',
+ '.www-method-legend', '.stat-chart-legend', '.mini-chart-legend',
+ '.connection-legend', '.traffic-protocol-legend', '.traffic-chart-legend',
+ '.function-live-chart-legend', '.stream-chart-legend', '.stream-traffic-legend'
+ ];
+ const legendNodes = [...document.querySelectorAll(legendSelectors.join(','))];
+ const requestLegend = document.querySelector('[data-dashboard-widget="traffic_chart"] .traffic-protocol-legend');
+ const referenceLegendSize = getComputedStyle(requestLegend).fontSize;
+ const titleNodes = Array.from(document.querySelectorAll(titleSelectors.join(',')));
+ const monoNodes = Array.from(document.querySelectorAll(monoSelectors.join(',')));
+ const base = document.createElement('div');
+ base.style.fontFamily = 'var(--mono)';
+ document.body.append(base);
+ const mono = getComputedStyle(base).fontFamily;
+ base.style.fontFamily = 'var(--sans)';
+ const sans = getComputedStyle(base).fontFamily;
+ base.remove();
+
+ const titleErrors = titleNodes.flatMap(node => {
+ const css = getComputedStyle(node);
+ const inOverview = !!node.closest('#panel-overview');
+ const isSmallCaption = inOverview && !!node.closest('.mini-live-chart-head');
+ const wantedFont = isSmallCaption ? mono : sans;
+ const wantedSize = isSmallCaption ? '10px' : '12px';
+ const wantedCase = inOverview ? 'none' : 'uppercase';
+ const problems = [];
+ if (css.fontFamily !== wantedFont) problems.push(`font=${css.fontFamily}`);
+ if (css.fontSize !== wantedSize) problems.push(`size=${css.fontSize}`);
+ if (css.textTransform !== wantedCase) problems.push(`case=${css.textTransform}`);
+ return problems.map(problem => `${node.outerHTML.slice(0, 100)}: ${problem}`);
+ });
+ const monoErrors = monoNodes.flatMap(node => {
+ const all = [node, ...node.querySelectorAll('*')].filter(el => el.tagName.toLowerCase() !== 'svg');
+ return all.filter(el => getComputedStyle(el).fontFamily !== mono)
+ .map(el => `${el.outerHTML.slice(0, 100)}: ${getComputedStyle(el).fontFamily}`);
+ });
+ const legendErrors = legendNodes.flatMap(root => {
+ const children = [root, ...root.querySelectorAll('span, b, strong, small, li, em')];
+ return children.flatMap(node => {
+ const style = getComputedStyle(node);
+ const issues = [];
+ if (style.fontSize !== referenceLegendSize) issues.push(`size=${style.fontSize}`);
+ if (style.fontFamily !== mono) issues.push(`font=${style.fontFamily}`);
+ return issues.map(issue => `${node.outerHTML.slice(0, 95)} ${issue}`);
+ });
+ });
+ // Keep localized Overview titles in sentence case; other sections retain
+ // their independent uppercase chart-heading convention.
+ const compression = document.querySelector('[data-dashboard-widget="wide_compression_activity"]');
+ const compressionHeading = compression.querySelector('.card-heading h2');
+ const compressionLegend = compression.querySelector('.overview-wide-legend');
+ const originalTranslation = compressionHeading.textContent.trim() === 'Aktywność kompresji';
+ const legendFonts = [...compressionLegend.querySelectorAll('span, b')]
+ .map(el => getComputedStyle(el).fontFamily);
+ const unrelatedHeading = document.querySelector('#panel-traffic .card-heading h2');
+ const numericReading = document.querySelector('#system-cpu');
+ return {
+ counts: {titles: titleNodes.length, mono: monoNodes.length, legends: legendNodes.length},
+ titleErrors, monoErrors, legendErrors, referenceLegendSize, mono, sans,
+ originalTranslation, compressionTitleSize: getComputedStyle(compressionHeading).fontSize,
+ compressionTitleCase: getComputedStyle(compressionHeading).textTransform,
+ compressionLegendIsMono: legendFonts.every(font => font === mono),
+ unrelatedTitleSize: getComputedStyle(unrelatedHeading).fontSize,
+ numericReadingIsMono: getComputedStyle(numericReading).fontFamily === mono,
+ };
+}'''
+
+with sync_playwright() as api:
+ browser = api.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
+ for width in (320, 375, 768, 1440, 1920):
+ page = browser.new_page(viewport={'width': width, 'height': 960})
+ page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
+ .replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
+ page.add_style_tag(content=CSS)
+ page.evaluate('''() => {
+ // Representative dynamically rendered Vhost/Web Service/Stream charts.
+ for (const panel of ['panel-vhosts','panel-web-server','panel-streams']) {
+ document.getElementById(panel).insertAdjacentHTML('beforeend', `
+