# Operate Shared Canvases

Shared mode is an operational choice. It changes where accepted canvas state
lives, how participants find the coordinator, how state is recovered, and what
the application must supervise.

It does not change how canvas commands work.

Use this guide after the basic shared-canvas setup is working.

## One-Node Backend

For a single BEAM node, use `LivePanels.Canvas.Shared.Local`:

```elixir
children = [
  {Phoenix.PubSub, name: MyApp.PubSub},
  LivePanels.Application
]
```

```elixir
defmodule MyApp.CanvasLayouts do
  use LivePanels.Canvas.LayoutStore.Ecto,
    repo: MyApp.Repo,
    schema: MyApp.LivePanelsCanvasLayout
end

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

The local backend uses local `Registry`, `DynamicSupervisor`, participant
tracking, and the configured layout store. It is the default backend shape for
development and one-node production.

You can also add backend child specs yourself:

```elixir
children =
  [
    {Phoenix.PubSub, name: MyApp.PubSub},
    LivePanels.Application
  ] ++ LivePanels.Canvas.Shared.child_specs({LivePanels.Canvas.Shared.Local, layout_store: MyApp.CanvasLayouts})
```

## Clustered Backend

For clustered coordinator lookup and supervision, configure a distributed
adapter such as `LivePanels.Canvas.Shared.Horde`:

```elixir
use LivePanels.Canvas,
  pubsub: MyApp.PubSub,
  session: [
    mode: :shared,
    id: {:param, "room_id"},
    backend:
      {LivePanels.Canvas.Shared.Horde,
       process_registry: MyApp.LivePanelsRegistry,
       dynamic_supervisor: MyApp.LivePanelsSupervisor,
       participant_tracker_supervisor: MyApp.LivePanelsParticipantTrackerSupervisor,
       layout_store: MyApp.CanvasLayouts}
  ]
```

The application still owns Horde membership, node discovery, deployment topology,
database choice, and release checks. LivePanels only needs the backend pieces
required to find or start the shared coordinator and participant tracker.

## Persistent Recovery

A shared coordinator can recover only what the layout store can load.

For production, use `LivePanels.Canvas.LayoutStore.Ecto` or another durable
`LivePanels.Canvas.LayoutStore`:

```elixir
defmodule MyApp.CanvasLayouts do
  use LivePanels.Canvas.LayoutStore.Ecto,
    repo: MyApp.Repo,
    schema: MyApp.LivePanelsCanvasLayout
end
```

Store the versioned layout payload unchanged. It may contain item topology,
vector state, and graph connectors between persistent items.

For PostgreSQL, the schema can store the payload as an array of maps:

```elixir
defmodule MyApp.LivePanelsCanvasLayout do
  use Ecto.Schema

  schema "livepanels_canvas_layouts" do
    field :canvas_id, :string
    field :layout, {:array, :map}, default: []

    timestamps(type: :utc_datetime_usec)
  end
end
```

```elixir
create table(:livepanels_canvas_layouts) do
  add :canvas_id, :string, null: false
  add :layout, {:array, :map}, null: false, default: []

  timestamps(type: :utc_datetime_usec)
end

create unique_index(:livepanels_canvas_layouts, [:canvas_id])
```

Use one shared layout store per shared canvas id. Do not let each participant
save an independent version of the same shared room.

## Check Backend Health

Run `LivePanels.Canvas.Shared.backend_health_check!/1` in an application test, release check, or smoke
test:

```elixir
LivePanels.Canvas.Shared.backend_health_check!(
  pubsub: MyApp.PubSub,
  backend:
    {LivePanels.Canvas.Shared.Local,
     layout_store: MyApp.CanvasLayouts},
  canvas_id: "backend-health-check-room"
)
```

The backend health check exercises the shared backend with a generated canvas. It checks
coordinator startup, participant tracking, command application, PubSub fanout,
and layout persistence wiring.

Use `LivePanels.Canvas.Shared.backend_health_check/1` when you want `{:ok, report}` or
`{:error, reason}` instead of an exception.

## Recovery Behavior

Expected recovery behavior:

- if the coordinator dies, participant LiveViews try to ensure or reconnect to
  the coordinator for the same canvas id;
- if durable layout exists, the restarted coordinator can restore the shared
  canvas from the layout store;
- if a participant disconnects, the participant tracker eventually emits a
  leave diff;
- if a participant held an interaction lease, the lease sweep can release it;
- if persistence fails, LivePanels logs and emits telemetry instead of crashing
  the canvas.

Applications should treat layout persistence failure as operationally important even
when the current in-memory canvas keeps running.

## Telemetry

Attach handlers to LivePanels telemetry events:

```elixir
:telemetry.attach_many(
  "my-app-livepanels-shared",
  [
    [:livepanels, :session, :started],
    [:livepanels, :session, :participant, :joined],
    [:livepanels, :session, :participant, :left],
    [:livepanels, :session, :recovery],
    [:livepanels, :broadcast, :state],
    [:livepanels, :broadcast, :delta],
    [:livepanels, :broadcast, :profile],
    [:livepanels, :layout, :persisted],
    [:livepanels, :layout, :persistence_failed]
  ],
  &MyApp.LivePanelsTelemetry.handle_event/4,
  nil
)
```

Useful operational signals:

- coordinator starts by canvas id and backend;
- participant join and leave counts;
- recovery status and reason;
- broadcast payload size and fanout time;
- layout persistence success and failure.

The public telemetry catalog is available through `LivePanels.Telemetry`.

## Participant Session Metadata

Applications can pass participant metadata through the LiveView session under
`LivePanels.Canvas.Shared.Session.session_key/0`.

Use this for viewer id, role, capabilities, or region id when shared runtime
policy should know who is connected. Do not put secrets or large domain records
in participant metadata.

Shared participants without a session role enter as `:viewer`. Pass
`"role" => "editor"` in the participant session for browsers that should mutate
canvas state.

Keep authorization in the application route, mount logic, and canvas policy. Shared
mode assumes the user is allowed to join the canvas you mounted. Canvas policy
runs in the coordinator command path; configure its timeout to bound command
acceptance checks.

## Known Limits

Plan for these boundaries:

- in-progress drawing drafts broadcast throttled intent, but are not canonical
  state until committed;
- application UI state is not automatically shared;
- durable recovery depends on the configured layout store;
- clustered behavior depends on the application's distributed registry/supervisor
  setup;
- product-level conflict resolution belongs in the application or policy layer.

If your app needs domain-level collaboration beyond canvas state, model that
domain data separately and use LivePanels commands only for the canvas effects.
