Canvas: Implement worker to manager communication

Register workers on manager side.
Let user force reload service workers.

Change-Id: I2635a04167e7c853151d8a1f5c3511646181a063
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 65eab15..0a35dfe 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -3,9 +3,13 @@
 import { env } from "node:process";
 import axios from "axios";
 import { GithubClient } from "./github";
+import { z } from "zod";
 
 const db = new PrismaClient();
 
+// Map to store worker addresses by project ID
+const workers = new Map<number, string[]>();
+
 const handleProjectCreate: express.Handler = async (req, resp) => {
 	try {
 		const { id } = await db.project.create({
@@ -346,6 +350,8 @@
 		resp.status(200);
 		resp.write(
 			JSON.stringify({
+				// TODO(gio): get from env or command line flags
+				managerAddr: "http://10.42.0.239:8080",
 				deployKey: project.deployKey,
 				integrations: {
 					github: !!project.githubToken,
@@ -371,6 +377,88 @@
 	}
 };
 
+const WorkerSchema = z.object({
+	address: z.string().url(),
+});
+
+const handleRegisterWorker: express.Handler = async (req, resp) => {
+	try {
+		const projectId = Number(req.params["projectId"]);
+
+		const result = WorkerSchema.safeParse(req.body);
+		console.log(result);
+		if (!result.success) {
+			resp.status(400);
+			resp.write(
+				JSON.stringify({
+					error: "Invalid request data",
+					details: result.error.format(),
+				}),
+			);
+			return;
+		}
+
+		const { address } = result.data;
+
+		// Get existing workers or initialize empty array
+		const projectWorkers = workers.get(projectId) || [];
+
+		// Add new worker if not already present
+		if (!projectWorkers.includes(address)) {
+			projectWorkers.push(address);
+		}
+
+		workers.set(projectId, projectWorkers);
+
+		resp.status(200);
+		resp.write(
+			JSON.stringify({
+				success: true,
+				workers: projectWorkers,
+			}),
+		);
+	} catch (e) {
+		console.log(e);
+		resp.status(500);
+		resp.write(JSON.stringify({ error: "Failed to register worker" }));
+	} finally {
+		resp.end();
+	}
+};
+
+const handleReload: express.Handler = async (req, resp) => {
+	try {
+		const projectId = Number(req.params["projectId"]);
+		const projectWorkers = workers.get(projectId) || [];
+
+		if (projectWorkers.length === 0) {
+			resp.status(404);
+			resp.write(JSON.stringify({ error: "No workers registered for this project" }));
+			return;
+		}
+
+		await Promise.all(
+			projectWorkers.map(async (workerAddress) => {
+				try {
+					const updateEndpoint = `${workerAddress}/update`;
+					await axios.post(updateEndpoint);
+				} catch (error: any) {
+					console.log(`Failed to update worker ${workerAddress}: ${error.message || "Unknown error"}`);
+				}
+			}),
+		);
+
+		resp.status(200);
+		resp.write(JSON.stringify({ success: true }));
+	} catch (e) {
+		console.log(e);
+		resp.status(500);
+		resp.write(JSON.stringify({ error: "Failed to reload workers" }));
+	} finally {
+		resp.end();
+	}
+};
+
 async function start() {
 	await db.$connect();
 	const app = express();
@@ -385,6 +473,8 @@
 	app.get("/api/project/:projectId/repos/github", handleGithubRepos);
 	app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
 	app.get("/api/project/:projectId/env", handleEnv);
+	app.post("/api/project/:projectId/workers", handleRegisterWorker);
+	app.post("/api/project/:projectId/reload", handleReload);
 	app.use("/", express.static("../front/dist"));
 	app.listen(env.DODO_PORT_WEB, () => {
 		console.log("started");