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:
+89
-6
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user