# `MishkaCmsCore.Activity.Helpers`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/activity/_helpers.ex#L5)

Shared helpers used by both the PaperTrail adapter's live-broadcast path
and the version-resource calculations.

Keeping these in one module makes the calc and the live path consistent:
if you change how a destroy action maps to a severity, both code paths
pick it up.

# `action_atom`

Normalise an action coming back from a raw SQL row (a string) to the canonical
atom, or `:other` for an unknown cross-extension action. Never creates a new
atom (no leak, no raise) — a foreign action name simply maps to `:other`.

# `action_icon`

Heroicon name for an action atom, for the activity timeline's per-row icon.
Covers both PaperTrail/AshEvents content actions and Oban job outcomes.

# `actor_label`

```elixir
@spec actor_label(map(), String.t() | nil) :: String.t()
```

The label `actor_labels/2` gave `user_id`: "System" for no user, "Unknown" for an id it was not
given.

# `actor_labels`

```elixir
@spec actor_labels([String.t() | nil], map() | nil) :: %{
  required(String.t()) =&gt; String.t()
}
```

What the log calls each of `user_ids`, for `viewer`, as `%{user_id => label}`.

The email of an actor `viewer` may read (see `MishkaCmsCore.Activity.Query.actor_emails/2`),
"Master" for a master's id, "Unknown" for any other id. Two reads for the whole list. Read a label
back with `actor_label/2`.

# `avatar_tone`

Deterministic palette tone for an avatar, derived from any seed value.

# `complete_entry`

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

Fill in whatever key a source left out of an entry.

Sources are pluggable and each writes its own map literal, so one that forgets a key hands a
renderer a map that `entry.that_key` raises on — which in a LiveView is a dead process, and on the
dashboard a reconnect loop. Both places an entry enters the system pass it through here first (the
DB merge in `MishkaCmsCore.Activity.Query.decode/2`, the live feed in
`MishkaCmsCoreWeb.Live.Admin.Components.ActivityFeed`), so dot-access downstream of either seam is
safe. A key the entry already carries is never overwritten, and extra keys ride along untouched.

`nil` passes through: the live path uses it to mean "no source claimed this broadcast".

# `entry_keys`

```elixir
@spec entry_keys() :: [atom()]
```

The keys every activity entry carries — the same list `MishkaCmsCore.Activity.Behaviours.Source`
states as a type, as data, so a test can hold a source to it.

# `humanize_module`

Human-readable label for a resource module. Splits the module name on
capitals (`MediaCategory` → `Media Category`). Used by the live path
where the version row's `:resource_label` calculation isn't loaded.

# `initials`

Up to two uppercase initials for an actor display name (`"?"` when blank).

# `paper_trail_badge`

Map a PaperTrail action atom to a `{badge_label, tone}` tuple used by
the activity feed UI. Tone is one of the Tailwind palette keys the
template knows how to render (`:emerald`, `:sky`, `:amber`, `:rose`,
`:indigo`, `:slate`).

# `reach`

```elixir
@spec reach(map() | nil) :: :everything | {:site, String.t()} | :nothing
```

Whose activity `viewer` may see: `:everything` for a master (a user whose `site_id` is nil),
`{:site, site_id}` for a site user, and `:nothing` for no viewer or anything that is not a user.

Every source, the activity log's read and the live feed ask this, so a missing viewer is never
read as a master.

# `resources_for`

```elixir
@spec resources_for(map() | nil) :: :all | [String.t()]
```

Whose entries `viewer`'s Activity log shows, by area: `:all` for a full dashboard admin
(`admin:dashboard:*`), otherwise the resources — as `"Elixir.Module"` strings, the way the log
names them — whose areas (`MishkaCmsCore.Activity.Discovery.areas/1`) the viewer holds on the
dashboard. A resource that names no area is a full admin's alone.

# `severity_for`

Map a PaperTrail action atom (e.g. `:create`, `:master_destroy`) to a
severity level used by the activity feed UI.

# `shows?`

```elixir
@spec shows?(:all | [String.t()], module() | String.t() | nil) :: boolean()
```

Whether a log showing `resources` (`resources_for/1`) shows `resource`, a module or its
`"Elixir.Module"` string. `:all` shows everything; otherwise an entry with no resource is shown to
nobody.

# `to_datetime`

Coerce a timestamp from a raw SQL row into a UTC `DateTime`. Postgres
`timestamp without time zone` columns come back as `NaiveDateTime`.

# `tone_class`

Tailwind `bg-/text-` classes for a badge/avatar palette tone (the timeline UI).

# `tone_for`

```elixir
@spec tone_for(atom()) :: :created | :updated | :destroyed | nil
```

Map a content-change action atom to the TONE the activity timeline tints its row by — the colour
that says WHAT happened, kept apart from `severity_for/1` (the KPI cards count by severity, so it
must not carry action meaning). A create reads `:created` (green), an update `:updated` (blue) and
any destroy — the same `warn_actions/0` — `:destroyed` (red). Anything that is not one of those
three (a publish, an unarchive, a job outcome, a compile) is `nil`, so the timeline falls back to
colouring that row by its `severity`.

# `translate`

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

The words `label` stands for in the process's locale, in the default Gettext domain; `label`
itself when no translation has it.

# `visible_to?`

Whether a live entry is visible to `viewer` on the realtime feed. A master (`site_id`
nil) sees every site; a tenant sees only its OWN site (never another tenant, never
global); no viewer, or anything that is not a user, sees nothing. The live feed is
subscription-scoped to a tenant's own site, so global/other-site broadcasts never
reach a tenant here; the per-resource "own + global" rule for shared runtime
resources is applied on the bulk DB read (see
`MishkaCmsCore.Activity.Query.shares_globals?/1`).

# `warn_actions`

```elixir
@spec warn_actions() :: [atom()]
```

The actions the activity feed reads as `:warn` — a destroy of any kind. The one list behind both
`severity_for/1` and any consumer that has to ask for them in a query (e.g. the event log's KPI
counts).

---

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