diff --git a/components/canvas/CLAUDE.md b/components/canvas/CLAUDE.md
index 15a6fd2..47464fc 100644
--- a/components/canvas/CLAUDE.md
+++ b/components/canvas/CLAUDE.md
@@ -10,15 +10,28 @@ Der Canvas ist das Herzstück von LemonSpace. Er basiert auf `@xyflow/react` (Re
app/(app)/canvas/[canvasId]/page.tsx
└── ← components/canvas/canvas.tsx
├──
- │ └── ← Haupt-Komponente (2800 Zeilen)
+ │ └── ← Haupt-Komponente (~1800 Zeilen)
│ ├── Convex useQuery ← Realtime-Sync
│ ├── nodeTypes Map ← node-types.ts
│ ├── localStorage Cache ← canvas-local-persistence.ts
+ │ ├── Interaction-Hooks ← canvas-*.ts Helper
│ └── Panel-Komponenten
└── Context Providers
```
-**`canvas.tsx`** ist die zentrale Datei. Sie enthält die gesamte State-Management-Logik, Convex-Mutations, Optimistic Updates und Event-Handler. Sehr groß — vor Änderungen immer den genauen Abschnitt lesen.
+**`canvas.tsx`** ist weiterhin die zentrale Orchestrierungsdatei. Viel Low-Level-Logik wurde in dedizierte Module ausgelagert, aber Mutations-Flow, Event-Wiring und Render-Composition liegen weiterhin hier.
+
+### Interne Module von `canvas.tsx`
+
+| Datei | Zweck |
+|------|-------|
+| `canvas-helpers.ts` | Shared Utility-Layer (Optimistic IDs, Node-Merge, Compare-Resolution, Edge/Hit-Helpers, Konstante Defaults) |
+| `canvas-node-change-helpers.ts` | Dimensions-/Resize-Transformationen für `asset` und `ai-image` Nodes |
+| `canvas-generation-failures.ts` | Hook für AI-Generation-Error-Tracking mit Schwellenwert-Toast |
+| `canvas-scissors.ts` | Hook für Scherenmodus (K/Esc Toggle, Click-Cut, Stroke-Cut) |
+| `canvas-delete-handlers.ts` | Hook für `onBeforeDelete`, `onNodesDelete`, `onEdgesDelete` inkl. Bridge-Edges |
+| `canvas-reconnect.ts` | Hook für Edge-Reconnect (`onReconnectStart`, `onReconnect`, `onReconnectEnd`) |
+| `canvas-media-utils.ts` | Media-Helfer wie `getImageDimensions(file)` |
---
diff --git a/components/canvas/canvas-delete-handlers.ts b/components/canvas/canvas-delete-handlers.ts
new file mode 100644
index 0000000..fba9802
--- /dev/null
+++ b/components/canvas/canvas-delete-handlers.ts
@@ -0,0 +1,185 @@
+import { useCallback } from "react";
+import type { Dispatch, MutableRefObject, SetStateAction } from "react";
+import {
+ getConnectedEdges,
+ type Edge as RFEdge,
+ type Node as RFNode,
+ type OnBeforeDelete,
+} from "@xyflow/react";
+
+import type { Id } from "@/convex/_generated/dataModel";
+import { computeBridgeCreatesForDeletedNodes } from "@/lib/canvas-utils";
+import { toast } from "@/lib/toast";
+import { msg, type CanvasNodeDeleteBlockReason } from "@/lib/toast-messages";
+
+import { getNodeDeleteBlockReason, isOptimisticEdgeId } from "./canvas-helpers";
+
+type UseCanvasDeleteHandlersParams = {
+ canvasId: Id<"canvases">;
+ nodes: RFNode[];
+ edges: RFEdge[];
+ deletingNodeIds: MutableRefObject>;
+ setAssetBrowserTargetNodeId: Dispatch>;
+ runBatchRemoveNodesMutation: (args: { nodeIds: Id<"nodes">[] }) => Promise;
+ runCreateEdgeMutation: (args: {
+ canvasId: Id<"canvases">;
+ sourceNodeId: Id<"nodes">;
+ targetNodeId: Id<"nodes">;
+ sourceHandle?: string;
+ targetHandle?: string;
+ }) => Promise;
+ runRemoveEdgeMutation: (args: { edgeId: Id<"edges"> }) => Promise;
+};
+
+export function useCanvasDeleteHandlers({
+ canvasId,
+ nodes,
+ edges,
+ deletingNodeIds,
+ setAssetBrowserTargetNodeId,
+ runBatchRemoveNodesMutation,
+ runCreateEdgeMutation,
+ runRemoveEdgeMutation,
+}: UseCanvasDeleteHandlersParams): {
+ onBeforeDelete: OnBeforeDelete;
+ onNodesDelete: (deletedNodes: RFNode[]) => void;
+ onEdgesDelete: (deletedEdges: RFEdge[]) => void;
+} {
+ const onBeforeDelete = useCallback(
+ async ({
+ nodes: matchingNodes,
+ edges: matchingEdges,
+ }: {
+ nodes: RFNode[];
+ edges: RFEdge[];
+ }) => {
+ if (matchingNodes.length === 0) {
+ return true;
+ }
+
+ const allowed: RFNode[] = [];
+ const blocked: RFNode[] = [];
+ const blockedReasons = new Set();
+ for (const node of matchingNodes) {
+ const reason = getNodeDeleteBlockReason(node);
+ if (reason !== null) {
+ blocked.push(node);
+ blockedReasons.add(reason);
+ } else {
+ allowed.push(node);
+ }
+ }
+
+ if (allowed.length === 0) {
+ const { title, desc } = msg.canvas.nodeDeleteBlockedExplain(blockedReasons);
+ toast.warning(title, desc);
+ return false;
+ }
+
+ if (blocked.length > 0) {
+ const { title, desc } = msg.canvas.nodeDeleteBlockedPartial(
+ blocked.length,
+ blockedReasons,
+ );
+ toast.warning(title, desc);
+ return {
+ nodes: allowed,
+ edges: getConnectedEdges(allowed, matchingEdges),
+ };
+ }
+
+ return true;
+ },
+ [],
+ );
+
+ const onNodesDelete = useCallback(
+ (deletedNodes: RFNode[]) => {
+ const count = deletedNodes.length;
+ if (count === 0) return;
+
+ const idsToDelete = deletedNodes.map((node) => node.id);
+ for (const id of idsToDelete) {
+ deletingNodeIds.current.add(id);
+ }
+
+ const removedTargetSet = new Set(idsToDelete);
+ setAssetBrowserTargetNodeId((current) =>
+ current !== null && removedTargetSet.has(current) ? null : current,
+ );
+
+ const bridgeCreates = computeBridgeCreatesForDeletedNodes(
+ deletedNodes,
+ nodes,
+ edges,
+ );
+ const edgePromises = bridgeCreates.map((bridgeCreate) =>
+ runCreateEdgeMutation({
+ canvasId,
+ sourceNodeId: bridgeCreate.sourceNodeId,
+ targetNodeId: bridgeCreate.targetNodeId,
+ sourceHandle: bridgeCreate.sourceHandle,
+ targetHandle: bridgeCreate.targetHandle,
+ }),
+ );
+
+ void Promise.all([
+ runBatchRemoveNodesMutation({
+ nodeIds: idsToDelete as Id<"nodes">[],
+ }),
+ ...edgePromises,
+ ])
+ .then(() => {
+ for (const id of idsToDelete) {
+ deletingNodeIds.current.delete(id);
+ }
+ })
+ .catch((error: unknown) => {
+ console.error("[Canvas] batch remove failed", error);
+ for (const id of idsToDelete) {
+ deletingNodeIds.current.delete(id);
+ }
+ });
+
+ const { title } = msg.canvas.nodesRemoved(count);
+ toast.info(title);
+ },
+ [
+ canvasId,
+ deletingNodeIds,
+ edges,
+ nodes,
+ runBatchRemoveNodesMutation,
+ runCreateEdgeMutation,
+ setAssetBrowserTargetNodeId,
+ ],
+ );
+
+ const onEdgesDelete = useCallback(
+ (deletedEdges: RFEdge[]) => {
+ for (const edge of deletedEdges) {
+ if (edge.className === "temp") {
+ continue;
+ }
+ if (isOptimisticEdgeId(edge.id)) {
+ continue;
+ }
+
+ void runRemoveEdgeMutation({ edgeId: edge.id as Id<"edges"> }).catch(
+ (error) => {
+ console.error("[Canvas edge remove failed] edge delete", {
+ edgeId: edge.id,
+ edgeClassName: edge.className ?? null,
+ source: edge.source,
+ target: edge.target,
+ error: String(error),
+ });
+ },
+ );
+ }
+ },
+ [runRemoveEdgeMutation],
+ );
+
+ return { onBeforeDelete, onNodesDelete, onEdgesDelete };
+}
diff --git a/components/canvas/canvas-generation-failures.ts b/components/canvas/canvas-generation-failures.ts
new file mode 100644
index 0000000..de7bf88
--- /dev/null
+++ b/components/canvas/canvas-generation-failures.ts
@@ -0,0 +1,70 @@
+import { useEffect, useRef } from "react";
+
+import type { Doc } from "@/convex/_generated/dataModel";
+import { toast } from "@/lib/toast";
+import { msg } from "@/lib/toast-messages";
+
+import {
+ GENERATION_FAILURE_THRESHOLD,
+ GENERATION_FAILURE_WINDOW_MS,
+} from "./canvas-helpers";
+
+export function useGenerationFailureWarnings(
+ convexNodes: Doc<"nodes">[] | undefined,
+): void {
+ const recentGenerationFailureTimestampsRef = useRef([]);
+ const previousNodeStatusRef = useRef