# `PhoenixKitWeb.Live.Dashboard.Overview`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.7/lib/phoenix_kit_web/live/dashboard/overview.ex#L1)

The LiveView half of the dashboard overview — the data and the message
handling that back `PhoenixKitWeb.Components.Core.DashboardOverview`.

Used by `/admin` (`PhoenixKitWeb.Live.Dashboard`), whose operator half the
overview is. The deprecated `/dashboard`
(`PhoenixKitWeb.Live.Dashboard.Index`) is a separate page and does NOT use
this.

## Gate the DATA, not only the markup

Hiding a card in HEEx is cosmetic — the operator aggregates would still have
run. `assign_scope_gates/1` therefore decides FIRST and works SECOND: a
visitor who may not see the statistics causes

  * no `Roles.get_extended_stats/0`, `Sessions.get_session_stats/0` or
    `Presence.get_presence_stats/0`,
  * no migration-version read,
  * and no `Events.subscribe_to_*` at all,

and the six statistics assigns are set to `nil`.

## One gate site, re-run on every scope change

Every scope-derived assign on this page — `:can_access_admin_area?`, the six
card gates, `:show_statistics`, the six statistics assigns — is computed in
`assign_scope_gates/1` and NOWHERE else. Mount calls it through
`assign_overview/3`; a mid-session permission change calls it through
`phoenix_kit_scope_changed/1`, the callback `use` injects and
`PhoenixKitWeb.Users.Auth`'s scope-refresh hook invokes right after it
reassigns `:phoenix_kit_current_scope`. A gate added to that function is
therefore recomputed for free, and cannot be the one that gets forgotten.

This matters because `/admin` is the guaranteed landing: a visitor is NOT
evicted from it when their rights change, so the page has to survive the
change in place. Without recomputation a revoked operator kept a card
pointing at a page that now rejects them, beside a sidebar (which re-derives
every render) that disagreed.

The statistics **subscriptions** move in both directions with the gate:
`sync_statistics_subscription/2` subscribes a visitor who gains the rights
mid-session and unsubscribes one who loses them. An unsubscribe, not just a
hidden card — a hidden card still costs three aggregates per broadcast.
`PhoenixKit.Admin.Events` had no unsubscribe counterpart before this page
needed one; it does now (`unsubscribe_from_stats/0` and friends), because
PhoenixKit runs its own PubSub instance and a caller cannot reach
`Phoenix.PubSub.unsubscribe/2` for it.

`refresh_statistics/1` re-derives the verdict from the CURRENT scope rather
than trusting `:show_statistics`, because `phx-click` events are
client-supplied — the button being hidden is not a guarantee that the event
cannot arrive.

## Visibility rules

| Block | Gate |
|---|---|
| Users / Roles / Sessions / Live Activity / Add User cards | `PhoenixKitWeb.Users.Auth.can_access_admin_view?/2` on each card's destination LiveView |
| Email card | the module being loaded and enabled, AND `can_access_admin_view?/2` on its admin LiveView |
| Platform Statistics, System Information, Refresh | `Scope.holds_all_enabled_permissions?/1` |

The card rule is derived, never restated: the card and the page it links to
ask the same function, so "a card is visible iff the visitor can open it"
holds by construction. The statistics have no destination LiveView to derive
from, so they use the role-agnostic "can reach everything" check — the same
posture the unmapped-admin-view fallback takes.

## Usage

    use PhoenixKitWeb.Live.Dashboard.Overview

    def mount(_params, session, socket) do
      {:ok, Overview.assign_overview(socket, session, Routes.path("/admin"))}
    end

`use` injects three things: `handle_event("refresh_stats", …)`, ONE
`handle_info/2` clause guarded by `is_overview_message/1`, and the
`phoenix_kit_scope_changed/1` callback described above.

That single guarded clause replaced nine shape-matched ones deliberately. A
socket can still be handed a message from a topic it has just left — the
unsubscribe and the broadcast race — and it will also see any FUTURE arity of
a message it does subscribe to. Nine exact-shape clauses turn either into an
unmatched `handle_info` and a crashed LiveView. The guard matches on the
message TAG alone and `apply_statistics_message/2` ends in a catch-all, so an
in-flight or reshaped message is a no-op rather than a crash — and, because
that function re-checks the gate first, never an operator query on behalf of
someone who just lost the permission.

The guard is deliberately narrow: it names ten tags and nothing else, so a
host LiveView keeps full control of every other message. It does mean any
additional `handle_event`/`handle_info` clauses must be defined together with
these (Elixir warns when clauses of one function are not grouped, and
`mix precommit` compiles with `--warnings-as-errors`).

The attrs are passed to the component one by one rather than via a
`Map.take/2` spread: an explicit attr keeps LiveView's change tracking, so a
presence update re-sends only the tile that changed instead of the whole
overview.

# `__using__`
*macro* 

Injects the statistics event clause, the statistics message clause and the
scope-change callback into a dashboard LiveView.

See the module documentation for what this means for a host LiveView that
defines `handle_event/3` or `handle_info/2` clauses of its own.

# `apply_statistics_message`

```elixir
@spec apply_statistics_message(Phoenix.LiveView.Socket.t(), tuple()) ::
  Phoenix.LiveView.Socket.t()
```

Applies one statistics/session/presence broadcast to the socket.

The body of the single `handle_info/2` clause `use` injects. Two guarantees
a set of exact-shape `handle_info` clauses could not give:

  * **it never crashes.** Any message the guard admits is handled — including
    one whose shape changed and one that arrived after this socket left the
    topic (unsubscribe races a broadcast already in flight). The fallback is
    a stale tile, not a dead LiveView.
  * **it never queries for someone who may not see the result.** The gate is
    re-checked before anything runs, so a revoked operator's in-flight
    messages cost nothing, and the three `reload_*` aggregates stop with the
    permission rather than with the socket.

# `assign_overview`

```elixir
@spec assign_overview(Phoenix.LiveView.Socket.t(), map(), String.t()) ::
  Phoenix.LiveView.Socket.t()
```

Mount-time entry point: tracks the visitor's presence on `page_path`, then
computes every gate through `assign_scope_gates/1`.

`page_path` is the page the caller actually serves — it is reported to
`Presence` as the visitor's `current_page`, and the Live Activity page reads
it back, so it belongs to the caller rather than being hardcoded here. Pass
an already-resolved path (`PhoenixKit.Utils.Routes.path/1`).

Presence tracking is the one thing here that happens ONCE per mount rather
than on every scope change — it reports where the socket is, which a
permission change does not alter.

# `assign_scope_gates`

```elixir
@spec assign_scope_gates(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
```

Computes EVERY scope-derived assign on the dashboard from
`:phoenix_kit_current_scope`, and reconciles the statistics subscriptions
with the verdict.

The single gate site. Called from `assign_overview/3` at mount and from
`phoenix_kit_scope_changed/1` whenever a permission change reassigns the
scope under a live socket, so the page never renders a gate that predates the
visitor's current rights. Idempotent: calling it with an unchanged scope
re-derives the same values and leaves the subscriptions alone.

# `is_overview_message`
*macro* 

Whether `message` is a broadcast from one of the three statistics topics.

Guards the single `handle_info/2` clause `use` injects. Tag-only by design:
see the "Usage" section of the module documentation for why matching the
full shape is a crash waiting for a downgrade or a schema change.

# `refresh_statistics`

```elixir
@spec refresh_statistics(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
```

Re-runs the operator aggregates behind the Refresh button.

Re-derives the verdict from the current scope rather than reading
`:show_statistics`: the button is hidden for everyone else, but a hidden
button does not stop a crafted `phx-click` from arriving, and these are
exactly the queries the gate exists to withhold. (The assign would answer the
same today — it is recomputed on every scope change — but a gate fed by
client-triggered input should not depend on that staying true.)

# `statistics_visible?`

```elixir
@spec statistics_visible?(PhoenixKit.Users.Auth.Scope.t() | nil) :: boolean()
```

Whether `scope` may see the operator statistics — Platform Statistics, System
Information and the Refresh button.

These blocks have no destination LiveView to derive a gate from, so they use
the role-agnostic "holds every grantable permission" check rather than a role
name. `nil` scope → `false`.

---

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