refactor: flatten monorepo structure to backend/ frontend/ devops/

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>
This commit is contained in:
Dennis
2026-03-07 00:25:29 +01:00
parent 4607af8def
commit 50e25e3ee8
253 changed files with 54 additions and 51 deletions
+32
View File
@@ -0,0 +1,32 @@
import request from "supertest";
import { app } from "../index.js";
describe("API Server", () => {
describe("GET /health", () => {
it("should return 200 with status ok", async () => {
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(res.body.status).toBe("ok");
expect(res.body.timestamp).toBeDefined();
});
});
describe("GET /", () => {
it("should return API info", async () => {
const res = await request(app).get("/");
expect(res.status).toBe(200);
expect(res.body.name).toBe("Website Monitoring API");
expect(res.body.version).toBe("1.0.0");
expect(res.body.endpoints).toContain("/health");
expect(res.body.endpoints).toContain("/api/lighthouse");
});
});
describe("POST /api/lighthouse", () => {
it("should return 400 when no URL provided", async () => {
const res = await request(app).post("/api/lighthouse").send({});
expect(res.status).toBe(400);
expect(res.body.error).toBe("Missing URL");
});
});
});
+79
View File
@@ -0,0 +1,79 @@
import request from "supertest";
import { app } from "../index.js";
describe("Lighthouse API", () => {
describe("POST /api/lighthouse", () => {
it("should return 400 when body is empty", async () => {
const res = await request(app).post("/api/lighthouse").send({});
expect(res.status).toBe(400);
expect(res.body.error).toBe("Missing URL");
});
it("should return 400 when URL is missing from body", async () => {
const res = await request(app)
.post("/api/lighthouse")
.send({ notUrl: "something" });
expect(res.status).toBe(400);
expect(res.body.error).toBe("Missing URL");
});
it("should accept a valid URL and return a clientId", async () => {
// This test will get a clientId back but Chrome won't be available in test
// The endpoint returns 200 with clientId before starting Chrome
const res = await request(app)
.post("/api/lighthouse")
.send({ url: "https://example.com" });
expect(res.status).toBe(200);
expect(res.body.clientId).toBeDefined();
expect(typeof res.body.clientId).toBe("string");
});
});
describe("GET /api/lighthouse/status/:id", () => {
it("should be a valid SSE endpoint", () => {
// SSE endpoints keep connections open, so we just verify the route exists
// Full SSE testing would require a dedicated SSE test client
expect(true).toBe(true);
});
});
});
describe("Health & Info Endpoints", () => {
describe("GET /health", () => {
it("should return status ok with timestamp", async () => {
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
status: "ok",
});
expect(res.body.timestamp).toBeDefined();
// Timestamp should be valid ISO string
expect(new Date(res.body.timestamp).toISOString()).toBe(res.body.timestamp);
});
});
describe("GET /", () => {
it("should return API metadata", async () => {
const res = await request(app).get("/");
expect(res.status).toBe(200);
expect(res.body.name).toBe("Website Monitoring API");
expect(res.body.version).toMatch(/^\d+\.\d+\.\d+$/);
expect(Array.isArray(res.body.endpoints)).toBe(true);
});
});
describe("404 handling", () => {
it("should return 404 for unknown routes", async () => {
const res = await request(app).get("/api/unknown");
expect(res.status).toBe(404);
});
});
describe("CORS", () => {
it("should include CORS headers", async () => {
const res = await request(app).get("/health");
// CORS middleware is enabled
expect(res.status).toBe(200);
});
});
});
+58
View File
@@ -0,0 +1,58 @@
import express, { Request, Response } from "express";
import cors from "cors";
import dotenv from "dotenv";
import lighthouseRouter from "./routes/lighthouse.js";
dotenv.config();
// Rate limiting (simple in-memory for single instance)
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
const RATE_LIMIT_WINDOW = 60_000; // 1 minute
const RATE_LIMIT_MAX = 30; // requests per window
function rateLimit(req: Request, res: Response, next: () => void) {
const ip = req.ip || req.headers["x-forwarded-for"] || "unknown";
const key = String(ip);
const now = Date.now();
const entry = rateLimitMap.get(key);
if (!entry || now > entry.resetAt) {
rateLimitMap.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW });
return next();
}
if (entry.count >= RATE_LIMIT_MAX) {
res.status(429).json({ error: "Too many requests" });
return;
}
entry.count++;
next();
}
const app = express();
app.use(cors({ origin: process.env.CORS_ORIGIN || "*" }));
app.use(express.json());
app.use(rateLimit);
app.get("/health", (_req: Request, res: Response) => {
res.status(200).json({ status: "ok", timestamp: new Date().toISOString() });
});
app.get("/", (_req: Request, res: Response) => {
res.status(200).json({
name: "Website Monitoring API",
version: "1.0.0",
endpoints: ["/health", "/api/lighthouse"],
});
});
app.use("/api/lighthouse", lighthouseRouter);
export { app };
if (process.env.NODE_ENV !== "test") {
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
}
+174
View File
@@ -0,0 +1,174 @@
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 were 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;