# `MishkaBlog.Dashboard.Overview`
[🔗](https://github.com/mishka-group/mishka_blog/blob/v0.0.1/lib/mishka_blog/dashboard/overview.ex#L5)

Every number on the blog admin overview page (`/admin/dashboard/blog`), read from the database.

`load/1` gathers the whole page in one pass — the five KPI cards, the eight section cards, the
"This Month" panel and the recent-activity list — and `calendar/2` reads one month of the editorial
calendar. Both are pure reads returning plain numbers and records; every label, delta and
abbreviation is the page's business, not this module's.

Every figure is read as `user` through `MishkaBlog.Dashboard.Scope`: a master (a user whose
`site_id` is nil) through each resource's master read, counting every site, and a site user
through the tenant read under its own site, counting only that one. The resources' policies decide
what the viewer reads. The two join resources — `MishkaBlog.PostView` and `MishkaBlog.TeamMember`
— have no master read and carry no `site_id`; both viewers read them through `:read`, which their
policies keep to the viewer's site, joined to the post or team they belong to
(`not is_nil(post.id)`), so a row of an archived post or team is not counted by anybody.

## What a viewer's figures are read from

Each figure is read only from the blog area the viewer holds (`areas/1`): posts, views, bloggers,
collections, tags, teams and auto posting from `admin:dashboard:blog:post`, comments from
`admin:dashboard:blog:comment` — the permissions those resources' policies ask for. A read of an
area the viewer does not hold is refused and counted as no data, so its counts are zero, its rows
are missing from `:activity`, and an engagement rate — comments over views — is zero unless both
are held. The calendar is a post holder's.

Counts are deliberately many small `Ash.count/2` calls rather than one hand-written query: each
one goes through the resource's own read action, so archival, base filters, multitenancy and
policies stay the resources' business. The caller is expected to run this off the render — see
`MishkaBlogWeb.Live.Admin.Dashboard.Home.Index`, which loads it in a `start_async/3`.

See `MishkaBlog.Dashboard.Widgets.DraftsScheduled` for the same idiom at widget scale.

# `areas`

```elixir
@type areas() :: %{post: boolean(), comment: boolean()}
```

Which blog areas a viewer holds on the dashboard.

# `areas`

```elixir
@spec areas(map() | nil) :: areas()
```

Which blog areas `user` holds on the dashboard: `:post` for `admin:dashboard:blog:post`,
`:comment` for `admin:dashboard:blog:comment`. `nil`, or a user holding neither, holds none.

# `calendar`

```elixir
@spec calendar(map(), Date.t()) :: map()
```

Reads the editorial calendar for the month containing `date`, as `user`.

Returns `%{month: Date.t(), cells: [cell]}` where the cells are the 42 squares of a six-week grid.
A cell outside the month carries `date: nil` and no posts; an in-month cell carries the published
posts whose `published_at` and the scheduled posts whose `scheduled_publish_at` land on that day.

Only those two states appear: a draft has no place on an editorial calendar even when it carries a
leftover `published_at`, and an archived post has already left it.

# `load`

```elixir
@spec load(map()) :: map()
```

Reads every statistic the overview page shows, as `user` (see `MishkaBlog.Dashboard.Scope`).

Returns a flat map of numbers plus `:activity`, the newest few blog records as
`%{kind, title, status, at}` — `kind` being `:post`, `:comment` or `:blogger`, and `status` the
record's own status where it has one (`nil` for a blogger) so the page can word the entry without
claiming a draft was published.

# `post_date`

```elixir
@spec post_date(map()) :: Date.t() | nil
```

The day a post belongs on in the calendar — when it went out, or when it is due to.

---

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