feat(media): record what depends on an asset, starting with avatars - #505
Open
mostafasadeghidev wants to merge 3 commits into
Open
feat(media): record what depends on an asset, starting with avatars#505mostafasadeghidev wants to merge 3 commits into
mostafasadeghidev wants to merge 3 commits into
Conversation
`media_usage_refs` has existed since the media schema landed and nothing has ever written to it. So the library cannot tell a decorative upload from an asset the product depends on, and one case of that loses data quietly. A profile picture is stored as an ordinary `media_assets` row. Nothing marks it: no `role` column, no filter hiding it, nothing in the grid to distinguish it from any other image. Tidying the library sweeps it into the trash, purging hard-deletes the row, and `users.avatar_media_id` goes to NULL through its `on delete set null` foreign key. The profile falls back to a Gravatar identicon and nothing anywhere says why. On one install it happened three times before anyone connected the two. This wires the table that was already designed for it. `setMediaUsageRef` points a `(kind, id)` pair at an asset, deleting the previous row first so a reference MOVES rather than accumulating — four avatar changes leave one row, not four, or the fifth deletion would warn about pictures replaced months ago and the warning becomes noise. Clearing an avatar clears the reference, because the asset deliberately stays in the library but nothing depends on it any more. `listMediaUsageRefs` answers for a whole selection in one query, since the question is always asked about a selection, and resolves a label an operator can act on — "Ada Lovelace", not "u1". `POST /media/usage` exposes it: a POST because a hundred ids is the wrong shape for a query string. `ref_kind` namespaces the source, so favicons, page nodes and CMS cells can register without touching consumers. This change only registers avatars and only reads them back; the confirmation copy that consumes it is separate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s in use `buildUsageWarning` turns the reference rows into the sentence a destructive confirmation puts above its buttons, and `useMediaWorkspace.lookupUsage` fetches them. Kept as a pure function with its own tests because the rules are judgement, not mechanics, and each one came from imagining the actual moment: eleven files selected, one of them a profile picture. SEPARATE. "1 of 11 is still in use" lets the operator see the other ten are safe. A blanket "some of these are in use" is the kind of warning people learn to click past, because it never says which. NAME IT. "profile picture — Ada Lovelace", not "has a reference". They have to recognise what they are about to lose. DO NOT BLOCK. Deleting an in-use asset is a legitimate thing to want — replacing an avatar begins exactly that way. The confirmation informs; the operator still decides. Past three named items it summarises, or the dialog becomes a wall of text nobody reads. An asset two things depend on counts once, because one file is what disappears. And the lookup never throws: it decorates a confirmation that must still appear if the request fails, so a network blip degrades to the plain warning rather than blocking the delete. The confirmation itself lives in a separate PR — this is the data and the copy it will render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev
added a commit
to mostafasadeghidev/Instatic
that referenced
this pull request
Sep 6, 2026
…s about it Wires the dormant `media_usage_refs` table so the library can tell a decorative upload from an asset the product depends on. Avatars were ordinary library rows with no marker — purging one hard-deleted it and `users.avatar_media_id` went quietly to NULL through its foreign key. Fork stack: PR CoreBunch#505.
mostafasadeghidev
added a commit
to mostafasadeghidev/Instatic
that referenced
this pull request
Sep 6, 2026
…at had no gate The fork gate stopped at CoreBunch#359. Everything merged onto the stack since — CoreBunch#497 through CoreBunch#501, and now CoreBunch#505 — was carried with nothing to catch it being dropped by a merge resolution, which is the exact failure the gate exists for. Pinning them turned one up immediately: CoreBunch#498's guard around `removePluginVersionAssets(…, fromVersion)` is gone, because CoreBunch#359 removed that call outright — an upgrade now leaves the old version's assets on disk so published pages keep resolving. What survives of CoreBunch#498 is the rollback guard, so that is what the row pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t one Registering a usage reference on upload leaves every existing install in the one state the feature was built for and silent about it: an avatar set before this code shipped has no reference, so the first confirmation that warns before a permanent delete would say nothing about it. The operator would have to re-upload the same picture to be told it matters. The backfill inserts a `user.avatar` reference for every user who has one. Idempotent by construction — `not exists` on the same key `setMediaUsageRef` writes, which is also what makes the backfilled row indistinguishable from an app-written one, so a later avatar change MOVES it instead of leaving the old picture warning forever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
media_usage_refshas existed since the media schema landed and nothing has ever written to it. So the library cannot tell a decorative upload from an asset the product depends on — and one case of that loses data quietly.How it goes wrong
A profile picture is stored as an ordinary
media_assetsrow. Nothing marks it: norolecolumn, no filter hiding it, nothing in the grid to distinguish it from any other image.So tidying the library sweeps it into the trash. Purging hard-deletes the row.
users.avatar_media_idgoes to NULL through itson delete set nullforeign key. The profile falls back to a Gravatar identicon and nothing anywhere says why.On the install where this was found it had happened three times before anyone connected the two — each time read as "the avatar disappears after an update".
What this adds
setMediaUsageRefpoints a(kind, id)pair at an asset, deleting the previous row first so a reference moves rather than accumulating. Four avatar changes leave one row, not four — otherwise the fifth deletion would warn about pictures replaced months ago and the warning becomes noise. Clearing an avatar clears the reference: the asset deliberately stays in the library, but nothing depends on it any more.listMediaUsageRefsanswers for a whole selection in one query, since the question is always asked about a selection, and resolves a label the operator can act on — "Ada Lovelace", not "u1".POST /media/usageexposes it. A POST because a hundred ids is the wrong shape for a query string.buildUsageWarningis the sentence a confirmation shows. Kept pure and separately tested because the rules are judgement, not mechanics:Migration
028backfills a reference for every avatar already set. Without it the feature is silent on exactly the installs it was built for: an existing avatar carries no reference, so the first confirmation that warns before a permanent delete would say nothing about it, and the operator would have to re-upload the same picture to be told it matters. Idempotent by construction —not existson the same keysetMediaUsageRefwrites, which is also what makes a backfilled row indistinguishable from an app-written one, so a later avatar change moves it instead of leaving the old picture warning forever.It takes
028and skips027because #335 already claims027_data_tables_created_by_plugin. Ids are only ever sorted, so the gap costs nothing, and whichever of the two lands first neither has to be renumbered — which matters because a migration an installation has already recorded can never be renamed.Scope
ref_kindnamespaces the source, so favicons, page nodes and CMS cells can register later without touching consumers. This PR registers avatars, reads them back, and builds the copy. The confirmation that renders it is a separate PR, so this one is reviewable as the data layer it is.Co-Authored-By: Claude Opus 5 noreply@anthropic.com