Canvas: Auto cleanup old workers

Change-Id: I046ce8dcbadd14b53e465fe0ffb60fae642c6951
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 9628ef5..b6a7098 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -1274,4 +1274,14 @@
 	});
 }
 
+function cleanupWorkers() {
+	const now = Date.now();
+	projectMonitors.forEach((monitor) => {
+		monitor.cleanupWorkers(now);
+	});
+	setTimeout(cleanupWorkers, 1000);
+}
+
+setTimeout(cleanupWorkers, 1000);
+
 start();
diff --git a/apps/canvas/back/src/project_monitor.ts b/apps/canvas/back/src/project_monitor.ts
index 7dd2f3c..342a3f3 100644
--- a/apps/canvas/back/src/project_monitor.ts
+++ b/apps/canvas/back/src/project_monitor.ts
@@ -47,8 +47,8 @@
 
 class ServiceMonitor {
 	private workers: Map<string, string> = new Map();
-	private logs: Map<string, LogItem[]> = new Map();
 	private statuses: Map<string, Worker["status"]> = new Map();
+	private lastPings: Map<string, number> = new Map();
 
 	constructor(public readonly serviceName: string) {}
 
@@ -57,24 +57,17 @@
 		if (workerStatus) {
 			this.statuses.set(workerId, workerStatus);
 		}
+		this.lastPings.set(workerId, Date.now());
 	}
 
 	getWorkerAddress(workerId: string): string | undefined {
 		return this.workers.get(workerId);
 	}
 
-	getWorkerLog(workerId: string): LogItem[] | undefined {
-		return this.logs.get(workerId);
-	}
-
 	getWorkerStatus(workerId: string): Worker["status"] | undefined {
 		return this.statuses.get(workerId);
 	}
 
-	getAllLogs(): Map<string, LogItem[]> {
-		return new Map(this.logs);
-	}
-
 	getAllStatuses(): Map<string, Worker["status"]> {
 		return new Map(this.statuses);
 	}
@@ -87,10 +80,6 @@
 		return Array.from(this.workers.keys());
 	}
 
-	hasLogs(): boolean {
-		return this.logs.size > 0;
-	}
-
 	async reloadWorker(workerId: string): Promise<void> {
 		const workerAddress = this.workers.get(workerId);
 		if (!workerAddress) {
@@ -126,6 +115,20 @@
 			throw error; // Re-throw to be caught by ProjectMonitor
 		}
 	}
+
+	cleanupWorkers(now: number) {
+		const toDelete: string[] = [];
+		this.workers.forEach((_, workerId) => {
+			if (now - (this.lastPings.get(workerId) ?? 0) > 1000 * 60) {
+				toDelete.push(workerId);
+			}
+		});
+		for (const workerId of toDelete) {
+			this.workers.delete(workerId);
+			this.statuses.delete(workerId);
+			this.lastPings.delete(workerId);
+		}
+	}
 }
 
 export class ProjectMonitor {
@@ -150,27 +153,10 @@
 		return Array.from(new Set(allAddresses));
 	}
 
-	getWorkerLog(serviceName: string, workerId: string): LogItem[] | undefined {
-		const serviceMonitor = this.serviceMonitors.get(serviceName);
-		if (serviceMonitor) {
-			return serviceMonitor.getWorkerLog(workerId);
-		}
-		return undefined;
-	}
-
 	getAllServiceNames(): string[] {
 		return Array.from(this.serviceMonitors.keys());
 	}
 
-	hasLogs(): boolean {
-		for (const serviceMonitor of this.serviceMonitors.values()) {
-			if (serviceMonitor.hasLogs()) {
-				return true;
-			}
-		}
-		return false;
-	}
-
 	getServiceMonitor(serviceName: string): ServiceMonitor | undefined {
 		return this.serviceMonitors.get(serviceName);
 	}
@@ -198,4 +184,10 @@
 		}
 		await serviceMonitor.terminateWorker(workerId);
 	}
+
+	cleanupWorkers(now: number) {
+		this.serviceMonitors.forEach((serviceMonitor) => {
+			serviceMonitor.cleanupWorkers(now);
+		});
+	}
 }
diff --git a/apps/canvas/front/src/Monitoring.tsx b/apps/canvas/front/src/Monitoring.tsx
index 0d86432..46197ef 100644
--- a/apps/canvas/front/src/Monitoring.tsx
+++ b/apps/canvas/front/src/Monitoring.tsx
@@ -62,19 +62,31 @@
 		[env?.services],
 	);
 
+	const handleViewLogsClick = useCallback(
+		(serviceName: string, workerId: string) => {
+			setSelectedServiceForLogs(serviceName);
+			setSelectedWorkerIdForLogs(workerId);
+		},
+		[setSelectedServiceForLogs, setSelectedWorkerIdForLogs],
+	);
+
 	useEffect(() => {
 		if (sortedServices.length > 0 && sortedServices[0].workers && sortedServices[0].workers.length > 0) {
-			if (!selectedServiceForLogs && !selectedWorkerIdForLogs) {
+			if (!selectedServiceForLogs || !selectedWorkerIdForLogs) {
+				handleViewLogsClick(sortedServices[0].name, sortedServices[0].workers[0].id);
+				return;
+			}
+			const service = sortedServices.find((s) => s.name === selectedServiceForLogs);
+			if (service == null) {
+				handleViewLogsClick(sortedServices[0].name, sortedServices[0].workers[0].id);
+				return;
+			}
+			const worker = service.workers.find((w) => w.id === selectedWorkerIdForLogs);
+			if (worker == null) {
 				handleViewLogsClick(sortedServices[0].name, sortedServices[0].workers[0].id);
 			}
 		}
-		// eslint-disable-next-line react-hooks/exhaustive-deps
-	}, [sortedServices]);
-
-	const handleViewLogsClick = (serviceName: string, workerId: string) => {
-		setSelectedServiceForLogs(serviceName);
-		setSelectedWorkerIdForLogs(workerId);
-	};
+	}, [sortedServices, selectedServiceForLogs, selectedWorkerIdForLogs, handleViewLogsClick]);
 
 	const handleReloadWorkerClick = useCallback(
 		(serviceName: string, workerId: string) => {
@@ -302,7 +314,7 @@
 				<div className="flex flex-col h-full">
 					{selectedServiceForLogs && selectedWorkerIdForLogs ? (
 						<>
-							<div className="p-2 border-b text-sm text-muted-foreground">
+							<div className="p-2 border-b">
 								Logs for: {selectedServiceForLogs} / {selectedWorkerIdForLogs}
 							</div>
 							<div className="flex-1 h-full p-4 bg-muted overflow-auto">