MishkaCmsCore.Activity.Helpers (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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

action_atom(action)

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(action)

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(labels, user_id)

@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(user_ids, viewer)

@spec actor_labels([String.t() | nil], map() | nil) :: %{
  required(String.t()) => 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(seed)

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

complete_entry(entry)

@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()

@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(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(name)

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

paper_trail_badge(action)

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(arg1)

@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(viewer)

@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(action)

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

shows?(resources, resource)

@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(at)

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

tone_class(tone)

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

tone_for(action)

@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(label)

@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?(entry_site_id, viewer)

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()

@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).