From c6486bfe85ecc34b014fffc140bdefd4717d6e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 15 Oct 2026 14:42:56 +0200 Subject: [PATCH] v0.33.4 --- AGENTS.md | 2 +- CHANGELOG.md | 11 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 14 +- ROADMAP.md | 10 ++ SHA256SUMS | 34 ++-- docs/WEB_SERVER.md | 14 +- langs/en_US.json | 4 +- langs/pl_pl.json | 4 +- .../web_observability_0330_browser_test.py | 2 +- scripts/tests/web_server_0330_ui_test.py | 12 +- scripts/tests/web_server_0334_browser_test.py | 154 ++++++++++++++++++ src/admin/openapi.json | 2 +- src/admin/web/style.css | 52 +----- src/admin/web/web-server.js | 39 +++-- 16 files changed, 254 insertions(+), 104 deletions(-) create mode 100755 scripts/tests/web_server_0334_browser_test.py diff --git a/AGENTS.md b/AGENTS.md index b7aa1a4..18599b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for -compatibility. Current source version: 0.33.3. +compatibility. Current source version: 0.33.4. ## Delivery diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ddb562..cbf15d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ > Operator-facing release history. Historical engineering notes are kept in > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). +## 0.33.4 + +- Unify per-Web-Service live metrics with Vhost cards: six matching live metric + tiles in a responsive, always-visible full-width grid instead of a collapsible + section, plus a full-width Vhost-style rate-history chart. +- Refresh visible WWW readings on service-list redraw and continue live status + updates without a disclosure toggle. Keep existing rate sampling, history + limits, PHP capacity semantics, language packs, authentication and API shapes. +- Add simulated browser regression coverage for layout parity at desktop and + phone widths, immediate metrics, refreshed counters and rate-chart width. + ## 0.33.3 - Avoid one-point holes in full-width Overview counter-rate histories and diff --git a/Cargo.lock b/Cargo.lock index c93a8f5..12059b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1881,7 +1881,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.33.3" +version = "0.33.4" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 1a5a7d4..65774c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.33.3" +version = "0.33.4" edition = "2021" rust-version = "1.99" description = "RustProxy - independent reverse proxy with local administration" diff --git a/README.md b/README.md index 415ce24..025c069 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,16 @@ -# RustProxy 0.33.3 +# RustProxy 0.33.4 + +## 0.33.4: unified Web Server live metrics + +- Web Services now display six always-visible live metric tiles using the same + responsive layout, borders, labels and typography as Vhost live metrics. +- Each service's request-rate history uses the same full-width Vhost chart + container, responsive SVG rendering and chart palette. The live metrics no + longer need to be expanded, and values appear as soon as status is available. +- Preserve the existing per-service counters, rate sampling, history limits, + authenticated status stream and PHP availability semantics. No configuration, + database or API changes are required. + ## 0.33.3 sidebar appearance follow-up diff --git a/ROADMAP.md b/ROADMAP.md index 4cf0a9a..36b1586 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,15 @@ # RustProxy roadmap +## 0.33.4 - Web Server/Vhost live-metric visual parity + +- [x] Show per-Web-Service live metrics without expanding a section. +- [x] Reuse Vhost metric tiles and the full-width traffic chart with the same + desktop/mobile layout, chart styling and theme-aware appearance. +- [x] Preserve counter sampling, PHP fallback, existing translations and the + existing status feed without new API or persistence state. +- [x] Add simulated Chromium layout, rendering and sampled-status regressions. + + ## 0.33.3 - Adjustable Overview card widths and cross-type ordering - [x] Apply Appearance **theme** highlighting and corner radius to sidebar diff --git a/SHA256SUMS b/SHA256SUMS index d98fda9..2c70bf0 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,13 +1,13 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore -676a30c813406f4e17972ee8bea175a7e3d5ee282158283a9cbe920f2b65da7f AGENTS.md -3a6bb0fdf41ef4519beefa9f4d9c124a4883c304e49e6a76bb64cd5333ffb174 CHANGELOG.md -1a59436e03172328606fc0c2142da05319e7673ae2a87197409a06aa22d87b27 Cargo.lock -551040f53df829264a6784c9a2ab4fe2cb4299d5d4f517857a74ee58797ab5b7 Cargo.toml +90823b5d38f99f0031cd5fe81248506ed8fc5b0c6f72292bb023e95ed98d9f38 AGENTS.md +d331e6aab4fd331e73fe89bfcb96f57dd502f3b2089d6298139c63b6d12154bc CHANGELOG.md +629a3594e8cffea80603ec4650b91e5f4bea9d65266a1e1122b815ed0d6c5f45 Cargo.lock +e1d3659e9f745d37ff27a8ecdf0233b450127da4410ca51d4b3582a4d92c59bf Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile -6aa95661d76c2ce0f317fe7004791568dbcc552ccff87fe6bdd2f0f235d6ef7c README.md -97becee1b75d0dde90dd412e8fdb8aa68fd11c5e1872bc15095ac6632b2ea0e9 ROADMAP.md +ca0c0f5658f91381c99a91fa3d2daddee82d185e310c8dce037c150c615dbf47 README.md +d02e695435b061b9d29408e72a1335390e549add0dc9ec901f852d982bfd66e2 ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md @@ -70,7 +70,7 @@ cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md fd1c187ea8eef101764670d4ebb0c554173412514e37b909c81ea3f22ba8e7ee docs/VHOSTS.md 09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md -a101fb3f3d8fa1ab50977418dbc16d33d4bf3590f966970e26044c9727045965 docs/WEB_SERVER.md +4257729a4c1b1f3949538410af78a12067bd3417c70525a15bd01411155d25fd docs/WEB_SERVER.md c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md 5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json 2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json @@ -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 -fe738b800824e8700fb8a4796b7316f5d4882e0d43aed7e743fc996d42aa99c1 langs/en_US.json -d9878b53adc3bdcf20fecb6b4ae6270158c9150e9df39186dc2c4b571d948aa2 langs/pl_pl.json +7783e629ca4b2c1614e776838bf90f3692d521e50ee580aa5e92236c92ada844 langs/en_US.json +58f2fdaf0aa9d3a3ce25d47659501a8b3df622e0c08eee0a4baaad4d5f1ac660 langs/pl_pl.json a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql @@ -135,15 +135,17 @@ aa3ca7419d6d7d506c5aaf4d624d20cc7e8ee66bec3cb0cf9b5e7d164bcbab57 scripts/tests/ 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 046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py -91bf803f05b97b4654a6e607de40fccdd0268882039d6d1c5d677edce9459006 scripts/tests/web_observability_0330_browser_test.py +2da8b8d56b6e53b061664c352df0e7a351602c4e460e5f35ce61b738467a5d23 scripts/tests/web_observability_0330_browser_test.py fc982558e5c27281d71c2b3cbc926c415ab9134cf595e7ac12b7214f8a180447 scripts/tests/web_observability_0330_source_test.py -8b600edbe0aff817f871f2bf3821a35ca8d2d29870c0f6b6bf3c0cef2139e221 scripts/tests/web_server_0330_ui_test.py +651330e4bd38d1f2c45d304e5f7461848f414e80de5089358775af4e5eba5b97 scripts/tests/web_server_0330_ui_test.py +17839b4048e055c55b15c46f95e6745d8d87bde17ea0d04e557a85411526e83e scripts/tests/web_server_0334_browser_test.py 22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py 55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs 63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs @@ -170,7 +172,7 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs 74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs 33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs -8e20a424d5e251ac713baaee063d9dfddcfe06063ee68eb9eac37bf9215c7664 src/admin/openapi.json +6884210022056d7f2a92160b6d37026e420483c2285368b7ae3c01b1c981df8b src/admin/openapi.json d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs 30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs 75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs @@ -196,7 +198,7 @@ f3b566d79ba47848021a74653dbd7fec72f0fb819c6b2a07259e047bc8c427fd src/admin/web/ 2f1669cbeb9f49aef43a548e2096825aa14c64c7097ffad520822bbbaefb2b4c src/admin/web/app/overview-extra-metrics.js 5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js -107b010a64ecc4135d610eecf4f8d4fb12329806003d286b4328e31e9e3d859f src/admin/web/app/overview-wide-metrics.js +94ca621117738e064276d426552cc4dfe87ba9b038ddcd73a8d4024c6659a474 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 @@ -204,7 +206,7 @@ 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 -20ff05cfddc7f48931b0e9348b63188f82e8eaec38a7fe3e1829fd2e87301568 src/admin/web/app/web-overview.js +655db0b4d5e7ee14dbe69fa22bdb9d99eecc32abcf3b1b76a5819a8015f72309 src/admin/web/app/web-overview.js 0603dc8793a731a0e582cbbec53aa6d1cc6e007faafcec5d8843730b2cdc39e1 src/admin/web/cache-panel.js 02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js aec5755244242424e5091ff57c0258d2618588618fe1d52d36f081d03879f686 src/admin/web/dashboard-defaults.json @@ -240,7 +242,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/ c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js 8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js -3a869db8f9cdf23eef6c99a52b892f14adf2210bce7f153351341247a878024c src/admin/web/style.css +c02d71ff3e1b63128045d3786f37bb2733bb70bf8d1037929e3cf65cbfc2df4e src/admin/web/style.css d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js 69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js @@ -250,7 +252,7 @@ b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/ 2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js b991e81a98dd604a1d7c30158c0cd8cd825479bd2cd94b082b3550de1071bb58 src/admin/web/vhosts/render.js 4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js -a273847795799679bf15540f7ce34347ff40c013969d09e9ffbf530776d13c0b src/admin/web/web-server.js +0049e5c2a35e3ec25f3c1590879684a7b15165584e96eb1b022fb60d3c548f2c src/admin/web/web-server.js 53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs 775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs diff --git a/docs/WEB_SERVER.md b/docs/WEB_SERVER.md index 5edf9c7..064ab05 100644 --- a/docs/WEB_SERVER.md +++ b/docs/WEB_SERVER.md @@ -22,12 +22,14 @@ The Web Server implementation is split under `src/web_server/` by responsibility ## Live Web Service diagnostics (0.33.0) -Each configured Web Service card now contains an **expandable Live metrics** -section with completed requests, responses/s, HTTP 5xx count, mean elapsed time -until response headers, PHP request permits in use/limit, static/PHP/other response -counts, delivered response body bytes and a compact per-service rate history. -The section reuses the existing authenticated live status stream and is closed -by default. Unconfigured/non-PHP services do not claim PHP worker capacity. +Each configured Web Service card contains an **always-visible Live metrics** +section (since 0.33.4), styled like the Vhost live metrics: six responsive +metric tiles and a full-width per-service response-rate history chart. It shows +completed requests, responses/s, HTTP 5xx count, mean elapsed time until +response headers, PHP request permits in use/limit and delivered response body +bytes. Static/PHP/other counts appear below the chart. The section needs no +expansion and reuses the existing authenticated live status stream. +Unconfigured/non-PHP services do not claim PHP worker capacity. **Overview** provides 12 additional opt-in Web Server blocks under **Settings -> Appearance -> Dashboard layout -> Overview cards**. All 12 are diff --git a/langs/en_US.json b/langs/en_US.json index 94d677d..92d119e 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -2695,10 +2695,10 @@ "webMetricSeriesThreexx": "3xx", "webMetricSeriesFourxx": "4xx", "webMetricSeriesFivexx": "5xx", - "webLiveMetrics": "Live metrics", "webLiveNoData": "Waiting for Web Server metrics", "webLiveRequests": "Responses total", "webLiveRate": "Rate", + "webLiveRateHistory": "Response rate history", "webLiveErrors": "5xx", "webLiveLatency": "Header latency", "webLivePhpCapacity": "PHP capacity", @@ -2710,4 +2710,4 @@ "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." -} +} \ No newline at end of file diff --git a/langs/pl_pl.json b/langs/pl_pl.json index 4e38c46..8d93210 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -2695,10 +2695,10 @@ "webMetricSeriesThreexx": "3xx", "webMetricSeriesFourxx": "4xx", "webMetricSeriesFivexx": "5xx", - "webLiveMetrics": "Metryki na żywo", "webLiveNoData": "Oczekiwanie na metryki WWW", "webLiveRequests": "Odpowiedzi łącznie", "webLiveRate": "Tempo", + "webLiveRateHistory": "Historia odpowiedzi/s", "webLiveErrors": "5xx", "webLiveLatency": "Czas do nagłówków", "webLivePhpCapacity": "Limit PHP", @@ -2710,4 +2710,4 @@ "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." -} +} \ No newline at end of file diff --git a/scripts/tests/web_observability_0330_browser_test.py b/scripts/tests/web_observability_0330_browser_test.py index 58ea942..2209978 100755 --- a/scripts/tests/web_observability_0330_browser_test.py +++ b/scripts/tests/web_observability_0330_browser_test.py @@ -116,7 +116,7 @@ with sync_playwright() as p: envelope = {config:{web_services:[{id:'public-site', name:'Public site',document_root:'/srv/www/public'}],vhosts:[]}}; window.rustProxyWebServer.render(); const details = document.querySelector('.www-service-panel'); - details.open = true; + if (!details || details.tagName !== 'SECTION') throw Error('Missing always-visible WWW metrics'); window.__clock = 7000; window.rustProxyWebServer.renderLive(before, true); window.__clock = 10000; diff --git a/scripts/tests/web_server_0330_ui_test.py b/scripts/tests/web_server_0330_ui_test.py index de8add9..1200268 100755 --- a/scripts/tests/web_server_0330_ui_test.py +++ b/scripts/tests/web_server_0330_ui_test.py @@ -98,7 +98,7 @@ with sync_playwright() as p: assert '/s' in state['top'], state assert state['layoutWidth'] <= state['screenWidth'] + 2, (width, state) assert not errors, (width, errors) - # Exercise the Web Server per-service accordion with the same status, + # Exercise the always-visible per-service metrics with the same status, # without any real HTTP/PHP request or extra polling. details = page.evaluate("""(status) => { envelope = { revision: 10, config: { @@ -110,11 +110,11 @@ with sync_playwright() as p: window.rustProxyWebServer.render(); const detail = document.querySelector('#web-service-list .www-service-panel'); if (!detail) return { missing: true }; - detail.open = true; window.rustProxyWebServer.renderLive(status, false); - return { text: detail.textContent, rows: detail.querySelectorAll('[data-web-live-field]').length }; + return { text: detail.textContent, rows: detail.querySelectorAll('[data-web-live-field]').length, + alwaysVisible: detail.tagName === 'SECTION' && !detail.querySelector('summary') }; }""", second) - assert not details.get('missing') and details['rows'] == 7, (width, details) + assert not details.get('missing') and details['rows'] == 7 and details['alwaysVisible'], (width, details) assert '18 / 32' in details['text'], (width, details) # The first reading after an overdue browser interval must establish a # fresh baseline without adding a fake 0/s dip to the service chart. @@ -129,7 +129,7 @@ with sync_playwright() as p: const during = chart.getAttribute('points'), unavailable = rate.textContent; window.__clock = 103000; api.renderLive(resumed); return { before, during, unavailable, after: chart.getAttribute('points'), resumedRate: rate.textContent }; - }""", [observation(2), observation(3), observation(10), observation(11)]) + }""", [observation(2), observation(3), observation(10), observation(12)]) assert continuity['before'] == continuity['during'], (width, continuity) assert continuity['unavailable'] == '-', (width, continuity) assert continuity['resumedRate'] != '-', (width, continuity) @@ -146,7 +146,7 @@ with sync_playwright() as p: }""") assert old == {'rate': '-', 'status': '-', 'top': 0}, (width, old) assert not errors, (width, errors) - print(f'PASS simulated Web Server cards and service accordion at {width}px: {state["rps"]}, {state["php"]}, layout {state["layoutWidth"]}px') + print(f'PASS simulated Web Server cards and always-visible service metrics at {width}px: {state["rps"]}, {state["php"]}, layout {state["layoutWidth"]}px') page.close() browser.close() print('PASS production JS bundle + all twelve visible WWW widgets (simulated data only)') diff --git a/scripts/tests/web_server_0334_browser_test.py b/scripts/tests/web_server_0334_browser_test.py new file mode 100755 index 0000000..5c94489 --- /dev/null +++ b/scripts/tests/web_server_0334_browser_test.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""WWW/Vhost visual parity in production UI files, using simulated status. + +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 +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() +CSS = (ROOT / 'src/admin/web/style.css').read_text() +ROUTER = (ROOT / 'src/admin/router.rs').read_text() + + +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' / (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) + + +def status(step): + return {'web_server_metrics': { + 'site-A': {'web': { + 'requests': step * 120, 'response_headers_us': step * 2_400_000, + 'status_5xx': step * 4, 'response_bytes': step * 20_480, + 'static_requests': step * 80, 'php_requests': step * 30, + 'other_requests': step * 10, + }, 'php': {'capacity_used': 3, 'capacity_limit': 16}}, + 'site-B': {'web': { + 'requests': step * 24, 'response_headers_us': step * 100_000, + 'status_5xx': 0, 'response_bytes': step * 8_192, + 'static_requests': step * 24, 'php_requests': 0, + 'other_requests': 0, + }, 'php': None}, + }} + + +with sync_playwright() as playwright: + browser = playwright.chromium.launch( + headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox']) + for width in (320, 375, 640, 800, 980, 1440, 1920): + page = browser.new_page(viewport={'width': width, 'height': 900}) + errors = [] + page.on('pageerror', lambda e: errors.append(str(e))) + 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()) + initial = page.evaluate('''(status) => { + document.documentElement.classList.remove('app-loading', 'app-startup-failed'); + document.getElementById('login').hidden = true; + document.getElementById('workspace').hidden = false; + document.getElementById('panel-web-server').hidden = false; + currentPanel = 'web-server'; + envelope = {revision: 10, config: {web_services: [ + {id: 'site-A', name: 'Example service', document_root: '/var/www/a', index: ['index.html'], php: {enabled: true}}, + {id: 'site-B', name: 'Static service', document_root: '/var/www/b', index: ['index.html']} + ], vhosts: [], php_fpm_profiles: []}}; + window.__clock = 1000; + Object.defineProperty(performance, 'now', {configurable: true, value: () => window.__clock}); + latestStatus = status; + window.rustProxyWebServer.render(); + const list = document.querySelector('#web-service-list'); + const cards = [...list.querySelectorAll(':scope > .vhost-card')]; + const metricPanels = cards.map(card => card.querySelector('.www-service-panel')); + const grid = metricPanels[0].querySelector('.vhost-live-grid'); + const chart = metricPanels[0].querySelector('.vhost-chart'); + const svg = chart.querySelector('svg'); + const svgBox = svg.getBoundingClientRect(), svgMatrix = svg.getScreenCTM(); + const rect = node => node.getBoundingClientRect(); + const expectedMetricLabels = ['webLiveRequests','webLiveRate','webLiveErrors', + 'webLiveLatency','webLivePhpCapacity','webLiveBytes']; + return { + cards: cards.length, sectionTags: metricPanels.map(panel => panel.tagName), + collapsibles: list.querySelectorAll('details, summary').length, + liveFields: metricPanels.map(panel => panel.querySelectorAll('[data-web-live-field]').length), + labels: [...grid.children].map(tile => tile.querySelector('span')?.dataset.i18n), + tileClasses: [...grid.children].map(tile => tile.className), + values: [...grid.querySelectorAll('strong')].map(el => el.textContent), + phpMissing: metricPanels[1].querySelector('[data-web-live-field="php"]').textContent, + columns: getComputedStyle(grid).gridTemplateColumns.split(' ').length, + chartWidth: rect(chart).width, gridWidth: rect(grid).width, + cardWidth: rect(cards[0]).width, listWidth: rect(list).width, + svgWidth: svgBox.width, svgContentWidth: svg.viewBox.baseVal.width * Math.hypot(svgMatrix.a, svgMatrix.b), + preserveAspect: svg.getAttribute('preserveAspectRatio'), + stroke: getComputedStyle(svg.querySelector('polyline')).vectorEffect, + chartLabel: !!chart.querySelector('.vhost-chart-head .small-label'), + rendered: getComputedStyle(metricPanels[0]).display !== 'none' && rect(chart).height > 60, + fitsViewport: document.documentElement.scrollWidth <= innerWidth + 2, + expectedMetricLabels, + }; + }''', status(1)) + expected_cols = 2 if width <= 640 else 3 if width <= 980 else 6 + assert initial['cards'] == 2, (width, initial) + assert initial['sectionTags'] == ['SECTION', 'SECTION'], (width, initial) + assert initial['collapsibles'] == 0, (width, initial) + assert initial['liveFields'] == [7, 7], (width, initial) + assert initial['labels'] == initial['expectedMetricLabels'], (width, initial) + assert initial['tileClasses'] == ['vhost-live-metric'] * 6, (width, initial) + assert initial['values'][0] != '-' and initial['values'][4] == '3 / 16', (width, initial) + assert initial['phpMissing'] != '-', (width, initial) + assert initial['columns'] == expected_cols, (width, initial) + assert initial['chartWidth'] > 0.95 * initial['gridWidth'], (width, initial) + assert initial['listWidth'] - initial['cardWidth'] < 3, (width, initial) + assert abs(initial['svgWidth'] - initial['svgContentWidth']) <= 2, (width, initial) + assert initial['preserveAspect'] == 'none' and initial['stroke'] == 'non-scaling-stroke', (width, initial) + assert initial['chartLabel'] and initial['rendered'] and initial['fitsViewport'], (width, initial) + + sampled = page.evaluate('''([first, second, last]) => { + const api = window.rustProxyWebServer; + const panel = document.querySelector('[data-web-live-id="site-A"]'); + const rate = panel.querySelector('[data-web-live-field="rate"]'); + window.__clock = 4000; api.renderLive(first, true); + window.__clock = 7000; api.renderLive(second, true); + const before = {rate: rate.textContent, points: panel.querySelector('polyline').getAttribute('points')}; + // Service-list redraw must show the current status without opening anything. + latestStatus = last; + api.render(); + const updated = document.querySelector('[data-web-live-id="site-A"]'); + const total = updated.querySelector('[data-web-live-field="requests"]').textContent; + const split = updated.querySelector('[data-web-live-field="split"]').textContent; + return {before, total, split, + pointsAfterRedraw: updated.querySelector('polyline').getAttribute('points'), + metricsRemainVisible: !!updated.querySelector('.vhost-live-metric'), + overflow: document.documentElement.scrollWidth - innerWidth}; + }''', [status(2), status(3), status(4)]) + assert sampled['before']['rate'] not in ('-', '0/s', '0.0/s'), (width, sampled) + assert sampled['before']['points'].count(' ') >= 1, (width, sampled) + assert sampled['total'] != initial['values'][0], (width, sampled) + assert sampled['split'] != '-' and sampled['metricsRemainVisible'], (width, sampled) + assert sampled['pointsAfterRedraw'] == sampled['before']['points'], (width, sampled) + assert sampled['overflow'] <= 2, (width, sampled) + assert not errors, (width, errors) + print(f'PASS 0.33.4 WWW live metrics visual parity at {width}px; {expected_cols} columns') + page.close() + browser.close() +print('PASS always-visible full-width WWW/Vhost parity with simulated updates (not Rust runtime)') diff --git a/src/admin/openapi.json b/src/admin/openapi.json index 39fb178..b5863dd 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.33.3", + "version": "0.33.4", "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": [ diff --git a/src/admin/web/style.css b/src/admin/web/style.css index f63f96a..e5dd73a 100644 --- a/src/admin/web/style.css +++ b/src/admin/web/style.css @@ -11588,62 +11588,22 @@ 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 { - border-top: 1px solid var(--line); - margin-top: 10px; - padding-top: 8px; -} - -#panel-web-server .www-service-panel summary { - cursor: pointer; - color: var(--chart-accent); - font-size: 12px; -} - -#panel-web-server .www-service-live { - padding: 12px 0 3px; -} - -#panel-web-server .www-service-values { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); - gap: 10px; -} - -#panel-web-server .www-service-values>div { - display: grid; - gap: 5px; min-width: 0; + margin-top: 10px; } -#panel-web-server .www-service-values small { - color: var(--muted); - font: 10px var(--mono); -} - -#panel-web-server .www-service-values strong { - font: 500 14px var(--mono); - overflow-wrap: anywhere; -} - -#panel-web-server .www-service-live svg { - display: block; - width: 100%; - max-height: 86px; - margin: 10px 0 0; -} - -#panel-web-server .www-service-live polyline { - fill: none; - stroke: var(--chart-accent); - stroke-width: 2; +#panel-web-server .www-service-heading { + margin: 0; + font-size: 11px; } #panel-web-server .www-service-split { color: var(--muted); font: 11px var(--mono); overflow-wrap: anywhere; - margin-top: 4px; + margin: 4px 10px 8px; } @media(prefers-reduced-motion:reduce) { diff --git a/src/admin/web/web-server.js b/src/admin/web/web-server.js index b0b2740..7a51acc 100644 --- a/src/admin/web/web-server.js +++ b/src/admin/web/web-server.js @@ -286,7 +286,6 @@ try { const preset = presetConfig($("web-service-preset").value); if (!preset) { toast(t("webServicePresetChoose"), true); return; } - // Keep deployment-specific FastCGI endpoint/tuning while replacing application policy. preset.php.endpoint = $("web-service-php-endpoint").value.trim() || preset.php.endpoint; preset.php.connect_timeout_ms = Number($("web-service-php-connect-timeout").value || preset.php.connect_timeout_ms); preset.php.request_timeout_ms = Number($("web-service-php-request-timeout").value || preset.php.request_timeout_ms); @@ -312,9 +311,6 @@ function presetChanged() { updatePresetSummary(); - // Presets are one-shot form generators. Selecting one must immediately - // materialize its normal Web Service fields; otherwise a user can select a - // CMS, save the form and unknowingly persist the previous/custom values. if ($("web-service-preset").value !== "custom") applyPreset(); } @@ -392,31 +388,34 @@ item.append(el("span", t(key), "small-label"), el("b", value)); details.append(item); } - const live = el("details", undefined, "www-service-panel"); - const summary = el("summary", t("webLiveMetrics")); live.append(summary); + const live = el("section", undefined, "www-service-panel"); + const panel = el("div", undefined, "www-service-live"); panel.dataset.webLiveId = service.id; - const figures = el("div", undefined, "www-service-values"); + const figures = el("div", undefined, "vhost-live-grid"); for (const [field, key] of [ ["requests", "webLiveRequests"], ["rate", "webLiveRate"], ["errors", "webLiveErrors"], ["latency", "webLiveLatency"], ["php", "webLivePhpCapacity"], ["bytes", "webLiveBytes"]]) { - const item = el("div"); - const name = el("small", t(key)); - name.dataset.i18n = key; + const item = el("div", undefined, "vhost-live-metric"); + const name = el("span", t(key)); name.dataset.i18n = key; const value = el("strong", "-"); value.dataset.webLiveField = field; item.append(name, value); figures.append(item); } - const chart = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - chart.setAttribute("viewBox", "0 0 300 60"); chart.setAttribute("role", "img"); - chart.setAttribute("aria-label", t("webLiveRate")); + const chart = el("div", undefined, "vhost-chart"); + const chartHead = el("div", undefined, "vhost-chart-head"); + chartHead.append(el("span", t("webLiveRateHistory"), "small-label")); chart.append(chartHead); + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 320 54"); svg.setAttribute("preserveAspectRatio", "none"); + svg.setAttribute("role", "img"); svg.setAttribute("aria-label", t("webLiveRate")); const line = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); - line.setAttribute("points", "0,55 300,55"); chart.append(line); + line.setAttribute("class", "vhost-chart-line"); + line.setAttribute("points", "0,50 320,50"); svg.append(line); chart.append(svg); const split = el("p", "-", "www-service-split"); split.dataset.webLiveField = "split"; - panel.append(figures, chart, split); live.append(panel); - live.addEventListener("toggle", () => { if (live.open && latestStatus) renderLive(latestStatus, false); }); + chart.append(split); panel.append(figures, chart); live.append(panel); box.append(details, live); return box; }); $("web-service-list").replaceChildren(...(cards.length ? cards : [el("p", t(query ? "webServiceSearchEmpty" : "webServiceEmpty"), "fine")])); + if (latestStatus && currentPanel === "web-server") renderLive(latestStatus, false); } const serviceHistory = new Map(); @@ -425,7 +424,6 @@ if (currentPanel !== "web-server") return; const services = status?.web_server_metrics || {}; for (const panel of document.querySelectorAll("#web-service-list [data-web-live-id]")) { - if (!panel.closest("details")?.open) continue; const id = panel.dataset.webLiveId, values = services[id], web = values?.web; const lookup = key => panel.querySelector(`[data-web-live-field="${key}"]`); const set = (key, value) => { const node = lookup(key); if (node) node.textContent = value; }; @@ -439,11 +437,12 @@ const deltas = record ? sample.deltas : null; const rate = deltas ? deltas[0] / sample.seconds : null; const previous = serviceHistory.get(id) || []; - // An unavailable counter delta is not a 0/s measurement; do not plot it. if (record && rate !== null) { previous.push(rate); window.rustProxyOverviewSamples.trim(previous); serviceHistory.set(id, previous); } const peak = Math.max(1, ...previous.filter(x => x != null)); - const points = previous.map((value, i) => `${300 * i / Math.max(1, previous.length - 1)},${55 - (value || 0) / peak * 50}`).join(" "); - panel.querySelector("polyline")?.setAttribute("points", points || "0,55 300,55"); + const y = value => 50 - (value || 0) / peak * 46; + const points = previous.length === 1 ? `0,${y(previous[0])} 320,${y(previous[0])}` : + previous.map((value, i) => `${320 * i / Math.max(1, previous.length - 1)},${y(value)}`).join(" "); + panel.querySelector("polyline")?.setAttribute("points", points || "0,50 320,50"); set("requests", fmt(total)); set("rate", rate == null ? "-" : `${fmt(rate, 1)}/s`); set("errors", fmt(n(web.status_5xx))); set("latency", deltas?.[0] > 0 ? `${fmt(deltas[1] / deltas[0] / 1000, 1)} ms` : "-");