This commit is contained in:
Mateusz Gruszczyński committed 2026-11-08 07:11:46 +01:00
1 parent 17e314e159
commit 34490fd5fc
22 files changed
+798 -613

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# RustProxy source and documentation maintenance # RustProxy source and documentation maintenance
Current source version: **0.36.3**. The Cargo package and binary are named `rust-proxy`. Current source version: **0.36.4**. The Cargo package and binary are named `rust-proxy`.
Before editing, read [README.md](README.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior. Before editing, read [README.md](README.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
+9
View File
@@ -2,6 +2,15 @@
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in the latest release. Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in the latest release.
## 0.36.4 — cache readability and editor UX polish
- **Cache → Domains and effectiveness:** decision counters are one compact line (`name count`, top three, the rest in a `+N` tooltip) instead of a wall of chips; the rest of the table keeps its 0.36.3 look.
- **Cache → Static response transformation:** the "Transformations by type" and "Vhost effectiveness" tables use the same presentation as Domains and effectiveness (right-aligned tabular numbers, emphasised vhost name, non-zero errors in red, reduction meter) and the skip reasons are compact `name count` lines.
- **Cache → Stored objects:** the tier is a badge in the same style as the other badges (follows the corner-radius setting), numeric columns are right-aligned, long targets and cache keys are truncated (full text on hover, click still copies), rows highlight on hover, and the Previous / range / Next pager is a single segmented control.
- **Docker vhosts:** the per-service link is now **Open** (same wording as in Vhosts); the expandable per-application forms and their protocol, ACL, timeout and health sections have a chevron, keyboard focus outline and a framed panel when open.
- **Automatic IDs:** when creating a vhost, web service, stream, redirect, 404 host or ACL profile, the ID is generated from the name (for vhosts, from the first host name) as a unique slug; the ID field comes after the name (after the hosts for vhosts) and is shown as a plain value with a **Change** link — the input only appears when you ask for it (or when validation needs it) — `example.com` becomes `example-com`, Polish and other accented letters are folded to ASCII, and `-2`, `-3`… is appended when the ID is taken. Typing in the ID field switches to a manual ID; a "Generate automatically" button (or clearing the field) restores it. Existing IDs are never changed.
- **Vhost, Web service, Stream, Redirect and ACL forms:** section tabs wrap onto a second line instead of scrolling horizontally, and use a readable 12 px underlined tab style with a clear active state and hover; form action bars are aligned consistently.
## 0.36.3 — settings, cache and dashboard layout polish ## 0.36.3 — settings, cache and dashboard layout polish
- **More chart styles:** Settings → Appearance → Chart style now offers eight additional presets: **Sunset**, **Aurora**, **Ocean**, **Forest**, **Pastel**, **Terminal** (green phosphor, square lines), **Blueprint** (thin technical lines on a blue tint) and **Colorblind safe** (Okabe–Ito palette with dashed grid). The `chart_style` preference accepts the new values (`sunset`, `aurora`, `ocean`, `forest`, `pastel`, `terminal`, `blueprint`, `colorblind`); existing values and the default are unchanged. - **More chart styles:** Settings → Appearance → Chart style now offers eight additional presets: **Sunset**, **Aurora**, **Ocean**, **Forest**, **Pastel**, **Terminal** (green phosphor, square lines), **Blueprint** (thin technical lines on a blue tint) and **Colorblind safe** (Okabe–Ito palette with dashed grid). The `chart_style` preference accepts the new values (`sunset`, `aurora`, `ocean`, `forest`, `pastel`, `terminal`, `blueprint`, `colorblind`); existing values and the default are unchanged.
Generated
+1 -1
View File
@@ -1943,7 +1943,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.36.3" version = "0.36.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rust-proxy" name = "rust-proxy"
version = "0.36.3" version = "0.36.4"
edition = "2021" edition = "2021"
rust-version = "1.99.1" rust-version = "1.99.1"
description = "RustProxy - independent reverse proxy with local administration" description = "RustProxy - independent reverse proxy with local administration"
+572 -572
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \
bash integrations/routeros-oci-build/build-routeros-oci.sh push bash integrations/routeros-oci-build/build-routeros-oci.sh push
``` ```
For example, with RustProxy version `0.36.3` and the defaults this publishes: For example, with RustProxy version `0.36.4` and the defaults this publishes:
```text ```text
zot.example.net/network/rustproxy:0.36.3 zot.example.net/network/rustproxy:0.36.4
zot.example.net/network/rustproxy:latest zot.example.net/network/rustproxy:latest
zot.example.net/network/rustproxy:routeros zot.example.net/network/rustproxy:routeros
``` ```
+6 -1
View File
@@ -1726,7 +1726,12 @@
"dockerAliasesShort": "aliases", "dockerAliasesShort": "aliases",
"dockerAgentDetails": "Agent details", "dockerAgentDetails": "Agent details",
"dockerServices": "Services", "dockerServices": "Services",
"dockerOpenApp": "Open app", "dockerOpenApp": "Open",
"autoIdHint": "Generated automatically from the name. Edit it to set your own ID.",
"autoIdReset": "Generate automatically",
"autoIdChange": "Change",
"autoIdPending": "Generated from the name",
"autoIdBadge": "auto",
"dockerLiveAccessLogsHelp": "Live HTTP access logs for this Docker vhost, not the container stdout/stderr.", "dockerLiveAccessLogsHelp": "Live HTTP access logs for this Docker vhost, not the container stdout/stderr.",
"networkInterfacesHistory": "Traffic by interface", "networkInterfacesHistory": "Traffic by interface",
"networkInterfaceRate": "RX / TX", "networkInterfaceRate": "RX / TX",
+6 -1
View File
@@ -1726,7 +1726,12 @@
"dockerAliasesShort": "aliasów", "dockerAliasesShort": "aliasów",
"dockerAgentDetails": "Szczegóły agenta", "dockerAgentDetails": "Szczegóły agenta",
"dockerServices": "Usługi", "dockerServices": "Usługi",
"dockerOpenApp": "Otwórz aplikację", "dockerOpenApp": "Otwórz",
"autoIdHint": "Generowane automatycznie z nazwy. Zmień, aby ustawić własne ID.",
"autoIdReset": "Generuj automatycznie",
"autoIdChange": "Zmień",
"autoIdPending": "Generowane z nazwy",
"autoIdBadge": "auto",
"dockerLiveAccessLogsHelp": "Logi żądań HTTP dla tego Docker vhosta, nie stdout/stderr kontenera.", "dockerLiveAccessLogsHelp": "Logi żądań HTTP dla tego Docker vhosta, nie stdout/stderr kontenera.",
"networkInterfacesHistory": "Ruch według interfejsów", "networkInterfacesHistory": "Ruch według interfejsów",
"networkInterfaceRate": "RX / TX", "networkInterfaceRate": "RX / TX",
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "RustProxy Administration API", "title": "RustProxy Administration API",
"version": "0.36.3", "version": "0.36.4",
"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." "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": [ "servers": [
+59
View File
@@ -668,3 +668,62 @@ $("tunnels-dialog").addEventListener("close", stopTunnelRefresh);
$("config-editor").addEventListener("input", () => setSettingsDirty("config-editor")); $("config-editor").addEventListener("input", () => setSettingsDirty("config-editor"));
$("save-json").addEventListener("click", async event => { const editor = $("config-editor"), page = editor.closest("[data-settings-page]"); if (!envelope || busy) return; try { await withFormSubmitState(page, event.currentTarget, async () => { let config; try { config = JSON.parse(editor.value); } catch { throw fieldError(editor, t("vhJsonInvalid")); } if (await save(config)) setSettingsDirty("config-editor", false); }); } catch (e) { toast(e.message, true); } }); $("save-json").addEventListener("click", async event => { const editor = $("config-editor"), page = editor.closest("[data-settings-page]"); if (!envelope || busy) return; try { await withFormSubmitState(page, event.currentTarget, async () => { let config; try { config = JSON.parse(editor.value); } catch { throw fieldError(editor, t("vhJsonInvalid")); } if (await save(config)) setSettingsDirty("config-editor", false); }); } catch (e) { toast(e.message, true); } });
$("export-config").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify(envelope.config, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = `RustProxy-config-r${envelope.revision}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }); $("export-config").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify(envelope.config, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = `RustProxy-config-r${envelope.revision}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
// Auto-generated identifiers: a new entry's ID follows its name (or first host) as a unique slug until the user edits the ID by hand.
const rustProxyAutoId = (() => {
const FOLD = { "ł": "l", "đ": "d", "ø": "o", "ß": "ss", "æ": "ae", "œ": "oe", "þ": "th", "ð": "d" };
const states = new WeakMap();
function slugify(value, max = 64) {
let slug = String(value ?? "").trim().toLowerCase().replace(/^\*\./, "").replace(/[łđøßæœþð]/g, ch => FOLD[ch]).normalize("NFKD").replace(/[̀-ͯ]/g, "");
slug = slug.replace(/[^a-z0-9_-]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-_]+/, "").slice(0, max).replace(/[-_]+$/, "");
return slug;
}
function unique(base, taken) {
if (!base) return "";
const used = new Set([...taken].map(value => String(value).toLowerCase()));
if (!used.has(base)) return base;
for (let n = 2; n < 10000; n++) { const suffix = `-${n}`, candidate = base.slice(0, 64 - suffix.length).replace(/[-_]+$/, "") + suffix; if (!used.has(candidate)) return candidate; }
return base;
}
const locked = input => input.readOnly || input.disabled;
function suggestion(state) { return unique(slugify(state.text()), state.taken()); }
// The ID is shown as a plain value with a "Change" link; the input only appears after the user asks for it (or when validation needs it).
function paint(input, state) {
const isLocked = locked(input), wanted = suggestion(state), value = input.value.trim(), compact = !state.open || isLocked;
state.holder.classList.toggle("is-compact", compact);
state.value.textContent = value || t("autoIdPending");
state.value.classList.toggle("is-pending", !value);
state.badge.hidden = !state.auto || !value; state.badge.textContent = t("autoIdBadge");
state.change.hidden = isLocked; state.change.textContent = t("autoIdChange");
state.row.hidden = !compact;
state.hint.hidden = compact || !state.auto; state.hint.textContent = t("autoIdHint");
state.reset.hidden = compact || state.auto || state.resettable === false || !wanted || wanted === value; state.reset.textContent = t("autoIdReset");
}
function regenerate(input, state) { if (!state.auto || locked(input)) return; input.value = suggestion(state); input.setCustomValidity?.(""); paint(input, state); }
// options: { elements: inputs whose changes drive the ID, text: () => source string, taken: () => existing IDs }
function attach(input, options) {
let state = states.get(input);
if (!state) {
state = { auto: true, open: false, holder: input.closest("label") || input.parentElement, hint: document.createElement("small"), reset: document.createElement("button"), row: document.createElement("div"), value: document.createElement("code"), badge: document.createElement("span"), change: document.createElement("button") };
state.holder.classList.add("auto-id-field");
state.hint.className = "fine auto-id-hint"; state.reset.type = "button"; state.reset.className = "quiet auto-id-reset";
state.row.className = "auto-id-row"; state.value.className = "auto-id-value"; state.badge.className = "auto-id-badge"; state.change.type = "button"; state.change.className = "quiet auto-id-change";
state.row.append(state.value, state.badge, state.change); state.holder.append(state.row, state.hint, state.reset);
input.addEventListener("input", event => { if (!event.isTrusted) return; state.auto = input.value.trim() === ""; if (state.auto) regenerate(input, state); else paint(input, state); });
input.addEventListener("invalid", () => { if (!state.open) { state.open = true; paint(input, state); } });
input.addEventListener("blur", () => { if (input.value.trim() && input.validity.valid) { state.open = false; paint(input, state); } });
state.change.addEventListener("click", () => { state.open = true; paint(input, state); input.focus(); input.select?.(); });
state.reset.addEventListener("mousedown", event => event.preventDefault());
state.reset.addEventListener("click", () => { state.auto = true; regenerate(input, state); input.dispatchEvent(new Event("input", { bubbles: true })); });
states.set(input, state);
for (const source of options.elements || []) source.addEventListener("input", () => { const current = states.get(input); if (current) regenerate(input, current); });
}
state.text = options.text || (() => (options.elements || []).map(node => node.value).find(value => value.trim()) || "");
state.taken = options.taken || (() => []); state.resettable = options.resettable;
state.auto = !locked(input) && input.value.trim() === ""; state.open = false;
paint(input, state);
return { refresh() { const current = states.get(input); if (current) regenerate(input, current); } };
}
return { slugify, unique, attach };
})();
window.rustProxyAutoId = rustProxyAutoId;
+17 -2
View File
@@ -67,6 +67,20 @@
return groups.filter(row => cacheEnabledFor(data, row) === wanted); return groups.filter(row => cacheEnabledFor(data, row) === wanted);
}; };
const reason = value => { const key = `cacheReason_${value}`, text = t(key); return text === key ? value : text; }; const reason = value => { const key = `cacheReason_${value}`, text = t(key); return text === key ? value : text; };
// Decision counters: label + count + share bar; top three visible, the rest folded into a details toggle.
function reasonsCell(reasons) {
const td = el("td", undefined, "cache-reasons"), entries = Object.entries(reasons || {}).sort((a, b) => b[1] - a[1]);
if (!entries.length) { td.append(el("span", "-", "cache-reason-none")); return td; }
const max = entries[0][1] || 1, line = ([key, count]) => {
const item = el("div", undefined, "cache-reason-line"), bar = el("i"); bar.style.width = `${Math.max(4, Math.round(count / max * 100))}%`;
const track = el("span", undefined, "cache-reason-track"); track.append(bar);
item.append(el("span", reason(key), "cache-reason-name"), el("b", number(count), "cache-reason-count"), track); item.title = key; return item;
};
entries.slice(0, 3).forEach(entry => td.append(line(entry)));
if (entries.length > 3) { const more = el("details", undefined, "cache-reason-more"); more.append(el("summary", `+${entries.length - 3}`)); entries.slice(3).forEach(entry => more.append(line(entry))); td.append(more); }
return td;
}
function resetLiveChart(selected = scope()) { function resetLiveChart(selected = scope()) {
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null; liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null;
for (const values of Object.values(liveChartSamples)) values.length = 0; for (const values of Object.values(liveChartSamples)) values.length = 0;
@@ -168,7 +182,7 @@
[[row.hits, "cache-outcome-hit"], [row.misses, "cache-outcome-miss"], [row.bypasses, "cache-outcome-bypass"]].forEach(([count, cls], index) => { if (index) outcomes.append(el("span", "/", "cache-outcome-sep")); outcomes.append(el("span", number(count), cls)); }); [[row.hits, "cache-outcome-hit"], [row.misses, "cache-outcome-miss"], [row.bypasses, "cache-outcome-bypass"]].forEach(([count, cls], index) => { if (index) outcomes.append(el("span", "/", "cache-outcome-sep")); outcomes.append(el("span", number(count), cls)); });
const ratioPercent = Math.max(0, Math.min(100, (row.hit_ratio || 0) * 100)), ratio = el("td", undefined, "cache-ratio-cell"), meter = el("span", undefined, "cache-ratio-meter"), fill = el("i"); fill.style.width = `${ratioPercent}%`; meter.append(fill); ratio.append(el("span", `${fmt(ratioPercent, 1)}%`, "cache-ratio-value"), meter); const ratioPercent = Math.max(0, Math.min(100, (row.hit_ratio || 0) * 100)), ratio = el("td", undefined, "cache-ratio-cell"), meter = el("span", undefined, "cache-ratio-meter"), fill = el("i"); fill.style.width = `${ratioPercent}%`; meter.append(fill); ratio.append(el("span", `${fmt(ratioPercent, 1)}%`, "cache-ratio-value"), meter);
tr.append(outcomes, ratio); tr.append(outcomes, ratio);
const td = el("td", undefined, "cache-reasons"); for (const [key, count] of Object.entries(row.reasons || {}).sort((a, b) => b[1] - a[1])) { const tag = el("span", `${reason(key)}: ${number(count)}`, "cache-reason"); tag.title = key; td.append(tag); } tr.append(td); return tr; tr.append(reasonsCell(row.reasons)); return tr;
})); }));
const domainEmpty = $("cache-domains-empty"); const domainEmpty = $("cache-domains-empty");
domainEmpty.textContent = groups.length ? t("cacheNoDomainsFiltered") : t("cacheNoDomains"); domainEmpty.textContent = groups.length ? t("cacheNoDomainsFiltered") : t("cacheNoDomains");
@@ -181,7 +195,8 @@
tr.append(target); tr.append(target);
const keyTags = `${row.cache_key || "-"}${row.tags?.length ? ` · ${row.tags.join(", ")}` : ""}`; const keyTags = `${row.cache_key || "-"}${row.tags?.length ? ` · ${row.tags.join(", ")}` : ""}`;
tr.append(makeCopyCell(keyTags)); tr.append(makeCopyCell(keyTags));
for (const value of [row.tier === "cold" ? t("cacheCold") : t("cacheHot"), fmtBytes(body), fmtBytes(storage), fmtBytes(Math.max(0, storage - body)), number(row.hits), `${number(row.age_seconds)} s`, `${number(row.resident_age_seconds)} s`, `${number(row.remaining_ttl_seconds)} s`]) tr.append(el("td", value)); tr.append(el("td", undefined)); tr.lastChild.append(el("span", row.tier === "cold" ? t("cacheCold") : t("cacheHot"), `cache-tier-pill ${row.tier === "cold" ? "is-cold" : "is-hot"}`));
for (const value of [fmtBytes(body), fmtBytes(storage), fmtBytes(Math.max(0, storage - body)), number(row.hits), `${number(row.age_seconds)} s`, `${number(row.resident_age_seconds)} s`, `${number(row.remaining_ttl_seconds)} s`]) tr.append(el("td", value));
const action = el("td"), button = el("button", t("cachePurgeUrl"), "quiet"); button.type = "button"; button.addEventListener("click", () => void purgeObject(row)); action.append(button); tr.append(action); return tr; const action = el("td"), button = el("button", t("cachePurgeUrl"), "quiet"); button.type = "button"; button.addEventListener("click", () => void purgeObject(row)); action.append(button); tr.append(action); return tr;
})); }));
const bans = data.bans || []; const bans = data.bans || [];
+38
View File
@@ -1147,3 +1147,41 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
background: var(--panel2); background: var(--panel2);
color: var(--muted); color: var(--muted);
} }
/* Cache → decision counters, stored objects and pager (0.36.4) */
#panel-cache .cache-table .cache-reasons { min-width: 200px; max-width: 320px; }
.cache-reason-line { display: inline-flex; align-items: baseline; gap: 4px; margin: 0 10px 0 0; font-size: 11px; line-height: 1.5; white-space: nowrap; }
.cache-reason-name { color: var(--muted); }
.cache-reason-count { font-variant-numeric: tabular-nums; font-weight: 600; }
.cache-reason-more { color: var(--muted); cursor: help; margin-right: 0; }
.cache-reason-none { color: var(--muted); }
#cache-object-scroll .cache-table td:nth-child(n+6):nth-child(-n+12),
#cache-object-scroll .cache-table th:nth-child(n+6):nth-child(-n+12) { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
#cache-object-scroll .cache-table td:nth-child(3),
#cache-object-scroll .cache-table td:nth-child(4) { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; }
#cache-object-scroll .cache-table td:nth-child(3):hover,
#cache-object-scroll .cache-table td:nth-child(4):hover { white-space: normal; overflow-wrap: anywhere; }
#cache-object-scroll .cache-table tbody tr:hover { background: var(--panel2); }
.cache-tier-pill { display: inline-flex; align-items: center; padding: 2px 6px; border: 1px solid var(--pill-line); border-radius: var(--rp-radius-control, 0); color: var(--muted); font: 9px var(--mono); letter-spacing: 1px; text-transform: uppercase; }
.cache-tier-pill.is-hot { color: var(--green); border-color: var(--pill-live-line); }
.cache-pager { display: inline-flex; align-items: center; gap: 0; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 6px); overflow: hidden; }
.cache-pager button { border: 0; border-radius: 0; min-width: 88px; margin: 0; }
.cache-pager button:disabled { opacity: .45; }
.cache-pager #cache-page { padding: 0 14px; min-width: 120px; text-align: center; font-variant-numeric: tabular-nums; border-inline: 1px solid var(--line); align-self: stretch; display: flex; align-items: center; justify-content: center; }
.cache-pager #cache-prev::before { content: "‹ "; }
.cache-pager #cache-next::after { content: " ›"; }
/* Static transformation tables (0.36.4): same presentation as Cache → Domains and effectiveness */
.transform-vhost-table :is(th, td):nth-child(n+2), .transform-type-table :is(th, td):nth-child(2) { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.transform-vhost-table td, .transform-type-table td { vertical-align: middle; }
.transform-vhost-table td:first-child { color: var(--text); font-weight: 600; }
.transform-type-table td:first-child { color: var(--muted); }
.transform-vhost-table tbody tr:hover, .transform-type-table tbody tr:hover { background: var(--panel2); }
.transform-zero { color: var(--muted); }
.transform-errors-nonzero { color: var(--red, #e5534b); font-weight: 600; }
.transform-reason-list { gap: 4px 12px; }
.transform-reason-list .cache-reason-line { margin: 0; }
.cache-ratio-meter, .cache-ratio-meter>i { border-radius: var(--rp-progress-radius, 0); }
+51
View File
@@ -1731,3 +1731,54 @@
font-size: 11px; font-size: 11px;
} }
} }
/* Editors lift (0.36.4): Docker per-app forms and shared vhost/web/stream/redirect/ACL form chrome */
.docker-service-editor:not(.docker-service-metrics)>summary,
.docker-health-editor>summary, .docker-acl-editor>summary, .docker-protocol-editor>summary, .docker-timeout-editor>summary, .docker-passive-health-editor>summary {
list-style: none;
}
.docker-service-editor>summary::-webkit-details-marker, .docker-health-editor>summary::-webkit-details-marker, .docker-acl-editor>summary::-webkit-details-marker,
.docker-protocol-editor>summary::-webkit-details-marker, .docker-timeout-editor>summary::-webkit-details-marker, .docker-passive-health-editor>summary::-webkit-details-marker { display: none; }
.docker-service-editor>summary::before, .docker-health-editor>summary::before, .docker-acl-editor>summary::before,
.docker-protocol-editor>summary::before, .docker-timeout-editor>summary::before, .docker-passive-health-editor>summary::before {
content: "›"; display: inline-block; width: 14px; flex: 0 0 14px; color: var(--muted); font-size: 14px; line-height: 1; transition: transform .15s ease;
}
.docker-service-editor[open]>summary::before, .docker-health-editor[open]>summary::before, .docker-acl-editor[open]>summary::before,
.docker-protocol-editor[open]>summary::before, .docker-timeout-editor[open]>summary::before, .docker-passive-health-editor[open]>summary::before { transform: rotate(90deg); color: var(--text); }
.docker-service-editor>summary>span { flex: 0 0 auto; }
.docker-service-editor>summary .docker-config-hint { flex: 1; min-width: 0; }
.docker-service-editor:not(.docker-service-metrics)>summary:hover>span { color: var(--green); }
.docker-service-editor[open]:not(.docker-service-metrics) { border: 1px solid var(--line); border-radius: var(--rp-radius-surface, 6px); background: var(--panel2); padding: 10px 12px 12px; }
.docker-service-editor[open]:not(.docker-service-metrics)>.docker-health-editor,
.docker-service-editor[open]:not(.docker-service-metrics)>.docker-acl-editor,
.docker-service-editor[open]:not(.docker-service-metrics)>.docker-protocol-editor,
.docker-service-editor[open]:not(.docker-service-metrics)>.docker-timeout-editor { margin-top: 10px; }
.docker-service-editor>summary:focus-visible, .docker-acl-editor>summary:focus-visible, .docker-protocol-editor>summary:focus-visible, .docker-timeout-editor>summary:focus-visible, .docker-health-editor>summary:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; border-radius: 4px; }
.vh-tabs { gap: 2px; padding: 4px 0 0; border-bottom: 1px solid var(--line); margin-bottom: 12px; }
.vh-tabs button { font-size: 12px; padding: 8px 12px; border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: transparent; color: var(--muted); margin-bottom: -1px; }
.vh-tabs button:hover { color: var(--text); background: var(--panel2); }
.vh-tabs button.active { color: var(--text); background: transparent; border-bottom-color: var(--green); font-weight: 600; }
.vh-tabs button:focus-visible { outline: 2px solid var(--green); outline-offset: -2px; }
.vh-form>.card-heading { gap: 10px; align-items: center; }
.vh-form .form-actions { display: flex; gap: 8px; align-items: center; }
.vh-form .form-actions .primary { order: 0; }
.vh-form .form-actions .quiet { order: 1; }
.vh-setting-section>h3 { letter-spacing: .01em; }
.vh-tabs { flex-wrap: wrap; overflow: visible; row-gap: 0; }
@media (max-width:700px) { .vh-tabs button { font-size: 11px; padding: 7px 8px; } }
.auto-id-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; }
.auto-id-reset { justify-self: start; margin-top: 3px; padding: 2px 8px; font-size: 11px; }
.auto-id-hint[hidden], .auto-id-reset[hidden] { display: none; }
.auto-id-field>span:first-child { color: var(--muted); }
.auto-id-field.is-compact>input { display: none; }
.auto-id-row { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 2px; }
.auto-id-value { font-family: var(--mono, monospace); font-size: 13px; color: var(--text); overflow-wrap: anywhere; }
.auto-id-value.is-pending { color: var(--muted); font-style: italic; font-family: inherit; font-size: 12px; }
.auto-id-badge { display: inline-flex; align-items: center; padding: 2px 6px; border: 1px solid var(--pill-line); border-radius: var(--rp-radius-control, 0); color: var(--accent); font: 9px var(--mono); letter-spacing: 1px; text-transform: uppercase; }
.auto-id-change { padding: 2px 8px; font-size: 11px; margin-left: auto; }
.auto-id-row[hidden], .auto-id-change[hidden], .auto-id-badge[hidden] { display: none; }
.auto-id-field input { font-family: var(--mono, monospace); font-size: 12px; }
.auto-id-hint { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; line-height: 1.35; }
.auto-id-reset { justify-self: start; margin-top: 3px; padding: 2px 8px; font-size: 11px; }
.auto-id-hint[hidden], .auto-id-reset[hidden] { display: none; }
+1
View File
@@ -44,6 +44,7 @@
editing = item.id || null; revision = envelope?.revision ?? null; dirty = false; editing = item.id || null; revision = envelope?.revision ?? null; dirty = false;
$("notfound-editing").textContent = editing || t("notfoundNewLabel"); $("notfound-editing").textContent = editing || t("notfoundNewLabel");
$("notfound-id").value = item.id || ""; $("notfound-id").disabled = Boolean(editing); $("notfound-name").value = item.name || ""; $("notfound-id").value = item.id || ""; $("notfound-id").disabled = Boolean(editing); $("notfound-name").value = item.name || "";
window.rustProxyAutoId.attach($("notfound-id"), { elements: [$("notfound-name")], taken: () => (envelope?.config.not_found_hosts || []).map(value => value.id) });
$("notfound-enabled").checked = item.enabled !== false; syncEnabledControl(); $("notfound-http2").checked = item.http2_enabled !== false; $("notfound-http3").checked = item.http3_enabled !== false; $("notfound-enabled").checked = item.enabled !== false; syncEnabledControl(); $("notfound-http2").checked = item.http2_enabled !== false; $("notfound-http3").checked = item.http3_enabled !== false;
const hostHolder = $("notfound-hosts-editor"); hostHolder.replaceChildren(); readHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "notfoundHosts", helpKey: "vhHostsHelp", requiredKey: "notfoundHostsRequired", onChange: changed }); const hostHolder = $("notfound-hosts-editor"); hostHolder.replaceChildren(); readHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "notfoundHosts", helpKey: "vhHostsHelp", requiredKey: "notfoundHostsRequired", onChange: changed });
listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id); listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id);
+12 -12
View File
@@ -772,10 +772,10 @@
</div> </div>
</div> </div>
<div class="field-grid"> <div class="field-grid">
<label><span data-i18n="aclProfileId">Profile ID</span><input id="acl-profile-id"
maxlength="64" pattern="[A-Za-z0-9_\-]{1,64}" required /></label>
<label><span data-i18n="aclProfileName">Display name</span><input id="acl-profile-name" <label><span data-i18n="aclProfileName">Display name</span><input id="acl-profile-name"
maxlength="96" required /></label> maxlength="96" required /></label>
<label class="auto-id-field"><span data-i18n="aclProfileId">Profile ID</span><input id="acl-profile-id"
maxlength="64" pattern="[A-Za-z0-9_\-]{1,64}" required /></label>
</div> </div>
<section class="acl-policy-builder" aria-labelledby="acl-policy-mode-label"> <section class="acl-policy-builder" aria-labelledby="acl-policy-mode-label">
<div class="acl-policy-step-head"> <div class="acl-policy-step-head">
@@ -1398,8 +1398,8 @@
<article class="card"> <article class="card">
<div class="card-heading"> <div class="card-heading">
<h2 data-i18n="cacheObjectList">Stored objects</h2> <h2 data-i18n="cacheObjectList">Stored objects</h2>
<div class="entity-actions"><button class="quiet" data-i18n="cachePrevious" id="cache-prev" <div class="entity-actions cache-pager"><button class="quiet" data-i18n="cachePrevious" id="cache-prev"
type="button">Previous</button><span class="fine" id="cache-page"></span><button type="button">Previous</button><span class="fine" id="cache-page" aria-live="polite"></span><button
class="quiet" data-i18n="cacheNext" id="cache-next" type="button">Next</button> class="quiet" data-i18n="cacheNext" id="cache-next" type="button">Next</button>
</div> </div>
</div> </div>
@@ -1519,7 +1519,7 @@
<h2 data-i18n="transformByType">Transformations by type</h2> <h2 data-i18n="transformByType">Transformations by type</h2>
</div> </div>
<div class="table-scroll"> <div class="table-scroll">
<table class="cache-table"> <table class="cache-table transform-type-table">
<thead> <thead>
<tr> <tr>
<th data-i18n="transformType">Type</th> <th data-i18n="transformType">Type</th>
@@ -1546,7 +1546,7 @@
<p class="fine" data-i18n="transformScopeHint">Counters are in-memory runtime diagnostics. A <p class="fine" data-i18n="transformScopeHint">Counters are in-memory runtime diagnostics. A
transformed cache HIT does not execute the transformer again.</p> transformed cache HIT does not execute the transformer again.</p>
<div class="table-scroll"> <div class="table-scroll">
<table class="cache-table"> <table class="cache-table transform-vhost-table">
<thead> <thead>
<tr> <tr>
<th data-i18n="vhVhost">Vhost</th> <th data-i18n="vhVhost">Vhost</th>
@@ -3799,10 +3799,10 @@
service ID/name and PHP-FPM endpoint are not replaced.</p> service ID/name and PHP-FPM endpoint are not replaced.</p>
</div> </div>
</fieldset> </fieldset>
<label><span data-i18n="webServiceId">Service ID</span><input id="web-service-id" maxlength="64"
pattern="[A-Za-z0-9_\-]{1,64}" required="" /></label>
<label><span data-i18n="webServiceName">Name</span><input id="web-service-name" maxlength="96" <label><span data-i18n="webServiceName">Name</span><input id="web-service-name" maxlength="96"
required="" /></label> required="" /></label>
<label class="auto-id-field"><span data-i18n="webServiceId">Service ID</span><input id="web-service-id" maxlength="64"
pattern="[A-Za-z0-9_\-]{1,64}" required="" /></label>
<label class="vh-wide"><span data-i18n="webServiceRoot">Document root</span><input <label class="vh-wide"><span data-i18n="webServiceRoot">Document root</span><input
id="web-service-root" maxlength="4096" placeholder="/var/www/main" required="" /></label> id="web-service-root" maxlength="4096" placeholder="/var/www/main" required="" /></label>
<div class="vh-wide" hidden id="web-service-permissions"></div> <div class="vh-wide" hidden id="web-service-permissions"></div>
@@ -4289,10 +4289,10 @@ log_errors = On</code></pre>
<fieldset class="vh-group editor-identity-section"> <fieldset class="vh-group editor-identity-section">
<legend data-i18n="hostIdentitySection">Host and domains</legend> <legend data-i18n="hostIdentitySection">Host and domains</legend>
<div class="vh-grid"> <div class="vh-grid">
<label><span data-i18n="redirectId">Redirect ID</span><input id="redirect-id" maxlength="64"
pattern="[A-Za-z0-9_\-]{1,64}" required="" /></label>
<label><span data-i18n="redirectName">Display name</span><input id="redirect-name" <label><span data-i18n="redirectName">Display name</span><input id="redirect-name"
maxlength="96" /></label> maxlength="96" /></label>
<label class="auto-id-field"><span data-i18n="redirectId">Redirect ID</span><input id="redirect-id" maxlength="64"
pattern="[A-Za-z0-9_\-]{1,64}" required="" /></label>
<label class="check"><input id="redirect-enabled" type="checkbox" /><span <label class="check"><input id="redirect-enabled" type="checkbox" /><span
data-i18n="enabled">Enabled</span></label> data-i18n="enabled">Enabled</span></label>
<div class="vh-wide" id="redirect-hosts-editor"></div> <div class="vh-wide" id="redirect-hosts-editor"></div>
@@ -4347,10 +4347,10 @@ log_errors = On</code></pre>
<fieldset class="vh-group editor-identity-section"> <fieldset class="vh-group editor-identity-section">
<legend data-i18n="hostIdentitySection">Host and domains</legend> <legend data-i18n="hostIdentitySection">Host and domains</legend>
<div class="vh-grid"> <div class="vh-grid">
<label><span data-i18n="notfoundId">Host ID</span><input id="notfound-id" maxlength="64"
pattern="[A-Za-z0-9_\-]{1,64}" required="" /></label>
<label><span data-i18n="notfoundName">Display name</span><input id="notfound-name" <label><span data-i18n="notfoundName">Display name</span><input id="notfound-name"
maxlength="96" /></label> maxlength="96" /></label>
<label class="auto-id-field"><span data-i18n="notfoundId">Host ID</span><input id="notfound-id" maxlength="64"
pattern="[A-Za-z0-9_\-]{1,64}" required="" /></label>
<label class="check"><input id="notfound-enabled" type="checkbox" /><span <label class="check"><input id="notfound-enabled" type="checkbox" /><span
data-i18n="enabled">Enabled</span></label> data-i18n="enabled">Enabled</span></label>
<div class="vh-wide" id="notfound-hosts-editor"></div> <div class="vh-wide" id="notfound-hosts-editor"></div>
+3 -2
View File
@@ -366,11 +366,12 @@
rules.placeholder = custom ? "allow 10.0.0.0/8\ndeny 10.21.0.0/16" : "10.0.0.0/8\n203.0.113.5"; rules.placeholder = custom ? "allow 10.0.0.0/8\ndeny 10.21.0.0/16" : "10.0.0.0/8\n203.0.113.5";
syncRuleRows(convertRules); renderPreview(); syncRuleRows(convertRules); renderPreview();
} }
function attachAclProfileId() { window.rustProxyAutoId.attach($("acl-profile-id"), { elements: [$("acl-profile-name")], taken: () => (envelope?.config.acl_profiles || []).map(value => value.id) }); }
function clear() { function clear() {
editing = null; dirty = false; form.reset(); setMode("allow_exceptions"); aclDefault.value = "deny"; countryMode.value = "off"; countryEnabled.checked = false; countriesInput.value = ""; $("acl-profile-id").readOnly = false; $("acl-profile-editing").textContent = t("aclProfileNew"); renderRuleRows([]); rules.value = ""; syncPolicy(); editing = null; dirty = false; form.reset(); setMode("allow_exceptions"); aclDefault.value = "deny"; countryMode.value = "off"; countryEnabled.checked = false; countriesInput.value = ""; $("acl-profile-id").readOnly = false; attachAclProfileId(); $("acl-profile-editing").textContent = t("aclProfileNew"); renderRuleRows([]); rules.value = ""; syncPolicy();
} }
function edit(profile) { function edit(profile) {
editing = profile.id; dirty = false; $("acl-profile-id").value = profile.id; $("acl-profile-id").readOnly = true; $("acl-profile-name").value = profile.name || profile.id; editing = profile.id; dirty = false; $("acl-profile-id").value = profile.id; $("acl-profile-id").readOnly = true; $("acl-profile-name").value = profile.name || profile.id; attachAclProfileId();
const cMode = profile.country_access?.mode || "off"; aclDefault.value = profile.acl?.default || (cMode === "allow" ? "deny" : "allow"); countryMode.value = cMode; const cMode = profile.country_access?.mode || "off"; aclDefault.value = profile.acl?.default || (cMode === "allow" ? "deny" : "allow"); countryMode.value = cMode;
countryEnabled.checked = cMode !== "off"; countriesInput.value = (profile.country_access?.countries || []).join("\n"); countryEnabled.checked = cMode !== "off"; countriesInput.value = (profile.country_access?.countries || []).join("\n");
setMode(inferMode(profile)); renderRuleRows(profile.acl?.rules || []); $("acl-profile-editing").textContent = t("aclProfileEdit"); dirty = false; syncPolicy(); setMode(inferMode(profile)); renderRuleRows(profile.acl?.rules || []); $("acl-profile-editing").textContent = t("aclProfileEdit"); dirty = false; syncPolicy();
+1
View File
@@ -70,6 +70,7 @@
$("redirect-editing").textContent = editing ? editing : t("redirectNewLabel"); $("redirect-editing").textContent = editing ? editing : t("redirectNewLabel");
$("redirect-id").value = item.id || ""; $("redirect-id").disabled = Boolean(editing); $("redirect-id").value = item.id || ""; $("redirect-id").disabled = Boolean(editing);
$("redirect-name").value = item.name || ""; $("redirect-name").value = item.name || "";
window.rustProxyAutoId.attach($("redirect-id"), { elements: [$("redirect-name")], taken: () => (envelope?.config.redirects || []).map(value => value.id) });
const hostHolder = $("redirect-hosts-editor"); hostHolder.replaceChildren(); readRedirectHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "redirectHosts", helpKey: "vhHostsHelp", requiredKey: "redirectHostsRequired", onChange: changed }); const hostHolder = $("redirect-hosts-editor"); hostHolder.replaceChildren(); readRedirectHosts = window.rustProxyIntegrations.hostnames(hostHolder, item.frontend?.hosts || [], { labelKey: "redirectHosts", helpKey: "vhHostsHelp", requiredKey: "redirectHostsRequired", onChange: changed });
$("redirect-enabled").checked = item.enabled !== false; syncEnabledControl(); $("redirect-http2").checked = item.http2_enabled !== false; $("redirect-http3").checked = item.http3_enabled !== false; $("redirect-enabled").checked = item.enabled !== false; syncEnabledControl(); $("redirect-http2").checked = item.http2_enabled !== false; $("redirect-http3").checked = item.http3_enabled !== false;
listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id); listenerChoices(item.frontend?.listener_ids || []); certificateChoices(item.certificate_id);
+3 -2
View File
@@ -127,8 +127,9 @@
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true; container.replaceChildren(); initTabs(); editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true; container.replaceChildren(); initTabs();
$("stream-editing").textContent = t(editing ? "streamEditLabel" : "streamNewLabel"); $("stream-editing").textContent = t(editing ? "streamEditLabel" : "streamNewLabel");
let g = group("streamEditor", "details"); let g = group("streamEditor", "details");
field(g, "id", "streamId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}", readOnly: Boolean(editing) }); const streamName = field(g, "name", "streamName", value.name, "text", { maxLength: 96 });
field(g, "name", "streamName", value.name, "text", { maxLength: 96 }); const streamIdInput = field(g, "id", "streamId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}", readOnly: Boolean(editing) });
window.rustProxyAutoId.attach(streamIdInput, { elements: [streamName], taken: () => (envelope?.config.streams || []).map(item => item.id) });
const protocol = field(g, "protocol", "streamProtocol", value.protocol || "tcp", "select", { values: [["tcp", "streamTcp"], ["udp", "streamUdp"]] }); const protocol = field(g, "protocol", "streamProtocol", value.protocol || "tcp", "select", { values: [["tcp", "streamTcp"], ["udp", "streamUdp"]] });
const enabledControl = field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox"); const enabledControl = field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
window.rustProxyEnableControl(enabledControl); window.rustProxyEnableControl(enabledControl);
+7 -11
View File
@@ -103,7 +103,7 @@
function renderReasons(counters) { function renderReasons(counters) {
const entries = Object.entries(counters?.reasons || {}).sort((a, b) => b[1] - a[1]); const entries = Object.entries(counters?.reasons || {}).sort((a, b) => b[1] - a[1]);
const target = $("transform-reason-list"); target.replaceChildren(...entries.map(([key, count]) => { const target = $("transform-reason-list"); target.replaceChildren(...entries.map(([key, count]) => {
const tag = el("span", `${key}: ${number(count)}`, "cache-reason"); tag.title = key; return tag; const tag = el("span", undefined, "cache-reason-line"); tag.append(el("span", key, "cache-reason-name"), el("b", number(count), "cache-reason-count")); tag.title = key; return tag;
})); }));
$("transform-reasons-empty").hidden = entries.length > 0; $("transform-reasons-empty").hidden = entries.length > 0;
$("tp-skip-top").textContent = entries.length ? entries.slice(0, 2).map(([key, count]) => `${key}: ${number(count)}`).join(" · ") : t("transformNoSkipsShort"); $("tp-skip-top").textContent = entries.length ? entries.slice(0, 2).map(([key, count]) => `${key}: ${number(count)}`).join(" · ") : t("transformNoSkipsShort");
@@ -113,16 +113,12 @@
const rows = (data?.metrics?.scopes || []).filter(row => wanted === "*" || row.vhost === wanted); const rows = (data?.metrics?.scopes || []).filter(row => wanted === "*" || row.vhost === wanted);
$("transform-vhost-rows").replaceChildren(...rows.map(row => { $("transform-vhost-rows").replaceChildren(...rows.map(row => {
const tr = el("tr"); const tr = el("tr");
for (const value of [ tr.append(el("td", row.vhost, "transform-vhost-name"));
row.vhost, for (const value of [number(row.applied), number(row.cache_hits), number(row.skipped)]) tr.append(el("td", value));
number(row.applied), tr.append(el("td", number(row.errors), Number(row.errors) > 0 ? "transform-errors-nonzero" : "transform-zero"), el("td", fmtBytes(row.saved_bytes || 0)));
number(row.cache_hits), const original = Number(row.original_bytes) || 0, share = original > 0 ? Math.max(0, Math.min(100, (Number(row.saved_bytes) || 0) / original * 100)) : 0;
number(row.skipped), const reduction = el("td", undefined, "cache-ratio-cell"), meter = el("span", undefined, "cache-ratio-meter"), fill = el("i"); fill.style.width = `${share}%`; meter.append(fill); reduction.append(el("span", percent(row), "cache-ratio-value"), meter);
number(row.errors), tr.append(reduction, el("td", averageMs(row)));
fmtBytes(row.saved_bytes || 0),
percent(row),
averageMs(row)
]) tr.append(el("td", value));
return tr; return tr;
})); }));
$("transform-vhosts-empty").hidden = rows.length > 0; $("transform-vhosts-empty").hidden = rows.length > 0;
+5 -3
View File
@@ -69,8 +69,9 @@ function load(value = defaults(), migrationIds = [], show = true) {
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true; editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true;
container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel"); container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
let g = group("vhEditor"); const basicFields = subsection(g, "vhGeneralDetailsSection"); let g = group("vhEditor"); const basicFields = subsection(g, "vhGeneralDetailsSection");
field(basicFields, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(basicFields, "vhIdRenameHelp"); const vhostIdInput = field(basicFields, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(basicFields, "vhIdRenameHelp");
let refreshCanonical = () => { }; let refreshCanonical = () => { }, vhostAutoId = null;
vhostAutoId = window.rustProxyAutoId.attach(vhostIdInput, { text: () => lines(read("hosts")?.value || "")[0] || "", taken: () => (envelope?.config.vhosts || []).map(item => item.id), resettable: !editing });
const enabledControl = field(basicFields, "enabled", "vhEnabled", value.enabled !== false, "checkbox"); const enabledControl = field(basicFields, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
window.rustProxyEnableControl(enabledControl, "vhEnableHint"); window.rustProxyEnableControl(enabledControl, "vhEnableHint");
const enabledLabel = enabledControl.closest("label"); const enabledLabel = enabledControl.closest("label");
@@ -88,7 +89,8 @@ function load(value = defaults(), migrationIds = [], show = true) {
const websocketLegacy = field(websocketFields, "websocket-legacy-support", "vhWebsocketLegacySupport", value.websocket_legacy_support === true, "checkbox"); const websocketLegacy = field(websocketFields, "websocket-legacy-support", "vhWebsocketLegacySupport", value.websocket_legacy_support === true, "checkbox");
note(websocketFields, "vhWebsocketLegacySupportHelp"); note(websocketFields, "vhWebsocketLegacySupportHelp");
g = group("vhFrontends"); g = group("vhFrontends");
readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => refreshCanonical(hosts)); readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => { refreshCanonical(hosts); vhostAutoId?.refresh(); });
if (!editing) g.lastElementChild?.after(vhostIdInput.closest("label"));
const bindingControl = window.rustProxyIntegrations.bindings(g, value.frontend?.listener_ids || [], !editing); readBindings = bindingControl.read; const bindingControl = window.rustProxyIntegrations.bindings(g, value.frontend?.listener_ids || [], !editing); readBindings = bindingControl.read;
note(g, "listenerDefaultPortsHint"); note(g, "listenerDefaultPortsHint");
field(g, "force-ssl", "vhForceSsl", value.force_ssl === true, "checkbox"); field(g, "force-ssl", "vhForceSsl", value.force_ssl === true, "checkbox");
+1
View File
@@ -330,6 +330,7 @@
$("web-service-id").value = item.id || ""; $("web-service-id").value = item.id || "";
$("web-service-id").readOnly = !!editing; $("web-service-id").readOnly = !!editing;
$("web-service-name").value = item.name || ""; $("web-service-name").value = item.name || "";
window.rustProxyAutoId.attach($("web-service-id"), { elements: [$("web-service-name")], taken: () => (envelope?.config.web_services || []).map(value => value.id) });
$("web-service-root").value = item.document_root || ""; $("web-service-root").value = item.document_root || "";
populateSettings(item); populateSettings(item);
$("web-service-preset").value = "custom"; $("web-service-preset").value = "custom";
+1 -1
View File
@@ -51,5 +51,5 @@ const events = openapi.paths["/api/events"].get;
const logsFlag = (events.parameters || []).find(parameter => parameter.name === "logs"); const logsFlag = (events.parameters || []).find(parameter => parameter.name === "logs");
assert.equal(logsFlag?.schema?.type, "boolean"); assert.equal(logsFlag?.schema?.type, "boolean");
assert.equal(logsFlag?.schema?.default, true); assert.equal(logsFlag?.schema?.default, true);
assert.equal(openapi.info.version, "0.36.3"); assert.equal(openapi.info.version, "0.36.4");
console.log("Panel refresh/API contract checks: OK"); console.log("Panel refresh/API contract checks: OK");