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.
| action | resource |
|---|---|
storage.profile.created / updated / deleted | storage_profile |
storage.profile.bucket_added / bucket_changed / bucket_removed | storage_profile |
storage.library.created / renamed / deleted | storage_library |
storage.library.profile_changed / variant_set_changed / setting_changed | storage_library |
storage.variant_set.created / updated / deleted / remade | storage_variant_set |
storage.variant_set.size_created / size_updated / size_deleted / sizes_reset | storage_variant_set |
storage.bucket.created / updated / deleted | storage_bucket |
storage.copy.damaged | bucket — a copy found missing or not matching its checksum |
storage.file.repaired | file — what a repair of one file did |
The last two are the trail of storage that went wrong: a bucket that keeps
losing or changing objects is a disk to look at, and damaged_copies/2 is how a
bucket's page says so.
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.
How many damaged copies were found in the bucket in the last days days
(distinct object keys among the storage.copy.damaged entries, so verifying the
same copy twice counts it once), and when the last was: %{count:, last_at:}.
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.
Writes one storage.copy.damaged entry for each result of a verification
(FileReport.verify/1) that is a copy missing from its bucket or differing from
its checksum, against that bucket. A copy that could not be read is not damage
(the bucket may only be unreachable), and a user's own bucket is private.
opts are the audit options (:actor_uuid) and :found_by ("verify" or
"repair"). Never raises.
Writes the storage.file.repaired entry of a repair of file: the actions it
took (rendition, what, which bucket) and how many problems were left. Nothing
is written when nothing was done. Never raises.
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
@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.
@spec bucket_fields() :: [atom()]
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.
@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.
@spec damaged_copies(term(), pos_integer()) :: %{ count: non_neg_integer(), last_at: term() }
How many damaged copies were found in the bucket in the last days days
(distinct object keys among the storage.copy.damaged entries, so verifying the
same copy twice counts it once), and when the last was: %{count:, last_at:}.
The "changes" map between two values of the same struct (before, after) for
fields. Empty when none differs.
@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.
@spec log_damage(PhoenixKit.Modules.Storage.File.t(), [map()], keyword()) :: :ok
Writes one storage.copy.damaged entry for each result of a verification
(FileReport.verify/1) that is a copy missing from its bucket or differing from
its checksum, against that bucket. A copy that could not be read is not damage
(the bucket may only be unreachable), and a user's own bucket is private.
opts are the audit options (:actor_uuid) and :found_by ("verify" or
"repair"). Never raises.
@spec log_repair( PhoenixKit.Modules.Storage.File.t(), [map()], non_neg_integer(), keyword() ) :: :ok
Writes the storage.file.repaired entry of a repair of file: the actions it
took (rendition, what, which bucket) and how many problems were left. Nothing
is written when nothing was done. Never raises.
Logs an update, when changes is not empty; extra is merged into the metadata.
@spec module_key() :: String.t()
The Activity module key every storage entry (configuration and runs) is filed under.
@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.