From 161ddc0f7d13d8d83e5e91408cba27ae0c04239d Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Mon, 5 Oct 2026 17:16:06 +0200 Subject: [PATCH] chore: groundwork for media purge system --- docs/design/README.md | 3 +- docs/design/components.md | 15 ++ docs/design/routes.md | 4 +- docs/design/screens/media.md | 5 +- docs/design/screens/settings.md | 54 ++++++- docs/design/source/MediaFrame.dc.html | 6 +- .../source/RenewCMS Screens API keys.dc.html | 2 +- .../RenewCMS Screens Back-office.dc.html | 2 + .../RenewCMS Screens Media cleanup.dc.html | 139 ++++++++++++++++++ docs/design/source/SettingsFrame.dc.html | 121 +++++++++++++++ docs/design/states.md | 24 +++ 11 files changed, 366 insertions(+), 9 deletions(-) create mode 100644 docs/design/source/RenewCMS Screens Media cleanup.dc.html create mode 100644 docs/design/source/SettingsFrame.dc.html diff --git a/docs/design/README.md b/docs/design/README.md index 7adad4e..6ee4f23 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -34,7 +34,7 @@ The visual spec of record for the back-office SPA and the transactional emails, | [Integration](screens/integration.md) | `/integration` | 8 | `source/RenewCMS Screens Back-office.dc.html` | | [API keys](screens/integration-api-keys.md) | `/integration/api-keys` | 12 | `source/RenewCMS Screens API keys.dc.html` | | [Users](screens/users.md) | `/users` | 17 | `source/RenewCMS Screens Back-office.dc.html` | -| [Settings](screens/settings.md) | `/settings` | 25 | `source/RenewCMS Screens Back-office.dc.html` | +| [Settings](screens/settings.md) | `/settings` | 25 | `source/RenewCMS Screens Back-office.dc.html`, `source/RenewCMS Screens Media cleanup.dc.html` | | [Profile](screens/profile.md) | `/profile` | 13 | `source/RenewCMS Screens Back-office.dc.html` | | [Accept invite](screens/accept-invite.md) | `/invite/accept?c=CODE` | 9 | `source/RenewCMS Screens Auth.dc.html` | | [Global](screens/global.md) | `any route` | 4 | `source/RenewCMS Screens Back-office.dc.html` | @@ -42,6 +42,7 @@ The visual spec of record for the back-office SPA and the transactional emails, ## Other boards - `source/MediaFrame.dc.html`: the frame that draws the Media and Media picker states (the Back-office board imports it). Like `AppFrame.dc.html`, it holds the exact values. +- `source/SettingsFrame.dc.html`: the Media cleanup card and its purge dialog (the Media cleanup board imports it, with `MediaFrame.dc.html` for the Duplicates toolbar button). They hold the exact values. - `source/IntegrationPage.dc.html` and `source/ApiKeysPage.dc.html`: the page components for the Integration and API keys screens (imported by `AppFrame.dc.html`). They hold the exact values. - `source/RenewCMS Prototype.dc.html`: the clickable flows (first run, edit and publish, password reset, delete, session expiry mid-edit). - `source/RenewCMS Handoff.dc.html`: the handoff page the first markdown here was generated from. It predates the Users and Profile screens and the invitation email, so it does not list them. Those are written by hand from the Back-office board, `AppFrame.dc.html`, the Prototype and `emails/invitation.html`. diff --git a/docs/design/components.md b/docs/design/components.md index 21f0afd..2ee5c29 100644 --- a/docs/design/components.md +++ b/docs/design/components.md @@ -814,3 +814,18 @@ Exact values live in `source/AppFrame.dc.html` (the Settings and Home states). C | RestartOverlay | custom full-page `role="status"` | title, text, startedAt, slow | reload | App shell | | UpdateBanner | UAlert | state: 'available'\|'ready' | go-to-settings, dismiss | Home (owner) | | UpdatePill | UBadge | none | none | DashboardShell Settings item (dot on mobile) | + +## Added with Media cleanup +### PurgeDuplicatesDialog +Used by Settings ("Purge now") and Media, Duplicates tab ("Purge duplicates"). Built on `UModal`, `sm:max-w-md`, bottom sheet on mobile, cancel first, confirm is `color="error"`. +- Props: `open`, `preview` (`groups`, `copies`, `articles`, `list[]`, `more`), `status` (`loading | ready | purging | nothing | error | partial | conflict`). +- While `purging`: confirm shows `loader-circle` and reads "Purging…"; `dismissible` false, Cancel disabled. +- Group row: 40×30 thumbnail, filename, "Kept · used in N articles", right-aligned "N copies merged". Max 5 rows, then "and N more groups". +- Notices sit above the summary in a `UAlert` (soft): error for failure, warning for partial and conflict. +- Emits `purged` (with counts). The parent shows the toast. + +### MediaCleanupCard +Settings card. `USwitch` (square, `rounded-none`), `UInputNumber` with "days" suffix, read-only lines, divider, outline button. Saves on change and on blur or Enter; shows "Saving…" then "Saved" in the header. Read-only mode disables controls, hides the button and shows an info `UAlert`. + +### Media toolbar button +Secondary `UButton` icon `merge`, label "Purge duplicates", after the sort menu on desktop; full width in the stacked toolbar on mobile (44 px). diff --git a/docs/design/routes.md b/docs/design/routes.md index 3f50769..e5b4cc0 100644 --- a/docs/design/routes.md +++ b/docs/design/routes.md @@ -15,11 +15,11 @@ A global `router.beforeEach` guard reads `meta.auth`. On the first navigation it | `/article` | DashboardShell | verified | ArticlesPage | GET /v1/article/ · PATCH /{id}/publish\|unpublish · DELETE /{id} | | `/article/new` | DashboardShell | verified | ArticlesPage + NewArticleModal | POST /v1/article/ | | `/article/:id/edit` | DashboardShell | verified | EditorPage | GET /{id} · PATCH /{id} · PATCH /{id}/publish\|unpublish · GET /v1/media/ · POST /v1/media/ · DELETE /{id} | -| `/media` | DashboardShell | verified | MediaPage | GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage | +| `/media` | DashboardShell | verified | MediaPage | GET /v1/media/purge-preview · POST /v1/media/purge · GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage | | `/integration` | DashboardShell | verified | IntegrationPage | none | | `/integration/api-keys` | DashboardShell | verified | ApiKeysPage + CreateApiKeyModal | GET /v1/api-key · POST /v1/api-key · DELETE /v1/api-key/{id} | | `/users` | DashboardShell | verified | UsersPage + InviteUserModal | GET /v1/user/ · POST /v1/user/invite · DELETE /v1/user/invite/:id · POST /v1/user/invite/:id/renew | -| `/settings` | DashboardShell | verified | SettingsPage | GET /v1/settings · GET /v1/system/update · POST /v1/system/update/check · POST /v1/system/update/download · POST /v1/system/update/restart | +| `/settings` | DashboardShell | verified | SettingsPage | GET /v1/settings · GET /v1/system/update · POST /v1/system/update/check · POST /v1/system/update/download · POST /v1/system/update/restart · GET /v1/media/purge-settings · PATCH /v1/media/purge-settings · GET /v1/media/purge-preview · POST /v1/media/purge | | `/profile` | DashboardShell | verified | ProfilePage | GET /v1/auth/me · PATCH /v1/user/me · POST /v1/auth/password-reset | | `/invite/accept` | AuthShell | public | AcceptInvitePage | GET /v1/auth/invite?c=* · POST /v1/auth/invite/accept* | | `/:pathMatch(.*)*` | either | any | NotFoundPage | none | diff --git a/docs/design/screens/media.md b/docs/design/screens/media.md index ad02f6f..a5b9b4e 100644 --- a/docs/design/screens/media.md +++ b/docs/design/screens/media.md @@ -1,7 +1,7 @@ # Media - **Route:** `/media` (verified users) -- **API:** GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage +- **API:** GET /v1/media/purge-preview · POST /v1/media/purge · GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage - **Visual reference:** `source/RenewCMS Screens Back-office.dc.html`, group "Media", drawn by `source/MediaFrame.dc.html` (open it in a browser; each state is drawn at desktop and mobile, with a light/dark toggle). The library of every uploaded image. It replaces "images belong to one article": an image belongs to the library and can be used in any number of articles. Deleting an article never deletes images. The sidebar gains **Media** (`images` icon) between Articles and Users, breadcrumb "Media". @@ -12,7 +12,7 @@ New route for verified users, in the sidebar between Articles and Users with the Header: title "Media", subtitle "Every image you've uploaded, ready to use in any article.", primary "Upload" button (`upload` icon) on the right. On mobile the button is a full-width 48 px button pinned to the bottom (it turns into "Try again in 0:42" while rate limited, and is replaced by the bulk bar while items are selected). -Toolbar: tabs **All**, **Unused** and **Duplicates** (count badge of duplicate groups, only while groups exist), then search ("Search by filename"), sort and the grid/list toggle. Sort options: Newest (default), Oldest, Name, Largest. Below md the toolbar stacks: search, tabs, then sort and toggle. +Toolbar: tabs **All**, **Unused** and **Duplicates** (count badge of duplicate groups, only while groups exist), then search ("Search by filename"), sort and the grid/list toggle. Sort options: Newest (default), Oldest, Name, Largest. On the Duplicates tab, owners and admins also get a secondary "Purge duplicates" button (`merge` icon) after the sort menu (full width, 44 px, in the stacked toolbar on mobile), hidden while there are no groups; it opens the PurgeDuplicatesDialog described in [settings](settings.md#media-cleanup-second-card). Below md the toolbar stacks: search, tabs, then sort and toggle. Grid (default): 5 columns on desktop, 2 on mobile, 16 px gap (10 px on mobile). Each card is one button: 4:3 thumbnail (checkerboard behind transparent PNG and SVG), filename (medium, ellipsis), `size · dimensions` (SVG dimensions read "Vector"), then badges. The accessible name reads "north-wall.jpg, 3.9 MB, used in 2 articles" or "…, unused". A "Select all" row with the image count sits above the grid. @@ -149,6 +149,7 @@ Grid items are buttons, one tab stop each. Enter opens the detail panel, Space t | Upload: rejected files (413, 400) | Type and size are checked in the browser first, so nothing is sent. A server 413 or 400 for a file that got through shows the same row with its own message. | | Upload: too many requests (429) | Rate limited. Retry-After is counted down in the button. It re-enables at zero. Already-added files are kept. | | Duplicates: groups | The Duplicates tab. Groups of identical images side by side, each with its uploader, upload date and "Used in N articles". The suggested copy to keep is the most used, then the oldest. | +| Duplicates: purge all | Owner or admin, groups exist. "Purge duplicates" opens the purge dialog (preview, confirm, progress, results), the same one as Settings "Purge now". "Merge into this one" is unchanged. | | Duplicates: empty | No groups. The Duplicates tab loses its count badge. | | Merge: confirm | "Merge into this one" on floorboards.jpg. The dialog says articles using the other copies will switch to this image and the other copies will be deleted, with the article count. | | Merge: in progress | Request in flight. The dialog can't be dismissed. | diff --git a/docs/design/screens/settings.md b/docs/design/screens/settings.md index f64eed1..9771e8e 100644 --- a/docs/design/screens/settings.md +++ b/docs/design/screens/settings.md @@ -1,7 +1,7 @@ # Settings - **Route:** `/settings` -- **API:** GET /v1/settings (version) · GET /v1/system/update · POST /v1/system/update/check · POST /v1/system/update/download · POST /v1/system/update/restart +- **API:** GET /v1/media/purge-settings · PATCH /v1/media/purge-settings · GET /v1/media/purge-preview · POST /v1/media/purge (see "Media cleanup (second card)" at the end) · GET /v1/settings (version) · GET /v1/system/update · POST /v1/system/update/check · POST /v1/system/update/download · POST /v1/system/update/restart - **Visual reference:** `source/RenewCMS Screens Back-office.dc.html`, group "Settings", drawn by `source/AppFrame.dc.html` (open it in a browser; each state is drawn at desktop and mobile, with a light/dark toggle). Opened from the sidebar (Settings, settings icon, after Integration) and from the mobile drawer. Every verified user sees it. It holds two sections: Version (everyone) and Updates (the owner has the buttons, admins read the status). @@ -125,3 +125,55 @@ For the owner only: an **Update** pill on the Settings sidebar item, and a dot o | Admin view | Admins see the version and the note, with no buttons. Up to date: "You're up to date" and "Only the owner can update or restart the site." No Home banner or Settings pill. | | Role changed (403) | An owner was changed to admin while the page was open and pressed a button. The card reloads in the admin view. | | Update already running (409) | Another owner started it. The card switches to that run's progress. | + +## Media cleanup (second card) + +Route: `/settings`. Visual reference: `RenewCMS Screens Media cleanup.dc.html` (frames: `SettingsFrame.dc.html`, `MediaFrame.dc.html`). +API: `GET/PATCH /v1/media/purge-settings` (owner, admin), `GET /v1/media/purge-preview`, `POST /v1/media/purge`. 401 and 403 are global: no local copy. + +Rule: a group is purged only if at least one copy is used in an article. All copies merge into the one used in the most articles (ties: oldest, then lowest id). Groups with no used copy are left alone. Automatic purge is off by default. + +### Card +Second card below Version, 20 px gap, same card styling. Page max width 720 px. +- Title: "Media cleanup". Description: "Merge identical images so your library stays tidy. Articles that use a copy are switched to the most used one." +- "Automatic purge" / helper "Runs in the background. Off by default." / USwitch. +- When on: "Every" / helper "Between 1 and 365 days." / number input, suffix "days", default 30. +- Read-only lines: "Last purge: 14 Jun 2026 · 3 copies merged · 5 articles updated" or "Never run". When on, also "Next purge: 14 Jul 2026". +- Divider, then "Purge now" / "Merge every group of duplicates that has an image in use, right away." / outline button "Purge duplicates" (merge). +- No Save button. Saves on toggle and on blur or Enter of the days field. "Saved" (check, success) sits in the card header and fades after 2 s; "Saving…" (loader-circle) while pending. +- Non-owner/admin: switch and input disabled, button and divider hidden, info alert "Only owners and admins can change this." + +### States (names matched exactly) +- Default: off, never run +- On: 30 days, last run +- Saving +- Saved +- Save failed: "Couldn't save. Try again in a moment." under the field; the control reverts. +- Validation error: "Enter a whole number of days between 1 and 365." Nothing is sent. +- Read-only (non-admin) +- Loading skeleton (after 150 ms) +- Offline: banner "You're offline. Changing automatic purge and purging duplicates need a connection." Switch, input and button disabled. + +### Dialog +UModal, `sm:max-w-md`, bottom sheet on mobile, cancel first. +- Title "Purge duplicates?". Description "Articles that use a duplicate will be switched to the most used copy of that image, and the other copies will be deleted. This can't be undone." +- Summary: "4 groups · 9 copies will be deleted · 12 articles will be updated". Singular: "1 group · 1 copy will be deleted · 2 articles will be updated". +- Bordered list of up to 5 groups: thumbnail, filename, "Kept · used in 4 articles", and "2 copies merged". More: "and 3 more groups". +- Link "Review in Duplicates" to `/media?filter=duplicates`. +- Buttons: "Cancel", danger "Purge 4 groups". Pending: "Purging…" with loader-circle, dialog can't be dismissed. +- Loading preview: skeleton rows, confirm disabled. +- Nothing to purge: icon copy, "Nothing to purge", "No duplicate is used in an article, so there is nothing to merge.", button "Close". No confirm. +- Error (stays open): "Couldn't purge the duplicates. Nothing was changed. Try again in a moment." +- Partial: "Some groups couldn't be merged. 3 of 4 groups were purged. Try again to finish." Buttons "Close", "Retry". Summary refreshes to the remaining groups. +- Conflict (409): "The library changed, so nothing was purged. Check the new summary and try again." Summary refreshes. + +### Results +- Toast (5 s): circle-check, "Duplicates purged" / "9 copies merged. 12 articles updated." +- "Last purge" updates in place; "Next purge" moves forward by the interval. +- An unattended run shows no toast. The next visit shows the updated line. + +### Media entry point +Duplicates tab: secondary "Purge duplicates" button (merge) next to the sort menu; stacked toolbar on mobile. Opens the same dialog. Hidden for non-owner/admin and when there are no groups. "Merge into this one" is unchanged. + +### Components (Nuxt UI v4) +`UCard`, `USwitch`, `UInputNumber` (min 1, max 365, suffix), `UFormField`, `UAlert`, `UButton`, `UModal`, `USkeleton`, `useToast`. diff --git a/docs/design/source/MediaFrame.dc.html b/docs/design/source/MediaFrame.dc.html index 3c0c406..94817bf 100644 --- a/docs/design/source/MediaFrame.dc.html +++ b/docs/design/source/MediaFrame.dc.html @@ -144,9 +144,10 @@ class Component extends DCLogic { if (s.empty === 'first') { parts.push(Empty('images', 'No images yet', 'Upload your first image. You can use it in any article.', 'Upload an image', true), S('align-self:center;color:var(--ui-text-muted);font-size:12px', 'PNG, JPEG, WEBP, SVG · max 10 MB each')); return parts } if (s.limited) parts.push(Alert('info', 'timer', 'Too many uploads', 'For your security, wait a moment before uploading again.')) if (s.error) parts.push(Alert('error', 'circle-x', 'Couldn\'t load your images', 'Check your connection and try again.', 'Try again')) + const pb = tab === 'dups' && !s.member && !s.dupEmpty && B('Purge duplicates', { icon: 'merge', dis: s.offline, full: m }) const tb = m - ? D('display:flex;flex-direction:column;gap:12px', Search(), TabRow(), D('display:flex;gap:8px', Sort(), Toggle())) - : D('display:flex;align-items:flex-end;gap:12px;border-bottom:1px solid var(--ui-border)', TabRow(), S('display:flex;padding-bottom:6px;gap:8px', Search(), Sort(), Toggle())) + ? D('display:flex;flex-direction:column;gap:12px', Search(), TabRow(), D('display:flex;gap:8px', Sort(), Toggle()), pb && D('display:flex', pb)) + : D('display:flex;align-items:flex-end;gap:12px;border-bottom:1px solid var(--ui-border)', TabRow(), S('display:flex;padding-bottom:6px;gap:8px', Search(), Sort(), pb, Toggle())) parts.push(tb) if (UP) parts.push(UpPanel()) if (s.loading) parts.push(Skel()) @@ -189,6 +190,7 @@ class Component extends DCLogic { if (k === 'delUsed') return { title: 'Delete an image that\'s in use?', desc: '"north-wall.jpg" is used in 2 articles. If you delete it, they will show a broken image. This can\'t be undone.', body: [ArtBox(usedList(2))], cancel: 'Cancel', confirm: md.loading ? 'Deleting…' : 'Delete image', tone: 'danger' } if (k === 'bulk') return { title: 'Delete 3 images?', desc: '2 of them are used in 5 articles, which will show a broken image. The other one isn\'t used anywhere. This can\'t be undone.', body: [D('display:flex;flex-direction:column;border:1px solid var(--ui-border)', ...[['north-wall.jpg', 'Used in 2'], ['kitchen-after.webp', 'Used in 3'], ['window-frame.jpg', 'Unused']].map(([n, u], i) => D('display:flex;align-items:center;gap:8px;min-height:36px;padding:0 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), S('flex:1;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', n), Bd(u === 'Unused' ? 'Unused' : u, u === 'Unused' ? 'unused' : 'used'))))], cancel: 'Cancel', confirm: md.loading ? 'Deleting…' : 'Delete 3 images', tone: 'danger' } if (k === 'merge') return { title: 'Merge into floorboards.jpg?', desc: 'Articles that use the other 2 copies will be switched to floorboards.jpg, and the other 2 copies will be deleted. 5 articles will be updated. This can\'t be undone.', body: [D('display:flex;gap:12px;align-items:center;border:1px solid var(--ui-border);padding:10px 12px', Th(byId(12), 'width:56px;height:42px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('display:flex;flex-direction:column;min-width:0', S('font-weight:500;color:var(--ui-text-highlighted)', 'floorboards.jpg'), S('font-size:12px;color:var(--ui-text-muted)', 'Kept · used in 4 articles now')))], cancel: 'Cancel', confirm: md.loading ? 'Merging…' : 'Merge 2 copies', tone: 'danger' } + if (k === 'purge') return { title: 'Purge duplicates?', desc: 'Articles that use a duplicate will be switched to the most used copy of that image, and the other copies will be deleted. This can\'t be undone.', body: [S('font-weight:500;color:var(--ui-text-highlighted)', '4 groups · 9 copies will be deleted · 12 articles will be updated'), D('border:1px solid var(--ui-border)', ...[[12, 'floorboards.jpg', 4, '2 copies'], [7, 'IMG_4471.jpg', 2, '1 copy'], [2, 'kitchen-after.webp', 3, '3 copies'], [4, 'logo-renew.svg', 5, '3 copies']].map(([id, n, u, c], i) => D('display:flex;align-items:center;gap:12px;padding:8px 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), Th(byId(id), 'width:40px;height:30px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('flex:1;min-width:0;display:flex;flex-direction:column', S('font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', n), S('font-size:12px;color:var(--ui-text-muted)', 'Kept · used in ' + pl(u))), S('font-size:12px;color:var(--ui-text-toned);white-space:nowrap', c + ' merged')))), S('display:inline-flex;align-items:center;gap:4px;align-self:flex-start;font-weight:500;color:var(--ui-text-highlighted);text-decoration:underline;text-underline-offset:3px;min-height:' + (m ? 44 : 20) + 'px', 'Review in Duplicates', I('arrow-right', 14))], cancel: 'Cancel', confirm: 'Purge 4 groups', tone: 'danger' } if (k === 'remove') return { title: 'Remove this image from the article?', desc: 'It\'s removed from this article only. The file stays in your library and in any other article that uses it.', body: [D('display:flex;gap:12px;align-items:center;border:1px solid var(--ui-border);padding:10px 12px', Th(byId(1), 'width:56px;height:42px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('display:flex;flex-direction:column;min-width:0', S('font-weight:500;color:var(--ui-text-highlighted)', 'north-wall.jpg'), S('font-size:12px;color:var(--ui-text-muted)', 'Stays in the library · 3.9 MB')))], cancel: 'Cancel', confirm: 'Remove image' } } const Shell = (o, body) => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'flex-start') + ';justify-content:center;padding:' + (m ? 16 : o.top || 140) + 'px 16px 16px', diff --git a/docs/design/source/RenewCMS Screens API keys.dc.html b/docs/design/source/RenewCMS Screens API keys.dc.html index df8d447..69d77b8 100644 --- a/docs/design/source/RenewCMS Screens API keys.dc.html +++ b/docs/design/source/RenewCMS Screens API keys.dc.html @@ -51,7 +51,7 @@ - + + + + + + + + + +
+ +
+
+

Screen: Media cleanup

+

A Media cleanup card on Settings, a purge dialog, and an entry point on the Media Duplicates tab (also in the Back-office board). 1440 px at 50%, 390 px at 75%. The theme switch shows every state in dark mode.

+
+
+ + + +
+
+ + +
+
+

{{ g.title }}

+
{{ g.route }}{{ g.api }}
+

{{ g.note }}

+
+
+ +
+
{{ st.name }}{{ st.trigger }}
+
+
+
+
+
+
+
+
{{ g.tableTitle }}
StatusWhereCopy
{{ r.a }}{{ r.b }}{{ r.c }}
+
+
+ +
+
+ + + diff --git a/docs/design/source/SettingsFrame.dc.html b/docs/design/source/SettingsFrame.dc.html new file mode 100644 index 0000000..9c2e150 --- /dev/null +++ b/docs/design/source/SettingsFrame.dc.html @@ -0,0 +1,121 @@ + + + + + + + + + +
{{ view }}
+
+ + + diff --git a/docs/design/states.md b/docs/design/states.md index 1948976..8e6515e 100644 --- a/docs/design/states.md +++ b/docs/design/states.md @@ -281,3 +281,27 @@ Added by the Media screens, none of which exist yet: - `POST /v1/media/merge`: `{ keep_id, merge_ids }`. Rewrites article bodies from the merged copies' URLs to the kept image's URL, then deletes the copies. 200 with `{ articles_updated, merged }`, 404, 409 when a listed copy is gone or the group changed. - `GET /v1/media/{id}/usage`: `[{ id, title, published }]`, read from article bodies. - Images no longer belong to an article, and deleting an article no longer deletes images. `POST /v1/article/{id}/image` and `DELETE /v1/article/{id}/image/{imageId}` are replaced by the endpoints above. + +## Added with Media cleanup +### Media cleanup +| State | Surface | Behaviour | +|---|---|---| +| Default | Settings card | Off, "Never run", no "Every" row | +| On | Settings card | "Every" 30 days, last and next run | +| Saving / Saved | Settings card header | "Saving…" then "Saved" for 2 s, polite live region | +| Save failed | Under the field | "Couldn't save. Try again in a moment." Control reverts | +| Validation | Under the field | "Enter a whole number of days between 1 and 365." Nothing sent | +| Read-only | Settings card | Controls disabled, button hidden, info alert | +| Loading | Settings card | Skeleton after 150 ms | +| Offline | Settings and Media | Banner as on Media. Controls and buttons disabled | +| Preview loading | Dialog | Skeleton rows, confirm disabled | +| Confirm | Dialog | Summary, up to 5 groups, link to Duplicates | +| In progress | Dialog | "Purging…", loader-circle, not dismissible | +| Nothing to purge | Dialog | Info state, "Close" only | +| Error | Dialog | Notice, stays open, confirm retries | +| Partial | Dialog | Warning, summary refreshes, "Close" and "Retry" | +| Conflict (409) | Dialog | Warning, summary refreshes | +| Success | Toast, 5 s | "Duplicates purged" / "9 copies merged. 12 articles updated." "Last purge" updates in place | +| Unattended run | Settings | No toast. Updated on next visit | + +401 and 403 are global and have no local copy.