Fix header layout regression, remove exploit-block toggle and perf logging, cache disk-usage check
This commit is contained in:
+2
-2
@@ -94,7 +94,7 @@ function advancedFormBody(form, body, scoped) {
|
||||
const read = (name, fallback = "") => scoped ? scopedValue(scoped.formEl, scoped.scope, name, fallback) : (form.get(name) || fallback);
|
||||
const checked = (name) => scoped ? Boolean(scoped.formEl.querySelector(`${scoped.scope} [name="${name}"]`)?.checked) : form.has(name);
|
||||
body.domains = String(form.get("domainsText") || "").split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
|
||||
body.hsts = form.has("hsts"); body.hstsSubdomains = checked("hstsSubdomains"); body.healthEnabled = checked("healthEnabled"); body.upstreamTlsInsecure = checked("upstreamTlsInsecure"); body.blockCommonExploits = checked("blockCommonExploits");
|
||||
body.hsts = form.has("hsts"); body.hstsSubdomains = checked("hstsSubdomains"); body.healthEnabled = checked("healthEnabled"); body.upstreamTlsInsecure = checked("upstreamTlsInsecure");
|
||||
body.accessListId = read("accessListId", body.accessListId || "");
|
||||
body.requestHeaders = parseHeaderLines(read("requestHeadersText")); body.responseHeaders = parseHeaderLines(read("responseHeadersText")); body.compression = read("compression", "automatic"); body.customConfig = read("customConfig");
|
||||
body.locations = String(form.get("customLocationsText") || "").split("\n").map(line => { const [path, target, behavior] = line.split("|").map(value => value.trim()); return path && target ? { path, target, stripPrefix:behavior.toLowerCase() === "strip" } : null; }).filter(Boolean);
|
||||
@@ -785,7 +785,7 @@ function openSettings(kind, id) {
|
||||
form.elements.name.value = item.name || ""; form.elements.domain.value = item.domain || ""; form.elements.target.value = item.target || ""; form.elements.tls.value = item.tls || "automatic"; form.elements.hsts.checked = Boolean(item.hsts); if (form.elements.settingsAccessListId) form.elements.settingsAccessListId.value = item.accessListId || "";
|
||||
if (kind === "proxy") {
|
||||
const scope = "#settings-advanced";
|
||||
setScoped(form, scope, "accessListId", item.accessListId || ""); setScoped(form, scope, "healthPath", item.healthPath || "/"); setScoped(form, scope, "healthMethod", item.healthMethod || "GET"); setScoped(form, scope, "healthExpected", item.healthExpected || "200-499"); setScoped(form, scope, "healthTimeoutSeconds", item.healthTimeoutSeconds || 4); setScoped(form, scope, "healthEnabled", item.healthEnabled !== false); setScoped(form, scope, "compression", item.compression || "automatic"); setScoped(form, scope, "blockCommonExploits", Boolean(item.blockCommonExploits));
|
||||
setScoped(form, scope, "accessListId", item.accessListId || ""); setScoped(form, scope, "healthPath", item.healthPath || "/"); setScoped(form, scope, "healthMethod", item.healthMethod || "GET"); setScoped(form, scope, "healthExpected", item.healthExpected || "200-499"); setScoped(form, scope, "healthTimeoutSeconds", item.healthTimeoutSeconds || 4); setScoped(form, scope, "healthEnabled", item.healthEnabled !== false); setScoped(form, scope, "compression", item.compression || "automatic");
|
||||
form.elements.customLocationsText.value = (item.locations || []).map(location => `${location.path} | ${location.target} | ${location.stripPrefix ? "strip" : "preserve"}`).join("\n");
|
||||
setScoped(form, scope, "requestHeadersText", (item.requestHeaders || []).map(header => `${header.name}: ${header.value}`).join("\n")); setScoped(form, scope, "responseHeadersText", (item.responseHeaders || []).map(header => `${header.name}: ${header.value}`).join("\n"));
|
||||
form.elements.upstreamTlsServerName.value = item.upstreamTlsServerName || ""; setScoped(form, scope, "upstreamTlsInsecure", Boolean(item.upstreamTlsInsecure)); setScoped(form, scope, "hstsSubdomains", Boolean(item.hstsSubdomains)); setScoped(form, scope, "customConfig", item.customConfig || ""); form.elements.upstreamsText.value = (item.upstreams || []).join("\n"); setScoped(form, scope, "lbPolicy", item.lbPolicy || "random");
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -65,7 +65,7 @@ h2{letter-spacing:-.025em}
|
||||
.status-pill{display:flex;align-items:center;gap:var(--space-2);text-transform:capitalize;font-size:var(--font-size-sm);color:var(--muted)}
|
||||
.icon-button,.launch{width:34px;height:34px;border-radius:var(--radius-sm);border:1px solid var(--line);display:grid;place-items:center;background:var(--icon-button-bg);color:var(--muted);cursor:pointer;text-decoration:none}
|
||||
.page-refresh{width:44px;height:44px;flex:0 0 auto}
|
||||
header .page-refresh{margin-left:8px}
|
||||
.header-actions{display:flex;align-items:center;gap:var(--space-5)}
|
||||
.menu-wrap{position:relative}
|
||||
.menu{display:none;position:absolute;right:0;top:var(--space-7);width:145px;background:var(--surface-raised);border:1px solid var(--line);border-radius:var(--radius-2xs);padding:6px;box-shadow:var(--shadow);z-index:3}
|
||||
.menu-open .menu{display:block}
|
||||
|
||||
+16
-49
@@ -193,22 +193,31 @@ async function sampleNetworkInterfaces() {
|
||||
}
|
||||
setInterval(sampleNetworkInterfaces, 5000).unref();
|
||||
sampleNetworkInterfaces();
|
||||
// A recursive walk of /data (directorySize()) is only needed when DATA_DIR_LIMIT_GB is set, and
|
||||
// only to compute one denominator-relative percentage -- disk usage doesn't change fast enough to
|
||||
// justify redoing that walk on every single hero-panel poll (every 7 seconds, times every
|
||||
// concurrent viewer). Cached in the background instead, same pattern as refreshDatabaseIntegrityCache()
|
||||
// above: compute once shortly after boot, then on a steady interval, and have the hot request path
|
||||
// just read the cached number.
|
||||
let dataDirSizeCache = { checkedAt: null, bytes: null };
|
||||
async function refreshDataDirSizeCache() {
|
||||
try { dataDirSizeCache = { checkedAt: new Date().toISOString(), bytes: await directorySize(dataDir) }; }
|
||||
catch (error) { console.warn("Could not compute data directory size:", error.message); }
|
||||
}
|
||||
// One combined snapshot for the System tab's hero panel -- CPU/memory/swap/network are all
|
||||
// container-scoped (cgroup v2 + this container's network namespace); disk reuses the same
|
||||
// statfs-on-the-data-volume approach as /api/system/storage.
|
||||
async function systemHealthSnapshot() {
|
||||
const assignedLimitGb = numberEnv("DATA_DIR_LIMIT_GB", null);
|
||||
const assignedLimitBytes = assignedLimitGb && assignedLimitGb > 0 ? assignedLimitGb * 1024 ** 3 : null;
|
||||
const [cpu, memory, swap, disk, appUsedBytes] = await Promise.all([
|
||||
const [cpu, memory, swap, disk] = await Promise.all([
|
||||
cgroupCpuPercent(),
|
||||
cgroupMemory(),
|
||||
cgroupSwap(),
|
||||
fsp.statfs(dataDir).catch(() => null),
|
||||
// Only walk /data (the same directorySize() the storage breakdown below already uses) when
|
||||
// an assigned limit is actually configured -- it's the one case that needs it, and the walk
|
||||
// isn't free, so skip it when the panel is just going to show whole-volume stats anyway.
|
||||
assignedLimitBytes !== null ? directorySize(dataDir) : Promise.resolve(null),
|
||||
]);
|
||||
// See refreshDataDirSizeCache() above -- this used to be a live directorySize() walk on every fetch.
|
||||
const appUsedBytes = assignedLimitBytes !== null ? dataDirSizeCache.bytes : null;
|
||||
return {
|
||||
cpu,
|
||||
memory,
|
||||
@@ -544,7 +553,6 @@ function applyAdvancedSettings(item, body) {
|
||||
if (body.accessListId !== undefined) item.accessListId = String(body.accessListId || "");
|
||||
if (body.compression !== undefined) item.compression = ["off", "gzip", "automatic"].includes(body.compression) ? body.compression : "automatic";
|
||||
if (body.hstsSubdomains !== undefined) item.hstsSubdomains = Boolean(body.hstsSubdomains);
|
||||
if (body.blockCommonExploits !== undefined) item.blockCommonExploits = Boolean(body.blockCommonExploits);
|
||||
if (body.requestHeaders !== undefined) item.requestHeaders = cleanHeaders(body.requestHeaders);
|
||||
if (body.responseHeaders !== undefined) item.responseHeaders = cleanHeaders(body.responseHeaders);
|
||||
if (body.upstreamTlsServerName !== undefined) item.upstreamTlsServerName = String(body.upstreamTlsServerName || "").trim().slice(0, 253);
|
||||
@@ -603,19 +611,8 @@ function accessDirectives(accessListId) {
|
||||
return output;
|
||||
}
|
||||
|
||||
// Static, general-purpose ruleset for the "Block common exploits" toggle — not a full WAF. Rejects
|
||||
// requests whose path matches common exploit-probe patterns before they reach the upstream: directory
|
||||
// traversal, WordPress/PHP admin and scanner paths, dotfile exposure attempts, and SQL-injection-style
|
||||
// query strings. One named matcher + one respond directive per host, so it's cheap to add or remove.
|
||||
const COMMON_EXPLOIT_PATTERN = String.raw`(?i)(\.\./|\.\.\\|/etc/passwd|/wp-login\.php|/wp-admin(?:/|$)|/xmlrpc\.php|/\.env(?:$|\?)|/\.git/|/\.aws/|/vendor/phpunit|/phpunit(?:/|$)|eval\(|base64_decode\(|union(?:\s|%20|\+)+select|<script)`;
|
||||
|
||||
function exploitBlockDirectives(id) {
|
||||
return [` @blocked-exploit-${id} {`, ` path_regexp ${caddyQuote(COMMON_EXPLOIT_PATTERN)}`, " }", ` respond @blocked-exploit-${id} 403`];
|
||||
}
|
||||
|
||||
function commonHostDirectives(item) {
|
||||
const output = [...accessDirectives(item.accessListId)];
|
||||
if (item.blockCommonExploits) output.push(...exploitBlockDirectives(item.id));
|
||||
if (item.compression !== "off") output.push(item.compression === "gzip" ? " encode gzip" : " encode zstd gzip");
|
||||
for (const header of item.responseHeaders || []) output.push(` header ${header.name} ${caddyQuote(header.value)}`);
|
||||
if (item.hsts && item.tls !== "http") output.push(` header Strict-Transport-Security ${caddyQuote(`max-age=31536000${item.hstsSubdomains ? "; includeSubDomains" : ""}`)}`);
|
||||
@@ -1037,30 +1034,12 @@ async function readAccessLogs(limit = 100, host = "") {
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Diagnostic timing (v0.16.44): this job reads Caddy's access-log files, JSON.parses up to 5000
|
||||
// lines, hashes each one, and batch-inserts them -- all synchronous work that runs on Node's single
|
||||
// thread and therefore blocks every other request in the app for its full duration, every time it
|
||||
// runs (every 30 seconds). A user reported requests as simple as GET /api/sites randomly taking
|
||||
// 6-14 seconds with a Network-tab Timing capture showing nearly all of it as server-side "Waiting"
|
||||
// (TTFB) rather than connection/DNS time -- consistent with getting stuck behind a job like this
|
||||
// one. Logging real durations here (only when a run takes long enough to plausibly explain that)
|
||||
// gives proof of whether this is the actual cause before changing how it works, rather than
|
||||
// shipping a fourth guess.
|
||||
async function importAccessLogsToSqlite() {
|
||||
if (!storage?.recordAccessEvents) return;
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const readStarted = performance.now();
|
||||
const entries = await readAccessLogs(5000);
|
||||
const readMs = Math.round(performance.now() - readStarted);
|
||||
const hashStarted = performance.now();
|
||||
const events = entries.map(entry => ({ ...entry, source: crypto.createHash("sha1").update(JSON.stringify([entry.at, entry.host, entry.method, entry.uri, entry.status, entry.size, entry.durationMs, entry.remoteIp])).digest("hex") }));
|
||||
const hashMs = Math.round(performance.now() - hashStarted);
|
||||
const insertStarted = performance.now();
|
||||
storage.recordAccessEvents(events);
|
||||
const insertMs = Math.round(performance.now() - insertStarted);
|
||||
const totalMs = Math.round(performance.now() - startedAt);
|
||||
if (totalMs > 500) console.warn(`[perf] importAccessLogsToSqlite took ${totalMs}ms for ${entries.length} entries (read ${readMs}ms, hash ${hashMs}ms, insert ${insertMs}ms) -- this blocks every other request while it runs.`);
|
||||
} catch (error) { console.warn("Could not import access logs into SQLite:", error.message); }
|
||||
}
|
||||
|
||||
@@ -1531,20 +1510,6 @@ app.disable("x-powered-by");
|
||||
// HTTP layer: Express app setup, auth middleware, and every /api/* route.
|
||||
// Routes below are grouped by area; see the section comments for each group.
|
||||
// ============================================================================================
|
||||
// Diagnostic timing (v0.16.44): logs any request that takes noticeably long to answer, alongside
|
||||
// the importAccessLogsToSqlite instrumentation above -- together these should show, in the
|
||||
// container's own logs, whether a slow page load lines up with a background job's run window or
|
||||
// is a slow request in its own right. Placed first so it wraps the full request, including any
|
||||
// auth/body-parsing work below it. Remove once the real cause behind reported multi-second page
|
||||
// loads is confirmed and fixed; this is a temporary aid, not a permanent feature.
|
||||
app.use((req, res, next) => {
|
||||
const startedAt = performance.now();
|
||||
res.on("finish", () => {
|
||||
const durationMs = Math.round(performance.now() - startedAt);
|
||||
if (durationMs > 1000) console.warn(`[perf] ${req.method} ${req.originalUrl} took ${durationMs}ms`);
|
||||
});
|
||||
next();
|
||||
});
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.get(["/", "/index.html"], (req, res) => {
|
||||
@@ -2634,6 +2599,8 @@ setInterval(() => checkConfigDrift().catch(error => console.warn("Config drift c
|
||||
// after boot and then every 30 minutes in the background, rather than on every dashboard fetch.
|
||||
setTimeout(refreshDatabaseIntegrityCache, 5000).unref();
|
||||
setInterval(refreshDatabaseIntegrityCache, 30 * 60000).unref();
|
||||
setTimeout(refreshDataDirSizeCache, 5000).unref();
|
||||
setInterval(refreshDataDirSizeCache, 60000).unref();
|
||||
|
||||
|
||||
// --- Scheduled jobs: automatic backups, log pruning, public IP checks, graceful shutdown ---------------------------------
|
||||
|
||||
Reference in New Issue
Block a user