Canvas: Add worker kill action to monitoring tab

Change-Id: I8387dcf2320f9eccdb2e443fd4039e91ff35ed31
diff --git a/apps/canvas/back/.env b/apps/canvas/back/.env
index 9efa576..d34ff70 100644
--- a/apps/canvas/back/.env
+++ b/apps/canvas/back/.env
@@ -1,3 +1,3 @@
 DATABASE_URL=file:/home/gio/dodo.db
 # PUBLIC_ADDR=https://canvas.p.v1.dodo.cloud
-INTERNAL_API_ADDR=http://10.42.0.39:8081
+INTERNAL_API_ADDR=http://10.42.1.95:8081
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index b33cbbc..cc8d9ef 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -902,6 +902,30 @@
 	}
 };
 
+const handleQuitWorker: express.Handler = async (req, resp) => {
+	const projectId = Number(req.params["projectId"]);
+	const serviceName = req.params["serviceName"];
+	const workerId = req.params["workerId"];
+
+	const projectMonitor = projectMonitors.get(projectId);
+	if (!projectMonitor) {
+		resp.status(404).send({ error: "Project monitor not found" });
+		return;
+	}
+
+	try {
+		await projectMonitor.terminateWorker(serviceName, workerId);
+		resp.status(200).send({ message: "Worker termination initiated" });
+	} catch (error) {
+		console.error(
+			`Failed to terminate worker ${workerId} in service ${serviceName} for project ${projectId}:`,
+			error,
+		);
+		const errorMessage = error instanceof Error ? error.message : "Unknown error";
+		resp.status(500).send({ error: `Failed to terminate worker: ${errorMessage}` });
+	}
+};
+
 const handleReloadWorker: express.Handler = async (req, resp) => {
 	const projectId = Number(req.params["projectId"]);
 	const serviceName = req.params["serviceName"];
@@ -1101,6 +1125,7 @@
 	projectRouter.post("/:projectId/anthropic-token", handleUpdateAnthropicToken);
 	projectRouter.get("/:projectId/env", handleEnv);
 	projectRouter.post("/:projectId/reload/:serviceName/:workerId", handleReloadWorker);
+	projectRouter.post("/:projectId/quitquitquit/:serviceName/:workerId", handleQuitWorker);
 	projectRouter.post("/:projectId/reload", handleReload);
 	projectRouter.get("/:projectId/logs/:service/:workerId", handleServiceLogs);
 	projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
diff --git a/apps/canvas/back/src/project_monitor.ts b/apps/canvas/back/src/project_monitor.ts
index df20e05..7dd2f3c 100644
--- a/apps/canvas/back/src/project_monitor.ts
+++ b/apps/canvas/back/src/project_monitor.ts
@@ -109,6 +109,23 @@
 			throw error; // Re-throw to be caught by ProjectMonitor
 		}
 	}
+
+	async terminateWorker(workerId: string): Promise<void> {
+		const workerAddress = this.workers.get(workerId);
+		if (!workerAddress) {
+			throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
+		}
+		try {
+			const response = await fetch(`${workerAddress}/quitquitquit`, { method: "POST" });
+			if (!response.ok) {
+				throw new Error(`Failed to terminate worker ${workerId} at ${workerAddress}: ${response.statusText}`);
+			}
+			console.log(`Terminated worker ${workerId} in service ${this.serviceName}`);
+		} catch (error) {
+			console.error(`Error terminating worker ${workerId} in service ${this.serviceName}:`, error);
+			throw error; // Re-throw to be caught by ProjectMonitor
+		}
+	}
 }
 
 export class ProjectMonitor {
@@ -173,4 +190,12 @@
 		}
 		await serviceMonitor.reloadWorker(workerId);
 	}
+
+	async terminateWorker(serviceName: string, workerId: string): Promise<void> {
+		const serviceMonitor = this.serviceMonitors.get(serviceName);
+		if (!serviceMonitor) {
+			throw new Error(`Service ${serviceName} not found`);
+		}
+		await serviceMonitor.terminateWorker(workerId);
+	}
 }
diff --git a/apps/canvas/front/src/Monitoring.tsx b/apps/canvas/front/src/Monitoring.tsx
index 3bc0b63..0d86432 100644
--- a/apps/canvas/front/src/Monitoring.tsx
+++ b/apps/canvas/front/src/Monitoring.tsx
@@ -6,7 +6,7 @@
 import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { useToast } from "@/hooks/use-toast";
-import { Check, Ellipsis, LoaderCircle, LogsIcon, X, RefreshCw } from "lucide-react";
+import { Check, Ellipsis, LoaderCircle, LogsIcon, X, RefreshCw, Power } from "lucide-react";
 import { XTerm } from "@/components/XTerm";
 
 export function Logs() {
@@ -107,6 +107,41 @@
 		[projectId, toast],
 	);
 
+	const handleQuitWorkerClick = useCallback(
+		(serviceName: string, workerId: string) => {
+			if (!projectId) return;
+			if (!window.confirm(`Are you sure you want to terminate worker ${workerId} for service ${serviceName}?`)) {
+				return;
+			}
+			toast({
+				title: "Worker ${serviceName} / ${workerId} is being terminated.",
+			});
+			fetch(`/api/project/${projectId}/quitquitquit/${serviceName}/${workerId}`, {
+				method: "POST",
+			})
+				.then((resp) => {
+					if (!resp.ok) {
+						toast({
+							title: `Failed to terminate worker ${serviceName} / ${workerId}`,
+							variant: "destructive",
+						});
+					} else {
+						toast({
+							title: `Successfully terminated worker ${serviceName} / ${workerId}`,
+						});
+					}
+				})
+				.catch((e) => {
+					console.error(e);
+					toast({
+						title: `Failed to terminate worker ${serviceName} / ${workerId}`,
+						variant: "destructive",
+					});
+				});
+		},
+		[projectId, toast],
+	);
+
 	const defaultExpandedServiceNames = useMemo(() => sortedServices.map((s) => s.name), [sortedServices]);
 	const defaultExpandedFirstWorkerId = useMemo(() => {
 		if (sortedServices.length > 0 && sortedServices[0].workers && sortedServices[0].workers.length > 0) {
@@ -167,6 +202,20 @@
 																		<RefreshCw className="w-4 h-4" />
 																		Reload Worker
 																	</Button>
+																	<Button
+																		onClick={() =>
+																			handleQuitWorkerClick(
+																				service.name,
+																				worker.id,
+																			)
+																		}
+																		size="sm"
+																		variant="link"
+																		className="!px-0"
+																	>
+																		<Power className="w-4 h-4" />
+																		Quit Worker
+																	</Button>
 																	{!worker.commit && (
 																		<p className="flex items-center">
 																			<FailureIcon />