50e25e3ee8
Rename subdirectories for a cleaner single-repo layout: - website-monitoring-backend/ → backend/ - website-monitoring-frontend/ → frontend/ - website-monitoring-devops/ → devops/ Update all references in package.json scripts, CI workflows, docker-compose, pre-commit hooks, and documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
175 lines
5.2 KiB
TypeScript
175 lines
5.2 KiB
TypeScript
import express, { Request, Response } from "express";
|
||
import { v4 as uuidv4 } from "uuid";
|
||
|
||
const router = express.Router();
|
||
const progressClients = new Map<string, Response>();
|
||
|
||
/** Send SSE progress data to the browser */
|
||
function sendProgress(clientId: string, data: any) {
|
||
const client = progressClients.get(clientId);
|
||
if (client) {
|
||
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
||
}
|
||
}
|
||
|
||
// SSE endpoint for progress
|
||
router.get("/status/:id", (req: Request, res: Response) => {
|
||
res.setHeader("Content-Type", "text/event-stream");
|
||
res.setHeader("Cache-Control", "no-cache");
|
||
res.setHeader("Connection", "keep-alive");
|
||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||
|
||
const clientId = req.params.id;
|
||
progressClients.set(clientId, res);
|
||
|
||
// Send an initial event
|
||
sendProgress(clientId, {
|
||
status: "Connected",
|
||
progress: 0,
|
||
stage: "setup",
|
||
});
|
||
|
||
req.on("close", () => {
|
||
progressClients.delete(clientId);
|
||
});
|
||
});
|
||
|
||
router.post("/", async (req: Request, res: Response) => {
|
||
const { url } = req.body;
|
||
if (!url) {
|
||
return res.status(400).json({ error: "Missing URL" });
|
||
}
|
||
|
||
const clientId = uuidv4(); // Generate a UUID
|
||
console.log(`New client ID: ${clientId}`);
|
||
let currentProgress = 0;
|
||
let auditCount = 0;
|
||
let totalAudits = 0;
|
||
|
||
try {
|
||
// Immediately tell the frontend we’re starting (0%)
|
||
sendProgress(clientId, {
|
||
status: "Starting analysis...",
|
||
progress: 0,
|
||
stage: "setup",
|
||
});
|
||
|
||
// Return clientId so the frontend can open the SSE channel
|
||
res.status(200).json({ clientId });
|
||
|
||
// Dynamically import chrome-launcher and lighthouse
|
||
const { launch } = await import("chrome-launcher");
|
||
const lighthouse = (await import("lighthouse")).default;
|
||
const log = (await import("lighthouse-logger")).default;
|
||
|
||
// Launch Chrome
|
||
const chrome = await launch({
|
||
chromeFlags: ["--headless", "--no-sandbox", "--disable-dev-shm-usage"],
|
||
});
|
||
|
||
// Update progress to 10% after launching Chrome
|
||
currentProgress = 10;
|
||
sendProgress(clientId, {
|
||
status: "Chrome launched",
|
||
progress: currentProgress,
|
||
stage: "setup",
|
||
});
|
||
|
||
// Turn on Lighthouse logs at "info" level
|
||
log.setLevel("info");
|
||
|
||
// Listen for Lighthouse status events
|
||
log.events.addListener("status", (lhStatus) => {
|
||
let msg = lhStatus[1].toLowerCase();
|
||
let newProgress = currentProgress;
|
||
let newStage = "analyzing";
|
||
|
||
if (msg.includes("initialize config")) {
|
||
newProgress = Math.max(newProgress, 15);
|
||
newStage = "setup";
|
||
} else if (msg.includes("resolve artifact definitions")) {
|
||
newProgress = Math.max(newProgress, 20);
|
||
newStage = "setup";
|
||
} else if (msg.includes("gather phase")) {
|
||
newProgress = Math.max(newProgress, 25);
|
||
newStage = "analyzing";
|
||
} else if (msg.includes("connecting to browser")) {
|
||
newProgress = Math.max(newProgress, 28);
|
||
newStage = "analyzing";
|
||
} else if (msg.includes("navigating to")) {
|
||
newProgress = Math.max(newProgress, 30);
|
||
newStage = "analyzing";
|
||
} else if (msg.includes("benchmarking machine")) {
|
||
newProgress = Math.max(newProgress, 35);
|
||
newStage = "analyzing";
|
||
} else if (msg.includes("getting artifact")) {
|
||
newProgress = Math.max(newProgress, 40);
|
||
newStage = "analyzing";
|
||
} else if (msg.includes("computing artifact")) {
|
||
newProgress = Math.max(newProgress, 50);
|
||
newStage = "analyzing";
|
||
} else if (msg.includes("audit phase")) {
|
||
newProgress = Math.max(newProgress, 60);
|
||
newStage = "auditing";
|
||
} else if (msg.includes("auditing:")) {
|
||
auditCount++;
|
||
totalAudits = Math.max(totalAudits, auditCount);
|
||
newProgress = Math.max(
|
||
newProgress,
|
||
60 + (auditCount / totalAudits) * 30,
|
||
);
|
||
newStage = "auditing";
|
||
} else if (msg.includes("generating results")) {
|
||
newProgress = Math.max(newProgress, 95);
|
||
newStage = "finishing";
|
||
}
|
||
|
||
// Add some randomness to the progress increments
|
||
newProgress += Math.random() * 2;
|
||
|
||
// Only send progress if it actually increased
|
||
if (newProgress > currentProgress) {
|
||
currentProgress = newProgress;
|
||
sendProgress(clientId, {
|
||
status: lhStatus.message || "Processing...",
|
||
progress: currentProgress,
|
||
stage: newStage,
|
||
});
|
||
}
|
||
});
|
||
|
||
// Run Lighthouse
|
||
const runnerResult = await lighthouse(url, {
|
||
port: chrome.port,
|
||
output: "json",
|
||
logLevel: "info",
|
||
onlyCategories: ["performance", "seo", "accessibility", "best-practices"],
|
||
});
|
||
|
||
if (!runnerResult) {
|
||
throw new Error("Lighthouse returned no result");
|
||
}
|
||
|
||
// Finish progress
|
||
sendProgress(clientId, {
|
||
status: "Analysis complete",
|
||
progress: 100,
|
||
stage: "complete",
|
||
report: runnerResult.lhr,
|
||
});
|
||
|
||
await chrome.kill();
|
||
} catch (error) {
|
||
console.error("Lighthouse error:", error);
|
||
sendProgress(clientId, {
|
||
status: "Error occurred",
|
||
progress: 0,
|
||
stage: "error",
|
||
error:
|
||
error instanceof Error ? error.message : "An unknown error occurred",
|
||
});
|
||
}
|
||
});
|
||
|
||
export default router;
|