Canvas: Check agent health

Change-Id: I429eecebde1d661513dfd94e27cdaa820bd49493
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index fb77431..9628ef5 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -25,7 +25,7 @@
 } from "config";
 import { Instant, DateTimeFormatter, ZoneId } from "@js-joda/core";
 import LogStore from "./log.js";
-import { GraphOrConfigSchema, GraphSchema, GraphConfigOrDraft } from "config/dist/graph.js";
+import { GraphOrConfigSchema, GraphSchema, GraphConfigOrDraft, AgentAccess } from "config/dist/graph.js";
 
 async function generateKey(root: string): Promise<[string, string]> {
 	const privKeyPath = path.join(root, "key");
@@ -1200,6 +1200,23 @@
 	};
 }
 
+const handleAgentStatus: express.Handler = async (req, resp) => {
+	const projectId = Number(req.params["projectId"]);
+	const agentName = req.params["agentName"];
+	try {
+		const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
+		const agent = env.access.find((a): a is AgentAccess => a.type === "https" && a.agentName === agentName);
+		if (!agent) {
+			resp.status(404).send({ status: 404 });
+			return;
+		}
+		const agentResp = await axios.get(agent.address);
+		resp.status(200).send({ status: agentResp.status });
+	} catch {
+		resp.status(200).send({ status: 500 });
+	}
+};
+
 async function start() {
 	await db.$connect();
 	const app = express();
@@ -1231,6 +1248,7 @@
 	projectRouter.post("/:projectId/quitquitquit/:serviceName/:workerId", handleQuitWorker);
 	projectRouter.post("/:projectId/reload", handleReload);
 	projectRouter.get("/:projectId/logs/:service/:workerId", handleServiceLogs);
+	projectRouter.get("/:projectId/agent/:agentName/status", handleAgentStatus);
 	projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
 	projectRouter.get("/", handleProjectAll);
 	projectRouter.post("/", handleProjectCreate);
diff --git a/apps/canvas/front/src/Agent.tsx b/apps/canvas/front/src/Agent.tsx
index 9ed47d5..3718805 100644
--- a/apps/canvas/front/src/Agent.tsx
+++ b/apps/canvas/front/src/Agent.tsx
@@ -1,6 +1,36 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
 import { AgentAccess } from "config";
+import { useProjectId } from "./lib/state";
 
 export function Agent({ agent }: { agent: AgentAccess }): React.ReactNode {
+	return <AgentIframe agent={agent} />;
+}
+
+export function AgentIframe({ agent }: { agent: AgentAccess }): React.ReactNode {
+	const projectId = useProjectId();
+	const [ok, setOk] = useState<boolean>(false);
+	useEffect(() => {
+		let timeout: NodeJS.Timeout | null = null;
+		const check = async () => {
+			const resp = await fetch(`/api/project/${projectId}/agent/${agent.agentName}/status`);
+			if (resp.ok) {
+				const status = (await resp.json()).status;
+				if (200 <= status && status < 300) {
+					setOk(true);
+					return;
+				}
+			}
+			timeout = setTimeout(check, 500);
+		};
+		check();
+		return () => {
+			if (timeout != null) {
+				clearTimeout(timeout);
+			}
+		};
+	}, [agent.agentName, agent.address, setOk, projectId]);
+	if (!ok) {
+		return <div>Agent {agent.agentName} is loading...</div>;
+	}
 	return <iframe key={agent.name} src={`${agent.address}?m`} title={agent.agentName} className="w-full h-full" />;
 }
diff --git a/apps/canvas/front/src/App.tsx b/apps/canvas/front/src/App.tsx
index 5100576..5f80c95 100644
--- a/apps/canvas/front/src/App.tsx
+++ b/apps/canvas/front/src/App.tsx
@@ -11,6 +11,7 @@
 import { useAgents } from "./lib/state";
 import { Bot } from "lucide-react";
 import { Preview } from "./components/preview";
+import { AgentIframe } from "./Agent";
 
 export default function App() {
 	return (
@@ -67,7 +68,7 @@
 			</TabsContent>
 			{agents.map((a) => (
 				<TabsContent value={`agent-${a.agentName}`} className="!mt-0 flex-1 min-h-0">
-					<iframe key={a.name} src={a.address} title={a.agentName} className="w-full h-full" />
+					<AgentIframe agent={a} />
 				</TabsContent>
 			))}
 		</Tabs>
diff --git a/apps/canvas/front/src/components/ChatBubble.tsx b/apps/canvas/front/src/components/ChatBubble.tsx
index 74e3605..24d8432 100644
--- a/apps/canvas/front/src/components/ChatBubble.tsx
+++ b/apps/canvas/front/src/components/ChatBubble.tsx
@@ -177,6 +177,7 @@
 			</div>
 			{isExpanded && (
 				<div className={iframeContainerClasses}>
+					// TODO(gio): use AgentIframe
 					<iframe
 						src={agentUrl}
 						title={agentName}