# Persist And Share Graph Canvases

Graph apps need topology to survive reloads and collaboration.

LivePanels keeps graph connectors inside canvas state so layouts, history,
clipboard, and shared mode can handle graph behavior without a second graph
store.

## Persist Graph Layouts

Configure a layout store:

```elixir
use LivePanels.Canvas,
  pubsub: MyApp.PubSub,
  session: [layout_store: MyApp.GraphLayouts]

use LivePanels.Toolkit.Graph
```

The versioned layout payload can include:

- node item ids, types, placement, visibility, and z-order;
- persistent node data;
- graph connectors between persistent items;
- connector presentation that belongs to canvas state;
- vector state if the graph app also uses vector items.

Persist the layout payload unchanged.

## Restore Requires Item Types And Ports

When restoring a graph layout, the canvas must know the item types and ports
referenced by the saved graph.

Before shipping persistence, test:

- every saved node type still exists;
- every saved connector endpoint still names a declared port;
- saved connector endpoints still match declared port names;
- `max_connections` rules still allow restored topology;
- hidden or removed item types do not break room recovery.

If your app changes port names between releases, transform app-owned layout data
before restoring connectors.

## Clipboard

Graph clipboard copies selected items and internal connector items:

```heex
<button {copy_selection_attrs()}>Copy</button>
<button {paste_selection_attrs()}>Paste</button>
```

If selected nodes have connectors to unselected nodes, only internal connector
items are copied with the selected group. This prevents pasted graph fragments
from pointing at the original graph by accident.

## History

Graph commands participate in canvas history:

```heex
<button {undo_attrs()}>Undo</button>
<button {redo_attrs()}>Redo</button>
```

Use transactions when one user action should create one history entry:

```elixir
socket =
  LivePanels.Canvas.transaction!(socket, fn tx ->
    tx
    |> LivePanels.Canvas.Item.add!(:step, id: "step-4")
    |> LivePanels.Graph.connect!(from: {"step-3", "out"}, to: {"step-4", "in"})
  end)
```

## Shared Graph Canvases

Enable shared mode normally:

```elixir
use LivePanels.Canvas,
  pubsub: MyApp.PubSub,
  session: [
    mode: :shared,
    id: {:param, "workflow_id"},
    backend:
      {LivePanels.Canvas.Shared.Local,
       layout_store: MyApp.GraphLayouts}
  ]

use LivePanels.Toolkit.Graph
```

Accepted graph changes are shared:

- node add/remove/place;
- connector connect/reconnect/disconnect;
- connector bend updates;
- graph layout commands;
- selection commands when the selected state is shared for that actor.

Connection drafts remain actor-local until completed.

## Participant Viewports

Shared graph state does not mean everyone has the same camera.

Viewport fit is actor-scoped:

```elixir
socket = LivePanels.Graph.fit(socket, :selection)
```

Use this for "follow selected", "fit my view", or "jump to node" interactions.
If the product needs a presenter mode, model that separately in your app or
participant metadata.

## Domain Synchronization

Decide whether canvas connectors are:

- the source of truth for the graph topology;
- a view of domain records;
- an editable projection that writes back to domain records.

For source-of-truth canvas graphs, persistence may be enough.

For domain-backed graphs, store domain records in your app and rebuild or update
canvas connectors from those records. When users edit the canvas, write the
domain record and then submit the LivePanels command for the canvas effect.

## Recovery Checklist

Before production:

- run `mix livepanels.check`;
- run `LivePanels.Canvas.Shared.backend_health_check/1` for shared graph backends;
- verify layout restore with real saved graph payloads;
- test port rename and item type removal scenarios;
- test copy/paste of graph fragments;
- test undo/redo for connect, reconnect, disconnect, layout, and node removal;
- monitor layout persistence failures.

Graph recovery bugs usually come from unstable item ids, unstable port names,
or domain state that was not restored before the canvas layout.
