PhoenixKit.Modules.Storage.Audit (phoenix_kit v2.52.2)

Copy Markdown View Source

The "who changed what" half of Media's history (dev_docs/plans/2026-10-03-job-runs.md, §7): every change to the site's storage configuration is written to the Activity log, and the action names live here, in one place. Settings → Media → History reads them back, together with the entries of the storage job runs.

actionresource
storage.profile.created / updated / deletedstorage_profile
storage.profile.bucket_added / bucket_changed / bucket_removedstorage_profile
storage.library.created / renamed / deletedstorage_library
storage.library.profile_changed / variant_set_changed / setting_changedstorage_library
storage.variant_set.created / updated / deleted / remadestorage_variant_set
storage.variant_set.size_created / size_updated / size_deleted / sizes_resetstorage_variant_set
storage.bucket.created / updated / deletedstorage_bucket

An entry carries the acting user (actor_uuid: in the context's options; the LiveViews pass PhoenixKitWeb.Actor.opts(socket)), a mode (manual when a person acted, auto when nothing did) and, for a change, the Activity log's own "changes" shape — %{"copies_originals" => %{"from" => 1, "to" => 2}} — which the feed already renders as "from → to". Entries are permanent: they are not pruned by activity_retention_days, because "who changed this bucket last year" is exactly what an audit is asked.

Only the site's configuration is recorded. A user's own library, profile and bucket (V203–V206) are theirs and private; their changes are not written here. And nothing secret is ever put in an entry: bucket changes name only the fields in bucket_fields/0, never a key or a secret.

Summary

Functions

Defers a cache invalidation or compatibility-settings sync until this module's outer transaction commits. Callbacks are best effort. Inside a caller-owned repo transaction it retains the callback's existing immediate behavior; callers needing commit ordering must use transaction/1 as their outer boundary.

The bucket fields whose changes are recorded: never a key or a secret.

Locks and reloads an audited resource so diffs describe its actual preceding state.

The "changes" map of an update: for each of fields that the changeset changes, %{"field" => %{"from" => old, "to" => new}} (values made loggable). Empty when nothing in fields changed.

The "changes" map between two values of the same struct (before, after) for fields. Empty when none differs.

Writes one configuration entry. Insert failures roll back an enclosing audited mutation; a standalone call returns the error. Entries are announced only after the transaction owned by this module commits.

Logs an update, when changes is not empty; extra is merged into the metadata.

The Activity module key every storage entry (configuration and runs) is filed under.

Runs a configuration mutation and its audit inserts in one transaction, then announces the entries after commit. Nested audited mutations share the entries. A failed mutation or audit insert rolls everything back.

Functions

after_commit(fun)

@spec after_commit((-> term())) :: :ok

Defers a cache invalidation or compatibility-settings sync until this module's outer transaction commits. Callbacks are best effort. Inside a caller-owned repo transaction it retains the callback's existing immediate behavior; callers needing commit ordering must use transaction/1 as their outer boundary.

bucket_fields()

@spec bucket_fields() :: [atom()]

The bucket fields whose changes are recorded: never a key or a secret.

change(map, fun)

@spec change(struct(), (struct() -> result)) :: result | {:error, term()}
when result: var

Locks and reloads an audited resource so diffs describe its actual preceding state.

changes(changeset, fields)

@spec changes(Ecto.Changeset.t(), [atom()]) :: %{required(String.t()) => map()}

The "changes" map of an update: for each of fields that the changeset changes, %{"field" => %{"from" => old, "to" => new}} (values made loggable). Empty when nothing in fields changed.

diff(before, later, fields)

@spec diff(map(), map(), [atom()]) :: %{required(String.t()) => map()}

The "changes" map between two values of the same struct (before, after) for fields. Empty when none differs.

log(action, resource_type, resource_uuid, opts, metadata \\ %{})

@spec log(String.t(), String.t(), String.t() | nil, keyword(), map()) ::
  {:ok, PhoenixKit.Activity.Entry.t()} | {:error, term()} | :skipped

Writes one configuration entry. Insert failures roll back an enclosing audited mutation; a standalone call returns the error. Entries are announced only after the transaction owned by this module commits.

opts are the context call's: :actor_uuid, and :mode (default "manual" with an actor, "auto" without). audit: false writes nothing (:skipped) — for a change one context makes to another's rows as a consequence of the one that is recorded. metadata is a map of string keys.

log_update(action, resource_type, uuid, opts, changes, extra \\ %{})

@spec log_update(String.t(), String.t(), String.t() | nil, keyword(), map(), map()) ::
  :ok

Logs an update, when changes is not empty; extra is merged into the metadata.

module_key()

@spec module_key() :: String.t()

The Activity module key every storage entry (configuration and runs) is filed under.

transaction(fun)

@spec transaction((-> result)) :: result | {:error, term()} when result: var

Runs a configuration mutation and its audit inserts in one transaction, then announces the entries after commit. Nested audited mutations share the entries. A failed mutation or audit insert rolls everything back.

Inside a caller's own repo transaction, entries commit with that transaction but are not announced: this module cannot know when the caller commits. The History tab's periodic refresh picks them up. Call this wrapper at the outer boundary when immediate announcements are wanted.