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:
+81
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user