chore: static analysis baseline

Pre-analysis checkpoint before implementing fixes for:
- bakeStyles field mapping
- error logging improvements
- HTML sanitization hardening
This commit is contained in:
2026-06-24 07:46:05 +02:00
parent e862827d2d
commit 3a21070d62
4 changed files with 175 additions and 10 deletions
+89 -6
View File
@@ -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 @@
<button class="btn btn-ghost btn-sm" id="debug-toggle" onclick="toggleDebug()">
🔍 Debug
</button>
<button class="btn btn-ghost btn-sm" onclick="openImportModal()">📤 Import</button>
<button class="btn btn-primary" onclick="openCreateModal()">+ New View</button>
</div>
</header>
@@ -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 = [
`<span class="badge badge-${v.method}">${v.method.toUpperCase()}</span>`,
v.metaRefreshEnabled ? '<span class="badge badge-refresh">META-REFRESH</span>' : "",
anyBake ? '<span class="badge badge-bake">BAKE</span>' : "",
anyWait ? '<span class="badge badge-wait">WAIT</span>' : "",
]
.filter(Boolean)
.join(" ");
@@ -798,6 +805,7 @@
<div class="card-header">
<span class="card-title">${esc(v.name)}</span>
<div style="display:flex;gap:6px;">
<button class="btn btn-icon btn-ghost btn-sm" onclick="exportView('${v.id}')" title="Export">📥</button>
<button class="btn btn-icon btn-ghost btn-sm" onclick="openEditModal('${v.id}')" title="Edit">✏️</button>
<button class="btn btn-icon btn-danger btn-sm" onclick="confirmDelete('${v.id}','${esc(v.name)}')" title="Delete">🗑</button>
</div>
@@ -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(`
<h2>Import View</h2>
<form id="import-form" onsubmit="handleImport(event)">
<div class="field">
<label>Paste View JSON</label>
<textarea id="import-json" rows="12" placeholder='{"name":"My View","urls":[{"url":"https://example.com","durationSec":30}],"method":"ssr"}' required style="font-family:monospace;font-size:13px;"></textarea>
<div class="hint">Paste the JSON content from an exported view file.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Import View</button>
</div>
</form>`);
}
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 @@
</form>`;
}
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 `
<div class="url-row" data-index="${index}">
<input type="url" placeholder="https://example.com/slideshow" value="${esc(url || "")}" class="url-input" required>
<input type="number" placeholder="Seconds" value="${durationSec || 30}" min="1" max="86400" class="duration-input" ${disabledAttr} ${grayStyle} required>
<div style="display:flex;gap:8px;flex:1;">
<input type="number" placeholder="Seconds" value="${durationSec || 30}" min="1" max="86400" class="duration-input" ${disabledAttr} ${grayStyle} required title="Display duration">
<input type="number" placeholder="Wait ms" value="${waitVal}" min="0" max="60000" class="wait-input" title="Wait before capture (ms)">
</div>
<label class="dark-toggle" title="Force dark mode">
<input type="checkbox" class="dark-mode-input" ${darkChecked}>
🌙
@@ -980,7 +1061,7 @@
const container = document.getElementById("url-rows");
const rows = container.querySelectorAll(".url-row");
const div = document.createElement("div");
div.innerHTML = buildUrlRowHTML("", 30, rows.length, rows.length + 1, false, false);
div.innerHTML = buildUrlRowHTML("", 30, 0, rows.length, rows.length + 1, false, false);
container.appendChild(div.firstElementChild);
updateUrlRowStates();
}
@@ -1037,9 +1118,11 @@
if (!urlVal) continue;
const forceDarkMode = row.querySelector(".dark-mode-input")?.checked ?? false;
const bakeStyles = row.querySelector(".bake-styles-input")?.checked ?? false;
const waitBeforeCaptureMs = parseInt(row.querySelector(".wait-input")?.value) || 0;
urls.push({
url: urlVal,
durationSec: parseInt(row.querySelector(".duration-input").value) || 30,
waitBeforeCaptureMs,
forceDarkMode,
bakeStyles,
});