v0.36.0
This commit is contained in:
1 parent
3a384baa6b
commit
672c84bc13
21 files changed
+450
-70
No files matched your search
+52
-18
@@ -237,11 +237,22 @@ async fn events(
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct TrafficQuery {
|
||||
vhost: Option<String>,
|
||||
view: Option<String>,
|
||||
}
|
||||
async fn traffic_status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<TrafficQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let live = match query.view.as_deref() {
|
||||
None => false,
|
||||
Some("live") => true,
|
||||
_ => {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid traffic view".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let snapshot = state.config.load_full();
|
||||
if query.vhost.as_ref().is_some_and(|id| {
|
||||
id != "*"
|
||||
@@ -258,14 +269,8 @@ async fn traffic_status(
|
||||
));
|
||||
}
|
||||
let scope = query.vhost.as_deref().unwrap_or("*");
|
||||
let vhosts = snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.map(|vhost| json!({"id": vhost.id, "hosts": vhost.frontend.hosts}))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Json(json!({
|
||||
let mut payload = json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"scope": scope,
|
||||
"since": "process_start",
|
||||
"settings": {
|
||||
@@ -273,9 +278,19 @@ async fn traffic_status(
|
||||
"capture_country": snapshot.envelope.config.logging.capture_country
|
||||
},
|
||||
"geoip": state.geoip.load().view(),
|
||||
"analytics": state.traffic.view(Some(scope)),
|
||||
"vhosts": vhosts
|
||||
})))
|
||||
"analytics": state.traffic.view(Some(scope))
|
||||
});
|
||||
if live {
|
||||
return Ok(Json(payload));
|
||||
}
|
||||
payload["vhosts"] = json!(snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.map(|vhost| json!({"id": vhost.id, "hosts": vhost.frontend.hosts}))
|
||||
.collect::<Vec<_>>());
|
||||
Ok(Json(payload))
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
@@ -284,11 +299,22 @@ struct CacheQuery {
|
||||
vhost: Option<String>,
|
||||
offset: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
view: Option<String>,
|
||||
}
|
||||
async fn cache_status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<CacheQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let live = match query.view.as_deref() {
|
||||
None => false,
|
||||
Some("live") => true,
|
||||
_ => {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid cache view".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let snapshot = state.config.load_full();
|
||||
if query
|
||||
.vhost
|
||||
@@ -300,16 +326,24 @@ async fn cache_status(
|
||||
"unknown vhost cache scope".into(),
|
||||
));
|
||||
}
|
||||
let scopes: Vec<_> = snapshot.envelope.config.vhosts.iter().map(|vhost| {
|
||||
json!({"id":vhost.id, "hosts":vhost.frontend.hosts, "metrics":state.cache.scope_view(&vhost.id),
|
||||
"settings":vhost.cache.resolve(&snapshot.envelope.config.cache).ok()})
|
||||
}).collect();
|
||||
let dashboard = state.cache.dashboard(
|
||||
query.vhost.as_deref(),
|
||||
query.offset.unwrap_or(0).min(16384),
|
||||
query.limit.unwrap_or(50),
|
||||
);
|
||||
Ok(Json(
|
||||
json!({"settings":snapshot.envelope.config.cache, "metrics":state.cache.view(), "vhosts":scopes, "dashboard":dashboard, "bans":state.cache.bans()}),
|
||||
))
|
||||
let mut payload = json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"metrics": state.cache.view(),
|
||||
"dashboard": dashboard,
|
||||
"bans": state.cache.bans()
|
||||
});
|
||||
if live {
|
||||
return Ok(Json(payload));
|
||||
}
|
||||
payload["settings"] = json!(snapshot.envelope.config.cache);
|
||||
payload["vhosts"] = json!(snapshot.envelope.config.vhosts.iter().map(|vhost| {
|
||||
json!({"id":vhost.id, "hosts":vhost.frontend.hosts, "metrics":state.cache.scope_view(&vhost.id),
|
||||
"settings":vhost.cache.resolve(&snapshot.envelope.config.cache).ok()})
|
||||
}).collect::<Vec<_>>());
|
||||
Ok(Json(payload))
|
||||
}
|
||||
+60
-7
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.35.5",
|
||||
"version": "0.36.0",
|
||||
"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. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -1618,6 +1618,11 @@
|
||||
"Cache": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Configuration revision used for this snapshot."
|
||||
},
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
@@ -4790,6 +4795,11 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Configuration revision used for this snapshot."
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/components/schemas/TransformSettings"
|
||||
},
|
||||
@@ -6194,6 +6204,11 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Configuration revision used for this snapshot."
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9872,14 +9887,14 @@
|
||||
},
|
||||
"/api/cache": {
|
||||
"get": {
|
||||
"summary": "Cache metrics, domains and paginated objects",
|
||||
"summary": "Cache metrics, domains and paginated objects; optional lightweight live view",
|
||||
"tags": [
|
||||
"Cache"
|
||||
],
|
||||
"operationId": "get_api_cache",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"description": "Full response by default. view=live omits static settings/vhost configuration while retaining revision, live metrics, dashboard objects/domains and BAN rules.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -9960,6 +9975,18 @@
|
||||
"minimum": 1,
|
||||
"default": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "view",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Set to live for polling. Omits static settings and vhost configuration; refetch the full view when revision changes.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"live"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
@@ -9974,14 +10001,14 @@
|
||||
},
|
||||
"/api/transform": {
|
||||
"get": {
|
||||
"summary": "Transformation runtime metrics and per-vhost effectiveness",
|
||||
"summary": "Transformation runtime metrics and per-vhost effectiveness; optional lightweight live view",
|
||||
"tags": [
|
||||
"Transform"
|
||||
],
|
||||
"operationId": "get_api_transform",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"description": "Full response by default. view=live omits static settings/vhost configuration while retaining revision, resources and live metrics.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -10031,6 +10058,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "view",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Set to live for polling. Omits static settings and vhost configuration; refetch the full view when revision changes.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"live"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"AdminSession": []
|
||||
@@ -12884,7 +12925,7 @@
|
||||
},
|
||||
"/api/traffic": {
|
||||
"get": {
|
||||
"summary": "Bounded per-vhost traffic analytics",
|
||||
"summary": "Bounded per-vhost traffic analytics; optional lightweight live view",
|
||||
"tags": [
|
||||
"Traffic"
|
||||
],
|
||||
@@ -12899,11 +12940,23 @@
|
||||
"type": "string",
|
||||
"default": "*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "view",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "Set to live for polling. Omits the static vhost list; refetch the full view when revision changes.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"live"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"description": "Full response by default. view=live omits the static vhost list while retaining revision, current settings, GeoIP state and analytics.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
|
||||
+34
-5
@@ -1,6 +1,34 @@
|
||||
async fn transform_status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct TransformQuery {
|
||||
view: Option<String>,
|
||||
}
|
||||
|
||||
async fn transform_status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<TransformQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let live = match query.view.as_deref() {
|
||||
None => false,
|
||||
Some("live") => true,
|
||||
_ => {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid transform view".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let snapshot = state.config.load_full();
|
||||
let global = &snapshot.envelope.config.transform;
|
||||
let resources = crate::transform::resource_usage(global);
|
||||
let metrics = state.transform_metrics.view();
|
||||
if live {
|
||||
return Ok(Json(json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"resources": resources,
|
||||
"metrics": metrics
|
||||
})));
|
||||
}
|
||||
let vhosts: Vec<_> = snapshot
|
||||
.envelope
|
||||
.config
|
||||
@@ -14,12 +42,13 @@ async fn transform_status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"revision": snapshot.envelope.revision,
|
||||
"settings": global,
|
||||
"resources": crate::transform::resource_usage(global),
|
||||
"metrics": state.transform_metrics.view(),
|
||||
"resources": resources,
|
||||
"metrics": metrics,
|
||||
"vhosts": vhosts
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
|
||||
@@ -3527,6 +3527,91 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn polling_views_omit_static_configuration_and_reject_unknown_modes() {
|
||||
let (router, store, _dir, secret, _) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
|
||||
for (path, omitted, retained) in [
|
||||
(
|
||||
"/api/cache?view=live",
|
||||
&["settings", "vhosts"][..],
|
||||
&["revision", "metrics", "dashboard", "bans"][..],
|
||||
),
|
||||
(
|
||||
"/api/transform?view=live",
|
||||
&["settings", "vhosts"][..],
|
||||
&["revision", "resources", "metrics"][..],
|
||||
),
|
||||
(
|
||||
"/api/traffic?view=live",
|
||||
&["vhosts"][..],
|
||||
&["revision", "settings", "geoip", "analytics"][..],
|
||||
),
|
||||
] {
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(path)
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK, "GET {path}");
|
||||
let value = json_body(response).await;
|
||||
for &field in omitted {
|
||||
assert!(value.get(field).is_none(), "{path} unexpectedly contains {field}");
|
||||
}
|
||||
for &field in retained {
|
||||
assert!(value.get(field).is_some(), "{path} is missing {field}");
|
||||
}
|
||||
}
|
||||
|
||||
for (path, required) in [
|
||||
("/api/cache", &["settings", "vhosts"][..]),
|
||||
("/api/transform", &["settings", "vhosts"][..]),
|
||||
("/api/traffic", &["vhosts"][..]),
|
||||
] {
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(path)
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK, "GET {path}");
|
||||
let value = json_body(response).await;
|
||||
assert!(value.get("revision").is_some());
|
||||
for &field in required {
|
||||
assert!(value.get(field).is_some(), "{path} is missing {field}");
|
||||
}
|
||||
}
|
||||
|
||||
for path in [
|
||||
"/api/cache?view=unknown",
|
||||
"/api/transform?view=unknown",
|
||||
"/api/traffic?view=unknown",
|
||||
] {
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(path)
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "GET {path}");
|
||||
}
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_and_partial_legacy_refresh_documents() {
|
||||
let previous = serde_json::to_value(AdminPreferences::default()).unwrap();
|
||||
|
||||
@@ -379,9 +379,13 @@ function renderFooter(s) {
|
||||
}
|
||||
let lastLogs = null;
|
||||
function renderLogs(data) { lastLogs = data; window.rustProxyLogs?.render(data); }
|
||||
async function refresh(force = false) {
|
||||
async function refresh(force = false, includeLogs = false) {
|
||||
if (!authenticated) return;
|
||||
if (force) return Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("logs-fallback")]);
|
||||
if (force) {
|
||||
const tasks = [rustProxyRefresh.manual("status-full")];
|
||||
if (includeLogs) tasks.push(rustProxyRefresh.manual("logs-fallback"));
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
return rustProxyRefresh.manual("status-fallback");
|
||||
}
|
||||
function renderLiveStatus(value) { renderStatus(value); }
|
||||
@@ -525,7 +529,7 @@ async function activate(info) {
|
||||
void window.rustProxyApiTokens?.refresh?.();
|
||||
window.rustProxySettings?.scrollToApiTokens?.();
|
||||
}
|
||||
void refresh(true).catch(error => {
|
||||
void refresh(true, true).catch(error => {
|
||||
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
||||
});
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, offset = 0, generation = 0;
|
||||
let loading = false, offset = 0, generation = 0, staticData = null, staticRevision = null;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
|
||||
const liveChartSamples = { hits: [], misses: [], bypasses: [] };
|
||||
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
|
||||
@@ -229,7 +229,16 @@
|
||||
const query = new URLSearchParams({ offset: String(offset), limit: "50" });
|
||||
if (scope() !== "*") query.set("vhost", scope());
|
||||
try {
|
||||
const data = await api(`/api/cache?${query}`, { signal });
|
||||
const base = `/api/cache?${query}`;
|
||||
if (staticData) query.set("view", "live");
|
||||
let data = await api(`/api/cache?${query}`, { signal });
|
||||
if (staticData && data.revision !== staticRevision) data = await api(base, { signal });
|
||||
if (data.settings && Array.isArray(data.vhosts)) {
|
||||
staticData = { settings: data.settings, vhosts: data.vhosts };
|
||||
staticRevision = data.revision;
|
||||
} else if (staticData) {
|
||||
data = { ...staticData, ...data };
|
||||
}
|
||||
if (isCurrent() && token === generation) render(data);
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError" && epoch === authEpoch && isCurrent()) {
|
||||
@@ -343,7 +352,7 @@
|
||||
$("cache-live")?.addEventListener("change", () => rustProxyRefresh.sync());
|
||||
window.rustProxyCachePanel = {
|
||||
refresh, configure, reason, reset() {
|
||||
generation++; offset = 0; resetLiveChart(); closeInvalidation(); closeBans();
|
||||
generation++; offset = 0; staticData = null; staticRevision = null; resetLiveChart(); closeInvalidation(); closeBans();
|
||||
$("cache-domain-rows").replaceChildren(); $("cache-object-rows").replaceChildren(); $("cache-ban-rows").replaceChildren();
|
||||
$("cache-panel-state").textContent = ""; operationMessage("cache-invalidation-status"); operationMessage("cache-bans-status"); operationMessage("cache-toolbar-operation-status");
|
||||
}
|
||||
|
||||
@@ -243,5 +243,4 @@ curl --fail --show-error --request POST \
|
||||
}
|
||||
};
|
||||
renderCertificateApi();
|
||||
loadCertificateApi();
|
||||
})();
|
||||
@@ -50,6 +50,7 @@
|
||||
window.rustProxyRefresh?.setSettingsTab(name);
|
||||
if (authenticated && name === "security") void window.rustProxyApiTokens?.refresh?.();
|
||||
if (authenticated && name === "prometheus") void window.rustProxyPrometheusToken?.refresh?.();
|
||||
if (authenticated && name === "certificate-api") void window.rustProxyCertificateApi?.load?.();
|
||||
if (name !== "prometheus") window.rustProxyPrometheusToken?.hideSecret?.();
|
||||
if (name !== "security") window.rustProxyApiTokens?.hideSecret?.();
|
||||
}
|
||||
@@ -251,7 +252,8 @@
|
||||
$("tls-policy-key-exchange").textContent = kxGroups.length ? `${t("tlsPqcKeyExchangeGroups")}: ${kxGroups.join(", ")}` : "";
|
||||
$("tls-policy-effective").textContent = effective.length ? `${t("tlsEffectiveCiphers")}: ${effective.join(", ")}` : "";
|
||||
$("tls-policy-available").textContent = available.length ? `${t("tlsAvailableCiphers")}: ${available.join(", ")}` : "";
|
||||
window.rustProxyCertificateApi?.render(); window.rustProxyCertificateApi?.load();
|
||||
window.rustProxyCertificateApi?.render();
|
||||
if (authenticated && active === "certificate-api") void window.rustProxyCertificateApi?.load?.();
|
||||
}
|
||||
$("geoip-format").addEventListener("change", syncGeoipFormat); $("geoip-enabled").addEventListener("change", syncGeoipFormat);
|
||||
bindSettingsForm("geoip-form", next => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, lastData = null, activeTab = "overview";
|
||||
let loading = false, lastData = null, activeTab = "overview", staticData = null, staticRevision = null;
|
||||
const chartPalette = ["var(--chart-accent)", "var(--chart-secondary)", "var(--chart-tertiary)", "var(--chart-quaternary)", "var(--chart-quinary)", "var(--chart-senary)", "var(--chart-septenary)", "var(--chart-octonary)"];
|
||||
const scope = () => $("traffic-scope")?.value || "*";
|
||||
const number = value => fmt(Number(value) || 0);
|
||||
@@ -116,7 +116,17 @@
|
||||
if (!authenticated || currentPanel !== "traffic" || loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
const data = await api(`/api/traffic?vhost=${encodeURIComponent(scope())}`, { signal });
|
||||
const base = `/api/traffic?vhost=${encodeURIComponent(scope())}`;
|
||||
let data = await api(staticData ? `${base}&view=live` : base, { signal });
|
||||
if (staticData && data.revision !== staticRevision) {
|
||||
data = await api(base, { signal });
|
||||
}
|
||||
if (Array.isArray(data.vhosts)) {
|
||||
staticData = { vhosts: data.vhosts };
|
||||
staticRevision = data.revision;
|
||||
} else if (staticData) {
|
||||
data = { ...staticData, ...data };
|
||||
}
|
||||
if (isCurrent()) render(data);
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError" && isCurrent()) $("traffic-panel-state").textContent = error.message;
|
||||
@@ -125,7 +135,7 @@
|
||||
function refresh() { return rustProxyRefresh.manual("traffic"); }
|
||||
|
||||
function reset() {
|
||||
lastData = null; if ($("traffic-panel-state")) $("traffic-panel-state").textContent = "";
|
||||
lastData = null; staticData = null; staticRevision = null; if ($("traffic-panel-state")) $("traffic-panel-state").textContent = "";
|
||||
document.querySelectorAll(".traffic-bar-chart").forEach(chart => chart.replaceChildren());
|
||||
for (const id of ["countries", "methods", "methods-detail", "statuses", "statuses-detail", "cache", "cache-detail"]) {
|
||||
const chart = $(`traffic-chart-${id}`), legend = $(`traffic-chart-${id}-legend`), total = $(`traffic-chart-${id}-total`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, generation = 0, lastData = null;
|
||||
let loading = false, generation = 0, lastData = null, staticData = null, staticRevision = null;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
|
||||
const liveChartSamples = { applied: [], cache_hits: [], skipped: [], errors: [] };
|
||||
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
|
||||
@@ -155,7 +155,14 @@
|
||||
loading = true;
|
||||
const epoch = authEpoch, token = generation;
|
||||
try {
|
||||
const data = await api("/api/transform", { signal });
|
||||
let data = await api(staticData ? "/api/transform?view=live" : "/api/transform", { signal });
|
||||
if (staticData && data.revision !== staticRevision) data = await api("/api/transform", { signal });
|
||||
if (data.settings && Array.isArray(data.vhosts)) {
|
||||
staticData = { settings: data.settings, vhosts: data.vhosts };
|
||||
staticRevision = data.revision;
|
||||
} else if (staticData) {
|
||||
data = { ...staticData, ...data };
|
||||
}
|
||||
if (isCurrent() && token === generation) render(data);
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError" && epoch === authEpoch && isCurrent()) {
|
||||
@@ -179,7 +186,7 @@
|
||||
window.rustProxyTransformPanel = {
|
||||
refresh,
|
||||
reset() {
|
||||
generation++; lastData = null; resetLiveChart();
|
||||
generation++; lastData = null; staticData = null; staticRevision = null; resetLiveChart();
|
||||
$("transform-type-rows")?.replaceChildren();
|
||||
$("transform-vhost-rows")?.replaceChildren();
|
||||
$("transform-reason-list")?.replaceChildren();
|
||||
|
||||
Reference in new issue
Block a user