Canvas: Prettier

Change-Id: I620dde109df0f29f0c85c6fe150e347d2c32a03e
diff --git a/apps/canvas/front/src/lib/categories.ts b/apps/canvas/front/src/lib/categories.ts
index 17e786e..8753455 100644
--- a/apps/canvas/front/src/lib/categories.ts
+++ b/apps/canvas/front/src/lib/categories.ts
@@ -1,101 +1,105 @@
 import { NodeType, InitData } from "@/lib/state";
 
 export interface CategoryItem<T extends NodeType> {
-    title: string;
-    init: InitData;
-    type: T;
-};
+	title: string;
+	init: InitData;
+	type: T;
+}
 
 export type Category = {
-    title: string;
-    items: CategoryItem<NodeType>[];
-    active?: boolean;
+	title: string;
+	items: CategoryItem<NodeType>[];
+	active?: boolean;
 };
 
 const defaultInit: Pick<InitData, "label" | "envVars" | "ports"> = {
-    label: "",
-    envVars: [],
-    ports: [],
+	label: "",
+	envVars: [],
+	ports: [],
 };
 
 export const defaultCategories: Category[] = [
-    {
-        title: "Repository",
-        items: [
-            {
-                title: "Github",
-                init: {
-                    ...defaultInit,
-                },
-                type: "github",
-            }
-        ]
-    },
-    {
-        title: "Services",
-        items: [
-            {
-                title: "Service",
-                init: {
-                    ...defaultInit,
-                },
-                type: "app",
-            }
-        ],
-    },
-    {
-        title: "Storage",
-        items: [
-            {
-                title: "File system",
-                init: {
-                    ...defaultInit,
-                },
-                type: "volume",
-            },
-            {
-                title: "PostgreSQL",
-                init: {
-                    ...defaultInit,
-                    ports: [{
-                        id: "connection",
-                        name: "connection",
-                        value: 5432,
-                    }],
-                },
-                type: "postgresql",
-            },
-            {
-                title: "MongoDB",
-                init: {
-                    ...defaultInit,
-                    ports: [{
-                        id: "connection",
-                        name: "connection",
-                        value: 27017,
-                    }],
-                },
-                type: "mongodb",
-            },
-        ],
-    },
-    {
-        title: "Gateways",
-        items: [
-            {
-                title: "HTTPS",
-                init: {
-                    ...defaultInit,
-                },
-                type: "gateway-https",
-            },
-            {
-                title: "TCP",
-                init: {
-                    ...defaultInit,
-                },
-                type: "gateway-tcp",
-            },
-        ],
-    },
-];
\ No newline at end of file
+	{
+		title: "Repository",
+		items: [
+			{
+				title: "Github",
+				init: {
+					...defaultInit,
+				},
+				type: "github",
+			},
+		],
+	},
+	{
+		title: "Services",
+		items: [
+			{
+				title: "Service",
+				init: {
+					...defaultInit,
+				},
+				type: "app",
+			},
+		],
+	},
+	{
+		title: "Storage",
+		items: [
+			{
+				title: "File system",
+				init: {
+					...defaultInit,
+				},
+				type: "volume",
+			},
+			{
+				title: "PostgreSQL",
+				init: {
+					...defaultInit,
+					ports: [
+						{
+							id: "connection",
+							name: "connection",
+							value: 5432,
+						},
+					],
+				},
+				type: "postgresql",
+			},
+			{
+				title: "MongoDB",
+				init: {
+					...defaultInit,
+					ports: [
+						{
+							id: "connection",
+							name: "connection",
+							value: 27017,
+						},
+					],
+				},
+				type: "mongodb",
+			},
+		],
+	},
+	{
+		title: "Gateways",
+		items: [
+			{
+				title: "HTTPS",
+				init: {
+					...defaultInit,
+				},
+				type: "gateway-https",
+			},
+			{
+				title: "TCP",
+				init: {
+					...defaultInit,
+				},
+				type: "gateway-tcp",
+			},
+		],
+	},
+];
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 3ea29b6..ea7f892 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -1,394 +1,525 @@
 import { AppNode, Env, GatewayHttpsNode, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
 
 export type AuthDisabled = {
-    enabled: false;
+	enabled: false;
 };
 
 export type AuthEnabled = {
-    enabled: true;
-    groups: string[];
-    noAuthPathPatterns: string[];
+	enabled: true;
+	groups: string[];
+	noAuthPathPatterns: string[];
 };
 
 export type Auth = AuthDisabled | AuthEnabled;
 
 export type Ingress = {
-    network: string;
-    subdomain: string;
-    port: { name: string; } | { value: string; };
-    auth: Auth;
+	network: string;
+	subdomain: string;
+	port: { name: string } | { value: string };
+	auth: Auth;
 };
 
 export type Domain = {
-    network: string;
-    subdomain: string;
+	network: string;
+	subdomain: string;
 };
 
-export type PortValue = {
-    name: string;
-} | {
-    value: number;
-};
+export type PortValue =
+	| {
+			name: string;
+	  }
+	| {
+			value: number;
+	  };
 
 export type PortDomain = Domain & {
-    port: PortValue;
-}
+	port: PortValue;
+};
 
 export type Service = {
-    type: ServiceType;
-    name: string;
-    source: {
-        repository: string;
-        branch: string;
-        rootDir: string;
-    };
-    ports?: {
-        name: string;
-        value: number;
-        protocol: "TCP" | "UDP";
-    }[];
-    env?: {
-        name: string;
-        alias?: string;
-    }[]
-    ingress?: Ingress[];
-    expose?: PortDomain[];
-    volume?: string[];
-    preBuildCommands?: { bin: string }[];
+	type: ServiceType;
+	name: string;
+	source: {
+		repository: string;
+		branch: string;
+		rootDir: string;
+	};
+	ports?: {
+		name: string;
+		value: number;
+		protocol: "TCP" | "UDP";
+	}[];
+	env?: {
+		name: string;
+		alias?: string;
+	}[];
+	ingress?: Ingress[];
+	expose?: PortDomain[];
+	volume?: string[];
+	preBuildCommands?: { bin: string }[];
 };
 
 export type Volume = {
-    name: string;
-    accessMode: VolumeType;
-    size: string;
+	name: string;
+	accessMode: VolumeType;
+	size: string;
 };
 
 export type PostgreSQL = {
-    name: string;
-    size: string;
-    expose?: Domain[];
+	name: string;
+	size: string;
+	expose?: Domain[];
 };
 
 export type MongoDB = {
-    name: string;
-    size: string;
-    expose?: Domain[];
+	name: string;
+	size: string;
+	expose?: Domain[];
 };
 
 export type Config = {
-    service?: Service[];
-    volume?: Volume[];
-    postgresql?: PostgreSQL[];
-    mongodb?: MongoDB[];
+	service?: Service[];
+	volume?: Volume[];
+	postgresql?: PostgreSQL[];
+	mongodb?: MongoDB[];
 };
 
 export function generateDodoConfig(nodes: AppNode[], env: Env): Config | null {
-    try {
-        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);
-        const findExpose = (n: AppNode): PortDomain[] => {
-            return n.data.ports.map((p) => [n.id, p.id, p.name]).flatMap((sp) => {
-                return tcpNodes.flatMap((i) => (i.data.exposed || []).filter((t) => t.serviceId === sp[0] && t.portId === sp[1]).map(() => ({
-                    network: networkMap.get(i.data.network!)!,
-                    subdomain: i.data.subdomain!,
-                    port: { name: sp[2] },
-                })));
-            });
-        };
-        return {
-            service: nodes.filter((n) => n.type === "app").map((n): Service => {
-                return {
-                    type: n.data.type,
-                    name: n.data.label,
-                    source: {
-                        repository: nodes.filter((i) => i.type === "github").find((i) => i.id === n.data.repository.id)!.data.repository!.sshURL,
-                        branch: n.data.repository.branch,
-                        rootDir: n.data.repository.rootDir,
-                    },
-                    ports: (n.data.ports || []).map((p) => ({
-                        name: p.name,
-                        value: p.value,
-                        protocol: "TCP", // TODO(gio)
-                    })),
-                    env: (n.data.envVars || []).filter((e) => "name" in e).map((e) => ({
-                        name: e.name,
-                        alias: "alias" in e ? e.alias : undefined,
-                    })),
-                    ingress: ingressNodes.filter((i) => i.data.https!.serviceId === n.id).map((i: GatewayHttpsNode): Ingress => ({
-                        network: networkMap.get(i.data.network!)!,
-                        subdomain: i.data.subdomain!,
-                        port: {
-                            name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
-                        },
-                        auth: (i.data.auth?.enabled || false ? {
-                            enabled: true,
-                            groups: i.data.auth!.groups,
-                            noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
-                        } : {
-                            enabled: false,
-                        }),
-                    })),
-                    expose: findExpose(n),
-                    preBuildCommands: n.data.preBuildCommands ? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd })) : [],
-                };
-            }),
-            volume: nodes.filter((n) => n.type === "volume").map((n): Volume => ({
-                name: n.data.label,
-                accessMode: n.data.type,
-                size: n.data.size,
-            })),
-            postgresql: nodes.filter((n) => n.type === "postgresql").map((n): PostgreSQL => ({
-                name: n.data.label,
-                size: "1Gi", // TODO(gio)
-                expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
-            })),
-            mongodb: nodes.filter((n) => n.type === "mongodb").map((n): MongoDB => ({
-                name: n.data.label,
-                size: "1Gi", // TODO(gio)
-                expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
-            })),
-        };
-    } catch (e) {
-        console.log(e);
-        return null;
-    }
+	try {
+		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);
+		const findExpose = (n: AppNode): PortDomain[] => {
+			return n.data.ports
+				.map((p) => [n.id, p.id, p.name])
+				.flatMap((sp) => {
+					return tcpNodes.flatMap((i) =>
+						(i.data.exposed || [])
+							.filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
+							.map(() => ({
+								network: networkMap.get(i.data.network!)!,
+								subdomain: i.data.subdomain!,
+								port: { name: sp[2] },
+							})),
+					);
+				});
+		};
+		return {
+			service: nodes
+				.filter((n) => n.type === "app")
+				.map((n): Service => {
+					return {
+						type: n.data.type,
+						name: n.data.label,
+						source: {
+							repository: nodes
+								.filter((i) => i.type === "github")
+								.find((i) => i.id === n.data.repository.id)!.data.repository!.sshURL,
+							branch: n.data.repository.branch,
+							rootDir: n.data.repository.rootDir,
+						},
+						ports: (n.data.ports || []).map((p) => ({
+							name: p.name,
+							value: p.value,
+							protocol: "TCP", // TODO(gio)
+						})),
+						env: (n.data.envVars || [])
+							.filter((e) => "name" in e)
+							.map((e) => ({
+								name: e.name,
+								alias: "alias" in e ? e.alias : undefined,
+							})),
+						ingress: ingressNodes
+							.filter((i) => i.data.https!.serviceId === n.id)
+							.map(
+								(i: GatewayHttpsNode): Ingress => ({
+									network: networkMap.get(i.data.network!)!,
+									subdomain: i.data.subdomain!,
+									port: {
+										name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
+									},
+									auth:
+										i.data.auth?.enabled || false
+											? {
+													enabled: true,
+													groups: i.data.auth!.groups,
+													noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
+												}
+											: {
+													enabled: false,
+												},
+								}),
+							),
+						expose: findExpose(n),
+						preBuildCommands: n.data.preBuildCommands
+							? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
+							: [],
+					};
+				}),
+			volume: nodes
+				.filter((n) => n.type === "volume")
+				.map(
+					(n): Volume => ({
+						name: n.data.label,
+						accessMode: n.data.type,
+						size: n.data.size,
+					}),
+				),
+			postgresql: nodes
+				.filter((n) => n.type === "postgresql")
+				.map(
+					(n): PostgreSQL => ({
+						name: n.data.label,
+						size: "1Gi", // TODO(gio)
+						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
+					}),
+				),
+			mongodb: nodes
+				.filter((n) => n.type === "mongodb")
+				.map(
+					(n): MongoDB => ({
+						name: n.data.label,
+						size: "1Gi", // TODO(gio)
+						expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
+					}),
+				),
+		};
+	} catch (e) {
+		console.log(e);
+		return null;
+	}
 }
 
 export interface Validator {
-    (nodes: AppNode[]): Message[];
+	(nodes: AppNode[]): Message[];
 }
 
 function CombineValidators(...v: Validator[]): Validator {
-    return (n) => v.flatMap((v) => v(n));
+	return (n) => v.flatMap((v) => v(n));
 }
 
 function MessageTypeToNumber(t: MessageType) {
-    switch (t) {
-        case "FATAL": return 0;
-        case "WARNING": return 1;
-        case "INFO": return 2;
-    }
+	switch (t) {
+		case "FATAL":
+			return 0;
+		case "WARNING":
+			return 1;
+		case "INFO":
+			return 2;
+	}
 }
 
 function NodeTypeToNumber(t?: NodeType) {
-    switch (t) {
-        case "github": return 0;
-        case "app": return 1;
-        case "volume": return 2;
-        case "postgresql": return 3;
-        case "mongodb": return 4;
-        case "gateway-https": return 5;
-        case undefined: return 100;
-    }
+	switch (t) {
+		case "github":
+			return 0;
+		case "app":
+			return 1;
+		case "volume":
+			return 2;
+		case "postgresql":
+			return 3;
+		case "mongodb":
+			return 4;
+		case "gateway-https":
+			return 5;
+		case undefined:
+			return 100;
+	}
 }
 
 function SortingValidator(v: Validator): Validator {
-    return (n) => {
-        const nt = new Map(n.map((n) => [n.id, NodeTypeToNumber(n.type)]))
-        return v(n).sort((a, b) => {
-            const at = MessageTypeToNumber(a.type);
-            const bt = MessageTypeToNumber(b.type);
-            if (a.nodeId === undefined && b.nodeId === undefined) {
-                if (at !== bt) {
-                    return at - bt;
-                }
-                return a.id.localeCompare(b.id);
-            }
-            if (a.nodeId === undefined) {
-                return -1;
-            }
-            if (b.nodeId === undefined) {
-                return 1;
-            }
-            if (a.nodeId === b.nodeId) {
-                if (at !== bt) {
-                    return at - bt;
-                }
-                return a.id.localeCompare(b.id);
-            }
-            const ant = nt.get(a.id)!;
-            const bnt = nt.get(b.id)!;
-            if (ant !== bnt) {
-                return ant - bnt;
-            }
-            return a.id.localeCompare(b.id);
-        });
-    };
+	return (n) => {
+		const nt = new Map(n.map((n) => [n.id, NodeTypeToNumber(n.type)]));
+		return v(n).sort((a, b) => {
+			const at = MessageTypeToNumber(a.type);
+			const bt = MessageTypeToNumber(b.type);
+			if (a.nodeId === undefined && b.nodeId === undefined) {
+				if (at !== bt) {
+					return at - bt;
+				}
+				return a.id.localeCompare(b.id);
+			}
+			if (a.nodeId === undefined) {
+				return -1;
+			}
+			if (b.nodeId === undefined) {
+				return 1;
+			}
+			if (a.nodeId === b.nodeId) {
+				if (at !== bt) {
+					return at - bt;
+				}
+				return a.id.localeCompare(b.id);
+			}
+			const ant = nt.get(a.id)!;
+			const bnt = nt.get(b.id)!;
+			if (ant !== bnt) {
+				return ant - bnt;
+			}
+			return a.id.localeCompare(b.id);
+		});
+	};
 }
 
 export function CreateValidators(): Validator {
-    return SortingValidator(
-        CombineValidators(
-            EmptyValidator,
-            GitRepositoryValidator,
-            ServiceValidator,
-            GatewayHTTPSValidator,
-            GatewayTCPValidator,
-        )
-    );
+	return SortingValidator(
+		CombineValidators(
+			EmptyValidator,
+			GitRepositoryValidator,
+			ServiceValidator,
+			GatewayHTTPSValidator,
+			GatewayTCPValidator,
+		),
+	);
 }
 
 function EmptyValidator(nodes: AppNode[]): Message[] {
-    nodes = nodes.filter((n) => n.type !== "network");
-    if (nodes.length > 0) {
-        return [];
-    }
-    return [{
-        id: "no-nodes",
-        type: "FATAL",
-        message: "Start by importing application source code",
-        onHighlight: (store) => store.setHighlightCategory("repository", true),
-        onLooseHighlight: (store) => store.setHighlightCategory("repository", false),
-    }];
+	nodes = nodes.filter((n) => n.type !== "network");
+	if (nodes.length > 0) {
+		return [];
+	}
+	return [
+		{
+			id: "no-nodes",
+			type: "FATAL",
+			message: "Start by importing application source code",
+			onHighlight: (store) => store.setHighlightCategory("repository", true),
+			onLooseHighlight: (store) => store.setHighlightCategory("repository", false),
+		},
+	];
 }
 
 function GitRepositoryValidator(nodes: AppNode[]): Message[] {
-    const git = nodes.filter((n) => n.type === "github");
-    const noAddress: Message[] = git.filter((n) => n.data == null || n.data.repository == null).map((n) => ({
-        id: `${n.id}-no-address`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Configure repository address",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-    } satisfies Message));
-    const noApp = git.filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id)).map((n) => ({
-        id: `${n.id}-no-app`,
-        type: "WARNING",
-        nodeId: n.id,
-        message: "Connect to service",
-        onHighlight: (store) => store.setHighlightCategory("Services", true),
-        onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
-    } satisfies Message));
-    return noAddress.concat(noApp);
+	const git = nodes.filter((n) => n.type === "github");
+	const noAddress: Message[] = git
+		.filter((n) => n.data == null || n.data.repository == null)
+		.map(
+			(n) =>
+				({
+					id: `${n.id}-no-address`,
+					type: "FATAL",
+					nodeId: n.id,
+					message: "Configure repository address",
+					onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+					onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+				}) satisfies Message,
+		);
+	const noApp = git
+		.filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id))
+		.map(
+			(n) =>
+				({
+					id: `${n.id}-no-app`,
+					type: "WARNING",
+					nodeId: n.id,
+					message: "Connect to service",
+					onHighlight: (store) => store.setHighlightCategory("Services", true),
+					onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
+				}) satisfies Message,
+		);
+	return noAddress.concat(noApp);
 }
 
 function ServiceValidator(nodes: AppNode[]): Message[] {
-    const apps = nodes.filter((n) => n.type === "app");
-    const noName = apps.filter((n) => n.data == null || n.data.label == null || n.data.label === "").map((n): Message => ({
-        id: `${n.id}-no-name`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Name the service",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-        onClick: (store) => {
-            store.updateNode(n.id, { selected: true });
-            store.updateNodeData<"app">(n.id, {
-                activeField: "name",
-            });
-        },
-    }));
-    const noSource = apps.filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "").map((n): Message => ({
-        id: `${n.id}-no-repo`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Connect to source repository",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-    }));
-    const noRuntime = apps.filter((n) => n.data == null || n.data.type == null).map((n): Message => ({
-        id: `${n.id}-no-runtime`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Choose runtime",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-        onClick: (store) => {
-            store.updateNode(n.id, { selected: true });
-            store.updateNodeData<"app">(n.id, {
-                activeField: "type",
-            });
-        },
-    }));
-    const noPorts = apps.filter((n) => n.data == null || n.data.ports == null || n.data.ports.length === 0).map((n): Message => ({
-        id: `${n.id}-no-ports`,
-        type: "INFO",
-        nodeId: n.id,
-        message: "Expose ports",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-    }));
-    const noIngress = apps.flatMap((n): Message[] => {
-        if (n.data == null) {
-            return [];
-        }
-        return (n.data.ports || []).filter((p) => !nodes.filter((i) => i.type === "gateway-https").some((i) => {
-            if (i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id) {
-                return true;
-            }
-            return false;
-        })).map((p): Message => ({
-            id: `${n.id}-${p.id}-no-ingress`,
-            type: "WARNING",
-            nodeId: n.id,
-            message: `Connect to ingress: ${p.name} - ${p.value}`,
-            onHighlight: (store) => {
-                store.updateNode(n.id, { selected: true });
-                store.setHighlightCategory("gateways", true);
-            },
-            onLooseHighlight: (store) => {
-                store.updateNode(n.id, { selected: false });
-                store.setHighlightCategory("gateways", false);
-            },
-        }));
-    });
-    const multipleIngress = apps.filter((n) => n.data != null && n.data.ports != null).flatMap((n) => n.data.ports.map((p): Message | undefined => {
-        const ing = nodes.filter((i) => i.type === "gateway-https").filter((i) => i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id);
-        if (ing.length < 2) {
-            return undefined;
-        }
-        return {
-            id: `${n.id}-${p.id}-multiple-ingress`,
-            type: "FATAL",
-            nodeId: n.id,
-            message: `Can not expose same port using multiple ingresses: ${p.name} - ${p.value}`,
-            onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-            onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-        };
-    })).filter((m) => m !== undefined);
-    return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
+	const apps = nodes.filter((n) => n.type === "app");
+	const noName = apps
+		.filter((n) => n.data == null || n.data.label == null || n.data.label === "")
+		.map(
+			(n): Message => ({
+				id: `${n.id}-no-name`,
+				type: "FATAL",
+				nodeId: n.id,
+				message: "Name the service",
+				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+				onClick: (store) => {
+					store.updateNode(n.id, { selected: true });
+					store.updateNodeData<"app">(n.id, {
+						activeField: "name",
+					});
+				},
+			}),
+		);
+	const noSource = apps
+		.filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "")
+		.map(
+			(n): Message => ({
+				id: `${n.id}-no-repo`,
+				type: "FATAL",
+				nodeId: n.id,
+				message: "Connect to source repository",
+				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+			}),
+		);
+	const noRuntime = apps
+		.filter((n) => n.data == null || n.data.type == null)
+		.map(
+			(n): Message => ({
+				id: `${n.id}-no-runtime`,
+				type: "FATAL",
+				nodeId: n.id,
+				message: "Choose runtime",
+				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+				onClick: (store) => {
+					store.updateNode(n.id, { selected: true });
+					store.updateNodeData<"app">(n.id, {
+						activeField: "type",
+					});
+				},
+			}),
+		);
+	const noPorts = apps
+		.filter((n) => n.data == null || n.data.ports == null || n.data.ports.length === 0)
+		.map(
+			(n): Message => ({
+				id: `${n.id}-no-ports`,
+				type: "INFO",
+				nodeId: n.id,
+				message: "Expose ports",
+				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+			}),
+		);
+	const noIngress = apps.flatMap((n): Message[] => {
+		if (n.data == null) {
+			return [];
+		}
+		return (n.data.ports || [])
+			.filter(
+				(p) =>
+					!nodes
+						.filter((i) => i.type === "gateway-https")
+						.some((i) => {
+							if (
+								i.data &&
+								i.data.https &&
+								i.data.https.serviceId === n.id &&
+								i.data.https.portId === p.id
+							) {
+								return true;
+							}
+							return false;
+						}),
+			)
+			.map(
+				(p): Message => ({
+					id: `${n.id}-${p.id}-no-ingress`,
+					type: "WARNING",
+					nodeId: n.id,
+					message: `Connect to ingress: ${p.name} - ${p.value}`,
+					onHighlight: (store) => {
+						store.updateNode(n.id, { selected: true });
+						store.setHighlightCategory("gateways", true);
+					},
+					onLooseHighlight: (store) => {
+						store.updateNode(n.id, { selected: false });
+						store.setHighlightCategory("gateways", false);
+					},
+				}),
+			);
+	});
+	const multipleIngress = apps
+		.filter((n) => n.data != null && n.data.ports != null)
+		.flatMap((n) =>
+			n.data.ports.map((p): Message | undefined => {
+				const ing = nodes
+					.filter((i) => i.type === "gateway-https")
+					.filter(
+						(i) =>
+							i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id,
+					);
+				if (ing.length < 2) {
+					return undefined;
+				}
+				return {
+					id: `${n.id}-${p.id}-multiple-ingress`,
+					type: "FATAL",
+					nodeId: n.id,
+					message: `Can not expose same port using multiple ingresses: ${p.name} - ${p.value}`,
+					onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+					onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+				};
+			}),
+		)
+		.filter((m) => m !== undefined);
+	return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
 }
 
 function GatewayHTTPSValidator(nodes: AppNode[]): Message[] {
-    const ing = nodes.filter((n) => n.type === "gateway-https");
-    const noNetwork: Message[] = ing.filter((n) => n.data == null || n.data.network == null || n.data.network == "" || n.data.subdomain == null || n.data.subdomain == "").map((n): Message => ({
-        id: `${n.id}-no-network`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Network and subdomain must be defined",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-    }));
-    const notConnected: Message[] = ing.filter((n) => n.data == null || n.data.https == null || n.data.https.serviceId == null || n.data.https.serviceId == "" || n.data.https.portId == null || n.data.https.portId == "").map((n) => ({
-        id: `${n.id}-not-connected`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Connect to a service port",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-    }));
-    return noNetwork.concat(notConnected);
+	const ing = nodes.filter((n) => n.type === "gateway-https");
+	const noNetwork: Message[] = ing
+		.filter(
+			(n) =>
+				n.data == null ||
+				n.data.network == null ||
+				n.data.network == "" ||
+				n.data.subdomain == null ||
+				n.data.subdomain == "",
+		)
+		.map(
+			(n): Message => ({
+				id: `${n.id}-no-network`,
+				type: "FATAL",
+				nodeId: n.id,
+				message: "Network and subdomain must be defined",
+				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+			}),
+		);
+	const notConnected: Message[] = ing
+		.filter(
+			(n) =>
+				n.data == null ||
+				n.data.https == null ||
+				n.data.https.serviceId == null ||
+				n.data.https.serviceId == "" ||
+				n.data.https.portId == null ||
+				n.data.https.portId == "",
+		)
+		.map((n) => ({
+			id: `${n.id}-not-connected`,
+			type: "FATAL",
+			nodeId: n.id,
+			message: "Connect to a service port",
+			onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+			onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+		}));
+	return noNetwork.concat(notConnected);
 }
 
 function GatewayTCPValidator(nodes: AppNode[]): Message[] {
-    const ing = nodes.filter((n) => n.type === "gateway-tcp");
-    const noNetwork: Message[] = ing.filter((n) => n.data == null || n.data.network == null || n.data.network == "" || n.data.subdomain == null || n.data.subdomain == "").map((n): Message => ({
-        id: `${n.id}-no-network`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Network and subdomain must be defined",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-    }));
-    const notConnected: Message[] = ing.filter((n) => n.data == null || n.data.exposed == null || n.data.exposed.length === 0).map((n) => ({
-        id: `${n.id}-not-connected`,
-        type: "FATAL",
-        nodeId: n.id,
-        message: "Connect to a service port",
-        onHighlight: (store) => store.updateNode(n.id, { selected: true }),
-        onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
-    }));
-    return noNetwork.concat(notConnected);
+	const ing = nodes.filter((n) => n.type === "gateway-tcp");
+	const noNetwork: Message[] = ing
+		.filter(
+			(n) =>
+				n.data == null ||
+				n.data.network == null ||
+				n.data.network == "" ||
+				n.data.subdomain == null ||
+				n.data.subdomain == "",
+		)
+		.map(
+			(n): Message => ({
+				id: `${n.id}-no-network`,
+				type: "FATAL",
+				nodeId: n.id,
+				message: "Network and subdomain must be defined",
+				onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+			}),
+		);
+	const notConnected: Message[] = ing
+		.filter((n) => n.data == null || n.data.exposed == null || n.data.exposed.length === 0)
+		.map((n) => ({
+			id: `${n.id}-not-connected`,
+			type: "FATAL",
+			nodeId: n.id,
+			message: "Connect to a service port",
+			onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+			onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+		}));
+	return noNetwork.concat(notConnected);
 }
diff --git a/apps/canvas/front/src/lib/github.ts b/apps/canvas/front/src/lib/github.ts
index 8537671..b1732a0 100644
--- a/apps/canvas/front/src/lib/github.ts
+++ b/apps/canvas/front/src/lib/github.ts
@@ -1,36 +1,36 @@
 export interface GitHubRepository {
-    id: number;
-    name: string;
-    full_name: string;
-    html_url: string;
-    ssh_url: string;
-    description: string | null;
-    private: boolean;
-    default_branch: string;
+	id: number;
+	name: string;
+	full_name: string;
+	html_url: string;
+	ssh_url: string;
+	description: string | null;
+	private: boolean;
+	default_branch: string;
 }
 
 export interface GitHubService {
-    /**
-     * Fetches a list of repositories for the authenticated user
-     * @returns Promise resolving to an array of GitHub repositories
-     */
-    getRepositories(): Promise<GitHubRepository[]>;
+	/**
+	 * Fetches a list of repositories for the authenticated user
+	 * @returns Promise resolving to an array of GitHub repositories
+	 */
+	getRepositories(): Promise<GitHubRepository[]>;
 }
 
 export class GitHubServiceImpl implements GitHubService {
-    private projectId: string;
+	private projectId: string;
 
-    constructor(projectId: string) {
-        this.projectId = projectId;
-    }
+	constructor(projectId: string) {
+		this.projectId = projectId;
+	}
 
-    async getRepositories(): Promise<GitHubRepository[]> {
-        const response = await fetch(`/api/project/${this.projectId}/repos/github`);
+	async getRepositories(): Promise<GitHubRepository[]> {
+		const response = await fetch(`/api/project/${this.projectId}/repos/github`);
 
-        if (!response.ok) {
-            throw new Error(`Failed to fetch repositories: ${response.statusText}`);
-        }
+		if (!response.ok) {
+			throw new Error(`Failed to fetch repositories: ${response.statusText}`);
+		}
 
-        return response.json();
-    }
-} 
\ No newline at end of file
+		return response.json();
+	}
+}
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 2492642..0749fbb 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,275 +1,302 @@
-import { v4 as uuidv4 } from "uuid";
-import { create } from 'zustand';
-import { addEdge, applyNodeChanges, applyEdgeChanges, Connection, EdgeChange, useNodes } from '@xyflow/react';
-import type {
-  Edge,
-  Node,
-  OnNodesChange,
-  OnEdgesChange,
-  OnConnect,
-} from '@xyflow/react';
-import type { DeepPartial } from "react-hook-form";
 import { Category, defaultCategories } from "./categories";
 import { CreateValidators, Validator } from "./config";
+import { GitHubService, GitHubServiceImpl } from "./github";
+import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange } from "@xyflow/react";
+import { addEdge, applyEdgeChanges, applyNodeChanges, Connection, EdgeChange, useNodes } from "@xyflow/react";
+import type { DeepPartial } from "react-hook-form";
+import { v4 as uuidv4 } from "uuid";
 import { z } from "zod";
-import { GitHubService, GitHubServiceImpl } from './github';
+import { create } from "zustand";
 
 export type InitData = {
-  label: string;
-  envVars: BoundEnvVar[];
-  ports: Port[];
+	label: string;
+	envVars: BoundEnvVar[];
+	ports: Port[];
 };
 
 export type NodeData = InitData & {
-  activeField?: string | undefined;
-  state: string | null;
+	activeField?: string | undefined;
+	state: string | null;
 };
 
 export type PortConnectedTo = {
-  serviceId: string;
-  portId: string;
-}
+	serviceId: string;
+	portId: string;
+};
 
 export type NetworkData = NodeData & {
-  domain: string;
+	domain: string;
 };
 
 export type NetworkNode = Node<NetworkData> & {
-  type: "network";
+	type: "network";
 };
 
 export type GatewayHttpsData = NodeData & {
-  network?: string;
-  subdomain?: string;
-  https?: PortConnectedTo;
-  auth?: {
-    enabled: boolean;
-    groups: string[];
-    noAuthPathPatterns: string[];
-  }
+	network?: string;
+	subdomain?: string;
+	https?: PortConnectedTo;
+	auth?: {
+		enabled: boolean;
+		groups: string[];
+		noAuthPathPatterns: string[];
+	};
 };
 
 export type GatewayHttpsNode = Node<GatewayHttpsData> & {
-  type: "gateway-https";
+	type: "gateway-https";
 };
 
 export type GatewayTCPData = NodeData & {
-  network?: string;
-  subdomain?: string;
-  exposed: PortConnectedTo[];
-  selected?: {
-    serviceId?: string;
-    portId?: string;
-  };
+	network?: string;
+	subdomain?: string;
+	exposed: PortConnectedTo[];
+	selected?: {
+		serviceId?: string;
+		portId?: string;
+	};
 };
 
 export type GatewayTCPNode = Node<GatewayTCPData> & {
-  type: "gateway-tcp";
+	type: "gateway-tcp";
 };
 
 export type Port = {
-  id: string;
-  name: string;
-  value: number;
+	id: string;
+	name: string;
+	value: number;
 };
 
 export const ServiceTypes = [
-  "deno:2.2.0",
-  "golang:1.20.0",
-  "golang:1.22.0",
-  "golang:1.24.0",
-  "hugo:latest",
-  "php:8.2-apache",
-  "nextjs:deno-2.0.0",
-  "node-23.1.0"
+	"deno:2.2.0",
+	"golang:1.20.0",
+	"golang:1.22.0",
+	"golang:1.24.0",
+	"hugo:latest",
+	"php:8.2-apache",
+	"nextjs:deno-2.0.0",
+	"node-23.1.0",
 ] as const;
-export type ServiceType = typeof ServiceTypes[number];
+export type ServiceType = (typeof ServiceTypes)[number];
 
 export type ServiceData = NodeData & {
-  type: ServiceType;
-  repository: {
-    id: string;
-  } | {
-    id: string;
-    branch: string;
-  } | {
-    id: string;
-    branch: string;
-    rootDir: string;
-  };
-  env: string[];
-  volume: string[];
-  preBuildCommands: string;
-  isChoosingPortToConnect: boolean;
+	type: ServiceType;
+	repository:
+		| {
+				id: string;
+		  }
+		| {
+				id: string;
+				branch: string;
+		  }
+		| {
+				id: string;
+				branch: string;
+				rootDir: string;
+		  };
+	env: string[];
+	volume: string[];
+	preBuildCommands: string;
+	isChoosingPortToConnect: boolean;
 };
 
 export type ServiceNode = Node<ServiceData> & {
-  type: "app";
+	type: "app";
 };
 
 export type VolumeType = "ReadWriteOnce" | "ReadOnlyMany" | "ReadWriteMany" | "ReadWriteOncePod";
 
 export type VolumeData = NodeData & {
-  type: VolumeType;
-  size: string;
-  attachedTo: string[];
+	type: VolumeType;
+	size: string;
+	attachedTo: string[];
 };
 
 export type VolumeNode = Node<VolumeData> & {
-  type: "volume";
+	type: "volume";
 };
 
 export type PostgreSQLData = NodeData & {
-  volumeId: string;
+	volumeId: string;
 };
 
 export type PostgreSQLNode = Node<PostgreSQLData> & {
-  type: "postgresql";
+	type: "postgresql";
 };
 
 export type MongoDBData = NodeData & {
-  volumeId: string;
+	volumeId: string;
 };
 
 export type MongoDBNode = Node<MongoDBData> & {
-  type: "mongodb";
+	type: "mongodb";
 };
 
 export type GithubData = NodeData & {
-  repository?: {
-    id: number;
-    sshURL: string;
-  };
+	repository?: {
+		id: number;
+		sshURL: string;
+	};
 };
 
 export type GithubNode = Node<GithubData> & {
-  type: "github";
+	type: "github";
 };
 
 export type NANode = Node<NodeData> & {
-  type: undefined;
+	type: undefined;
 };
 
-export type AppNode = NetworkNode | GatewayHttpsNode | GatewayTCPNode | ServiceNode | VolumeNode | PostgreSQLNode | MongoDBNode | GithubNode | NANode;
+export type AppNode =
+	| NetworkNode
+	| GatewayHttpsNode
+	| GatewayTCPNode
+	| ServiceNode
+	| VolumeNode
+	| PostgreSQLNode
+	| MongoDBNode
+	| GithubNode
+	| NANode;
 
 export function nodeLabel(n: AppNode): string {
-  switch (n.type) {
-    case "network": return n.data.domain;
-    case "app": return n.data.label || "Service";
-    case "github": return n.data.repository?.sshURL || "Github";
-    case "gateway-https": {
-      if (n.data && n.data.network && n.data.subdomain) {
-        return `https://${n.data.subdomain}.${n.data.network}`;
-      } else {
-        return "HTTPS Gateway";
-      }
-    }
-    case "gateway-tcp": {
-      if (n.data && n.data.network && n.data.subdomain) {
-        return `${n.data.subdomain}.${n.data.network}`;
-      } else {
-        return "TCP Gateway";
-      }
-    }
-    case "mongodb": return n.data.label || "MongoDB";
-    case "postgresql": return n.data.label || "PostgreSQL";
-    case "volume": return n.data.label || "Volume";
-    case undefined: throw new Error("MUST NOT REACH!");
-  }
+	switch (n.type) {
+		case "network":
+			return n.data.domain;
+		case "app":
+			return n.data.label || "Service";
+		case "github":
+			return n.data.repository?.sshURL || "Github";
+		case "gateway-https": {
+			if (n.data && n.data.network && n.data.subdomain) {
+				return `https://${n.data.subdomain}.${n.data.network}`;
+			} else {
+				return "HTTPS Gateway";
+			}
+		}
+		case "gateway-tcp": {
+			if (n.data && n.data.network && n.data.subdomain) {
+				return `${n.data.subdomain}.${n.data.network}`;
+			} else {
+				return "TCP Gateway";
+			}
+		}
+		case "mongodb":
+			return n.data.label || "MongoDB";
+		case "postgresql":
+			return n.data.label || "PostgreSQL";
+		case "volume":
+			return n.data.label || "Volume";
+		case undefined:
+			throw new Error("MUST NOT REACH!");
+	}
 }
 
 export function nodeIsConnectable(n: AppNode, handle: string): boolean {
-  switch (n.type) {
-    case "network":
-      return true;
-    case "app":
-      if (handle === "ports") {
-        return n.data !== undefined && n.data.ports !== undefined && n.data.ports.length > 0;
-      } else if (handle === "repository") {
-        if (!n.data || !n.data.repository || !n.data.repository.id) {
-          return true;
-        }
-        return false;
-      }
-      return false;
-    case "github":
-      if (n.data.repository?.id !== undefined) {
-        return true;
-      }
-      return false;
-    case "gateway-https":
-      if (handle === "subdomain") {
-        return n.data.network === undefined;
-      }
-      return n.data === undefined || n.data.https === undefined;
-    case "gateway-tcp":
-      if (handle === "subdomain") {
-        return n.data.network === undefined;
-      }
-      return true;
-    case "mongodb":
-      return true;
-    case "postgresql":
-      return true;
-    case "volume":
-      if (n.data === undefined || n.data.type === undefined) {
-        return false;
-      }
-      if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
-        return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
-      }
-      return true;
-    case undefined: throw new Error("MUST NOT REACH!");
-  }
+	switch (n.type) {
+		case "network":
+			return true;
+		case "app":
+			if (handle === "ports") {
+				return n.data !== undefined && n.data.ports !== undefined && n.data.ports.length > 0;
+			} else if (handle === "repository") {
+				if (!n.data || !n.data.repository || !n.data.repository.id) {
+					return true;
+				}
+				return false;
+			}
+			return false;
+		case "github":
+			if (n.data.repository?.id !== undefined) {
+				return true;
+			}
+			return false;
+		case "gateway-https":
+			if (handle === "subdomain") {
+				return n.data.network === undefined;
+			}
+			return n.data === undefined || n.data.https === undefined;
+		case "gateway-tcp":
+			if (handle === "subdomain") {
+				return n.data.network === undefined;
+			}
+			return true;
+		case "mongodb":
+			return true;
+		case "postgresql":
+			return true;
+		case "volume":
+			if (n.data === undefined || n.data.type === undefined) {
+				return false;
+			}
+			if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
+				return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
+			}
+			return true;
+		case undefined:
+			throw new Error("MUST NOT REACH!");
+	}
 }
 
-export type BoundEnvVar = {
-  id: string;
-  source: string | null;
-} | {
-  id: string;
-  source: string | null;
-  name: string;
-  isEditting: boolean;
-} | {
-  id: string;
-  source: string | null;
-  name: string;
-  alias: string;
-  isEditting: boolean;
-} | {
-  id: string;
-  source: string | null;
-  portId: string;
-  name: string;
-  alias: string;
-  isEditting: boolean;
-};
+export type BoundEnvVar =
+	| {
+			id: string;
+			source: string | null;
+	  }
+	| {
+			id: string;
+			source: string | null;
+			name: string;
+			isEditting: boolean;
+	  }
+	| {
+			id: string;
+			source: string | null;
+			name: string;
+			alias: string;
+			isEditting: boolean;
+	  }
+	| {
+			id: string;
+			source: string | null;
+			portId: string;
+			name: string;
+			alias: string;
+			isEditting: boolean;
+	  };
 
 export type EnvVar = {
-  name: string;
-  value: string;
+	name: string;
+	value: string;
 };
 
 export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
-  return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
+	return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
 }
 
 export function nodeEnvVarNames(n: AppNode): string[] {
-  switch (n.type) {
-    case "app": return [
-      `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
-      ...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
-    ];
-    case "github": return [];
-    case "gateway-https": return [];
-    case "gateway-tcp": return [];
-    case "mongodb": return [`DODO_MONGODB_${n.data.label.toUpperCase()}_URL`];
-    case "postgresql": return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
-    case "volume": return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
-    case undefined: throw new Error("MUST NOT REACH");
-    default: throw new Error("MUST NOT REACH");
-  }
+	switch (n.type) {
+		case "app":
+			return [
+				`DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
+				...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
+			];
+		case "github":
+			return [];
+		case "gateway-https":
+			return [];
+		case "gateway-tcp":
+			return [];
+		case "mongodb":
+			return [`DODO_MONGODB_${n.data.label.toUpperCase()}_URL`];
+		case "postgresql":
+			return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
+		case "volume":
+			return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
+		case undefined:
+			throw new Error("MUST NOT REACH");
+		default:
+			throw new Error("MUST NOT REACH");
+	}
 }
 
 export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
@@ -277,68 +304,72 @@
 export type MessageType = "INFO" | "WARNING" | "FATAL";
 
 export type Message = {
-  id: string;
-  type: MessageType;
-  nodeId?: string;
-  message: string;
-  onHighlight?: (state: AppState) => void;
-  onLooseHighlight?: (state: AppState) => void;
-  onClick?: (state: AppState) => void;
+	id: string;
+	type: MessageType;
+	nodeId?: string;
+	message: string;
+	onHighlight?: (state: AppState) => void;
+	onLooseHighlight?: (state: AppState) => void;
+	onClick?: (state: AppState) => void;
 };
 
 export const envSchema = z.object({
-  deployKey: z.optional(z.string().min(1)),
-  networks: z.array(z.object({
-    name: z.string().min(1),
-    domain: z.string().min(1),
-  })).default([]),
-  integrations: z.object({
-    github: z.boolean(),
-  }),
+	deployKey: z.optional(z.string().min(1)),
+	networks: z
+		.array(
+			z.object({
+				name: z.string().min(1),
+				domain: z.string().min(1),
+			}),
+		)
+		.default([]),
+	integrations: z.object({
+		github: z.boolean(),
+	}),
 });
 
 export type Env = z.infer<typeof envSchema>;
 
 const defaultEnv: Env = {
-  deployKey: undefined,
-  networks: [],
-  integrations: {
-    github: false,
-  }
+	deployKey: undefined,
+	networks: [],
+	integrations: {
+		github: false,
+	},
 };
 
 export type Project = {
-  id: string;
-  name: string;
-}
+	id: string;
+	name: string;
+};
 
 export type IntegrationsConfig = {
-  github: boolean;
+	github: boolean;
 };
 
 type NodeUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>>;
 type NodeDataUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>["data"]>;
 
 export type AppState = {
-  projectId: string | undefined;
-  projects: Project[];
-  nodes: AppNode[];
-  edges: Edge[];
-  categories: Category[];
-  messages: Message[];
-  env: Env;
-  githubService: GitHubService | null;
-  setHighlightCategory: (name: string, active: boolean) => void;
-  onNodesChange: OnNodesChange<AppNode>;
-  onEdgesChange: OnEdgesChange;
-  onConnect: OnConnect;
-  setNodes: (nodes: AppNode[]) => void;
-  setEdges: (edges: Edge[]) => void;
-  setProject: (projectId: string | undefined) => void;
-  updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
-  updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
-  replaceEdge: (c: Connection, id?: string) => void;
-  refreshEnv: () => Promise<void>;
+	projectId: string | undefined;
+	projects: Project[];
+	nodes: AppNode[];
+	edges: Edge[];
+	categories: Category[];
+	messages: Message[];
+	env: Env;
+	githubService: GitHubService | null;
+	setHighlightCategory: (name: string, active: boolean) => void;
+	onNodesChange: OnNodesChange<AppNode>;
+	onEdgesChange: OnEdgesChange;
+	onConnect: OnConnect;
+	setNodes: (nodes: AppNode[]) => void;
+	setEdges: (edges: Edge[]) => void;
+	setProject: (projectId: string | undefined) => void;
+	updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
+	updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
+	replaceEdge: (c: Connection, id?: string) => void;
+	refreshEnv: () => Promise<void>;
 };
 
 const projectIdSelector = (state: AppState) => state.projectId;
@@ -348,326 +379,325 @@
 const envSelector = (state: AppState) => state.env;
 
 export function useProjectId(): string | undefined {
-  return useStateStore(projectIdSelector);
+	return useStateStore(projectIdSelector);
 }
 
 export function useCategories(): Category[] {
-  return useStateStore(categoriesSelector);
+	return useStateStore(categoriesSelector);
 }
 
 export function useMessages(): Message[] {
-  return useStateStore(messagesSelector);
+	return useStateStore(messagesSelector);
 }
 
 export function useNodeMessages(id: string): Message[] {
-  return useMessages().filter((m) => m.nodeId === id);
+	return useMessages().filter((m) => m.nodeId === id);
 }
 
 export function useNodeLabel(id: string): string {
-  return nodeLabel(useNodes<AppNode>().find((n) => n.id === id)!);
+	return nodeLabel(useNodes<AppNode>().find((n) => n.id === id)!);
 }
 
 export function useNodePortName(id: string, portId: string): string {
-  return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
+	return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
 }
 
 export function useEnv(): Env {
-  return useStateStore(envSelector);
+	return useStateStore(envSelector);
 }
 
 export function useGithubService(): GitHubService | null {
-  return useStateStore(githubServiceSelector);
+	return useStateStore(githubServiceSelector);
 }
 
 const v: Validator = CreateValidators();
 
 export const useStateStore = create<AppState>((set, get): AppState => {
-  const setN = (nodes: AppNode[]) => {
-    set((state) => ({
-      ...state,
-      nodes,
-    }));
-  };
+	const setN = (nodes: AppNode[]) => {
+		set((state) => ({
+			...state,
+			nodes,
+		}));
+	};
 
-  function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
-    setN(
-      get().nodes.map((n) => {
-        if (n.id === id) {
-          return {
-            ...n,
-            data: {
-              ...n.data,
-              ...data,
-            },
-          } as Extract<AppNode, { type: T }>;
-        }
-        return n;
-      })
-    );
-  }
+	function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
+		setN(
+			get().nodes.map((n) => {
+				if (n.id === id) {
+					return {
+						...n,
+						data: {
+							...n.data,
+							...data,
+						},
+					} as Extract<AppNode, { type: T }>;
+				}
+				return n;
+			}),
+		);
+	}
 
-  function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
-    setN(
-      get().nodes.map((n) => {
-        if (n.id === id) {
-          return {
-            ...n,
-            ...node,
-          } as Extract<AppNode, { type: T }>;
-        }
-        return n;
-      })
-    );
-  }
+	function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
+		setN(
+			get().nodes.map((n) => {
+				if (n.id === id) {
+					return {
+						...n,
+						...node,
+					} as Extract<AppNode, { type: T }>;
+				}
+				return n;
+			}),
+		);
+	}
 
-  function onConnect(c: Connection) {
-    const { nodes, edges } = get();
-    set({
-      edges: addEdge(c, edges),
-    });
-    const sn = nodes.filter((n) => n.id === c.source)[0]!;
-    const tn = nodes.filter((n) => n.id === c.target)[0]!;
-    if (tn.type === "network") {
-      if (sn.type === "gateway-https") {
-        updateNodeData<"gateway-https">(sn.id, {
-          network: tn.data.domain,
-        });
-      } else if (sn.type === "gateway-tcp") {
-        updateNodeData<"gateway-tcp">(sn.id, {
-          network: tn.data.domain,
-        });
-      }
-    }
-    if (tn.type === "app") {
-      if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
-        const sourceEnvVars = nodeEnvVarNames(sn);
-        if (sourceEnvVars.length === 0) {
-          throw new Error("MUST NOT REACH!");
-        }
-        const id = uuidv4();
-        if (sourceEnvVars.length === 1) {
-          updateNode<"app">(c.target, {
-            ...tn,
-            data: {
-              ...tn.data,
-              envVars: [
-                ...(tn.data.envVars || []),
-                {
-                  id: id,
-                  source: c.source,
-                  name: sourceEnvVars[0],
-                  isEditting: false,
-                },
-              ],
-            },
-          });
-        } else {
-          updateNode<"app">(c.target, {
-            ...tn,
-            data: {
-              ...tn.data,
-              envVars: [
-                ...(tn.data.envVars || []),
-                {
-                  id: id,
-                  source: c.source,
-                },
-              ],
-            },
-          });
-        }
-      }
-      if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
-        const sourcePorts = sn.data.ports || [];
-        const id = uuidv4();
-        if (sourcePorts.length === 1) {
-          updateNode<"app">(c.target, {
-            ...tn,
-            data: {
-              ...tn.data,
-              envVars: [
-                ...(tn.data.envVars || []),
-                {
-                  id: id,
-                  source: c.source,
-                  name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
-                  portId: sourcePorts[0].id,
-                  isEditting: false,
-                },
-              ],
-            },
-          });
-        }
-      }
-    }
-    if (c.sourceHandle === "volume") {
-      updateNodeData<"volume">(c.source, {
-        attachedTo: ((sn as VolumeNode).data.attachedTo || []).concat(c.source),
-      });
-    }
-    if (c.targetHandle === "volume") {
-      if (tn.type === "postgresql" || tn.type === "mongodb") {
-        updateNodeData(c.target, {
-          volumeId: c.source,
-        });
-      }
-    }
-    if (c.targetHandle === "https") {
-      if ((sn.data.ports || []).length === 1) {
-        updateNodeData<"gateway-https">(c.target, {
-          https: {
-            serviceId: c.source,
-            portId: sn.data.ports![0].id,
-          }
-        });
-      } else {
-        updateNodeData<"gateway-https">(c.target, {
-          https: {
-            serviceId: c.source,
-            portId: "", // TODO(gio)
-          }
-        });
-      }
-    }
-    if (c.targetHandle === "tcp") {
-      const td = tn.data as GatewayTCPData;
-      if ((sn.data.ports || []).length === 1) {
-        updateNodeData<"gateway-tcp">(c.target, {
-          exposed: (td.exposed || []).concat({
-            serviceId: c.source,
-            portId: sn.data.ports![0].id,
-          }),
-        });
-      } else {
-        updateNodeData<"gateway-tcp">(c.target, {
-          selected: {
-            serviceId: c.source,
-            portId: undefined,
-          },
-        });
-      }
-    }
-    if (sn.type === "app") {
-      if (c.sourceHandle === "ports") {
-        updateNodeData<"app">(sn.id, {
-          isChoosingPortToConnect: true,
-        });
-      }
-    }
-    if (tn.type === "app") {
-      if (c.targetHandle === "repository") {
-        updateNodeData<"app">(tn.id, {
-          repository: {
-            id: c.source,
-            branch: "master",
-            rootDir: "/",
-          }
-        });
-      }
-    }
-  }
-  return {
-    projectId: undefined,
-    projects: [],
-    nodes: [],
-    edges: [],
-    categories: defaultCategories,
-    messages: v([]),
-    env: defaultEnv,
-    githubService: null,
-    setHighlightCategory: (name, active) => {
-      set({
-        categories: get().categories.map(
-          (c) => {
-            if (c.title.toLowerCase() !== name.toLowerCase()) {
-              return c;
-            } else {
-              return {
-                ...c,
-                active,
-              }
-            }
-          })
-      });
-    },
-    onNodesChange: (changes) => {
-      const nodes = applyNodeChanges(changes, get().nodes);
-      setN(nodes);
-    },
-    onEdgesChange: (changes) => {
-      set({
-        edges: applyEdgeChanges(changes, get().edges),
-      });
-    },
-    setNodes: (nodes) => {
-      setN(nodes);
-    },
-    setEdges: (edges) => {
-      set({ edges });
-    },
-    replaceEdge: (c, id) => {
-      let change: EdgeChange;
-      if (id === undefined) {
-        change = {
-          type: "add",
-          item: {
-            id: uuidv4(),
-            ...c,
-          }
-        };
-        onConnect(c);
-      } else {
-        change = {
-          type: "replace",
-          id,
-          item: {
-            id,
-            ...c,
-          }
-        };
-      }
-      set({
-        edges: applyEdgeChanges([change], get().edges),
-      })
-    },
-    updateNode,
-    updateNodeData,
-    onConnect,
-    refreshEnv: async () => {
-      const projectId = get().projectId;
-      let env: Env = defaultEnv;
+	function onConnect(c: Connection) {
+		const { nodes, edges } = get();
+		set({
+			edges: addEdge(c, edges),
+		});
+		const sn = nodes.filter((n) => n.id === c.source)[0]!;
+		const tn = nodes.filter((n) => n.id === c.target)[0]!;
+		if (tn.type === "network") {
+			if (sn.type === "gateway-https") {
+				updateNodeData<"gateway-https">(sn.id, {
+					network: tn.data.domain,
+				});
+			} else if (sn.type === "gateway-tcp") {
+				updateNodeData<"gateway-tcp">(sn.id, {
+					network: tn.data.domain,
+				});
+			}
+		}
+		if (tn.type === "app") {
+			if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
+				const sourceEnvVars = nodeEnvVarNames(sn);
+				if (sourceEnvVars.length === 0) {
+					throw new Error("MUST NOT REACH!");
+				}
+				const id = uuidv4();
+				if (sourceEnvVars.length === 1) {
+					updateNode<"app">(c.target, {
+						...tn,
+						data: {
+							...tn.data,
+							envVars: [
+								...(tn.data.envVars || []),
+								{
+									id: id,
+									source: c.source,
+									name: sourceEnvVars[0],
+									isEditting: false,
+								},
+							],
+						},
+					});
+				} else {
+					updateNode<"app">(c.target, {
+						...tn,
+						data: {
+							...tn.data,
+							envVars: [
+								...(tn.data.envVars || []),
+								{
+									id: id,
+									source: c.source,
+								},
+							],
+						},
+					});
+				}
+			}
+			if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
+				const sourcePorts = sn.data.ports || [];
+				const id = uuidv4();
+				if (sourcePorts.length === 1) {
+					updateNode<"app">(c.target, {
+						...tn,
+						data: {
+							...tn.data,
+							envVars: [
+								...(tn.data.envVars || []),
+								{
+									id: id,
+									source: c.source,
+									name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
+									portId: sourcePorts[0].id,
+									isEditting: false,
+								},
+							],
+						},
+					});
+				}
+			}
+		}
+		if (c.sourceHandle === "volume") {
+			updateNodeData<"volume">(c.source, {
+				attachedTo: ((sn as VolumeNode).data.attachedTo || []).concat(c.source),
+			});
+		}
+		if (c.targetHandle === "volume") {
+			if (tn.type === "postgresql" || tn.type === "mongodb") {
+				updateNodeData(c.target, {
+					volumeId: c.source,
+				});
+			}
+		}
+		if (c.targetHandle === "https") {
+			if ((sn.data.ports || []).length === 1) {
+				updateNodeData<"gateway-https">(c.target, {
+					https: {
+						serviceId: c.source,
+						portId: sn.data.ports![0].id,
+					},
+				});
+			} else {
+				updateNodeData<"gateway-https">(c.target, {
+					https: {
+						serviceId: c.source,
+						portId: "", // TODO(gio)
+					},
+				});
+			}
+		}
+		if (c.targetHandle === "tcp") {
+			const td = tn.data as GatewayTCPData;
+			if ((sn.data.ports || []).length === 1) {
+				updateNodeData<"gateway-tcp">(c.target, {
+					exposed: (td.exposed || []).concat({
+						serviceId: c.source,
+						portId: sn.data.ports![0].id,
+					}),
+				});
+			} else {
+				updateNodeData<"gateway-tcp">(c.target, {
+					selected: {
+						serviceId: c.source,
+						portId: undefined,
+					},
+				});
+			}
+		}
+		if (sn.type === "app") {
+			if (c.sourceHandle === "ports") {
+				updateNodeData<"app">(sn.id, {
+					isChoosingPortToConnect: true,
+				});
+			}
+		}
+		if (tn.type === "app") {
+			if (c.targetHandle === "repository") {
+				updateNodeData<"app">(tn.id, {
+					repository: {
+						id: c.source,
+						branch: "master",
+						rootDir: "/",
+					},
+				});
+			}
+		}
+	}
+	return {
+		projectId: undefined,
+		projects: [],
+		nodes: [],
+		edges: [],
+		categories: defaultCategories,
+		messages: v([]),
+		env: defaultEnv,
+		githubService: null,
+		setHighlightCategory: (name, active) => {
+			set({
+				categories: get().categories.map((c) => {
+					if (c.title.toLowerCase() !== name.toLowerCase()) {
+						return c;
+					} else {
+						return {
+							...c,
+							active,
+						};
+					}
+				}),
+			});
+		},
+		onNodesChange: (changes) => {
+			const nodes = applyNodeChanges(changes, get().nodes);
+			setN(nodes);
+		},
+		onEdgesChange: (changes) => {
+			set({
+				edges: applyEdgeChanges(changes, get().edges),
+			});
+		},
+		setNodes: (nodes) => {
+			setN(nodes);
+		},
+		setEdges: (edges) => {
+			set({ edges });
+		},
+		replaceEdge: (c, id) => {
+			let change: EdgeChange;
+			if (id === undefined) {
+				change = {
+					type: "add",
+					item: {
+						id: uuidv4(),
+						...c,
+					},
+				};
+				onConnect(c);
+			} else {
+				change = {
+					type: "replace",
+					id,
+					item: {
+						id,
+						...c,
+					},
+				};
+			}
+			set({
+				edges: applyEdgeChanges([change], get().edges),
+			});
+		},
+		updateNode,
+		updateNodeData,
+		onConnect,
+		refreshEnv: async () => {
+			const projectId = get().projectId;
+			let env: Env = defaultEnv;
 
-      try {
-        if (projectId) {
-          const response = await fetch(`/api/project/${projectId}/env`);
-          if (response.ok) {
-            const data = await response.json();
-            const result = envSchema.safeParse(data);
-            if (result.success) {
-              env = result.data;
-            } else {
-              console.error("Invalid env data:", result.error);
-            }
-          }
-        }
-      } catch (error) {
-        console.error("Failed to fetch integrations:", error);
-      } finally {
-        set({ env: env });
-        if (env.integrations.github) {
-          set({ githubService: new GitHubServiceImpl(projectId!) });
-        } else {
-          set({ githubService: null });
-        }
-      }
-    },
-    setProject: (projectId) => {
-      set({
-        projectId,
-      });
-      if (projectId) {
-        get().refreshEnv();
-      }
-    },
-  };
+			try {
+				if (projectId) {
+					const response = await fetch(`/api/project/${projectId}/env`);
+					if (response.ok) {
+						const data = await response.json();
+						const result = envSchema.safeParse(data);
+						if (result.success) {
+							env = result.data;
+						} else {
+							console.error("Invalid env data:", result.error);
+						}
+					}
+				}
+			} catch (error) {
+				console.error("Failed to fetch integrations:", error);
+			} finally {
+				set({ env: env });
+				if (env.integrations.github) {
+					set({ githubService: new GitHubServiceImpl(projectId!) });
+				} else {
+					set({ githubService: null });
+				}
+			}
+		},
+		setProject: (projectId) => {
+			set({
+				projectId,
+			});
+			if (projectId) {
+				get().refreshEnv();
+			}
+		},
+	};
 });
diff --git a/apps/canvas/front/src/lib/utils.ts b/apps/canvas/front/src/lib/utils.ts
index bd0c391..ac680b3 100644
--- a/apps/canvas/front/src/lib/utils.ts
+++ b/apps/canvas/front/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
 
 export function cn(...inputs: ClassValue[]) {
-  return twMerge(clsx(inputs))
+	return twMerge(clsx(inputs));
 }