diff --git a/AGENTS.md b/AGENTS.md index b293e5a..f850fb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # RustProxy source and documentation maintenance -Current source version: **0.36.6**. The Cargo package and binary are named `rust-proxy`. +Current source version: **0.36.7**. 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index fc059ef..a6d16f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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.7 — Docker vhosts and cache panel polish + +- **Docker vhosts:** the collapsible sections (Configure vhost, Protocols, Access control, timeouts, health checks) now align arrow, title and hint to the left with a common title column. Traffic statistics are expanded by default for every enabled app (a section you collapse stays collapsed). A new **Hide disabled** button hides all unpublished vhosts from the view at once; **Show hidden** brings them back. +- **404 hosts:** cards use the same host chips → response layout as redirects, with listeners and certificate on a quiet meta line; redirect and 404 cards are separated by the same gap as vhost cards. +- **Cache panel:** the "transformed" badge stays visible next to long object URLs (the URL is shortened instead). Decision counters show a thin share bar under each counter (relative to the largest) and a compact "+N" toggle; the pager no longer shows doubled arrows; the folded Decision counters ("+N") keep their open/closed state across automatic refreshes. +- **Dashboard cache outcomes:** the legend under the bar shows only the request counts; percentages are shown once, inside the bar and in the headline (tooltips give both). +- **Build:** `SHA256SUMS` uses repository-relative paths again. + ## 0.36.6 — redirect priority, copy and SNI documentation - **Deploy script:** `update.sh` / `install.sh` retry the GeoIP download (transient 502/503/504, timeouts) up to four times and, if it still fails, warn and continue with the installed GeoIP data instead of aborting the update with `HTTP Error 504`. diff --git a/Cargo.lock b/Cargo.lock index 6e660d7..05ceab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1943,7 +1943,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.36.6" +version = "0.36.7" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 12c8faa..e4250cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.36.6" +version = "0.36.7" edition = "2021" rust-version = "1.99.1" description = "RustProxy - independent reverse proxy with local administration" diff --git a/SHA256SUMS b/SHA256SUMS index c1faa52..85b014e 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,10 +1,10 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 .gitignore -9533bda057fd758323d04b2928ab6c13ee82ed8ed5c9aaa67a0bc293a75ff4d1 AGENTS.md -c7ac863203142f93119fd9a29297a06e768e43d64f2c9f13d6dd16fbce4e5aca CHANGELOG.md -240f1748925cf041e3bdbb85ff380c64f3a1641943aed23bd762352d625f81aa Cargo.lock -168a2e8054b405d0e8baa52e7a80f4f02598f4d4f9446e807ea0a3987522190e Cargo.toml +357a4c6e4ac6aca6baf93f43013fa8a1701e68289b7d0d47d1728d03734846eb AGENTS.md +c43200ec7df77449be7074265cce582e9af7c10af70dca843e9a13285fa0b317 CHANGELOG.md +881fafd324cb38c3c016cd853e65b9870819d2ac6ce932a6dd6b6755701444d4 Cargo.lock +90664b6534166fb8bfb8d2b61885e3397d5df78f584e5a334ff81c522833c555 Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE 82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md @@ -88,7 +88,7 @@ de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 integrations/o 45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example 11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 integrations/routeros-oci-build/Dockerfile.routeros 7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore -28880995ead34f2f22b43b57525109c5fe915206919509333182d6e514a531cb integrations/routeros-oci-build/README.md +5025cf1086254f3e664cd50be2737b437286cbf311c79d87578c3d1d6c4a4f64 integrations/routeros-oci-build/README.md 3f092a9ab80221eddd67be4d1a776acbc6e04c1eb6011b588280c28e83b2582f integrations/routeros-oci-build/build-routeros-oci.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md @@ -165,7 +165,7 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file 77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs 743bb7436f77e04015328a0e7e7d425e43ef8aa8d61467bde157e462c1dcdc2e src/admin/logging_api.rs 243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs -3a2f3fd6d373f4a5f6085312ec9a0de3a404ad7e219b90bb2e13069106f6f34a src/admin/openapi.json +96101a80516c9cd668feb90a2cdd766363b4335b37888a926bb322e0cf3a160a src/admin/openapi.json f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 src/admin/operations.rs 257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs @@ -203,20 +203,20 @@ bfd9fafb3112197e8f875dbf87a9a66c81aef0fe83687601a84a66f5e13f69bc src/admin/web/ e540119edc555cefbe5147d97e06527adf49c984ba7a4dd51dc67603d3a46041 src/admin/web/app/preferences.js 9e2b12d34ab1fb29716645720577ad49b16c03b46c482a03695e8f645d170e79 src/admin/web/app/rate-sampling.js 3b8e566d81a5b94ef21c73e7805ae128454f085554de9358040de266bcbdb0e0 src/admin/web/app/refresh.js -f2c347cdd592ca5702d8b398d6ea9e9f75fa9639d70c5cbf3836334ca5b2b0d2 src/admin/web/app/runtime.js +e5f8a0124d57df71638df86b2262d5c210fe7947736712dda7f97e6ca36048f6 src/admin/web/app/runtime.js 31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/app/stream-dashboard.js 7de070cc8cdde45d7e0932259299e341e25b32c9ffbd87e4fcce934d0f090de4 src/admin/web/app/web-overview.js -0625f3bfdd8f67d375fbb7204fbed3108fb5091cc12c4dcc2b60b26ad97223da src/admin/web/cache-panel.js +f87af546830f01411dcf93749b4f75ca0f0bb3745a3390eff73ee14fc768cef0 src/admin/web/cache-panel.js 9f323507649910561518b63cf6632e81b3ecc4b09501ae53b635705f34ea6587 src/admin/web/certificate-api.js 4810137aa4217a86770617d442cf5dc6bec0d9f19e20bb2155e932a9a111cedf src/admin/web/css/00-tokens.css 466a24e234e1ec15f17e7d1994e838ca1ee0f52f03a4ca8fb6d9903bf2afde55 src/admin/web/css/10-base.css 8a1996d37f138790c12a704deedde9de74f733e7dcbb6be816fd68c281e8e0a9 src/admin/web/css/20-shell.css -b01f645124c7dd5b2ddbeba2aa9fbb706bb281579ff5487f17b2174263808042 src/admin/web/css/30-overview.css +d4b20ada2d91ddbc1613c642194df19e601948b34c4483fd04930c3e6db02394 src/admin/web/css/30-overview.css ca93e41d1c6001d82f4891cacdd712e896a0355d6db9f76cf62dd2f2225a8674 src/admin/web/css/40-editors.css 87df062d7478176641364c07de396ca988a37b1a8bcb892b53cc578fe8471850 src/admin/web/css/50-settings.css -733d10b6713006f53247fa6ea1cef31be75f90f989ed5811f8646b3a813667a4 src/admin/web/css/60-monitoring.css +9775d86312c03b9c722ce713f2dc4a99985157d2f6b98277f9c0dabdcf53de2c src/admin/web/css/60-monitoring.css 483664859787a2c54b51e73891cb760cfa15ff5e2e4132c96989eaa8109cf3a4 src/admin/web/css/70-responsive.css 8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md 8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 src/admin/web/css/manifest.json @@ -251,7 +251,7 @@ ac9f667273918e18406df06cb2613c923f50a88897693a03a18f500e22dc6ec2 src/admin/web/ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js 44b44790341067304e15974afcbbc7cc99686e88c2e43f62979d285eca6f726e src/admin/web/management.js 1f3d10e389c9c20e6eb019509fa6e82b50c6b43bbf988c1210a4d42bb73985d0 src/admin/web/partials/overview-charts.html -284d6f9dbe1cf00fe7c33226a4c5f72132edef5300498ad963fabb7c258b8233 src/admin/web/partials/overview-metrics.html +c8d6ab08b1fde881b1ca40222b282be039e2c4fba1b79f59454221cf893a9bde src/admin/web/partials/overview-metrics.html 3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js 6f33915d33d3a8bbfaeec424651a63eda3d23b0f296ae028a59ea46463ba9fdf src/admin/web/redirects.js 76a01f590d49649dcb1f554c221290919b2a8438e6001737f3c096fa6daec74d src/admin/web/settings.js @@ -561,7 +561,7 @@ f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures 102b83171a6429b6375d8001d6c5294702385cd58ee55727cb657b1f3566e877 tests/panel_icons_browser.py 52e61c389a60d147184ee76925cac76e454f8f2e3cb91039bb06e17787ab900f tests/panel_icons_contract.js 01af3008be27fabb486c141c67099af104f2725cc56f4c656aea9741a3c150cd tests/panel_interface_contract.py -200802ca30f82d8da50240573251dd976de500034608ad4384ddd95631ec904d tests/panel_refresh_contract.js +f9ac7a0117056cce2bcf1a20404db9977a9b1194c3d7df0eaef48b7e7f82b9f0 tests/panel_refresh_contract.js e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 third_party/swagger-ui-5.33.1/LICENSE 0d20d1adef18aee3f40dd258172155521ce702ac445cb5f7b7d60ed32dad2fb2 third_party/swagger-ui-5.33.1/NOTICE diff --git a/integrations/routeros-oci-build/README.md b/integrations/routeros-oci-build/README.md index b3bd93d..9d06a1d 100644 --- a/integrations/routeros-oci-build/README.md +++ b/integrations/routeros-oci-build/README.md @@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \ bash integrations/routeros-oci-build/build-routeros-oci.sh push ``` -For example, with RustProxy version `0.36.6` and the defaults this publishes: +For example, with RustProxy version `0.36.7` and the defaults this publishes: ```text -zot.example.net/network/rustproxy:0.36.6 +zot.example.net/network/rustproxy:0.36.7 zot.example.net/network/rustproxy:latest zot.example.net/network/rustproxy:routeros ``` diff --git a/src/admin/openapi.json b/src/admin/openapi.json index ce41ad9..101bfc3 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.36.6", + "version": "0.36.7", "description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. 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": [ diff --git a/src/admin/web/app/runtime.js b/src/admin/web/app/runtime.js index df5e8e5..75a43a2 100644 --- a/src/admin/web/app/runtime.js +++ b/src/admin/web/app/runtime.js @@ -408,7 +408,7 @@ function cacheOutcomeMix(totals) { return { counts, percentages: counts.map(value => value / requests * 100), requests }; } // Percent labels live inside the bar segments; a label that does not fit is hidden -// (the exact count and percentage stay in the segment and legend tooltips). +// (the legend carries the request counts; tooltips give both). function fitCacheMixLabels() { const bar = $("cache-mix-bar"); if (!bar) return; @@ -430,6 +430,7 @@ function renderCacheMix(mix, totalRequests) { segment.style.width = `${pct}%`; segment.textContent = mix && pct > 0 ? `${fmt(pct, pct < 10 ? 1 : 0)}%` : ""; const detail = mix ? `${name.toUpperCase()}: ${fmt(mix.counts[i])} (${fmt(pct, 1)}%)` : ""; + $(`cache-mix-${name}-count`).textContent = mix ? fmt(mix.counts[i]) : "-"; segment.title = detail; $(`cache-mix-${name}-key`).title = detail; }); fitCacheMixLabels(); diff --git a/src/admin/web/cache-panel.js b/src/admin/web/cache-panel.js index d4c3176..72359bb 100644 --- a/src/admin/web/cache-panel.js +++ b/src/admin/web/cache-panel.js @@ -69,7 +69,9 @@ 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) { + // Folded counters keep their open/closed state across auto-refresh re-renders. + const openReasonRows = new Set(); + function reasonsCell(reasons, rowKey = "") { 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]) => { @@ -78,7 +80,7 @@ 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); } + if (entries.length > 3) { const more = el("details", undefined, "cache-reason-more"); more.open = openReasonRows.has(rowKey); more.addEventListener("toggle", () => { if (more.open) openReasonRows.add(rowKey); else openReasonRows.delete(rowKey); }); 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()) { @@ -182,7 +184,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)); }); 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(reasonsCell(row.reasons)); return tr; + tr.append(reasonsCell(row.reasons, `${row.vhost || ""}\u0000${row.domain || ""}`)); return tr; })); const domainEmpty = $("cache-domains-empty"); domainEmpty.textContent = groups.length ? t("cacheNoDomainsFiltered") : t("cacheNoDomains"); @@ -191,7 +193,12 @@ const tr = el("tr"), resident = Number(row.resident_bytes) || 0, disk = Number(row.storage_bytes) || 0, body = Number(row.body_bytes) || 0, storage = resident || disk; tr.append(el("td", row.vhost || t("cacheLegacy")), el("td", row.domain)); const targetValue = row.target || row.path || "-", target = makeCopyCell(targetValue); - if (row.transformed) target.append(el("span", t("transformBadge"), "cache-transform-badge")); + if (row.transformed) { + // Keep the badge outside the ellipsised text so a long URL never clips it. + const wrap = el("div", undefined, "cache-target-wrap"); + wrap.append(el("span", targetValue, "cache-target-text"), el("span", t("transformBadge"), "cache-transform-badge")); + target.replaceChildren(wrap); + } tr.append(target); const keyTags = `${row.cache_key || "-"}${row.tags?.length ? ` · ${row.tags.join(", ")}` : ""}`; tr.append(makeCopyCell(keyTags)); diff --git a/src/admin/web/css/30-overview.css b/src/admin/web/css/30-overview.css index 391df67..57ac568 100644 --- a/src/admin/web/css/30-overview.css +++ b/src/admin/web/css/30-overview.css @@ -2246,6 +2246,12 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over background: var(--chart-secondary); } +#panel-overview .cache-mix-legend b { + margin-left: 1px; + color: var(--text); + font: 500 9px var(--mono); +} + #panel-overview .system-card .system-uptime-meta, #panel-overview .connection-card .connection-backends-meta { white-space: nowrap; diff --git a/src/admin/web/css/60-monitoring.css b/src/admin/web/css/60-monitoring.css index 2d46e0e..8418989 100644 --- a/src/admin/web/css/60-monitoring.css +++ b/src/admin/web/css/60-monitoring.css @@ -1184,3 +1184,21 @@ html[data-table-density=ultra] .file-log-inline-detail pre { .transform-reason-list .cache-reason-line { margin: 0; } .cache-ratio-meter, .cache-ratio-meter>i { border-radius: var(--rp-progress-radius, 0); } + +/* Decision counters (0.36.7): name + count on one line, a thin share bar below (relative to the largest counter) */ +.cache-reasons .cache-reason-line { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; align-items: baseline; margin: 0 0 6px; white-space: nowrap; } +.cache-reasons .cache-reason-name { overflow: hidden; text-overflow: ellipsis; } +.cache-reasons .cache-reason-track { grid-column: 1 / -1; display: block; height: 2px; margin-top: 3px; background: var(--line); border-radius: var(--rp-progress-radius, 0); overflow: hidden; } +.cache-reasons .cache-reason-track>i { display: block; height: 100%; background: var(--muted); opacity: .7; border-radius: var(--rp-progress-radius, 0); } +.cache-reasons .cache-reason-more { cursor: default; } +.cache-reasons .cache-reason-more>summary { cursor: pointer; display: inline-flex; align-items: center; gap: 5px; margin: 0 0 6px; padding: 1px 6px; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 0); font: 10px/1.5 var(--mono); color: var(--muted); list-style: none; user-select: none; } +.cache-reasons .cache-reason-more>summary::-webkit-details-marker { display: none; } +.cache-reasons .cache-reason-more>summary::before { content: "›"; display: inline-block; font-size: 12px; line-height: 1; transition: transform .15s ease; } +.cache-reasons .cache-reason-more[open]>summary::before { transform: rotate(90deg); } +.cache-reasons .cache-reason-more>summary:hover { color: var(--text); border-color: var(--muted); } + +/* Transformed badge (0.36.7): the URL ellipsises, the badge always stays visible */ +.cache-target-wrap { display: flex; align-items: center; gap: 6px; min-width: 0; } +.cache-target-text { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.cache-target-wrap .cache-transform-badge { flex: 0 0 auto; margin-left: 0; } + diff --git a/src/admin/web/partials/overview-metrics.html b/src/admin/web/partials/overview-metrics.html index f450ce0..19c3a4c 100644 --- a/src/admin/web/partials/overview-metrics.html +++ b/src/admin/web/partials/overview-metrics.html @@ -46,10 +46,10 @@ - HIT - MISS - BYPASS - OTHER + HIT - + MISS - + BYPASS - + OTHER -
Process diff --git a/tests/panel_refresh_contract.js b/tests/panel_refresh_contract.js index 29ac4cb..e1c55fc 100644 --- a/tests/panel_refresh_contract.js +++ b/tests/panel_refresh_contract.js @@ -51,5 +51,5 @@ const events = openapi.paths["/api/events"].get; const logsFlag = (events.parameters || []).find(parameter => parameter.name === "logs"); assert.equal(logsFlag?.schema?.type, "boolean"); assert.equal(logsFlag?.schema?.default, true); -assert.equal(openapi.info.version, "0.36.6"); +assert.equal(openapi.info.version, "0.36.7"); console.log("Panel refresh/API contract checks: OK");