Canvas: Rework monitoring page

Display worker statuses with list of commands
Commit hash

Change-Id: I7054ecc5ce81f35cad3fe26fc20677b6f50d3147
diff --git a/apps/canvas/back/src/project_monitor.ts b/apps/canvas/back/src/project_monitor.ts
new file mode 100644
index 0000000..6c178f0
--- /dev/null
+++ b/apps/canvas/back/src/project_monitor.ts
@@ -0,0 +1,130 @@
+import { z } from "zod";
+
+export const WorkerSchema = z.object({
+	id: z.string(),
+	service: z.string(),
+	address: z.string().url(),
+	status: z.optional(
+		z.object({
+			repoOK: z.boolean(),
+			commit: z.string(),
+			commands: z.optional(
+				z.array(
+					z.object({
+						command: z.string(),
+						state: z.string(),
+					}),
+				),
+			),
+		}),
+	),
+	logs: z.optional(z.string()),
+});
+
+export type Worker = z.infer<typeof WorkerSchema>;
+
+class ServiceMonitor {
+	private workers: Map<string, string> = new Map();
+	private logs: Map<string, string> = new Map();
+	private statuses: Map<string, Worker["status"]> = new Map();
+
+	constructor(public readonly serviceName: string) {}
+
+	registerWorker(workerId: string, workerAddress: string, workerLog?: string, workerStatus?: Worker["status"]): void {
+		this.workers.set(workerId, workerAddress);
+		if (workerLog) {
+			this.logs.set(workerId, workerLog);
+		}
+		if (workerStatus) {
+			this.statuses.set(workerId, workerStatus);
+		}
+	}
+
+	getWorkerAddress(workerId: string): string | undefined {
+		return this.workers.get(workerId);
+	}
+
+	getWorkerLog(workerId: string): string | undefined {
+		return this.logs.get(workerId);
+	}
+
+	getWorkerStatus(workerId: string): Worker["status"] | undefined {
+		return this.statuses.get(workerId);
+	}
+
+	getAllLogs(): Map<string, string> {
+		return new Map(this.logs);
+	}
+
+	getAllStatuses(): Map<string, Worker["status"]> {
+		return new Map(this.statuses);
+	}
+
+	getWorkerAddresses(): string[] {
+		return Array.from(this.workers.values());
+	}
+
+	getWorkerIds(): string[] {
+		return Array.from(this.workers.keys());
+	}
+
+	hasLogs(): boolean {
+		return this.logs.size > 0;
+	}
+}
+
+export class ProjectMonitor {
+	private serviceMonitors: Map<string, ServiceMonitor> = new Map();
+
+	constructor() {}
+
+	registerWorker(workerData: Worker): void {
+		let serviceMonitor = this.serviceMonitors.get(workerData.service);
+		if (!serviceMonitor) {
+			serviceMonitor = new ServiceMonitor(workerData.service);
+			this.serviceMonitors.set(workerData.service, serviceMonitor);
+		}
+		serviceMonitor.registerWorker(workerData.id, workerData.address, workerData.logs, workerData.status);
+	}
+
+	getWorkerAddresses(): string[] {
+		let allAddresses: string[] = [];
+		for (const serviceMonitor of this.serviceMonitors.values()) {
+			allAddresses = allAddresses.concat(serviceMonitor.getWorkerAddresses());
+		}
+		return Array.from(new Set(allAddresses));
+	}
+
+	getWorkerLog(serviceName: string, workerId: string): string | 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);
+	}
+
+	getWorkerStatusesForService(serviceName: string): Map<string, Worker["status"]> {
+		const serviceMonitor = this.serviceMonitors.get(serviceName);
+		if (serviceMonitor) {
+			return serviceMonitor.getAllStatuses();
+		}
+		return new Map();
+	}
+}