Canvas: Fix linter errors

Change-Id: I602c1562d4ab2d948bb4dcf6caf66f185585d720
diff --git a/apps/canvas/front/eslint.config.js b/apps/canvas/front/eslint.config.js
index 092408a..545cef9 100644
--- a/apps/canvas/front/eslint.config.js
+++ b/apps/canvas/front/eslint.config.js
@@ -23,6 +23,14 @@
         'warn',
         { allowConstantExport: true },
       ],
+      "@typescript-eslint/no-unused-vars": [
+        "error",
+        {
+          "argsIgnorePattern": "^_$",
+          "varsIgnorePattern": "^_$",
+          "caughtErrorsIgnorePattern": "^_$",
+        }
+      ]
     },
   },
 )
diff --git a/apps/canvas/front/package-lock.json b/apps/canvas/front/package-lock.json
index 39c0823..86e3762 100644
--- a/apps/canvas/front/package-lock.json
+++ b/apps/canvas/front/package-lock.json
@@ -21,7 +21,7 @@
         "@radix-ui/react-separator": "^1.1.0",
         "@radix-ui/react-slot": "^1.1.0",
         "@radix-ui/react-tabs": "^1.1.1",
-        "@radix-ui/react-toast": "^1.2.2",
+        "@radix-ui/react-toast": "^1.2.13",
         "@radix-ui/react-tooltip": "^1.1.4",
         "@xyflow/react": "^12.3.3",
         "class-variance-authority": "^0.7.0",
@@ -1868,22 +1868,276 @@
       }
     },
     "node_modules/@radix-ui/react-toast": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
-      "integrity": "sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==",
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.13.tgz",
+      "integrity": "sha512-e/e43mQAwgYs8BY4y9l99xTK6ig1bK2uXsFLOMn9IZ16lAgulSTsotcPHVT2ZlSb/ye6Sllq7IgyDB8dGhpeXQ==",
       "dependencies": {
-        "@radix-ui/primitive": "1.1.0",
-        "@radix-ui/react-collection": "1.1.0",
-        "@radix-ui/react-compose-refs": "1.1.0",
-        "@radix-ui/react-context": "1.1.1",
-        "@radix-ui/react-dismissable-layer": "1.1.1",
-        "@radix-ui/react-portal": "1.1.2",
-        "@radix-ui/react-presence": "1.1.1",
-        "@radix-ui/react-primitive": "2.0.0",
-        "@radix-ui/react-use-callback-ref": "1.1.0",
-        "@radix-ui/react-use-controllable-state": "1.1.0",
-        "@radix-ui/react-use-layout-effect": "1.1.0",
-        "@radix-ui/react-visually-hidden": "1.1.0"
+        "@radix-ui/primitive": "1.1.2",
+        "@radix-ui/react-collection": "1.1.6",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-dismissable-layer": "1.1.9",
+        "@radix-ui/react-portal": "1.1.8",
+        "@radix-ui/react-presence": "1.1.4",
+        "@radix-ui/react-primitive": "2.1.2",
+        "@radix-ui/react-use-callback-ref": "1.1.1",
+        "@radix-ui/react-use-controllable-state": "1.2.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1",
+        "@radix-ui/react-visually-hidden": "1.2.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/primitive": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+      "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz",
+      "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-primitive": "2.1.2",
+        "@radix-ui/react-slot": "1.2.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-compose-refs": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+      "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+      "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz",
+      "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.2",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-primitive": "2.1.2",
+        "@radix-ui/react-use-callback-ref": "1.1.1",
+        "@radix-ui/react-use-escape-keydown": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz",
+      "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==",
+      "dependencies": {
+        "@radix-ui/react-primitive": "2.1.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+      "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
+      "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
+      "dependencies": {
+        "@radix-ui/react-slot": "1.2.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-slot": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
+      "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-callback-ref": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+      "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-controllable-state": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+      "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+      "dependencies": {
+        "@radix-ui/react-use-effect-event": "0.0.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-escape-keydown": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+      "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+      "dependencies": {
+        "@radix-ui/react-use-callback-ref": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-layout-effect": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+      "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz",
+      "integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==",
+      "dependencies": {
+        "@radix-ui/react-primitive": "2.1.2"
       },
       "peerDependencies": {
         "@types/react": "*",
diff --git a/apps/canvas/front/package.json b/apps/canvas/front/package.json
index 4c9ba0c..6bfe3cb 100644
--- a/apps/canvas/front/package.json
+++ b/apps/canvas/front/package.json
@@ -27,7 +27,7 @@
     "@radix-ui/react-separator": "^1.1.0",
     "@radix-ui/react-slot": "^1.1.0",
     "@radix-ui/react-tabs": "^1.1.1",
-    "@radix-ui/react-toast": "^1.2.2",
+    "@radix-ui/react-toast": "^1.2.13",
     "@radix-ui/react-tooltip": "^1.1.4",
     "@xyflow/react": "^12.3.3",
     "class-variance-authority": "^0.7.0",
diff --git a/apps/canvas/front/src/Header.tsx b/apps/canvas/front/src/Header.tsx
index 2d6d68d..83c658c 100644
--- a/apps/canvas/front/src/Header.tsx
+++ b/apps/canvas/front/src/Header.tsx
@@ -11,10 +11,6 @@
     const { toast } = useToast();
     const store = useStateStore();
     const [projects, setProjects] = useState<Project[]>([]);
-    // TODO(gio): sth fishy is here
-    useEffect(() => {
-        store.setProjects(projects);
-    }, [projects]);
     const refreshProjects = useCallback(async () => {
         try {
             const resp = await fetch("/api/project");
@@ -87,7 +83,7 @@
                 title: `Failed to create project: ${name}`,
             });
         });
-    }, [name, setCreateNewOpen, toast]); // store
+    }, [name, setCreateNewOpen, toast, store, refreshProjects]);
     return (
         <div className="flex flex-row h-9">
             <Select onValueChange={onSelect} value={project}>
@@ -109,7 +105,7 @@
                     </SelectItem>
                 </SelectContent>
             </Select>
-            
+
         </div>
     );
 }
\ No newline at end of file
diff --git a/apps/canvas/front/src/Integrations.tsx b/apps/canvas/front/src/Integrations.tsx
index a566c22..00fe9f1 100644
--- a/apps/canvas/front/src/Integrations.tsx
+++ b/apps/canvas/front/src/Integrations.tsx
@@ -87,7 +87,7 @@
                         </Button>
                     )}
 
-                    {(!!!githubService || isEditing) && (
+                    {(!githubService || isEditing) && (
                         <Form {...form}>
                             <form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
                                 <FormField
diff --git a/apps/canvas/front/src/Messages.tsx b/apps/canvas/front/src/Messages.tsx
index 94cd42e..ecf7777 100644
--- a/apps/canvas/front/src/Messages.tsx
+++ b/apps/canvas/front/src/Messages.tsx
@@ -8,7 +8,7 @@
 export function Messages() {
     const store = useStateStore();
     const nodes = useNodes<AppNode>();
-    const [nodeMap, setNodeMap] = useState<Map<string, AppNode>>();
+    const [nodeMap, setNodeMap] = useState<Map<string, AppNode>>(new Map());
     useEffect(() => {
         setNodeMap(new Map(nodes.map((n) => [n.id, n])));
     }, [nodes, setNodeMap]);
@@ -37,22 +37,22 @@
         setOpen([...grouped.keys()]);
     }, [grouped, setOpen]);
     return (
-            <Accordion type="multiple" value={open} onValueChange={(v) => setOpen(v)}>
-                {[...grouped.entries()].map(([id, messages]) => (
-                    <AccordionItem key={id} value={id}>
-                        <AccordionTrigger className="flex flex-row-reverse !space-x-4 !justify-end">
-                            <Badge>{messages.length}</Badge>
-                            <div>{id === "global" ? "Global" : nodeLabel(nodeMap?.get(id)!)}</div>
-                        </AccordionTrigger>
-                        <AccordionContent>
-                            <div className="flex flex-col space-y-1">
-                                {messages.map((m) => (
-                                    <Button key={m.id} variant="ghost" style={{ justifyContent: "flex-start" }} onMouseOver={onClick(m.onHighlight)} onMouseLeave={onClick(m.onLooseHighlight)} onClick={onClick(m.onClick)}>{m.message}</Button>
-                                ))}
-                            </div>
-                        </AccordionContent>
-                    </AccordionItem>
-                ))}
-            </Accordion>
+        <Accordion type="multiple" value={open} onValueChange={(v) => setOpen(v)}>
+            {[...grouped.entries()].map(([id, messages]) => (
+                <AccordionItem key={id} value={id}>
+                    <AccordionTrigger className="flex flex-row-reverse !space-x-4 !justify-end">
+                        <Badge>{messages.length}</Badge>
+                        <div>{id === "global" ? "Global" : nodeLabel(nodeMap.get(id)!)}</div>
+                    </AccordionTrigger>
+                    <AccordionContent>
+                        <div className="flex flex-col space-y-1">
+                            {messages.map((m) => (
+                                <Button key={m.id} variant="ghost" style={{ justifyContent: "flex-start" }} onMouseOver={onClick(m.onHighlight)} onMouseLeave={onClick(m.onLooseHighlight)} onClick={onClick(m.onClick)}>{m.message}</Button>
+                            ))}
+                        </div>
+                    </AccordionContent>
+                </AccordionItem>
+            ))}
+        </Accordion>
     )
 }
\ No newline at end of file
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index 0f39e4e..54449c4 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -57,7 +57,7 @@
             }
         };
         setTimeout(m, 100);
-    }, [projectId, nodes]);
+    }, [projectId, nodes, store]);
     const deploy = useCallback(async () => {
         if (projectId == null) {
             return;
@@ -99,8 +99,7 @@
         } finally {
             setLoading(false);
         }
-    }, [projectId, instance, nodes, env, setLoading]);
-    const [st, setSt] = useState<string>();
+    }, [projectId, instance, nodes, env, setLoading, toast, monitor]);
     const save = useCallback(async () => {
         if (projectId == null) {
             return;
@@ -123,7 +122,7 @@
                 description: await resp.text(),
             });
         }
-    }, [projectId, instance, setSt]);
+    }, [projectId, instance, toast]);
     const restoreSaved = useCallback(async () => {
         if (projectId == null) {
             return;
@@ -136,12 +135,12 @@
         store.setNodes(inst.nodes || []);
         store.setEdges(inst.edges || []);
         instance.setViewport({ x, y, zoom });
-    }, [projectId, instance, st]);
+    }, [projectId, instance, store]);
     const clear = useCallback(() => {
         store.setEdges([]);
         store.setNodes([]);
         instance.setViewport({ x: 0, y: 0, zoom: 1 });
-    }, [store]);
+    }, [store, instance]);
     // TODO(gio): Update store
     const deleteProject = useCallback(async () => {
         if (projectId == null) {
@@ -163,7 +162,7 @@
                 description: await resp.text(),
             });
         }
-    }, [store, clear]);
+    }, [store, clear, projectId, toast]);
     const [props, setProps] = useState({});
     useEffect(() => {
         if (loading) {
diff --git a/apps/canvas/front/src/components/details.tsx b/apps/canvas/front/src/components/details.tsx
index ffac03f..d498771 100644
--- a/apps/canvas/front/src/components/details.tsx
+++ b/apps/canvas/front/src/components/details.tsx
@@ -3,7 +3,7 @@
 import { NodeDetails } from "@/components/node-details";
 import { Accordion, AccordionContent, AccordionTrigger } from "./ui/accordion";
 import { AccordionItem } from "@radix-ui/react-accordion";
-import { useCallback, useMemo, useState } from "react";
+import { useMemo, useState } from "react";
 import { Icon } from "./icon";
 
 function unique<T>(v: T, i: number, a: T[]) {
@@ -21,23 +21,22 @@
   ["gateway-https", 8],
 ]);
 
+function cmpNodes(x: AppNode, y: AppNode): number {
+  if (x.type === y.type) {
+    if (nodeLabel(x) < nodeLabel(y)) {
+      return -1;
+    } else if (nodeLabel(x) > nodeLabel(y)) {
+      return 1;
+    }
+    return 0;
+  }
+  // TODO(gio): why !
+  return (nodeTypeIndex.get(x.type!) || 0) - (nodeTypeIndex.get(y.type!) || 0);
+}
+
 export function Details() {
   const nodes = useNodes<AppNode>();
-  const cmpNodes = useCallback(() => {
-    return (x: AppNode, y: AppNode): number => {
-      if (x.type === y.type) {
-        if (nodeLabel(x) < nodeLabel(y)) {
-          return -1;
-        } else if (nodeLabel(x) > nodeLabel(y)) {
-          return 1;
-        }
-        return 0;
-      }
-      // TODO(gio): why !
-      return (nodeTypeIndex.get(x.type!) || 0) - (nodeTypeIndex.get(y.type!) || 0);
-    };
-  }, [nodes]);
-  const sorted = useMemo(() => nodes.filter((n) => n.type !== "network").sort(cmpNodes()), [nodes, cmpNodes]);
+  const sorted = useMemo(() => nodes.filter((n) => n.type !== "network").sort(cmpNodes), [nodes]);
   const [open, setOpen] = useState<string[]>([]);
   const selected = useMemo(() => nodes.filter((n) => n.selected).map((n) => n.id), [nodes]);
   const all = useMemo(() => open.concat(selected).filter(unique), [open, selected]);
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 5b4b3fa..3027f27 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -1,9 +1,9 @@
 import { v4 as uuidv4 } from "uuid";
 import { NodeRect } from './node-rect';
-import { useStateStore, ServiceNode, ServiceTypes, nodeLabel, BoundEnvVar, AppState, nodeIsConnectable, GatewayTCPNode, GatewayHttpsNode } from '@/lib/state';
+import { useStateStore, ServiceNode, ServiceTypes, nodeLabel, BoundEnvVar, AppState, nodeIsConnectable, GatewayTCPNode, GatewayHttpsNode, AppNode } from '@/lib/state';
 import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
 import { z } from "zod";
-import { DeepPartial, EventType, useForm } from 'react-hook-form';
+import { DeepPartial, EventType, useForm, ControllerRenderProps, FieldPath } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
 import { Input } from './ui/input';
@@ -69,7 +69,7 @@
 
 export function NodeAppDetails({ id, data }: ServiceNode) {
   const store = useStateStore();
-  const nodes = useNodes();
+  const nodes = useNodes<AppNode>();
   const form = useForm<z.infer<typeof schema>>({
     resolver: zodResolver(schema),
     mode: "onChange",
@@ -102,7 +102,7 @@
       }),
     });
     portForm.reset();
-  }, [data, portForm]);
+  }, [id, data, portForm, store]);
   useEffect(() => {
     const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name, type }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
       console.log({ name, type });
@@ -129,8 +129,8 @@
       }
     });
     return () => sub.unsubscribe();
-  }, [form, store]);
-  const focus = useCallback((field: any, name: string) => {
+  }, [id, form, store]);
+  const focus = useCallback((field: ControllerRenderProps<z.infer<typeof schema>, FieldPath<z.infer<typeof schema>>>, name: string) => {
     return (e: HTMLElement | null) => {
       field.ref(e);
       if (e != null && name === data.activeField) {
@@ -141,7 +141,7 @@
         });
       }
     }
-  }, [data, store]);
+  }, [id, data, store]);
   const [typeProps, setTypeProps] = useState({});
   useEffect(() => {
     if (data.activeField === "type") {
@@ -152,7 +152,7 @@
     } else {
       setTypeProps({});
     }
-  }, [store, data, setTypeProps]);
+  }, [id, data, store, setTypeProps]);
   const editAlias = useCallback((e: BoundEnvVar) => {
     return () => {
       store.updateNodeData(id, {
@@ -168,7 +168,7 @@
       });
     };
   }, [id, data, store]);
-  const saveAlias = (e: BoundEnvVar, value: string, store: AppState) => {
+  const saveAlias = useCallback((e: BoundEnvVar, value: string, store: AppState) => {
     store.updateNodeData(id, {
       ...data,
       envVars: data.envVars!.map((o) => {
@@ -184,7 +184,7 @@
         }
         console.log(o);
         if ("alias" in o) {
-          const { alias: tmp, ...rest } = o;
+          const { alias: _, ...rest } = o;
           console.log(rest);
           return {
             ...rest,
@@ -197,7 +197,7 @@
         };
       }),
     });
-  };
+  }, [id, data]);
   const saveAliasOnEnter = useCallback((e: BoundEnvVar) => {
     return (event: KeyboardEvent<HTMLInputElement>) => {
       if (event.key === "Enter") {
@@ -205,12 +205,12 @@
         saveAlias(e, event.currentTarget.value, store);
       }
     }
-  }, [id, data, store]);
+  }, [store, saveAlias]);
   const saveAliasOnBlur = useCallback((e: BoundEnvVar) => {
     return (event: FocusEvent<HTMLInputElement>) => {
       saveAlias(e, event.currentTarget.value, store);
     }
-  }, [id, data, store]);
+  }, [store, saveAlias]);
   const removePort = useCallback((portId: string) => {
     // TODO(gio): this is ugly
     const tcpRemoved = new Set<string>();
@@ -394,7 +394,7 @@
                     </SelectTrigger>
                   </FormControl>
                   <SelectContent>
-                    {nodes.filter((n) => n.type === "github" && n.data.repository?.id !== undefined).map((n) => (
+                    {(nodes.filter((n) => n.type === "github" && n.data.repository?.id !== undefined) as GithubNode[]).map((n) => (
                       <SelectItem key={n.id} value={n.id}>{`${n.data.repository?.sshURL}`}</SelectItem>
                     ))}
                   </SelectContent>
diff --git a/apps/canvas/front/src/components/node-gateway-https.tsx b/apps/canvas/front/src/components/node-gateway-https.tsx
index e618943..6effb26 100644
--- a/apps/canvas/front/src/components/node-gateway-https.tsx
+++ b/apps/canvas/front/src/components/node-gateway-https.tsx
@@ -125,7 +125,7 @@
       return nodes.find((n) => n.id === https.serviceId)! as ServiceNode;
     }
     return null;
-  }, [data]);
+  }, [data, nodes]);
   const selectable = useMemo(() => {
     return nodes.filter((n) => {
       if (n.id === id) {
@@ -139,14 +139,14 @@
       }
       return n.data && n.data.ports && n.data.ports.length > 0;
     })
-  }, [nodes, selected]);
+  }, [id, nodes, selected]);
   useEffect(() => {
     const sub = connectedToForm.watch((value: DeepPartial<z.infer<typeof connectedToSchema>>, { name, type }: { name?: keyof z.infer<typeof connectedToSchema> | undefined, type?: EventType | undefined }) => {
       if (type !== "change") {
         return;
       }
       switch (name) {
-        case "id":
+        case "id": {
           if (!value.id) {
             break;
           }
@@ -159,6 +159,7 @@
             targetHandle: "https",
           }, cid);
           break;
+        }
         case "portId":
           store.updateNodeData<"gateway-https">(id, {
             https: {
@@ -170,7 +171,7 @@
       }
     });
     return () => sub.unsubscribe();
-  }, [connectedToForm, store, selectable]);
+  }, [id, connectedToForm, store, selectable]);
   const authEnabledForm = useForm<z.infer<typeof authEnabledSchema>>({
     resolver: zodResolver(authEnabledSchema),
     mode: "onChange",
@@ -224,7 +225,7 @@
       },
     });
     authGroupForm.reset();
-  }, [id, data, store]);
+  }, [id, data, store, authGroupForm]);
   const removeNoAuthPathPattern = useCallback((path: string) => {
     const noAuthPathPatterns = data?.auth?.noAuthPathPatterns || [];
     store.updateNodeData<"gateway-https">(id, {
@@ -245,7 +246,7 @@
       },
     });
     authNoAuthPatternFrom.reset();
-  }, [id, data, store]);
+  }, [id, data, store, authNoAuthPatternFrom]);
   return (
     <>
       <Form {...form}>
diff --git a/apps/canvas/front/src/components/node-gateway-tcp.tsx b/apps/canvas/front/src/components/node-gateway-tcp.tsx
index 3588502..e16fdd2 100644
--- a/apps/canvas/front/src/components/node-gateway-tcp.tsx
+++ b/apps/canvas/front/src/components/node-gateway-tcp.tsx
@@ -104,7 +104,7 @@
       portId: data.selected?.portId,
     });
     console.log(connectedToForm.getValues());
-  }, [connectedToForm, data]);
+  }, [id, connectedToForm, data]);
   const nodes = useNodes<AppNode>();
   const [selected, setSelected] = useState<AppNode | undefined>(undefined);
   useEffect(() => {
@@ -114,7 +114,7 @@
       const serviceId = data.selected.serviceId;
       setSelected(nodes.find((n) => n.id === serviceId));
     }
-  }, [data, setSelected]);
+  }, [id, data, setSelected, nodes]);
   const selectable = useMemo(() => {
     console.log(selected);
     return nodes.filter((n) => {
@@ -129,7 +129,7 @@
       }
       return false;
     })
-  }, [nodes, selected]);
+  }, [id, nodes, selected]);
   useEffect(() => {
     const sub = connectedToForm.watch((value: DeepPartial<z.infer<typeof connectedToSchema>>, { name, type }: { name?: keyof z.infer<typeof connectedToSchema> | undefined, type?: EventType | undefined }) => {
       if (type !== "change") {
@@ -160,7 +160,7 @@
       }
     });
     return () => sub.unsubscribe();
-  }, [connectedToForm, store]);
+  }, [id, connectedToForm, store]);
   const [nodeLabels, setNodeLabels] = useState(new Map<string, string>());
   const [portLabels, setPortLabels] = useState(new Map<string, string>());
   useEffect(() => {
@@ -184,7 +184,7 @@
       target: id,
       targetHandle: "tcp",
     }))));
-  }, [id, data, connectedToForm, store, setNodeLabels, setPortLabels]);
+  }, [id, data, store]);
   return (
     <>
       <Form {...form}>
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index 3ae779e..6ece1b0 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -100,7 +100,7 @@
 
   useEffect(() => {
     const fetchRepositories = async () => {
-      if (!!!githubService) return;
+      if (!githubService) return;
 
       setLoading(true);
       setError(null);
@@ -129,11 +129,11 @@
                 <Select
                   onValueChange={(value) => field.onChange(Number(value))}
                   value={field.value?.toString()}
-                  disabled={loading || !projectId || !!!githubService}
+                  disabled={loading || !projectId || !githubService}
                 >
                   <FormControl>
                     <SelectTrigger>
-                      <SelectValue placeholder={!!githubService ? "Select a repository" : "GitHub not configured"} />
+                      <SelectValue placeholder={githubService ? "Select a repository" : "GitHub not configured"} />
                     </SelectTrigger>
                   </FormControl>
                   <SelectContent>
@@ -148,7 +148,7 @@
                 <FormMessage />
                 {error && <p className="text-sm text-red-500">{error}</p>}
                 {loading && <p className="text-sm text-gray-500">Loading repositories...</p>}
-                {!!!githubService && (
+                {!githubService && (
                   <Alert variant="destructive" className="mt-2">
                     <AlertCircle className="h-4 w-4" />
                     <AlertDescription>
diff --git a/apps/canvas/front/src/components/node-mongodb.tsx b/apps/canvas/front/src/components/node-mongodb.tsx
index 40c9748..9ead671 100644
--- a/apps/canvas/front/src/components/node-mongodb.tsx
+++ b/apps/canvas/front/src/components/node-mongodb.tsx
@@ -21,7 +21,7 @@
           isConnectableStart={true}
           isConnectableEnd={true}
           isConnectable={true}
-         />
+        />
       </div>
     </NodeRect>
   );
@@ -50,7 +50,7 @@
       });
     });
     return () => sub.unsubscribe();
-  }, [form, store]);
+  }, [id, form, store]);
   return (
     <>
       <Form {...form}>
diff --git a/apps/canvas/front/src/components/node-postgresql.tsx b/apps/canvas/front/src/components/node-postgresql.tsx
index 4213645..a0bd558 100644
--- a/apps/canvas/front/src/components/node-postgresql.tsx
+++ b/apps/canvas/front/src/components/node-postgresql.tsx
@@ -50,7 +50,7 @@
       });
     });
     return () => sub.unsubscribe();
-  }, [form, store]);
+  }, [id, form, store]);
   return (
     <>
       <Form {...form}>
diff --git a/apps/canvas/front/src/components/node-rect.tsx b/apps/canvas/front/src/components/node-rect.tsx
index 06f3307..615f7a1 100644
--- a/apps/canvas/front/src/components/node-rect.tsx
+++ b/apps/canvas/front/src/components/node-rect.tsx
@@ -5,7 +5,7 @@
 export type Props = {
     id: string;
     selected?: boolean;
-    children: any;
+    children: React.ReactNode;
     type: NodeType;
     state: string | null;
 };
@@ -32,7 +32,7 @@
             classes.push("border");
         }
         setClasses(classes);
-        let stateClasses: string[] = [];
+        const stateClasses: string[] = [];
         if (state === "processing") {
             stateClasses.push("bg-yellow-500");
             stateClasses.push("animate-pulse");
diff --git a/apps/canvas/front/src/components/node-volume.tsx b/apps/canvas/front/src/components/node-volume.tsx
index 430d1e9..cb42241 100644
--- a/apps/canvas/front/src/components/node-volume.tsx
+++ b/apps/canvas/front/src/components/node-volume.tsx
@@ -25,7 +25,7 @@
           isConnectableStart={isConnectable}
           isConnectableEnd={isConnectable}
           isConnectable={isConnectable}
-         />
+        />
       </div>
     </NodeRect>
   );
@@ -63,7 +63,7 @@
       });
     });
     return () => sub.unsubscribe();
-  }, [form, store]);
+  }, [id, form, store]);
   useEffect(() => {
     form.reset({
       name: data.label,
@@ -75,7 +75,7 @@
     <>
       <Form {...form}>
         <form className="space-y-2">
-        <FormField 
+          <FormField
             control={form.control}
             name="name"
             render={({ field }) => (
@@ -87,7 +87,7 @@
               </FormItem>
             )}
           />
-        <FormField
+          <FormField
             control={form.control}
             name="type"
             render={({ field }) => (
@@ -108,7 +108,7 @@
               </FormItem>
             )}
           />
-          <FormField 
+          <FormField
             control={form.control}
             name="size"
             render={({ field }) => (
diff --git a/apps/canvas/front/src/components/resources.tsx b/apps/canvas/front/src/components/resources.tsx
index 7471c1f..085301c 100644
--- a/apps/canvas/front/src/components/resources.tsx
+++ b/apps/canvas/front/src/components/resources.tsx
@@ -22,9 +22,9 @@
 }
 
 export function Resources() {
-  let flow = useReactFlow();
+  const flow = useReactFlow();
   const categories = useCategories();
-  let onResourceAdd = useCallback((item: CategoryItem) => {
+  const onResourceAdd = useCallback((item: CategoryItem) => {
     return () => addResource(item, flow);
   }, [flow]);
   const [open, setOpen] = useState<string[]>(categories.map((c) => c.title));
diff --git a/apps/canvas/front/src/hooks/use-toast.ts b/apps/canvas/front/src/hooks/use-toast.ts
index 02e111d..55ec184 100644
--- a/apps/canvas/front/src/hooks/use-toast.ts
+++ b/apps/canvas/front/src/hooks/use-toast.ts
@@ -36,21 +36,21 @@
 
 type Action =
   | {
-      type: ActionType["ADD_TOAST"]
-      toast: ToasterToast
-    }
+    type: ActionType["ADD_TOAST"]
+    toast: ToasterToast
+  }
   | {
-      type: ActionType["UPDATE_TOAST"]
-      toast: Partial<ToasterToast>
-    }
+    type: ActionType["UPDATE_TOAST"]
+    toast: Partial<ToasterToast>
+  }
   | {
-      type: ActionType["DISMISS_TOAST"]
-      toastId?: ToasterToast["id"]
-    }
+    type: ActionType["DISMISS_TOAST"]
+    toastId?: ToasterToast["id"]
+  }
   | {
-      type: ActionType["REMOVE_TOAST"]
-      toastId?: ToasterToast["id"]
-    }
+    type: ActionType["REMOVE_TOAST"]
+    toastId?: ToasterToast["id"]
+  }
 
 interface State {
   toasts: ToasterToast[]
@@ -108,9 +108,9 @@
         toasts: state.toasts.map((t) =>
           t.id === toastId || toastId === undefined
             ? {
-                ...t,
-                open: false,
-              }
+              ...t,
+              open: false,
+            }
             : t
         ),
       }
diff --git a/apps/canvas/front/src/lib/categories.ts b/apps/canvas/front/src/lib/categories.ts
index e45c4fd..6330536 100644
--- a/apps/canvas/front/src/lib/categories.ts
+++ b/apps/canvas/front/src/lib/categories.ts
@@ -1,6 +1,6 @@
 import { NodeType, InitData } from "@/lib/state";
 
-export interface CategoryItem<T extends NodeType = any> {
+export interface CategoryItem<T extends NodeType> {
     title: string;
     init: InitData;
     type: T;
@@ -8,7 +8,7 @@
 
 export type Category = {
     title: string;
-    items: CategoryItem[];
+    items: CategoryItem<NodeType>[];
     active?: boolean;
 };
 
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index a3cd755..2492642 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -335,7 +335,6 @@
   setNodes: (nodes: AppNode[]) => void;
   setEdges: (edges: Edge[]) => void;
   setProject: (projectId: string | undefined) => void;
-  setProjects: (projects: Project[]) => 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;
@@ -670,6 +669,5 @@
         get().refreshEnv();
       }
     },
-    setProjects: (projects) => set({ projects }),
   };
 });