This commit is contained in:
Mateusz Gruszczyński committed 2026-10-06 10:16:16 +02:00
1 parent 32e6875980
commit cf25423dde
68 files changed
+2314 -422

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.26.2.
compatibility. Current source version: 0.26.3.
## Delivery
+20
View File
@@ -4,8 +4,28 @@
> test-only changes and packaging implementation notes were moved to
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.26.3
- Make the **Settings -> Appearance** interface scale global across the authenticated administration UI, including dialogs, overlays and other top-level surfaces, and extend the selectable range from 80-125% to 70-125%. Form fields also receive a bounded proportional size adjustment so they track the selected scale more visibly.
- Reduce padding and typography in the Overview telemetry cards so the dashboard stays compact at wide resolutions while retaining the existing responsive layout.
- Expand chart presets from four to eight: Default, Soft, Technical, High contrast, Neon, Monochrome, Warm and Cool.
- Make chart presets alter the full presentation palette: line colors, chart surfaces, grid treatment, line weight/caps and contrast now change together across overview diagnostics, vhost/stream charts and Traffic donut/bar charts.
- Restyle the session draft dock as a square-corner RustProxy panel with ordinary panel surfaces, thin separators and compact rows instead of a rounded blurred floating card.
- Let the draft dock collapse to a single title bar and show the age of each suspended editor draft, while keeping drafts session-only and independently restorable/discardable.
- Add a Settings search field that filters settings categories by their translated label and page content without rebuilding forms or discarding unsaved values.
- Add color-palette selectors beside chart presets so the visual character of each chart theme is visible before/while selecting it.
- Add independent Normal/Compact/Ultra compact density controls for tables/logs and for Overview telemetry cards; these are separate from the global 70-125% interface scale.
- Make the Overview telemetry row configurable per administrator: cards can be hidden, restored and reordered without changing runtime telemetry collection.
- Add per-section Appearance reset actions for Interface, Charts, Tables/Logs and Dashboard, while retaining the full Appearance reset and preserving unrelated account-scoped preferences.
- Add an account-scoped **Button style** preference: Normal keeps the refreshed control sizing, while Compact restores the smaller source-style button dimensions without changing global interface scale. Apply it consistently to generated vhost actions and tabs inside dialogs.
- Keep appearance data in the existing account-scoped administrator preference JSON; SQLite schema 8 and the saved proxy configuration format are unchanged, so no data migration is required from 0.26.2.
## 0.26.2
- Add a bottom-right draft dock for unfinished Web Server, Vhost, Redirect and Stream editors. Closing a dirty editor minimizes its exact in-session state instead of leaving stale modal state or blocking a new editor; drafts can be restored or discarded independently.
- Add **Settings -> Appearance** with account-scoped theme, automatic/explicit language, 80-125% interface scale, 12/24-hour time display and chart-style presets. These preferences are stored in the existing administrator preferences row.
- Apply the selected time format across dashboard timestamps and make expanded Stored Logs records more compact for long entries.
- Combine Stored Logs and Live Logs under one top-bar **Logs** menu.
- Replace the Stored Logs filter matrix with an optional source selector plus a single field-query search supporting `field:value`, explicit `AND`, `!` negation, quoted values, wildcards and numeric comparisons.
- Build selectable Stored Logs columns dynamically from JSON fields instead of fixed `matched_by`/SNI protocol columns; saved account preferences can retain discovered fields.
Generated
+1 -1
View File
@@ -1688,7 +1688,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.26.2"
version = "0.26.3"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.26.2"
version = "0.26.3"
edition = "2021"
rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration"
+23 -6
View File
@@ -1,9 +1,9 @@
# RustProxy 0.26.2
# RustProxy 0.26.3
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
**0.26.2 release source.**
**0.26.3 release source.**
## Layer-4 streams
@@ -11,7 +11,24 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
## What changed in 0.26.2
## What changed in 0.26.3
- Make interface scaling truly global: the 70-125% setting now scales the authenticated dashboard, navigation, dialogs, overlays, draft dock and other top-level administration UI consistently. Form fields receive an additional proportional size adjustment so inputs/selects/textarea do not remain visually oversized at low scale or undersized at high scale.
- Tighten the Overview metric cards so wide dashboards fit more telemetry with less empty vertical space.
- Expand chart presentation presets to Default, Soft, Technical, High contrast, Neon, Monochrome, Warm and Cool. Presets now alter palette, surface treatment, grid character, line weight and chart contrast instead of changing only stroke shape.
- Restyle the bottom-right draft dock as a square-corner panel that follows the existing RustProxy card/navigation language instead of using a rounded glass/floating treatment.
- Let the draft dock collapse to one bar and show the age of every suspended draft.
- Add Settings search across translated section labels and settings content, per-section Appearance resets, and a full Appearance reset.
- Add visual palette buttons for all chart presets, independent table/log density, independent dashboard density, and an Ultra compact option for both data tables/logs and Overview cards.
- Add a per-user Button style selector: **Normal** keeps the refreshed buttons and **Compact** restores the smaller source-style button sizing independently from interface scale. The selection now applies consistently to generated vhost actions and tab buttons inside dialogs.
- Let administrators hide/show and reorder the Overview telemetry cards from Appearance without changing metric collection.
- Keep all new appearance choices account-scoped in the existing SQLite administrator-preferences document; schema 8 and the saved proxy configuration format remain unchanged.
0.26.3 includes the 0.26.2 administration workflow improvements below:
- Add session-only editor drafts for Web Server, Vhosts, Redirects and Streams. Closing an edited form minimizes it into a bottom-right draft dock, while opening another item starts a clean independent editor.
- Add **Settings -> Appearance**. Theme, language, interface scale, 12/24-hour time display and chart style are account-scoped preferences stored in SQLite; the scale slider now spans 70-125%.
- Apply time-format preferences to dashboard timestamps and compact expanded Stored Logs details to reduce scrolling on large records.
- Merge the Stored Logs and Live Logs top-bar actions into one **Logs** menu.
- Replace the Stored Logs filter row with an optional log-source selector, one query field and page-size control. Search accepts Kibana-style field clauses such as `req: GET AND !country: Poland`, quoted values, wildcards and numeric comparisons.
@@ -22,7 +39,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
- Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`).
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration.
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.2 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.3 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
## Documentation
@@ -100,7 +117,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
```
The 0.26.2 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
The 0.26.3 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
updates only this package's own version entry. This packaging environment has no Cargo, so the
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
@@ -339,7 +356,7 @@ python3 scripts/release_smoke.py
# Runs those checks first, then the Rust suite when Cargo is available:
bash dev.sh test
# Source-only package, with no Git checkout required:
python3 make_zip.py /tmp/RustProxy-0.26.2.zip
python3 make_zip.py /tmp/RustProxy-0.26.3.zip
```
## New management and test-origin guide
+24 -1
View File
@@ -1,8 +1,31 @@
# RustProxy roadmap
## 0.26.3 - Administration appearance and draft workflow polish
Current direction: 0.26.2 -> 0.26.3
- [x] Make interface scaling global across navigation, workspace, dialogs, overlays and the draft dock; extend the range to 70-125% and make form-control sizing respond more visibly across that range.
- [x] Make Overview telemetry cards denser without changing responsive breakpoints.
- [x] Expand chart themes to eight full-palette presets affecting colors, surfaces, grid and line treatment.
- [x] Restyle the draft dock as a square-corner RustProxy panel without rounded/glass styling.
- [x] Add a collapse-to-bar state and per-draft age indicator to the session-only draft dock.
- [x] Add Settings search without rebuilding forms, preserving unsaved values while categories are filtered.
- [x] Add chart-style palette previews beside the eight chart presets.
- [x] Add independent Normal/Compact/Ultra table-log density and Overview dashboard density preferences.
- [x] Add per-account Overview card visibility and ordering controls.
- [x] Add per-section Appearance resets plus a full Appearance reset while keeping unrelated account preferences intact.
- [x] Add an account-scoped Normal/Compact button-style selector, with Compact matching the smaller source-style control dimensions independently from global scale, including generated vhost actions and modal tabs.
- [x] Retain sticky Settings category navigation and unsaved-category indicators.
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.2.
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
## 0.26.2 - Unified logs search and dynamic fields
Current direction: 0.26.1 -> 0.26.2
Completed direction: 0.26.1 -> 0.26.2
- [x] Add independent in-session editor drafts for Web Server, Vhosts, Redirects and Streams, with minimize/restore/discard actions in a bottom-right dock.
- [x] Consolidate theme/language/display controls under **Settings -> Appearance**, replace density modes with an interface-scale slider and persist appearance choices in administrator preferences.
- [x] Add 12/24-hour timestamp formatting, chart presentation presets and a denser expanded Stored Logs record view.
- [x] Merge Stored Logs and Live Logs into one top-bar Logs menu.
- [x] Replace fixed Stored Logs filters with one bounded field-query syntax supporting AND, negation, quotes, wildcards and numeric comparisons.
+67 -63
View File
@@ -1,17 +1,17 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
f927259ddc3f564e17884b2f2cdf0e060b2d8e921efc41739bff39c7c8fd186b AGENTS.md
1b2622333d04fbf440aef8cdadbb59fc133d98a1ed5a12f6d2df7ecfffcea526 CHANGELOG.md
4a2e8f63e0c5d72f18f636c6e8eea4018cadd14340bdd44e0aeaab8f931488b7 Cargo.lock
4fee895e239bcfe93b945101dc556cf06189e960610da2ba4520c54eeea5b040 Cargo.toml
78bfdaf9ea3b88e84a6acf6720dcef80c4b3f4eaab233ed202d17c33393c7e5f AGENTS.md
8a4a00a320fece83cf042bcd9145da98b8951d76d3e2bd191ab6a8fd668efbb5 CHANGELOG.md
d1b0cf138814b68b272c32616aac2334699f906b0b43489601f21cf36b513088 Cargo.lock
61d358ab03a30e1425bb91f84b70221b8b39c5fd6324475ec098c5b1f53ac959 Cargo.toml
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
7142091e55af5998d73cab966b50563f0304c7bd13bdd94c7d24208075240922 README.md
0c0e9cb5583e389b393aac6d5fcf9269ee3c0dd8ef41fd6c02f74aff9c9090dc ROADMAP.md
612e0d17a2fcda2a5799a44d8fd099ab285c284c1327fb84c1475d8e71ec116c README.md
6aab906375937d1d0ccea95280a6d1efa41089b8d150278e1d73ca50dfb7dfc2 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
e42acfb9e917d6e04ea6692f2a8512c3856d1aabdf96d4a17efaa0b2d49e187b debian/README.md
bcf9fba9f796a7051a47bace3b6ff6d090f72dffe02642181bd828692e208988 debian/build-package.sh
f8cc5f463b4642faadfa2bada5fb09298b6947c7284d938ae3ba4db12cb448ee debian/README.md
fe510e22d31893d4df86de9bb48196dfed31ecb16d5c7649bb1fdba246fd49ce debian/build-package.sh
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
b2579151f48cd68a751d8d99043de2a1b78cfaa51514fc6becf1223f2decb144 debian/changelog
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
@@ -24,7 +24,7 @@ cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustpro
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
999c0141133436be1ca8757a7a7570c406c63e89af8ecf1b587ba86a84b23695 dev.sh
4e766ffd22c9a096890a2555a7eac0ebafbe0599e3fc462413c1b782c393e450 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -33,14 +33,14 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
7706c216c99152c5c707ebe8c7bc9e9d900208e79cfd4f29dcade9549f24b791 docker-agent/model.go
1803047c9588c2892b6846072ca23d7db20f09d35591c47f1460beb828e47de9 docker-agent/model.go
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md
3056b3db4b69d65db8392b2f6328b52bc3dcdb5962cea677c3e66f347b99653d docs/API_ACCESS.md
35d798657697a8031cac5efd417b6bb8de819dd7d4e79666e61d1805d8545d6e docs/API_ACCESS.md
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
9cb338ed07ac1f92ad491e2099a7e3af63e66262417b1fef24d98c6a791d13fa docs/CACHE_DIAGNOSTICS.md
@@ -49,9 +49,9 @@ db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md
613e844c12e32be23c46e8d88c081bf8d2c0395ea53dbd4d725f9d07e21b7a60 docs/FILE_LOGS.md
1594b5eee60bace5111d2a7ea3d59f6d6a479d8515c2e9256c52387c64cf18cd docs/FILE_LOGS.md
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
0e8e7c9291555b0f31a7af0d4826cc2dfbf33cbc42332e69360caeb4bc349768 docs/INSTALL.md
5c753bf5de6709b8dd55ac449a4ef38757d9c7c8b55fbe1f5fd9b9caf999ec58 docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
@@ -62,7 +62,7 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
3f8169931302a4abf0a65109ff98c2b9154f06ac39d4c08e2ab89dfeacfb39bc docs/README.md
cb8ebf15c59eacd4d5567b98c13101a1fd225d43c6dd5586100727fd00917fea docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
@@ -70,9 +70,9 @@ ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.m
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
4245408286d8bdc3bed585c2f0928ecd4c69a3e48dfacbac9b952079ec2075b1 docs/SETTINGS.md
f8baa8bdc8a67bf4e4a6e6d233435f5a43075a09bf08b08b597fcef1853ce5c5 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
ac2815cb3cd746a228cc489699d0b2031056750cfd744bb30e48aa550b3b515e docs/TEST_REPORT.md
c8eeed55e34d1a33a2d9d7ea61569eec786060b4f9d8342ea39b2eeaa1be9116 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
@@ -102,6 +102,7 @@ d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_A
bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_AUDIT.md
05e0b4cc074b07ff37d45fabc20c7272bada217286b3db3d7997cae25c1f6f11 docs/V0.26.1_IMPLEMENTATION.md
91774f2c23f587a502c65fe2ffaf009aba81b221cb3ede4861ea79abb4d4a528 docs/V0.26.2_IMPLEMENTATION.md
57edc3d5ca6c268b2b6b2e8027e81fb543c0dab86e8f69c4ee5776ea8df901c7 docs/V0.26.3_IMPLEMENTATION.md
ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md
b979afd3d56731eeb06e9dc38d37730693e7fbf3aa78467932842c0e712e7b26 docs/VHOST_ACCESS.md
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
@@ -116,14 +117,14 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
ca2142afb74a3f2434815f6f0ba8b0859f67c8740d98e25e15ec3997a74d0220 grafana/README.md
6baa1cf4a0a1894a09214a591472aedb5880e6311d6df41083d9c71f68bbbbb1 grafana/rustproxy-overview.json
ce0094ff903e0fcc979efbcd0a79d7aeb6d59eb8aa212c8c31fdbe8c742fdd12 grafana/README.md
e93062ce0b61ddc6ee46f868c7de80a8ae00dfbe23542a887c9d096dadbbfd47 grafana/rustproxy-overview.json
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
71af021055dd5affbc353f51126342c81b6ac91180e94300cf989e9c1032da22 langs/en_US.json
efce73aa41a21cae9debe335d7fb89245f73d132d2dfe4c97421d7241a7a77f9 langs/pl_pl.json
24c8fe7bfb42652a63c4414e38fb8ef0c89656b9045e1a3c404856de1f77efbe make_zip.py
17e39fd69378d4fdad965dcce96006393c8d2469b91d78611b73aacbd6a4ad74 langs/en_US.json
f67f061e72bc14ebfcf883fab8c10e625e399157017e696e4a55cc26854cff08 langs/pl_pl.json
089cd7c39e6e3c79f5e02b7ffe38648cef6b5cb89cc5ac6d6e7e249f0f91ee0b make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
@@ -142,11 +143,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
80261fe4b453c1fa7724700fa6ff8b838bc63379b193356e3cc8053739fc95ce scripts/check_source.py
2f91b721757d75e88fdef4cdfd1fcb87a56f95b4470cbb8b86894fb23d6976c7 scripts/check_source.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
5b87be7439dd561eb8dd45681a60f63a2f752325db8eb7ee35258de0e0543236 scripts/deploy_core.py
39932ae4797c966703fce9e2f999891066f7c3541dd171f02d2e37e88be2ef0d scripts/deploy_core.py
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
674968e62b14f818c1e68c49b9c9bde4fa75f885ac7e1f4fe269e542c52538bf scripts/docs_check.py
fab405bd0bfa9a8f8f70462aa536b4fc8ac9976e6bab25751726309ec679c6f7 scripts/docs_check.py
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
@@ -156,8 +157,8 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
b65c46669e1d07bdb64ddaff2c9a07b61de3c34db1591f2e024ac97c60f227ba scripts/preflight.py
705311b16b803bcaa7644478c03f163f31c4ec08b34efd140fada249b20f2a4b scripts/release_smoke.py
d3c8a25c1b80eb00df494a1a8dab4a6325dd0a7b6e0d9f07e5bbfa511e9eb500 scripts/preflight.py
31f0a16da2d97740be5130a76b128b6de617c21f1a66990f1c6192e1a72c2ec9 scripts/release_smoke.py
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
@@ -168,13 +169,13 @@ d36a5f5532f7d458b4e8249acf0fdf733b3c9531e9a590ea8d1b1dddec8fa7ef scripts/rust_m
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_source.py
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
a65010f828194885ffc7f0f733937957f28e6049cd978bec1380a2b8bfbd0113 scripts/source_module_contracts.py
c5d1a2c36bb8649d93ad9c40bfd542d21109234ebc87405b7d81632c85f71c84 scripts/swagger_assets.py
6fefb5c459aacdef3dcabe9c5e6e7718a2145a0190b165c29d7a46216ac53bad scripts/source_module_contracts.py
44d2b525cc7caf8fcfa4db67e3ec6b550bcb48623893acf4bacf0fb6b0d3bf60 scripts/swagger_assets.py
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
dc6d4cdfbd08150d45c04ed2c17f90f72cc2bae6f0a40a5917b4072fc60c8605 scripts/upgrade.py
43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py
29abb682a3dad39c4df3d9d6f0874e95c631144eb2e246970d54c5f15bbe4c61 scripts/web_server_contracts.py
b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs
d5937dca7ae4e360c6d9bdbd2386f52826d0e5554d0db9abd617bab9cefb80fb scripts/upgrade.py
0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f scripts/web_assets.py
0b3556f0cbaa4fe9025634ebac49a6eeea7151879fa7c72419432033b5375b28 scripts/web_server_contracts.py
ced042b5e283fd40fe25c261457dceb57d946dffebd89876cd80d69b92823932 scripts/web_service_ui_test.mjs
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
4e3be1ea1be0a81529c7c9021fb9bb77c24e5044acd54094972745443ad2c26a src/acl/tests.rs
992126cb77249a6a173a71b2e5387f218b987545a04b26bc0340f922b4600ec1 src/acme.rs
@@ -186,38 +187,41 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
c96ddc9befca3834d1bf29ffc7b92b9bab9e36abec42020df59196d960d6884a src/admin/documentation/documents.rs
e4aac0947a38d0cf6160328db853df940546a7b6b758fea4a33eedaf56f9f791 src/admin/documentation/documents.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs
d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
1c2176d3db666e1780372534cdc0c83aa29c665265d7e3de2aaf1d42bc5d5aee src/admin/openapi.json
20c73fc2d6cc0efda6016d2a9830815bea6882faf23f5a3fdb23bb1af67e48f7 src/admin/openapi.json
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
f8ced8092063785faa64c744625412f103447ead249d8f50f5388591841b5a56 src/admin/preferences.rs
e9ea015aabdfdbb2f8dd72ea734002b5d648dc1bbef4cc4a0b85d2bcc059e9a5 src/admin/preferences.rs
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
d246389975c87d125ebcb8c24282a3729d509cc70e13e0f3b56b4008d80d76b4 src/admin/router.rs
684d5d28e0d2b10fb2baf2a56857b226d6334cc3346e1ca86de173ff914bee73 src/admin/router.rs
333d5f9f4c27941c535ac689e5826264ffa2bb86b00832b597a9bb9f811f01e8 src/admin/session.rs
f7938b202919a49fd2ff86ecdf09efadae12e0ffc0a4246a72a76218db11aa88 src/admin/status.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
e5f973c6fbddc3897bc0e6d9f8e70cbe9279acea6161ffb46e013792e845e1e4 src/admin/tests.rs
59ea2bb773c86c3d8240ccc212a8bf7f670e17e360def9e773906469ef135ac5 src/admin/tests.rs
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs
77608ad33da348dbea41d2a7ceadb75b0bcf05336cb5a20b7f2a47502f1e4842 src/admin/tests/part_03.rs
60ad5c76db3cd6fdd144d504a7b50153e133ab357e49c8ebadb99c1c8bce562c src/admin/tests/part_04.rs
bf1757ec16c144c7791c8e08ac2c93118aa53a88209315a6928a63ef2aaeec31 src/admin/tests/part_04.rs
5f0062abc42a3b81c79088dc0824ab4c81a22c6ea765e77d2527d07d13bffd58 src/admin/tests/part_05.rs
f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/tests/part_06.rs
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
78e7d54d6536cd600dcbe66f0fc06ec109e121dfee1e57f3ca6e61cc6ae04a6a src/admin/web/app/config-history.js
b47ce2fe8d139c1c7007a539e120324ccb3c8fdd3cd3fd39d0ac365f7372edaf src/admin/web/app/core.js
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
5c4256e97517d7cb38e0c985da150631f6ca1bed9c2030f2d21a02bd28436f5f src/admin/web/app/settings.js
d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js
5fa006413fd93529ac9aaa4ffb28bf6b0b88d0a55a0bf0c49576db4a06fe9fc3 src/admin/tests/part_07.rs
9afbca5b4f3d17690c2a7aedf0fd08de06e04c5e492b151d9969a326e07b19b8 src/admin/web/acme.js
446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js
77db5269365ee387f4e83d08e665bc1939586676431dc9153f874267b3613100 src/admin/web/app/core.js
520de5c5df22aa0c26d472e83533ca288cc71ca0f75db62912b2c2fb7ab1f1c3 src/admin/web/app/preferences.js
d7f6675277c927f4eae1b83c7aa91b315429e7034dfe61c489c2f737a9c19f07 src/admin/web/app/runtime.js
7642150e62978fb7dc1362b862a3805ee2e4f764a5a31767226825d32097c62a src/admin/web/app/settings.js
642d013e2e18dc8a8eccc7a2fe93d6570521dc9a0644beac7af4bbe8c5cea1ee src/admin/web/cache-panel.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
d4aef376cb44b2a13174dca1cb9383f64c04b0b93279f4f0b4805e76002548a1 src/admin/web/docs.css
60a4944a0a24b889296991b8af44b467f776fc85ec3597eb2e2940ef1f6fc713 src/admin/web/docs.html
9a8f19f3298769c2c5589cabc6621d73a05ec9f350c21b33e1257ad506c12aab src/admin/web/docs.js
6adb291fcc98d516fd9745d9673db16e408854a32cbf41335ae956bda11d46de src/admin/web/docs.js
4356ef9c862bd1095ab396b644aee227be51a2a3c960cc3fa1a02eafbc712429 src/admin/web/drafts.js
232b766f278ae4424705af351910677fcd7ac9a20bb8320d9a12b763c91219a2 src/admin/web/error_401.html
16293363c7848471d866092fec5e83023df087aa824989e87450cee7b7ca1263 src/admin/web/error_403.html
f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/error_404.html
@@ -225,24 +229,24 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
efefd94e021e7846cc0d9cd98a8ca0c605bb00e209aa33ecd54201ccdb3bef4b src/admin/web/file-logs.js
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
9321d491cede2438b9a9e9beb82f5886e7e0459d36fba64a20cda3d5779f64e1 src/admin/web/index.html
ab1726532cae53df4e653d80c4855ccad069fbe0446c513492c64cede361b69d src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
dc092f7120b6502e7d322b5f0953f34a131b7af7d1476207a934605ee65f3968 src/admin/web/login.js
44939a885a00d6a10d5be23b03c9552567010ec66c9e015b2bc7dc28a7e53000 src/admin/web/management.js
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
668f5162fa9f8bbe2fefce4115644a6469e01aa7defc9c7f760c28afd8e2afc7 src/admin/web/settings.js
1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js
ed3b9b327cde32bfd2b7861977b28b5d76298badd70ffb4407db7b6ca794b557 src/admin/web/style.css
47e98065281c04b4c185d53df4ed0cb12fabc716e52143e6ae3c3c25db62126a src/admin/web/traffic-panel.js
d818580e61bbb70da1f758b9231c0667639cd947d875a55adc21dcf9ed1319c2 src/admin/web/transform-panel.js
ac79a0f5471f110850bf5ceaaee9a3903bb92fb2edc10e969d8fda81f66571c0 src/admin/web/vhosts/core.js
45ca44ca82e43373f018629839287645218f225747f02b9b4a5d80e2bc8821cf src/admin/web/login.js
1b096d96a9d6b904a87bd4194fa40f22310a42c765f3ffa299c4da1f14c6e248 src/admin/web/management.js
e65311ad425bd91364ee8c0cc3f35671e1bcc726bea533e4a10d9e1f57706e12 src/admin/web/redirects.js
f1ab20afa09f75f3635511756e36afea46a7917f04937d6f4e89c4e9f3f43204 src/admin/web/settings.js
f25a4395dc3ba7bf152fffb577aedfbf0aa4d06ea442ea78ef7f2a62e9922816 src/admin/web/streams.js
f2ff4b717fdb12003f2c9841c455a783c336a1e9a78ca1ca228c645578c1badb src/admin/web/style.css
a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js
e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js
6856c230c4f6b18145e16efb6636433f8b21ce3ec5d34246d6561f0150ea0554 src/admin/web/vhosts/core.js
d4938e0bf8cf41f79e9b49cf9b5cf2c8340668e792b73d5350cb1872e7ef1edf src/admin/web/vhosts/editor.js
91efb2d4f02d12bbbb22818f1215486329a91293d85a84c789dae8d77632059f src/admin/web/vhosts/render.js
0de35a0671f3ac8fed62260d63ae43d04efce4947428ebdb9254cff0295295f9 src/admin/web/web-server.js
e0011edc708d245cb514be170c1bbe846a0298b81d86276fe29af05010d215a4 src/admin/web/vhosts/render.js
b3d8d3e22e4cbab10c5d588c268da6eb3075a9ec857728ce6b5d56e3ffd3c9a6 src/admin/web/web-server.js
98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
97b5a060f24a102bbaa2f406426e985236c0d719b8038db210c25b34fcd8e923 src/auth/mod.rs
@@ -346,11 +350,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
8678b85fb46fa6813c9483e1ceb59a25458cae655418571bfa491b1e391f7ed5 src/main/bootstrap.rs
777cc490f53417047b90ba359aa400961f10234b88684b47df9a5025e3929c36 src/main/bootstrap.rs
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
97f8dea981ea0b54f64144724f565ed0fa3d42dfcc043ce88f6f851f4ea458fe src/main/tasks.rs
689d0eb4637b826c3802818a00f689eb7b40c3c96918d86377efb9ce047df01b src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
@@ -457,7 +461,7 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
87c20f3a28ea6cfde496f05a381ee3b4be784bae4d6bf65580ab6c0306bb46fb src/web_server/fastcgi/protocol.rs
f11eb148a9f5f3c8f65ad067f497d88e19375306cfd10016110863b4a2bd6c75 src/web_server/fastcgi/protocol.rs
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
@@ -467,14 +471,14 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
4c3490918e10074226d75971c743249482d3e174dddc80699f8cd8da7d90d9df src/web_server/response/file.rs
6b5506a79c48c58b9ee69bdcbed16fba63bd3b2dc5c11ddb2deb9b087cc8f35c src/web_server/response/file.rs
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
9bd3acd815604a4ce349c7b2d41cede676ebafc33183dd9d32fb3193ef63d68f src/web_server/tests/part_02.rs
aa142445a5c7a50f6f667a2b84ec25f80ea79790bb0d0aca3c922dfc3118594b src/web_server/tests/part_02.rs
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
+1 -1
View File
@@ -1,6 +1,6 @@
# Debian packaging
This directory builds the `rustproxy` binary package for RustProxy 0.26.2.
This directory builds the `rustproxy` binary package for RustProxy 0.26.3.
The package installs:
- `/usr/bin/rust-proxy`
+1 -1
View File
@@ -21,6 +21,6 @@ cargo fetch --locked
dpkg-buildpackage -us -uc -b
mkdir -p dist
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.2_*.deb' -o -name 'rustproxy_0.26.2_*.changes' -o -name 'rustproxy_0.26.2_*.buildinfo' \) \
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.3_*.deb' -o -name 'rustproxy_0.26.3_*.changes' -o -name 'rustproxy_0.26.3_*.buildinfo' \) \
-exec cp -f {} dist/ \;
echo "Packages copied to $ROOT/dist"
+2 -2
View File
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
case "$mode" in
help|-h|--help)
cat <<'HELP'
RustProxy 0.26.2
RustProxy 0.26.3
./dev.sh Build and start proxy + local panel
./dev.sh run [serve flags] Same, with extra server options
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
printf 'Checking all Rust targets before tests...\n' >&2
cargo check --locked --all-targets
fi
printf 'Building RustProxy 0.26.2...\n' >&2
printf 'Building RustProxy 0.26.3...\n' >&2
cargo build --locked
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
case "$mode" in
+1 -1
View File
@@ -1,6 +1,6 @@
package main
const version = "0.26.2"
const version = "0.26.3"
type config struct {
Listen string
+1 -1
View File
@@ -1,4 +1,4 @@
# Administration API access - 0.26.2
# Administration API access - 0.26.3
## Sign-in boundary
+1 -1
View File
@@ -1,6 +1,6 @@
# Stored log explorer
RustProxy 0.26.2 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
RustProxy 0.26.3 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
## Separation from Live Logs
+1 -1
View File
@@ -1,4 +1,4 @@
# Installing and updating RustProxy 0.26.2
# Installing and updating RustProxy 0.26.3
The scripts prepare a Linux/systemd installation. This source archive is **not a
verified production build**: Rust/Cargo, real CA operations, live systemd startup
+2 -1
View File
@@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards.
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status.
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
- [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish.
- [0.26.2 implementation note](V0.26.2_IMPLEMENTATION.md) - unified Logs navigation, field-query search, dynamic JSON columns and RAW parsing.
- [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - historical vhost-aware protocol logging and Stored Logs UX milestone.
- [TEST_REPORT](TEST_REPORT.md) - verification commands, results and environment limitations.
@@ -62,7 +63,7 @@ This is the canonical index for the current project documentation. The authentic
## Historical or version-labelled context
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current release implementation note is [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md); [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) is retained as the prior Stored Logs/protocol-attribution milestone; the latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); [V0.25.4_AUDIT.md](V0.25.4_AUDIT.md) records the prior retained-history restore milestone and [V0.25.3_AUDIT.md](V0.25.3_AUDIT.md) records the prior request-policy/history milestone and [V0.25.2_AUDIT.md](V0.25.2_AUDIT.md) records the prior backup/session hardening milestone and [V0.25.1_AUDIT.md](V0.25.1_AUDIT.md) records the prior Traffic/module-split milestone and [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md) remains the Web Server foundation audit.
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md); [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) is retained as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence.
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
+12
View File
@@ -1,5 +1,17 @@
# Listener and response settings
## Administration appearance preferences
**Settings -> Appearance** contains dashboard-only preferences. They are stored per administrator in the existing `admin_preferences` SQLite document and do not create a proxy configuration revision. The preferences are theme (`dark`/`light`), language (`auto` or a language-pack id), global interface scale (70-125%), timestamp format (`24h`/`12h`), chart style (`default`, `soft`, `technical`, `high_contrast`, `neon`, `monochrome`, `warm` or `cool`), button style (`normal` or `compact`), table/log density (`normal`, `compact`, `ultra`), dashboard density (`normal`, `compact`, `ultra`), Overview card order and Overview card visibility. `auto` language follows the browser language when a matching pack exists and otherwise falls back to the configured default pack.
The scale applies to the complete authenticated administration surface, including navigation, workspace content, dialogs, overlays and the draft dock. Text fields, selects, textareas, checkboxes and radios also receive a small proportional control-size adjustment on top of global zoom so field proportions track the selected scale more visibly. Button style is independent: **Normal** keeps the refreshed control sizing and **Compact** restores the smaller source-style sizing, including generated entity actions and tab buttons inside dialogs. Table/log and dashboard density are additional layout controls and do not change the whole-interface scale. The dashboard editor can hide/show and reorder the telemetry cards at the top of Overview; this changes presentation only, not metric collection. The selected time format is shared by logs, backend/runtime diagnostics, audit/session timestamps, certificate views and configuration-history timestamps. Chart presets change colors, surfaces, grid and line presentation only; they do not change sampled data, and the palette selector exposes the main colors for each preset. Each Appearance subsection has its own reset action, and **Reset appearance** restores all documented Appearance defaults without touching unrelated account preferences. A small local cache is retained only to avoid a theme/scale flash before authentication, while the authenticated SQLite preference remains authoritative.
Unfinished Web Server, Vhost, Redirect and Stream editors remain session-only drafts. Their bottom-right dock uses square corners, can collapse to a single title bar, shows the age of each suspended draft and does not persist draft form values to SQLite.
The Settings page includes a local search field that filters category tabs using translated category labels and page content. Filtering never rebuilds settings forms, so unsaved values remain in the DOM. Category navigation remains sticky on desktop and marks categories containing unsaved forms.
Unfinished Web Server, Vhost, Redirect and Stream forms use a session-only draft dock. Closing a dirty editor minimizes its exact browser state to the bottom-right dock so another editor can be opened independently. The dock follows the square-corner panel styling of the dashboard and intentionally avoids rounded/glass presentation. Drafts are intentionally not written to SQLite because form state can contain credentials or other secrets; they are discarded on logout/session reset.
## Configuration and update semantics
GET `/api/config` returns a revisioned envelope. PUT the entire document with
+7 -5
View File
@@ -1,18 +1,20 @@
# Verification report - RustProxy 0.26.2
# Verification report - RustProxy 0.26.3
## Scope
RustProxy 0.26.2 keeps the 0.26.1 vhost-aware persistent protocol diagnostics and focuses on the administration log workflow. Stored Logs and Live Logs now share one top-bar Logs menu while retaining independent data paths.
RustProxy 0.26.3 keeps the 0.26.2 log workflow and focuses on administration appearance, scaling and draft/settings ergonomics. Stored Logs and Live Logs now share one top-bar Logs menu while retaining independent data paths.
Stored Logs replaces the fixed event/status/client/vhost controls with an optional source selector, one bounded field-query input and page size. The query parser supports `field:value`, explicit `AND`, `!` negation, quoted values, wildcards, numeric comparisons and aliases such as `req` -> `method`. JSON/JSONL records expose a bounded discovered-field list for dynamic columns; protocol-specific fields such as `matched_by` and `sni` are no longer hard-coded table columns. RAW error-log lines are best-effort parsed into timestamp, level, target, message and detected `key=value` fields while preserving `raw`.
The dashboard removes the Configured files inventory but keeps an optional source selector with `All configured files` as the default, moves the column picker to the table toolbar, expands selected rows as parsed fields plus optional raw JSON, and uses `↑`/`↓` for backend sort direction. SQLite schema 8 and the saved proxy configuration format remain unchanged.
This workspace retains session-only draft docking for dirty Web Server, Vhost, Redirect and Stream editors; opening a second editor no longer reuses or blocks the suspended form state. In 0.26.3 the dock uses the same square-corner panel language as the rest of RustProxy, can collapse to one bar and shows draft age. **Settings -> Appearance** persists theme, language, 70-125% global interface scale, 12/24-hour display, eight chart styles, Normal/Compact button style, table/log density, dashboard density and Overview-card layout/visibility in the existing account-scoped administrator preference document. Scaling includes dialogs, overlays and the draft dock; form fields also receive a bounded additional size response so control proportions follow 70-125% more visibly. Compact buttons restore the smaller source-style control dimensions without changing the whole-interface scale and now apply explicitly to generated vhost/entity actions and dialog tab buttons. Settings adds content search; Appearance exposes chart palette previews, per-section resets and a full reset. Expanded Stored Logs details remain compact. SQLite schema 8 remains unchanged.
## Source-level regression coverage
0.26.2-specific Rust tests cover field aliases, explicit AND, negation, quoted values, wildcard/status/numeric matching and built-in RAW error parsing. Retained 0.26.1 tests continue to cover canonical field recovery, safe source derivation, symlink refusal, bounded reads and protocol diagnostic normalization.
0.26.2 log-query Rust tests continue to cover field aliases, explicit AND, negation, quoted values, wildcard/status/numeric matching and built-in RAW error parsing. Retained 0.26.1 tests continue to cover canonical field recovery, safe source derivation, symlink refusal, bounded reads and protocol diagnostic normalization.
Administration preference validation now permits up to 64 bounded, control-free Stored Logs field names so dynamically discovered JSON fields can be persisted per account. Browser JavaScript is syntax-checked, and the language/documentation/source-contract suites cover the new UI strings and release identity.
Administration preference validation now permits up to 64 bounded, control-free Stored Logs field names so dynamically discovered JSON fields can be persisted per account. It also validates the language, 70-125% interface-scale, time-format, expanded chart-style fields, Normal/Compact button style, table/log density, dashboard density, dashboard card ordering and hidden-card list while retaining defaults for legacy preference JSON. Browser JavaScript is syntax-checked, the Web Server UI harness loads the draft manager in production order, and the language/documentation/source-contract suites cover the new UI strings and release identity.
## Verification status in this workspace
@@ -27,7 +29,7 @@ Administration preference validation now permits up to 64 bounded, control-free
| `python3 scripts/rust_module_contracts.py` | PASS after keeping modified Rust modules within the 500-line contract |
| `python3 scripts/docs_check.py` | PASS |
| `node scripts/web_service_ui_test.mjs` | PASS |
| `node --check src/admin/web/file-logs.js` | PASS |
| `node --check` for `settings.js`, `drafts.js`, `app/preferences.js`, `traffic-panel.js` and `file-logs.js` | PASS |
| language/OpenAPI JSON parse | PASS |
| `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime as not verified |
| `python3 scripts/release_smoke.py` | PASS, including upgrade/package/reproducibility contracts |
+29
View File
@@ -0,0 +1,29 @@
# RustProxy 0.26.3 implementation note
## Scope
Version 0.26.3 is an administration-interface polish release. It keeps SQLite schema 8 and the saved proxy configuration format unchanged from 0.26.2.
## Appearance
The account-scoped interface scale now accepts 70-125% and is applied to the whole authenticated administration body, not only the workspace. This includes navigation, dialogs, overlays, toasts and the session draft dock.
Chart presentation expands to eight presets: Default, Soft, Technical, High contrast, Neon, Monochrome, Warm and Cool. Presets share CSS chart variables so line colors, multi-series palette, chart surfaces, grid treatment, stroke geometry and contrast change together across Overview diagnostics, vhost/stream charts and Traffic charts. Chart styles do not alter samples or runtime metrics.
The Appearance page also provides a reset action that restores theme, language, scale, time format and chart style to documented defaults while retaining unrelated administrator preferences.
## Draft dock
Web Server, Vhost, Redirect and Stream drafts remain session-only because editor state may contain secrets. The dock is restyled to use the existing RustProxy panel vocabulary: square corners, opaque panel surfaces, thin separators and compact rows. It does not use rounded glass-card styling.
## Settings navigation
Settings now includes a search field. It filters category tabs using the translated tab label and the text of each settings page. Filtering only changes tab visibility and never reconstructs forms, preserving dirty/unsaved values. Existing sticky desktop category navigation and unsaved-category markers remain active.
## Compatibility
No database migration is required from 0.26.2. Existing administrator preference JSON remains compatible through serde defaults; the only validation changes are the lower 70% scale bound and the additional chart-style enum values.
## Verification
Rust-independent source, language, documentation, JavaScript and packaging checks are expected to run in the release workspace. Rust compiler/type/runtime verification remains blocked when Cargo is unavailable and must be repeated on a Rust-enabled release host before describing the release as compiler-verified.
+1 -1
View File
@@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
are intentionally absent from Prometheus labels.
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
dashboard expects RustProxy 0.26.2 metric names and a scrape interval around 15s.
dashboard expects RustProxy 0.26.3 metric names and a scrape interval around 15s.
Collectors can be disabled in RustProxy settings; panels for disabled collectors
will simply show no series.
+2 -2
View File
@@ -4529,9 +4529,9 @@
},
"timepicker": {},
"timezone": "browser",
"title": "RustProxy 0.26.2 - Operations",
"title": "RustProxy 0.26.3 - Operations",
"uid": "rustproxy-0170",
"version": 5,
"weekStart": "",
"description": "RustProxy 0.26.2 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
"description": "RustProxy 0.26.3 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
}
+62 -2
View File
@@ -2012,6 +2012,66 @@
"previous": "Previous",
"next": "Next",
"refresh": "Refresh",
"applyFilters": "Apply filters"
"applyFilters": "Apply filters",
"appearance": "Appearance",
"appearanceEyebrow": "INTERFACE",
"appearanceHelp": "These preferences are saved for your administrator account and follow you between sessions.",
"languageAutomatic": "Automatic (browser)",
"interfaceScale": "Interface scale",
"interfaceScaleHelp": "Scale the entire administration interface, including dialogs and overlays, without changing browser zoom.",
"timeFormat": "Time format",
"timeFormat24": "24-hour",
"timeFormat12": "12-hour",
"chartStyle": "Chart style",
"chartStyleDefault": "Default",
"chartStyleSoft": "Soft",
"chartStyleTechnical": "Technical",
"chartStyleHighContrast": "High contrast",
"chartStylePreview": "Preview",
"draftDockTitle": "Drafts",
"draftUntitled": "New",
"draftRestore": "Restore draft",
"draftDiscard": "Discard draft",
"draftDiscardConfirm": "Discard this unsaved draft?",
"draftMinimized": "Draft moved to the dock.",
"chartStyleNeon": "Neon",
"chartStyleMonochrome": "Monochrome",
"chartStyleWarm": "Warm",
"chartStyleCool": "Cool",
"settingsSearch": "Find settings",
"settingsSearchPlaceholder": "Search settings, sections and options...",
"settingsSearchResults": "Matching sections",
"appearanceReset": "Reset appearance",
"appearanceResetDone": "Appearance preferences reset",
"appearanceInterfaceSection": "INTERFACE",
"appearanceInterfaceTitle": "Interface",
"appearanceChartsSection": "CHARTS",
"appearanceDataSection": "TABLES / LOGS",
"appearanceDashboardSection": "DASHBOARD",
"resetSection": "Reset section",
"appearanceSectionResetDone": "Section preferences reset",
"chartStylePalettes": "Chart style palettes",
"tableDensity": "Tables and logs density",
"densityNormal": "Normal",
"densityUltra": "Ultra compact",
"tableDensityHelp": "Changes row height and log spacing independently from interface scale.",
"dashboardLayout": "Dashboard layout",
"dashboardDensity": "Dashboard density",
"dashboardDensityHelp": "Controls Overview telemetry card size without changing the rest of the interface.",
"dashboardWidgets": "Overview cards",
"dashboardWidgetsHelp": "Show, hide and reorder the telemetry cards at the top of Overview.",
"dashboardWidgetVisible": "Visible",
"moveUp": "Move up",
"moveDown": "Move down",
"draftDockCollapse": "Collapse drafts",
"draftDockExpand": "Expand drafts",
"draftAgeNow": "now",
"draftAgeMinutes": "{count} min",
"draftAgeHours": "{count} h",
"draftAgeDays": "{count} d",
"buttonStyle": "Button style",
"buttonStyleNormal": "Normal",
"buttonStyleCompact": "Compact",
"buttonStyleHelp": "Normal uses the refreshed controls; Compact restores the smaller source-style buttons across all actions and dialog tabs."
}
}
}
+70 -10
View File
@@ -1962,16 +1962,16 @@
"vhPoolRecycleSeconds": "Maks. wiek puli upstream (sekundy, 0 = wył.)",
"vhHttp1MaxBuffer": "Maks. bufor odczytu HTTP/1 upstream (bajty)",
"vhPoolRecycleHelp": "Odświeżanie podmienia tylko generację klienta Hyper danego backendu; aktywne wymiany kończą się normalnie. Domyślne limity ograniczają retencję pamięci puli/TLS bez przeładowania vhosta ani cache. Limit żądań: 0 lub 1000..10000000; limit czasu: 0 lub 60..604800.",
"backendSearch": "Szukaj backendów",
"backendSearch": "Szukaj backendów",
"backendSearchPlaceholder": "Szukaj vhosta, backendu lub stanu",
"backendSortBy": "Sortuj według",
"backendSortAscending": "Sortowanie rosnące",
"backendSortDescending": "Sortowanie malejące",
"backendSortBy": "Sortuj według",
"backendSortAscending": "Sortowanie rosnące",
"backendSortDescending": "Sortowanie malejące",
"backendColumns": "Kolumny",
"backendSearchEmpty": "Brak backendów pasujących do wyszukiwania.",
"vhQuickEnable": "Włącz",
"vhQuickDisable": "Wyłącz",
"vhEnableHint": "Określa, czy ten vhost przyjmuje ruch.",
"backendSearchEmpty": "Brak backendów pasujących do wyszukiwania.",
"vhQuickEnable": "Włącz",
"vhQuickDisable": "Wyłącz",
"vhEnableHint": "Określa, czy ten vhost przyjmuje ruch.",
"backendColumnsRequired": "Co najmniej jedna kolumna tabeli backendów musi pozostać widoczna.",
"logs": "Logs",
"storedLogs": "Zapisane logi",
@@ -2012,6 +2012,66 @@
"previous": "Poprzednia",
"next": "Następna",
"refresh": "Odśwież",
"applyFilters": "Zastosuj filtry"
"applyFilters": "Zastosuj filtry",
"appearance": "Wygląd",
"appearanceEyebrow": "INTERFEJS",
"appearanceHelp": "Te preferencje są zapisywane dla konta administratora i obowiązują również w kolejnych sesjach.",
"languageAutomatic": "Automatycznie (przeglądarka)",
"interfaceScale": "Skala interfejsu",
"interfaceScaleHelp": "Skaluj cały interfejs administracyjny, łącznie z modalami i nakładkami, bez zmiany powiększenia przeglądarki.",
"timeFormat": "Format czasu",
"timeFormat24": "24-godzinny",
"timeFormat12": "12-godzinny",
"chartStyle": "Styl wykresów",
"chartStyleDefault": "Domyślny",
"chartStyleSoft": "Łagodny",
"chartStyleTechnical": "Techniczny",
"chartStyleHighContrast": "Wysoki kontrast",
"chartStylePreview": "Podgląd",
"draftDockTitle": "Szkice",
"draftUntitled": "Nowy",
"draftRestore": "Otwórz szkic",
"draftDiscard": "Usuń szkic",
"draftDiscardConfirm": "Usunąć ten niezapisany szkic?",
"draftMinimized": "Szkic przeniesiono do docka.",
"chartStyleNeon": "Neonowy",
"chartStyleMonochrome": "Monochromatyczny",
"chartStyleWarm": "Ciepły",
"chartStyleCool": "Chłodny",
"settingsSearch": "Znajdź ustawienia",
"settingsSearchPlaceholder": "Szukaj ustawień, sekcji i opcji...",
"settingsSearchResults": "Pasujące sekcje",
"appearanceReset": "Przywróć wygląd",
"appearanceResetDone": "Przywrócono domyślne ustawienia wyglądu",
"appearanceInterfaceSection": "INTERFEJS",
"appearanceInterfaceTitle": "Interfejs",
"appearanceChartsSection": "WYKRESY",
"appearanceDataSection": "TABELE / LOGI",
"appearanceDashboardSection": "DASHBOARD",
"resetSection": "Resetuj sekcję",
"appearanceSectionResetDone": "Ustawienia sekcji zostały zresetowane",
"chartStylePalettes": "Palety stylów wykresów",
"tableDensity": "Gęstość tabel i logów",
"densityNormal": "Normalna",
"densityUltra": "Ultra kompaktowa",
"tableDensityHelp": "Zmienia wysokość wierszy i odstępy w logach niezależnie od globalnej skali interfejsu.",
"dashboardLayout": "Układ dashboardu",
"dashboardDensity": "Gęstość dashboardu",
"dashboardDensityHelp": "Steruje rozmiarem kafelków telemetrii w Overview bez zmiany reszty interfejsu.",
"dashboardWidgets": "Kafelki Overview",
"dashboardWidgetsHelp": "Pokazuj, ukrywaj i zmieniaj kolejność kafelków telemetrii na górze Overview.",
"dashboardWidgetVisible": "Widoczny",
"moveUp": "Przenieś wyżej",
"moveDown": "Przenieś niżej",
"draftDockCollapse": "Zwiń szkice",
"draftDockExpand": "Rozwiń szkice",
"draftAgeNow": "teraz",
"draftAgeMinutes": "{count} min",
"draftAgeHours": "{count} godz.",
"draftAgeDays": "{count} d",
"buttonStyle": "Styl przycisków",
"buttonStyleNormal": "Normal",
"buttonStyleCompact": "Compact",
"buttonStyleHelp": "Normal używa odświeżonych przycisków; Compact przywraca drobniejsze przyciski ze źródłowego interfejsu we wszystkich akcjach i zakładkach modali."
}
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.2.zip')
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.3.zip')
output = parser.parse_args().output.resolve()
paths = files()
checksum = ROOT / 'SHA256SUMS'
+11 -11
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.26.2"
assert manifest["package"]["version"] == "0.26.3"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock"
if lock_path.exists():
lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.26.2"' in lock
assert 'name = "rust-proxy"\nversion = "0.26.3"' in lock
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in {
"README.md": "# RustProxy 0.26.2",
"CHANGELOG.md": "## 0.26.2",
"ROADMAP.md": "## 0.26.2",
"AGENTS.md": "Current source version: 0.26.2.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.2",
"README.md": "# RustProxy 0.26.3",
"CHANGELOG.md": "## 0.26.3",
"ROADMAP.md": "## 0.26.3",
"AGENTS.md": "Current source version: 0.26.3.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.3",
}.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -240,7 +240,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.26.2"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
for marker in ['"version": "0.26.3"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
@@ -800,7 +800,7 @@ assert 'function uptime(value)' in app_js and 'setOptionalMetric("process-uptime
assert 'connection-uptime-footer' not in html and 'id="process-uptime"' in html
assert 'id="panel-transform"' in html and 'data-panel="transform"' in html and '/assets/transform-panel.js' in html
assert '/api/transform' in transform_panel_js and 'tp-cache-hits' in transform_panel_js and 'transform-reason-list' in transform_panel_js
assert '--transform-chart:' in css and 'stroke: var(--transform-chart)' in css and 'background: var(--transform-chart)' in css
assert '--transform-chart:' in css and '--chart-quaternary:' in css and 'stroke: var(--chart-quaternary)' in css and 'background: var(--transform-chart)' in css
assert '.cache-transform-badge {' in css and 'border: 1px solid var(--pill-line)' in css and 'border-radius: 3px' in css
assert 'pub transformed: bool' in cache_source and 'annotate_transformed' in cache_source and 'format!("{base}; transformed")' in cache_source
assert 'x-cache-status' in cache_source and 'Cache-Status (RFC 9211)' in html
@@ -1203,7 +1203,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.26.2"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.26.3"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
@@ -1248,5 +1248,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
assert "maxItems" not in node, schema_name
print("PASS: 0.26.2 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
print("PASS: 0.26.3 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
+1 -1
View File
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
def fetch_https(url: str, max_bytes: int) -> bytes:
if urllib.parse.urlparse(url).scheme != 'https':
raise ValueError('GeoIP downloads require HTTPS.')
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.2'})
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.3'})
with urllib.request.urlopen(request, timeout=30) as response:
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
raise ValueError('GeoIP download redirected away from HTTPS.')
+5 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.26.2 release."""
"""Rust-independent documentation/Reader consistency checks for the 0.26.3 release."""
from pathlib import Path
import re
from rust_source import rust_source
@@ -55,16 +55,16 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.26.2")
assert readme.startswith("# RustProxy 0.26.3")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.2" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.3" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
assert changelog.startswith("# Changelog\n")
for release in ["0.26.2", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
for release in ["0.26.3", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
assert f"## {release}" in changelog, release
assert changelog.index("## 0.26.2") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
assert changelog.index("## 0.26.3") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
+3 -3
View File
@@ -73,12 +73,12 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
flat = module.parent.with_suffix(".rs")
if flat.is_file():
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.26.2 source tree: missing {path}")
raise ValueError(f"Incomplete RustProxy 0.26.3 source tree: missing {path}")
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.26.2 entry point; extract the complete package, not individual files.")
raise ValueError("src/lib.rs is not the 0.26.3 entry point; extract the complete package, not individual files.")
return messages
+7 -7
View File
@@ -123,7 +123,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.26.2"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.26.3"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file()
@@ -134,7 +134,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.26.2"
assert manifest["to_version"] == "0.26.3"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -554,7 +554,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.2"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.3"' in openapi_0241
assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker
@@ -687,7 +687,7 @@ def source_contract_smoke() -> None:
assert 'sparklinePoints(liveChartSamples.hits, 900, 180, 10, peak)' in cache_panel_ui
assert 'sparklinePoints(liveChartSamples.applied, 900, 180, 10, peak)' in transform_panel
assert '.function-live-chart svg {' in ui_css and 'height: clamp(180px, 22vw, 270px)' in ui_css
assert '--transform-chart:' in ui_css and 'stroke: var(--transform-chart)' in ui_css and 'background: var(--transform-chart)' in ui_css
assert '--transform-chart:' in ui_css and '--chart-quaternary:' in ui_css and 'stroke: var(--chart-quaternary)' in ui_css and 'background: var(--transform-chart)' in ui_css
assert '.cache-transform-badge {' in ui_css and 'border: 1px solid var(--pill-line)' in ui_css and 'border-radius: 3px' in ui_css
html = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
cache_panel_js = (ROOT / 'src/admin/web/cache-panel.js').read_text(encoding='utf-8')
@@ -1011,7 +1011,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.26.2"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
for marker in ['"version": "0.26.3"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
@@ -1147,7 +1147,7 @@ def reproducible_archive_smoke(tmp: Path) -> None:
assert f"RustProxy/src/web_server/{module}" in names
assert "RustProxy/src/web_server.rs" not in names
assert "RustProxy/src/admin/web/web-server.js" in names
for module in ["app/core.js", "app/config-history.js", "app/runtime.js", "app/settings.js", "vhosts/core.js", "vhosts/editor.js", "vhosts/render.js"]:
for module in ["app/core.js", "app/preferences.js", "app/config-history.js", "app/runtime.js", "app/settings.js", "drafts.js", "vhosts/core.js", "vhosts/editor.js", "vhosts/render.js"]:
assert f"RustProxy/src/admin/web/{module}" in names
assert "RustProxy/src/admin/web/app.js" not in names and "RustProxy/src/admin/web/vhosts.js" not in names
for module in ["model.go", "docker.go", "network.go", "certificate.go", "server.go", "main.go", "main_test.go"]:
@@ -1186,7 +1186,7 @@ def main() -> None:
source_contract_smoke()
container_contract_smoke()
debian_grafana_contract_smoke()
print("PASS: 0.26.2 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
print("PASS: 0.26.3 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
if __name__ == "__main__":
+1 -1
View File
@@ -35,7 +35,7 @@ for pattern in ("deploy*.py", "migrate_npm*.py"):
for old in (ROOT / "src/admin/web/app.js", ROOT / "src/admin/web/vhosts.js"):
assert not old.exists(), f"oversized browser monolith returned: {old.relative_to(ROOT)}"
for required in (
"src/admin/web/app/core.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js",
"src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js",
"src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js",
"docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go",
"docker-agent/certificate.go", "docker-agent/server.go",
+1 -1
View File
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
"""TLS verification uses Python's default trust store; never executes a shell."""
started = time.monotonic()
opener = build_opener(HttpsRedirects())
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.2"})
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.3"})
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
length = response.headers.get("Content-Length")
if length and int(length) > MAX_ARCHIVE:
+3 -3
View File
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
for name in MANAGED:
if (target / name).is_symlink():
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
backup = target / ".upgrade-backup" / ("before-0.26.2-" + uuid.uuid4().hex[:12])
backup = target / ".upgrade-backup" / ("before-0.26.3-" + uuid.uuid4().hex[:12])
backup.mkdir(parents=True, mode=0o700)
installed: list[str] = []
moved: list[str] = []
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
remove_legacy = legacy.exists()
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.2", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.3", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
if remove_legacy:
legacy.unlink()
except BaseException:
@@ -115,7 +115,7 @@ def main() -> None:
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
except (OSError, ValueError) as error:
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
print(f"Installed RustProxy 0.26.2. Previous managed source files: {backup}")
print(f"Installed RustProxy 0.26.3. Previous managed source files: {backup}")
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
+1 -1
View File
@@ -1,6 +1,6 @@
from pathlib import Path
APP_PARTS = ("core.js", "config-history.js", "runtime.js", "settings.js")
APP_PARTS = ("core.js", "preferences.js", "config-history.js", "runtime.js", "settings.js")
VHOST_PARTS = ("core.js", "editor.js", "render.js")
def _read(root: Path, folder: str, parts: tuple[str, ...]) -> str:
+1 -1
View File
@@ -96,7 +96,7 @@ need(web,
'HeaderValue::from_static("no-store")',
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
"empty_index_can_go_directly_to_directory_listing",
"RustProxy <b>0.26.2</b>",
"RustProxy <b>0.26.3</b>",
"traversal_and_symlink_escape_are_not_served",
"try_files_rewrites_cache_rules_headers_and_downloads_work",
"precompressed_sidecars_keep_mime_validators_and_range_semantics",
+2
View File
@@ -65,6 +65,7 @@ elements.get("web-service-php-header-limit").value = "65536";
elements.get("web-service-php-stderr-limit").value = "4096";
const document = {
body: new Element("body"),
querySelectorAll(selector) {
if (selector === "#web-service-tabs button") return elements.get("web-service-tabs").children;
if (selector === "[data-web-service-page]") return pages;
@@ -96,6 +97,7 @@ const context = {
appConfirm: async () => true,
};
vm.createContext(context);
vm.runInContext(fs.readFileSync("src/admin/web/drafts.js", "utf8"), context, { filename: "drafts.js" });
vm.runInContext(fs.readFileSync("src/admin/web/web-server.js", "utf8"), context, { filename: "web-server.js" });
const preset = elements.get("web-service-preset");
+8
View File
@@ -399,6 +399,14 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"/docs/V0.25.1_AUDIT.md"
)),
),
(
"docs/V0.26.3_IMPLEMENTATION.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.26.3_IMPLEMENTATION.md"
)),
),
(
"docs/V0.26.2_IMPLEMENTATION.md",
"technical",
+135 -17
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.26.2",
"version": "0.26.3",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
},
"servers": [
@@ -2912,23 +2912,12 @@
"file_log_columns": {
"type": "array",
"minItems": 1,
"maxItems": 11,
"maxItems": 64,
"uniqueItems": true,
"items": {
"type": "string",
"enum": [
"time",
"event",
"status",
"client_ip",
"vhost",
"transport",
"matched_by",
"sni",
"authority",
"record",
"source"
]
"minLength": 1,
"maxLength": 128
},
"default": [
"time",
@@ -2939,7 +2928,7 @@
"record",
"source"
],
"description": "Account-scoped Stored Logs explorer columns shown in the dashboard."
"description": "Account-scoped Stored Logs explorer columns shown in the dashboard. Field names may be discovered dynamically from bounded JSON/JSONL records."
},
"theme": {
"type": "string",
@@ -2950,6 +2939,135 @@
"default": "dark",
"description": "Account-scoped administration interface theme. Existing accounts and omitted values default to the current dark theme."
},
"language": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"default": "auto",
"description": "Account-scoped administration language id, or auto to follow the browser language when a matching language pack exists."
},
"ui_scale": {
"type": "integer",
"minimum": 70,
"maximum": 125,
"default": 100,
"description": "Account-scoped administration workspace scale in percent."
},
"time_format": {
"type": "string",
"enum": [
"24h",
"12h"
],
"default": "24h",
"description": "Account-scoped administration timestamp display preference used by dashboard log and diagnostics views."
},
"chart_style": {
"type": "string",
"enum": [
"default",
"soft",
"technical",
"high_contrast",
"neon",
"monochrome",
"warm",
"cool"
],
"default": "default",
"description": "Account-scoped visual presentation preset applied consistently to administration charts."
},
"button_style": {
"type": "string",
"enum": [
"normal",
"compact"
],
"default": "normal",
"description": "Account-scoped administration button sizing. Normal uses the refreshed controls; Compact restores the smaller source-style button dimensions."
},
"table_density": {
"type": "string",
"enum": [
"normal",
"compact",
"ultra"
],
"default": "normal",
"description": "Account-scoped table and log row density independent from whole-interface scale."
},
"dashboard_density": {
"type": "string",
"enum": [
"normal",
"compact",
"ultra"
],
"default": "compact",
"description": "Account-scoped Overview telemetry-card density."
},
"dashboard_widgets": {
"type": "array",
"minItems": 1,
"maxItems": 12,
"uniqueItems": true,
"items": {
"type": "string",
"enum": [
"requests",
"active",
"bandwidth",
"errors",
"cache",
"memory",
"websocket",
"http2",
"http3",
"tls",
"http_protocol",
"websocket_protocol"
]
},
"default": [
"requests",
"active",
"bandwidth",
"errors",
"cache",
"memory",
"websocket",
"http2",
"http3",
"tls",
"http_protocol",
"websocket_protocol"
],
"description": "Account-scoped order for configurable Overview telemetry cards."
},
"dashboard_hidden_widgets": {
"type": "array",
"maxItems": 12,
"uniqueItems": true,
"items": {
"type": "string",
"enum": [
"requests",
"active",
"bandwidth",
"errors",
"cache",
"memory",
"websocket",
"http2",
"http3",
"tls",
"http_protocol",
"websocket_protocol"
]
},
"default": [],
"description": "Account-scoped Overview telemetry cards hidden from the dashboard."
},
"backend_table_columns": {
"type": "array",
"minItems": 1,
@@ -9840,4 +9958,4 @@
}
}
}
}
}
+165
View File
@@ -30,10 +30,51 @@ const BACKEND_TABLE_COLUMNS: &[&str] = &[
"lastFailure",
"lastRecovery",
];
const DASHBOARD_WIDGETS: &[&str] = &[
"requests",
"active",
"bandwidth",
"errors",
"cache",
"memory",
"websocket",
"http2",
"http3",
"tls",
"http_protocol",
"websocket_protocol",
];
fn default_admin_theme() -> String {
"dark".to_owned()
}
fn default_admin_language() -> String {
"auto".to_owned()
}
fn default_admin_ui_scale() -> u16 {
100
}
fn default_admin_time_format() -> String {
"24h".to_owned()
}
fn default_admin_chart_style() -> String {
"default".to_owned()
}
fn default_admin_button_style() -> String {
"normal".to_owned()
}
fn default_admin_table_density() -> String {
"normal".to_owned()
}
fn default_admin_dashboard_density() -> String {
"compact".to_owned()
}
fn default_admin_dashboard_widgets() -> Vec<String> {
DASHBOARD_WIDGETS
.iter()
.map(|value| (*value).to_owned())
.collect()
}
fn default_file_log_columns() -> Vec<String> {
[
"time",
@@ -64,6 +105,24 @@ struct AdminPreferences {
live_log_columns: Vec<String>,
#[serde(default = "default_admin_theme")]
theme: String,
#[serde(default = "default_admin_language")]
language: String,
#[serde(default = "default_admin_ui_scale")]
ui_scale: u16,
#[serde(default = "default_admin_time_format")]
time_format: String,
#[serde(default = "default_admin_chart_style")]
chart_style: String,
#[serde(default = "default_admin_button_style")]
button_style: String,
#[serde(default = "default_admin_table_density")]
table_density: String,
#[serde(default = "default_admin_dashboard_density")]
dashboard_density: String,
#[serde(default = "default_admin_dashboard_widgets")]
dashboard_widgets: Vec<String>,
#[serde(default)]
dashboard_hidden_widgets: Vec<String>,
#[serde(default = "default_file_log_columns")]
file_log_columns: Vec<String>,
#[serde(default = "default_backend_table_columns")]
@@ -82,6 +141,15 @@ impl Default for AdminPreferences {
.map(|value| (*value).to_owned())
.collect(),
theme: default_admin_theme(),
language: default_admin_language(),
ui_scale: default_admin_ui_scale(),
time_format: default_admin_time_format(),
chart_style: default_admin_chart_style(),
button_style: default_admin_button_style(),
table_density: default_admin_table_density(),
dashboard_density: default_admin_dashboard_density(),
dashboard_widgets: default_admin_dashboard_widgets(),
dashboard_hidden_widgets: Vec::new(),
file_log_columns: default_file_log_columns(),
backend_table_columns: default_backend_table_columns(),
backend_table_sort: default_backend_table_sort(),
@@ -135,6 +203,103 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE
"invalid administration theme preference".into(),
));
}
if preferences.language != "auto"
&& (preferences.language.is_empty()
|| preferences.language.len() > 64
|| !preferences
.language
.chars()
.all(|value| value.is_ascii_alphanumeric() || matches!(value, '_' | '-')))
{
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration language preference".into(),
));
}
if !(70..=125).contains(&preferences.ui_scale) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration interface scale preference".into(),
));
}
if !matches!(preferences.time_format.as_str(), "24h" | "12h") {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration time format preference".into(),
));
}
if !matches!(
preferences.chart_style.as_str(),
"default"
| "soft"
| "technical"
| "high_contrast"
| "neon"
| "monochrome"
| "warm"
| "cool"
) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration chart style preference".into(),
));
}
if !matches!(preferences.button_style.as_str(), "normal" | "compact") {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration button style preference".into(),
));
}
if !matches!(
preferences.table_density.as_str(),
"normal" | "compact" | "ultra"
) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration table density preference".into(),
));
}
if !matches!(
preferences.dashboard_density.as_str(),
"normal" | "compact" | "ultra"
) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration dashboard density preference".into(),
));
}
if preferences.dashboard_widgets.is_empty()
|| preferences.dashboard_widgets.len() > DASHBOARD_WIDGETS.len()
{
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration dashboard widget order".into(),
));
}
let mut seen = std::collections::HashSet::new();
for widget in &preferences.dashboard_widgets {
if !DASHBOARD_WIDGETS.contains(&widget.as_str()) || !seen.insert(widget.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration dashboard widget order".into(),
));
}
}
if preferences.dashboard_hidden_widgets.len() > DASHBOARD_WIDGETS.len() {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration hidden dashboard widgets".into(),
));
}
let mut seen = std::collections::HashSet::new();
for widget in &preferences.dashboard_hidden_widgets {
if !DASHBOARD_WIDGETS.contains(&widget.as_str()) || !seen.insert(widget.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid administration hidden dashboard widgets".into(),
));
}
}
if preferences.backend_table_columns.is_empty()
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
{
+10
View File
@@ -33,6 +33,7 @@ fn admin_app_javascript() -> &'static str {
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
[
include_str!("web/app/core.js"),
include_str!("web/app/preferences.js"),
include_str!("web/app/config-history.js"),
include_str!("web/app/runtime.js"),
include_str!("web/app/settings.js"),
@@ -148,6 +149,15 @@ pub fn router(state: Arc<App>) -> Router {
)
}),
)
.route(
"/assets/drafts.js",
get(|| async {
(
[("content-type", "text/javascript; charset=utf-8")],
include_str!("web/drafts.js"),
)
}),
)
.route(
"/assets/integrations.js",
get(|| async {
+1
View File
@@ -5,4 +5,5 @@ mod tests {
include!("tests/part_04.rs");
include!("tests/part_05.rs");
include!("tests/part_06.rs");
include!("tests/part_07.rs");
}
+41 -59
View File
@@ -15,11 +15,13 @@
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let initial = json_body(response).await;
assert_eq!(
initial["live_log_columns"].as_array().unwrap().len(),
LIVE_LOG_COLUMNS.len()
);
assert_eq!(initial["live_log_columns"].as_array().unwrap().len(), LIVE_LOG_COLUMNS.len());
assert_eq!(initial["theme"], "dark");
assert_eq!(initial["language"], "auto");
assert_eq!(initial["ui_scale"], 100);
assert_eq!(initial["time_format"], "24h");
assert_eq!(initial["chart_style"], "default");
assert_eq!(initial["button_style"], "normal");
assert_eq!(
initial["file_log_columns"].as_array().unwrap().len(),
default_file_log_columns().len()
@@ -49,6 +51,11 @@
let legacy = json_body(response).await;
assert_eq!(legacy["live_log_columns"], json!(["time", "status"]));
assert_eq!(legacy["theme"], "dark");
assert_eq!(legacy["language"], "auto");
assert_eq!(legacy["ui_scale"], 100);
assert_eq!(legacy["time_format"], "24h");
assert_eq!(legacy["chart_style"], "default");
assert_eq!(legacy["button_style"], "normal");
assert_eq!(legacy["file_log_columns"], json!(default_file_log_columns()));
assert_eq!(
legacy["backend_table_columns"].as_array().unwrap().len(),
@@ -60,6 +67,15 @@
let selected = json!({
"live_log_columns":["time","request","status","upstream_error"],
"theme":"light",
"language":"pl_pl",
"ui_scale":70,
"time_format":"12h",
"chart_style":"neon",
"button_style":"compact",
"table_density":"ultra",
"dashboard_density":"ultra",
"dashboard_widgets":["errors","requests","active","bandwidth","cache","memory","websocket","http2","http3","tls","http_protocol","websocket_protocol"],
"dashboard_hidden_widgets":["http3","tls"],
"file_log_columns":["time","event","client_ip","record"],
"backend_table_columns":["vhost","backend","status","requests","latency"],
"backend_table_sort":"requests",
@@ -140,6 +156,26 @@
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
let response = router
.clone()
.oneshot(
request("/api/preferences")
.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(
r#"{"live_log_columns":["time"],"ui_scale":150}"#,
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
let response = router
.oneshot(
request("/api/preferences")
@@ -320,6 +356,7 @@
"/assets/langs.json",
"/assets/app.js",
"/assets/management.js",
"/assets/drafts.js",
"/assets/integrations.js",
"/assets/traffic-panel.js",
"/assets/cache-panel.js",
@@ -408,58 +445,3 @@
}
store.close().await;
}
#[tokio::test]
async fn public_document_and_discovery_contain_only_login_information() {
let (router, store, _dir, _, _) = fixture().await;
for (path, expected) in [
("/healthz", json!({"status":"ok"})),
("/api/auth/status", json!({"setup_required":false})),
] {
let response = router
.clone()
.oneshot(request(path).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(json_body(response).await, expected);
}
let response = router
.clone()
.oneshot(request("/").body(Body::empty()).unwrap())
.await
.unwrap();
let bytes = axum::body::to_bytes(response.into_body(), 100_000)
.await
.unwrap();
let text = std::str::from_utf8(&bytes).unwrap();
assert!(text.contains("/assets/login.js"));
for absent in [
"/assets/app.js",
"id=\"workspace\"",
"loginLead",
"config-editor",
"version",
] {
assert!(!text.contains(absent), "unexpected public content {absent}");
}
let response = router
.clone()
.oneshot(
request("/assets/login-langs.json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let bundle = json_body(response).await;
let keys: Vec<String> = serde_json::from_str(include_str!("../web/login-keys.json")).unwrap();
for language in bundle["languages"].as_array().unwrap() {
assert!(language["translations"]
.as_object()
.unwrap()
.keys()
.all(|key| keys.contains(key)));
assert!(language["translations"].get("cacheTitle").is_none());
}
store.close().await;
}
+55
View File
@@ -0,0 +1,55 @@
#[tokio::test]
async fn public_document_and_discovery_contain_only_login_information() {
let (router, store, _dir, _, _) = fixture().await;
for (path, expected) in [
("/healthz", json!({"status":"ok"})),
("/api/auth/status", json!({"setup_required":false})),
] {
let response = router
.clone()
.oneshot(request(path).body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(json_body(response).await, expected);
}
let response = router
.clone()
.oneshot(request("/").body(Body::empty()).unwrap())
.await
.unwrap();
let bytes = axum::body::to_bytes(response.into_body(), 100_000)
.await
.unwrap();
let text = std::str::from_utf8(&bytes).unwrap();
assert!(text.contains("/assets/login.js"));
for absent in [
"/assets/app.js",
"id=\"workspace\"",
"loginLead",
"config-editor",
"version",
] {
assert!(!text.contains(absent), "unexpected public content {absent}");
}
let response = router
.clone()
.oneshot(
request("/assets/login-langs.json")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let bundle = json_body(response).await;
let keys: Vec<String> = serde_json::from_str(include_str!("../web/login-keys.json")).unwrap();
for language in bundle["languages"].as_array().unwrap() {
assert!(language["translations"]
.as_object()
.unwrap()
.keys()
.all(|key| keys.contains(key)));
assert!(language["translations"].get("cacheTitle").is_none());
}
store.close().await;
}
+1 -1
View File
@@ -87,7 +87,7 @@
renewalLogs = logs || {};
const list = $("renewal-list"); list.replaceChildren();
const certificates = envelope?.config.tls?.certificates || [];
const stamp = value => value ? new Date(value * 1000).toLocaleString(locale()) : "-";
const stamp = value => value ? formatDateTime(value * 1000) : "-";
for (const cert of certificates.filter(c => c.issuer)) {
const state = values[cert.id], enabled = cert.issuer.auto_renew !== false;
const entry = el("div", undefined, "renewal-entry"); entry.dataset.state = state?.status || "idle";
+4 -2
View File
@@ -1,7 +1,9 @@
let configHistoryEntries = [], configHistorySelectedRevision = null;
function historyDate(value, options = {}) {
try { return new Intl.DateTimeFormat(locale(), options).format(new Date(Number(value) * 1000)); }
catch { return new Date(Number(value) * 1000).toLocaleString(); }
const effective = { ...options };
if (Object.hasOwn(effective, "timeStyle") || Object.hasOwn(effective, "hour")) effective.hour12 = (adminPreferences?.time_format || "24h") === "12h";
try { return new Intl.DateTimeFormat(locale(), effective).format(new Date(Number(value) * 1000)); }
catch { return formatDateTime(Number(value) * 1000); }
}
function historyDayKey(value) {
const date = new Date(Number(value) * 1000);
+11 -57
View File
@@ -1,7 +1,7 @@
"use strict";
const $ = (id) => document.getElementById(id);
let language = null;
try { language = localStorage.getItem("rp-language"); } catch { }
let language = null, languagePreference = null;
try { languagePreference = localStorage.getItem("rp-language"); language = languagePreference; } catch { }
let csrf = "", authenticated = false, setupRequired = false, sessionInfo = null;
const ORIGIN_RECOVERY_KEY = "rp-origin-recovery-v1";
function recoverInvalidOrigin(error) {
@@ -20,47 +20,6 @@ function recoverInvalidOrigin(error) {
function clearInvalidOriginRecovery() {
try { sessionStorage.removeItem(ORIGIN_RECOVERY_KEY); } catch { recoverInvalidOrigin.attempted = false; }
}
const ADMIN_THEMES = new Set(["dark", "light"]);
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve();
function normalizeAdminTheme(value) { return ADMIN_THEMES.has(value) ? value : "dark"; }
function applyAdminTheme(value, cache = true) {
const theme = normalizeAdminTheme(value);
document.documentElement.dataset.theme = theme;
const select = $("ui-theme"); if (select) select.value = theme;
if (cache) { try { localStorage.setItem("rp-theme", theme); } catch { } }
return theme;
}
try { applyAdminTheme(localStorage.getItem("rp-theme") || "dark", false); } catch { applyAdminTheme("dark", false); }
async function loadAdminPreferences(force = false) {
if (!authenticated) return adminPreferences;
if (adminPreferences && !force) return adminPreferences;
if (adminPreferencesLoading) return adminPreferencesLoading;
const epoch = authEpoch;
adminPreferencesLoading = (async () => {
const value = await api("/api/preferences");
if (!authenticated || epoch !== authEpoch) return adminPreferences;
adminPreferences = { ...value, theme: applyAdminTheme(value?.theme) };
return adminPreferences;
})();
try { return await adminPreferencesLoading; } finally { adminPreferencesLoading = null; }
}
function updateAdminPreferences(patch) {
const epoch = authEpoch;
const task = adminPreferencesWrite.then(async () => {
if (!authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
const current = await loadAdminPreferences();
if (!current || !authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
const next = { ...current, ...patch }; next.theme = normalizeAdminTheme(next.theme);
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
if (!authenticated || epoch !== authEpoch) return adminPreferences;
adminPreferences = { ...saved, theme: applyAdminTheme(saved?.theme) };
return adminPreferences;
});
adminPreferencesWrite = task.catch(() => undefined);
return task;
}
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme };
const PANEL_PATHS = { overview: "/overview", vhosts: "/vhosts", "web-server": "/web-server", "docker-vhosts": "/docker-vhosts", redirects: "/redirects", notfound: "/404-hosts", streams: "/streams", "acl-profiles": "/acl-profiles", tls: "/tls", traffic: "/traffic", cache: "/cache", transform: "/transform", filelogs: "/logs", settings: "/settings" };
function panelFromLocation() { if (typeof location === "undefined") return "overview"; if (location.pathname === "/configuration" || location.pathname === "/logging") return "settings"; const found = Object.entries(PANEL_PATHS).find(([, path]) => path === location.pathname); return found?.[0] || "overview"; }
let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null;
@@ -83,8 +42,9 @@ async function loadLanguages() {
const packs = [...languagePacks.values()];
const selected = candidates.map(value => packs.find(p => p.id.replaceAll("_", "-").toLowerCase() === value || p.meta.locale.toLowerCase() === value) || packs.find(p => p.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean);
language = selected?.id || defaultLanguage;
$("language").replaceChildren(...packs.map(pack => { const option = el("option", pack.meta.native_name); option.value = pack.id; return option; }));
$("language").value = language;
const automatic = el("option", t("languageAutomatic")); automatic.value = "auto"; automatic.dataset.i18n = "languageAutomatic";
$("language").replaceChildren(automatic, ...packs.map(pack => { const option = el("option", pack.meta.native_name); option.value = pack.id; return option; }));
$("language").value = languagePreference === "auto" ? "auto" : language;
}
let latestStatus = null, certificateInventory = new Map();
let selectedCacheMemoryPreset = null, selectedCacheColdPreset = null, selectedTransformPreset = null;
@@ -94,7 +54,9 @@ function translate() {
document.querySelectorAll("[data-i18n]").forEach(e => e.textContent = t(e.dataset.i18n));
document.querySelectorAll("[data-i18n-placeholder]").forEach(e => e.placeholder = t(e.dataset.i18nPlaceholder));
document.querySelectorAll("[data-i18n-aria]").forEach(e => e.setAttribute("aria-label", t(e.dataset.i18nAria)));
$("language").value = language; setPanel(currentPanel, false); syncAuthForm(); renderSession();
$("language").value = languagePreference === "auto" ? "auto" : language; setPanel(currentPanel, false); syncAuthForm(); renderSession();
window.rustProxyDrafts?.refresh();
window.rustProxyPreferences?.refreshAppearanceUi?.();
$("connection-label").textContent = t(connected ? "connected" : "disconnected");
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); } if (lastLogs) renderLogs(lastLogs);
syncSettingsDirtyIndicators();
@@ -332,7 +294,7 @@ async function saveRequest(path, payload) { if (busy) { toast(t("busy"), true);
async function save(config) { return saveRequest("/api/config", { expected_revision: envelope?.revision, config }); }
async function saveSection(section, value) { return saveRequest(`/api/config/${section}`, { expected_revision: envelope?.revision, value }); }
function syncAuthForm() { $("setup-fields").hidden = !setupRequired; $("confirm-field").hidden = !setupRequired; $("setup-token").required = setupRequired; $("password-confirm").required = setupRequired; $("password").minLength = setupRequired ? 12 : 1; $("password").autocomplete = setupRequired ? "new-password" : "current-password"; $("auth-title").textContent = t(setupRequired ? "setupTitle" : "signIn"); $("login-submit").textContent = t(setupRequired ? "createAndSignIn" : "signIn"); $("auth-badge").textContent = setupRequired ? "SETUP" : "LOCAL"; }
function renderSession() { $("session-user").textContent = sessionInfo?.username || ""; $("session-expiry").textContent = sessionInfo ? t("expires") + new Date(sessionInfo.expires_at * 1000).toLocaleString(locale()) : ""; }
function renderSession() { $("session-user").textContent = sessionInfo?.username || ""; $("session-expiry").textContent = sessionInfo ? t("expires") + formatDateTime(sessionInfo.expires_at * 1000) : ""; }
function authReturnDestination() {
if (typeof location === "undefined") return "/";
if (location.pathname === "/logging") return "/settings?tab=logging";
@@ -344,7 +306,7 @@ function authReturnDestination() {
}
function lock(notify = true) {
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
$("workspace").querySelectorAll("form").forEach(form => form.reset());
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
@@ -356,7 +318,7 @@ function lock(notify = true) {
}
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-menu").hidden = false; $("logs-open").hidden = false; $("file-logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyFileLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [new Date(item.time * 1000).toLocaleString(locale()), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [formatDateTime(item.time * 1000), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
let tunnelRefreshTimer = null;
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
async function loadTunnels() {
@@ -394,14 +356,6 @@ $("login-form").addEventListener("submit", async event => { event.preventDefault
$("logout").addEventListener("click", async () => { try { await api("/api/auth/logout", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { if (e.status === 401) lock(); toast(e.message, true); } });
$("about-open").addEventListener("click", () => { const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
$("about-close").addEventListener("click", () => $("about-dialog").close());
$("language").addEventListener("change", () => { language = $("language").value; try { localStorage.setItem("rp-language", language); } catch { } translate(); });
$("ui-theme").addEventListener("change", async () => {
const select = $("ui-theme"), previousTheme = adminPreferences?.theme || document.documentElement.dataset.theme || "dark", nextTheme = normalizeAdminTheme(select.value);
applyAdminTheme(nextTheme); select.disabled = true;
try { await updateAdminPreferences({ theme: nextTheme }); }
catch (error) { applyAdminTheme(previousTheme); if (error.status !== 401) toast(error.message, true); }
finally { select.disabled = false; }
});
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");
function setMobileMenu(open) {
if (!mobileMenuToggle || !dashboardSidebar) return;
+230
View File
@@ -0,0 +1,230 @@
const ADMIN_THEMES = new Set(["dark", "light"]);
const ADMIN_TIME_FORMATS = new Set(["24h", "12h"]);
const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool"]);
const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]);
const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]);
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(["requests", "active", "bandwidth", "errors", "cache", "memory", "websocket", "http2", "http3", "tls", "http_protocol", "websocket_protocol"]);
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
button_style: "normal", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [],
});
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve();
function normalizeAdminTheme(value) { return ADMIN_THEMES.has(value) ? value : "dark"; }
function normalizeAdminLanguage(value) { return value === "auto" || /^[A-Za-z0-9_-]{1,64}$/.test(value || "") ? value : "auto"; }
function normalizeAdminUiScale(value) { const numeric = Number(value); return Number.isInteger(numeric) ? Math.min(125, Math.max(70, numeric)) : 100; }
function normalizeAdminTimeFormat(value) { return ADMIN_TIME_FORMATS.has(value) ? value : "24h"; }
function normalizeAdminChartStyle(value) { return ADMIN_CHART_STYLES.has(value) ? value : "default"; }
function normalizeAdminButtonStyle(value) { return ADMIN_BUTTON_STYLES.has(value) ? value : "normal"; }
function normalizeAdminDensity(value, fallback = "normal") { return ADMIN_DENSITIES.has(value) ? value : fallback; }
function normalizeDashboardWidgets(value) {
const result = [], seen = new Set();
for (const id of Array.isArray(value) ? value : []) if (ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id)) { seen.add(id); result.push(id); }
for (const id of ADMIN_DASHBOARD_WIDGETS) if (!seen.has(id)) result.push(id);
return result;
}
function normalizeHiddenDashboardWidgets(value) {
const seen = new Set();
return (Array.isArray(value) ? value : []).filter(id => ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id) && seen.add(id));
}
function browserLanguage() {
const packs = [...languagePacks.values()], candidates = (navigator.languages || [navigator.language]).filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase());
return candidates.map(value => packs.find(pack => pack.meta.locale.toLowerCase() === value || pack.id.replaceAll("_", "-").toLowerCase() === value) || packs.find(pack => pack.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean)?.id || defaultLanguage;
}
function applyAdminTheme(value, cache = true) {
const theme = normalizeAdminTheme(value); document.documentElement.dataset.theme = theme;
const select = $("ui-theme"); if (select) select.value = theme;
if (cache) { try { localStorage.setItem("rp-theme", theme); } catch { } }
return theme;
}
function applyAdminLanguage(value, cache = true, rerender = true) {
const preference = normalizeAdminLanguage(value); languagePreference = preference;
if (languagePacks.size) language = preference === "auto" ? browserLanguage() : (languagePacks.has(preference) ? preference : browserLanguage());
const select = $("language"); if (select) select.value = preference === "auto" ? "auto" : language;
if (cache) { try { localStorage.setItem("rp-language", preference); } catch { } }
if (rerender && languagePacks.size) translate();
return preference;
}
function applyAdminUiScale(value, cache = true) {
const scale = normalizeAdminUiScale(value), root = document.documentElement, fieldScale = Math.min(1.1, Math.max(.9, 1 + (scale - 100) / 300));
root.style.setProperty("--ui-scale", String(scale / 100));
root.style.setProperty("--ui-field-font-size", `${(12 * fieldScale).toFixed(2)}px`);
root.style.setProperty("--ui-field-pad-y", `${(6 * fieldScale).toFixed(2)}px`);
root.style.setProperty("--ui-field-pad-x", `${(8 * fieldScale).toFixed(2)}px`);
root.style.setProperty("--ui-field-min-height", `${(27 * fieldScale).toFixed(2)}px`);
root.style.setProperty("--ui-check-size", `${(15 * fieldScale).toFixed(2)}px`);
root.style.setProperty("--ui-range-height", `${(18 * fieldScale).toFixed(2)}px`);
const slider = $("ui-scale"), output = $("ui-scale-value"); if (slider) slider.value = String(scale); if (output) output.textContent = `${scale}%`;
if (cache) { try { localStorage.setItem("rp-ui-scale", String(scale)); } catch { } }
return scale;
}
function applyAdminTimeFormat(value) { const format = normalizeAdminTimeFormat(value); const select = $("ui-time-format"); if (select) select.value = format; return format; }
function applyAdminChartStyle(value) {
const style = normalizeAdminChartStyle(value); document.documentElement.dataset.chartStyle = style;
const select = $("ui-chart-style"); if (select) select.value = style; refreshChartStylePalette(style); return style;
}
function applyAdminButtonStyle(value) {
const style = normalizeAdminButtonStyle(value); document.documentElement.dataset.buttonStyle = style;
const select = $("ui-button-style"); if (select) select.value = style; return style;
}
function applyAdminTableDensity(value) {
const density = normalizeAdminDensity(value); document.documentElement.dataset.tableDensity = density;
const select = $("ui-table-density"); if (select) select.value = density; return density;
}
function applyAdminDashboardDensity(value) {
const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = density;
const select = $("ui-dashboard-density"); if (select) select.value = density; return density;
}
function applyDashboardLayout(order, hidden) {
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden));
const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
for (const id of normalizedOrder) { const node = nodes.get(id); if (!node) continue; node.hidden = hiddenSet.has(id); grid.append(node); }
return { order: normalizedOrder, hidden: [...hiddenSet] };
}
function dashboardWidgetLabel(id) {
const node = document.querySelector(`[data-dashboard-widget="${CSS.escape(id)}"]`);
return node?.querySelector("[data-i18n]")?.textContent?.trim() || id;
}
function renderDashboardWidgetSettings() {
const list = $("dashboard-widget-list"); if (!list) return;
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
const rows = order.map((id, index) => {
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
const visible = document.createElement("input"); visible.type = "checkbox"; visible.checked = !hidden.has(id); visible.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
const label = el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label");
const actions = el("span", undefined, "dashboard-widget-setting-actions");
const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp"));
const down = el("button", "↓", "quiet"); down.type = "button"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("moveDown"));
visible.addEventListener("change", () => {
const previous = [...hidden], next = new Set(hidden); if (visible.checked) next.delete(id); else next.add(id);
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, visible, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous); renderDashboardWidgetSettings(); });
});
const move = delta => {
const previous = [...order], next = [...order], at = next.indexOf(id), target = at + delta; if (at < 0 || target < 0 || target >= next.length) return;
[next[at], next[target]] = [next[target], next[at]]; if (adminPreferences) adminPreferences.dashboard_widgets = next;
applyDashboardLayout(next, [...hidden]); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_widgets: next }, null, () => { if (adminPreferences) adminPreferences.dashboard_widgets = previous; applyDashboardLayout(previous, [...hidden]); renderDashboardWidgetSettings(); });
};
up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(visible, label, actions); return row;
});
list.replaceChildren(...rows);
}
function refreshChartStylePalette(active = adminPreferences?.chart_style || "default") {
const target = $("chart-style-palette"); if (!target || !languagePacks.size) return;
const buttons = [...ADMIN_CHART_STYLES].map(style => {
const button = el("button", undefined, "chart-style-choice"); button.type = "button"; button.dataset.chartPreset = style; button.classList.toggle("active", style === active); button.setAttribute("aria-pressed", String(style === active));
const swatches = el("span", undefined, "chart-style-swatches"); for (let i = 1; i <= 4; i++) swatches.append(el("i"));
button.append(swatches, el("span", t(CHART_STYLE_KEYS[style]))); button.addEventListener("click", () => { const select = $("ui-chart-style"); if (!select) return; select.value = style; select.dispatchEvent(new Event("change", { bubbles: true })); }); return button;
});
target.replaceChildren(...buttons);
}
function refreshAppearanceUi() { refreshChartStylePalette(); renderDashboardWidgetSettings(); window.rustProxySettings?.refreshSearch?.(); }
function formatDateTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
function formatTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleTimeString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
function applyAdminPreferences(value, cache = true) {
if (!value) return value;
const dashboard = applyDashboardLayout(value.dashboard_widgets, value.dashboard_hidden_widgets);
const normalized = {
...value,
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), button_style: applyAdminButtonStyle(value.button_style), table_density: applyAdminTableDensity(value.table_density),
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: dashboard.order, dashboard_hidden_widgets: dashboard.hidden,
};
adminPreferences = normalized;
if (languagePacks.size) translate();
refreshAppearanceUi(); return normalized;
}
try { applyAdminTheme(localStorage.getItem("rp-theme") || "dark", false); } catch { applyAdminTheme("dark", false); }
try { applyAdminUiScale(localStorage.getItem("rp-ui-scale") || 100, false); } catch { applyAdminUiScale(100, false); }
async function loadAdminPreferences(force = false) {
if (!authenticated) return adminPreferences;
if (adminPreferences && !force) return adminPreferences;
if (adminPreferencesLoading) return adminPreferencesLoading;
const epoch = authEpoch;
adminPreferencesLoading = (async () => {
const value = await api("/api/preferences");
if (!authenticated || epoch !== authEpoch) return adminPreferences;
return applyAdminPreferences(value);
})();
try { return await adminPreferencesLoading; } finally { adminPreferencesLoading = null; }
}
function updateAdminPreferences(patch) {
const epoch = authEpoch;
const task = adminPreferencesWrite.then(async () => {
if (!authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
const current = await loadAdminPreferences();
if (!current || !authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
const next = { ...current, ...patch };
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.button_style = normalizeAdminButtonStyle(next.button_style); next.table_density = normalizeAdminDensity(next.table_density);
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets);
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
if (!authenticated || epoch !== authEpoch) return adminPreferences;
return applyAdminPreferences(saved);
});
adminPreferencesWrite = task.catch(() => undefined); return task;
}
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme, refreshAppearanceUi };
async function saveAppearancePreference(patch, control, rollback) {
if (control) control.disabled = true;
try { await updateAdminPreferences(patch); }
catch (error) { rollback?.(); if (error.status !== 401) toast(error.message, true); }
finally { if (control) control.disabled = false; }
}
$("language")?.addEventListener("change", () => {
const control = $("language"), previous = adminPreferences?.language || "auto", next = normalizeAdminLanguage(control.value);
applyAdminLanguage(next); void saveAppearancePreference({ language: next }, control, () => applyAdminLanguage(previous));
});
$("ui-theme")?.addEventListener("change", () => {
const control = $("ui-theme"), previous = adminPreferences?.theme || "dark", next = normalizeAdminTheme(control.value);
applyAdminTheme(next); void saveAppearancePreference({ theme: next }, control, () => applyAdminTheme(previous));
});
$("ui-scale")?.addEventListener("input", () => applyAdminUiScale($("ui-scale").value));
$("ui-scale")?.addEventListener("change", () => {
const control = $("ui-scale"), previous = adminPreferences?.ui_scale || 100, next = normalizeAdminUiScale(control.value);
void saveAppearancePreference({ ui_scale: next }, control, () => applyAdminUiScale(previous));
});
$("ui-time-format")?.addEventListener("change", () => {
const control = $("ui-time-format"), previous = adminPreferences?.time_format || "24h", next = normalizeAdminTimeFormat(control.value);
applyAdminTimeFormat(next); if (adminPreferences) adminPreferences.time_format = next; translate();
void saveAppearancePreference({ time_format: next }, control, () => { if (adminPreferences) adminPreferences.time_format = previous; applyAdminTimeFormat(previous); translate(); });
});
$("ui-chart-style")?.addEventListener("change", () => {
const control = $("ui-chart-style"), previous = adminPreferences?.chart_style || "default", next = normalizeAdminChartStyle(control.value);
applyAdminChartStyle(next); if (adminPreferences) adminPreferences.chart_style = next; void saveAppearancePreference({ chart_style: next }, control, () => { if (adminPreferences) adminPreferences.chart_style = previous; applyAdminChartStyle(previous); });
});
$("ui-button-style")?.addEventListener("change", () => {
const control = $("ui-button-style"), previous = adminPreferences?.button_style || "normal", next = normalizeAdminButtonStyle(control.value);
applyAdminButtonStyle(next); if (adminPreferences) adminPreferences.button_style = next; void saveAppearancePreference({ button_style: next }, control, () => { if (adminPreferences) adminPreferences.button_style = previous; applyAdminButtonStyle(previous); });
});
$("ui-table-density")?.addEventListener("change", () => {
const control = $("ui-table-density"), previous = adminPreferences?.table_density || "normal", next = normalizeAdminDensity(control.value);
applyAdminTableDensity(next); if (adminPreferences) adminPreferences.table_density = next; void saveAppearancePreference({ table_density: next }, control, () => { if (adminPreferences) adminPreferences.table_density = previous; applyAdminTableDensity(previous); });
});
$("ui-dashboard-density")?.addEventListener("change", () => {
const control = $("ui-dashboard-density"), previous = adminPreferences?.dashboard_density || "compact", next = normalizeAdminDensity(control.value, "compact");
applyAdminDashboardDensity(next); if (adminPreferences) adminPreferences.dashboard_density = next; void saveAppearancePreference({ dashboard_density: next }, control, () => { if (adminPreferences) adminPreferences.dashboard_density = previous; applyAdminDashboardDensity(previous); });
});
async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") {
if (control) control.disabled = true; let previous = null;
try {
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets] } : null;
if (current) applyAdminPreferences({ ...current, ...patch }); await updateAdminPreferences(patch); toast(t(messageKey));
} catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); }
finally { if (control) control.disabled = false; }
}
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
const patches = {
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal" }, charts: { chart_style: "default" }, tables: { table_density: "normal" },
dashboard: { dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [] },
};
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
}));
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [] }, $("appearance-reset"), "appearanceResetDone"));
refreshAppearanceUi();
+2 -2
View File
@@ -62,7 +62,7 @@ function renderCertificates() {
const details = el("details", undefined, "certificate-details");
details.append(el("summary", t("certificateDetails")));
const detailBody = el("div", undefined, "certificate-details-body");
const stamp = value => Number(value) ? new Date(Number(value) * 1000).toLocaleString(locale()) : "-";
const stamp = value => Number(value) ? formatDateTime(Number(value) * 1000) : "-";
detailBody.append(
el("p", `${t("certificateBindings")}: ${bindings.join(", ") || "-"}`, "entity-details"),
el("p", `${t("certificateFiles")}: ${cert.cert_file} / ${cert.key_file}`, "entity-details"),
@@ -304,7 +304,7 @@ function renderDiagnostics(status, appendSample = true) {
setOptionalMetric("process-threads", d.process_threads != null, fmt(d.process_threads));
const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
setOptionalMetric("process-virtual-memory", virtualMemory != null, virtualMemory);
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : new Date(Number(d.process_start_time_seconds) * 1000).toLocaleString(locale()));
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000));
setOptionalMetric("process-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds));
setOptionalMetric("process-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes));
const memory = status.memory_accounting || {};
+1 -1
View File
@@ -6,7 +6,7 @@ function renderSettings() {
if (!settingsDirty.has("response-form")) {
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
$("expose-request-id").checked = !!diagnostics.request_id;
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.2";
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.3";
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
}
+2 -2
View File
@@ -155,7 +155,7 @@
const bans = data.bans || [];
$("cache-ban-rows").replaceChildren(...bans.map(rule => {
const tr = el("tr");
for (const value of [String(rule.id), rule.selector, rule.value, rule.host || "*", rule.vhost || "*", new Date(rule.expires_at_ms).toLocaleString(locale())]) tr.append(el("td", value));
for (const value of [String(rule.id), rule.selector, rule.value, rule.host || "*", rule.vhost || "*", formatDateTime(rule.expires_at_ms)]) tr.append(el("td", value));
const action = el("td"), button = el("button", t("cacheRemoveBan"), "quiet"); button.type = "button"; button.addEventListener("click", () => void removeBan(rule.id)); action.append(button); tr.append(action); return tr;
}));
$("cache-bans-empty").hidden = !!bans.length;
@@ -164,7 +164,7 @@
$("cache-objects-empty").hidden = !!d.objects?.length;
$("cache-page").textContent = `${d.matching_objects ? offset + 1 : 0}-${Math.min(offset + (d.objects?.length || 0), d.matching_objects || 0)} / ${number(d.matching_objects)}`;
$("cache-prev").disabled = offset === 0; $("cache-next").disabled = offset + 50 >= (d.matching_objects || 0);
$("cache-panel-state").textContent = `${t("cacheUpdated")}: ${new Date().toLocaleTimeString(locale())}`;
$("cache-panel-state").textContent = `${t("cacheUpdated")}: ${formatTime(new Date())}`;
$("cache-panel-cleanup").disabled = !data.settings?.cold_enabled;
}
async function runInvalidation(payload, messageTarget = "cache-invalidation-status") {
+5 -3
View File
@@ -166,9 +166,11 @@
async function boot() {
const mine = ++epoch; try {
const info = await get("/api/auth/session"); if (mine !== epoch) return;
const [documentSpec, bundle, docBundle] = await Promise.all([get("/docs/openapi.json"), get("/assets/langs.json"), get("/docs/content.json")]); if (mine !== epoch) return;
const packs = bundle.languages || []; fallback = packs.find(p => p.id === bundle.default)?.translations || {}; let selected = null; try { selected = localStorage.getItem("rp-language"); } catch { }
const pack = packs.find(p => p.id === selected) || packs.find(p => p.id === bundle.default); translations = pack?.translations || {};
const [documentSpec, bundle, docBundle, preferences] = await Promise.all([get("/docs/openapi.json"), get("/assets/langs.json"), get("/docs/content.json"), get("/api/preferences")]); if (mine !== epoch) return;
const packs = bundle.languages || []; fallback = packs.find(p => p.id === bundle.default)?.translations || {};
const selected = preferences?.language || "auto", candidates = selected === "auto" ? (navigator.languages || [navigator.language]) : [selected];
const normalized = candidates.filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase());
const pack = normalized.map(value => packs.find(p => p.id.replaceAll("_", "-").toLowerCase() === value || p.meta.locale.toLowerCase() === value) || packs.find(p => p.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean) || packs.find(p => p.id === bundle.default); translations = pack?.translations || {};
window.document.documentElement.lang = pack?.meta.locale || "und"; window.document.documentElement.dir = pack?.meta.direction || "ltr";
window.document.querySelectorAll("[data-i18n]").forEach(n => n.textContent = t(n.dataset.i18n)); window.document.querySelectorAll("[data-i18n-placeholder]").forEach(n => n.placeholder = t(n.dataset.i18nPlaceholder));
session = info; spec = documentSpec; documents = Array.isArray(docBundle.documents) ? docBundle.documents : []; docsByPath = new Map(documents.map(item => [item.path, item]));
+98
View File
@@ -0,0 +1,98 @@
"use strict";
(() => {
const editors = new Map();
const drafts = [];
let sequence = 0, collapsed = false;
const MAX_DRAFTS = 12;
const dock = el("aside", undefined, "draft-dock"); dock.id = "draft-dock"; dock.hidden = true;
const heading = el("div", undefined, "draft-dock-heading");
const title = el("strong", t("draftDockTitle")); title.dataset.i18n = "draftDockTitle";
const headingActions = el("div", undefined, "draft-dock-heading-actions");
const count = el("span", "0", "pill");
const toggle = el("button", "−", "draft-dock-toggle"); toggle.type = "button"; toggle.setAttribute("aria-controls", "draft-dock-list");
headingActions.append(count, toggle); heading.append(title, headingActions);
const list = el("div", undefined, "draft-dock-list"); list.id = "draft-dock-list"; dock.append(heading, list); document.body.append(dock);
function editorLabel(type) { const key = editors.get(type)?.labelKey; return key ? t(key) : type; }
function draftTitle(value) { return String(value || t("draftUntitled")).trim() || t("draftUntitled"); }
function draftAge(createdAt) {
const seconds = Math.max(0, Math.floor((Date.now() - createdAt) / 1000));
if (seconds < 60) return t("draftAgeNow");
const minutes = Math.floor(seconds / 60); if (minutes < 60) return t("draftAgeMinutes").replace("{count}", minutes);
const hours = Math.floor(minutes / 60); if (hours < 24) return t("draftAgeHours").replace("{count}", hours);
return t("draftAgeDays").replace("{count}", Math.floor(hours / 24));
}
function addDraft(type, packed) {
if (!packed?.state) return;
drafts.push({ id: ++sequence, type, title: draftTitle(packed.title), state: packed.state, createdAt: Date.now() });
while (drafts.length > MAX_DRAFTS) drafts.shift();
render(); toast(t("draftMinimized"));
}
function stashOpen() {
for (const [type, api] of editors) {
if (!api.isOpen?.()) continue;
if (api.isDirty?.()) addDraft(type, api.suspend());
else api.close?.();
}
}
async function restore(id) {
const index = drafts.findIndex(item => item.id === id); if (index < 0) return;
const draft = drafts[index], api = editors.get(draft.type); if (!api?.restore) return;
stashOpen(); drafts.splice(index, 1); render();
try { api.restore(draft.state); }
catch (error) { drafts.splice(Math.min(index, drafts.length), 0, draft); render(); toast(error.message || String(error), true); }
}
async function discard(id) {
const index = drafts.findIndex(item => item.id === id); if (index < 0) return;
if (!(await appConfirm(t("draftDiscardConfirm"), { danger: true, confirmLabel: t("discardAction") }))) return;
drafts.splice(index, 1); render();
}
function render() {
dock.hidden = drafts.length === 0; count.textContent = String(drafts.length); dock.classList.toggle("is-collapsed", collapsed);
list.hidden = collapsed; toggle.textContent = collapsed ? "+" : "−"; toggle.setAttribute("aria-expanded", String(!collapsed)); toggle.setAttribute("aria-label", t(collapsed ? "draftDockExpand" : "draftDockCollapse"));
list.replaceChildren();
for (const draft of drafts) {
const row = el("div", undefined, "draft-dock-item");
const open = el("button", undefined, "draft-dock-open"); open.type = "button"; open.setAttribute("aria-label", t("draftRestore"));
const identity = el("span", undefined, "draft-dock-identity"), meta = el("span", undefined, "draft-dock-meta");
meta.append(el("small", editorLabel(draft.type)), el("time", draftAge(draft.createdAt)));
identity.append(meta, el("b", draft.title)); open.append(identity); open.addEventListener("click", () => void restore(draft.id));
const remove = el("button", "×", "draft-dock-remove"); remove.type = "button"; remove.setAttribute("aria-label", t("draftDiscard")); remove.addEventListener("click", () => void discard(draft.id));
row.append(open, remove); list.append(row);
}
}
toggle.addEventListener("click", () => { collapsed = !collapsed; render(); });
function register(type, api) { editors.set(type, api); render(); }
function dockEditor(type) {
const api = editors.get(type); if (!api?.isOpen?.()) return false;
if (api.isDirty?.()) addDraft(type, api.suspend()); else api.close?.();
return true;
}
function prepare() { stashOpen(); }
function reset() { for (const api of editors.values()) api.close?.(); drafts.splice(0); collapsed = false; render(); }
function captureControls(form) {
return [...form.elements].map((control, index) => ({
index, id: control.id || "", name: control.name || "", type: control.type || control.tagName.toLowerCase(),
value: control.value, checked: "checked" in control ? control.checked : null,
}));
}
function restoreControls(form, state) {
const controls = [...form.elements];
for (const saved of state || []) {
let control = saved.id ? form.querySelector(`#${CSS.escape(saved.id)}`) : null;
if (!control && saved.name) {
const same = [...form.querySelectorAll(`[name="${CSS.escape(saved.name)}"]`)]; control = same.find(item => controls.indexOf(item) === saved.index) || same[0];
}
if (!control) control = controls[saved.index]; if (!control) continue;
if (saved.checked !== null && "checked" in control) control.checked = saved.checked;
if (control.type !== "file") control.value = saved.value;
}
}
const takeChildren = node => { const children = [...node.childNodes]; node.replaceChildren(); return children; };
const restoreChildren = (node, children) => node.replaceChildren(...(children || []));
if (typeof setInterval === "function") setInterval(() => { if (drafts.length && !collapsed) render(); }, 60000);
window.rustProxyDrafts = { register, dock: dockEditor, prepare, reset, refresh: render, captureControls, restoreControls, takeChildren, restoreChildren };
})();
+1 -1
View File
@@ -37,7 +37,7 @@
const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
function entryTime(entry) {
if (entry.time_ms) return new Date(Number(entry.time_ms)).toLocaleString(locale());
if (entry.time_ms) return formatDateTime(Number(entry.time_ms));
if (entry.timestamp) return String(entry.timestamp);
return "—";
}
+181 -39
View File
@@ -10,6 +10,7 @@
<link href="/assets/style.css" rel="stylesheet" />
<script defer="" src="/assets/app.js"></script>
<script defer="" src="/assets/management.js"></script>
<script defer="" src="/assets/drafts.js"></script>
<script defer="" src="/assets/integrations.js"></script>
<script defer="" src="/assets/vhosts.js"></script>
<script defer="" src="/assets/web-server.js"></script>
@@ -71,8 +72,7 @@
id="file-logs-open" type="button">Stored logs</button><button class="quiet"
data-i18n="liveLogs" hidden="" id="logs-open" type="button">Live access logs</button>
</div>
</details><select aria-label="Language" class="language-select" data-i18n-aria="language"
id="language"></select><button class="quiet" data-i18n="about" id="about-open"
</details><button class="quiet" data-i18n="about" id="about-open"
type="button">About</button><button class="quiet" data-i18n="logout" hidden=""
id="logout">Lock</button>
</div>
@@ -117,24 +117,30 @@
</section>
<section class="panel" id="panel-overview">
<div class="stats-grid">
<article class="stat"><span data-i18n="requests">Requests</span><strong
id="metric-requests">0</strong><small id="request-rate">0 req/s</small></article>
<article class="stat"><span data-i18n="activeExchanges">Active exchanges</span><strong
id="metric-active">0</strong><small data-i18n="bodyLifetime">Full body lifetime</small>
<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"><span data-i18n="bandwidth">Payload transferred</span><strong
id="metric-bytes">0 B</strong><small class="payload-split"
<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"><span data-i18n="errors">Errors</span><strong
id="metric-errors">0</strong><small data-i18n="errorDescription">5xx / body /
<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"><span data-i18n="cacheHitRatio">Cache hit ratio</span><strong
id="metric-cache-ratio">-</strong><small id="cache-summary">-</small></article>
<article class="stat"><span data-i18n="processMemory">Process memory</span><strong
id="metric-rss">-</strong><small id="process-summary">-</small></article>
<article class="stat stat-with-action"><span data-i18n="wsActive">WebSocket
<article class="stat" data-dashboard-widget="cache"><span data-i18n="cacheHitRatio">Cache hit
ratio</span><strong id="metric-cache-ratio">-</strong><small
id="cache-summary">-</small></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
@@ -144,14 +150,16 @@
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"><span data-i18n="h2Requests">HTTP/2 requests</span><strong
id="metric-h2">0</strong><small><span data-i18n="h2ActiveConnections">Active H2
<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"><span data-i18n="h3Requests">HTTP/3 requests</span><strong
id="metric-h3">0</strong></article>
<article class="stat"><span data-i18n="tlsFailures">TLS handshake errors</span><strong
id="metric-tls-errors">0</strong></article>
<article class="stat stat-chart"><span data-i18n="httpProtocolMix">HTTP protocol
<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
aria-label="HTTP protocol activity" data-i18n-aria="httpProtocolMix"
id="http-protocol-chart" role="img" viewBox="0 0 300 60">
@@ -166,7 +174,8 @@
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"><span data-i18n="wsProtocolMix">WebSocket protocol
<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
aria-label="WebSocket protocol activity" data-i18n-aria="wsProtocolMix"
id="ws-protocol-chart" role="img" viewBox="0 0 300 60">
@@ -1569,21 +1578,21 @@
response policies also have per-vhost controls.</p>
</div><button class="outline" data-i18n="security" id="settings-security-open"
type="button">Security</button><button class="quiet" data-i18n="vhosts"
id="settings-vhost-open" type="button">Vhosts</button><label
class="ui-preference-control"><span data-i18n="theme">Theme</span><select id="ui-theme">
<option data-i18n="themeDark" value="dark">Dark</option>
<option data-i18n="themeLight" value="light">Light</option>
</select></label><label class="ui-preference-control"><span data-i18n="density">Interface
density</span><select id="ui-density">
<option data-i18n="densityCompact" value="compact">Compact</option>
<option data-i18n="densityDense" value="dense">Dense</option>
<option data-i18n="densityComfortable" value="comfortable">Comfortable</option>
</select></label>
id="settings-vhost-open" type="button">Vhosts</button>
</div>
<div class="settings-filter-row">
<label class="settings-filter"><span class="small-label" data-i18n="settingsSearch">Find
settings</span><input autocomplete="off"
data-i18n-placeholder="settingsSearchPlaceholder" id="settings-search"
placeholder="Search settings..." type="search" /></label>
<span class="fine" id="settings-search-status" role="status"></span>
</div>
<nav aria-label="Settings" class="settings-tabs" id="settings-tabs" role="tablist"><button
aria-controls="settings-page-listeners" aria-selected="false" data-i18n="listeners"
data-settings-tab="listeners" id="settings-tab-listeners" role="tab" tabindex="-1"
type="button"></button><button aria-controls="settings-page-connections"
aria-controls="settings-page-appearance" aria-selected="false" data-i18n="appearance"
data-settings-tab="appearance" id="settings-tab-appearance" role="tab" tabindex="-1"
type="button"></button><button aria-controls="settings-page-listeners" aria-selected="false"
data-i18n="listeners" data-settings-tab="listeners" id="settings-tab-listeners" role="tab"
tabindex="-1" type="button"></button><button aria-controls="settings-page-connections"
aria-selected="false" data-i18n="httpConnections" data-settings-tab="connections"
id="settings-tab-connections" role="tab" tabindex="-1" type="button"></button><button
aria-controls="settings-page-docker-agents" aria-selected="false" data-i18n="dockerAgents"
@@ -1622,6 +1631,139 @@
data-settings-tab="security" id="settings-tab-security" role="tab" tabindex="-1"
type="button"></button></nav>
<div class="settings-pages" id="settings-pages">
<div aria-labelledby="settings-tab-appearance" class="settings-page"
data-settings-page="appearance" hidden="" id="settings-page-appearance" role="tabpanel">
<div class="card edit-form appearance-settings-card">
<div class="card-heading">
<div><span class="small-label" data-i18n="appearanceEyebrow">INTERFACE</span>
<h2 data-i18n="appearance">Appearance</h2>
</div>
</div>
<p class="fine" data-i18n="appearanceHelp">These preferences are saved for your
administrator account and follow you between sessions.</p>
<section class="appearance-section">
<div class="appearance-section-heading">
<div><span class="small-label"
data-i18n="appearanceInterfaceSection">INTERFACE</span>
<h3 data-i18n="appearanceInterfaceTitle">Interface</h3>
</div><button class="quiet appearance-section-reset"
data-appearance-reset="interface" data-i18n="resetSection"
type="button">Reset section</button>
</div>
<div class="appearance-settings-grid">
<label><span data-i18n="theme">Theme</span><select id="ui-theme">
<option data-i18n="themeDark" value="dark">Dark</option>
<option data-i18n="themeLight" value="light">Light</option>
</select></label>
<label><span data-i18n="language">Language</span><select aria-label="Language"
data-i18n-aria="language" id="language"></select></label>
<label class="appearance-scale-control"><span><span
data-i18n="interfaceScale">Interface scale</span>
<output id="ui-scale-value" for="ui-scale">100%</output></span>
<input id="ui-scale" max="125" min="70" step="5" type="range" value="100" />
<small class="fine" data-i18n="interfaceScaleHelp">Scale the entire
administration interface, including dialogs and overlays, without
changing browser zoom.</small></label>
<label><span data-i18n="timeFormat">Time format</span><select
id="ui-time-format">
<option data-i18n="timeFormat24" value="24h">24-hour</option>
<option data-i18n="timeFormat12" value="12h">12-hour</option>
</select></label>
<label><span data-i18n="buttonStyle">Button style</span><select
id="ui-button-style">
<option data-i18n="buttonStyleNormal" value="normal">Normal</option>
<option data-i18n="buttonStyleCompact" value="compact">Compact</option>
</select><small class="fine" data-i18n="buttonStyleHelp">Normal uses the
refreshed controls; Compact restores the smaller source-style buttons
across all actions and dialog tabs.</small></label>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-heading">
<div><span class="small-label" data-i18n="appearanceChartsSection">CHARTS</span>
<h3 data-i18n="chartStyle">Chart style</h3>
</div><button class="quiet appearance-section-reset"
data-appearance-reset="charts" data-i18n="resetSection" type="button">Reset
section</button>
</div>
<div class="appearance-settings-grid appearance-chart-settings">
<label><span data-i18n="chartStyle">Chart style</span><select
id="ui-chart-style">
<option data-i18n="chartStyleDefault" value="default">Default</option>
<option data-i18n="chartStyleSoft" value="soft">Soft</option>
<option data-i18n="chartStyleTechnical" value="technical">Technical
</option>
<option data-i18n="chartStyleHighContrast" value="high_contrast">High
contrast</option>
<option data-i18n="chartStyleNeon" value="neon">Neon</option>
<option data-i18n="chartStyleMonochrome" value="monochrome">Monochrome
</option>
<option data-i18n="chartStyleWarm" value="warm">Warm</option>
<option data-i18n="chartStyleCool" value="cool">Cool</option>
</select></label>
<div class="appearance-chart-preview" aria-hidden="true">
<svg viewBox="0 0 300 74" preserveAspectRatio="none">
<path class="chart-grid" d="M0 18H300 M0 37H300 M0 56H300"></path>
<polyline class="appearance-chart-primary"
points="0,58 48,42 92,50 138,25 182,35 228,16 300,29"></polyline>
<polyline class="appearance-chart-secondary"
points="0,63 48,56 92,38 138,45 182,27 228,37 300,19"></polyline>
</svg>
<span data-i18n="chartStylePreview">Preview</span>
</div>
</div>
<div class="chart-style-palette" id="chart-style-palette"
aria-label="Chart style palettes" data-i18n-aria="chartStylePalettes"></div>
</section>
<section class="appearance-section">
<div class="appearance-section-heading">
<div><span class="small-label" data-i18n="appearanceDataSection">TABLES /
LOGS</span>
<h3 data-i18n="tableDensity">Tables and logs density</h3>
</div><button class="quiet appearance-section-reset"
data-appearance-reset="tables" data-i18n="resetSection" type="button">Reset
section</button>
</div>
<div class="appearance-settings-grid">
<label><span data-i18n="tableDensity">Tables and logs density</span><select
id="ui-table-density">
<option data-i18n="densityNormal" value="normal">Normal</option>
<option data-i18n="densityCompact" value="compact">Compact</option>
<option data-i18n="densityUltra" value="ultra">Ultra compact</option>
</select><small class="fine" data-i18n="tableDensityHelp">Changes row height
and log spacing independently from interface scale.</small></label>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-heading">
<div><span class="small-label"
data-i18n="appearanceDashboardSection">DASHBOARD</span>
<h3 data-i18n="dashboardLayout">Dashboard layout</h3>
</div><button class="quiet appearance-section-reset"
data-appearance-reset="dashboard" data-i18n="resetSection"
type="button">Reset section</button>
</div>
<div class="appearance-settings-grid">
<label><span data-i18n="dashboardDensity">Dashboard density</span><select
id="ui-dashboard-density">
<option data-i18n="densityNormal" value="normal">Normal</option>
<option data-i18n="densityCompact" value="compact">Compact</option>
<option data-i18n="densityUltra" value="ultra">Ultra compact</option>
</select><small class="fine" data-i18n="dashboardDensityHelp">Controls
Overview telemetry card size without changing the rest of the
interface.</small></label>
<div class="dashboard-widget-settings"><span
data-i18n="dashboardWidgets">Overview cards</span><small class="fine"
data-i18n="dashboardWidgetsHelp">Show, hide and reorder the telemetry
cards at the top of Overview.</small>
<div class="dashboard-widget-list" id="dashboard-widget-list"></div>
</div>
</div>
</section>
<div class="appearance-actions"><button class="outline" data-i18n="appearanceReset"
id="appearance-reset" type="button">Reset appearance</button></div>
</div>
</div>
<div aria-labelledby="settings-tab-listeners" class="settings-page"
data-settings-page="listeners" hidden="" id="settings-page-listeners" role="tabpanel">
<form class="card edit-form" id="listeners-form">
@@ -1735,7 +1877,7 @@
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
</select>
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.2"
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.3"
spellcheck="false" />
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
<div class="switch-grid">
@@ -2534,7 +2676,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.26.2</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.26.3</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -3245,7 +3387,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card">
<div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.26.2</strong></div>
<div><span class="small-label">RustProxy</span><strong>0.26.3</strong></div>
</div>
<dl class="about-meta">
<div>
+1 -1
View File
@@ -5,7 +5,7 @@
const themes = new Set(["dark", "light"]);
try { const cached = localStorage.getItem("rp-theme"); document.documentElement.dataset.theme = themes.has(cached) ? cached : "dark"; } catch { document.documentElement.dataset.theme = "dark"; }
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/traffic", "/cache", "/transform", "/logging", "/logs", "/settings"]);
const settingsTabs = new Set(["listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
const settingsTabs = new Set(["appearance", "listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
let packs = new Map(), fallback = null, language = null, setupRequired = false, ready = false;
try { language = localStorage.getItem("rp-language"); } catch { }
const t = key => packs.get(language)?.translations?.[key] ?? packs.get(fallback)?.translations?.[key] ?? key;
+1 -4
View File
@@ -39,9 +39,6 @@
bindSettingsForm("pages-form", next => { next.pages = global.read(); }, "pages");
$("settings-security-open").addEventListener("click", () => setPanel("security"));
$("settings-vhost-open").addEventListener("click", () => setPanel("vhosts"));
const density = $("ui-density"); let chosen = "compact"; try { chosen = localStorage.getItem("rp-density") || chosen; } catch { }
if (!["compact", "dense", "comfortable"].includes(chosen)) chosen = "compact"; density.value = chosen; document.body.dataset.density = chosen;
density.addEventListener("change", () => { document.body.dataset.density = density.value; try { localStorage.setItem("rp-density", density.value); } catch { } });
if (envelope) render();
})();
@@ -49,7 +46,7 @@
const dialog = $("logs-dialog"); let paused = null, expanded = null, docked = false, pinning = false, preferencesLoaded = false, preferencesLoading = null, preferenceSaveRunning = false, preferenceSaveQueued = false;
const filters = ["log-vhost", "log-method", "log-status", "log-outcome", "log-filter"];
const columns = [
{ id: "time", label: "time", value: e => new Date(e.time_ms).toLocaleTimeString(locale()) },
{ id: "time", label: "time", value: e => formatTime(e.time_ms) },
{ id: "request", label: "request", value: e => `${e.method} ${e.path}` },
{ id: "host", label: "logHost", value: e => e.host },
{ id: "source_ip", label: "sourceIp", value: e => e.client_ip || "-" },
+20 -4
View File
@@ -74,6 +74,21 @@
const get = name => box.querySelector(`[data-field="${name}"]`);
return { id: get("id").value.trim(), enabled: get("enabled").checked, source_scheme: get("source_scheme").value, match_type: get("match_type").value, path_prefix: get("path_prefix").value.trim(), target_type: get("target_type").value, target: get("target").value.trim(), target_scheme: get("target_scheme").value, path_mode: get("path_mode").value, query_mode: get("query_mode").value, status_code: Number(get("status_code").value), cache_seconds: Number(get("cache_seconds").value) || 0 };
}
function suspendDraft() {
const state = {
editing, revision, label: $("redirect-editing").textContent, controls: window.rustProxyDrafts.captureControls(form),
hosts: window.rustProxyDrafts.takeChildren($("redirect-hosts-editor")), listeners: window.rustProxyDrafts.takeChildren($("redirect-listeners")), rules: window.rustProxyDrafts.takeChildren(rules),
readRedirectBindings, readRedirectHosts
};
const title = value("redirect-id") || editing || t("draftUntitled"); editing = null; revision = null; dirty = false; dialog.close(); return { title, state };
}
function restoreDraft(state) {
window.rustProxyDrafts.restoreChildren($("redirect-hosts-editor"), state.hosts); window.rustProxyDrafts.restoreChildren($("redirect-listeners"), state.listeners); window.rustProxyDrafts.restoreChildren(rules, state.rules);
readRedirectBindings = state.readRedirectBindings; readRedirectHosts = state.readRedirectHosts; editing = state.editing; revision = state.revision; dirty = true; $("redirect-editing").textContent = state.label;
window.rustProxyDrafts.restoreControls(form, state.controls); if (!dialog.open) dialog.showModal();
}
function closeEditor() { if (dirty) return window.rustProxyDrafts.dock("redirect"); if (dialog.open) dialog.close(); return true; }
function collect() {
const id = value("redirect-id"), name = value("redirect-name") || id, hosts = readRedirectHosts();
const listener_ids = readRedirectBindings();
@@ -87,7 +102,7 @@
list.replaceChildren(...entries.map(item => {
const card = el("article", undefined, "vhost-card redirect-card"); const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity"), title = el("div", undefined, "vhost-title-line");
title.append(el("strong", item.name || item.id, "entity-title")); if (item.enabled === false) title.append(el("span", t("vhDisabled"), "pill")); identity.append(title, el("div", (item.frontend?.hosts || []).join(" · "), "vhost-hosts")); head.append(identity);
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => load(structuredClone(item))), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.redirects || []).filter(value => value.id !== item.id); if (await saveSection("redirects", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head);
const actions = el("div", undefined, "entity-actions"); actions.append(action(t("edit"), () => { window.rustProxyDrafts.prepare(); load(structuredClone(item)); }), action(t("remove"), async () => { if (busy || !(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = (envelope.config.redirects || []).filter(value => value.id !== item.id); if (await saveSection("redirects", next)) { if (editing === item.id) dialog.close(); render(); } })); head.append(actions); card.append(head);
const targets = [...new Set((item.rules || []).filter(rule => rule.enabled !== false).map(rule => rule.target).filter(Boolean))];
const targetSummary = targets.slice(0, 2).join(" · ") + (targets.length > 2 ? ` · +${targets.length - 2}` : "");
const detail = el("div", undefined, "vhost-detail-strip"); for (const [k, v] of [["redirectTarget", targetSummary || "-"], ["redirectRules", String((item.rules || []).length)], ["redirectListeners", (item.frontend?.listener_ids || []).join(", ")], ["vhCertificate", item.certificate_id || t("vhCertificateDefault")]]) { const node = el("div", undefined, "vhost-detail"); node.append(el("span", t(k), "small-label"), el("b", v)); detail.append(node); } card.append(detail); return card;
@@ -100,9 +115,10 @@
if (revision !== envelope.revision) { toast(t("stale"), true); return; }
try { const item = collect(), next = structuredClone(envelope.config.redirects || []); if (!editing && next.some(value => value.id === item.id)) throw new Error(t("redirectDuplicate")); const index = next.findIndex(value => value.id === editing); if (index < 0) next.push(item); else next[index] = item; if (await saveSection("redirects", next)) { dirty = false; editing = item.id; dialog.close(); render(); } } catch (error) { toast(error.message, true); }
});
$("redirect-new").addEventListener("click", async () => { if (await allowDiscard()) load(); }); $("redirect-search").addEventListener("input", render); $("redirect-rule-add").addEventListener("click", () => { addRule({ id: `rule-${rules.children.length + 1}` }); changed(); });
$("redirect-close").addEventListener("click", async () => { if (await allowDiscard()) dialog.close(); }); $("redirect-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.redirects || []).find(item => item.id === editing) || defaults()) : defaults()); });
dialog.addEventListener("cancel", event => { if (!dirty) return; event.preventDefault(); void allowDiscard().then(ok => { if (ok) dialog.close(); }); });
$("redirect-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); }); $("redirect-search").addEventListener("input", render); $("redirect-rule-add").addEventListener("click", () => { addRule({ id: `rule-${rules.children.length + 1}` }); changed(); });
$("redirect-close").addEventListener("click", closeEditor); $("redirect-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.redirects || []).find(item => item.id === editing) || defaults()) : defaults()); });
dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
window.rustProxyDrafts.register("redirect", { labelKey: "redirects", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
window.rustProxyRedirects = { render, reset() { dialog.close(); editing = null; revision = null; dirty = false; list.replaceChildren(); rules.replaceChildren(); $("redirect-search").value = ""; } };
if (envelope) render();
})();
+21 -6
View File
@@ -2,6 +2,7 @@
// Persistent tab DOM: switching tabs never discards unsaved form values.
(() => {
const buttons = [...document.querySelectorAll("[data-settings-tab]")];
const search = $("settings-search"), searchStatus = $("settings-search-status");
const defaultTab = (typeof location !== "undefined" && location.pathname === "/configuration") ? "manual-config" : "listeners";
const requestedTab = typeof location !== "undefined" && location.pathname === "/logging"
? "logging"
@@ -9,6 +10,17 @@
const tabAliases = { http2: "connections" };
const normalizedRequestedTab = tabAliases[requestedTab] || requestedTab;
let active = buttons.some(button => button.dataset.settingsTab === normalizedRequestedTab) ? normalizedRequestedTab : defaultTab;
function applySettingsSearch() {
const query = String(search?.value || "").trim().toLocaleLowerCase();
const matches = [];
for (const button of buttons) {
const page = document.querySelector(`[data-settings-page="${CSS.escape(button.dataset.settingsTab)}"]`);
const haystack = `${button.textContent || ""} ${page?.textContent || ""}`.toLocaleLowerCase();
const match = !query || haystack.includes(query); button.hidden = !match; if (match) matches.push(button);
}
if (searchStatus) searchStatus.textContent = query ? `${t("settingsSearchResults")}: ${matches.length}` : "";
if (query && !matches.some(button => button.dataset.settingsTab === active) && matches[0]) select(matches[0].dataset.settingsTab, false, false);
}
function syncLocation() {
if (typeof location === "undefined" || !["/settings", "/configuration", "/logging"].includes(location.pathname) || !["http:", "https:"].includes(location.protocol)) return;
const url = new URL(location.href); const alreadyCanonical = url.pathname === "/settings" && url.searchParams.get("tab") === active; if (alreadyCanonical) return;
@@ -16,18 +28,21 @@
history.replaceState({ ...(history.state || {}), panel: "settings", settingsTab: active }, "", `${url.pathname}${url.search}${url.hash}`);
}
function select(name, focus = false, persist = true) {
if (!buttons.some(b => b.dataset.settingsTab === name)) return; active = name;
const target = buttons.find(button => button.dataset.settingsTab === name); if (!target) return;
if (target.hidden && search?.value) { search.value = ""; applySettingsSearch(); }
active = name;
for (const b of buttons) { const chosen = b.dataset.settingsTab === name; b.setAttribute("aria-selected", String(chosen)); b.tabIndex = chosen ? 0 : -1; if (chosen && focus) b.focus(); }
document.querySelectorAll("[data-settings-page]").forEach(p => p.hidden = p.dataset.settingsPage !== name);
if (persist) syncLocation();
}
for (const b of buttons) {
b.addEventListener("click", () => select(b.dataset.settingsTab)); b.addEventListener("keydown", event => {
const index = buttons.indexOf(b); let next;
if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (index + 1) % buttons.length; else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (index + buttons.length - 1) % buttons.length; else if (event.key === "Home") next = 0; else if (event.key === "End") next = buttons.length - 1; else return;
event.preventDefault(); select(buttons[next].dataset.settingsTab, true);
const visible = buttons.filter(button => !button.hidden), index = visible.indexOf(b); let next;
if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (index + 1) % visible.length; else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (index + visible.length - 1) % visible.length; else if (event.key === "Home") next = 0; else if (event.key === "End") next = visible.length - 1; else return;
if (!visible.length) return; event.preventDefault(); select(visible[next].dataset.settingsTab, true);
});
}
search?.addEventListener("input", applySettingsSearch);
const cacheTabs = [...document.querySelectorAll("[data-cache-settings-tab]")];
function selectCacheTab(name, focus = false) {
if (!cacheTabs.some(button => button.dataset.cacheSettingsTab === name)) return;
@@ -101,7 +116,7 @@ curl --fail --show-error --request POST \
const info = el("div", undefined, "certificate-api-credential-info");
info.append(el("b", credential.key_id || "-", "mono"));
if (credential.comment) info.append(el("span", credential.comment, "fine"));
const lastUsed = credential.last_used_at ? new Date(Number(credential.last_used_at) * 1000).toLocaleString() : t("certificateApiNeverUsed");
const lastUsed = credential.last_used_at ? formatDateTime(Number(credential.last_used_at) * 1000) : t("certificateApiNeverUsed");
info.append(el("span", `${t("certificateApiLastUsed")}: ${lastUsed}`, "fine"));
const remove = action(t("certificateApiRevoke"), async () => {
if (!await appConfirm(t("certificateApiRevokeOneConfirm").replace("{key}", credential.key_id), { danger: true })) return;
@@ -206,6 +221,6 @@ curl --fail --show-error --request POST \
const text = await response.text(); if (epoch !== authEpoch || !authenticated) return; $("prom-preview-output").textContent = text.slice(0, 262144); $("prom-preview-output").hidden = false;
} catch (error) { if (epoch === authEpoch) toast(error.message, true); } finally { button.disabled = false; }
});
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, current: () => active, reset() { select("listeners", false, false); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); certificateApiState = null; certificateApiLoading = false; $("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = ""; $("certificate-api-credentials").replaceChildren(); $("certificate-api-secret-wrap").hidden = true; $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, refreshSearch: applySettingsSearch, current: () => active, reset() { if (search) search.value = ""; applySettingsSearch(); select("listeners", false, false); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); certificateApiState = null; certificateApiLoading = false; $("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = ""; $("certificate-api-credentials").replaceChildren(); $("certificate-api-secret-wrap").hidden = true; $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
select(active); render();
})();
+14 -5
View File
@@ -83,7 +83,15 @@
idle_timeout_seconds: num("idle-timeout"), max_connections: num("max-connections")
};
}
async function closeEditor() { if (!(await allowDiscard())) return false; dialog.close(); return true; }
function suspendDraft() {
const state = { editing, revision, label: $("stream-editing").textContent, fields: window.rustProxyDrafts.takeChildren(container), tabs: window.rustProxyDrafts.takeChildren($("stream-tabs")), readListen };
const title = read("id")?.value.trim() || editing || t("draftUntitled"); editing = null; revision = null; dirty = false; dialog.close(); return { title, state };
}
function restoreDraft(state) {
window.rustProxyDrafts.restoreChildren(container, state.fields); window.rustProxyDrafts.restoreChildren($("stream-tabs"), state.tabs); readListen = state.readListen;
editing = state.editing; revision = state.revision; initialized = true; dirty = true; $("stream-editing").textContent = state.label; if (!dialog.open) dialog.showModal(); if (latestStatus) renderStatus(latestStatus);
}
function closeEditor() { if (dirty) return window.rustProxyDrafts.dock("stream"); if (dialog.open) dialog.close(); return true; }
function render() {
if (!envelope) return;
@@ -100,7 +108,7 @@
if (stream.enabled === false) title.append(el("span", t("vhDisabled"), "pill"));
identity.append(title, el("div", stream.id, "vhost-hosts")); head.append(identity);
const actions = el("div", undefined, "entity-actions");
actions.append(button("edit", async () => { if (await allowDiscard()) load(structuredClone(stream)); }), button("remove", async () => {
actions.append(button("edit", () => { window.rustProxyDrafts.prepare(); load(structuredClone(stream)); }), button("remove", async () => {
if (busy || !(await appConfirm(t("streamDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.streams = (next.streams || []).filter(item => item.id !== stream.id); if (await saveSection("streams", next.streams || [])) { if (editing === stream.id) load(); render(); }
}));
head.append(actions); box.append(head);
@@ -166,12 +174,13 @@
if (await saveSection("streams", next.streams || [])) { dirty = false; load(value, false); dialog.close(); render(); }
} catch (error) { toast(error.message, true); }
});
$("stream-new").addEventListener("click", async () => { if (await allowDiscard()) load(); });
$("stream-listener-add").addEventListener("click", async () => { if (await allowDiscard()) load(); });
$("stream-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); });
$("stream-listener-add").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); });
$("stream-search").addEventListener("input", render);
$("stream-close").addEventListener("click", closeEditor);
$("stream-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.streams || []).find(item => item.id === editing) || defaults()) : defaults()); });
dialog.addEventListener("cancel", event => { if (!dirty) return; event.preventDefault(); void allowDiscard().then(ok => { if (ok) dialog.close(); }); });
dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
window.rustProxyDrafts.register("stream", { labelKey: "streams", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
window.rustProxyStreams = { render, renderStatus, reset() { dialog.close(); editing = null; revision = null; dirty = false; initialized = false; chartHistory.clear(); container.replaceChildren(); $("stream-search").value = ""; $("stream-list").replaceChildren(); } };
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
})();
+874 -54
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -1,7 +1,7 @@
"use strict";
(() => {
let loading = false, timer = null, lastData = null, activeTab = "overview";
const chartPalette = ["var(--accent)", "var(--green)", "var(--blue)", "var(--transform-chart)", "var(--red)", "var(--notice-text)", "var(--border-hover)", "var(--separator)"];
const chartPalette = ["var(--chart-accent)", "var(--chart-secondary)", "var(--chart-tertiary)", "var(--chart-quaternary)", "var(--chart-quinary)", "var(--chart-senary)", "var(--chart-septenary)", "var(--chart-octonary)"];
const scope = () => $("traffic-scope")?.value || "*";
const number = value => fmt(Number(value) || 0);
@@ -38,7 +38,7 @@
const total = values.reduce((sum, item) => sum + Number(item.count || 0), 0);
totalNode.textContent = number(total); if (empty) empty.hidden = total > 0;
if (!total) {
chart.style.background = "var(--panel2)"; chart.setAttribute("aria-label", t("trafficNoData"));
chart.style.background = "var(--chart-surface)"; chart.setAttribute("aria-label", t("trafficNoData"));
legend.replaceChildren(el("li", t("trafficNoData"), "fine")); return;
}
const shown = values.slice(0, 7).map(item => ({ value: item.value || "-", count: Number(item.count) || 0 }));
@@ -98,7 +98,7 @@
{ value: t("trafficRequestBytes"), count: Number(stats.request_bytes) || 0 },
{ value: t("trafficResponseBytes"), count: Number(stats.response_bytes) || 0 },
], true, fmtBytes);
$("traffic-panel-state").textContent = `${t("trafficUpdated")}: ${new Date().toLocaleTimeString(locale())}`;
$("traffic-panel-state").textContent = `${t("trafficUpdated")}: ${formatTime(new Date())}`;
}
async function refresh() {
@@ -113,7 +113,7 @@
document.querySelectorAll(".traffic-bar-chart").forEach(chart => chart.replaceChildren());
for (const id of ["countries", "methods", "methods-detail", "statuses", "statuses-detail", "cache", "cache-detail"]) {
const chart = $(`traffic-chart-${id}`), legend = $(`traffic-chart-${id}-legend`), total = $(`traffic-chart-${id}-total`);
if (chart) chart.style.background = "var(--panel2)"; legend?.replaceChildren(); if (total) total.textContent = "-";
if (chart) chart.style.background = "var(--chart-surface)"; legend?.replaceChildren(); if (total) total.textContent = "-";
}
selectTab("overview");
}
+1 -1
View File
@@ -142,7 +142,7 @@
$("tp-inflight").textContent = Number.isFinite(inflight) ? number(inflight) : "-";
$("tp-inflight-detail").textContent = Number.isFinite(maxInflight) ? `${t("cacheLimitShort")}: ${number(maxInflight)}` : "-";
renderTypes(counters); renderReasons(counters); renderVhosts(data);
$("transform-panel-state").textContent = `${t("cacheUpdated")}: ${new Date().toLocaleTimeString(locale())}`;
$("transform-panel-state").textContent = `${t("cacheUpdated")}: ${formatTime(new Date())}`;
}
async function refresh() {
if (!authenticated || currentPanel !== "transform" || loading) return;
+23 -2
View File
@@ -132,8 +132,29 @@
note(parent, "locationHeadersHelp");
return { read: () => validateHeaders(JSON.parse(target.value)) };
}
async function closeEditor() { if (!(await allowDiscard())) return false; draftEpoch++; form.querySelectorAll('[type="password"]').forEach(input => input.value = ""); dialog.close(); return true; }
$("vhost-close").addEventListener("click", () => void closeEditor()); dialog.addEventListener("cancel", event => { event.preventDefault(); void closeEditor(); });
function suspendDraft() {
const state = {
editing, revision, label: $("vhost-editing").textContent,
fields: window.rustProxyDrafts.takeChildren(container), tabs: window.rustProxyDrafts.takeChildren($("vhost-tabs")),
basicEditor, locationsReader, aclControl, readBindings, readHosts, countryRatesReader, pagesEditor,
stagedCertificates, uploading
};
const title = read("id")?.value.trim() || editing || t("draftUntitled");
draftEpoch++; editing = null; revision = null; dirty = false; stagedCertificates = []; uploading = false; dialog.close();
return { title, state };
}
function restoreDraft(state) {
draftEpoch++; window.rustProxyDrafts.restoreChildren(container, state.fields); window.rustProxyDrafts.restoreChildren($("vhost-tabs"), state.tabs);
editing = state.editing; revision = state.revision; basicEditor = state.basicEditor; locationsReader = state.locationsReader; aclControl = state.aclControl;
readBindings = state.readBindings; readHosts = state.readHosts; countryRatesReader = state.countryRatesReader; pagesEditor = state.pagesEditor;
stagedCertificates = state.stagedCertificates || []; uploading = state.uploading === true; initialized = true; dirty = true; $("vhost-editing").textContent = state.label;
if (!dialog.open) dialog.showModal(); if (latestStatus) renderStatus(latestStatus);
}
function closeEditor() {
if (dirty) return window.rustProxyDrafts.dock("vhost");
draftEpoch++; form.querySelectorAll('[type="password"]').forEach(input => input.value = ""); if (dialog.open) dialog.close(); return true;
}
$("vhost-close").addEventListener("click", closeEditor); dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
function administrationListenCandidates() {
return [
envelope?.config.administration?.listen,
+5 -4
View File
@@ -119,7 +119,7 @@
openButton.disabled = !browseHost || v.enabled === false;
const quickToggle = button(v.enabled === false ? "vhQuickEnable" : "vhQuickDisable", () => toggleVhostEnabled(v), `quiet vhost-quick-toggle ${v.enabled === false ? "is-enable" : "is-disable"}`);
quickToggle.setAttribute("aria-pressed", String(v.enabled !== false));
actions.append(quickToggle, openButton, button("edit", async () => { if (await allowDiscard()) load(structuredClone(v)); }), button("remove", async () => {
actions.append(quickToggle, openButton, button("edit", () => { window.rustProxyDrafts.prepare(); load(structuredClone(v)); }), button("remove", async () => {
if (busy || !(await appConfirm(t("vhDelete"), { danger: true }))) return;
const next = structuredClone(envelope.config); next.vhosts = (next.vhosts || []).filter(item => item.id !== v.id);
if (await saveSection("vhosts", next.vhosts || [])) { if (editing === v.id) load(); render(); }
@@ -225,8 +225,8 @@
const memberTraffic = backendTrafficRate(pool.id, member, trafficSnapshot, trafficNow, seenTraffic);
const label = vhostDisabled ? t("vhDisabled") : (pool.health_mode === "off" && member.status === "up" ? t("vhHealthDisabled") : t(statusKeys[member.status] || "vhHealthUnknown"));
const activeLimit = Number(member.max_concurrent_requests) > 0 ? `${member.active ?? 0}/${member.max_concurrent_requests}` : `${member.active ?? 0}/∞`;
const lastFailure = Number(member.last_failure_at_ms) > 0 ? `${member.last_failure_class || "-"} · ${new Date(member.last_failure_at_ms).toLocaleTimeString(locale())}` : "-";
const lastRecovery = Number(member.last_recovered_ms) > 0 ? new Date(member.last_recovered_ms).toLocaleTimeString(locale()) : "-";
const lastFailure = Number(member.last_failure_at_ms) > 0 ? `${member.last_failure_class || "-"} · ${formatTime(member.last_failure_at_ms)}` : "-";
const lastRecovery = Number(member.last_recovered_ms) > 0 ? formatTime(member.last_recovered_ms) : "-";
const healthDetail = `${t("vhActiveHealth")}: ${member.last_result || "-"} · ${t("vhPassiveFailures")}: ${member.passive_failures ?? 0} (${member.last_passive_result || "-"})`;
const generationAgeRaw = member.upstream_client_generation_age_seconds;
const generationAge = Math.max(0, Number(generationAgeRaw));
@@ -322,7 +322,7 @@
if (saved) { load(value, [], false); dialog.close(); render(); }
} catch (error) { toast(error.message, true); }
});
$("vhost-new").addEventListener("click", async () => { if (await allowDiscard()) load(); });
$("vhost-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); });
$("vhost-search").addEventListener("input", render);
$("vhost-reset").addEventListener("click", async () => {
if (!(await allowDiscard())) return;
@@ -332,5 +332,6 @@
$("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: applyPoolPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; poolTablePrefs = defaultPoolTablePrefs(); 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; applyPoolTableView(); } };
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
+27 -11
View File
@@ -1,6 +1,6 @@
"use strict";
(() => {
let editing = null;
let editing = null, dirty = false;
const form = $("web-service-form"), dialog = $("web-service-dialog");
const lines = value => value.split(/[\n,]+/).map(item => item.trim()).filter(Boolean);
const strictLines = value => value.split(/\n+/).map(item => item.trim()).filter(Boolean);
@@ -280,7 +280,7 @@
preset.hardening.block_common_attacks = $("web-service-hardening-attacks").checked;
preset.hardening.strip_unnecessary_headers = $("web-service-hardening-headers").checked;
populateSettings(preset);
updatePresetSummary();
updatePresetSummary(); dirty = true;
toast(t("webServicePresetApplied"));
} catch (error) {
toast(error.message, true);
@@ -310,11 +310,24 @@
$("web-service-preset").value = "custom";
updatePresetSummary();
$("web-service-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
activateTab("general");
activateTab("general"); dirty = false;
dialog.showModal();
}
function close() { editing = null; form.reset(); if (dialog.open) dialog.close(); }
function clearEditor() { editing = null; dirty = false; form.reset(); if (dialog.open) dialog.close(); }
function suspendDraft() {
const activeTab = $("web-service-tabs").querySelector("button.active")?.dataset.tab || "general";
const state = { editing, activeTab, label: $("web-service-editing").textContent, controls: window.rustProxyDrafts.captureControls(form) };
const title = $("web-service-id").value.trim() || $("web-service-name").value.trim() || editing || t("draftUntitled");
editing = null; dirty = false; form.reset(); dialog.close(); return { title, state };
}
function restoreDraft(state) {
const saved = state.editing ? (envelope?.config.web_services || []).find(item => item.id === state.editing) : null;
open(saved ? structuredClone(saved) : (state.editing ? { id: state.editing } : null));
window.rustProxyDrafts.restoreControls(form, state.controls); editing = state.editing; $("web-service-editing").textContent = state.label;
syncPhp(); syncPrecompressed(); syncHtaccess(); updatePresetSummary(); activateTab(state.activeTab || "general"); dirty = true;
}
function closeEditor() { if (dirty) return window.rustProxyDrafts.dock("web-server"); clearEditor(); return true; }
function render() {
if (!envelope) return;
@@ -327,7 +340,7 @@
title.append(el("strong", service.name || service.id, "entity-title"), el("span", t("webServer"), "pill live"));
identity.append(title, el("div", service.document_root, "vhost-hosts"));
const actions = el("div", undefined, "entity-actions");
const edit = el("button", t("edit"), "quiet"); edit.type = "button"; edit.addEventListener("click", () => open(structuredClone(service)));
const edit = el("button", t("edit"), "quiet"); edit.type = "button"; edit.addEventListener("click", () => { window.rustProxyDrafts.prepare(); open(structuredClone(service)); });
const remove = el("button", t("remove"), "quiet"); remove.type = "button"; remove.addEventListener("click", async () => {
const used = usage(service.id);
if (used.length) { toast(`${t("webServiceInUse")}: ${used.join(", ")}`, true); return; }
@@ -412,13 +425,15 @@
if (duplicate) { toast(t("webServiceDuplicate"), true); return; }
const position = next.findIndex(service => service.id === editing);
if (position >= 0) next[position] = item; else next.push(item);
if (await saveSection("web-services", next)) { close(); render(); }
if (await saveSection("web-services", next)) { dirty = false; clearEditor(); render(); }
} catch (error) {
toast(error.message, true);
}
});
initTabs();
form.addEventListener("input", () => { dirty = true; });
form.addEventListener("change", () => { dirty = true; });
$("web-service-php-enabled").addEventListener("change", syncPhp);
$("web-service-precompressed").addEventListener("change", syncPrecompressed);
$("web-service-htaccess-enabled").addEventListener("change", syncHtaccess);
@@ -428,11 +443,12 @@
$("htaccess-help-close").addEventListener("click", () => $("htaccess-help-dialog").close());
$("web-service-php-help").addEventListener("click", () => { const help = $("php-help-dialog"); if (!help.open) help.showModal(); });
$("php-help-close").addEventListener("click", () => $("php-help-dialog").close());
$("web-service-new").addEventListener("click", () => open());
$("web-service-close").addEventListener("click", close);
$("web-service-reset").addEventListener("click", close);
$("web-service-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); open(); });
$("web-service-close").addEventListener("click", closeEditor);
$("web-service-reset").addEventListener("click", async () => { if (!dirty || await appConfirm(t("vhUnsaved"), { danger: true, confirmLabel: t("discardAction") })) clearEditor(); });
$("web-service-search").addEventListener("input", render);
dialog.addEventListener("cancel", event => { event.preventDefault(); close(); });
window.rustProxyWebServer = { render, reset: close };
dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
window.rustProxyDrafts.register("web-server", { labelKey: "webServer", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: clearEditor });
window.rustProxyWebServer = { render, reset: clearEditor };
if (envelope) render();
})();
+1 -1
View File
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
#[command(
name = "rust-proxy",
version,
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.2"
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.3"
)]
struct Cli {
#[arg(
+1 -1
View File
@@ -239,7 +239,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
}
});
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
println!("RustProxy live logs 0.26.2 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
println!("RustProxy live logs 0.26.3 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
if interactive { print!("> "); io::stdout().flush()?; }
let mut filters = LiveLogFilters::default();
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
+1 -1
View File
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
if context.tls { "on" } else { "off" }.into(),
);
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.2".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.3".into());
params.insert("REDIRECT_STATUS".into(), "200".into());
if let Some(value) = headers
.get(CONTENT_TYPE)
+1 -1
View File
@@ -185,7 +185,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
));
}
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.26.2</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.26.3</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
html.into_bytes()
}
+1 -1
View File
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
let body = std::str::from_utf8(&body).unwrap();
assert!(body.contains("file.txt"));
assert!(body.contains("DIRECTORY LISTING"));
assert!(body.contains("RustProxy <b>0.26.2</b>"));
assert!(body.contains("RustProxy <b>0.26.3</b>"));
}
#[tokio::test]