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/front/src/Config.tsx b/apps/canvas/front/src/Config.tsx
index d2f1aed..7306456 100644
--- a/apps/canvas/front/src/Config.tsx
+++ b/apps/canvas/front/src/Config.tsx
@@ -1,10 +1,11 @@
 import { useNodes } from "@xyflow/react";
-import { AppNode, useEnv } from "./lib/state";
+import { AppNode, useEnv, useProjectId } from "./lib/state";
 import { generateDodoConfig } from "./lib/config";
 import { useEffect, useMemo, useState } from "react";
 
 export function Config() {
 	const env = useEnv();
+	const projectId = useProjectId();
 	const [nodes, setNodes] = useState<AppNode[]>([]);
 	const n = useNodes<AppNode>();
 	useEffect(() => {
@@ -13,7 +14,7 @@
 			setNodes(n);
 		}
 	}, [n, setNodes]);
-	const config = useMemo(() => generateDodoConfig(nodes, env), [nodes, env]);
+	const config = useMemo(() => generateDodoConfig(projectId, nodes, env), [nodes, env]);
 	const configS = useMemo(() => JSON.stringify(config, undefined, 4), [config]);
 	return (
 		<div className="px-5">
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index eb89b8a..509b51c 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -25,6 +25,7 @@
 	const instance = useReactFlow();
 	const [ok, setOk] = useState(false);
 	const [loading, setLoading] = useState(false);
+	const [reloading, setReloading] = useState(false);
 	useEffect(() => {
 		setOk(!messages.some((m) => m.type === "FATAL"));
 	}, [messages, setOk]);
@@ -64,7 +65,7 @@
 		}
 		setLoading(true);
 		try {
-			const config = generateDodoConfig(nodes, env);
+			const config = generateDodoConfig(projectId, nodes, env);
 			if (config == null) {
 				throw new Error("MUST NOT REACH!");
 			}
@@ -163,21 +164,64 @@
 			});
 		}
 	}, [store, clear, projectId, toast]);
-	const [props, setProps] = useState({});
+	const reload = useCallback(async () => {
+		if (projectId == null) {
+			return;
+		}
+		setReloading(true);
+		try {
+			const resp = await fetch(`/api/project/${projectId}/reload`, {
+				method: "POST",
+				headers: {
+					"Content-Type": "application/json",
+				},
+			});
+			if (resp.ok) {
+				toast({
+					title: "Reload triggered successfully",
+				});
+			} else {
+				toast({
+					variant: "destructive",
+					title: "Reload failed",
+					description: await resp.text(),
+				});
+			}
+		} catch (e) {
+			console.log(e);
+			toast({
+				variant: "destructive",
+				title: "Reload failed",
+			});
+		} finally {
+			setReloading(false);
+		}
+	}, [projectId, toast]);
+	const [deployProps, setDeployProps] = useState({});
+	const [reloadProps, setReloadProps] = useState({});
 	useEffect(() => {
 		if (loading) {
-			setProps({ loading: true });
+			setDeployProps({ loading: true });
 		} else if (ok) {
-			setProps({ disabled: false });
+			setDeployProps({ disabled: false });
 		} else {
-			setProps({ disabled: true });
+			setDeployProps({ disabled: true });
 		}
-	}, [ok, loading, setProps]);
+
+		if (reloading) {
+			setReloadProps({ loading: true });
+		} else {
+			setReloadProps({ disabled: projectId === undefined });
+		}
+	}, [ok, loading, reloading, projectId]);
 	return (
 		<>
-			<Button onClick={deploy} {...props}>
+			<Button onClick={deploy} {...deployProps}>
 				Deploy
 			</Button>
+			<Button onClick={reload} {...reloadProps}>
+				Reload
+			</Button>
 			<Button onClick={save}>Save</Button>
 			<Button onClick={restoreSaved}>Restore</Button>
 			<Button onClick={clear} variant="destructive">
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 187f51c..a3784c1 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -78,14 +78,21 @@
 };
 
 export type Config = {
+	input: {
+		appId: string;
+		managerAddr: string;
+	};
 	service?: Service[];
 	volume?: Volume[];
 	postgresql?: PostgreSQL[];
 	mongodb?: MongoDB[];
 };
 
-export function generateDodoConfig(nodes: AppNode[], env: Env): Config | null {
+export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): Config | null {
 	try {
+		if (appId == null || env.managerAddr == null) {
+			return null;
+		}
 		const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
 		const ingressNodes = nodes.filter((n) => n.type === "gateway-https").filter((n) => n.data.https !== undefined);
 		const tcpNodes = nodes.filter((n) => n.type === "gateway-tcp").filter((n) => n.data.exposed !== undefined);
@@ -105,6 +112,10 @@
 				});
 		};
 		return {
+			input: {
+				appId: appId,
+				managerAddr: env.managerAddr,
+			},
 			service: nodes
 				.filter((n) => n.type === "app")
 				.map((n): Service => {
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index f9c1f56..0b3507d 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -314,6 +314,7 @@
 };
 
 export const envSchema = z.object({
+	managerAddr: z.optional(z.string().min(1)),
 	deployKey: z.optional(z.string().min(1)),
 	networks: z
 		.array(
@@ -331,6 +332,7 @@
 export type Env = z.infer<typeof envSchema>;
 
 const defaultEnv: Env = {
+	managerAddr: undefined,
 	deployKey: undefined,
 	networks: [],
 	integrations: {