From 3a21070d62d20080c9cd2533e4e8231f8c4717a8 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Thu, 14 May 2026 10:55:21 +0200 Subject: [PATCH] chore: static analysis baseline Pre-analysis checkpoint before implementing fixes for: - bakeStyles field mapping - error logging improvements - HTML sanitization hardening --- bun.lock | 3 ++ package.json | 3 +- public/index.html | 95 ++++++++++++++++++++++++++++++++++++++++++++--- src/server.ts | 84 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 175 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index 100d541..09246c3 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "add": "^2.0.6", "oxfmt": "^0.49.0", "oxlint": "^1.64.0", + "typescript": "^6.0.3", }, }, }, @@ -282,6 +283,8 @@ "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="], diff --git a/package.json b/package.json index c7ca70a..f34741d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/bun": "^1.3.13", "add": "^2.0.6", "oxfmt": "^0.49.0", - "oxlint": "^1.64.0" + "oxlint": "^1.64.0", + "typescript": "^6.0.3" } } diff --git a/public/index.html b/public/index.html index 99cbda4..704b318 100644 --- a/public/index.html +++ b/public/index.html @@ -179,6 +179,10 @@ background: rgba(46, 204, 113, 0.1); color: var(--green); } + .badge-wait { + background: rgba(255, 152, 0, 0.15); + color: #ff9800; + } .card-url-preview { font-size: 13px; color: var(--muted); @@ -578,6 +582,7 @@ + @@ -782,13 +787,15 @@ .map((v) => { const viewUrl = origin + "/v/" + v.id; const urlPreview = v.urls.map((u, i) => - `${u.url} (${u.durationSec}s)${u.forceDarkMode ? " 🌙" : ""}${u.bakeStyles ? " 🍞" : ""}` + `${u.url} (${u.durationSec}s${u.waitBeforeCaptureMs ? ` +${u.waitBeforeCaptureMs}ms` : ""})${u.forceDarkMode ? " 🌙" : ""}${u.bakeStyles ? " 🍞" : ""}` ).join(" → "); const anyBake = v.urls.some((u) => u.bakeStyles); + const anyWait = v.urls.some((u) => u.waitBeforeCaptureMs > 0); const badges = [ `${v.method.toUpperCase()}`, v.metaRefreshEnabled ? 'META-REFRESH' : "", anyBake ? 'BAKE' : "", + anyWait ? 'WAIT' : "", ] .filter(Boolean) .join(" "); @@ -798,6 +805,7 @@
${esc(v.name)}
+
@@ -873,6 +881,75 @@ } } + // ═══════════════════════════════════════════════ + // EXPORT / IMPORT + // ═══════════════════════════════════════════════ + + async function exportView(id) { + try { + const response = await fetch("/api/views/" + id + "?export=true"); + if (!response.ok) throw new Error("Export failed"); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `view-${id}.json`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + toast("View exported!"); + } catch (err) { + toast(err.message, "error"); + } + } + + function openImportModal() { + openModal(` +

Import View

+
+
+ + +
Paste the JSON content from an exported view file.
+
+ +
`); + } + + async function handleImport(e) { + e.preventDefault(); + const jsonText = document.getElementById("import-json").value.trim(); + if (!jsonText) { + toast("Please paste JSON content", "error"); + return; + } + + try { + // Validate JSON structure + const data = JSON.parse(jsonText); + if (!data.name || !data.urls || !Array.isArray(data.urls)) { + toast("Invalid view JSON: missing required fields", "error"); + return; + } + + await api("POST", "/api/views/import", data); + closeModal(); + toast("View imported successfully!"); + loadViews(); + } catch (err) { + if (err.message.includes("JSON")) { + toast("Invalid JSON format", "error"); + } else { + toast(err.message, "error"); + } + } + } + // ═══════════════════════════════════════════════ // EDITOR BUILDER // ═══════════════════════════════════════════════ @@ -881,9 +958,9 @@ const isEdit = !!existing; const title = isEdit ? "Edit View" : "Create New View"; const idAttr = isEdit ? ` data-id="${existing.id}"` : ""; - const urls = existing ? existing.urls : [{ url: "", durationSec: 30 }]; + const urls = existing ? existing.urls : [{ url: "", durationSec: 30, waitBeforeCaptureMs: 0 }]; let urlRowsHTML = urls - .map((u, i) => buildUrlRowHTML(u.url, u.durationSec, i, urls.length, u.forceDarkMode, u.bakeStyles)) + .map((u, i) => buildUrlRowHTML(u.url, u.durationSec, u.waitBeforeCaptureMs, i, urls.length, u.forceDarkMode, u.bakeStyles)) .join(""); const method = existing ? existing.method : "ssr"; const metaChecked = existing ? existing.metaRefreshEnabled : false; @@ -950,16 +1027,20 @@ `; } - function buildUrlRowHTML(url, durationSec, index, total, forceDarkMode, bakeStyles) { + function buildUrlRowHTML(url, durationSec, waitBeforeCaptureMs, index, total, forceDarkMode, bakeStyles) { const isSingle = total <= 1; const disabledAttr = isSingle ? "disabled" : ""; const grayStyle = isSingle ? 'style="opacity:0.4;"' : ""; const darkChecked = forceDarkMode ? "checked" : ""; const bakeChecked = bakeStyles ? "checked" : ""; + const waitVal = waitBeforeCaptureMs ?? 0; return `
- +
+ + +