v0.25.5
This commit is contained in:
1 parent
1dea7f55c8
commit
c819f0a335
151 files changed
+7610
-4264
No files matched your search
+3
-3
@@ -1,8 +1,8 @@
|
||||
fn valid_certificate_api_token(value: &str) -> bool {
|
||||
(32..=128).contains(&value.len())
|
||||
&& value.bytes().all(|byte| {
|
||||
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~')
|
||||
})
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~'))
|
||||
}
|
||||
|
||||
fn valid_certificate_api_key_id(value: &str) -> bool {
|
||||
|
||||
+31
-20
@@ -169,8 +169,7 @@ async fn certificate_deploy(
|
||||
}
|
||||
|
||||
let administration_restart_required = config.administration.tls
|
||||
&& config.administration.certificate_id.as_deref()
|
||||
== Some(certificate_id.as_str());
|
||||
&& config.administration.certificate_id.as_deref() == Some(certificate_id.as_str());
|
||||
let result = state
|
||||
.update_as(
|
||||
Update {
|
||||
@@ -196,8 +195,12 @@ async fn certificate_deploy(
|
||||
})
|
||||
.await
|
||||
{
|
||||
Err(join_error) => tracing::warn!(%join_error, "external certificate rollback task failed"),
|
||||
Ok(Err(rollback_error)) => tracing::warn!(%rollback_error, "external certificate rollback failed"),
|
||||
Err(join_error) => {
|
||||
tracing::warn!(%join_error, "external certificate rollback task failed")
|
||||
}
|
||||
Ok(Err(rollback_error)) => {
|
||||
tracing::warn!(%rollback_error, "external certificate rollback failed")
|
||||
}
|
||||
Ok(Ok(())) => {}
|
||||
}
|
||||
}
|
||||
@@ -227,8 +230,12 @@ async fn certificate_deploy(
|
||||
})
|
||||
.await
|
||||
{
|
||||
Err(join_error) => tracing::warn!(%join_error, "previous external certificate cleanup task failed"),
|
||||
Ok(Err(cleanup_error)) => tracing::warn!(%cleanup_error, "previous external certificate cleanup failed"),
|
||||
Err(join_error) => {
|
||||
tracing::warn!(%join_error, "previous external certificate cleanup task failed")
|
||||
}
|
||||
Ok(Err(cleanup_error)) => {
|
||||
tracing::warn!(%cleanup_error, "previous external certificate cleanup failed")
|
||||
}
|
||||
Ok(Ok(())) => {}
|
||||
}
|
||||
}
|
||||
@@ -271,12 +278,13 @@ async fn certificate_reload(
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let revision = state.reload_certificates().await?;
|
||||
let snapshot = state.config.load_full();
|
||||
let admin_certificate_restart_required = snapshot
|
||||
.envelope
|
||||
.config
|
||||
.administration
|
||||
.tls
|
||||
&& snapshot.envelope.config.administration.certificate_id.is_some();
|
||||
let admin_certificate_restart_required = snapshot.envelope.config.administration.tls
|
||||
&& snapshot
|
||||
.envelope
|
||||
.config
|
||||
.administration
|
||||
.certificate_id
|
||||
.is_some();
|
||||
state
|
||||
.store
|
||||
.audit(
|
||||
@@ -295,23 +303,26 @@ async fn certificate_reload(
|
||||
})))
|
||||
}
|
||||
|
||||
|
||||
async fn certificate_reload_one(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Path(certificate_id): Path<String>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
if !crate::config::valid_id(&certificate_id) {
|
||||
return Err(ApiError(StatusCode::NOT_FOUND, "certificate not found".into()));
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"certificate not found".into(),
|
||||
));
|
||||
}
|
||||
let revision = state.reload_certificate(&certificate_id).await?;
|
||||
let snapshot = state.config.load_full();
|
||||
let admin_certificate_restart_required = snapshot
|
||||
.envelope
|
||||
.config
|
||||
.administration
|
||||
.tls
|
||||
&& snapshot.envelope.config.administration.certificate_id.as_deref()
|
||||
let admin_certificate_restart_required = snapshot.envelope.config.administration.tls
|
||||
&& snapshot
|
||||
.envelope
|
||||
.config
|
||||
.administration
|
||||
.certificate_id
|
||||
.as_deref()
|
||||
== Some(certificate_id.as_str());
|
||||
state
|
||||
.store
|
||||
|
||||
+448
-67
@@ -12,73 +12,454 @@ use std::sync::OnceLock;
|
||||
// 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.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"))),
|
||||
(
|
||||
"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")),
|
||||
),
|
||||
];
|
||||
|
||||
pub(super) async fn index() -> Html<&'static str> {
|
||||
|
||||
+19
-10
@@ -188,14 +188,12 @@ async fn logs(
|
||||
})
|
||||
.take(limit)
|
||||
.collect();
|
||||
Ok(Json(
|
||||
json!({
|
||||
"entries": entries,
|
||||
"capacity": crate::logging::HISTORY_CAPACITY,
|
||||
"window": crate::logging::LIVE_WINDOW,
|
||||
"dropped": state.logs.dropped.load(Ordering::Relaxed)
|
||||
}),
|
||||
))
|
||||
Ok(Json(json!({
|
||||
"entries": entries,
|
||||
"capacity": crate::logging::HISTORY_CAPACITY,
|
||||
"window": crate::logging::LIVE_WINDOW,
|
||||
"dropped": state.logs.dropped.load(Ordering::Relaxed)
|
||||
})))
|
||||
}
|
||||
|
||||
async fn live_logs_stream(
|
||||
@@ -208,7 +206,12 @@ async fn live_logs_stream(
|
||||
.viewer_slots
|
||||
.clone()
|
||||
.try_acquire_owned()
|
||||
.map_err(|_| ApiError(StatusCode::TOO_MANY_REQUESTS, "events.too_many_viewers".into()))?;
|
||||
.map_err(|_| {
|
||||
ApiError(
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
"events.too_many_viewers".into(),
|
||||
)
|
||||
})?;
|
||||
let receiver = state.logs.subscribe();
|
||||
let next_auth_check = tokio::time::Instant::now() + Duration::from_secs(10);
|
||||
let stream = futures_util::stream::unfold(
|
||||
@@ -344,7 +347,13 @@ async fn traffic_status(
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let snapshot = state.config.load_full();
|
||||
if query.vhost.as_ref().is_some_and(|id| {
|
||||
id != "*" && !snapshot.envelope.config.vhosts.iter().any(|vhost| &vhost.id == id)
|
||||
id != "*"
|
||||
&& !snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.any(|vhost| &vhost.id == id)
|
||||
}) {
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
|
||||
+29
-3
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.25.4",
|
||||
"version": "0.25.5",
|
||||
"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": [
|
||||
@@ -489,7 +489,7 @@
|
||||
"cloudflare_client_ip": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Use a verified Cloudflare connecting-IP header only when the immediate peer is in the configured trusted Cloudflare ranges."
|
||||
"description": "Use verified Cloudflare client-IP and country headers only when the immediate peer is in the built-in Cloudflare edge ranges; country falls back to local GeoIP when unavailable or invalid."
|
||||
},
|
||||
"timing_headers": {
|
||||
"type": "boolean",
|
||||
@@ -2305,6 +2305,11 @@
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"include_traffic": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Export bounded Traffic analytics aggregates. Raw client IPs, request paths and full User-Agent strings are intentionally excluded."
|
||||
},
|
||||
"include_backends": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
@@ -4584,6 +4589,13 @@
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"user_agent_families": {
|
||||
"type": "array",
|
||||
"description": "Fixed-cardinality User-Agent families used by Prometheus; raw User-Agent top values remain available only in the authenticated Traffic API/UI.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"client_ips": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -4596,6 +4608,20 @@
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"method_families": {
|
||||
"type": "array",
|
||||
"description": "Fixed-cardinality common HTTP method families with all extension methods folded into OTHER.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"description": "Fixed serving-source families: reverse_proxy, web_server, local_response or unresolved.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrafficTopEntry"
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -9350,4 +9376,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-17
@@ -66,7 +66,6 @@ async fn cache_purge(
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
async fn cache_invalidate(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
@@ -123,11 +122,18 @@ async fn cache_ban_delete(
|
||||
Path(id): Path<u64>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
if !state.cache.remove_ban(id) {
|
||||
return Err(ApiError(StatusCode::NOT_FOUND, "unknown cache BAN rule".into()));
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"unknown cache BAN rule".into(),
|
||||
));
|
||||
}
|
||||
state
|
||||
.store
|
||||
.audit(&session.username, "cache.ban.remove", &format!("ban_id={id}"))
|
||||
.audit(
|
||||
&session.username,
|
||||
"cache.ban.remove",
|
||||
&format!("ban_id={id}"),
|
||||
)
|
||||
.await
|
||||
.map_err(auth::internal)?;
|
||||
Ok(Json(json!({"removed": id, "bans": state.cache.bans()})))
|
||||
@@ -164,7 +170,10 @@ async fn kill_tunnel(
|
||||
));
|
||||
}
|
||||
let Some(tunnel) = state.tunnel_registry.kill(id) else {
|
||||
return Err(ApiError(StatusCode::NOT_FOUND, "unknown active tunnel".into()));
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"unknown active tunnel".into(),
|
||||
));
|
||||
};
|
||||
state
|
||||
.store
|
||||
@@ -207,10 +216,12 @@ async fn cache_cold_cleanup_job(
|
||||
State(state): State<Arc<App>>,
|
||||
Path(id): Path<u64>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let job = state
|
||||
.cache
|
||||
.cold_cleanup_job(id)
|
||||
.ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "unknown cold-cache cleanup job".into()))?;
|
||||
let job = state.cache.cold_cleanup_job(id).ok_or_else(|| {
|
||||
ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"unknown cold-cache cleanup job".into(),
|
||||
)
|
||||
})?;
|
||||
Ok(Json(json!({"job": job})))
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
@@ -268,7 +279,6 @@ async fn certificate_upload(
|
||||
Ok(Json(json!({"certificate":certificate, "staged":true})))
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct CertificateApiRotate {
|
||||
@@ -339,12 +349,7 @@ async fn certificate_api_rotate(
|
||||
let token_hash = auth::secret_hash(&token);
|
||||
let credential = state
|
||||
.store
|
||||
.upsert_certificate_api_credential(
|
||||
&key_id,
|
||||
&token_hash,
|
||||
&comment,
|
||||
&session.username,
|
||||
)
|
||||
.upsert_certificate_api_credential(&key_id, &token_hash, &comment, &session.username)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
if error.to_string().contains("credential limit reached") {
|
||||
@@ -371,7 +376,10 @@ async fn certificate_api_revoke(
|
||||
Path(key_id): Path<String>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
if !valid_certificate_api_key_id(&key_id) {
|
||||
return Err(ApiError(StatusCode::NOT_FOUND, "certificate_api.not_found".into()));
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"certificate_api.not_found".into(),
|
||||
));
|
||||
}
|
||||
let revoked = state
|
||||
.store
|
||||
@@ -379,7 +387,10 @@ async fn certificate_api_revoke(
|
||||
.await
|
||||
.map_err(auth::internal)?;
|
||||
if !revoked {
|
||||
return Err(ApiError(StatusCode::NOT_FOUND, "certificate_api.not_found".into()));
|
||||
return Err(ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"certificate_api.not_found".into(),
|
||||
));
|
||||
}
|
||||
Ok(Json(json!({"revoked": true, "key_id": key_id})))
|
||||
}
|
||||
|
||||
+11
-7
@@ -36,7 +36,8 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/app/config-history.js"),
|
||||
include_str!("web/app/runtime.js"),
|
||||
include_str!("web/app/settings.js"),
|
||||
].join("\n")
|
||||
]
|
||||
.join("\n")
|
||||
});
|
||||
ASSET.as_str()
|
||||
}
|
||||
@@ -49,7 +50,8 @@ fn admin_vhosts_javascript() -> &'static str {
|
||||
include_str!("web/vhosts/editor.js"),
|
||||
include_str!("web/vhosts/render.js"),
|
||||
"})();",
|
||||
].join("\n")
|
||||
]
|
||||
.join("\n")
|
||||
});
|
||||
ASSET.as_str()
|
||||
}
|
||||
@@ -449,9 +451,11 @@ fn certificate_api_operation(method: &Method, path: &str) -> bool {
|
||||
if method != Method::POST {
|
||||
return false;
|
||||
}
|
||||
matches!(path, "/api/certificates/deploy" | "/api/certificates/reload")
|
||||
|| path
|
||||
.strip_prefix("/api/certificates/")
|
||||
.and_then(|tail| tail.strip_suffix("/reload"))
|
||||
.is_some_and(crate::config::valid_id)
|
||||
matches!(
|
||||
path,
|
||||
"/api/certificates/deploy" | "/api/certificates/reload"
|
||||
) || path
|
||||
.strip_prefix("/api/certificates/")
|
||||
.and_then(|tail| tail.strip_suffix("/reload"))
|
||||
.is_some_and(crate::config::valid_id)
|
||||
}
|
||||
@@ -45,7 +45,10 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
||||
Ok(None) => None,
|
||||
Ok(Some((key_id, token))) => {
|
||||
if peer.is_some_and(|peer| !peer.0.ip().is_loopback()) && scheme != "https" {
|
||||
return secure(reject(StatusCode::FORBIDDEN, "certificate_api.https_required"));
|
||||
return secure(reject(
|
||||
StatusCode::FORBIDDEN,
|
||||
"certificate_api.https_required",
|
||||
));
|
||||
}
|
||||
let token_hash = auth::secret_hash(&token);
|
||||
match state
|
||||
|
||||
+14
-4
@@ -158,8 +158,8 @@ fn status_payload(state: &App) -> Value {
|
||||
payload["transform_resources"] = json!(&transform_resources);
|
||||
payload["geoip"] = json!(state.geoip.view());
|
||||
let system = state.diagnostics.view();
|
||||
let cache_ram_bytes = (cache_view.hot_allocated_bytes as u64)
|
||||
.saturating_add(cache_view.transient_bytes);
|
||||
let cache_ram_bytes =
|
||||
(cache_view.hot_allocated_bytes as u64).saturating_add(cache_view.transient_bytes);
|
||||
let non_cache_rss_bytes = system
|
||||
.process_rss_bytes
|
||||
.map(|rss| rss.saturating_sub(cache_ram_bytes));
|
||||
@@ -359,7 +359,12 @@ async fn config_history(
|
||||
.configuration_revision(revision)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
.ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "configuration revision not found".into()))?;
|
||||
.ok_or_else(|| {
|
||||
ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"configuration revision not found".into(),
|
||||
)
|
||||
})?;
|
||||
return Ok(Json(json!({"revision": detail})));
|
||||
}
|
||||
let limit = query.limit.unwrap_or(512).clamp(1, 512);
|
||||
@@ -393,7 +398,12 @@ async fn restore_config_history(
|
||||
.configuration_revision(input.revision)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
.ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "configuration revision not found".into()))?;
|
||||
.ok_or_else(|| {
|
||||
ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"configuration revision not found".into(),
|
||||
)
|
||||
})?;
|
||||
state
|
||||
.restore_as(
|
||||
Update {
|
||||
|
||||
@@ -390,6 +390,8 @@ function stopTunnelRefresh() { if (tunnelRefreshTimer) clearInterval(tunnelRefre
|
||||
function startTunnelRefresh() { stopTunnelRefresh(); tunnelRefreshTimer = setInterval(() => { if ($("tunnels-dialog")?.open) void loadTunnels(); }, 2000); }
|
||||
$("login-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; authMessage(""); try { await withFormSubmitState(form, event.submitter, async () => { const username = $("username").value.trim(); const password = $("password").value; if (setupRequired) { if (password !== $("password-confirm").value) throw fieldError("password-confirm", t("passwordMismatch")); await api("/api/auth/setup", { method: "POST", body: JSON.stringify({ token: $("setup-token").value.trim(), username, password }) }); setupRequired = false; syncAuthForm(); } const info = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); await activate(info); clearTimeout(toastTimer); hideToast(); }); } catch (e) { if (e.code === "auth.already_initialized") { setupRequired = false; syncAuthForm(); } $("password").value = ""; $("password-confirm").value = ""; authMessage(e.message); } });
|
||||
$("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);
|
||||
@@ -398,8 +400,17 @@ $("ui-theme").addEventListener("change", async () => {
|
||||
catch (error) { applyAdminTheme(previousTheme); if (error.status !== 401) toast(error.message, true); }
|
||||
finally { select.disabled = false; }
|
||||
});
|
||||
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => setPanel(button.dataset.panel)));
|
||||
window.addEventListener("popstate", () => setPanel(panelFromLocation(), false));
|
||||
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");
|
||||
function setMobileMenu(open) {
|
||||
if (!mobileMenuToggle || !dashboardSidebar) return;
|
||||
dashboardSidebar.classList.toggle("mobile-nav-open", open);
|
||||
mobileMenuToggle.setAttribute("aria-expanded", String(open));
|
||||
}
|
||||
mobileMenuToggle?.addEventListener("click", event => { event.stopPropagation(); setMobileMenu(!dashboardSidebar.classList.contains("mobile-nav-open")); });
|
||||
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => { setPanel(button.dataset.panel); setMobileMenu(false); }));
|
||||
document.addEventListener("click", event => { if (dashboardSidebar?.classList.contains("mobile-nav-open") && !dashboardSidebar.contains(event.target)) setMobileMenu(false); });
|
||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && dashboardSidebar?.classList.contains("mobile-nav-open")) { setMobileMenu(false); mobileMenuToggle?.focus(); } });
|
||||
window.addEventListener("popstate", () => { setPanel(panelFromLocation(), false); setMobileMenu(false); });
|
||||
$("host-policy-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const hosts = [...new Set($("policy-hosts").value.split(",").map(h => h.trim().toLowerCase()).filter(Boolean))]; const acl = { default: $("policy-default").value, rules: parseRules($("policy-rules").value, $("policy-rules")) }; const next = structuredClone(envelope.config); next.host_policies = (next.host_policies || []).map(p => ({ ...p, hosts: p.hosts.filter(h => !hosts.includes(h)) })).filter(p => p.hosts.length); next.host_policies.push({ hosts, acl }); if (await saveSection("host-policies", next.host_policies)) { form.reset(); if ($("security-policy-editor")) $("security-policy-editor").open = false; } }); } catch (e) { toast(e.message, true); } });
|
||||
$("password-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; try { await withFormSubmitState(form, event.submitter, async () => { if ($("new-password").value !== $("new-password-confirm").value) throw fieldError("new-password-confirm", t("passwordMismatch")); await api("/api/auth/password", { method: "POST", body: JSON.stringify({ current_password: $("current-password").value, new_password: $("new-password").value }) }); lock(); toast(t("passwordChanged")); }); } catch (e) { toast(e.message, true); } });
|
||||
$("revoke-sessions").addEventListener("click", async () => { if (!(await appConfirm(t("confirmRevoke"), { danger: true }))) return; try { await api("/api/auth/logout-all", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { toast(e.message, true); } });
|
||||
|
||||
@@ -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.4";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.5";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ main {
|
||||
background: #101923;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.docs-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -312,7 +313,7 @@ main {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
|
||||
}
|
||||
|
||||
.markdown-reader :not(pre) > code {
|
||||
.markdown-reader :not(pre)>code {
|
||||
padding: 2px 4px;
|
||||
background: #edf1f4
|
||||
}
|
||||
@@ -342,4 +343,4 @@ main {
|
||||
.markdown-reader {
|
||||
padding: 18px
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-6
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -10,10 +11,12 @@
|
||||
<script defer src="/docs/swagger-ui-bundle.js"></script>
|
||||
<script defer src="/docs/init.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="docs-bar"><a class="docs-name" href="/">RustProxy</a>
|
||||
<h1 data-i18n="docsTitle">Documentation</h1>
|
||||
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout" data-i18n="docsLogout">Sign out</button>
|
||||
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout"
|
||||
data-i18n="docsLogout">Sign out</button>
|
||||
</header>
|
||||
<main>
|
||||
<section class="docs-session">
|
||||
@@ -22,14 +25,16 @@
|
||||
</section>
|
||||
<p id="docs-error" role="alert"></p>
|
||||
<nav class="docs-tabs" aria-label="Documentation sections">
|
||||
<button type="button" id="docs-tab-reader" class="active" data-docs-tab="reader" data-i18n="docsReader">Reader</button>
|
||||
<button type="button" id="docs-tab-reader" class="active" data-docs-tab="reader"
|
||||
data-i18n="docsReader">Reader</button>
|
||||
<button type="button" id="docs-tab-api" data-docs-tab="api" data-i18n="docsApi">API</button>
|
||||
</nav>
|
||||
<section id="docs-reader-pane" class="docs-pane">
|
||||
<div class="reader-layout">
|
||||
<aside class="reader-sidebar">
|
||||
<label for="docs-reader-search" data-i18n="docsReaderSearch">Search documentation</label>
|
||||
<input id="docs-reader-search" type="search" autocomplete="off" data-i18n-placeholder="docsReaderSearchPlaceholder" placeholder="Search title or path">
|
||||
<input id="docs-reader-search" type="search" autocomplete="off"
|
||||
data-i18n-placeholder="docsReaderSearchPlaceholder" placeholder="Search title or path">
|
||||
<div id="docs-reader-list" class="reader-list"></div>
|
||||
</aside>
|
||||
<article id="docs-reader-content" class="markdown-reader" aria-live="polite"></article>
|
||||
@@ -37,12 +42,15 @@
|
||||
</section>
|
||||
<section id="docs-api-pane" class="docs-pane" hidden>
|
||||
<section class="docs-api-controls">
|
||||
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write operations</span></label>
|
||||
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed here.</p>
|
||||
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write
|
||||
operations</span></label>
|
||||
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed
|
||||
here.</p>
|
||||
</section>
|
||||
<div id="swagger-ui"></div>
|
||||
<p><a href="/docs/licenses.txt" data-i18n="docsLicenses">Swagger UI licenses</a></p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
+577
-248
File diff suppressed because it is too large.
Load diff
@@ -126,7 +126,7 @@ curl --fail --show-error --request POST \
|
||||
catch (error) { $("certificate-api-status").textContent = error.message; }
|
||||
finally { certificateApiLoading = false; }
|
||||
}
|
||||
const defaults = { enabled: false, listen: "127.0.0.1:9191", path: "/metrics", allowed_cidrs: ["127.0.0.0/8", "::1/128"], bearer_token_env: null, allow_trusted_network_without_token: false, include_system: true, include_vhosts: true, include_backends: true, include_docker_vhosts: true, include_cache_domains: true, include_streams: true, include_logging: true };
|
||||
const defaults = { enabled: false, listen: "127.0.0.1:9191", path: "/metrics", allowed_cidrs: ["127.0.0.0/8", "::1/128"], bearer_token_env: null, allow_trusted_network_without_token: false, include_system: true, include_vhosts: true, include_traffic: true, include_backends: true, include_docker_vhosts: true, include_cache_domains: true, include_streams: true, include_logging: true };
|
||||
function example() {
|
||||
const addr = $("prom-listen").value.trim(), path = $("prom-path").value.trim(), env = $("prom-token-env").value.trim();
|
||||
$("prom-scrape-example").textContent = `scrape_configs:\n - job_name: rustproxy\n metrics_path: ${JSON.stringify(path)}\n static_configs:\n - targets: [${JSON.stringify(addr)}]${env ? "\n authorization:\n credentials_file: /etc/prometheus/rustproxy.token" : ""}`;
|
||||
@@ -136,7 +136,7 @@ curl --fail --show-error --request POST \
|
||||
if (!settingsDirty.has("prometheus-form")) {
|
||||
$("prom-enabled").checked = p.enabled; $("prom-listen").value = p.listen; $("prom-path").value = p.path;
|
||||
$("prom-cidrs").value = p.allowed_cidrs.join("\n"); $("prom-token-env").value = p.bearer_token_env || ""; $("prom-trusted-no-token").checked = !!p.allow_trusted_network_without_token;
|
||||
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
|
||||
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["traffic", "include_traffic"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
|
||||
}
|
||||
const status = latestStatus?.prometheus;
|
||||
$("prom-status").textContent = status?.restart_required ? t("restartPending") : status?.active?.enabled ? `${t("prometheusActive")}: ${status.active.listen}${p.path}` : t("prometheusDisabled"); example();
|
||||
@@ -170,7 +170,7 @@ curl --fail --show-error --request POST \
|
||||
bindSettingsForm("prometheus-form", next => {
|
||||
next.prometheus = {
|
||||
enabled: $("prom-enabled").checked, listen: $("prom-listen").value.trim(), path: $("prom-path").value.trim(), allowed_cidrs: $("prom-cidrs").value.split(/[\n,]/).map(v => v.trim()).filter(Boolean), bearer_token_env: $("prom-token-env").value.trim() || null, allow_trusted_network_without_token: $("prom-trusted-no-token").checked,
|
||||
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
|
||||
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_traffic: $("prom-traffic").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
|
||||
};
|
||||
}, "prometheus");
|
||||
bindSettingsForm("administration-form", next => {
|
||||
|
||||
+230
-28
@@ -3781,7 +3781,7 @@ body[data-density="dense"] .stat strong {
|
||||
max-width: none
|
||||
}
|
||||
|
||||
.settings-page>.edit-form + .edit-form {
|
||||
.settings-page>.edit-form+.edit-form {
|
||||
margin-top: var(--ui-gap)
|
||||
}
|
||||
|
||||
@@ -3920,7 +3920,8 @@ body[data-density="dense"] .stat strong {
|
||||
dialog.vhost-dialog,
|
||||
dialog.logs-dialog,
|
||||
dialog.audit-dialog,
|
||||
dialog.certificate-dialog {
|
||||
dialog.certificate-dialog,
|
||||
dialog.config-history-dialog {
|
||||
border-radius: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
@@ -4692,11 +4693,16 @@ dialog .window-heading {
|
||||
}
|
||||
|
||||
@media(max-width:1100px) {
|
||||
.traffic-wide-chart { grid-column: span 1 }
|
||||
.traffic-wide-chart {
|
||||
grid-column: span 1
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:650px) {
|
||||
.traffic-bar-row { grid-template-columns: minmax(88px, 1.2fr) minmax(90px, 2fr) minmax(60px, auto); gap: 7px }
|
||||
.traffic-bar-row {
|
||||
grid-template-columns: minmax(88px, 1.2fr) minmax(90px, 2fr) minmax(60px, auto);
|
||||
gap: 7px
|
||||
}
|
||||
}
|
||||
|
||||
.logging-grid label {
|
||||
@@ -4720,6 +4726,7 @@ dialog .window-heading {
|
||||
}
|
||||
|
||||
@media(max-width:720px) {
|
||||
|
||||
.traffic-chart-grid,
|
||||
.logging-grid {
|
||||
grid-template-columns: 1fr
|
||||
@@ -6778,7 +6785,6 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* Certificates workspace (0.20.0): use the same tab language as Settings. */
|
||||
#panel-tls .certificate-workspace-heading {
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px
|
||||
@@ -7031,7 +7037,6 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline operation feedback mirrors the login feedback pattern and remains local to the action. */
|
||||
.operation-message:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -7330,7 +7335,6 @@ body[data-density="compact"] .page-footer {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 0.24.3 dashboard refinements */
|
||||
.payload-split {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
@@ -7515,7 +7519,7 @@ body[data-density="compact"] .page-footer {
|
||||
color: var(--notice-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 0.24.8 cache occupancy charts and compact memory-accounting help */
|
||||
|
||||
.mini-heading-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -7607,7 +7611,6 @@ body[data-density="compact"] .page-footer {
|
||||
color: var(--transform-chart);
|
||||
}
|
||||
|
||||
/* Web Service editor mirrors the vhost tabbed configuration layout. */
|
||||
.web-service-pages {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -7628,21 +7631,10 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
|
||||
.config-history-dialog {
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
padding: 0;
|
||||
width: min(1180px, calc(100vw - 24px));
|
||||
max-width: calc(100vw - 24px);
|
||||
max-height: calc(100dvh - 28px);
|
||||
overflow: hidden;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 18px 70px var(--shadow-strong);
|
||||
}
|
||||
|
||||
.config-history-dialog::backdrop {
|
||||
background: var(--shadow-strong);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.config-history-layout {
|
||||
@@ -7667,7 +7659,6 @@ body[data-density="compact"] .page-footer {
|
||||
|
||||
.config-history-month {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 9px;
|
||||
background: var(--panel);
|
||||
}
|
||||
@@ -7695,7 +7686,6 @@ body[data-density="compact"] .page-footer {
|
||||
min-height: 34px;
|
||||
padding: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
@@ -7769,7 +7759,6 @@ button.config-history-day.is-selected {
|
||||
|
||||
.config-history-change {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
@@ -7797,9 +7786,222 @@ button.config-history-day.is-selected {
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.config-history-layout { grid-template-columns: 1fr; overflow: auto; }
|
||||
.config-history-browser, .config-history-detail { overflow: visible; }
|
||||
.config-history-change-values { grid-template-columns: 1fr; }
|
||||
.config-history-detail-head { flex-direction: column; }
|
||||
.config-history-detail-actions { align-items: flex-start; text-align: left; }
|
||||
.config-history-layout {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.config-history-browser,
|
||||
.config-history-detail {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.config-history-change-values {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.config-history-detail-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-history-detail-actions {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media(max-width:850px) {
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 60;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.sidebar .brand {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
min-height: 36px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: 600 11px var(--mono);
|
||||
}
|
||||
|
||||
.mobile-menu-toggle:hover,
|
||||
.mobile-menu-toggle[aria-expanded="true"] {
|
||||
color: var(--text);
|
||||
border-color: var(--border-hover);
|
||||
background: var(--nav-active-bg);
|
||||
}
|
||||
|
||||
.mobile-menu-icon {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
width: 15px;
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
box-shadow: 0 -5px currentColor, 0 5px currentColor;
|
||||
}
|
||||
|
||||
.sidebar .main-navigation {
|
||||
grid-column: 1 / -1;
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sidebar.mobile-nav-open .main-navigation {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.sidebar .main-navigation .nav {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-left: 2px solid transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar .main-navigation .nav.active {
|
||||
border-color: var(--line);
|
||||
border-left-color: var(--green);
|
||||
background: var(--nav-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:600px) {
|
||||
.sidebar .main-navigation .nav span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.about-dialog {
|
||||
width: min(560px, calc(100vw - 24px));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.about-dialog .window-heading button {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.about-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.about-product {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.about-product>div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.about-product strong {
|
||||
font: 600 16px var(--mono);
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.about-meta>div {
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.about-meta dt {
|
||||
margin-bottom: 5px;
|
||||
color: var(--muted);
|
||||
font: 9px var(--mono);
|
||||
letter-spacing: .8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.about-meta dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.about-card>p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.about-repository {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.about-repository span:last-child {
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media(max-width:620px) {
|
||||
.topbar {
|
||||
height: auto;
|
||||
min-height: 46px;
|
||||
padding: 7px 0;
|
||||
}
|
||||
|
||||
.topbar .breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.about-repository {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,12 @@
|
||||
liveChartLimit = LIVE_CHART_LIMITS.has(parsed) ? parsed : 60;
|
||||
for (const key of Object.keys(liveChartSamples)) liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit);
|
||||
if ($("transform-live-samples")) $("transform-live-samples").value = String(liveChartLimit);
|
||||
if (persist) { try { localStorage.setItem(LIVE_CHART_STORAGE_KEY, String(liveChartLimit)); } catch {} }
|
||||
if (persist) { try { localStorage.setItem(LIVE_CHART_STORAGE_KEY, String(liveChartLimit)); } catch { } }
|
||||
redrawLiveChart();
|
||||
}
|
||||
function initializeLiveChartLimit() {
|
||||
let saved = 60;
|
||||
try { saved = Number(localStorage.getItem(LIVE_CHART_STORAGE_KEY) || 60); } catch {}
|
||||
try { saved = Number(localStorage.getItem(LIVE_CHART_STORAGE_KEY) || 60); } catch { }
|
||||
setLiveChartLimit(saved, false);
|
||||
}
|
||||
function renderLiveChart(counters) {
|
||||
|
||||
Reference in new issue
Block a user