This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 12:22:41 +02:00
1 parent e1a59f6e56
commit d168ee2afa
49 files changed
+5608 -4680

No files matched your search

+2 -450
View File
@@ -11,456 +11,8 @@ use std::sync::OnceLock;
// Keep Reader input independent from build-script output. `include_str!` makes every
// document a direct compiler input, so a clean or partially removed target directory
// cannot leave the crate pointing at a missing OUT_DIR artifact.
static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
(
"README.md",
"project",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md")),
),
(
"ROADMAP.md",
"project",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/ROADMAP.md")),
),
(
"CHANGELOG.md",
"project",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/CHANGELOG.md")),
),
(
"docs/ACME_PROVIDERS.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/ACME_PROVIDERS.md"
)),
),
(
"docs/ACME_RFC2136.md",
"historical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ACME_RFC2136.md")),
),
(
"docs/API.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/API.md")),
),
(
"docs/API_ACCESS.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/API_ACCESS.md")),
),
(
"docs/ARCHITECTURE.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ARCHITECTURE.md")),
),
(
"docs/CACHE.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CACHE.md")),
),
(
"docs/CACHE_DIAGNOSTICS.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/CACHE_DIAGNOSTICS.md"
)),
),
(
"docs/CHANGELOG_TECHNICAL_ARCHIVE.md",
"historical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/CHANGELOG_TECHNICAL_ARCHIVE.md"
)),
),
(
"docs/CONTAINER.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CONTAINER.md")),
),
(
"docs/CUSTOM_LOCATIONS.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/CUSTOM_LOCATIONS.md"
)),
),
(
"docs/DOCKER_DISCOVERY.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/DOCKER_DISCOVERY.md"
)),
),
(
"docs/ERROR_LOGGING.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/ERROR_LOGGING.md"
)),
),
(
"docs/HTTP3.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/HTTP3.md")),
),
(
"docs/INSTALL.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/INSTALL.md")),
),
(
"docs/INTEGRATIONS_040.md",
"historical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/INTEGRATIONS_040.md"
)),
),
(
"docs/LIVE_LOGS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/LIVE_LOGS.md")),
),
(
"docs/MANAGEMENT.md",
"historical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MANAGEMENT.md")),
),
(
"docs/MIGRATION.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MIGRATION.md")),
),
(
"docs/MOCK_AUTH.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MOCK_AUTH.md")),
),
(
"docs/MOCK_SERVER.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MOCK_SERVER.md")),
),
(
"docs/NOT_FOUND_HOSTS.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/NOT_FOUND_HOSTS.md"
)),
),
(
"docs/NPM_MIGRATION.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/NPM_MIGRATION.md"
)),
),
(
"docs/PHP_FPM.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PHP_FPM.md")),
),
(
"docs/PROMETHEUS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PROMETHEUS.md")),
),
(
"docs/README.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/README.md")),
),
(
"docs/REDIRECTS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/REDIRECTS.md")),
),
(
"docs/REFERENCES.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/REFERENCES.md")),
),
(
"docs/RENEWAL.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/RENEWAL.md")),
),
(
"docs/ROADMAP_HISTORY.md",
"historical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/ROADMAP_HISTORY.md"
)),
),
(
"docs/SECRET_STORE_BACKUP.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/SECRET_STORE_BACKUP.md"
)),
),
(
"docs/SECURITY.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SECURITY.md")),
),
(
"docs/SECURITY_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/SECURITY_AUDIT.md"
)),
),
(
"docs/SETTINGS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SETTINGS.md")),
),
(
"docs/STREAMS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/STREAMS.md")),
),
(
"docs/TEST_REPORT.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TEST_REPORT.md")),
),
(
"docs/TODO.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TODO.md")),
),
(
"docs/TRANSFORM.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TRANSFORM.md")),
),
(
"docs/TRANSPORT.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TRANSPORT.md")),
),
(
"docs/V0.21.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.21.0_AUDIT.md"
)),
),
(
"docs/V0.22.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.0_AUDIT.md"
)),
),
(
"docs/V0.22.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.1_AUDIT.md"
)),
),
(
"docs/V0.22.2_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.2_AUDIT.md"
)),
),
(
"docs/V0.22.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.3_AUDIT.md"
)),
),
(
"docs/V0.23.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.0_AUDIT.md"
)),
),
(
"docs/V0.23.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.1_AUDIT.md"
)),
),
(
"docs/V0.23.2_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.2_AUDIT.md"
)),
),
(
"docs/V0.23.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.3_AUDIT.md"
)),
),
(
"docs/V0.24.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.0_AUDIT.md"
)),
),
(
"docs/V0.24.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.1_AUDIT.md"
)),
),
(
"docs/V0.24.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.3_AUDIT.md"
)),
),
(
"docs/V0.24.4_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.4_AUDIT.md"
)),
),
(
"docs/V0.24.5_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.5_AUDIT.md"
)),
),
(
"docs/V0.24.6_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.6_AUDIT.md"
)),
),
(
"docs/V0.24.7_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.7_AUDIT.md"
)),
),
(
"docs/V0.24.8_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.8_AUDIT.md"
)),
),
(
"docs/V0.25.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.0_AUDIT.md"
)),
),
(
"docs/V0.25.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.1_AUDIT.md"
)),
),
(
"docs/V0.25.5_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.5_AUDIT.md"
)),
),
(
"docs/V0.25.4_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.4_AUDIT.md"
)),
),
(
"docs/V0.25.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.3_AUDIT.md"
)),
),
(
"docs/V0.25.2_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.2_AUDIT.md"
)),
),
(
"docs/V0.25.0_SECURITY_PREPROD_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"
)),
),
(
"docs/VHOSTS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOSTS.md")),
),
(
"docs/VHOST_ACCESS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOST_ACCESS.md")),
),
(
"docs/WEB_SERVER.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/WEB_SERVER.md")),
),
];
mod documents;
use documents::READER_DOCUMENTS;
pub(super) async fn index() -> Html<&'static str> {
Html(include_str!("web/docs.html"))
+452
View File
@@ -0,0 +1,452 @@
//! Embedded Markdown documents for the authenticated Reader.
pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
(
"README.md",
"project",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md")),
),
(
"ROADMAP.md",
"project",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/ROADMAP.md")),
),
(
"CHANGELOG.md",
"project",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/CHANGELOG.md")),
),
(
"docs/ACME_PROVIDERS.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/ACME_PROVIDERS.md"
)),
),
(
"docs/ACME_RFC2136.md",
"historical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ACME_RFC2136.md")),
),
(
"docs/API.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/API.md")),
),
(
"docs/API_ACCESS.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/API_ACCESS.md")),
),
(
"docs/ARCHITECTURE.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/ARCHITECTURE.md")),
),
(
"docs/CACHE.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CACHE.md")),
),
(
"docs/CACHE_DIAGNOSTICS.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/CACHE_DIAGNOSTICS.md"
)),
),
(
"docs/CHANGELOG_TECHNICAL_ARCHIVE.md",
"historical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/CHANGELOG_TECHNICAL_ARCHIVE.md"
)),
),
(
"docs/CONTAINER.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CONTAINER.md")),
),
(
"docs/CUSTOM_LOCATIONS.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/CUSTOM_LOCATIONS.md"
)),
),
(
"docs/DOCKER_DISCOVERY.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/DOCKER_DISCOVERY.md"
)),
),
(
"docs/ERROR_LOGGING.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/ERROR_LOGGING.md"
)),
),
(
"docs/HTTP3.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/HTTP3.md")),
),
(
"docs/INSTALL.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/INSTALL.md")),
),
(
"docs/INTEGRATIONS_040.md",
"historical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/INTEGRATIONS_040.md"
)),
),
(
"docs/LIVE_LOGS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/LIVE_LOGS.md")),
),
(
"docs/MANAGEMENT.md",
"historical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MANAGEMENT.md")),
),
(
"docs/MIGRATION.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MIGRATION.md")),
),
(
"docs/MOCK_AUTH.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MOCK_AUTH.md")),
),
(
"docs/MOCK_SERVER.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MOCK_SERVER.md")),
),
(
"docs/NOT_FOUND_HOSTS.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/NOT_FOUND_HOSTS.md"
)),
),
(
"docs/NPM_MIGRATION.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/NPM_MIGRATION.md"
)),
),
(
"docs/PHP_FPM.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PHP_FPM.md")),
),
(
"docs/PROMETHEUS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PROMETHEUS.md")),
),
(
"docs/README.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/README.md")),
),
(
"docs/REDIRECTS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/REDIRECTS.md")),
),
(
"docs/REFERENCES.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/REFERENCES.md")),
),
(
"docs/RENEWAL.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/RENEWAL.md")),
),
(
"docs/ROADMAP_HISTORY.md",
"historical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/ROADMAP_HISTORY.md"
)),
),
(
"docs/SECRET_STORE_BACKUP.md",
"guides",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/SECRET_STORE_BACKUP.md"
)),
),
(
"docs/SECURITY.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SECURITY.md")),
),
(
"docs/SECURITY_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/SECURITY_AUDIT.md"
)),
),
(
"docs/SETTINGS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/SETTINGS.md")),
),
(
"docs/STREAMS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/STREAMS.md")),
),
(
"docs/TEST_REPORT.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TEST_REPORT.md")),
),
(
"docs/TODO.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TODO.md")),
),
(
"docs/TRANSFORM.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TRANSFORM.md")),
),
(
"docs/TRANSPORT.md",
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/TRANSPORT.md")),
),
(
"docs/V0.21.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.21.0_AUDIT.md"
)),
),
(
"docs/V0.22.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.0_AUDIT.md"
)),
),
(
"docs/V0.22.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.1_AUDIT.md"
)),
),
(
"docs/V0.22.2_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.2_AUDIT.md"
)),
),
(
"docs/V0.22.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.22.3_AUDIT.md"
)),
),
(
"docs/V0.23.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.0_AUDIT.md"
)),
),
(
"docs/V0.23.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.1_AUDIT.md"
)),
),
(
"docs/V0.23.2_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.2_AUDIT.md"
)),
),
(
"docs/V0.23.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.23.3_AUDIT.md"
)),
),
(
"docs/V0.24.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.0_AUDIT.md"
)),
),
(
"docs/V0.24.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.1_AUDIT.md"
)),
),
(
"docs/V0.24.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.3_AUDIT.md"
)),
),
(
"docs/V0.24.4_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.4_AUDIT.md"
)),
),
(
"docs/V0.24.5_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.5_AUDIT.md"
)),
),
(
"docs/V0.24.6_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.6_AUDIT.md"
)),
),
(
"docs/V0.24.7_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.7_AUDIT.md"
)),
),
(
"docs/V0.24.8_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.24.8_AUDIT.md"
)),
),
(
"docs/V0.25.0_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.0_AUDIT.md"
)),
),
(
"docs/V0.25.1_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.1_AUDIT.md"
)),
),
(
"docs/V0.25.5_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.5_AUDIT.md"
)),
),
(
"docs/V0.25.4_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.4_AUDIT.md"
)),
),
(
"docs/V0.25.3_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.3_AUDIT.md"
)),
),
(
"docs/V0.25.2_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.2_AUDIT.md"
)),
),
(
"docs/V0.25.0_SECURITY_PREPROD_AUDIT.md",
"audits",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"
)),
),
(
"docs/VHOSTS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOSTS.md")),
),
(
"docs/VHOST_ACCESS.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOST_ACCESS.md")),
),
(
"docs/WEB_SERVER.md",
"guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/WEB_SERVER.md")),
),
];
+70 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.25.6",
"version": "0.25.7",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
},
"servers": [
@@ -2917,6 +2917,74 @@
],
"default": "dark",
"description": "Account-scoped administration interface theme. Existing accounts and omitted values default to the current dark theme."
},
"backend_table_columns": {
"type": "array",
"minItems": 1,
"maxItems": 14,
"uniqueItems": true,
"items": {
"type": "string",
"enum": [
"vhost",
"backend",
"status",
"active",
"requests",
"errors",
"latency",
"traffic",
"generation",
"generationRequests",
"generationAge",
"rotations",
"lastFailure",
"lastRecovery"
]
},
"default": [
"vhost",
"backend",
"status",
"active",
"requests",
"errors",
"latency",
"traffic",
"generation",
"generationRequests",
"generationAge",
"rotations",
"lastFailure",
"lastRecovery"
],
"description": "Account-scoped Backends diagnostics columns shown in the dashboard."
},
"backend_table_sort": {
"type": "string",
"enum": [
"vhost",
"backend",
"status",
"active",
"requests",
"errors",
"latency",
"traffic",
"generation",
"generationRequests",
"generationAge",
"rotations",
"lastFailure",
"lastRecovery"
],
"default": "vhost",
"description": "Account-scoped sort key for the Backends diagnostics table."
},
"backend_table_descending": {
"type": "boolean",
"default": false,
"description": "Account-scoped sort direction for the Backends diagnostics table."
}
},
"required": [
@@ -5093,7 +5161,7 @@
}
}
},
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7\u201330 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7–30 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
"security": [],
"parameters": [
{
+57
View File
@@ -267,10 +267,35 @@ const LIVE_LOG_COLUMNS: &[&str] = &[
"websocket",
"upstream_error",
];
const BACKEND_TABLE_COLUMNS: &[&str] = &[
"vhost",
"backend",
"status",
"active",
"requests",
"errors",
"latency",
"traffic",
"generation",
"generationRequests",
"generationAge",
"rotations",
"lastFailure",
"lastRecovery",
];
fn default_admin_theme() -> String {
"dark".to_owned()
}
fn default_backend_table_columns() -> Vec<String> {
BACKEND_TABLE_COLUMNS
.iter()
.map(|value| (*value).to_owned())
.collect()
}
fn default_backend_table_sort() -> String {
"vhost".to_owned()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
@@ -278,6 +303,12 @@ struct AdminPreferences {
live_log_columns: Vec<String>,
#[serde(default = "default_admin_theme")]
theme: String,
#[serde(default = "default_backend_table_columns")]
backend_table_columns: Vec<String>,
#[serde(default = "default_backend_table_sort")]
backend_table_sort: String,
#[serde(default)]
backend_table_descending: bool,
}
impl Default for AdminPreferences {
@@ -288,6 +319,9 @@ impl Default for AdminPreferences {
.map(|value| (*value).to_owned())
.collect(),
theme: default_admin_theme(),
backend_table_columns: default_backend_table_columns(),
backend_table_sort: default_backend_table_sort(),
backend_table_descending: false,
}
}
}
@@ -316,6 +350,29 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE
"invalid administration theme preference".into(),
));
}
if preferences.backend_table_columns.is_empty()
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
{
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"at least one backend table column must be visible".into(),
));
}
let mut seen = std::collections::HashSet::new();
for column in &preferences.backend_table_columns {
if !BACKEND_TABLE_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid backend table column preference".into(),
));
}
}
if !BACKEND_TABLE_COLUMNS.contains(&preferences.backend_table_sort.as_str()) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid backend table sort preference".into(),
));
}
Ok(())
}
+35 -1
View File
@@ -20,6 +20,12 @@
LIVE_LOG_COLUMNS.len()
);
assert_eq!(initial["theme"], "dark");
assert_eq!(
initial["backend_table_columns"].as_array().unwrap().len(),
BACKEND_TABLE_COLUMNS.len()
);
assert_eq!(initial["backend_table_sort"], "vhost");
assert_eq!(initial["backend_table_descending"], false);
assert!(store
.save_admin_preferences("owner", r#"{"live_log_columns":["time","status"]}"#)
@@ -39,10 +45,19 @@
let legacy = json_body(response).await;
assert_eq!(legacy["live_log_columns"], json!(["time", "status"]));
assert_eq!(legacy["theme"], "dark");
assert_eq!(
legacy["backend_table_columns"].as_array().unwrap().len(),
BACKEND_TABLE_COLUMNS.len()
);
assert_eq!(legacy["backend_table_sort"], "vhost");
assert_eq!(legacy["backend_table_descending"], false);
let selected = json!({
"live_log_columns":["time","request","status","upstream_error"],
"theme":"light"
"theme":"light",
"backend_table_columns":["vhost","backend","status","requests","latency"],
"backend_table_sort":"requests",
"backend_table_descending":true
});
let response = router
.clone()
@@ -101,6 +116,7 @@
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
let response = router
.clone()
.oneshot(
request("/api/preferences")
.method("PUT")
@@ -117,6 +133,24 @@
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
let response = router
.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"],"backend_table_columns":["unknown"]}"#,
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
store.close().await;
}
+1 -1
View File
@@ -353,7 +353,7 @@ function lock(notify = true) {
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
}
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.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 activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.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); } }
let tunnelRefreshTimer = null;
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
+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.25.6";
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.7";
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
}
+89 -20
View File
@@ -523,9 +523,75 @@
</article>
</div>
<article class="card pool-card" id="backend-diagnostics-card">
<div class="card-heading">
<h2 data-i18n="vhHealthTitle">Backends</h2><button class="quiet" data-i18n="vhManage"
id="pool-open" type="button">Manage vhosts</button>
<div class="card-heading backend-card-heading">
<h2 data-i18n="vhHealthTitle">Backends</h2>
<div class="backend-table-tools">
<label class="sr-only" data-i18n="backendSearch" for="pool-search">Search
backends</label>
<input autocomplete="off" data-i18n-aria="backendSearch"
data-i18n-placeholder="backendSearchPlaceholder" id="pool-search" type="search" />
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
by</span><select data-i18n-aria="backendSortBy" id="pool-sort">
<option data-i18n="vhVhost" value="vhost">Vhost</option>
<option data-i18n="backend" value="backend">Backend</option>
<option data-i18n="status" value="status">Status</option>
<option data-i18n="vhActive" value="active">Active</option>
<option data-i18n="vhRequests" value="requests">Requests</option>
<option data-i18n="vhErrors" value="errors">Errors</option>
<option data-i18n="vhLatency" value="latency">Latency</option>
<option data-i18n="vhTraffic" value="traffic">Traffic/s</option>
<option data-i18n="vhClientGeneration" value="generation">Client gen.</option>
<option data-i18n="vhGenerationRequests" value="generationRequests">Gen.
requests</option>
<option data-i18n="vhGenerationAge" value="generationAge">Gen. age</option>
<option data-i18n="vhClientRotations" value="rotations">Rotations</option>
<option data-i18n="vhLastFailure" value="lastFailure">Last failure</option>
<option data-i18n="vhLastRecovery" value="lastRecovery">Last recovery</option>
</select></label>
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
id="pool-sort-direction" type="button">A-Z</button>
<details class="backend-column-picker">
<summary class="quiet" data-i18n="backendColumns">Columns</summary>
<div class="backend-column-options">
<label class="check"><input checked data-pool-column-toggle="vhost"
type="checkbox" /><span data-i18n="vhVhost">Vhost</span></label>
<label class="check"><input checked data-pool-column-toggle="backend"
type="checkbox" /><span data-i18n="backend">Backend</span></label>
<label class="check"><input checked data-pool-column-toggle="status"
type="checkbox" /><span data-i18n="status">Status</span></label>
<label class="check"><input checked data-pool-column-toggle="active"
type="checkbox" /><span data-i18n="vhActive">Active</span></label>
<label class="check"><input checked data-pool-column-toggle="requests"
type="checkbox" /><span data-i18n="vhRequests">Requests</span></label>
<label class="check"><input checked data-pool-column-toggle="errors"
type="checkbox" /><span data-i18n="vhErrors">Errors</span></label>
<label class="check"><input checked data-pool-column-toggle="latency"
type="checkbox" /><span data-i18n="vhLatency">Latency</span></label>
<label class="check"><input checked data-pool-column-toggle="traffic"
type="checkbox" /><span data-i18n="vhTraffic">Traffic/s</span></label>
<label class="check"><input checked data-pool-column-toggle="generation"
type="checkbox" /><span data-i18n="vhClientGeneration">Client
gen.</span></label>
<label class="check"><input checked data-pool-column-toggle="generationRequests"
type="checkbox" /><span data-i18n="vhGenerationRequests">Gen.
requests</span></label>
<label class="check"><input checked data-pool-column-toggle="generationAge"
type="checkbox" /><span data-i18n="vhGenerationAge">Gen.
age</span></label>
<label class="check"><input checked data-pool-column-toggle="rotations"
type="checkbox" /><span
data-i18n="vhClientRotations">Rotations</span></label>
<label class="check"><input checked data-pool-column-toggle="lastFailure"
type="checkbox" /><span data-i18n="vhLastFailure">Last
failure</span></label>
<label class="check"><input checked data-pool-column-toggle="lastRecovery"
type="checkbox" /><span data-i18n="vhLastRecovery">Last
recovery</span></label>
</div>
</details>
<button class="quiet" data-i18n="vhManage" id="pool-open" type="button">Manage
vhosts</button>
</div>
</div>
<p class="fine" data-i18n="vhHealthHint">Live state: leases cover the entire exchange; traffic
is an EWMA of payload bytes.</p>
@@ -533,20 +599,23 @@
<table class="pool-table">
<thead>
<tr>
<th data-i18n="vhVhost">Vhost</th>
<th data-i18n="backend">Backend</th>
<th data-i18n="status">Status</th>
<th data-i18n="vhActive">Active</th>
<th data-i18n="vhRequests">Requests</th>
<th data-i18n="vhErrors">Errors</th>
<th data-i18n="vhLatency">Latency</th>
<th data-i18n="vhTraffic">Traffic/s</th>
<th data-i18n="vhClientGeneration">Client gen.</th>
<th data-i18n="vhGenerationRequests">Gen. requests</th>
<th data-i18n="vhGenerationAge">Gen. age</th>
<th data-i18n="vhClientRotations">Rotations</th>
<th data-i18n="vhLastFailure">Last failure</th>
<th data-i18n="vhLastRecovery">Last recovery</th>
<th data-i18n="vhVhost" data-pool-column="vhost">Vhost</th>
<th data-i18n="backend" data-pool-column="backend">Backend</th>
<th data-i18n="status" data-pool-column="status">Status</th>
<th data-i18n="vhActive" data-pool-column="active">Active</th>
<th data-i18n="vhRequests" data-pool-column="requests">Requests</th>
<th data-i18n="vhErrors" data-pool-column="errors">Errors</th>
<th data-i18n="vhLatency" data-pool-column="latency">Latency</th>
<th data-i18n="vhTraffic" data-pool-column="traffic">Traffic/s</th>
<th data-i18n="vhClientGeneration" data-pool-column="generation">Client gen.
</th>
<th data-i18n="vhGenerationRequests" data-pool-column="generationRequests">Gen.
requests</th>
<th data-i18n="vhGenerationAge" data-pool-column="generationAge">Gen. age</th>
<th data-i18n="vhClientRotations" data-pool-column="rotations">Rotations</th>
<th data-i18n="vhLastFailure" data-pool-column="lastFailure">Last failure</th>
<th data-i18n="vhLastRecovery" data-pool-column="lastRecovery">Last recovery
</th>
</tr>
</thead>
<tbody id="pool-rows"></tbody>
@@ -1598,7 +1667,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.25.6"
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.7"
spellcheck="false" />
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
<div class="switch-grid">
@@ -2386,7 +2455,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.25.6</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.25.7</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -3097,7 +3166,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.25.6</strong></div>
<div><span class="small-label">RustProxy</span><strong>0.25.7</strong></div>
</div>
<dl class="about-meta">
<div>
+196 -2
View File
@@ -50,6 +50,9 @@
--shadow-mid: #0006;
--shadow-strong: #0009;
--neutral-soft: rgba(127, 127, 127, .05);
--scrollbar-track: #151a16;
--scrollbar-thumb: #4f5d50;
--scrollbar-thumb-hover: #6b7b69;
--mono: ui-monospace, SFMono-Regular, Consolas, monospace;
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
}
@@ -97,11 +100,39 @@ html[data-theme="light"] {
--transform-chart: #7d4fa3;
--shadow-mid: rgb(31 39 33 / .16);
--shadow-strong: rgb(31 39 33 / .26);
--neutral-soft: rgba(70, 78, 72, .05)
--neutral-soft: rgba(70, 78, 72, .05);
--scrollbar-track: #e7ece4;
--scrollbar-thumb: #a9b5a6;
--scrollbar-thumb-hover: #879684
}
* {
box-sizing: border-box
box-sizing: border-box;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
scrollbar-width: thin
}
*::-webkit-scrollbar {
width: 10px;
height: 10px
}
*::-webkit-scrollbar-track {
background: var(--scrollbar-track)
}
*::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border: 2px solid var(--scrollbar-track);
border-radius: 999px
}
*::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover)
}
*::-webkit-scrollbar-corner {
background: var(--scrollbar-track)
}
html.app-loading {
@@ -2463,6 +2494,63 @@ td {
flex: 0 0 auto
}
.vh-enable-control {
display: grid !important;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-rows: auto auto;
align-items: center;
gap: 2px 10px;
margin-top: 5px !important;
padding: 11px 12px;
border: 1px solid var(--pill-live-line);
background: var(--panel2);
box-shadow: inset 3px 0 0 var(--green)
}
.vh-enable-control>input {
grid-column: 1;
grid-row: 1/3;
width: 18px;
height: 18px;
accent-color: var(--green)
}
.vh-enable-control>span {
grid-column: 2;
grid-row: 1;
color: var(--text);
font-size: 12px;
font-weight: 700
}
.vh-enable-hint {
grid-column: 2;
grid-row: 2;
color: var(--muted);
font-size: 10px
}
.vh-enable-state {
grid-column: 3;
grid-row: 1/3;
padding: 3px 7px;
border: 1px solid var(--pill-live-line);
color: var(--green);
font: 600 10px var(--mono);
text-transform: uppercase;
letter-spacing: .04em
}
.vh-enable-control.is-disabled {
border-color: var(--danger-line-strong);
box-shadow: inset 3px 0 0 var(--red)
}
.vh-enable-control.is-disabled .vh-enable-state {
border-color: var(--danger-line);
color: var(--red)
}
.vh-wide {
grid-column: 1/-1;
min-width: 0
@@ -2524,6 +2612,78 @@ td {
margin-bottom: 12px
}
.backend-card-heading {
align-items: flex-start;
gap: 10px
}
.backend-table-tools {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 7px;
margin-left: auto
}
#pool-search {
width: min(250px, 30vw);
min-width: 170px
}
.backend-sort-control {
display: flex;
align-items: center;
gap: 6px;
color: var(--muted);
font-size: 11px
}
.backend-sort-control select {
width: auto;
min-width: 120px
}
.backend-sort-direction {
min-width: 48px
}
.backend-column-picker {
position: relative
}
.backend-column-picker>summary {
list-style: none;
cursor: pointer;
user-select: none
}
.backend-column-picker>summary::-webkit-details-marker {
display: none
}
.backend-column-options {
position: absolute;
z-index: 20;
top: calc(100% + 6px);
right: 0;
display: grid;
grid-template-columns: repeat(2, minmax(145px, 1fr));
gap: 1px 12px;
min-width: 330px;
max-height: 330px;
overflow: auto;
padding: 10px 12px 12px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: 0 12px 28px var(--shadow-strong)
}
.backend-column-options .check {
margin-top: 4px !important;
font-size: 11px
}
.pool-card .fine {
margin: 0 0 8px
}
@@ -4861,6 +5021,16 @@ dialog .window-heading {
margin-top: 8px
}
.vhost-quick-toggle.is-enable {
border-color: var(--pill-live-line);
color: var(--green)
}
.vhost-quick-toggle.is-disable {
border-color: var(--danger-line);
color: var(--red)
}
@media(max-width:980px) {
.logging-grid {
grid-template-columns: 1fr
@@ -4896,6 +5066,30 @@ dialog .window-heading {
min-width: 0
}
.backend-card-heading {
align-items: stretch;
flex-direction: column
}
.backend-table-tools {
width: 100%;
justify-content: flex-start;
margin-left: 0
}
#pool-search {
flex: 1 1 100%;
width: 100%;
min-width: 0
}
.backend-column-options {
right: auto;
left: 0;
grid-template-columns: 1fr;
min-width: min(290px, 82vw)
}
.vhost-card-head .entity-actions {
width: 100%;
justify-content: flex-start
+5 -1
View File
@@ -38,7 +38,11 @@
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true;
container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
let g = group("vhEditor"); field(g, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(g, "vhIdRenameHelp");
readHosts = hostTagEditor(g, value.frontend?.hosts || []); field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
readHosts = hostTagEditor(g, value.frontend?.hosts || []);
const enabledControl = field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox"), enabledLabel = enabledControl.closest("label"), enabledState = el("strong", "", "vh-enable-state");
if (enabledLabel) { enabledLabel.classList.add("vh-enable-control", "vh-wide"); enabledLabel.append(text("small", "vhEnableHint", "vh-enable-hint"), enabledState); }
const syncEnabledState = () => { const key = enabledControl.checked ? "vhEnabled" : "vhDisabled"; enabledLabel?.classList.toggle("is-disabled", !enabledControl.checked); enabledLabel?.classList.toggle("is-enabled", enabledControl.checked); enabledState.dataset.i18n = key; enabledState.textContent = t(key); };
enabledControl.addEventListener("change", syncEnabledState); syncEnabledState();
const mainSource = field(g, "source-type", "sourceType", value.source?.type || "reverse_proxy", "select", { values: [["reverse_proxy", "sourceReverseProxy"], ["web_server", "sourceWebServer"]] });
const mainWebService = field(g, "source-web-service", "webService", value.source?.web_service_id || "", "select"); fillWebServices(mainWebService, value.source?.web_service_id || "");
const syncMainSource = () => { const web = mainSource.value === "web_server"; mainWebService.disabled = !web; mainWebService.required = web; }; mainSource.addEventListener("change", syncMainSource); syncMainSource();
+93 -4
View File
@@ -1,3 +1,87 @@
const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] });
let poolTablePrefs = defaultPoolTablePrefs(), poolPreferenceSaveRunning = false, poolPreferenceSaveQueued = false;
function normalizePoolTablePrefs(preferences) {
const selected = Array.isArray(preferences?.backend_table_columns) ? preferences.backend_table_columns.filter(key => poolColumns.includes(key)) : [];
return {
sort: poolColumns.includes(preferences?.backend_table_sort) ? preferences.backend_table_sort : "vhost",
descending: preferences?.backend_table_descending === true,
columns: selected.length ? selected : [...poolColumns],
};
}
function applyPoolPreferences(preferences) {
poolTablePrefs = normalizePoolTablePrefs(preferences);
const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort;
syncPoolColumns(); syncPoolSortDirection(); applyPoolTableView();
}
async function savePoolTablePrefs() {
if (!authenticated) return;
poolPreferenceSaveQueued = true;
if (poolPreferenceSaveRunning) return;
poolPreferenceSaveRunning = true;
try {
while (poolPreferenceSaveQueued && authenticated) {
poolPreferenceSaveQueued = false;
const saved = await window.rustProxyPreferences.update({
backend_table_columns: poolColumns.filter(key => poolTablePrefs.columns.includes(key)),
backend_table_sort: poolTablePrefs.sort,
backend_table_descending: poolTablePrefs.descending,
});
if (!authenticated) return;
if (!poolPreferenceSaveQueued) poolTablePrefs = normalizePoolTablePrefs(saved);
}
syncPoolColumns(); syncPoolSortDirection();
} catch (error) { if (error.status !== 401) toast(error.message, true); }
finally { poolPreferenceSaveRunning = false; }
}
function syncPoolColumns() {
const visible = new Set(poolTablePrefs.columns);
document.querySelectorAll("[data-pool-column]").forEach(node => { node.hidden = !visible.has(node.dataset.poolColumn); });
document.querySelectorAll("[data-pool-column-toggle]").forEach(input => { input.checked = visible.has(input.dataset.poolColumnToggle); });
}
function syncPoolSortDirection() {
const control = $("pool-sort-direction"); if (!control) return;
control.textContent = poolTablePrefs.descending ? "Z-A" : "A-Z";
control.setAttribute("aria-label", t(poolTablePrefs.descending ? "backendSortDescending" : "backendSortAscending"));
}
function applyPoolTableView() {
const body = $("pool-rows"); if (!body) return;
const query = ($("pool-search")?.value || "").trim().toLowerCase();
const key = $("pool-sort")?.value || poolTablePrefs.sort;
const rows = [...body.querySelectorAll("tr")];
rows.sort((left, right) => {
const a = left.poolSort?.[key] ?? "", b = right.poolSort?.[key] ?? "";
const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" });
return poolTablePrefs.descending ? -result : result;
});
body.replaceChildren(...rows);
let visible = 0;
for (const row of rows) { const match = !query || (row.poolSearchValue || "").includes(query); row.hidden = !match; if (match) visible++; }
syncPoolColumns(); syncPoolSortDirection();
const empty = $("pool-empty");
if (empty) { empty.textContent = query && rows.length && !visible ? t("backendSearchEmpty") : t("vhHealthEmpty"); empty.hidden = visible > 0 || (!rows.length && !document.querySelector("#backend-diagnostics-card:not([hidden])")); }
}
function initPoolTableTools() {
const sort = $("pool-sort"), direction = $("pool-sort-direction"), search = $("pool-search");
if (sort) { sort.value = poolTablePrefs.sort; sort.addEventListener("change", () => { poolTablePrefs.sort = sort.value; void savePoolTablePrefs(); applyPoolTableView(); }); }
direction?.addEventListener("click", () => { poolTablePrefs.descending = !poolTablePrefs.descending; void savePoolTablePrefs(); applyPoolTableView(); });
search?.addEventListener("input", applyPoolTableView);
document.querySelectorAll("[data-pool-column-toggle]").forEach(input => input.addEventListener("change", () => {
const visible = new Set(poolTablePrefs.columns);
if (input.checked) visible.add(input.dataset.poolColumnToggle); else visible.delete(input.dataset.poolColumnToggle);
if (!visible.size) { input.checked = true; toast(t("backendColumnsRequired"), true); return; }
poolTablePrefs.columns = poolColumns.filter(key => visible.has(key)); void savePoolTablePrefs(); syncPoolColumns();
}));
syncPoolColumns(); syncPoolSortDirection();
}
async function toggleVhostEnabled(vhost) {
if (!envelope || busy) return;
const next = structuredClone(envelope.config), target = (next.vhosts || []).find(item => item.id === vhost.id);
if (!target) return;
target.enabled = vhost.enabled === false;
if (await saveSection("vhosts", next.vhosts || [])) render();
}
function render() {
if (!envelope) return;
if (!initialized) load(defaults(), [], false);
@@ -33,7 +117,9 @@
const browseTls = configuredEndpoints.some(endpoint => endpoint.binding?.tls === true);
const openButton = button("vhOpenBrowser", () => { if (browseHost) window.open(`${browseTls ? "https" : "http"}://${browseHost}/`, "_blank", "noopener,noreferrer"); });
openButton.disabled = !browseHost || v.enabled === false;
actions.append(openButton, button("edit", async () => { if (await allowDiscard()) load(structuredClone(v)); }), button("remove", async () => {
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 () => {
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(); }
@@ -147,7 +233,9 @@
const generationAgeText = generationAgeRaw == null || !Number.isFinite(generationAge) ? "-" : generationAge >= 3600 ? `${Math.floor(generationAge / 3600)}h ${Math.floor((generationAge % 3600) / 60)}m` : generationAge >= 60 ? `${Math.floor(generationAge / 60)}m ${Math.floor(generationAge % 60)}s` : `${Math.floor(generationAge)}s`;
const row = el("tr"); if (vhostDisabled) row.classList.add("pool-vhost-disabled"); const poolDomain = status.backend_pool_domains?.[pool.id]; const poolLabel = poolDomain ? `${pool.id} (${poolDomain})` : pool.id; const cells = [poolLabel, `${member.id}${member.backup ? " / " + t("vhBackup") : ""}`, label, activeLimit, member.requests, member.errors,
member.ewma_latency_ms > 0 ? `${member.ewma_latency_ms.toFixed(1)} ms` : "-", `${rateBytes(memberTraffic)}/s`, member.upstream_client_generation ?? "-", member.upstream_client_generation_requests ?? "-", generationAgeText, member.upstream_client_rotations_total ?? "-", lastFailure, lastRecovery];
cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); cell.title = index === 2 ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : index === 12 ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (index === 2 && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (index === 12 && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row);
const sortValues = { vhost: poolLabel.toLowerCase(), backend: String(member.id || "").toLowerCase(), status: label.toLowerCase(), active: Number(member.active) || 0, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic, generation: Number(member.upstream_client_generation) || 0, generationRequests: Number(member.upstream_client_generation_requests) || 0, generationAge: Number.isFinite(generationAge) ? generationAge : -1, rotations: Number(member.upstream_client_rotations_total) || 0, lastFailure: Number(member.last_failure_at_ms) || 0, lastRecovery: Number(member.last_recovered_ms) || 0 };
row.poolSort = sortValues; row.poolSearchValue = [poolLabel, member.id, label, member.status, member.last_failure_class, member.last_failure_detail].filter(Boolean).join(" ").toLowerCase();
cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); cell.dataset.poolColumn = poolColumns[index]; cell.title = index === 2 ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : index === 12 ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (index === 2 && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (index === 12 && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row);
if (!vhostDisabled && member.status === "up") up++; active += Number(member.active) || 0; requests += Number(member.requests) || 0; errors += Number(member.errors) || 0; traffic += memberTraffic;
const latency = Number(member.ewma_latency_ms || member.latency_ms) || 0; if (latency > 0) { const weight = Math.max(1, Number(member.requests) || 1); weightedLatency += latency * weight; latencyWeight += weight; }
}
@@ -190,9 +278,9 @@
vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy")
);
$("pool-rows").replaceChildren(...rows);
$("pool-empty").hidden = rows.length > 0 || !hasConfiguredProxySource;
const backendDiagnosticsCard = $("backend-diagnostics-card");
if (backendDiagnosticsCard) backendDiagnosticsCard.hidden = !hasConfiguredProxySource;
applyPoolTableView();
document.querySelectorAll("[data-health-for]").forEach(node => node.textContent = summaries.get(node.dataset.healthFor) || t("vhHealthEmpty"));
const fill = (attr, format) => document.querySelectorAll(`[data-${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}]`).forEach(node => { const data = aggregate.get(node.dataset[attr]); node.textContent = data ? format(data) : "-"; });
fill("vhostHealth", d => d.disabled ? t("vhDisabled") : d.staticOnly ? t("vhUp") : d.healthMode === "off" ? t("vhHealthDisabled") : `${d.up}/${d.total} ${t("vhUp")}`);
@@ -242,6 +330,7 @@
load(saved ? structuredClone(saved) : defaults());
});
$("pool-open").addEventListener("click", () => setPanel("vhosts"));
initPoolTableTools();
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
window.rustProxyVhosts = { render, renderStatus, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); } };
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);
-1
View File
@@ -29,7 +29,6 @@ impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for RedactingStderr {
}
}
}
fn redact_regex(pattern: &'static str) -> &'static regex::Regex {
static AUTH: OnceLock<regex::Regex> = OnceLock::new();
static QUERY: OnceLock<regex::Regex> = OnceLock::new();
+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.25.6"
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.7"
)]
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.25.6 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
println!("RustProxy live logs 0.25.7 - 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();
+36
View File
@@ -102,6 +102,8 @@ fn render_backends(
("io_errors_total", "passive_io_errors_total", "counter"),
("slow_responses_total", "slow_responses_total", "counter"),
("slow_responses_total", "passive_slow_total", "counter"),
("retry_attempts_total", "retry_attempts_total", "counter"),
("retry_successes_total", "retry_successes_total", "counter"),
(
"capacity_rejections_total",
"capacity_rejections_total",
@@ -117,6 +119,17 @@ fn render_backends(
),
("read_timeouts_total", "read_timeouts_total", "counter"),
("send_timeouts_total", "send_timeouts_total", "counter"),
("upstream_client_generation", "upstream_client_generation", "gauge"),
(
"upstream_client_generation_requests",
"upstream_client_generation_requests",
"gauge",
),
(
"upstream_client_rotations_total",
"upstream_client_rotations_total",
"counter",
),
(
"max_concurrent_requests",
"max_concurrent_requests",
@@ -151,6 +164,11 @@ fn render_backends(
1000.0,
),
("traffic_bytes_per_second", "traffic_bytes_per_second", 1.0),
(
"upstream_client_generation_age_seconds",
"upstream_client_generation_age_seconds",
1.0,
),
] {
if let Some(n) = member[field].as_f64() {
f.add(
@@ -379,6 +397,8 @@ fn render_docker(
("io_errors_total", "passive_io_errors_total", "counter"),
("slow_responses_total", "slow_responses_total", "counter"),
("slow_responses_total", "passive_slow_total", "counter"),
("retry_attempts_total", "retry_attempts_total", "counter"),
("retry_successes_total", "retry_successes_total", "counter"),
(
"capacity_rejections_total",
"capacity_rejections_total",
@@ -394,6 +414,17 @@ fn render_docker(
),
("read_timeouts_total", "read_timeouts_total", "counter"),
("send_timeouts_total", "send_timeouts_total", "counter"),
("upstream_client_generation", "upstream_client_generation", "gauge"),
(
"upstream_client_generation_requests",
"upstream_client_generation_requests",
"gauge",
),
(
"upstream_client_rotations_total",
"upstream_client_rotations_total",
"counter",
),
(
"max_concurrent_requests",
"max_concurrent_requests",
@@ -428,6 +459,11 @@ fn render_docker(
1000.0,
),
("traffic_bytes_per_second", "traffic_bytes_per_second", 1.0),
(
"upstream_client_generation_age_seconds",
"upstream_client_generation_age_seconds",
1.0,
),
] {
if let Some(n) = member[field].as_f64() {
f.add(
+1 -1
View File
@@ -156,7 +156,7 @@ pub(super) fn fastcgi_params(
}
params.insert("HTTPS".into(), if context.tls { "on" } else { "off" }.into());
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.6".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.7".into());
params.insert("REDIRECT_STATUS".into(), "200".into());
if let Some(value) = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()) {
params.insert("CONTENT_TYPE".into(), value.into());
+1 -1
View File
@@ -180,7 +180,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.25.6</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.25.7</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.25.6</b>"));
assert!(body.contains("RustProxy <b>0.25.7</b>"));
}
#[tokio::test]