v0.33.7
This commit is contained in:
1 parent
162e986d37
commit
6b937aaf85
54 files changed
+3527
-1325
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.33.6"
|
||||
version = "0.33.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+29
@@ -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
|
||||
|
||||
+59
-43
@@ -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
|
||||
|
||||
+32
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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`,
|
||||
|
||||
+37
-7
@@ -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."
|
||||
}
|
||||
+37
-7
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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.")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', `
|
||||
<div class="vhost-chart"><div class="vhost-chart-head">
|
||||
<span class="small-label">Connection history</span>
|
||||
<span class="fine">42 req/s</span></div>
|
||||
<svg viewBox="0 0 300 54"><path d=""></path></svg>
|
||||
<div class="stream-chart-legend"><span>RX 2 MB/s</span><span>TX 1 MB/s</span></div>
|
||||
</div>`);
|
||||
}
|
||||
const comp = document.querySelector('[data-dashboard-widget="wide_compression_activity"] .card-heading h2');
|
||||
comp.textContent = 'Aktywność kompresji';
|
||||
// The traffic renderer inserts these series dynamically.
|
||||
document.querySelectorAll('.traffic-chart-legend').forEach(root => {
|
||||
root.innerHTML = `<li><span class="traffic-chart-label">GET requests</span><b>12</b></li>`;
|
||||
});
|
||||
const sample = document.createElement('div');
|
||||
sample.innerHTML = `<div class="www-top-row"><span>php-worker</span><b>11/s</b></div>
|
||||
<div class="traffic-bar-row"><span class="traffic-bar-label">GET</span><b class="traffic-bar-value">22</b></div>
|
||||
<div class="network-interface-rate">100 Mb/s</div>`;
|
||||
document.getElementById('panel-overview').append(sample);
|
||||
}''')
|
||||
for density in ('normal','compact','ultra'):
|
||||
for compact_body in (False, True):
|
||||
for theme in ('dark', 'light'):
|
||||
result = page.evaluate(CHECK, {'density': density, 'compactBody': compact_body, 'theme': theme})
|
||||
description = (width, density, compact_body, theme)
|
||||
assert result['counts']['titles'] >= 43, (description, result['counts'])
|
||||
assert result['counts']['mono'] >= 90, (description, result['counts'])
|
||||
assert not result['titleErrors'], (description, result['titleErrors'][:10])
|
||||
assert not result['monoErrors'], (description, result['monoErrors'][:10])
|
||||
assert result['counts']['legends'] >= 39, (description, result['counts'])
|
||||
assert result['referenceLegendSize'] == '9px', description
|
||||
assert not result['legendErrors'], (description, result['legendErrors'][:10])
|
||||
assert result['originalTranslation'], description
|
||||
assert result['compressionTitleSize'] == '12px', description
|
||||
assert result['compressionTitleCase'] == 'none', description
|
||||
assert result['compressionLegendIsMono'], description
|
||||
assert result['numericReadingIsMono'], description
|
||||
assert result['unrelatedTitleSize'] != '12px', (description, result['unrelatedTitleSize'])
|
||||
print(f'PASS all chart families at {width}px (3 dashboard densities, 2 interface densities, 2 themes)')
|
||||
page.close()
|
||||
browser.close()
|
||||
@@ -7,10 +7,11 @@ fake network-interface samples. This does not exercise Rust HTTP/SSE.
|
||||
import json
|
||||
import re
|
||||
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 = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
HTML = render_admin_index(ROOT)
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.33.5 CPU plot autoscaling regression with production HTML/CSS/JS.
|
||||
"""Legacy CPU plot regression: low and high real values on a fixed 0-100% axis.
|
||||
|
||||
Uses Chromium with simulated authenticated DOM and status samples, not a live
|
||||
RustProxy server, SSE stream or real CPU telemetry.
|
||||
Autoscaling was intentionally replaced by 0.33.5 fixed scaling. This test
|
||||
retains the original history/reset and label checks using the current scale.
|
||||
Chromium is simulated, not a live RustProxy server.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
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')
|
||||
SCALE_JS = (ROOT / 'src/admin/web/app/chart-scales.js').read_text(encoding='utf-8')
|
||||
WIDE_JS = (ROOT / 'src/admin/web/app/overview-wide-metrics.js').read_text(encoding='utf-8')
|
||||
|
||||
|
||||
@@ -83,6 +86,7 @@ with sync_playwright() as p:
|
||||
window.fmtBytes = value => `${value} B`;
|
||||
window.rustProxyOverviewSamples = {getLimit: () => 60, onChange: () => {}};
|
||||
}''')
|
||||
page.add_script_tag(content=SCALE_JS)
|
||||
page.add_script_tag(content=WIDE_JS)
|
||||
|
||||
for density in ('compact', 'normal', 'ultra'):
|
||||
@@ -97,21 +101,21 @@ with sync_playwright() as p:
|
||||
assert result['svgWidth'] > 100 and abs(result['svgWidth'] - result['drawnWidth']) < 2, result
|
||||
assert result['legendInCard'] and result['overflow'] <= 2, (viewport, density, result)
|
||||
|
||||
# Measured 0.5-0.8% must be visible, not flattened by a 100% ceiling.
|
||||
# Measured 0.5-0.8% stay numerically visible without axis auto-expansion.
|
||||
first = render_cpu(page, 1, 0.5, 0.2)
|
||||
second = render_cpu(page, 1, 0.8, 0.6)
|
||||
assert first['system']['label'] == '0.5%' and second['process']['label'] == '0.6%'
|
||||
assert ys(second['system']['path']) == [77, 41], (viewport, second)
|
||||
assert ys(second['process']['path']) == [113, 65], (viewport, second)
|
||||
assert ys(second['system']['path']) == [136, 136], (viewport, second)
|
||||
assert ys(second['process']['path']) == [137, 136], (viewport, second)
|
||||
|
||||
# The peak follows observed history and also works above 100% (multi-core).
|
||||
# Multi-core values display accurately but clip at the fixed 100% ceiling.
|
||||
third = render_cpu(page, 1, 3.4, 2.2)
|
||||
assert ys(third['system']['path'])[-1] == 17, (viewport, third)
|
||||
assert ys(third['process']['path'])[-1] == 59, (viewport, third)
|
||||
assert ys(third['system']['path'])[-1] == 133, (viewport, third)
|
||||
assert ys(third['process']['path'])[-1] == 134, (viewport, third)
|
||||
high = render_cpu(page, 1, 250, 123)
|
||||
assert high['system']['label'] == '250.0%'
|
||||
assert ys(high['system']['path'])[-1] == 17, (viewport, high)
|
||||
assert 17 <= ys(high['process']['path'])[-1] < 137, (viewport, high)
|
||||
assert ys(high['process']['path'])[-1] == 17, (viewport, high)
|
||||
|
||||
# Revision changes reset the history. Zero stays at the plot baseline.
|
||||
idle = render_cpu(page, 2, 0, 0)
|
||||
@@ -119,6 +123,6 @@ with sync_playwright() as p:
|
||||
missing = render_cpu(page, 3, None, None)
|
||||
assert missing['system']['path'] == '' and missing['process']['label'] == '-'
|
||||
assert not errors, (viewport, errors)
|
||||
print(f'PASS CPU auto Y-scale and standard plot height at {viewport}px (all densities)')
|
||||
print(f'PASS legacy CPU labels/history on fixed Y-scale at {viewport}px (all densities)')
|
||||
page.close()
|
||||
browser.close()
|
||||
@@ -5,11 +5,12 @@ Uses Chromium with simulated authenticated DOM and status samples, not a live
|
||||
RustProxy server, SSE stream or real CPU telemetry.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
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')
|
||||
SCALE_JS = (ROOT / 'src/admin/web/app/chart-scales.js').read_text(encoding='utf-8')
|
||||
WIDE_JS = (ROOT / 'src/admin/web/app/overview-wide-metrics.js').read_text(encoding='utf-8')
|
||||
|
||||
@@ -7,6 +7,7 @@ not a Rust server integration test.
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -41,7 +42,7 @@ assert set(PRESET['dashboard_hidden_widgets']).issubset(set(PRESET['dashboard_wi
|
||||
assert PRESET['dashboard_density'] == 'compact' and PRESET['overview_samples'] == 600
|
||||
assert sum(w['id'].startswith('www_') and w['id'] in PRESET['dashboard_hidden_widgets'] for w in REGISTRY) == 12
|
||||
|
||||
HTML = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
HTML = render_admin_index(ROOT)
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
BUNDLE = app_bundle()
|
||||
assert BUNDLE.index('const ADMIN_DASHBOARD_DEFAULTS =') < BUNDLE.index('window.rustProxyOverviewSamples =')
|
||||
@@ -94,6 +95,8 @@ with sync_playwright() as p:
|
||||
return window.__savedDashboard;
|
||||
};
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
api = async path => { if (path === '/api/dashboard-presets') return []; throw new Error('unexpected API ' + path); };
|
||||
appConfirm = async () => true;
|
||||
return {
|
||||
order: ADMIN_DASHBOARD_WIDGETS,
|
||||
hidden: ADMIN_DEFAULT_HIDDEN_WIDGETS,
|
||||
@@ -166,13 +169,12 @@ with sync_playwright() as p:
|
||||
window.rustProxyStreamDashboard.render(status);
|
||||
}''')
|
||||
assert_visible(page, ['backend_health'], ['backend_traffic'])
|
||||
# Restore defaults in the modal after a custom layout was saved.
|
||||
# Restore factory layout through the new named-presets modal.
|
||||
page.locator('#overview-layout-open').evaluate('(node) => node.hidden = false')
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
page.locator('#overview-layout-default').click()
|
||||
page.locator('#overview-layout-save').click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
page.wait_for_function("document.getElementById('overview-presets-dialog').open")
|
||||
page.locator('#overview-presets-factory').click()
|
||||
page.wait_for_function("!document.getElementById('overview-presets-dialog').open")
|
||||
assert_visible(page, ['backend_traffic'], ['backend_health', 'stream_sessions', 'www_status'])
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets']
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_hidden_widgets') == PRESET['dashboard_hidden_widgets']
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderAdminIndex } from './panel_html.mjs';
|
||||
// Deterministic Overview widget regression; simulated DOM/status, no running API.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
@@ -6,7 +7,7 @@ import vm from 'node:vm';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const html = fs.readFileSync(path.join(root, 'src/admin/web/index.html'), 'utf8');
|
||||
const html = renderAdminIndex(root);
|
||||
const registry = JSON.parse(fs.readFileSync(path.join(root, 'src/admin/web/dashboard-widgets.json'), 'utf8'));
|
||||
const lang = JSON.parse(fs.readFileSync(path.join(root, 'langs/en_US.json'), 'utf8')).translations;
|
||||
const code = file => fs.readFileSync(path.join(root, 'src/admin/web', file), 'utf8');
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression: Overview visual layout editor fits without horizontal overflow.
|
||||
"""Regression: named Overview presets, responsive modal and browser actions.
|
||||
|
||||
Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences.
|
||||
Runs the real panel DOM/CSS/bundled JavaScript with mocked preferences/API.
|
||||
This is browser layout testing, not Rust server acceptance.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
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 = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
HTML = render_admin_index(ROOT)
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
|
||||
PACKS = {
|
||||
@@ -52,7 +53,7 @@ def load(page):
|
||||
defaultLanguage = 'en_US';
|
||||
language = 'pl_pl';
|
||||
document.documentElement.lang = 'pl-PL';
|
||||
document.querySelectorAll('#overview-layout-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
|
||||
document.querySelectorAll('#overview-presets-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
|
||||
}''', PACKS)
|
||||
page.evaluate('''() => {
|
||||
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
|
||||
@@ -72,98 +73,104 @@ def load(page):
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
language = 'pl_pl';
|
||||
document.documentElement.lang = 'pl-PL';
|
||||
document.querySelectorAll('#overview-layout-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
|
||||
document.querySelectorAll('#overview-presets-dialog [data-i18n]').forEach(node => node.textContent = t(node.dataset.i18n));
|
||||
}''')
|
||||
|
||||
|
||||
def geometry(page):
|
||||
return page.evaluate('''() => {
|
||||
const d = document.getElementById('overview-layout-dialog');
|
||||
const grid = document.getElementById('overview-layout-visible');
|
||||
const rect = node => node.getBoundingClientRect();
|
||||
const tiles = Array.from(grid.querySelectorAll('.overview-layout-tile'));
|
||||
const overflows = [];
|
||||
for (const tile of tiles) {
|
||||
const r = rect(tile);
|
||||
const toolbar = tile.querySelector('.overview-layout-tile-toolbar');
|
||||
if (tile.scrollWidth > tile.clientWidth + 2)
|
||||
overflows.push([tile.dataset.previewWidget, 'tile-scroll', tile.clientWidth, tile.scrollWidth]);
|
||||
for (const button of toolbar.querySelectorAll('button')) {
|
||||
const b = rect(button);
|
||||
if (b.left < r.left - 2 || b.right > r.right + 2)
|
||||
overflows.push([tile.dataset.previewWidget, 'button-outside', button.className, b.left, b.right, r.left, r.right]);
|
||||
}
|
||||
const buttons = Array.from(toolbar.querySelectorAll('button'));
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const a = rect(buttons[i]);
|
||||
for (let j = i + 1; j < buttons.length; j++) {
|
||||
const b = rect(buttons[j]);
|
||||
if (a.right - b.left > 1 && b.right - a.left > 1 && a.bottom - b.top > 1 && b.bottom - a.top > 1)
|
||||
overflows.push([tile.dataset.previewWidget, 'overlapping-buttons', buttons[i].className, buttons[j].className]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const hiddenList = document.getElementById('overview-layout-hidden');
|
||||
for (const row of hiddenList.querySelectorAll('.overview-layout-hidden-item')) {
|
||||
const r = rect(row), button = row.querySelector('button');
|
||||
const b = rect(button);
|
||||
if (b.right > r.right + 2 || b.left < r.left - 2)
|
||||
overflows.push(['hidden card', 'button-outside', b.left, b.right, r.left, r.right]);
|
||||
}
|
||||
const panelPreview = grid.querySelector('[data-preview-widget="network_diagnostics"]');
|
||||
const dialogRect = rect(d);
|
||||
return {
|
||||
cols: getComputedStyle(grid).gridTemplateColumns.split(' ').length,
|
||||
panelWidth: panelPreview ? rect(panelPreview).width : null,
|
||||
dialogWidth: d.clientWidth, dialogScrollWidth: d.scrollWidth,
|
||||
gridWidth: grid.clientWidth, gridScrollWidth: grid.scrollWidth,
|
||||
dialogLeft:dialogRect.left, dialogRight:dialogRect.right,
|
||||
visibleCount:tiles.length, overflows,
|
||||
firstTileWidth:rect(tiles[0]).width,
|
||||
};
|
||||
}''')
|
||||
|
||||
|
||||
# Regression for the replacement named-presets modal, with a simulated SQLite API.
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
for width in (320, 375, 550, 620, 800, 900, 960, 1158, 1440, 1920):
|
||||
context = browser.new_context(viewport={'width': width, 'height': 823})
|
||||
for width in (320, 375, 800, 1440):
|
||||
context = browser.new_context(viewport={'width': width, 'height': 880})
|
||||
page = context.new_page()
|
||||
errors = []
|
||||
page.on('pageerror', lambda err: errors.append(str(err)))
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
load(page)
|
||||
page.evaluate('''() => {
|
||||
const saved = structuredClone(ADMIN_APPEARANCE_DEFAULTS);
|
||||
window.__savedDashboard = saved;
|
||||
window.__presets = new Map();
|
||||
window.rustProxyPreferences.load = async () => window.__savedDashboard;
|
||||
window.rustProxyPreferences.current = () => window.__savedDashboard;
|
||||
window.rustProxyPreferences.update = async patch => {
|
||||
window.__savedDashboard = {...window.__savedDashboard, ...patch};
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
return window.__savedDashboard;
|
||||
};
|
||||
api = async (path, options={}) => {
|
||||
const method = options.method || 'GET';
|
||||
if (path === '/api/dashboard-presets' && method === 'GET')
|
||||
return [...window.__presets.entries()].map(([name, data]) => ({ name, ...structuredClone(data) }));
|
||||
if (path.startsWith('/api/dashboard-presets/')) {
|
||||
const name = decodeURIComponent(path.split('/').slice(3).join('/'));
|
||||
if (method === 'PUT') { window.__presets.set(name, JSON.parse(options.body)); return {}; }
|
||||
if (method === 'DELETE') { window.__presets.delete(name); return {}; }
|
||||
if (method === 'PATCH') {
|
||||
const changed = JSON.parse(options.body).name;
|
||||
if (window.__presets.has(changed)) throw new Error('already exists');
|
||||
window.__presets.set(changed, window.__presets.get(name));
|
||||
window.__presets.delete(name);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
throw new Error('unexpected API: ' + method + ' ' + path);
|
||||
};
|
||||
appConfirm = async () => true;
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
document.getElementById('overview-layout-open').hidden = false;
|
||||
}''')
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
check = geometry(page)
|
||||
expected = 1 if width <= 620 else 2 if width <= 900 else 3
|
||||
assert check['cols'] == expected, (width, check)
|
||||
assert check['visibleCount'] > 20
|
||||
assert check['panelWidth'] is not None
|
||||
assert abs(check['firstTileWidth'] - check['panelWidth']) < 3, (width, check)
|
||||
assert check['dialogLeft'] >= -1 and check['dialogRight'] <= width + 1, (width, check)
|
||||
assert check['dialogScrollWidth'] <= check['dialogWidth'] + 2, (width, check)
|
||||
assert check['gridScrollWidth'] <= check['gridWidth'] + 2, (width, check)
|
||||
assert not check['overflows'], (width, check['overflows'][:20])
|
||||
tile = '#overview-layout-visible [data-preview-widget="http_protocol"]'
|
||||
before = page.locator(tile).evaluate('(el) => el.getBoundingClientRect().width')
|
||||
page.locator(tile + ' .overview-layout-widen').click()
|
||||
assert page.locator(tile).get_attribute('data-dashboard-span') == '3'
|
||||
after = page.locator(tile).evaluate('(el) => el.getBoundingClientRect().width')
|
||||
assert abs(before - after) < 2, (width, before, after)
|
||||
assert not geometry(page)['overflows']
|
||||
page.locator(tile + ' .overview-layout-narrow').click()
|
||||
assert page.locator(tile).get_attribute('data-dashboard-span') == '2'
|
||||
if width == 1158:
|
||||
page.screenshot(path=str(ROOT.parent / 'modal_preview_1158.png'))
|
||||
if width == 375:
|
||||
page.screenshot(path=str(ROOT.parent / 'modal_preview_375.png'))
|
||||
page.locator(tile + ' .overview-layout-hide').click()
|
||||
assert page.locator(tile).count() == 0
|
||||
assert not geometry(page)['overflows']
|
||||
page.locator('#overview-layout-cancel').click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
page.wait_for_function("document.getElementById('overview-presets-dialog').open")
|
||||
assert page.locator('#overview-presets-list .overview-presets-row').count() == 0
|
||||
widths = page.evaluate('''() => {
|
||||
const d = document.getElementById('overview-presets-dialog');
|
||||
const r = d.getBoundingClientRect();
|
||||
return {left:r.left,right:r.right,overflow:d.scrollWidth - d.clientWidth};
|
||||
}''')
|
||||
assert widths['left'] >= -1 and widths['right'] <= width+1 and widths['overflow'] <= 2, (width, widths)
|
||||
# Persist the factory arrangement as a user preset.
|
||||
page.locator('#overview-presets-name').fill('Daily')
|
||||
page.locator('#overview-presets-save').click()
|
||||
page.wait_for_function("window.__presets.has('Daily')")
|
||||
assert page.locator('#overview-presets-list .overview-presets-row').count() == 1
|
||||
assert page.evaluate("window.__presets.get('Daily').overview_samples") == 600
|
||||
# Create another view, with customized widths, density and history.
|
||||
page.evaluate('''async () => {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_hidden_widgets:['requests'], dashboard_widget_sizes:{requests:3},
|
||||
dashboard_density:'ultra',
|
||||
});
|
||||
window.rustProxyOverviewSamples.setLimit(120, true);
|
||||
}''')
|
||||
page.locator('#overview-presets-name').fill('Detailed')
|
||||
page.locator('#overview-presets-save').click()
|
||||
page.wait_for_function("window.__presets.has('Detailed')")
|
||||
assert page.evaluate('''() => {
|
||||
const p=window.__presets.get('Detailed');
|
||||
return [p.dashboard_widget_sizes.requests,p.dashboard_density,p.overview_samples];
|
||||
}''') == [3,'ultra',120]
|
||||
# Named load must restore full layout and history.
|
||||
page.locator('.overview-presets-row').filter(has_text='Daily').locator('[data-preset-action="dashboardPresetsLoad"]').click()
|
||||
page.wait_for_function("!document.getElementById('overview-presets-dialog').open")
|
||||
assert page.evaluate('''() => [window.__savedDashboard.dashboard_density,
|
||||
window.__savedDashboard.dashboard_widget_sizes,
|
||||
window.rustProxyOverviewSamples.getLimit()]''') == ['compact',None,600]
|
||||
# Rename and delete remain administrator-scoped operations.
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-presets-dialog').open")
|
||||
page.locator('#overview-presets-name').fill('Nightly')
|
||||
page.locator('.overview-presets-row').filter(has_text='Detailed').locator('[data-preset-action="dashboardPresetsRename"]').click()
|
||||
page.wait_for_function("window.__presets.has('Nightly')")
|
||||
assert not page.evaluate("window.__presets.has('Detailed')")
|
||||
page.locator('.overview-presets-row').filter(has_text='Nightly').locator('[data-preset-action="dashboardPresetsDelete"]').click()
|
||||
page.wait_for_function("!window.__presets.has('Nightly')")
|
||||
assert page.evaluate("window.__presets.has('Daily')")
|
||||
page.locator('#overview-presets-factory').click()
|
||||
page.wait_for_function("!document.getElementById('overview-presets-dialog').open")
|
||||
assert page.evaluate("window.__savedDashboard.dashboard_widgets") == json.loads((ROOT / 'src/admin/web/dashboard-defaults.json').read_text())['dashboard_widgets']
|
||||
assert not errors, (width, errors)
|
||||
print('PASS named Dashboard presets save/load/rename/delete/factory and responsive modal:', width)
|
||||
context.close()
|
||||
print('PASS visual modal grid, no overlap/scroll, width/hide/cancel:', width)
|
||||
browser.close()
|
||||
print('PASS 0.33.3 modal regression (simulated API)')
|
||||
print('PASS Overview preset modal (simulated SQLite API; no running Rust server)')
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""All Overview widget labels and small values in simulated Chromium.
|
||||
|
||||
This is a DOM/CSS regression test, not a live RustProxy integration test.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import json
|
||||
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')
|
||||
PACKS = {name: json.loads((ROOT / f'langs/{name}.json').read_text(encoding='utf-8'))['translations']
|
||||
for name in ('en_US', 'pl_pl')}
|
||||
|
||||
CHECK = r'''({density, lang, translations}) => {
|
||||
document.documentElement.dataset.dashboardDensity = density;
|
||||
document.documentElement.lang = lang === 'pl_pl' ? 'pl-PL' : 'en-US';
|
||||
const grid = document.getElementById('dashboard-overview-grid');
|
||||
grid.querySelectorAll('[data-i18n]').forEach(node => {
|
||||
if (Object.hasOwn(translations, node.dataset.i18n)) node.textContent = translations[node.dataset.i18n];
|
||||
});
|
||||
const cards = [...grid.children].filter(node => node.hasAttribute('data-dashboard-widget'));
|
||||
const font = value => getComputedStyle(value).fontFamily;
|
||||
const probe = document.createElement('div');
|
||||
grid.append(probe);
|
||||
probe.style.fontFamily = 'var(--sans)'; const sans = font(probe);
|
||||
probe.style.fontFamily = 'var(--mono)'; const mono = font(probe);
|
||||
probe.remove();
|
||||
|
||||
const titleErrors = [];
|
||||
const titles = [];
|
||||
const labelErrors = [];
|
||||
const numericErrors = [];
|
||||
for (const card of cards) {
|
||||
let title;
|
||||
if (card.classList.contains('stat')) title = card.querySelector(':scope > span:first-child');
|
||||
else if (card.classList.contains('system-card') || card.classList.contains('connection-card'))
|
||||
title = card.querySelector('.overview-chart-heading');
|
||||
else if (card.querySelector('.mini-heading > h3')) title = card.querySelector('.mini-heading > h3');
|
||||
else title = card.querySelector('.card-heading h2');
|
||||
if (!title) { titleErrors.push(`${card.dataset.dashboardWidget}: missing title`); continue; }
|
||||
const style = getComputedStyle(title);
|
||||
titles.push(title);
|
||||
if (style.fontFamily !== sans || style.fontSize !== '12px' || style.textTransform !== 'none')
|
||||
titleErrors.push(`${card.dataset.dashboardWidget}: ${style.fontFamily} ${style.fontSize} ${style.textTransform}`);
|
||||
if (title.textContent.trim() === title.textContent.trim().toLocaleUpperCase('pl') &&
|
||||
/[a-ząćęłńóśżź]{4}/i.test(title.textContent) &&
|
||||
!/^(?:HIT|MISS|HTTP|CPU|RAM|WWW|RX|TX)/.test(title.textContent.trim()))
|
||||
titleErrors.push(`${card.dataset.dashboardWidget}: all-uppercase title ${title.textContent.trim()}`);
|
||||
for (const node of card.querySelectorAll(':scope > small, .mini-metrics dt, .mini-metrics dd, .mini-live-chart-head, .memory-line, .chart-footer, .meta, .small-label')) {
|
||||
if (font(node) !== mono) labelErrors.push(`${card.dataset.dashboardWidget}: ${node.outerHTML.slice(0, 85)}`);
|
||||
}
|
||||
const readings = card.classList.contains('stat')
|
||||
? [...card.querySelectorAll(':scope > strong'), ...card.querySelectorAll(':scope > small')]
|
||||
: [...card.querySelectorAll('.mini-metrics dd')];
|
||||
for (const node of readings) if (font(node) !== mono)
|
||||
numericErrors.push(`${card.dataset.dashboardWidget}: ${node.outerHTML.slice(0, 85)}`);
|
||||
}
|
||||
const nested = [...grid.querySelectorAll('.mini-live-chart > .mini-live-chart-head > span:first-child')];
|
||||
for (const node of nested) {
|
||||
const style = getComputedStyle(node);
|
||||
if (style.fontFamily !== mono || style.fontSize !== '10px' || style.textTransform !== 'none')
|
||||
labelErrors.push(`mini-live-chart: ${style.fontFamily} ${style.fontSize} ${style.textTransform}`);
|
||||
}
|
||||
const systemNumeric = getComputedStyle(document.querySelector('#system-cpu'));
|
||||
return {
|
||||
cardCount: cards.length, titleCount: titles.length, secondaryCount: nested.length,
|
||||
titleErrors, labelErrors, numericErrors,
|
||||
systemNumericMono: systemNumeric.fontFamily === mono,
|
||||
cpuLabel: document.querySelector('[data-dashboard-widget="system_chart"] .overview-chart-heading').textContent,
|
||||
connectionsLabel: document.querySelector('[data-dashboard-widget="connection_chart"] .overview-chart-heading').textContent,
|
||||
memoryLabel: document.querySelector('[data-dashboard-widget="memory"] > span:first-child').textContent,
|
||||
backendLabel: document.querySelector('[data-dashboard-widget="backend_health"] > span:first-child').textContent,
|
||||
diagnosticsTitle: document.querySelector('[data-dashboard-widget="process_diagnostics"] .mini-heading h3').textContent,
|
||||
};
|
||||
}'''
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.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': 900})
|
||||
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
|
||||
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
|
||||
page.add_style_tag(content=CSS)
|
||||
for density in ('normal', 'compact', 'ultra'):
|
||||
for lang in ('en_US', 'pl_pl'):
|
||||
result = page.evaluate(CHECK, {'density': density, 'lang': lang, 'translations': PACKS[lang]})
|
||||
descriptor = (width, density, lang)
|
||||
assert result['cardCount'] == result['titleCount'] >= 75, (descriptor, result)
|
||||
assert result['secondaryCount'] >= 6, descriptor
|
||||
assert not result['titleErrors'], (descriptor, result['titleErrors'][:10])
|
||||
assert not result['labelErrors'], (descriptor, result['labelErrors'][:10])
|
||||
assert not result['numericErrors'], (descriptor, result['numericErrors'][:10])
|
||||
assert result['systemNumericMono'], descriptor
|
||||
expected = ('System load', 'Connection activity') if lang == 'en_US' else ('Obciążenie systemu', 'Aktywność połączeń')
|
||||
assert (result['cpuLabel'], result['connectionsLabel']) == expected, (descriptor, result)
|
||||
assert result['memoryLabel'] and result['backendLabel'] and result['diagnosticsTitle'], descriptor
|
||||
print(f'PASS all Overview titles and small readings at {width}px (3 densities, 2 languages)')
|
||||
page.close()
|
||||
browser.close()
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline SQLite regression for additive admin preset migration and 16-slot upsert."""
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
all_scripts = sorted((ROOT / 'migrations').glob('*.sql'))
|
||||
assert [p.name for p in all_scripts][-1] == '0010_dashboard_presets.sql'
|
||||
con = sqlite3.connect(':memory:')
|
||||
con.execute('PRAGMA foreign_keys = ON')
|
||||
for _ in range(2):
|
||||
for script in all_scripts:
|
||||
con.executescript(script.read_text())
|
||||
con.execute("INSERT INTO administrator(id,username,password_hash,generation,created_at) VALUES(1,'owner','hash',1,0)")
|
||||
con.execute("INSERT INTO admin_preferences(administrator_id,document) VALUES(1,'{}')")
|
||||
|
||||
upsert = '''
|
||||
INSERT INTO dashboard_presets(administrator_id, name, document, updated_at)
|
||||
SELECT a.id, ?, ?, ? FROM administrator a WHERE a.id = 1 AND a.username = ?
|
||||
AND ((SELECT count(*) FROM dashboard_presets WHERE administrator_id = a.id) < 16
|
||||
OR EXISTS(SELECT 1 FROM dashboard_presets WHERE administrator_id = a.id AND name = ?))
|
||||
ON CONFLICT(administrator_id, name) DO UPDATE SET document = excluded.document, updated_at = excluded.updated_at
|
||||
'''
|
||||
for i in range(16):
|
||||
name = f'Preset {i}'
|
||||
assert con.execute(upsert, (name,'{}',i,'owner',name)).rowcount == 1
|
||||
assert con.execute(upsert, ('Overflow','{}',16,'owner','Overflow')).rowcount == 0
|
||||
assert con.execute(upsert, ('Preset 0','{"changed":true}',17,'owner','Preset 0')).rowcount == 1
|
||||
assert con.execute(upsert, ('Other account','{}',18,'intruder','Other account')).rowcount == 0
|
||||
assert con.execute('SELECT count(*) FROM dashboard_presets').fetchone()[0] == 16
|
||||
assert con.execute('SELECT document FROM dashboard_presets WHERE name="Preset 0"').fetchone()[0] == '{"changed":true}'
|
||||
assert con.execute('UPDATE OR IGNORE dashboard_presets SET name=? WHERE name=?', ('Preset 1','Preset 0')).rowcount == 0
|
||||
assert con.execute('UPDATE OR IGNORE dashboard_presets SET name=? WHERE name=?', ('Renamed','Preset 0')).rowcount == 1
|
||||
con.execute('DELETE FROM administrator WHERE id=1')
|
||||
assert con.execute('SELECT count(*) FROM dashboard_presets').fetchone()[0] == 0
|
||||
assert con.execute('SELECT count(*) FROM admin_preferences').fetchone()[0] == 0
|
||||
print('PASS SQLite migrations repeated, named-preset CRUD, cap, account ownership and cascade')
|
||||
@@ -7,6 +7,7 @@ not a Rust server integration test.
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -41,7 +42,7 @@ assert set(PRESET['dashboard_hidden_widgets']).issubset(set(PRESET['dashboard_wi
|
||||
assert PRESET['dashboard_density'] == 'compact' and PRESET['overview_samples'] == 600
|
||||
assert sum(w['id'].startswith('www_') and w['id'] in PRESET['dashboard_hidden_widgets'] for w in REGISTRY) == 12
|
||||
|
||||
HTML = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
HTML = render_admin_index(ROOT)
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
BUNDLE = app_bundle()
|
||||
assert BUNDLE.index('const ADMIN_DASHBOARD_DEFAULTS =') < BUNDLE.index('window.rustProxyOverviewSamples =')
|
||||
@@ -96,6 +97,8 @@ with sync_playwright() as p:
|
||||
return window.__savedDashboard;
|
||||
};
|
||||
applyAdminPreferences(window.__savedDashboard);
|
||||
api = async path => { if (path === '/api/dashboard-presets') return []; throw new Error('unexpected API ' + path); };
|
||||
appConfirm = async () => true;
|
||||
}''')
|
||||
# Unchanged previous accounts preserve historic grouped rows.
|
||||
assert page.evaluate('''() => ({
|
||||
@@ -142,27 +145,26 @@ with sync_playwright() as p:
|
||||
assert page.locator(selector).get_attribute('data-dashboard-span') == '2'
|
||||
assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '6'
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None
|
||||
# In a fresh draft, the modal editor also provides width controls.
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
tile = '#overview-layout-visible [data-preview-widget="wide_transform_activity"]'
|
||||
page.locator(tile + ' .overview-layout-narrow').click()
|
||||
# The actual unlocked dashboard remains the only layout editor.
|
||||
page.locator('#overview-layout-unlock').click()
|
||||
page.wait_for_function('window.rustProxyInlineLayout.isEditing()')
|
||||
tile = '[data-dashboard-widget="wide_transform_activity"]'
|
||||
page.locator(tile + ' .overview-inline-narrow').click()
|
||||
assert page.locator(tile).get_attribute('data-dashboard-span') == '4'
|
||||
page.locator(tile + ' .overview-layout-narrow').click()
|
||||
page.locator(tile + ' .overview-inline-narrow').click()
|
||||
assert page.locator(tile).get_attribute('data-dashboard-span') == '3'
|
||||
# Modal drag-and-drop also works across former small/wide size groups.
|
||||
page.evaluate('''() => {
|
||||
const from = document.querySelector('[data-preview-widget="wide_transform_activity"]');
|
||||
const to = document.querySelector('[data-preview-widget="requests"]');
|
||||
const from = document.querySelector('[data-dashboard-widget="wide_transform_activity"] .overview-inline-grip');
|
||||
const to = document.querySelector('[data-dashboard-widget="requests"] .overview-inline-grip');
|
||||
const transfer = new DataTransfer();
|
||||
from.dispatchEvent(new DragEvent('dragstart', {bubbles:true,cancelable:true,dataTransfer:transfer}));
|
||||
to.dispatchEvent(new DragEvent('drop', {bubbles:true,cancelable:true,dataTransfer:transfer}));
|
||||
to.dispatchEvent(new DragEvent('drop', {bubbles:true,cancelable:true,dataTransfer:transfer,clientX:0}));
|
||||
from.dispatchEvent(new DragEvent('dragend', {bubbles:true}));
|
||||
}''')
|
||||
page.locator('#overview-layout-save').click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
page.locator('#overview-inline-save').click()
|
||||
page.wait_for_function('!window.rustProxyInlineLayout.isEditing()')
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes.wide_transform_activity') == 3
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets[0]') == 'wide_transform_activity'
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets[0]') == 'wide_transform_activity'
|
||||
# Reload account settings: new card width persists.
|
||||
page.evaluate('applyAdminPreferences(window.__savedDashboard)')
|
||||
assert page.locator('[data-dashboard-widget="wide_transform_activity"]').get_attribute('data-dashboard-span') == '3'
|
||||
@@ -185,10 +187,9 @@ with sync_playwright() as p:
|
||||
assert all(b['w'] > 150 for b in data['boxes']), (width,data)
|
||||
assert page.evaluate('document.documentElement.scrollWidth - window.innerWidth') <= 2, (width, data)
|
||||
page.locator('#overview-layout-open').click()
|
||||
page.wait_for_function("document.getElementById('overview-layout-dialog').open")
|
||||
page.locator('#overview-layout-default').click()
|
||||
page.locator('#overview-layout-save').click()
|
||||
page.wait_for_function("!document.getElementById('overview-layout-dialog').open")
|
||||
page.wait_for_function("document.getElementById('overview-presets-dialog').open")
|
||||
page.locator('#overview-presets-factory').click()
|
||||
page.wait_for_function("!document.getElementById('overview-presets-dialog').open")
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widget_sizes') is None
|
||||
assert page.evaluate('window.__savedDashboard.dashboard_widgets') == PRESET['dashboard_widgets']
|
||||
assert page.evaluate('document.getElementById("dashboard-overview-grid").dataset.overviewLayoutMode') == 'legacy'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderAdminIndex } from './panel_html.mjs';
|
||||
// Simulated DOM/status regression: no holes or artificial zeros after tab resume.
|
||||
// This intentionally does not exercise the running Rust server or real browser.
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -8,7 +9,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const code = file => fs.readFileSync(path.join(root, 'src/admin/web', file), 'utf8');
|
||||
const html = code('index.html');
|
||||
const html = renderAdminIndex(root);
|
||||
|
||||
class Element {
|
||||
constructor() { this.attributes = new Map(); this.style = {}; this.textContent = ''; this.children = []; this.hidden = false; }
|
||||
|
||||
Regular → Executable
+2
-1
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.33.6 plot size/scale checks in simulated Chromium, not a running RustProxy."""
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
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')
|
||||
SCALES = (ROOT / 'src/admin/web/app/chart-scales.js').read_text(encoding='utf-8')
|
||||
RATES = (ROOT / 'src/admin/web/app/rate-sampling.js').read_text(encoding='utf-8')
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Simulated Chromium regression for unified subdued metric/chart titles.
|
||||
|
||||
Tests computed CSS in the assembled panel DOM; not a live Rust server.
|
||||
"""
|
||||
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'''({theme, density}) => {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.documentElement.dataset.dashboardDensity = density;
|
||||
const grid = document.getElementById('dashboard-overview-grid');
|
||||
const probe = document.createElement('span');
|
||||
probe.style.color = 'var(--muted)';
|
||||
document.body.append(probe);
|
||||
const expected = getComputedStyle(probe).color;
|
||||
probe.style.color = 'var(--text)';
|
||||
const primary = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
|
||||
const color = node => getComputedStyle(node).color;
|
||||
const cards = [...grid.children].filter(node => node.hasAttribute('data-dashboard-widget'));
|
||||
const titles = cards.map(card => {
|
||||
let node;
|
||||
if (card.classList.contains('stat')) node = card.querySelector(':scope > span:first-child');
|
||||
else if (card.matches('.system-card,.connection-card')) node = card.querySelector('.overview-chart-heading');
|
||||
else node = card.querySelector('.mini-heading > h3') || card.querySelector('.card-heading h2');
|
||||
return [card.dataset.dashboardWidget, node];
|
||||
});
|
||||
const otherSelectors = [
|
||||
'#panel-cache .stats-grid .stat > span:first-child',
|
||||
'#panel-transform .stats-grid .stat > span:first-child',
|
||||
'#panel-traffic .stat > span:first-child',
|
||||
'#panel-cache > .card .card-heading h2',
|
||||
'#panel-transform > .card .card-heading h2',
|
||||
'.traffic-chart-card > h3.subheading',
|
||||
'.function-live-chart-heading > strong',
|
||||
'.mini-live-chart-head > span:first-child',
|
||||
'.vhost-chart-head > span:first-child',
|
||||
'.vhost-live-metric > span',
|
||||
];
|
||||
// Include dynamically rendered Vhost/Streams chart and Vhost metric titles.
|
||||
const container = document.getElementById('panel-vhosts');
|
||||
const fixture = document.createElement('div');
|
||||
fixture.innerHTML = `<div class="vhost-chart"><div class="vhost-chart-head">
|
||||
<span class="small-label">Connections history</span><span class="fine">42/s</span>
|
||||
</div></div><div class="vhost-live-metric"><span>Requests</span><strong>42</strong></div>`;
|
||||
container.append(fixture);
|
||||
const otherTitles = otherSelectors.flatMap(selector => [...document.querySelectorAll(selector)]
|
||||
.map(node => [selector, node]));
|
||||
const errors = [...titles, ...otherTitles].flatMap(([name, node]) => {
|
||||
if (!node) return [`${name} missing title`];
|
||||
if (color(node) !== expected) return [`${name}: ${color(node)} != ${expected} (${node.textContent.trim().slice(0, 35)})`];
|
||||
return [];
|
||||
});
|
||||
const numeric = [
|
||||
document.querySelector('#system-cpu'),
|
||||
document.querySelector('#panel-overview .system-card .card-heading h2'),
|
||||
document.querySelector('#panel-overview .connection-card .card-heading h2'),
|
||||
document.querySelector('[data-dashboard-widget="requests"] > strong'),
|
||||
fixture.querySelector('.vhost-live-metric strong'),
|
||||
].map(node => ({node: node && node.outerHTML.slice(0, 80), color: node && color(node)}));
|
||||
fixture.remove();
|
||||
return {expected, primary, count: titles.length, otherCount: otherTitles.length,
|
||||
errors, numeric};
|
||||
}'''
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
for width in (320, 768, 1440):
|
||||
page = browser.new_page(viewport={'width': width, 'height': 900})
|
||||
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
|
||||
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
|
||||
page.add_style_tag(content=CSS)
|
||||
for theme in ('dark', 'light', 'ultra_dark'):
|
||||
for density in ('normal', 'compact', 'ultra'):
|
||||
result = page.evaluate(CHECK, {'theme': theme, 'density': density})
|
||||
descriptor = width, theme, density
|
||||
assert result['count'] == 78, (descriptor, result['count'])
|
||||
assert result['otherCount'] >= 35, (descriptor, result['otherCount'])
|
||||
assert result['primary'] != result['expected'], (descriptor, result)
|
||||
assert not result['errors'], (descriptor, result['errors'][:25])
|
||||
assert all(item['color'] == result['primary'] for item in result['numeric']), (descriptor, result['numeric'])
|
||||
print(f'PASS muted titles, primary numeric readings at {width}px (3 themes, 3 densities)')
|
||||
page.close()
|
||||
browser.close()
|
||||
print('PASS subdued metric/chart title CSS (simulated Chromium, no Rust runtime)')
|
||||
@@ -1,3 +1,4 @@
|
||||
import { renderAdminIndex } from './panel_html.mjs';
|
||||
// Simulated browser/status contract: this does not exercise the Rust server.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
@@ -7,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const html = read('src/admin/web/index.html');
|
||||
const html = renderAdminIndex(root);
|
||||
const controllerSource = read('src/admin/web/app/overview-samples.js');
|
||||
const store = new Map();
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Same four compile-time substitutions as src/admin/public.rs.
|
||||
export function renderAdminIndex(root) {
|
||||
const web = path.join(root, 'src/admin/web');
|
||||
let html = fs.readFileSync(path.join(web, 'index.html'), 'utf8');
|
||||
const metrics = fs.readFileSync(path.join(web, 'partials/overview-metrics.html'), 'utf8')
|
||||
.split('<!-- RP_METRICS_SPLIT -->');
|
||||
const charts = fs.readFileSync(path.join(web, 'partials/overview-charts.html'), 'utf8')
|
||||
.split('<!-- RP_CHARTS_SPLIT -->');
|
||||
if (metrics.length !== 2 || charts.length !== 2) throw new Error('Invalid Overview partials');
|
||||
for (const [marker, text] of [
|
||||
['METRICS_MAIN', metrics[0]], ['CHARTS_MAIN', charts[0]],
|
||||
['METRICS_WEB', metrics[1]], ['CHARTS_WEB', charts[1]],
|
||||
]) {
|
||||
const token = `<!-- RP_${marker} -->`;
|
||||
if (!html.includes(token)) throw new Error(`Missing ${token}`);
|
||||
html = html.replace(token, text);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
"""Return the actual assembled embedded Overview HTML to simulated-browser tests."""
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def render_admin_index(root: Path) -> str:
|
||||
web = root / 'src/admin/web'
|
||||
html = (web / 'index.html').read_text(encoding='utf-8')
|
||||
metrics = (web / 'partials/overview-metrics.html').read_text(encoding='utf-8').split('<!-- RP_METRICS_SPLIT -->')
|
||||
charts = (web / 'partials/overview-charts.html').read_text(encoding='utf-8').split('<!-- RP_CHARTS_SPLIT -->')
|
||||
assert len(metrics) == len(charts) == 2
|
||||
for key, fragment in (
|
||||
('METRICS_MAIN', metrics[0]), ('CHARTS_MAIN', charts[0]),
|
||||
('METRICS_WEB', metrics[1]), ('CHARTS_WEB', charts[1]),
|
||||
):
|
||||
marker = '<!-- RP_' + key + ' -->'
|
||||
assert html.count(marker) == 1
|
||||
html = html.replace(marker, fragment)
|
||||
return html
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
import re
|
||||
|
||||
from PIL import Image
|
||||
@@ -14,7 +15,7 @@ from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
|
||||
APP_HTML = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
||||
APP_HTML = render_admin_index(ROOT)
|
||||
assert re.search(r'<div class="overview-meter system-memory-meter" id="memory-meter"', APP_HTML)
|
||||
assert 'id="memory-meter-fill"' in APP_HTML
|
||||
CORNERS = {'default': '3px', 'square': '0px', 'subtle': '2px', 'rounded': '4px', 'extra': '999px'}
|
||||
|
||||
@@ -5,10 +5,11 @@ Uses actual production HTML/CSS in headless Chromium, not a live RustProxy
|
||||
server or authenticated preference/session roundtrip.
|
||||
"""
|
||||
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 = (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')
|
||||
THEMES = ('dark', 'ultra_dark', 'light', 'cream')
|
||||
PALETTES = ('default', 'soft', 'technical', 'high_contrast', 'neon', 'monochrome', 'warm', 'cool')
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
This is a simulated authenticated DOM/status test, not a Rust runtime integration test.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
import json
|
||||
import re
|
||||
from playwright.sync_api import sync_playwright
|
||||
@@ -29,7 +30,7 @@ def bundle_for(section):
|
||||
result.append(json.loads(line[:-1]))
|
||||
return '\n'.join(result)
|
||||
|
||||
html = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
html = render_admin_index(ROOT)
|
||||
css = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
js = bundle_for('admin_app_javascript')
|
||||
with sync_playwright() as playwright:
|
||||
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Simulated Chromium Vhost backend quick-view, without a Rust server.
|
||||
|
||||
Verify both backend triggers, the shared click-to-open upstream H2 protocol
|
||||
popover without native title flicker, and the two Cache card margins.
|
||||
Exercise live updates, safe text, keyboard/outside dismissal and mobile sizing.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
from playwright.sync_api import sync_playwright
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HTML = render_admin_index(ROOT)
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
|
||||
PACKS = {lang: {'id': lang, **json.loads((ROOT / 'langs' / f'{lang}.json').read_text())}
|
||||
for lang in ('en_US', 'pl_pl')}
|
||||
|
||||
|
||||
def app_bundle():
|
||||
found = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
|
||||
assert found
|
||||
code = ROUTER[found.end():]
|
||||
code = code[:re.search(r'\]\s*\.join\("\\n"\)', code).start()]
|
||||
fragments = []
|
||||
for line in code.splitlines():
|
||||
line = line.strip()
|
||||
include = re.search(r'include_str!\("([^"]+)"\)', line)
|
||||
if include:
|
||||
fragments.append((ROOT / 'src/admin' / include.group(1)).read_text())
|
||||
elif line == 'admin_vhosts_javascript(),':
|
||||
fragments.append('(() => {\n' + '\n'.join(
|
||||
(ROOT / 'src/admin/web/vhosts' / f'{name}.js').read_text()
|
||||
for name in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();')
|
||||
elif line.startswith('"') and line.endswith('",'):
|
||||
fragments.append(json.loads(line[:-1]))
|
||||
return '\n'.join(fragments)
|
||||
|
||||
|
||||
CONFIG = {
|
||||
'vhosts': [{
|
||||
'id': 'api-one', 'enabled': True, 'source': {'type': 'reverse_proxy'},
|
||||
'frontend': {'hosts': ['api.example.test'], 'listener_ids': []},
|
||||
'backends': [
|
||||
{'id': 'origin-http', 'url': 'http://10.12.0.5:8080/api', 'protocol': 'http2', 'enabled': True},
|
||||
{'id': '<origin-dns>', 'url': 'https://hiddenuser:secret@backend.example.test:8443/path', 'protocol': 'auto', 'enabled': True},
|
||||
]
|
||||
}, {'id': 'static-only', 'enabled': True,
|
||||
'source': {'type': 'web_server', 'web_service_id': 'static-test'},
|
||||
'frontend': {'hosts': ['static.example.test'], 'listener_ids': []}, 'backends': []}],
|
||||
'listeners': {'endpoints': []}, 'web_services': [], 'acl_profiles': [],
|
||||
}
|
||||
|
||||
|
||||
def snapshot(first=200):
|
||||
return {'backend_pools': [{
|
||||
'id': 'api-one', 'health_mode': 'on',
|
||||
'members': [
|
||||
{'id': 'origin-http', 'status': 'up', 'last_status': first,
|
||||
'requests': 12, 'active': 0, 'errors': 0, 'bytes_in': 0, 'bytes_out': 0},
|
||||
{'id': '<origin-dns>', 'status': 'down', 'last_status': 0,
|
||||
'requests': 0, 'active': 0, 'errors': 0, 'bytes_in': 0, 'bytes_out': 0},
|
||||
]
|
||||
}], 'vhost_metrics': {}}
|
||||
|
||||
|
||||
def protocol_snapshot(h2_count):
|
||||
status = snapshot()
|
||||
status['backend_pools'][0]['members'][0].update({
|
||||
'upstream_protocol_observed': 'h2',
|
||||
'upstream_protocol_last_seen_ms': int(time.time() * 1000),
|
||||
'upstream_protocol_http1_total': 3,
|
||||
'upstream_protocol_http2_total': h2_count,
|
||||
})
|
||||
return status
|
||||
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
for width in (320, 375, 768, 1440):
|
||||
page = browser.new_page(viewport={'width': width, 'height': 820})
|
||||
errors = []
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
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.add_script_tag(content=app_bundle())
|
||||
page.evaluate('''([config, status, packs]) => {
|
||||
languagePacks = new Map(Object.entries(packs));
|
||||
defaultLanguage = 'en_US'; language = 'pl_pl';
|
||||
document.documentElement.lang = 'pl-PL';
|
||||
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
|
||||
document.getElementById('login').hidden = true;
|
||||
document.getElementById('workspace').hidden = false;
|
||||
document.getElementById('panel-vhosts').hidden = false;
|
||||
currentPanel = 'vhosts';
|
||||
envelope = {revision: 12, config};
|
||||
latestStatus = status;
|
||||
window.rustProxyVhosts.render();
|
||||
}''', [CONFIG, snapshot(), PACKS])
|
||||
vhost = page.locator('[data-vhost-card="api-one"]')
|
||||
backend = vhost.locator('.vhost-detail.vhost-backend-trigger')
|
||||
health = vhost.locator('.vhost-live-metric.vhost-backend-trigger')
|
||||
assert backend.count() == health.count() == 1, width
|
||||
assert backend.get_attribute('aria-expanded') == 'false'
|
||||
assert backend.get_attribute('aria-haspopup') == 'dialog'
|
||||
# Clickable backend/health fields must match their non-button peers,
|
||||
# including for user-selected interface corner styles.
|
||||
shapes = page.evaluate('''() => {
|
||||
const root = document.querySelector('[data-vhost-card="api-one"]');
|
||||
const peers = [
|
||||
[root.querySelector('.vhost-detail.vhost-backend-trigger'), root.querySelector('.vhost-detail:not(.vhost-backend-trigger)')],
|
||||
[root.querySelector('.vhost-live-metric.vhost-backend-trigger'), root.querySelector('.vhost-live-metric:not(.vhost-backend-trigger)')]
|
||||
];
|
||||
const checks = ['square', 'subtle', 'rounded', 'extra'].map(corner => {
|
||||
document.documentElement.dataset.cornerStyle = corner;
|
||||
return {corner, tiles: peers.map(([trigger, peer]) => {
|
||||
const left = getComputedStyle(trigger), right = getComputedStyle(peer);
|
||||
return {radius: left.borderRadius, peerRadius: right.borderRadius,
|
||||
padding: left.padding, peerPadding: right.padding,
|
||||
font: left.fontFamily, peerFont: right.fontFamily};
|
||||
})};
|
||||
});
|
||||
document.documentElement.removeAttribute('data-corner-style');
|
||||
return checks;
|
||||
}''')
|
||||
for shape in shapes:
|
||||
for tile in shape['tiles']:
|
||||
assert tile['radius'] == tile['peerRadius'] == '0px', (width, shape)
|
||||
assert tile['padding'] == tile['peerPadding'], (width, shape)
|
||||
assert tile['font'] == tile['peerFont'], (width, shape)
|
||||
backend.click()
|
||||
popup = page.locator('#vhost-backend-popup')
|
||||
assert popup.count() == 1, width
|
||||
assert backend.get_attribute('aria-expanded') == 'true'
|
||||
assert popup.get_attribute('role') == 'dialog'
|
||||
# The anchored popup is a surface, not a permanently rounded tooltip.
|
||||
# It must follow live interface corner preference changes without
|
||||
# dismissing/recreating the open backend (or shared H2) popup.
|
||||
corners = page.evaluate('''() => {
|
||||
const node = document.getElementById('vhost-backend-popup');
|
||||
const saved = document.documentElement.getAttribute('data-corner-style');
|
||||
const out = {};
|
||||
for (const style of ['default', 'square', 'subtle', 'rounded', 'extra']) {
|
||||
document.documentElement.dataset.cornerStyle = style;
|
||||
out[style] = {
|
||||
popup: getComputedStyle(node).borderRadius,
|
||||
row: getComputedStyle(node.querySelector('.vhost-backend-popup-row')).borderRadius,
|
||||
close: getComputedStyle(node.querySelector('.vhost-backend-popup-close')).borderRadius,
|
||||
reference: getComputedStyle(document.querySelector('[data-vhost-card="api-one"]')).borderRadius,
|
||||
sameNode: node === document.getElementById('vhost-backend-popup'),
|
||||
};
|
||||
}
|
||||
if (saved === null) document.documentElement.removeAttribute('data-corner-style');
|
||||
else document.documentElement.dataset.cornerStyle = saved;
|
||||
return out;
|
||||
}''')
|
||||
expected_radii = {'default': ('0px', '0px'), 'square': ('0px', '0px'),
|
||||
'subtle': ('5px', '4px'), 'rounded': ('12px', '8px'),
|
||||
'extra': ('18px', '12px')}
|
||||
for style, (surface, control) in expected_radii.items():
|
||||
actual = corners[style]
|
||||
assert actual == {'popup': surface, 'row': surface, 'close': control,
|
||||
'reference': surface, 'sameNode': True}, (width, style, actual)
|
||||
values = popup.evaluate('''node => ({
|
||||
cards: node.querySelectorAll('.vhost-backend-popup-row').length,
|
||||
text: node.textContent, html: node.innerHTML,
|
||||
box: (() => { const r = node.getBoundingClientRect();
|
||||
return {left: r.left, right: r.right, top:r.top, bottom:r.bottom}; })(),
|
||||
})''')
|
||||
assert values['cards'] == 2, (width, values)
|
||||
assert '10.12.0.5:8080' in values['text'] and 'HTTP 200' in values['text'], (width, values)
|
||||
assert 'backend.example.test:8443' in values['text'], width
|
||||
assert 'secret' not in values['text'], width
|
||||
assert '<origin-dns>' in values['text'] and '<origin-dns>' in values['html'], width
|
||||
assert 'Brak odpowiedzi HTTP' in values['text'], width
|
||||
assert 'HTTPS · AUTO' in values['text'], width
|
||||
assert values['box']['left'] >= -1 and values['box']['right'] <= width + 1, (width, values['box'])
|
||||
assert values['box']['top'] >= -1 and values['box']['bottom'] <= 821, (width, values['box'])
|
||||
assert page.evaluate('document.documentElement.scrollWidth - innerWidth') <= 2, width
|
||||
page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', snapshot(503))
|
||||
assert 'HTTP 503' in popup.inner_text(), width
|
||||
assert popup.locator('.has-error').count() == 1, width
|
||||
health.click()
|
||||
assert popup.count() == 1 and health.get_attribute('aria-expanded') == 'true', width
|
||||
assert backend.get_attribute('aria-expanded') == 'false', width
|
||||
page.keyboard.press('Escape')
|
||||
assert popup.count() == 0 and health.get_attribute('aria-expanded') == 'false', width
|
||||
assert health.evaluate('(node) => document.activeElement === node'), width
|
||||
backend.click()
|
||||
page.locator('#vhost-search').click(force=True)
|
||||
assert popup.count() == 0, width
|
||||
# A static-only vhost presents a translated empty state, not stale pool values.
|
||||
page.locator('[data-vhost-card="static-only"] .vhost-detail.vhost-backend-trigger').click()
|
||||
assert 'Brak skonfigurowanych backendów.' in popup.inner_text(), width
|
||||
popup.locator('.vhost-backend-popup-close').click()
|
||||
assert popup.count() == 0, width
|
||||
backend.click()
|
||||
assert popup.count() == 1, width
|
||||
|
||||
# The existing backend popover is also used by the H2 policy badge.
|
||||
protocol = vhost.locator('[data-upstream-protocol-backend="origin-http"]')
|
||||
assert protocol.count() == 1 and protocol.evaluate('n => n.tagName') == 'BUTTON', width
|
||||
assert protocol.get_attribute('title') is None, width
|
||||
protocol.click()
|
||||
assert backend.get_attribute('aria-expanded') == 'false', width
|
||||
assert protocol.get_attribute('aria-expanded') == 'true', width
|
||||
assert 'Brak odpowiedzi backendu' in popup.inner_text(), width
|
||||
page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', protocol_snapshot(8))
|
||||
assert 'origin-http · H2 compatible → H2' in protocol.inner_text(), width
|
||||
assert 'Ostatnia wersja HTTP' in popup.inner_text(), width
|
||||
assert popup.locator('.vhost-backend-popup-field > span:last-child').all_inner_texts() == ['3', '8'], width
|
||||
page.evaluate('''() => {
|
||||
window.__popupReference = document.getElementById('vhost-backend-popup');
|
||||
window.__headingReference = window.__popupReference.querySelector('h4');
|
||||
window.__popupRemoved = 0;
|
||||
window.__popupObserver = new MutationObserver(records => {
|
||||
for (const record of records) for (const item of record.removedNodes) {
|
||||
if (item === window.__popupReference) window.__popupRemoved++;
|
||||
}
|
||||
});
|
||||
window.__popupObserver.observe(document.body, {childList: true});
|
||||
}''')
|
||||
for count in range(9, 19):
|
||||
page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', protocol_snapshot(count))
|
||||
unchanged = page.evaluate('''() => ({
|
||||
samePopup: document.getElementById('vhost-backend-popup') === window.__popupReference,
|
||||
sameHeading: window.__popupReference.querySelector('h4') === window.__headingReference,
|
||||
removed: window.__popupRemoved,
|
||||
expanded: document.querySelector('[data-upstream-protocol-backend="origin-http"]').getAttribute('aria-expanded'),
|
||||
})''')
|
||||
assert unchanged == {'samePopup': True, 'sameHeading': True, 'removed': 0, 'expanded': 'true'}, (width, unchanged)
|
||||
assert popup.locator('.vhost-backend-popup-field > span:last-child').all_inner_texts() == ['3', '18'], width
|
||||
page.evaluate('window.__popupObserver.disconnect()')
|
||||
assert protocol.get_attribute('title') is None, width
|
||||
# A stale observation should change the displayed information in
|
||||
# place, not dismiss/recreate the click popover.
|
||||
stale = protocol_snapshot(18)
|
||||
stale['backend_pools'][0]['members'][0]['upstream_protocol_last_seen_ms'] -= 600000
|
||||
page.evaluate('''s => window.rustProxyVhosts.renderStatus(s)''', stale)
|
||||
assert 'H2 compatible → ?' in protocol.inner_text(), width
|
||||
assert popup.locator('.vhost-backend-popup-list').is_hidden(), width
|
||||
assert page.evaluate('document.getElementById("vhost-backend-popup") === window.__popupReference'), width
|
||||
page.keyboard.press('Escape')
|
||||
assert popup.count() == 0 and protocol.get_attribute('aria-expanded') == 'false', width
|
||||
assert protocol.evaluate('node => document.activeElement === node'), width
|
||||
|
||||
# Cache Domains and Stored objects are the only directly adjacent
|
||||
# table cards and need a visible, small gap at any viewport width.
|
||||
cache_gap = page.evaluate('''() => {
|
||||
const panel = document.getElementById('panel-cache');
|
||||
panel.hidden = false;
|
||||
const [domains, stored] = panel.querySelectorAll(':scope > article.card');
|
||||
const gap = stored.getBoundingClientRect().top - domains.getBoundingClientRect().bottom;
|
||||
panel.hidden = true;
|
||||
return gap;
|
||||
}''')
|
||||
assert 9.5 <= cache_gap <= 10.5, (width, cache_gap)
|
||||
page.evaluate('window.rustProxyVhosts.reset()')
|
||||
assert popup.count() == 0, width
|
||||
assert not errors, (width, errors)
|
||||
print(f'PASS Vhosts shared backend/H2 popover and Cache gap at {width}px')
|
||||
page.close()
|
||||
browser.close()
|
||||
print('PASS Vhost quick-view with simulated runtime status (not live Rust)')
|
||||
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
import re, json
|
||||
from playwright.sync_api import sync_playwright
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
@@ -26,7 +27,7 @@ bundle=concatenate('admin_app_javascript')
|
||||
assert 'window.rustProxyOverviewSamples =' in bundle
|
||||
assert bundle.index('window.rustProxyOverviewSamples =') < bundle.index('window.rustProxyOverviewSamples.onChange(')
|
||||
css=(root/'src/admin/web/style.css').read_text()
|
||||
html=(root/'src/admin/web/index.html').read_text()
|
||||
html=render_admin_index(root)
|
||||
with sync_playwright() as p:
|
||||
browser=p.chromium.launch(headless=True, executable_path='/usr/bin/chromium',args=['--no-sandbox','--allow-file-access-from-files'])
|
||||
for width in [320,375,800,1440]:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static contracts for 0.33.0 WWW observability (not Rust compilation)."""
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
import json
|
||||
import re
|
||||
import tomllib
|
||||
@@ -18,7 +19,7 @@ widgets = [w for w in registry if w['id'].startswith('www_')]
|
||||
assert len(widgets) == 12
|
||||
assert all(w['default_hidden'] is True for w in widgets)
|
||||
assert len({w['id'] for w in registry}) == len(registry)
|
||||
html = read('src/admin/web/index.html')
|
||||
html = render_admin_index(ROOT)
|
||||
english = json.loads(read('langs/en_US.json'))['translations']
|
||||
polish = json.loads(read('langs/pl_pl.json'))['translations']
|
||||
for widget in widgets:
|
||||
|
||||
@@ -7,6 +7,7 @@ Run: python3 scripts/tests/web_server_0330_ui_test.py
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
from panel_html import render_admin_index
|
||||
import re
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
@@ -58,7 +59,7 @@ with sync_playwright() as p:
|
||||
page = browser.new_page(viewport={'width': width, 'height': 900})
|
||||
errors = []
|
||||
page.on('pageerror', lambda err: errors.append(str(err)))
|
||||
page.set_content((ROOT / 'src/admin/web/index.html').read_text())
|
||||
page.set_content(render_admin_index(ROOT))
|
||||
page.add_style_tag(content=(ROOT / 'src/admin/web/style.css').read_text())
|
||||
page.add_script_tag(content=BUNDLE)
|
||||
page.evaluate("""() => {
|
||||
|
||||
@@ -5,12 +5,13 @@ Chromium exercises the real assembled admin JS/CSS and responsive layouts.
|
||||
It does not establish Rust, SSE or PHP-FPM runtime acceptance.
|
||||
"""
|
||||
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()
|
||||
HTML = render_admin_index(ROOT)
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text()
|
||||
ROUTER = (ROOT / 'src/admin/router.rs').read_text()
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/// Account-scoped snapshots of Overview order, visibility, widths and display.
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DashboardPresetLayout {
|
||||
dashboard_widgets: Vec<String>,
|
||||
dashboard_hidden_widgets: Vec<String>,
|
||||
dashboard_widget_sizes: Option<BTreeMap<String, u8>>,
|
||||
dashboard_density: String,
|
||||
overview_samples: u16,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct NamedDashboardPreset {
|
||||
name: String,
|
||||
#[serde(flatten)]
|
||||
layout: DashboardPresetLayout,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct DashboardPresetRename {
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn valid_dashboard_preset_name(value: &str) -> bool {
|
||||
!value.is_empty()
|
||||
&& value.len() <= 64
|
||||
&& value.trim() == value
|
||||
&& !value.chars().any(|ch| ch.is_control() || ch == '/')
|
||||
}
|
||||
|
||||
fn validate_dashboard_preset_layout(layout: &DashboardPresetLayout) -> Result<(), ApiError> {
|
||||
if !matches!(layout.overview_samples, 60 | 120 | 300 | 600) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid Overview history samples".into(),
|
||||
));
|
||||
}
|
||||
let mut pref = AdminPreferences::default();
|
||||
pref.dashboard_widgets.clone_from(&layout.dashboard_widgets);
|
||||
pref.dashboard_hidden_widgets
|
||||
.clone_from(&layout.dashboard_hidden_widgets);
|
||||
pref.dashboard_widget_sizes
|
||||
.clone_from(&layout.dashboard_widget_sizes);
|
||||
pref.dashboard_density.clone_from(&layout.dashboard_density);
|
||||
validate_admin_preferences(&pref)
|
||||
}
|
||||
|
||||
async fn dashboard_presets(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
) -> Result<Json<Vec<NamedDashboardPreset>>, ApiError> {
|
||||
let entries = state
|
||||
.store
|
||||
.list_admin_dashboard_presets(&session.username)
|
||||
.await
|
||||
.map_err(auth::internal)?;
|
||||
let mut presets = Vec::with_capacity(entries.len());
|
||||
for (name, document) in entries {
|
||||
let Ok(mut layout) = serde_json::from_str::<DashboardPresetLayout>(&document) else {
|
||||
tracing::warn!("skipped invalid saved Overview preset");
|
||||
continue;
|
||||
};
|
||||
// Compatibility with future/retired widgets: use the same registry
|
||||
// reconciliation logic as ordinary saved administrator preferences.
|
||||
let mut prefs = AdminPreferences::default();
|
||||
prefs.dashboard_widgets = layout.dashboard_widgets;
|
||||
prefs.dashboard_hidden_widgets = layout.dashboard_hidden_widgets;
|
||||
prefs.dashboard_widget_sizes = layout.dashboard_widget_sizes;
|
||||
reconcile_dashboard_widgets(&mut prefs);
|
||||
layout.dashboard_widgets = prefs.dashboard_widgets;
|
||||
layout.dashboard_hidden_widgets = prefs.dashboard_hidden_widgets;
|
||||
layout.dashboard_widget_sizes = prefs.dashboard_widget_sizes;
|
||||
if validate_dashboard_preset_layout(&layout).is_ok() {
|
||||
presets.push(NamedDashboardPreset { name, layout });
|
||||
}
|
||||
}
|
||||
Ok(Json(presets))
|
||||
}
|
||||
|
||||
async fn save_dashboard_preset(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Path(name): Path<String>,
|
||||
Json(layout): Json<DashboardPresetLayout>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
if !valid_dashboard_preset_name(&name) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid dashboard preset name".into(),
|
||||
));
|
||||
}
|
||||
validate_dashboard_preset_layout(&layout)?;
|
||||
let document = serde_json::to_string(&layout).map_err(auth::internal)?;
|
||||
if !state
|
||||
.store
|
||||
.save_admin_dashboard_preset(&session.username, &name, &document)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
{
|
||||
return Err(ApiError(
|
||||
StatusCode::CONFLICT,
|
||||
"dashboard preset limit (16) reached".into(),
|
||||
));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn delete_dashboard_preset(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
if !state
|
||||
.store
|
||||
.delete_admin_dashboard_preset(&session.username, &name)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
{
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"dashboard preset not found".into(),
|
||||
));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn rename_dashboard_preset(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Path(name): Path<String>,
|
||||
Json(rename): Json<DashboardPresetRename>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
if !valid_dashboard_preset_name(&rename.name) || !valid_dashboard_preset_name(&name) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid dashboard preset name".into(),
|
||||
));
|
||||
}
|
||||
if !state
|
||||
.store
|
||||
.rename_admin_dashboard_preset(&session.username, &name, &rename.name)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
{
|
||||
return Err(ApiError(
|
||||
StatusCode::CONFLICT,
|
||||
"dashboard preset missing or name in use".into(),
|
||||
));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dashboard_preset_validation_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn names_are_bounded_and_paths_are_not_accepted() {
|
||||
assert!(valid_dashboard_preset_name("Work layout"));
|
||||
assert!(valid_dashboard_preset_name("Układ na telefon"));
|
||||
for invalid in ["", " name", "name ", "bad/name", "bad\nname"] {
|
||||
assert!(!valid_dashboard_preset_name(invalid));
|
||||
}
|
||||
assert!(!valid_dashboard_preset_name(&"X".repeat(65)));
|
||||
}
|
||||
#[test]
|
||||
fn layouts_reject_invalid_history_and_widgets() {
|
||||
let mut layout = DashboardPresetLayout {
|
||||
dashboard_widgets: default_admin_dashboard_widgets_for_tests(),
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_density: default_admin_dashboard_density(),
|
||||
overview_samples: 600,
|
||||
};
|
||||
assert!(validate_dashboard_preset_layout(&layout).is_ok());
|
||||
layout.overview_samples = 601;
|
||||
assert!(validate_dashboard_preset_layout(&layout).is_err());
|
||||
layout.overview_samples = 600;
|
||||
layout.dashboard_widgets.push("undefined-widget".into());
|
||||
assert!(validate_dashboard_preset_layout(&layout).is_err());
|
||||
}
|
||||
fn default_admin_dashboard_widgets_for_tests() -> Vec<String> {
|
||||
AdminPreferences::default().dashboard_widgets
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ include!("router.rs");
|
||||
include!("access.rs");
|
||||
include!("session.rs");
|
||||
include!("preferences.rs");
|
||||
include!("dashboard_presets.rs");
|
||||
include!("status.rs");
|
||||
include!("logging_api.rs");
|
||||
include!("file_logs.rs");
|
||||
|
||||
+389
-9
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.33.4",
|
||||
"version": "0.33.7",
|
||||
"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."
|
||||
},
|
||||
"servers": [
|
||||
@@ -1241,15 +1241,37 @@
|
||||
"type": "object",
|
||||
"description": "Cached OS/process metrics for fast SSE/live charts only; full System details remain on /api/system-info",
|
||||
"properties": {
|
||||
"sampled_at_ms": { "type": "integer", "format": "int64" },
|
||||
"cpu_percent": { "type": "number", "nullable": true },
|
||||
"process_cpu_percent": { "type": "number", "nullable": true },
|
||||
"process_rss_bytes": { "type": "integer", "format": "int64", "nullable": true },
|
||||
"network_rx_bytes_per_second": { "type": "number", "nullable": true },
|
||||
"network_tx_bytes_per_second": { "type": "number", "nullable": true },
|
||||
"sampled_at_ms": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"cpu_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"process_cpu_percent": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"process_rss_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true
|
||||
},
|
||||
"network_rx_bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"network_tx_bytes_per_second": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"network_interfaces": {
|
||||
"type": "array",
|
||||
"items": { "type": "object", "additionalProperties": true }
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -3570,7 +3592,13 @@
|
||||
"maxProperties": 78,
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"enum": [2, 3, 4, 6, 12]
|
||||
"enum": [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
6,
|
||||
12
|
||||
]
|
||||
},
|
||||
"description": "Nullable account-scoped per-card widths (12-column grid). Null preserves the legacy grouped visual arrangement; an object, including an empty one, enables freely ordered cards. Key must be a registered Overview widget ID. Panels require width >= 4, charts >= 3 and backend table >= 6."
|
||||
},
|
||||
@@ -6631,6 +6659,82 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DashboardPresetLayout": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Validated Overview snapshot. Factory layout is separate from stored user presets.",
|
||||
"properties": {
|
||||
"dashboard_widgets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"description": "Ordered registry widget identifiers"
|
||||
},
|
||||
"dashboard_hidden_widgets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Invisible registry widget IDs"
|
||||
},
|
||||
"dashboard_widget_sizes": {
|
||||
"nullable": true,
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"minimum": 2,
|
||||
"maximum": 12
|
||||
},
|
||||
"description": "Width overrides in a 12-column grid; null selects legacy groups"
|
||||
},
|
||||
"dashboard_density": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"normal",
|
||||
"compact",
|
||||
"ultra"
|
||||
]
|
||||
},
|
||||
"overview_samples": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
60,
|
||||
120,
|
||||
300,
|
||||
600
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dashboard_widgets",
|
||||
"dashboard_hidden_widgets",
|
||||
"dashboard_widget_sizes",
|
||||
"dashboard_density",
|
||||
"overview_samples"
|
||||
]
|
||||
},
|
||||
"NamedDashboardPreset": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DashboardPresetLayout"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -11931,6 +12035,282 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/dashboard-presets": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "List administrator dashboard presets",
|
||||
"operationId": "get_dashboard_presets",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Named layouts owned by the authenticated administrator",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NamedDashboardPreset"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/dashboard-presets/{name}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "URL-encoded user-defined preset name (1-64 UTF-8 bytes)",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
}
|
||||
}
|
||||
],
|
||||
"put": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "Save or overwrite an Overview dashboard preset",
|
||||
"operationId": "put_dashboard_preset",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DashboardPresetLayout"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Preset saved"
|
||||
},
|
||||
"409": {
|
||||
"description": "Account already has 16 named presets",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid preset name or layout",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "Rename an Overview dashboard preset",
|
||||
"operationId": "patch_dashboard_preset",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Preset renamed"
|
||||
},
|
||||
"409": {
|
||||
"description": "Source name not found or target name already exists",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid preset name",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Administration"
|
||||
],
|
||||
"summary": "Delete an Overview dashboard preset",
|
||||
"operationId": "delete_dashboard_preset",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Preset deleted"
|
||||
},
|
||||
"404": {
|
||||
"description": "Preset not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Administrator session required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid Host, Origin, request marker or CSRF token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Storage unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-3
@@ -1,6 +1,14 @@
|
||||
//! Deliberately small anonymous surface. Operational templates/assets are private.
|
||||
use crate::{auth::Session, runtime::{ApiError, App}, settings::PanelCompression};
|
||||
use axum::{extract::{rejection::JsonRejection, State}, response::{Html, IntoResponse, Response}, Extension};
|
||||
use crate::{
|
||||
auth::Session,
|
||||
runtime::{ApiError, App},
|
||||
settings::PanelCompression,
|
||||
};
|
||||
use axum::{
|
||||
extract::{rejection::JsonRejection, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
Extension,
|
||||
};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
@@ -10,7 +18,19 @@ static ADMIN_INDEX: OnceLock<PanelContent> = OnceLock::new();
|
||||
|
||||
fn panel_response(headers: &HeaderMap, compression: &PanelCompression) -> Response {
|
||||
let content = ADMIN_INDEX.get_or_init(|| {
|
||||
// Compile-time embedded HTML fragments are assembled once at startup.
|
||||
// No additional browser requests, changed DOM nodes or script ordering.
|
||||
let (metrics, web_metrics) = include_str!("web/partials/overview-metrics.html")
|
||||
.split_once("<!-- RP_METRICS_SPLIT -->")
|
||||
.expect("invalid Overview metrics partial");
|
||||
let (charts, web_charts) = include_str!("web/partials/overview-charts.html")
|
||||
.split_once("<!-- RP_CHARTS_SPLIT -->")
|
||||
.expect("invalid Overview charts partial");
|
||||
let html = include_str!("web/index.html")
|
||||
.replace("<!-- RP_METRICS_MAIN -->", metrics)
|
||||
.replace("<!-- RP_CHARTS_MAIN -->", charts)
|
||||
.replace("<!-- RP_METRICS_WEB -->", web_metrics)
|
||||
.replace("<!-- RP_CHARTS_WEB -->", web_charts)
|
||||
.replace("__RUSTPROXY_ASSET_ID__", super::asset_build_id())
|
||||
.replace("__RUSTPROXY_VERSION__", env!("CARGO_PKG_VERSION"));
|
||||
// Exactly one static panel document shared between all administrator accounts.
|
||||
@@ -30,7 +50,10 @@ pub(super) async fn index(
|
||||
return Html(html).into_response();
|
||||
}
|
||||
let snapshot = state.config.load();
|
||||
panel_response(&headers, &snapshot.envelope.config.administration.panel_compression)
|
||||
panel_response(
|
||||
&headers,
|
||||
&snapshot.envelope.config.administration.panel_compression,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn invalid_json(error: JsonRejection) -> ApiError {
|
||||
|
||||
+9
-1
@@ -69,7 +69,7 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/dashboard-widgets.json"),
|
||||
");",
|
||||
include_str!("web/app/preferences.js"),
|
||||
include_str!("web/app/dashboard-layout-editor.js"),
|
||||
include_str!("web/app/dashboard-presets.js"),
|
||||
include_str!("web/app/dashboard-inline-layout.js"),
|
||||
include_str!("web/app/stream-dashboard.js"),
|
||||
include_str!("web/app/backend-dashboard.js"),
|
||||
@@ -249,6 +249,14 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
.route("/api/file-logs", get(file_logs))
|
||||
.route("/api/file-logs/query", get(file_logs_query))
|
||||
.route("/api/preferences", get(preferences).put(update_preferences))
|
||||
.route("/api/dashboard-presets", get(dashboard_presets))
|
||||
.route(
|
||||
"/api/dashboard-presets/{name}",
|
||||
put(save_dashboard_preset)
|
||||
.patch(rename_dashboard_preset)
|
||||
.delete(delete_dashboard_preset)
|
||||
.layer(DefaultBodyLimit::max(10240)),
|
||||
)
|
||||
.route("/api/audit", get(audit))
|
||||
.route("/api/events", get(events))
|
||||
.route("/metrics", get(metrics))
|
||||
|
||||
@@ -1639,6 +1639,87 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn named_dashboard_presets_are_private_validated_and_persistent() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
let preset = DashboardPresetLayout {
|
||||
dashboard_widgets: AdminPreferences::default().dashboard_widgets,
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_density: "compact".to_owned(),
|
||||
overview_samples: 600,
|
||||
};
|
||||
let body = serde_json::to_string(&preset).unwrap();
|
||||
let anon = router.clone().oneshot(
|
||||
request("/api/dashboard-presets").body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(anon.status(), StatusCode::UNAUTHORIZED);
|
||||
let forbidden = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Daily").method("PUT")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.clone())).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(forbidden.status(), StatusCode::FORBIDDEN);
|
||||
let saved = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Daily").method("PUT")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.clone())).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(saved.status(), StatusCode::NO_CONTENT);
|
||||
let listing = router.clone().oneshot(
|
||||
request("/api/dashboard-presets")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(listing.status(), StatusCode::OK);
|
||||
let entries = json_body(listing).await;
|
||||
assert_eq!(entries[0]["name"], "Daily");
|
||||
assert_eq!(entries[0]["overview_samples"], 600);
|
||||
assert_eq!(entries[0]["dashboard_widget_sizes"], Value::Null);
|
||||
assert_eq!(store.list_admin_dashboard_presets("owner").await.unwrap().len(), 1);
|
||||
assert!(store.list_admin_dashboard_presets("someone-else").await.unwrap().is_empty());
|
||||
let rejected = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Invalid").method("PUT")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.replace("\"overview_samples\":600", "\"overview_samples\":999"))).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(rejected.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
let renamed = router.clone().oneshot(
|
||||
request("/api/dashboard-presets/Daily").method("PATCH")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(r#"{"name":"Nightly"}"#)).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(renamed.status(), StatusCode::NO_CONTENT);
|
||||
assert_eq!(store.list_admin_dashboard_presets("owner").await.unwrap()[0].0, "Nightly");
|
||||
let deleted = router.oneshot(
|
||||
request("/api/dashboard-presets/Nightly").method("DELETE")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(deleted.status(), StatusCode::NO_CONTENT);
|
||||
assert!(store.list_admin_dashboard_presets("owner").await.unwrap().is_empty());
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_preferences_are_account_scoped_and_csrf_guarded() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
@@ -3010,6 +3091,7 @@ mod tests {
|
||||
"/api/system-info",
|
||||
"/api/config",
|
||||
"/api/preferences",
|
||||
"/api/dashboard-presets",
|
||||
"/api/cache",
|
||||
"/api/cache/bans",
|
||||
"/api/audit",
|
||||
@@ -3044,6 +3126,9 @@ mod tests {
|
||||
("DELETE", "/api/cache/bans/1"),
|
||||
("PUT", "/api/config/cache"),
|
||||
("PUT", "/api/preferences"),
|
||||
("PUT", "/api/dashboard-presets/example"),
|
||||
("DELETE", "/api/dashboard-presets/example"),
|
||||
("PATCH", "/api/dashboard-presets/example"),
|
||||
("POST", "/api/auth/logout-all"),
|
||||
] {
|
||||
let no_session = request(path).method(method).body(Body::empty()).unwrap();
|
||||
|
||||
@@ -162,7 +162,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
draft = original = null;
|
||||
}
|
||||
async function begin() {
|
||||
if (draft || opening || saving || $("overview-layout-dialog")?.open) return;
|
||||
if (draft || opening || saving || $("overview-presets-dialog")?.open) return;
|
||||
opening = true;
|
||||
unlock.disabled = true;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
window.rustProxyDashboardLayout = (() => {
|
||||
const dialog = $("overview-presets-dialog");
|
||||
const list = $("overview-presets-list");
|
||||
const input = $("overview-presets-name");
|
||||
const message = $("overview-presets-message");
|
||||
const save = $("overview-presets-save");
|
||||
const factory = $("overview-presets-factory");
|
||||
const close = $("overview-presets-close");
|
||||
let presets = [];
|
||||
let busy = false;
|
||||
|
||||
const presetPath = name => `/api/dashboard-presets/${encodeURIComponent(name)}`;
|
||||
const nameValid = name => name.length > 0 && new TextEncoder().encode(name).length <= 64
|
||||
&& !/[\x00-\x1f\x7f/]/.test(name);
|
||||
const snapshot = () => {
|
||||
const current = window.rustProxyPreferences.current() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
return {
|
||||
dashboard_widgets: [...normalizeDashboardWidgets(current.dashboard_widgets)],
|
||||
dashboard_hidden_widgets: [...normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets)],
|
||||
dashboard_widget_sizes: normalizeDashboardWidgetSizes(current.dashboard_widget_sizes),
|
||||
dashboard_density: normalizeAdminDensity(current.dashboard_density, "compact"),
|
||||
overview_samples: window.rustProxyOverviewSamples.getLimit(),
|
||||
};
|
||||
};
|
||||
const translateName = (key, name) => t(key).replace("{name}", name);
|
||||
function notify(key, isError = false) {
|
||||
message.textContent = isError ? key : t(key);
|
||||
message.classList.toggle("error", isError);
|
||||
}
|
||||
function lock(value) {
|
||||
busy = value;
|
||||
for (const control of [save, factory, close, input, ...list.querySelectorAll("button")])
|
||||
if (control) control.disabled = value;
|
||||
}
|
||||
async function run(fn) {
|
||||
if (busy) return;
|
||||
lock(true);
|
||||
try { await fn(); }
|
||||
catch (error) {
|
||||
const message = error.status === 409
|
||||
? (String(error.code || "").includes("limit (16)")
|
||||
? t("dashboardPresetsLimit") : t("dashboardPresetsConflict"))
|
||||
: (error.message || String(error));
|
||||
notify(message, true);
|
||||
}
|
||||
finally { lock(false); }
|
||||
}
|
||||
async function reload() {
|
||||
presets = await api("/api/dashboard-presets");
|
||||
render();
|
||||
}
|
||||
function entryButton(key, className, fn, presetName) {
|
||||
const button = el("button", t(key), className);
|
||||
button.type = "button";
|
||||
button.setAttribute("aria-label", `${t(key)}: ${presetName}`);
|
||||
button.dataset.presetAction = key;
|
||||
button.addEventListener("click", () => void run(fn));
|
||||
return button;
|
||||
}
|
||||
function render() {
|
||||
const elements = presets.map(preset => {
|
||||
const row = el("div", undefined, "overview-presets-row");
|
||||
row.setAttribute("role", "listitem");
|
||||
const actions = el("div", undefined, "overview-presets-actions");
|
||||
actions.append(
|
||||
entryButton("dashboardPresetsLoad", "primary", async () => {
|
||||
await apply(preset);
|
||||
dialog.close();
|
||||
toast(t("dashboardPresetsLoaded"));
|
||||
}, preset.name),
|
||||
entryButton("dashboardPresetsOverwrite", "outline", async () => {
|
||||
if (!(await appConfirm(translateName("dashboardPresetsOverwriteConfirm", preset.name)))) return;
|
||||
await api(presetPath(preset.name), { method: "PUT", body: JSON.stringify(snapshot()) });
|
||||
await reload(); notify("dashboardPresetsSaved");
|
||||
}, preset.name),
|
||||
entryButton("dashboardPresetsRename", "quiet", async () => {
|
||||
const name = input.value.trim();
|
||||
if (!nameValid(name)) { notify("dashboardPresetsNameRequired"); return; }
|
||||
if (name === preset.name) return;
|
||||
await api(presetPath(preset.name), { method: "PATCH", body: JSON.stringify({ name }) });
|
||||
input.value = "";
|
||||
await reload(); notify("dashboardPresetsRenamed");
|
||||
}, preset.name),
|
||||
entryButton("dashboardPresetsDelete", "quiet", async () => {
|
||||
if (!(await appConfirm(translateName("dashboardPresetsDeleteConfirm", preset.name), { danger: true }))) return;
|
||||
await api(presetPath(preset.name), { method: "DELETE" });
|
||||
await reload(); notify("dashboardPresetsDeleted");
|
||||
}, preset.name),
|
||||
);
|
||||
row.append(el("strong", preset.name), actions);
|
||||
return row;
|
||||
});
|
||||
list.replaceChildren(...(elements.length ? elements : [el("p", t("dashboardPresetsEmpty"), "fine")]));
|
||||
}
|
||||
async function apply(layout) {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...layout.dashboard_widgets],
|
||||
dashboard_hidden_widgets: [...layout.dashboard_hidden_widgets],
|
||||
dashboard_widget_sizes: layout.dashboard_widget_sizes === null ? null : { ...layout.dashboard_widget_sizes },
|
||||
dashboard_density: layout.dashboard_density,
|
||||
});
|
||||
window.rustProxyOverviewSamples.setLimit(layout.overview_samples, true);
|
||||
}
|
||||
async function open() {
|
||||
if (dialog?.open || busy || window.rustProxyInlineLayout?.isEditing?.()) return;
|
||||
await run(async () => {
|
||||
await window.rustProxyPreferences.load();
|
||||
await reload();
|
||||
input.value = "";
|
||||
notify("");
|
||||
dialog.showModal();
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
save?.addEventListener("click", () => void run(async () => {
|
||||
const name = input.value.trim();
|
||||
if (!nameValid(name)) { notify("dashboardPresetsNameRequired"); return; }
|
||||
if (presets.length >= 16 && !presets.some(preset => preset.name === name)) {
|
||||
notify("dashboardPresetsLimit"); return;
|
||||
}
|
||||
if (presets.some(preset => preset.name === name)
|
||||
&& !(await appConfirm(translateName("dashboardPresetsOverwriteConfirm", name)))) return;
|
||||
await api(presetPath(name), { method: "PUT", body: JSON.stringify(snapshot()) });
|
||||
input.value = "";
|
||||
await reload(); notify("dashboardPresetsSaved");
|
||||
}));
|
||||
factory?.addEventListener("click", () => void run(async () => {
|
||||
if (!(await appConfirm(t("dashboardPresetsResetConfirm")))) return;
|
||||
await apply({
|
||||
dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS],
|
||||
dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS],
|
||||
dashboard_widget_sizes: null,
|
||||
dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density,
|
||||
overview_samples: ADMIN_DASHBOARD_DEFAULTS.overview_samples,
|
||||
});
|
||||
dialog.close();
|
||||
toast(t("dashboardPresetsRestored"));
|
||||
}));
|
||||
close?.addEventListener("click", () => { if (!busy) dialog.close(); });
|
||||
dialog?.addEventListener("cancel", event => { if (busy) event.preventDefault(); });
|
||||
for (const id of ["overview-layout-open", "overview-layout-open-settings"])
|
||||
$(id)?.addEventListener("click", () => { void open(); });
|
||||
return { open, refresh: () => { if (dialog?.open) render(); } };
|
||||
})();
|
||||
+25
-1055
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,373 @@
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_protocol_rates" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideProtocolRates">HTTP/1.1 · HTTP/2 · HTTP/3 traffic</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_protocol_rates-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="HTTP/1.1 · HTTP/2 · HTTP/3 traffic">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_protocol_rates-h1-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_protocol_rates-h2-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="overview-wide-wide_protocol_rates-h3-line" d="">
|
||||
</path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span>H1</span><b
|
||||
id="overview-wide-wide_protocol_rates-h1-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span>H2</span><b
|
||||
id="overview-wide-wide_protocol_rates-h2-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span>H3</span><b
|
||||
id="overview-wide-wide_protocol_rates-h3-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_cpu" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCpu">System and process CPU</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_cpu-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="System and process CPU">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_cpu-system-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_cpu-process-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideSystem">System</span><b
|
||||
id="overview-wide-wide_cpu-system-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideProcess">Process</span><b
|
||||
id="overview-wide-wide_cpu-process-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_memory" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideMemory">Process RAM and cache memory</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_memory-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Process RAM and cache memory">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_memory-process-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_memory-cache-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideProcess">Process</span><b
|
||||
id="overview-wide-wide_memory-process-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideCache">Cache</span><b
|
||||
id="overview-wide-wide_memory-cache-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_cache_activity" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCacheActivity">Cache hits / misses per second</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_cache_activity-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Cache hits / misses per second">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_cache_activity-hits-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_cache_activity-misses-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideHits">HIT</span><b
|
||||
id="overview-wide-wide_cache_activity-hits-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideMisses">MISS</span><b
|
||||
id="overview-wide-wide_cache_activity-misses-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_connections" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideConnections">Connections and WebSocket tunnels</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_connections-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Connections and WebSocket tunnels">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_connections-open-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_connections-websockets-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideOpen">Connections</span><b
|
||||
id="overview-wide-wide_connections-open-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideWebsockets">WebSockets</span><b
|
||||
id="overview-wide-wide_connections-websockets-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_backend_health" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideBackendHealth">Backend availability over time</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_backend_health-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Backend availability over time">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_backend_health-up-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_backend_health-down-line" d="">
|
||||
</path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideHealthy">Healthy</span><b
|
||||
id="overview-wide-wide_backend_health-up-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideUnhealthy">Unhealthy</span><b
|
||||
id="overview-wide-wide_backend_health-down-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_latency" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideLatency">Response and backend probe latency</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_latency-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Response and backend probe latency">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_latency-response-line" d="">
|
||||
</path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_latency-probe-line" d="">
|
||||
</path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideResponse">HTTP</span><b
|
||||
id="overview-wide-wide_latency-response-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewWideProbe">Probes</span><b
|
||||
id="overview-wide-wide_latency-probe-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_transform_activity">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideTransformActivity">Transformation activity</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_transform_activity-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
data-i18n-aria="overviewWideTransformActivity" aria-label="Transformation activity">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
id="overview-wide-wide_transform_activity-applied-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1"
|
||||
id="overview-wide-wide_transform_activity-cache_hits-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2"
|
||||
id="overview-wide-wide_transform_activity-skipped-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3" id="overview-wide-wide_transform_activity-errors-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewTransformApplied">Applied</span><b
|
||||
id="overview-wide-wide_transform_activity-applied-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="overviewTransformCacheHits">Cache HIT</span><b
|
||||
id="overview-wide-wide_transform_activity-cache_hits-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span data-i18n="overviewTransformSkipped">Skipped</span><b
|
||||
id="overview-wide-wide_transform_activity-skipped-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span data-i18n="overviewTransformErrors">Errors</span><b
|
||||
id="overview-wide-wide_transform_activity-errors-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_compression_activity">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCompressionActivity">Compression activity</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_compression_activity-summary">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
data-i18n-aria="overviewWideCompressionActivity" aria-label="Compression activity">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="overview-wide-wide_compression_activity-gzip-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="overview-wide-wide_compression_activity-br-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="overview-wide-wide_compression_activity-zstd-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3"
|
||||
id="overview-wide-wide_compression_activity-deflate-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span>gzip</span><b
|
||||
id="overview-wide-wide_compression_activity-gzip-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span>Brotli</span><b
|
||||
id="overview-wide-wide_compression_activity-br-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span>Zstd</span><b
|
||||
id="overview-wide-wide_compression_activity-zstd-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span>deflate</span><b
|
||||
id="overview-wide-wide_compression_activity-deflate-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<!-- RP_CHARTS_SPLIT -->
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_requests" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricRequests">webMetricRequests</h2><span class="overview-wide-summary"
|
||||
id="www_requests-value">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server request rate" data-i18n-aria="webMetricRequests">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_requests-all-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_requests-php-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesAll">all</span><b id="www_requests-all-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_requests-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_latency" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricLatency">webMetricLatency</h2><span class="overview-wide-summary"
|
||||
id="www_latency-value">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server header latency" data-i18n-aria="webMetricLatency">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_latency-web-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_latency-php-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesWeb">web</span><b id="www_latency-web-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_latency-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_failures" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricFailures">webMetricFailures</h2><span class="overview-wide-summary"
|
||||
id="www_failures-value">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="PHP FastCGI failures" data-i18n-aria="webMetricFailures">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_failures-timeout-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_failures-connection-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="www_failures-protocol-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3" id="www_failures-capacity-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesTimeout">timeout</span><b id="www_failures-timeout-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesConnection">connection</span><b
|
||||
id="www_failures-connection-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span data-i18n="webMetricSeriesProtocol">protocol</span><b
|
||||
id="www_failures-protocol-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span data-i18n="webMetricSeriesCapacity">capacity</span><b
|
||||
id="www_failures-capacity-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_traffic" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricTraffic">webMetricTraffic</h2><span class="overview-wide-summary"
|
||||
id="www_traffic-value">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server body byte rate" data-i18n-aria="webMetricTraffic">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_traffic-nonphp-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_traffic-php-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesNonphp">nonphp</span><b id="www_traffic-nonphp-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_traffic-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_status_trend" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricStatusTrend">webMetricStatusTrend</h2><span class="overview-wide-summary"
|
||||
id="www_status_trend-value">-</span>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server HTTP statuses" data-i18n-aria="webMetricStatusTrend">
|
||||
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_status_trend-2xx-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_status_trend-3xx-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="www_status_trend-4xx-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3" id="www_status_trend-5xx-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesTwoxx">2xx</span><b id="www_status_trend-2xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span data-i18n="webMetricSeriesThreexx">3xx</span><b
|
||||
id="www_status_trend-3xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span data-i18n="webMetricSeriesFourxx">4xx</span><b
|
||||
id="www_status_trend-4xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span data-i18n="webMetricSeriesFivexx">5xx</span><b
|
||||
id="www_status_trend-5xx-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_top_services" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricTopServices">Top Web Services</h2>
|
||||
<span class="overview-wide-summary" id="www_top_services-value">-</span>
|
||||
</div>
|
||||
<div id="www_top_services-bars" class="www-top-bars"></div>
|
||||
<small class="fine" data-i18n="webMetricTopServicesHelp">Requests per second by Web
|
||||
Service.</small>
|
||||
</article>
|
||||
<article class="card activity-card" data-dashboard-widget="traffic_chart">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="requestsPerSecond">Requests per second</h2>
|
||||
<span id="chart-peak">max 0.0 req/s</span>
|
||||
</div><svg preserveAspectRatio="none" aria-label="Recent request rate" id="traffic-chart" role="img"
|
||||
viewbox="0 0 600 130">
|
||||
<path class="chart-grid" d="M0 25H600 M0 65H600 M0 105H600"></path>
|
||||
<polyline class="traffic-line traffic-line-total" id="sparkline" points="0,120 600,120">
|
||||
</polyline>
|
||||
<polyline class="traffic-line traffic-line-h1" id="sparkline-h1" points="0,120 600,120">
|
||||
</polyline>
|
||||
<polyline class="traffic-line traffic-line-h2" id="sparkline-h2" points="0,120 600,120">
|
||||
</polyline>
|
||||
<polyline class="traffic-line traffic-line-h3" id="sparkline-h3" points="0,120 600,120">
|
||||
</polyline>
|
||||
<polyline class="traffic-line traffic-line-transform" id="sparkline-transform" points="0,120 600,120">
|
||||
</polyline>
|
||||
</svg>
|
||||
<div class="traffic-protocol-legend" aria-label="Request rate by HTTP protocol and transformed response rate">
|
||||
<span class="legend-total"><i></i>Total <b id="rate-total">0.0/s</b></span>
|
||||
<span class="legend-h1"><i></i>HTTP/1.1 <b id="rate-h1">0.0/s</b></span>
|
||||
<span class="legend-h2"><i></i>HTTP/2 <b id="rate-h2">0.0/s</b></span>
|
||||
<span class="legend-h3"><i></i>HTTP/3 <b id="rate-h3">0.0/s</b></span>
|
||||
<span class="legend-transform"><i></i><span data-i18n="liveTransformed">Transformed</span> <b
|
||||
id="rate-transform">0.0/s</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card system-card" data-dashboard-widget="system_chart">
|
||||
<div class="card-heading">
|
||||
<div>
|
||||
<h3 class="overview-chart-heading" data-i18n="systemLoad">System load</h3>
|
||||
<h2><span id="system-cpu">-</span> <small>CPU</small></h2>
|
||||
</div><span class="meta" id="system-source">-</span>
|
||||
</div><svg preserveAspectRatio="none" aria-label="System CPU history" data-i18n-aria="systemChart" id="system-chart"
|
||||
role="img" viewbox="0 0 300 100">
|
||||
<path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path>
|
||||
<polyline id="system-sparkline" points="0,95 300,95"></polyline>
|
||||
</svg>
|
||||
<div class="memory-line"><span data-i18n="systemMemory">System RAM</span><b id="system-memory">-</b></div>
|
||||
<div class="overview-meter system-memory-meter" id="memory-meter" role="progressbar" aria-label="System RAM"
|
||||
data-i18n-aria="systemMemory" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="memory-meter-fill"></span>
|
||||
</div>
|
||||
<div class="memory-line"><span data-i18n="systemSwap">System swap</span><b id="system-swap">-</b></div>
|
||||
<div class="chart-footer"><span data-i18n="loadAverage">Load 1 / 5 / 15 min</span><span id="system-load">-</span>
|
||||
</div>
|
||||
<div class="chart-footer"><span data-i18n="systemUptime">System uptime</span><span id="system-uptime">-</span></div>
|
||||
</article>
|
||||
<article class="card connection-card" data-dashboard-widget="connection_chart">
|
||||
<div class="card-heading">
|
||||
<div>
|
||||
<h3 class="overview-chart-heading" data-i18n="connectionActivity">Connection activity</h3>
|
||||
<h2><span id="connection-current">0</span> <small data-i18n="connections">Open
|
||||
connections</small></h2>
|
||||
</div>
|
||||
</div>
|
||||
<svg preserveAspectRatio="none" aria-label="Connection activity history" data-i18n-aria="connectionActivity"
|
||||
id="connection-chart" role="img" viewbox="0 0 300 100">
|
||||
<path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path>
|
||||
<polyline class="connection-open-line" id="connection-open-sparkline" points="0,95 300,95"></polyline>
|
||||
<polyline class="connection-exchange-line" id="connection-exchange-sparkline" points="0,95 300,95"></polyline>
|
||||
</svg>
|
||||
<div class="connection-legend"><span class="connection-open-key"><i></i><span data-i18n="connections">Open
|
||||
connections</span> <b id="connection-open-value">0</b></span><span
|
||||
class="connection-exchange-key"><i></i><span data-i18n="activeExchanges">Active
|
||||
exchanges</span> <b id="connection-exchange-value">0</b></span></div>
|
||||
<div class="chart-footer"><span><span data-i18n="backends">Backends</span>: <b id="connection-backends">0</b></span>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,471 @@
|
||||
<article class="stat" data-dashboard-widget="requests"><span data-i18n="requests">Requests</span><strong
|
||||
id="metric-requests">0</strong><small id="request-rate">0 req/s</small></article>
|
||||
<article class="stat" data-dashboard-widget="active"><span data-i18n="activeExchanges">Active
|
||||
exchanges</span><strong id="metric-active">0</strong><small data-i18n="bodyLifetime">Full body lifetime</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="bandwidth"><span data-i18n="bandwidth">Payload
|
||||
transferred</span><strong id="metric-bytes">0 B</strong><small class="payload-split"
|
||||
id="metric-bytes-split"><span>↓ <b id="metric-bytes-down">0 B</b></span><span>↑ <b id="metric-bytes-up">0
|
||||
B</b></span></small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="errors"><span data-i18n="errors">Errors</span><strong
|
||||
id="metric-errors">0</strong><small data-i18n="errorDescription">5xx / body /
|
||||
cancelled</small></article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="cache"><span data-i18n="cacheHitRatio">Cache hit
|
||||
ratio</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="cache-ratio-chart" role="img" aria-label="Cache HIT and MISS distribution"
|
||||
viewBox="0 0 64 64">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="overview-donut-hit" id="cache-ratio-hit-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
<circle class="overview-donut-miss" id="cache-ratio-miss-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
</svg>
|
||||
<div class="overview-donut-info">
|
||||
<strong id="metric-cache-ratio">-</strong>
|
||||
<small class="overview-donut-legend" id="cache-summary">
|
||||
<span class="overview-donut-hit-key"><i></i>HIT <b id="cache-ratio-hit-count">0</b></span>
|
||||
<span class="overview-donut-miss-key"><i></i>MISS <b id="cache-ratio-miss-count">0</b></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="memory"><span data-i18n="processMemory">Process
|
||||
memory</span><strong id="metric-rss">-</strong><small id="process-summary">-</small>
|
||||
</article>
|
||||
<article class="stat stat-with-action" data-dashboard-widget="websocket"><span data-i18n="wsActive">WebSocket
|
||||
tunnels</span><button class="quiet stat-corner-action" data-i18n="tunnelsOpen" id="tunnels-open"
|
||||
type="button">Active tunnels</button><strong id="metric-ws">0</strong><small
|
||||
aria-label="Active WebSocket tunnels by frontend protocol"><span data-i18n="wsH1">H1
|
||||
Upgrade</span> <span id="metric-ws-h1">0</span> · <span data-i18n="wsH2Rfc8441">H2
|
||||
RFC 8441</span> <span id="metric-ws-h2">0</span> · <span data-i18n="wsH3Rfc9220">H3
|
||||
RFC 9220</span> <span id="metric-ws-h3">0</span><br><span data-i18n="wsEstablished">Established</span>:
|
||||
<span id="metric-ws-total">0</span></small></article>
|
||||
<article class="stat" data-dashboard-widget="http2"><span data-i18n="h2Requests">HTTP/2
|
||||
requests</span><strong id="metric-h2">0</strong><small><span data-i18n="h2ActiveConnections">Active H2
|
||||
connections</span>: <span id="metric-h2-active">0</span></small></article>
|
||||
<article class="stat" data-dashboard-widget="http3"><span data-i18n="h3Requests">HTTP/3
|
||||
requests</span><strong id="metric-h3">0</strong></article>
|
||||
<article class="stat" data-dashboard-widget="tls"><span data-i18n="tlsFailures">TLS handshake
|
||||
errors</span><strong id="metric-tls-errors">0</strong></article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="http_protocol"><span data-i18n="httpProtocolMix">HTTP protocol
|
||||
mix</span><strong id="metric-http-proto-total">0.0/s</strong><svg preserveAspectRatio="none"
|
||||
aria-label="HTTP protocol activity" data-i18n-aria="httpProtocolMix" id="http-protocol-chart" role="img"
|
||||
viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="http-protocol-h1" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="http-protocol-h2" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line proto-h3-line" id="http-protocol-h3" points="0,55 300,55"></polyline>
|
||||
</svg><small class="stat-chart-legend"><span class="legend-h1"><i></i>H1 <b
|
||||
id="metric-http-proto-h1">0.0/s</b></span><span class="legend-h2"><i></i>H2 <b
|
||||
id="metric-http-proto-h2">0.0/s</b></span><span class="legend-h3"><i></i>H3 <b
|
||||
id="metric-http-proto-h3">0.0/s</b></span></small></article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="websocket_protocol"><span data-i18n="wsProtocolMix">WebSocket
|
||||
protocol
|
||||
mix</span><strong id="metric-ws-proto-total">0</strong><svg preserveAspectRatio="none"
|
||||
aria-label="WebSocket protocol activity" data-i18n-aria="wsProtocolMix" id="ws-protocol-chart" role="img"
|
||||
viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="ws-protocol-h1" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="ws-protocol-h2" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line proto-h3-line" id="ws-protocol-h3" points="0,55 300,55"></polyline>
|
||||
</svg><small class="stat-chart-legend"><span class="legend-h1"><i></i>H1 <b
|
||||
id="metric-ws-proto-h1">0</b></span><span class="legend-h2"><i></i>H2 <b
|
||||
id="metric-ws-proto-h2">0</b></span><span class="legend-h3"><i></i>H3 <b
|
||||
id="metric-ws-proto-h3">0</b></span></small></article>
|
||||
<article class="stat" data-dashboard-widget="stream_overview" hidden>
|
||||
<span data-i18n="streamWidgetOverview">Stream overview</span><strong id="stream-dashboard-overview">-</strong>
|
||||
<small id="stream-dashboard-overview-detail" data-i18n="streamWidgetUnavailable">No stream
|
||||
data</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="stream_traffic" hidden>
|
||||
<span data-i18n="streamWidgetTraffic">Stream traffic</span><strong id="stream-dashboard-traffic">-</strong>
|
||||
<svg preserveAspectRatio="none" aria-label="Stream traffic RX/TX" data-i18n-aria="streamWidgetTraffic" role="img"
|
||||
viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line stream-chart-rx" id="stream-dashboard-rx" points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line stream-chart-tx" id="stream-dashboard-tx" points="0,55 300,55"></polyline>
|
||||
</svg><small class="stat-chart-legend stream-traffic-legend" id="stream-dashboard-traffic-detail">
|
||||
<span class="legend-rx"><i></i>RX <b id="stream-dashboard-rx-rate">-</b></span>
|
||||
<span class="legend-tx"><i></i>TX <b id="stream-dashboard-tx-rate">-</b></span>
|
||||
</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="stream_sessions" hidden>
|
||||
<span data-i18n="streamWidgetSessions">Stream sessions</span><strong id="stream-dashboard-sessions">-</strong>
|
||||
<small id="stream-dashboard-sessions-detail" data-i18n="streamWidgetUnavailable">No stream
|
||||
data</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="stream_backend_health" hidden>
|
||||
<span data-i18n="streamWidgetHealth">Stream backend health</span><strong id="stream-dashboard-health">-</strong>
|
||||
<small id="stream-dashboard-health-detail" data-i18n="streamWidgetUnavailable">No stream
|
||||
data</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_health" hidden>
|
||||
<span data-i18n="backendWidgetHealth">Backend health</span><strong id="backend-dashboard-health">-</strong>
|
||||
<small id="backend-dashboard-health-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_activity" hidden>
|
||||
<span data-i18n="backendWidgetActivity">Active backend requests</span><strong
|
||||
id="backend-dashboard-activity">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetActivity" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-active-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-activity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_errors" hidden>
|
||||
<span data-i18n="backendWidgetErrors">Backend errors</span><strong id="backend-dashboard-errors">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetErrors" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h3-line" id="backend-dashboard-error-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-errors-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_latency" hidden>
|
||||
<span data-i18n="backendWidgetLatency">Health probe RTT</span><strong id="backend-dashboard-latency">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetLatency" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-latency-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-latency-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_traffic" hidden>
|
||||
<span data-i18n="backendWidgetTraffic">Backend traffic</span><strong id="backend-dashboard-traffic">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetTraffic" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line stream-chart-rx" id="backend-dashboard-in-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
<polyline class="diagnostic-line stream-chart-tx" id="backend-dashboard-out-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small class="stat-chart-legend stream-traffic-legend">
|
||||
<span class="legend-rx"><i></i>IN <b id="backend-dashboard-in-rate">-</b></span>
|
||||
<span class="legend-tx"><i></i>OUT <b id="backend-dashboard-out-rate">-</b></span>
|
||||
</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_protocols" hidden>
|
||||
<span data-i18n="backendWidgetProtocols">Configured backend modes</span><strong
|
||||
id="backend-dashboard-protocols">-</strong>
|
||||
<div class="backend-mode-bar" role="img" data-i18n-aria="backendWidgetProtocols">
|
||||
<span class="backend-mode-h1" id="backend-mode-h1"></span>
|
||||
<span class="backend-mode-auto" id="backend-mode-auto"></span>
|
||||
<span class="backend-mode-compat" id="backend-mode-compat"></span>
|
||||
<span class="backend-mode-h2" id="backend-mode-h2"></span>
|
||||
</div><small id="backend-dashboard-protocols-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_requests" hidden>
|
||||
<span data-i18n="backendWidgetRequests">Backend request rate</span><strong
|
||||
id="backend-dashboard-requests">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetRequests" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-request-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-requests-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="backend_capacity" hidden>
|
||||
<span data-i18n="backendWidgetCapacity">Backend capacity</span><strong id="backend-dashboard-capacity">-</strong>
|
||||
<div class="overview-meter" id="backend-dashboard-capacity-meter" role="progressbar"
|
||||
data-i18n-aria="backendWidgetCapacity" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="backend-dashboard-capacity-meter-fill"></span>
|
||||
</div><small id="backend-dashboard-capacity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden>
|
||||
<span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><strong id="backend-dashboard-http5xx">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetHttp5xx" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-5xx-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-http5xx-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_timeouts" hidden>
|
||||
<span data-i18n="backendWidgetTimeouts">backendWidgetTimeouts</span><strong
|
||||
id="backend-dashboard-timeouts">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetTimeouts" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-timeout-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-timeouts-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_network" hidden>
|
||||
<span data-i18n="backendWidgetNetwork">backendWidgetNetwork</span><strong id="backend-dashboard-network">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetNetwork" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-network-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-network-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_retries" hidden>
|
||||
<span data-i18n="backendWidgetRetries">backendWidgetRetries</span><strong id="backend-dashboard-retries">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetRetries" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-retry-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-retries-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_queue" hidden>
|
||||
<span data-i18n="backendWidgetQueue">backendWidgetQueue</span><strong id="backend-dashboard-queue">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetQueue" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-queue-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="backend-dashboard-queue-detail">-</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_rotations" hidden>
|
||||
<span data-i18n="backendWidgetRotations">Backend client pool rotations</span><strong
|
||||
id="backend-dashboard-rotations">-</strong>
|
||||
<small id="backend-dashboard-rotations-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_cpu" hidden>
|
||||
<span data-i18n="overviewExtraProcessCpu">overviewExtraProcessCpu</span><strong
|
||||
id="overview-extra-extra_process_cpu">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraProcessCpu" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_process_cpu-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_process_cpu-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_rss" hidden>
|
||||
<span data-i18n="overviewExtraProcessRss">overviewExtraProcessRss</span><strong
|
||||
id="overview-extra-extra_process_rss">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraProcessRss" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_process_rss-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_process_rss-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_process_fds" hidden>
|
||||
<span data-i18n="overviewExtraProcessFds">overviewExtraProcessFds</span><strong
|
||||
id="overview-extra-extra_process_fds">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_process_fds-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraProcessFds" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_process_fds-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_process_fds-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_open_connections" hidden>
|
||||
<span data-i18n="overviewExtraOpenConnections">overviewExtraOpenConnections</span><strong
|
||||
id="overview-extra-extra_open_connections">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraOpenConnections" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_open_connections-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_open_connections-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_websocket_active" hidden>
|
||||
<span data-i18n="overviewExtraWebsocketActive">overviewExtraWebsocketActive</span><strong
|
||||
id="overview-extra-extra_websocket_active">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraWebsocketActive" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_websocket_active-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_websocket_active-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_tls_error_rate" hidden>
|
||||
<span data-i18n="overviewExtraTlsErrorRate">overviewExtraTlsErrorRate</span><strong
|
||||
id="overview-extra-extra_tls_error_rate">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraTlsErrorRate" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_tls_error_rate-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_tls_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_request_error_rate" hidden>
|
||||
<span data-i18n="overviewExtraRequestErrorRate">overviewExtraRequestErrorRate</span><strong
|
||||
id="overview-extra-extra_request_error_rate">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraRequestErrorRate" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_request_error_rate-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_request_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="extra_cache_hit_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheHitRate">Cache hits share (recent)</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="extra-cache-ratio-chart" role="img"
|
||||
aria-label="Recent cache HIT and MISS distribution" viewBox="0 0 64 64">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="overview-donut-hit" id="extra-cache-ratio-hit-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
<circle class="overview-donut-miss" id="extra-cache-ratio-miss-arc" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100"></circle>
|
||||
</svg>
|
||||
<div class="overview-donut-info">
|
||||
<strong id="overview-extra-extra_cache_hit_rate">-</strong>
|
||||
<small class="overview-donut-legend" id="overview-extra-extra_cache_hit_rate-detail">
|
||||
<span class="overview-donut-hit-key"><i></i>HIT <b id="extra-cache-ratio-hit-count">0</b></span>
|
||||
<span class="overview-donut-miss-key"><i></i>MISS <b id="extra-cache-ratio-miss-count">0</b></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cache_memory">
|
||||
<span data-i18n="overviewExtraCacheMemory">overviewExtraCacheMemory</span><strong
|
||||
id="overview-extra-extra_cache_memory">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cache_memory-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraCacheMemory" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_cache_memory-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_cache_memory-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cold_queue_memory">
|
||||
<span data-i18n="overviewExtraColdQueueMemory">COLD queue RAM utilization</span>
|
||||
<strong id="overview-extra-extra_cold_queue_memory">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cold_queue_memory-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraColdQueueMemory" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_cold_queue_memory-meter-fill"></span>
|
||||
</div>
|
||||
<small id="overview-extra-extra_cold_queue_memory-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cold_storage">
|
||||
<span data-i18n="overviewExtraColdStorage">COLD disk utilization</span>
|
||||
<strong id="overview-extra-extra_cold_storage">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cold_storage-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraColdStorage" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_cold_storage-meter-fill"></span>
|
||||
</div>
|
||||
<small id="overview-extra-extra_cold_storage-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h2_share" hidden>
|
||||
<span data-i18n="overviewExtraH2Share">overviewExtraH2Share</span><strong
|
||||
id="overview-extra-extra_h2_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h2_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH2Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h2_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h2_share-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_response_latency" hidden>
|
||||
<span data-i18n="overviewExtraResponseLatency">overviewExtraResponseLatency</span><strong
|
||||
id="overview-extra-extra_response_latency">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraResponseLatency" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_response_latency-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_response_latency-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_backend_unhealthy" hidden>
|
||||
<span data-i18n="overviewExtraBackendUnhealthy">overviewExtraBackendUnhealthy</span><strong
|
||||
id="overview-extra-extra_backend_unhealthy">-</strong>
|
||||
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraBackendUnhealthy" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_backend_unhealthy-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_backend_unhealthy-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_total_rps" hidden>
|
||||
<span data-i18n="overviewExtraTotalRps">HTTP requests / s</span><strong
|
||||
id="overview-extra-extra_total_rps">-</strong>
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img" aria-label="HTTP requests / s">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_total_rps-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_total_rps-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_active_exchanges" hidden>
|
||||
<span data-i18n="overviewExtraActiveExchanges">Active HTTP exchanges</span><strong
|
||||
id="overview-extra-extra_active_exchanges">-</strong>
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img" aria-label="Active HTTP exchanges">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_active_exchanges-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_active_exchanges-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h1_share" hidden>
|
||||
<span data-i18n="overviewExtraH1Share">HTTP/1.1 share</span><strong id="overview-extra-extra_h1_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h1_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH1Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h1_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h1_share-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h3_share" hidden>
|
||||
<span data-i18n="overviewExtraH3Share">HTTP/3 share</span><strong id="overview-extra-extra_h3_share">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_h3_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH3Share" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h3_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h3_share-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheMissRate">Cache misses / s</span><strong
|
||||
id="overview-extra-extra_cache_miss_rate">-</strong>
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img" aria-label="Cache misses / s">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_cache_miss_rate-line" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg><small id="overview-extra-extra_cache_miss_rate-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_backend_active" hidden>
|
||||
<span data-i18n="overviewExtraBackendActive">In-flight backend requests</span><strong
|
||||
id="overview-extra-extra_backend_active">-</strong>
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img" aria-label="In-flight backend requests">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_backend_active-line" points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_backend_active-detail" data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<!-- RP_METRICS_SPLIT -->
|
||||
<article class="stat stat-donut" data-dashboard-widget="www_status" hidden>
|
||||
<span data-i18n="webMetricStatus">webMetricStatus</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="www_status-chart" role="img" viewBox="0 0 64 64" aria-label="WWW status"
|
||||
data-i18n-aria="webMetricStatus">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="www-ring-arc www-ring-0" id="www_status-arc-0" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-1" id="www_status-arc-1" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-2" id="www_status-arc-2" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-3" id="www_status-arc-3" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
</svg>
|
||||
<div class="overview-donut-info"><strong id="www_status-value">-</strong><small
|
||||
class="overview-donut-legend www-ring-legend"><span><i class="www-dot-0"></i>2xx
|
||||
<b id="www_status-count-0">0</b></span>
|
||||
<span><i class="www-dot-1"></i>3xx <b id="www_status-count-1">0</b></span>
|
||||
<span><i class="www-dot-2"></i>4xx <b id="www_status-count-2">0</b></span>
|
||||
<span><i class="www-dot-3"></i>5xx <b id="www_status-count-3">0</b></span></small></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="www_kind" hidden>
|
||||
<span data-i18n="webMetricKind">webMetricKind</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="www_kind-chart" role="img" viewBox="0 0 64 64" aria-label="Static / PHP"
|
||||
data-i18n-aria="webMetricKind">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="www-ring-arc www-ring-0" id="www_kind-arc-0" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-1" id="www_kind-arc-1" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-2" id="www_kind-arc-2" cx="32" cy="32" r="24" pathLength="100"
|
||||
stroke-dasharray="0 100" />
|
||||
</svg>
|
||||
<div class="overview-donut-info"><strong id="www_kind-value">-</strong><small
|
||||
class="overview-donut-legend www-ring-legend"><span><i class="www-dot-0"></i>Static <b
|
||||
id="www_kind-count-0">0</b></span>
|
||||
<span><i class="www-dot-1"></i>PHP <b id="www_kind-count-1">0</b></span>
|
||||
<span><i class="www-dot-2"></i>Other <b id="www_kind-count-2">0</b></span></small></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="www_capacity" hidden>
|
||||
<span data-i18n="webMetricCapacity">webMetricCapacity</span><strong id="www_capacity-value">-</strong>
|
||||
<div class="overview-meter" id="www_capacity-meter" role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="www_capacity-meter-fill"></span>
|
||||
</div><small id="www_capacity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="www_error_rate" hidden>
|
||||
<span data-i18n="webMetricErrorRate">webMetricErrorRate</span><strong id="www_error_rate-value">-</strong>
|
||||
<div class="overview-meter" id="www_error_rate-meter" role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="www_error_rate-meter-fill"></span>
|
||||
</div><small id="www_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="www_methods" hidden>
|
||||
<span data-i18n="webMetricMethods">HTTP Methods</span><strong id="www_methods-value">-</strong>
|
||||
<div class="www-segmented" id="www_methods-bar" role="img" aria-label="HTTP method share"
|
||||
data-i18n-aria="webMetricMethods"><span class="www-segment-0" id="www_methods-segment-0"></span><span
|
||||
class="www-segment-1" id="www_methods-segment-1"></span><span class="www-segment-2"
|
||||
id="www_methods-segment-2"></span><span class="www-segment-3" id="www_methods-segment-3"></span><span
|
||||
class="www-segment-4" id="www_methods-segment-4"></span></div>
|
||||
<small class="www-method-legend" id="www_methods-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="www_php_requests" hidden>
|
||||
<span data-i18n="webMetricPhpRequests">PHP requests/s</span><strong id="www_php_requests-value">-</strong>
|
||||
<svg preserveAspectRatio="none" role="img" viewBox="0 0 300 60" aria-label="PHP requests per second"
|
||||
data-i18n-aria="webMetricPhpRequests">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="www_php_requests-line" points="0,55 300,55"></polyline>
|
||||
</svg>
|
||||
<small data-i18n="webMetricPerSecond">Requests per second</small>
|
||||
</article>
|
||||
+347
-2
@@ -3508,6 +3508,10 @@ body[data-density=dense] .stat strong {
|
||||
margin: 6px 0
|
||||
}
|
||||
|
||||
#panel-cache>article.card+article.card {
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.cache-table {
|
||||
font-size: 12px
|
||||
}
|
||||
@@ -11210,7 +11214,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), 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-hidden-toggle, .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, .badge, .quiet, .outline, .primary, .code, .command, .dashboard-widget-toggle, .settings-tabs button, .overview-inline-hidden-toggle, .overview-inline-restore) {
|
||||
border-radius: var(--rp-radius-control) !important;
|
||||
}
|
||||
|
||||
@@ -11237,6 +11241,16 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
padding: 3px 7px;
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.vhost-backend-protocol-badge:is(:hover, [aria-expanded=true]) {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.vhost-backend-protocol-badge:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vhost-backend-protocol-badge[data-observed=true] {
|
||||
@@ -11596,7 +11610,6 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Web Services reuse the same full-width live tiles and chart as Vhosts. */
|
||||
#panel-web-server .www-service-panel {
|
||||
min-width: 0;
|
||||
margin-top: 10px;
|
||||
@@ -11813,4 +11826,336 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] {
|
||||
padding-top: 135px;
|
||||
}
|
||||
}
|
||||
|
||||
#panel-overview :is(.system-card, .connection-card) :is(.card-heading h2, .card-heading .meta, .memory-line, .chart-footer, .connection-legend) {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview :is(.system-card, .connection-card) .card-heading h2 {
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.overview-presets-dialog {
|
||||
width: calc(min(730px, 100vw - 24px));
|
||||
max-height: calc(100dvh - 24px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.overview-presets-content {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.overview-presets-content>p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.overview-presets-create {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.overview-presets-create-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.overview-presets-create-controls input {
|
||||
min-width: 150px;
|
||||
flex: 1 1 240px;
|
||||
}
|
||||
|
||||
.overview-presets-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.overview-presets-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius, 8px);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.overview-presets-row strong {
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.overview-presets-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.overview-presets-dialog .overview-layout-footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media(max-width:480px) {
|
||||
.overview-presets-create-controls>button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.overview-presets-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview-presets-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#panel-overview .activity-card .card-heading h2,
|
||||
#panel-overview .overview-wide-chart .card-heading h2,
|
||||
#panel-overview :is(.stat-chart, .stat-donut)>span:first-child,
|
||||
#panel-overview :is(.system-card, .connection-card) .overview-chart-heading,
|
||||
.function-live-chart-heading>strong,
|
||||
.mini-live-chart>.mini-live-chart-head>span:first-child,
|
||||
.traffic-chart-card>h3.subheading,
|
||||
.vhost-chart-head>span:first-child {
|
||||
font-family: var(--sans);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
letter-spacing: normal;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.activity-card .card-heading h2,
|
||||
html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.overview-wide-chart .card-heading h2,
|
||||
html[data-dashboard-density] #panel-overview .stat:is(.stat-chart, .stat-donut)>span:first-child {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#panel-overview :is(.system-card, .connection-card) .overview-chart-heading {
|
||||
color: var(--text);
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
:is(.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, .network-interface-rate) {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview .overview-wide-chart>.fine,
|
||||
#panel-overview .stat-chart>small,
|
||||
#panel-overview .www-top-row,
|
||||
.traffic-bar-row {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview :is(.overview-wide-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stat-chart-legend, .mini-chart-legend, .connection-legend, .traffic-protocol-legend, .stream-traffic-legend),
|
||||
:is(.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) {
|
||||
font-family: var(--mono);
|
||||
font-size: 9px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#panel-overview :is(.overview-wide-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stat-chart-legend, .mini-chart-legend, .connection-legend, .traffic-protocol-legend, .stream-traffic-legend) :is(span, b, strong, small, li, em),
|
||||
:is(.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) :is(span, b, strong, small, li, em) {
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stream-traffic-legend),
|
||||
html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .overview-donut-legend, .www-ring-legend, .www-method-legend, .stream-traffic-legend) :is(span, b, small, strong, li) {
|
||||
font-size: 9px;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.vhost-detail.vhost-backend-trigger,
|
||||
.vhost-live-metric.vhost-backend-trigger {
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vhost-detail.vhost-backend-trigger {
|
||||
border: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.vhost-live-metric.vhost-backend-trigger {
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vhost-backend-trigger:is(:hover, [aria-expanded=true]) {
|
||||
background: var(--panel2);
|
||||
}
|
||||
|
||||
.vhost-backend-trigger:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup {
|
||||
position: fixed;
|
||||
z-index: 1600;
|
||||
max-height: calc(min(360px, 100dvh - 24px));
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--rp-radius-surface, 0);
|
||||
box-shadow: 0 12px 35px var(--shadow-strong);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup h4 {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-close {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font: 18px/1 var(--sans);
|
||||
border-radius: var(--rp-radius-control, 0);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-close:hover {
|
||||
background: var(--panel2);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-row {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--rp-radius-surface, 0);
|
||||
background: var(--bg);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-name {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
overflow-wrap: anywhere;
|
||||
font: 650 11px var(--mono);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-columns {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
|
||||
gap: 6px 10px;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field>span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field>span:first-child {
|
||||
font-size: 9px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field>span:last-child {
|
||||
font: 10px/1.35 var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field.has-success>span:last-child {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-field.has-error>span:last-child {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.vhost-backend-popup-hint {
|
||||
margin: 10px 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>.card .mini-heading>h3,
|
||||
#panel-overview .dashboard-overview-grid>.card .overview-chart-heading,
|
||||
#panel-overview .dashboard-overview-grid>.card:is(.overview-wide-chart, .activity-card, .pool-card) .card-heading h2,
|
||||
#panel-overview .dashboard-overview-grid>.stat>span:first-child {
|
||||
font-family: var(--sans);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid>[data-dashboard-widget] :is(small, .mini-metrics dt, .mini-metrics dd, .mini-live-chart-head, .memory-line, .chart-footer, .small-label, .meta, .fine) {
|
||||
font-family: var(--mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid .mini-live-chart>.mini-live-chart-head>span:first-child {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.pool-card .card-heading h2 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#panel-cache>.card .card-heading h2,
|
||||
#panel-overview .dashboard-overview-grid .mini-live-chart-head>span:first-child,
|
||||
#panel-overview .dashboard-overview-grid>.card .mini-heading>h3,
|
||||
#panel-overview .dashboard-overview-grid>.card .overview-chart-heading,
|
||||
#panel-overview .dashboard-overview-grid>.card:not(.system-card):not(.connection-card) .card-heading h2,
|
||||
#panel-overview .dashboard-overview-grid>.stat>span:first-child,
|
||||
#panel-transform>.card .card-heading h2,
|
||||
.function-live-chart-heading>strong,
|
||||
.traffic-chart-card>h3.subheading,
|
||||
.vhost-chart-head>span:first-child {
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -1,3 +1,158 @@
|
||||
// Shared, anchored Vhost popover for backend quick-view and upstream HTTP
|
||||
// protocol observations. Both consume existing live status without extra probes.
|
||||
let activeVhostBackendPopup = null;
|
||||
function closeVhostBackendPopup(returnFocus = false) {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
activeVhostBackendPopup = null;
|
||||
current.trigger.setAttribute("aria-expanded", "false");
|
||||
current.node.remove();
|
||||
if (returnFocus && current.trigger.isConnected) current.trigger.focus();
|
||||
}
|
||||
function positionVhostBackendPopup() {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
const rect = current.trigger.getBoundingClientRect(), edge = 12, gap = 6;
|
||||
const width = Math.min(430, Math.max(0, window.innerWidth - edge * 2));
|
||||
const height = Math.min(current.node.scrollHeight, 360, Math.max(100, window.innerHeight - edge * 2));
|
||||
const below = window.innerHeight - rect.bottom - edge - gap;
|
||||
const above = rect.top - edge - gap;
|
||||
const placeAbove = below < height && above > below;
|
||||
const desiredTop = placeAbove ? rect.top - height - gap : rect.bottom + gap;
|
||||
current.node.style.width = `${width}px`;
|
||||
current.node.style.left = `${Math.max(edge, Math.min(rect.left, window.innerWidth - width - edge))}px`;
|
||||
current.node.style.top = `${Math.max(edge, Math.min(desiredTop, window.innerHeight - height - edge))}px`;
|
||||
}
|
||||
function vhostPopupHeading(label) {
|
||||
const heading = el("h4", label);
|
||||
heading.id = "vhost-backend-popup-heading";
|
||||
const headingRow = el("div", undefined, "vhost-backend-popup-heading");
|
||||
const closeButton = el("button", "×", "vhost-backend-popup-close");
|
||||
closeButton.type = "button";
|
||||
closeButton.setAttribute("aria-label", t("close"));
|
||||
closeButton.addEventListener("click", () => closeVhostBackendPopup(true));
|
||||
headingRow.append(heading, closeButton);
|
||||
return { heading, headingRow };
|
||||
}
|
||||
function refreshVhostProtocolPopup(current, vhost, members) {
|
||||
const backend = (vhost.backends || []).find(item => item.id === current.backendId);
|
||||
if (!backend) { closeVhostBackendPopup(); return; }
|
||||
const member = members.get(backend.id);
|
||||
const observed = upstreamProtocolBadgeText(backend, member);
|
||||
if (!current.protocolView) {
|
||||
const { heading, headingRow } = vhostPopupHeading(observed.label);
|
||||
const info = el("p", "", "fine vhost-backend-popup-hint");
|
||||
const list = el("ul", undefined, "vhost-backend-popup-list");
|
||||
const row = el("li", undefined, "vhost-backend-popup-row");
|
||||
const columns = el("div", undefined, "vhost-backend-popup-columns");
|
||||
const counters = [];
|
||||
for (const protocol of ["H1", "H2"]) {
|
||||
const field = el("div", undefined, "vhost-backend-popup-field");
|
||||
const value = el("span", "0");
|
||||
field.append(el("span", protocol), value);
|
||||
columns.append(field); counters.push(value);
|
||||
}
|
||||
row.append(columns); list.append(row);
|
||||
current.node.append(headingRow, info, list);
|
||||
current.node.setAttribute("aria-labelledby", heading.id);
|
||||
current.protocolView = { heading, info, list, counters };
|
||||
}
|
||||
const view = current.protocolView;
|
||||
const explanation = t(observed.recent ? "vhBackendProtocolRecent" : "vhBackendProtocolUnobserved");
|
||||
if (view.heading.textContent !== observed.label) view.heading.textContent = observed.label;
|
||||
if (view.info.textContent !== explanation) view.info.textContent = explanation;
|
||||
view.list.hidden = !observed.recent;
|
||||
if (observed.recent) {
|
||||
const totals = [member.upstream_protocol_http1_total || 0, member.upstream_protocol_http2_total || 0];
|
||||
totals.forEach((total, index) => {
|
||||
const label = String(total);
|
||||
if (view.counters[index].textContent !== label) view.counters[index].textContent = label;
|
||||
});
|
||||
}
|
||||
positionVhostBackendPopup();
|
||||
}
|
||||
function refreshVhostBackendPopup(status = latestStatus) {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
const vhost = (envelope?.config.vhosts || []).find(item => item.id === current.id);
|
||||
if (!vhost || !current.trigger.isConnected) { closeVhostBackendPopup(); return; }
|
||||
const pool = (status?.backend_pools || []).find(item => item.id === vhost.id);
|
||||
const members = new Map((pool?.members || []).map(member => [member.id, member]));
|
||||
if (current.backendId !== null) {
|
||||
refreshVhostProtocolPopup(current, vhost, members);
|
||||
return;
|
||||
}
|
||||
const { heading, headingRow } = vhostPopupHeading(`${t("vhBackendPopupTitle")} · ${vhost.id}`);
|
||||
const list = el("ul", undefined, "vhost-backend-popup-list");
|
||||
for (const backend of vhost.backends || []) {
|
||||
const member = members.get(backend.id);
|
||||
const row = el("li", undefined, "vhost-backend-popup-row");
|
||||
row.append(el("strong", backend.id, "vhost-backend-popup-name"));
|
||||
let address = "-", scheme = "-";
|
||||
try {
|
||||
const url = new URL(backend.url);
|
||||
scheme = url.protocol.replace(":", "").toUpperCase();
|
||||
const port = url.port || (scheme === "HTTPS" ? "443" : scheme === "HTTP" ? "80" : "");
|
||||
address = `${url.hostname}${port ? `:${port}` : ""}`;
|
||||
} catch { /* Configuration may have changed while status was refreshing. */ }
|
||||
const upstream = upstreamProtocolBadgeText(backend, member);
|
||||
const protocol = `${scheme} · ${upstream.mode}${upstream.observed ? ` → ${upstream.observed}` : ""}`;
|
||||
const statusCode = Number(member?.last_status);
|
||||
const hasStatus = member && Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599;
|
||||
const httpResponse = hasStatus ? `HTTP ${statusCode}` : t("vhBackendPopupNoResponse");
|
||||
const statusLabel = !member ? t("vhHealthUnknown") : t(({
|
||||
up: "vhUp", down: "vhDown", disabled: "vhDisabled", checking: "vhChecking", draining: "vhDraining"
|
||||
})[member.status] || "vhHealthUnknown");
|
||||
const columns = el("div", undefined, "vhost-backend-popup-columns");
|
||||
for (const [key, value] of [
|
||||
["vhBackendPopupAddress", address], ["vhBackendPopupProtocol", protocol],
|
||||
["vhBackendPopupLastHttp", httpResponse], ["status", statusLabel]
|
||||
]) {
|
||||
const field = el("div", undefined, "vhost-backend-popup-field");
|
||||
field.append(text("span", key), el("span", value));
|
||||
if (key === "vhBackendPopupLastHttp" && hasStatus) field.classList.add(statusCode >= 500 ? "has-error" : statusCode < 400 ? "has-success" : "");
|
||||
columns.append(field);
|
||||
}
|
||||
row.append(columns); list.append(row);
|
||||
}
|
||||
if (!list.children.length) list.append(el("li", t("vhBackendPopupEmpty"), "fine"));
|
||||
const foot = el("p", t("vhBackendPopupHint"), "fine vhost-backend-popup-hint");
|
||||
current.node.replaceChildren(headingRow, list, foot);
|
||||
current.node.setAttribute("aria-labelledby", heading.id);
|
||||
positionVhostBackendPopup();
|
||||
}
|
||||
function toggleVhostBackendPopup(id, trigger, backendId = null) {
|
||||
if (activeVhostBackendPopup?.trigger === trigger) { closeVhostBackendPopup(); return; }
|
||||
closeVhostBackendPopup();
|
||||
const node = el("section", undefined, "vhost-backend-popup");
|
||||
node.id = "vhost-backend-popup";
|
||||
node.setAttribute("role", "dialog");
|
||||
node.setAttribute("aria-modal", "false");
|
||||
trigger.setAttribute("aria-expanded", "true");
|
||||
activeVhostBackendPopup = { id, trigger, node, backendId, protocolView: null };
|
||||
document.body.append(node);
|
||||
refreshVhostBackendPopup();
|
||||
}
|
||||
function enableVhostBackendPopup(trigger, vhost, backendId = null) {
|
||||
trigger.dataset.vhostBackendTrigger = vhost.id;
|
||||
trigger.setAttribute("aria-haspopup", "dialog");
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
trigger.setAttribute("aria-controls", "vhost-backend-popup");
|
||||
if (backendId === null) trigger.setAttribute("aria-label", `${t("vhBackendPopupOpen")}: ${vhost.id}`);
|
||||
trigger.addEventListener("click", () => toggleVhostBackendPopup(vhost.id, trigger, backendId));
|
||||
}
|
||||
document.addEventListener("pointerdown", event => {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (current && !current.node.contains(event.target) && !current.trigger.contains(event.target)) closeVhostBackendPopup();
|
||||
});
|
||||
document.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape" && activeVhostBackendPopup) { event.preventDefault(); event.stopPropagation(); closeVhostBackendPopup(true); }
|
||||
});
|
||||
window.addEventListener("resize", positionVhostBackendPopup);
|
||||
document.addEventListener("scroll", event => {
|
||||
if (activeVhostBackendPopup && !activeVhostBackendPopup.node.contains(event.target)) positionVhostBackendPopup();
|
||||
}, true);
|
||||
|
||||
const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
|
||||
const vhostSortColumns = ["id", "hostname", "status", "listeners", "certificate", "backends"];
|
||||
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] });
|
||||
@@ -107,7 +262,8 @@ function upstreamProtocolBadgeText(backend, member, now = Date.now()) {
|
||||
const seen = Number(member?.upstream_protocol_last_seen_ms);
|
||||
const recent = member && (member.upstream_protocol_observed === "h1" || member.upstream_protocol_observed === "h2") &&
|
||||
Number.isFinite(seen) && seen > 0 && now >= seen && now - seen <= VHOST_PROTOCOL_FRESH_MS;
|
||||
return { label: `${backend.id} · ${mode} → ${recent ? member.upstream_protocol_observed.toUpperCase() : "?"}`, recent };
|
||||
const observed = recent ? member.upstream_protocol_observed.toUpperCase() : null;
|
||||
return { label: `${backend.id} · ${mode} → ${observed || "?"}`, recent, mode, observed };
|
||||
}
|
||||
function renderUpstreamProtocolBadges(status) {
|
||||
const members = new Map((status.backend_pools || []).flatMap(pool => (pool.members || []).map(member =>
|
||||
@@ -119,13 +275,12 @@ function renderUpstreamProtocolBadges(status) {
|
||||
if (!backend) return;
|
||||
const member = members.get(`${vhost}\u0000${id}`);
|
||||
const observed = upstreamProtocolBadgeText(backend, member, now);
|
||||
node.textContent = observed.label;
|
||||
if (node.textContent !== observed.label) node.textContent = observed.label;
|
||||
node.dataset.observed = String(!!observed.recent);
|
||||
const counts = member && observed.recent ? ` H1: ${member.upstream_protocol_http1_total || 0}; H2: ${member.upstream_protocol_http2_total || 0}.` : "";
|
||||
node.title = `${t(observed.recent ? "vhBackendProtocolRecent" : "vhBackendProtocolUnobserved")}${counts}`;
|
||||
});
|
||||
}
|
||||
function render() {
|
||||
closeVhostBackendPopup();
|
||||
if (!envelope) return;
|
||||
if (!initialized) load(defaults(), [], false);
|
||||
const config = envelope.config;
|
||||
@@ -209,10 +364,21 @@ function render() {
|
||||
: t("webServer"))
|
||||
: `${(v.backends || []).length}`],
|
||||
];
|
||||
for (const [key, value] of detailItems) { const item = el("div", undefined, "vhost-detail"); item.append(text("span", key, "small-label"), el("b", value)); details.append(item); }
|
||||
for (const [key, value] of detailItems) {
|
||||
const isBackend = key === "vhBackendPool";
|
||||
const item = el(isBackend ? "button" : "div", undefined, `vhost-detail${isBackend ? " vhost-backend-trigger" : ""}`);
|
||||
if (isBackend) { item.type = "button"; enableVhostBackendPopup(item, v); }
|
||||
item.append(text("span", key, "small-label"), el("b", value)); details.append(item);
|
||||
}
|
||||
box.append(details);
|
||||
const live = el("div", undefined, "vhost-live-grid");
|
||||
const metric = (key, suffix) => { const item = el("div", undefined, "vhost-live-metric"); item.append(text("span", key), el("strong", "-")); item.querySelector("strong").dataset[suffix] = v.id; live.append(item); };
|
||||
const metric = (key, suffix) => {
|
||||
const isHealth = key === "status";
|
||||
const item = el(isHealth ? "button" : "div", undefined, `vhost-live-metric${isHealth ? " vhost-backend-trigger" : ""}`);
|
||||
if (isHealth) { item.type = "button"; enableVhostBackendPopup(item, v); }
|
||||
item.append(text("span", key), el("strong", "-"));
|
||||
item.querySelector("strong").dataset[suffix] = v.id; live.append(item);
|
||||
};
|
||||
metric("status", "vhostHealth"); metric("vhRequests", "vhostRequests"); metric("vhActive", "vhostActive"); metric("vhErrors", "vhostErrors"); metric("vhLatency", "vhostLatency"); metric("vhTraffic", "vhostTraffic");
|
||||
box.append(live);
|
||||
const chart = el("div", undefined, "vhost-chart"); const chartHead = el("div", undefined, "vhost-chart-head"); chartHead.append(text("span", "vhLifetimeChart", "small-label")); const chartValue = el("span", "-", "fine"); chartValue.dataset.chartValueFor = v.id; chartHead.append(chartValue); chart.append(chartHead);
|
||||
@@ -243,11 +409,12 @@ function render() {
|
||||
// Retain their own badge class so observed H1/H2 keeps its green styling.
|
||||
if (hasProxySource) {
|
||||
for (const backend of v.backends || []) {
|
||||
const badge = el("span", undefined, "vhost-backend-protocol-badge");
|
||||
const badge = el("button", undefined, "vhost-backend-protocol-badge");
|
||||
badge.type = "button";
|
||||
badge.dataset.upstreamProtocolVhost = v.id;
|
||||
badge.dataset.upstreamProtocolBackend = backend.id;
|
||||
badge.textContent = upstreamProtocolBadgeText(backend, null).label;
|
||||
badge.title = t("vhBackendProtocolUnobserved");
|
||||
enableVhostBackendPopup(badge, v, backend.id);
|
||||
policy.append(badge);
|
||||
}
|
||||
}
|
||||
@@ -282,6 +449,7 @@ function vhostTrafficRate(id, metrics, snapshot, now, seen) {
|
||||
}
|
||||
function renderStatus(status) {
|
||||
renderUpstreamProtocolBadges(status);
|
||||
refreshVhostBackendPopup(status);
|
||||
form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.());
|
||||
const rows = [], summaries = new Map(), aggregate = new Map();
|
||||
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set();
|
||||
@@ -415,5 +583,5 @@ $("pool-open").addEventListener("click", () => setPanel("vhosts"));
|
||||
initPoolTableTools();
|
||||
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
|
||||
window.rustProxyDrafts.register("vhost", { labelKey: "vhosts", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { closeVhostBackendPopup(); dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
@@ -2,6 +2,7 @@
|
||||
//! No database call is made from the public HTTP forwarding path.
|
||||
use crate::config::{Config, Envelope};
|
||||
mod certificate_api;
|
||||
mod dashboard_presets;
|
||||
mod history;
|
||||
use anyhow::{ensure, Context, Result};
|
||||
pub use history::{ConfigurationHistoryDetail, ConfigurationRevision};
|
||||
@@ -12,7 +13,7 @@ use sqlx::{
|
||||
};
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
pub const SCHEMA_VERSION: i64 = 9;
|
||||
pub const SCHEMA_VERSION: i64 = 10;
|
||||
pub const CERTIFICATE_API_CREDENTIAL_CAPACITY: i64 = 32;
|
||||
pub const AUDIT_CAPACITY: i64 = 10_000;
|
||||
|
||||
@@ -123,6 +124,9 @@ impl Store {
|
||||
))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::raw_sql(include_str!("../migrations/0010_dashboard_presets.sql"))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("INSERT OR IGNORE INTO configuration(id, revision, document) VALUES(1, 0, ?)")
|
||||
.bind(serde_json::to_string(initial)?)
|
||||
.execute(&mut *tx)
|
||||
@@ -133,7 +137,7 @@ impl Store {
|
||||
.bind(crate::auth::now())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("PRAGMA user_version = 9")
|
||||
sqlx::query("PRAGMA user_version = 10")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! SQLite persistence for user-named Overview snapshots (control plane only).
|
||||
use super::Store;
|
||||
use anyhow::{ensure, Result};
|
||||
use sqlx::Row;
|
||||
|
||||
impl Store {
|
||||
pub async fn list_admin_dashboard_presets(
|
||||
&self,
|
||||
username: &str,
|
||||
) -> Result<Vec<(String, String)>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT p.name, p.document FROM dashboard_presets p \
|
||||
JOIN administrator a ON a.id = p.administrator_id \
|
||||
WHERE a.id = 1 AND a.username = ? ORDER BY p.name COLLATE NOCASE",
|
||||
)
|
||||
.bind(username)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
rows.iter()
|
||||
.map(|row| Ok((row.try_get("name")?, row.try_get("document")?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn save_admin_dashboard_preset(
|
||||
&self,
|
||||
username: &str,
|
||||
name: &str,
|
||||
document: &str,
|
||||
) -> Result<bool> {
|
||||
ensure!(
|
||||
document.len() >= 2 && document.len() <= 8192,
|
||||
"invalid dashboard preset size"
|
||||
);
|
||||
// Limit to 16 user-named presets. The cap and upsert are checked in one
|
||||
// SQLite statement, including under concurrent requests.
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO dashboard_presets(administrator_id, name, document, updated_at) \
|
||||
SELECT a.id, ?, ?, ? FROM administrator a WHERE a.id = 1 AND a.username = ? \
|
||||
AND ((SELECT count(*) FROM dashboard_presets WHERE administrator_id = a.id) < 16 \
|
||||
OR EXISTS(SELECT 1 FROM dashboard_presets WHERE administrator_id = a.id AND name = ?)) \
|
||||
ON CONFLICT(administrator_id, name) DO UPDATE SET \
|
||||
document = excluded.document, updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(name).bind(document).bind(crate::auth::now())
|
||||
.bind(username).bind(name)
|
||||
.execute(&self.pool).await?;
|
||||
Ok(result.rows_affected() == 1)
|
||||
}
|
||||
|
||||
pub async fn delete_admin_dashboard_preset(&self, username: &str, name: &str) -> Result<bool> {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM dashboard_presets WHERE administrator_id = \
|
||||
(SELECT id FROM administrator WHERE id = 1 AND username = ?) AND name = ?",
|
||||
)
|
||||
.bind(username)
|
||||
.bind(name)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() == 1)
|
||||
}
|
||||
|
||||
pub async fn rename_admin_dashboard_preset(
|
||||
&self,
|
||||
username: &str,
|
||||
old: &str,
|
||||
new: &str,
|
||||
) -> Result<bool> {
|
||||
// UPDATE OR IGNORE ensures an existing target name is never overwritten.
|
||||
let result = sqlx::query(
|
||||
"UPDATE OR IGNORE dashboard_presets SET name = ?, updated_at = ? \
|
||||
WHERE administrator_id = (SELECT id FROM administrator WHERE id = 1 AND username = ?) \
|
||||
AND name = ? AND name <> ?",
|
||||
)
|
||||
.bind(new)
|
||||
.bind(crate::auth::now())
|
||||
.bind(username)
|
||||
.bind(old)
|
||||
.bind(new)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() == 1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user