Synthetic, read-only resource representing one unified activity-log entry, and the gervaz timeline table over it.
No database table — Ash.DataLayer.Simple plus a single manual read whose
manual_read/2 delegates to MishkaCmsCore.Activity.Query, which merges every
AshPaperTrail version table + AshEvents event log + Oban jobs in ONE DB-side
UNION. Attributes mirror the MishkaCmsCore.Activity.Behaviours.Source entry
shape so gervaz columns/filters bind by name and the stream gets a stable :id.
Tenant scoping is enforced in the query (Ash won't enforce multitenancy for a
manual-over-Simple read): a master sees every site; a tenant sees its own site
plus global rows of the shared runtime resources (Query.shares_globals?/1),
never another tenant. The :activity_site column is shown to everyone (a tenant's
rows resolve to its own site or "Global").
Summary
Functions
The log's KPI totals for viewer — %{total:, info:, warn:, error:} over the sites and the areas
it may see (MishkaCmsCore.Activity.Query.counts/1). All zero for no viewer.
Builds an %ActivityEntry{} struct from a unified source entry map, deriving
:kind (content vs background job). Used by the manual read and the live path.
Validates that the keys in the provided input are valid for at least one action on the resource.
Same as input/1, except restricts the keys to values accepted by the action provided.
Whether a live record should appear in a given table view — tenant-visible AND
matching the active filters. Used by the realtime on_realtime hook so a live
insert never violates the current Severity/Source filter.
How many entries the table's current view matches — the total behind "20 of 10,000+ shown", scoped
to the viewer and narrowed by whatever filters are active. Called from the on_load hook, which is
where gervaz's filter state lives.
Types
@type t() :: %MishkaCmsCore.Activity.ActivityEntry{ __lateral_join_source__: term(), __meta__: term(), __metadata__: term(), __order__: term(), action: term(), actor_label: term(), aggregates: term(), badge_label: term(), badge_tone: term(), calculations: term(), id: term(), inserted_at: term(), kind: term(), message: term(), resource_label: term(), severity: term(), site_id: term(), site_label: term(), source_id: term(), tone: term(), user_id: term() }
Functions
@spec counts(map() | nil) :: %{ total: non_neg_integer(), info: non_neg_integer(), warn: non_neg_integer(), error: non_neg_integer() }
The log's KPI totals for viewer — %{total:, info:, warn:, error:} over the sites and the areas
it may see (MishkaCmsCore.Activity.Query.counts/1). All zero for no viewer.
Builds an %ActivityEntry{} struct from a unified source entry map, deriving
:kind (content vs background job). Used by the manual read and the live path.
The day heading the timeline draws is the read's, not the entry's, so mark_days/2's marks ride
along as metadata (see MishkaCmsCoreWeb.Templates.ActivityTimeline); a live entry carries none
and simply gets no heading.
Validates that the keys in the provided input are valid for at least one action on the resource.
Raises a KeyError error at compile time if not. This exists because generally a struct should only ever
be created by Ash as a result of a successful action. You should not be creating records manually in code,
e.g %MyResource{value: 1, value: 2}. Generally that is fine, but often with embedded resources it is nice
to be able to validate the keys that are being provided, e.g
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
Same as input/1, except restricts the keys to values accepted by the action provided.
Whether a live record should appear in a given table view — tenant-visible AND
matching the active filters. Used by the realtime on_realtime hook so a live
insert never violates the current Severity/Source filter.
@spec total(map()) :: non_neg_integer()
How many entries the table's current view matches — the total behind "20 of 10,000+ shown", scoped
to the viewer and narrowed by whatever filters are active. Called from the on_load hook, which is
where gervaz's filter state lives.