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
+81 -3
View File
@@ -334,9 +334,9 @@ if (viewCount === 0) {
crypto.randomUUID(),
"Demo View",
JSON.stringify([
{ url: "https://www.google.com", durationSec: 30, forceDarkMode: false, bakeStyles: false },
{ url: "https://duck.ai", durationSec: 30, forceDarkMode: false, bakeStyles: false },
{ url: "https://www.msn.com", durationSec: 30, forceDarkMode: false, bakeStyles: false },
{ url: "https://www.google.com", durationSec: 30, waitBeforeCaptureMs: 0, forceDarkMode: false, bakeStyles: false },
{ url: "https://duck.ai", durationSec: 30, waitBeforeCaptureMs: 0, forceDarkMode: false, bakeStyles: false },
{ url: "https://www.msn.com", durationSec: 30, waitBeforeCaptureMs: 0, forceDarkMode: false, bakeStyles: false },
]),
"ssr",
0,
@@ -839,6 +839,7 @@ async function renderViewToCache(view: View, urlIndex: number, key: string): Pro
const urlItem = view.urls[urlIndex];
const forceDarkMode = urlItem.forceDarkMode ?? false;
const bakeStyles = urlItem.bakeStyles ?? false;
const waitBeforeCaptureMs = urlItem.waitBeforeCaptureMs ?? 0;
let body: string;
if (view.method === "mjpeg") {
@@ -847,6 +848,7 @@ async function renderViewToCache(view: View, urlIndex: number, key: string): Pro
view.viewportWidth,
view.viewportHeight,
forceDarkMode,
waitBeforeCaptureMs,
);
body = jpeg.toString("base64");
} else {
@@ -856,6 +858,7 @@ async function renderViewToCache(view: View, urlIndex: number, key: string): Pro
view.viewportHeight,
forceDarkMode,
bakeStyles,
waitBeforeCaptureMs,
);
}
@@ -1308,6 +1311,7 @@ async function renderScreenshotFrame(
viewportWidth = 1920,
viewportHeight = 1080,
forceDarkMode = false,
waitBeforeCaptureMs = 0,
): Promise<Buffer> {
const page = await pagePool.acquire();
try {
@@ -1326,6 +1330,11 @@ async function renderScreenshotFrame(
}
}
// Wait before capture (for animations, dynamic content, etc.)
if (waitBeforeCaptureMs > 0) {
await new Promise((r) => setTimeout(r, waitBeforeCaptureMs));
}
return Buffer.from(await page.screenshot({ type: "jpeg", quality: 75, fullPage: false }));
} finally {
await pagePool.release(page);
@@ -1428,6 +1437,7 @@ function json(data: unknown, status = 200): Response {
async function handleAPI(req: Request, path: string): Promise<Response> {
const method = req.method;
const url = new URL(req.url);
// GET /api/views
if (path === "/api/views" && method === "GET") {
@@ -1464,6 +1474,7 @@ async function handleAPI(req: Request, path: string): Promise<Response> {
urls: body.urls.map((u) => ({
url: u.url.trim(),
durationSec: Math.max(1, Math.round(u.durationSec)),
waitBeforeCaptureMs: Math.max(0, u.waitBeforeCaptureMs ?? 0),
forceDarkMode: u.forceDarkMode ?? false,
bakeStyles: u.bakeStyles ?? false,
})),
@@ -1507,6 +1518,7 @@ async function handleAPI(req: Request, path: string): Promise<Response> {
view.urls = body.urls.map((u) => ({
url: u.url.trim(),
durationSec: Math.max(1, Math.round(u.durationSec)),
waitBeforeCaptureMs: Math.max(0, u.waitBeforeCaptureMs ?? 0),
forceDarkMode: u.forceDarkMode ?? false,
bakeStyles: u.bakeStyles ?? false,
}));
@@ -1541,6 +1553,72 @@ async function handleAPI(req: Request, path: string): Promise<Response> {
console.log(`[api] Deleted ${viewId}`);
return json({ ok: true });
}
// GET /api/views/:id/export - Export single view as JSON
if (method === "GET" && url.searchParams.get("export") === "true") {
const view = viewCacheGet(viewId);
if (!view) return json({ error: "View not found" }, 404);
const exportData = {
name: view.name,
urls: view.urls,
method: view.method,
metaRefreshEnabled: view.metaRefreshEnabled,
cacheTtlSec: view.cacheTtlSec,
viewportWidth: view.viewportWidth,
viewportHeight: view.viewportHeight,
};
return new Response(JSON.stringify(exportData, null, 2), {
headers: {
"Content-Type": "application/json",
"Content-Disposition": `attachment; filename="view-${viewId}.json"`,
},
});
}
}
// POST /api/views/import - Import a view from JSON
if (path === "/api/views/import" && method === "POST") {
try {
const importData = JSON.parse(await req.text());
if (!importData.name?.trim()) return json({ error: "Name is required" }, 400);
if (!importData.urls?.length) return json({ error: "At least one URL is required" }, 400);
for (const u of importData.urls) {
if (!u.url?.trim()) return json({ error: "Each URL must have a url" }, 400);
if (typeof u.durationSec !== "number" || u.durationSec <= 0)
return json({ error: "Each URL must have positive durationSec" }, 400);
}
const method = importData.method ?? "ssr";
if (!["mjpeg", "ssr"].includes(method))
return json({ error: 'method must be "mjpeg" or "ssr"' }, 400);
const view: View = {
id: crypto.randomUUID(),
name: importData.name.trim(),
urls: importData.urls.map((u: URLItem) => ({
url: u.url.trim(),
durationSec: Math.max(1, Math.round(u.durationSec)),
waitBeforeCaptureMs: Math.max(0, u.waitBeforeCaptureMs ?? 0),
forceDarkMode: u.forceDarkMode ?? false,
bakeStyles: u.bakeStyles ?? false,
})),
method: method,
metaRefreshEnabled: importData.metaRefreshEnabled ?? false,
cacheTtlSec: Math.max(0, importData.cacheTtlSec ?? 60),
viewportWidth: Math.max(320, Math.min(7680, importData.viewportWidth ?? 1920)),
viewportHeight: Math.max(240, Math.min(4320, importData.viewportHeight ?? 1080)),
createdAt: Math.floor(Date.now() / 1000),
};
dbInsert(view);
viewCache.set(view.id, view);
console.log(`[api] Imported "${view.name}" (${view.id}) method=${view.method}`);
return json(view, 201);
} catch {
return json({ error: "Invalid JSON body" }, 400);
}
}
// POST /api/admin/disconnect/:ip