v0.30.0
This commit is contained in:
1 parent
1431dd47f0
commit
f3dd34dae4
42 files changed
+950
-133
No files matched your search
@@ -137,6 +137,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
"historical",
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/MANAGEMENT.md")),
|
||||
),
|
||||
reader_doc!("docs/MEMORY_STRESS.md", "technical"),
|
||||
(
|
||||
"docs/MIGRATION.md",
|
||||
"guides",
|
||||
|
||||
@@ -84,7 +84,7 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
|
||||
.ok()
|
||||
.is_some_and(|meta| meta.file_type().is_dir() && !meta.file_type().is_symlink());
|
||||
if directory_safe {
|
||||
let mut entries = std::fs::read_dir(&directory)
|
||||
let entries = newest_log_files(std::fs::read_dir(&directory)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flat_map(|iter| iter.filter_map(|entry| entry.ok()))
|
||||
@@ -102,10 +102,7 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
|
||||
}
|
||||
let meta = entry.metadata().ok()?;
|
||||
Some((name, path, file_modified_ms(&meta)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
|
||||
entries.truncate(256);
|
||||
}), 256);
|
||||
for (name, path, _) in entries {
|
||||
let id = if name.ends_with(".jsonl") {
|
||||
format!("vhost:{}", name.trim_end_matches(".jsonl"))
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
// Bound candidate retention while scanning potentially enormous rotation folders.
|
||||
// The heap's top is the least desirable result: oldest timestamp, then
|
||||
// lexicographically last name. Final output preserves newest/name order.
|
||||
fn newest_log_files(
|
||||
candidates: impl Iterator<Item = (String, std::path::PathBuf, u64)>,
|
||||
limit: usize,
|
||||
) -> Vec<(String, std::path::PathBuf, u64)> {
|
||||
use std::{cmp::Reverse, collections::BinaryHeap};
|
||||
if limit == 0 { return Vec::new(); }
|
||||
let mut bounded = BinaryHeap::with_capacity(limit);
|
||||
for (name, path, modified_ms) in candidates {
|
||||
let item = (Reverse(modified_ms), name, path);
|
||||
if bounded.len() < limit {
|
||||
bounded.push(item);
|
||||
} else if bounded.peek().is_some_and(|worst| item.cmp(worst).is_lt()) {
|
||||
bounded.pop();
|
||||
bounded.push(item);
|
||||
}
|
||||
}
|
||||
let mut result = bounded.into_iter()
|
||||
.map(|(Reverse(modified), name, path)| (name, path, modified))
|
||||
.collect::<Vec<_>>();
|
||||
result.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
|
||||
result
|
||||
}
|
||||
|
||||
// Bounded file-log inventory helpers and gzip tail reader.
|
||||
fn rotated_gzip_sources(
|
||||
active: &std::path::Path,
|
||||
@@ -12,16 +38,14 @@ fn rotated_gzip_sources(
|
||||
let safe = std::fs::symlink_metadata(parent).ok().is_some_and(|meta| meta.file_type().is_dir() && !meta.file_type().is_symlink());
|
||||
if !safe { return Vec::new(); }
|
||||
let prefix = format!("{base}.");
|
||||
let mut entries = std::fs::read_dir(parent).ok().into_iter().flat_map(|iter| iter.filter_map(|entry| entry.ok())).filter_map(|entry| {
|
||||
let entries = newest_log_files(std::fs::read_dir(parent).ok().into_iter().flat_map(|iter| iter.filter_map(|entry| entry.ok())).filter_map(|entry| {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if name.len() > 180 || !name.starts_with(&prefix) || !name.ends_with(".gz") { return None; }
|
||||
let file_type = entry.file_type().ok()?;
|
||||
if !file_type.is_file() || file_type.is_symlink() { return None; }
|
||||
let meta = entry.metadata().ok()?;
|
||||
Some((name, entry.path(), file_modified_ms(&meta)))
|
||||
}).collect::<Vec<_>>();
|
||||
entries.sort_by(|a,b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
|
||||
entries.truncate(64);
|
||||
}), 64);
|
||||
entries.into_iter().map(|(name,path,_)| source_from_path(
|
||||
format!("{id_prefix}:gz:{name}"),
|
||||
format!("{label_prefix} · {name}"),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.29.7",
|
||||
"version": "0.30.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."
|
||||
},
|
||||
"servers": [
|
||||
|
||||
@@ -48,6 +48,7 @@ fn admin_vhosts_javascript() -> &'static str {
|
||||
[
|
||||
"(() => {",
|
||||
include_str!("web/vhosts/core.js"),
|
||||
include_str!("web/vhosts/health.js"),
|
||||
include_str!("web/vhosts/editor.js"),
|
||||
include_str!("web/vhosts/render.js"),
|
||||
"})();",
|
||||
|
||||
@@ -6,4 +6,5 @@ mod tests {
|
||||
include!("tests/part_05.rs");
|
||||
include!("tests/part_06.rs");
|
||||
include!("tests/part_07.rs");
|
||||
include!("tests/part_08.rs");
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#[test]
|
||||
fn directory_inventory_retains_newest_with_deterministic_ties() {
|
||||
let candidates = (0..10_000).map(|index| {
|
||||
let name = format!("{index:05}.jsonl.gz");
|
||||
(name.clone(), std::path::PathBuf::from(name), (index / 10) as u64)
|
||||
});
|
||||
let files = newest_log_files(candidates, 5);
|
||||
assert_eq!(files.len(), 5);
|
||||
let names: Vec<_> = files.iter().map(|entry| entry.0.as_str()).collect();
|
||||
assert_eq!(names, ["09990.jsonl.gz", "09991.jsonl.gz", "09992.jsonl.gz", "09993.jsonl.gz", "09994.jsonl.gz"]);
|
||||
assert!(newest_log_files(std::iter::once(("x".into(), "x".into(), 1)), 0).is_empty());
|
||||
}
|
||||
@@ -5788,6 +5788,42 @@ dialog .window-heading {
|
||||
padding: 4px 0 8px
|
||||
}
|
||||
|
||||
.vh-backend-health-editor {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 11px;
|
||||
background: var(--surface, transparent)
|
||||
}
|
||||
|
||||
.vh-backend-health-editor h3 {
|
||||
margin: 0;
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
.vh-backend-health-editor .fine {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.vh-backend-health-editor>.vh-field {
|
||||
max-width: 620px
|
||||
}
|
||||
|
||||
.vh-backend-health-editor select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.vh-backend-health-panel {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 12px
|
||||
}
|
||||
|
||||
.vh-backend-health-fields {
|
||||
margin-top: 4px
|
||||
}
|
||||
|
||||
.vh-backend-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
// Vhost UI: language content comes from langs/*.json, never a locale switch.
|
||||
// Drafts are isolated from SSE/config rendering; writes share revision+CSRF CAS.
|
||||
let basicEditor = null, locationsReader = () => [], aclControl = null, editing = null, revision = null, dirty = false, initialized = false, readBindings = () => [], readHosts = () => [], countryRatesReader = () => [], pagesEditor = null, stagedCertificates = [], uploading = false, draftEpoch = 0;
|
||||
let basicEditor = null, locationsReader = () => [], aclControl = null, editing = null, revision = null, dirty = false, initialized = false, readBindings = () => [], readHosts = () => [], countryRatesReader = () => [], pagesEditor = null, stagedCertificates = [], uploading = false, draftEpoch = 0, backendHealthEditorControl = null;
|
||||
const chartHistory = new Map();
|
||||
const backendTrafficHistory = new Map();
|
||||
const vhostTrafficHistory = new Map();
|
||||
@@ -137,7 +137,7 @@
|
||||
const state = {
|
||||
editing, revision, label: $("vhost-editing").textContent,
|
||||
fields: window.rustProxyDrafts.takeChildren(container), tabs: window.rustProxyDrafts.takeChildren($("vhost-tabs")),
|
||||
basicEditor, locationsReader, aclControl, readBindings, readHosts, countryRatesReader, pagesEditor,
|
||||
basicEditor, locationsReader, aclControl, readBindings, readHosts, countryRatesReader, pagesEditor, backendHealthEditorControl,
|
||||
stagedCertificates, uploading
|
||||
};
|
||||
const title = read("id")?.value.trim() || editing || t("draftUntitled");
|
||||
@@ -147,7 +147,7 @@
|
||||
function restoreDraft(state) {
|
||||
draftEpoch++; window.rustProxyDrafts.restoreChildren(container, state.fields); window.rustProxyDrafts.restoreChildren($("vhost-tabs"), state.tabs);
|
||||
editing = state.editing; revision = state.revision; basicEditor = state.basicEditor; locationsReader = state.locationsReader; aclControl = state.aclControl;
|
||||
readBindings = state.readBindings; readHosts = state.readHosts; countryRatesReader = state.countryRatesReader; pagesEditor = state.pagesEditor;
|
||||
readBindings = state.readBindings; readHosts = state.readHosts; countryRatesReader = state.countryRatesReader; pagesEditor = state.pagesEditor; backendHealthEditorControl = state.backendHealthEditorControl || null;
|
||||
stagedCertificates = state.stagedCertificates || []; uploading = state.uploading === true; initialized = true; dirty = true; $("vhost-editing").textContent = state.label;
|
||||
if (!dialog.open) dialog.showModal(); if (latestStatus) renderStatus(latestStatus);
|
||||
}
|
||||
@@ -194,7 +194,7 @@
|
||||
const row = el("article", undefined, "vh-member vh-row vh-backend-card");
|
||||
const head = el("div", undefined, "vh-backend-head"), identity = el("div", undefined, "vh-backend-identity");
|
||||
const title = el("strong", value.id || t("backend"), "vh-backend-title"), endpoint = el("span", value.url || "-", "fine vh-backend-endpoint");
|
||||
identity.append(title, endpoint); head.append(identity); const remove = button("remove", () => { row.remove(); changed(); }); head.append(remove); row.append(head);
|
||||
identity.append(title, endpoint); head.append(identity); const remove = button("remove", () => { row.remove(); backendHealthEditorControl?.refresh(); changed(); }); head.append(remove); row.append(head);
|
||||
|
||||
const main = el("div", undefined, "vh-grid vh-backend-main"); row.append(main);
|
||||
const id = field(main, "member-id", "vhMemberId", value.id, "text", { pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
|
||||
@@ -222,9 +222,12 @@
|
||||
field(extra, "member-max-concurrent", "vhMaxConcurrentRequests", value.max_concurrent_requests ?? 0, "number", { required: true, min: 0, max: 1000000 });
|
||||
field(extra, "member-queue-timeout", "vhBackendQueueTimeout", value.queue_timeout_ms ?? 0, "number", { required: true, min: 0, max: 60000 });
|
||||
note(extra, "vhBackendCapacityHelp");
|
||||
const customHealth = field(extra, "member-health-custom", "backendHealthOverride", value.health_check != null, "checkbox");
|
||||
// The per-member settings live in the Health tab. Keep one persistent form
|
||||
// per backend, so switching the selection never discards unsaved edits.
|
||||
const healthPanel = el("section", undefined, "vh-grid vh-backend-health-panel");
|
||||
const customHealth = field(healthPanel, "member-health-custom", "backendHealthOverride", value.health_check != null, "checkbox");
|
||||
const health = value.health_check || {};
|
||||
const healthFields = el("div", undefined, "vh-wide vh-grid vh-backend-health-fields"); extra.append(healthFields);
|
||||
const healthFields = el("div", undefined, "vh-wide vh-grid vh-backend-health-fields"); healthPanel.append(healthFields);
|
||||
field(healthFields, "member-health-mode", "vhHealthMode", health.mode || "tcp", "select", { values: [["off", "vhOff"], ["tcp", "vhTcp"], ["http", "vhHttp"]] });
|
||||
field(healthFields, "member-health-path", "vhHealthPath", health.path ?? "/", "text", { maxLength: 1024 });
|
||||
field(healthFields, "member-health-host", "vhHealthHost", health.host ?? "", "text", { maxLength: 253 });
|
||||
@@ -236,11 +239,19 @@
|
||||
field(healthFields, "member-health-min", "vhStatusMin", health.expected_status_min ?? 200, "number", { min: 100, max: 599, required: true });
|
||||
field(healthFields, "member-health-max", "vhStatusMax", health.expected_status_max ?? 399, "number", { min: 100, max: 599, required: true });
|
||||
const syncHealth = () => {
|
||||
const editable = controls["member-health-enabled"].checked && customHealth.checked;
|
||||
healthFields.hidden = !customHealth.checked;
|
||||
healthFields.querySelectorAll("input,select").forEach(input => { input.disabled = !customHealth.checked; });
|
||||
healthFields.querySelectorAll("input,select").forEach(input => { input.disabled = !editable; });
|
||||
};
|
||||
customHealth.addEventListener("change", syncHealth); syncHealth();
|
||||
note(extra, "backendHealthOverridesHelp");
|
||||
let customInitialized = value.health_check != null;
|
||||
customHealth.addEventListener("change", () => {
|
||||
if (customHealth.checked && !customInitialized) { inheritBackendHealthInputs(healthPanel); customInitialized = true; }
|
||||
syncHealth();
|
||||
});
|
||||
controls["member-health-enabled"].addEventListener("change", () => { syncHealth(); backendHealthEditorControl?.refresh(); });
|
||||
row.memberHealthPanel = healthPanel;
|
||||
row.memberHealthEnabled = controls["member-health-enabled"];
|
||||
syncHealth();
|
||||
row.append(advanced);
|
||||
|
||||
const actions = el("div", undefined, "vh-backend-actions");
|
||||
@@ -258,10 +269,11 @@
|
||||
riskGate.hidden = show !== true;
|
||||
if (clearWhenHidden && show === false) localRisk.checked = false;
|
||||
};
|
||||
id.addEventListener("input", syncIdentity); url.addEventListener("input", () => { syncIdentity(); syncRisk(true); }); url.addEventListener("change", () => syncRisk(true));
|
||||
id.addEventListener("input", () => { syncIdentity(); backendHealthEditorControl?.refresh(); }); url.addEventListener("input", () => { syncIdentity(); syncRisk(true); backendHealthEditorControl?.refresh(); }); url.addEventListener("change", () => syncRisk(true));
|
||||
row.syncLocalRisk = () => syncRisk(false);
|
||||
syncIdentity(); syncRisk(false); parent.append(row);
|
||||
syncIdentity(); syncRisk(false); parent.append(row); backendHealthEditorControl?.refresh();
|
||||
}
|
||||
|
||||
function countryRateEditor(parent, values = []) {
|
||||
const rows = el("div", undefined, "vh-wide vh-country-rates"); parent.append(rows);
|
||||
const add = (value = {}) => {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
function load(value = defaults(), migrationIds = [], show = true) {
|
||||
draftEpoch++; stagedCertificates = []; uploading = false;
|
||||
draftEpoch++; stagedCertificates = []; uploading = false; backendHealthEditorControl = null;
|
||||
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");
|
||||
@@ -205,6 +205,8 @@
|
||||
field(g, "health-path", "vhHealthPath", health.path || "/", "text", { required: true, maxLength: 1024 });
|
||||
field(g, "health-host", "vhHealthHost", health.host || "", "text", { maxLength: 260 });
|
||||
for (const [name, key, val, min, max] of [["interval", "vhInterval", health.interval_ms ?? 5000, 250, 300000], ["timeout", "vhTimeout", health.timeout_ms ?? 2000, 50, 60000], ["slow", "vhSlow", health.slow_response_ms ?? 1500, 0, 60000], ["fall", "vhFall", health.fall ?? 3, 1, 20], ["rise", "vhRise", health.rise ?? 2, 1, 20], ["status-min", "vhStatusMin", health.expected_status_min ?? 200, 100, 599], ["status-max", "vhStatusMax", health.expected_status_max ?? 399, 100, 599]]) field(g, name, key, val, "number", { required: true, min, max }); note(g, "vhHealthNote");
|
||||
backendHealthEditorControl = backendHealthEditor(g, members);
|
||||
note(g, "backendHealthOverridesHelp");
|
||||
const passive = settings.passive_health || {};
|
||||
g = group("vhPassiveHealth");
|
||||
const passiveEnabled = field(g, "passive-enabled", "vhPassiveEnabled", passive.enabled === true, "checkbox");
|
||||
@@ -391,11 +393,11 @@
|
||||
id: str("member-id", row), url: str("member-url", row), weight: num("member-weight", row), enabled: yes("member-enabled", row),
|
||||
pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row),
|
||||
health_check_enabled: yes("member-health-enabled", row),
|
||||
health_check: yes("member-health-custom", row) ? {
|
||||
mode: str("member-health-mode", row), path: str("member-health-path", row), host: nullable("member-health-host", row),
|
||||
interval_ms: num("member-health-interval", row), timeout_ms: num("member-health-timeout", row), slow_response_ms: num("member-health-slow", row),
|
||||
fall: num("member-health-fall", row), rise: num("member-health-rise", row),
|
||||
expected_status_min: num("member-health-min", row), expected_status_max: num("member-health-max", row)
|
||||
health_check: yes("member-health-custom", row.memberHealthPanel) ? {
|
||||
mode: str("member-health-mode", row.memberHealthPanel), path: str("member-health-path", row.memberHealthPanel), host: nullable("member-health-host", row.memberHealthPanel),
|
||||
interval_ms: num("member-health-interval", row.memberHealthPanel), timeout_ms: num("member-health-timeout", row.memberHealthPanel), slow_response_ms: num("member-health-slow", row.memberHealthPanel),
|
||||
fall: num("member-health-fall", row.memberHealthPanel), rise: num("member-health-rise", row.memberHealthPanel),
|
||||
expected_status_min: num("member-health-min", row.memberHealthPanel), expected_status_max: num("member-health-max", row.memberHealthPanel)
|
||||
} : null,
|
||||
allow_private: yes("member-private", row), accept_local_listener_risk: yes("member-local-risk", row),
|
||||
preserve_host: yes("member-host", row), protocol: str("member-protocol", row), address_allowlist: lines(str("member-allowlist", row)),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
// Per-member health editor shares the vhost form state from core.js.
|
||||
// Starting a custom override should copy the CURRENT shared policy once,
|
||||
// rather than surprising the operator with unrelated hardcoded defaults.
|
||||
function inheritBackendHealthInputs(panel) {
|
||||
for (const [local, shared] of [
|
||||
["member-health-mode", "health-mode"], ["member-health-path", "health-path"],
|
||||
["member-health-host", "health-host"], ["member-health-interval", "interval"],
|
||||
["member-health-timeout", "timeout"], ["member-health-slow", "slow"],
|
||||
["member-health-fall", "fall"], ["member-health-rise", "rise"],
|
||||
["member-health-min", "status-min"], ["member-health-max", "status-max"]
|
||||
]) {
|
||||
const source = read(shared), target = read(local, panel);
|
||||
if (source && target) target.value = source.value;
|
||||
}
|
||||
}
|
||||
function backendHealthEditor(parent, members) {
|
||||
const section = el("section", undefined, "vh-wide vh-backend-health-editor");
|
||||
section.append(text("h3", "backendHealthPerBackend"));
|
||||
note(section, "backendHealthSelectionHelp");
|
||||
const selection = field(section, "health-backend-select", "backendHealthSelect", "", "select");
|
||||
const empty = note(section, "backendHealthNoEnabled");
|
||||
const panels = el("div", undefined, "vh-health-member-panels"); section.append(panels); parent.append(section);
|
||||
let active = null, eligible = [];
|
||||
const refresh = () => {
|
||||
eligible = [...members.children].filter(row => row.memberHealthEnabled.checked);
|
||||
if (!eligible.includes(active)) active = eligible[0] || null;
|
||||
selection.replaceChildren(...eligible.map((row, index) => {
|
||||
const id = str("member-id", row) || t("backend");
|
||||
const address = str("member-url", row);
|
||||
const option = el("option", address ? `${id} — ${address}` : id);
|
||||
option.value = String(index); return option;
|
||||
}));
|
||||
selection.value = active ? String(eligible.indexOf(active)) : "";
|
||||
selection.disabled = eligible.length === 0;
|
||||
empty.hidden = eligible.length !== 0;
|
||||
panels.replaceChildren(...[...members.children].map(row => row.memberHealthPanel));
|
||||
for (const row of members.children) row.memberHealthPanel.hidden = row !== active;
|
||||
};
|
||||
selection.addEventListener("change", () => { active = eligible[Number(selection.value)] || null; refresh(); });
|
||||
refresh();
|
||||
return { refresh, reveal(row) { if (eligible.includes(row)) { active = row; refresh(); } } };
|
||||
}
|
||||
@@ -340,7 +340,13 @@
|
||||
form.addEventListener("input", changed); form.addEventListener("change", changed);
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault(); if (!envelope || busy || uploading) return;
|
||||
const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) { activateTab(invalid.closest(".vh-group")?.dataset.tab || "details"); const details = invalid.closest("details"); if (details) details.open = true; invalid.reportValidity(); return; }
|
||||
const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) {
|
||||
const healthPanel = invalid.closest(".vh-backend-health-panel");
|
||||
if (healthPanel) backendHealthEditorControl?.reveal([...form.querySelector(".vh-members").children].find(row => row.memberHealthPanel === healthPanel));
|
||||
activateTab(invalid.closest(".vh-group")?.dataset.tab || "details");
|
||||
const details = invalid.closest("details"); if (details) details.open = true;
|
||||
invalid.reportValidity(); return;
|
||||
}
|
||||
if (revision !== envelope.revision) { toast(t("stale"), true); return; }
|
||||
try {
|
||||
const epoch = draftEpoch; uploading = true;
|
||||
@@ -370,5 +376,5 @@
|
||||
initPoolTableTools();
|
||||
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
|
||||
window.rustProxyDrafts.register("vhost", { labelKey: "vhosts", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); 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; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); 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; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
Reference in new issue
Block a user