This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 14:42:56 +02:00
1 parent de090d20fa
commit c6486bfe85
16 files changed
+254 -104

No files matched your search

+1 -1
View File
@@ -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
+11
View File
@@ -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
Generated
+1 -1
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.33.3"
version = "0.33.4"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -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"
+13 -1
View File
@@ -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
+10
View File
@@ -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
+18 -16
View File
@@ -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
+8 -6
View File
@@ -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
+2 -2
View File
@@ -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."
}
}
+2 -2
View File
@@ -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."
}
}
@@ -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;
+6 -6
View File
@@ -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)')
+154
View File
@@ -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)')
+1 -1
View File
@@ -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": [
+6 -46
View File
@@ -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) {
+19 -20
View File
@@ -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` : "-");