# `MishkaCmsCore.Runtime.SiteAvailability`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/site_availability.ex#L5)

Whether a site is up, and who may reach it when it is not.

A site is unavailable in two ways:

| state | set on | who may reach it |
|-------|--------|------------------|
| `:inactive` | Admin → Runtime → Sites | a master admin of the surface asked about |
| `:offline` | Settings → "Take this site offline" (the site's own or the global switch) | its own admins and master admins of the surface asked about |

A site the site cache does not know — archived, deleted, or not loaded — is `:inactive`, so a gate
asking about it closes rather than opens.

Every answer is read from ETS: `MishkaCmsCore.Runtime.States.SiteState` for `active`,
`MishkaCmsCore.Settings.offline?/1` for the switch, and the permissions the user was loaded with. No
query, no process call, so it can sit on every request.

An admin is an admin of a surface: the screens (`:dashboard`, the default) or the admin JSON:API
and GraphQL (`:api`), or `:any` for either one. See `MishkaCmsCore.Accounts.Helper.master_admin?/2`
and `MishkaCmsCore.Accounts.Helper.site_admin?/3` for who is who.

## Open LiveViews

`on_mount/4` closes an open LiveView the moment its site closes to its user: on a live navigation,
on every event, and when `changed/1` announces that the site's state changed. Add it to every
LiveView:

    on_mount MishkaCmsCore.Runtime.SiteAvailability

A view on a sign-in address (`MishkaCmsCore.Runtime.Plugs.SiteLookupPlug.sign_in_path?/2`, read
from the address of every navigation) stays open while its site is offline, so the site's admins
can sign in; every view closes on an inactive site. A refused view is redirected to the address it
was on, a full page load that the pipeline answers.

A view that shows a record of another site — a preview, opened on any address — follows that site
too, with `follow/3` from its `mount/3`: it is refused there, and closed later, by the rule
`allowed?/2` answers for that site.

A view whose viewer the site's closed sign-in signs out (`MishkaCmsCore.Accounts.AuthGate.signs_out?/2`)
reloads the address it was on the same way, and the pipeline signs them out there. The gate's
settings call `changed/1` as the offline switch does.

## Events

- `runtime:state:site:availability_changed` — `%{key: site_id}`, `nil` for every site, each time
  `changed/1` is called.
- `web:live:site_unavailable` — the socket an open LiveView is about to be sent away with, with
  `%{site_id: site_id, path: path}` as its meta. A plugin may redirect it elsewhere.

# `state`

```elixir
@type state() :: :open | :offline | :inactive
```

# `allowed?`

```elixir
@spec allowed?(
  String.t() | nil,
  map() | nil,
  MishkaCmsCore.Accounts.Helper.admin_surface()
) ::
  boolean()
```

Whether `user` may reach the site `site_id` through `surface` — `:dashboard` (the default) for its
pages, files, previews and screens, `:api` for its admin APIs, `:any` for either.

`nil` for `user` is a visitor who is not signed in.

    SiteAvailability.allowed?(site.id, user)
    SiteAvailability.allowed?(site.id, user, :api)

# `allowed_in?`

```elixir
@spec allowed_in?(
  state(),
  String.t() | nil,
  map() | nil,
  MishkaCmsCore.Accounts.Helper.admin_surface()
) ::
  boolean()
```

`allowed?/3` for a state already read with `state/1`.

# `changed`

```elixir
@spec changed(String.t() | nil) :: :ok
```

Tells this node's open LiveViews that `site_id`'s state may have changed, and disconnects this
node's sockets bound to it (`socket_id/1`) when it is no longer open; `nil` for every site.

`SiteState` and `SettingsState` call it after writing, so a view that checks again reads the new
state. Local to the node, because each node's caches call it for their own write.

# `follow`

```elixir
@spec follow(Phoenix.LiveView.Socket.t(), String.t() | nil, map()) ::
  {:ok, Phoenix.LiveView.Socket.t()}
  | {:error, String.t() | nil, :offline | :inactive}
```

Makes an open LiveView follow `site_id` as well as its host's site, from its `mount/3`.

`{:error, site_id, state}` — the site that is closed to the viewer, the host's or `site_id` — when
either is closed now; otherwise the view is closed as soon as one is. `nil` — a global record —
follows the host's site only. Needs `on_mount/4`.

# `on_mount`

Closes an open LiveView the moment its site closes to its user. See the moduledoc.

# `open_site_ids`

```elixir
@spec open_site_ids() :: [String.t()]
```

The ids of every site that is up: active, and not offline by its own or the global switch.

# `socket_id`

```elixir
@spec socket_id(String.t()) :: String.t()
```

The id a socket bound to `site_id` answers to from `c:Phoenix.Socket.id/1`, as the GraphQL
socket's does. `changed/1` disconnects every socket under it once the site is no longer open, and
each one that reconnects is asked again.

Needs the endpoint's PubSub server to be `MishkaCmsCore.PubSub`.

# `state`

```elixir
@spec state(String.t() | nil) :: state()
```

The site's state by its id.

    SiteAvailability.state(site.id)
    #=> :offline

---

*Consult [api-reference.md](api-reference.md) for complete listing*
