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.
Summary
Functions
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.
Heroicon name for an action atom, for the activity timeline's per-row icon. Covers both PaperTrail/AshEvents content actions and Oban job outcomes.
The label actor_labels/2 gave user_id: "System" for no user, "Unknown" for an id it was not
given.
What the log calls each of user_ids, for viewer, as %{user_id => label}.
Deterministic palette tone for an avatar, derived from any seed value.
Fill in whatever key a source left out of an entry.
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.
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.
Up to two uppercase initials for an actor display name ("?" when blank).
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).
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.
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.
Map a PaperTrail action atom (e.g. :create, :master_destroy) to a
severity level used by the activity feed UI.
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.
Coerce a timestamp from a raw SQL row into a UTC DateTime. Postgres
timestamp without time zone columns come back as NaiveDateTime.
Tailwind bg-/text- classes for a badge/avatar palette tone (the timeline UI).
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.
The words label stands for in the process's locale, in the default Gettext domain; label
itself when no translation has it.
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).
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).
Functions
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.
Heroicon name for an action atom, for the activity timeline's per-row icon. Covers both PaperTrail/AshEvents content actions and Oban job outcomes.
The label actor_labels/2 gave user_id: "System" for no user, "Unknown" for an id it was not
given.
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.
Deterministic palette tone for an avatar, derived from any seed value.
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".
@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.
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.
Up to two uppercase initials for an actor display name ("?" when blank).
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).
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.
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.
Map a PaperTrail action atom (e.g. :create, :master_destroy) to a
severity level used by the activity feed UI.
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.
Coerce a timestamp from a raw SQL row into a UTC DateTime. Postgres
timestamp without time zone columns come back as NaiveDateTime.
Tailwind bg-/text- classes for a badge/avatar palette tone (the timeline UI).
@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.
The words label stands for in the process's locale, in the default Gettext domain; label
itself when no translation has it.
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).
@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).