FlowView: Persistent Node Positions

Goal

Store FlowView node positions in the database. On drag-end, save position. When a new process appears without a stored position, auto-layout it sensibly relative to existing positioned nodes, then save that position too.


Backend

1. Migration — add flow_x, flow_y to processes table

New Alembic migration adding two nullable Float columns to processes. Nullable because positions are assigned by the frontend on first render.

2. Model — Process (backend/models/process.py)

3. Router — new PATCH /process/{process_id}/position endpoint (backend/routers/processes.py)


Frontend

4. API helper (frontend/src/datamodel/api.js)

Add updateProcessPosition(processId, x, y)PATCH /process/{processId}/position.

5. FlowView — position sourcing (frontend/src/widgets/FlowView/index.js)

Change the node position assignment to a three-tier fallback:

const position =
  userPositionedNodes.current[p.id]          // 1. dragged this session
  || (p.flow_x != null && p.flow_y != null
       ? { x: p.flow_x, y: p.flow_y }        // 2. stored in DB
       : null)
  || autoLayout(p, depth, layer);             // 3. compute fresh

When the auto-layout path fires (tier 3), call updateProcessPosition(p.id, pos.x, pos.y) to persist it immediately.

6. FlowView — drag-end save

handleNodesChangeWithTracking already detects change.type === 'position' && change.dragging === false. After updating userPositionedNodes.current, also call updateProcessPosition(change.id, change.position.x, change.position.y).

7. FlowView — remove position reset on new process

Currently newProcessAdded sets userPositionedNodes.current = {}, wiping all in-session positions. Remove this: the new process simply won't be in userPositionedNodes.current, so it falls through to tier 2 (DB) or tier 3 (auto-layout). Existing nodes keep their positions.

8. Auto-layout for unpositioned nodes (tier 3)

Keep the existing depth/layer algorithm but avoid placing new nodes on top of already-positioned nodes in the same layer. Concretely: within a depth layer, find the maximum flow_y among nodes that already have stored positions; place the new node below that. This prevents a newly added node from spawning on top of a manually repositioned one.