refac: migration crash, real-database tests, site deleted during create, ruels out of the repo, lists

This commit is contained in:
Florian Sylvain committed 2026-11-03 05:06:04 +01:00
1 parent 680b9a1308
commit 9d284e65a9
62 files changed
+1858 -987

No files matched your search

+47 -21
View File
@@ -191,7 +191,7 @@ States and copy in screens/account.md. Board: RenewCMS Screens Account.dc.html (
## Localization state IDs (groups L1-L12 on the Localization board; source of truth: locale-spec.js)
Every state has a stable ID, a frame selector and a fixture. Legacy IDs from the first pass keep their names and are the canonical ID of the state they map to; the loc.lN.xxx alias is listed below.
Total: 169 states (was 109).
Total: 183 states in 16 groups (was 109 in the first pass, 169 after the corrections pass). L7s (single-language list) and 13 other states were added in pass 3; see localization-changelog.md.
## L1 Site languages
Route: /settings/sites/:key/languages
@@ -244,6 +244,7 @@ Owner-only. One pattern per site, for example https://example.com/{lang}/{slug}.
| loc.l1b.saved | Saved | Toast | LocaleFrame{s:{v:"pu",x:"saved"}} | 1440, 390 | Journal |
| loc.l1b.unset | Unset | No pattern; opened from "Public URL not configured" | LocaleFrame{s:{v:"pu",x:"unset"}} | 1440, 390 | Journal |
| loc.l1b.viewOnly | Read-only view | Non-owner: pattern readable, no controls | LocaleFrame{s:{v:"pu",x:"viewOnly"}} | 1440, 390 | Journal |
| loc.l1b.invalidHost | Invalid host | Pattern with an empty host label | LocaleFrame{s:{v:"pu",x:"invalidHost"}} | 1440, 390 | Journal |
## L2 First-time setup for existing articles
Route: /settings/sites/:key/languages/setup
@@ -271,7 +272,7 @@ Single-language sites show no language field. Multilingual sites show Language a
|---|---|---|---|---|---|
| loc.l3.single | Single language | Title and site only | LocaleFrame{s:{v:"na",x:"single"}} | 1440, 390 | Archive |
| loc.l3.multi | Multilingual: new article | Language defaults to the site default | LocaleFrame{s:{v:"na",x:"multi"}} | 1440, 390 | Journal |
| loc.l3.translation | Translation of an article | Article picker + language | LocaleFrame{s:{v:"na",x:"translation"}} | 1440, 390 | Journal |
| loc.l3.translation | Translation of an article | Same fields as Add translation: article, language, source version, title, starting content | LocaleFrame{s:{v:"na",x:"translation"}} | 1440, 390 | Journal |
| loc.l3.sitechange | Site changed | Selected language not offered by the new site | LocaleFrame{s:{v:"na",x:"sitechange"}} | 1440, 390 | Journal, Notes |
## L4 Add a translation
@@ -283,13 +284,13 @@ Always creates a draft. Source: the user picks which readable version to copy fr
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l4.form | Copy from source | Editor → language menu → Add translation | LocaleFrame{s:{v:"at",x:"form"}} | 1440, 390 | Journal |
| loc.l4.empty | Start empty | Title only | LocaleFrame{s:{v:"at",x:"empty"}} | 1440, 390 | Journal |
| loc.l4.empty | Start empty | Title only; Source version reads No source, Freshness will show No source | LocaleFrame{s:{v:"at",x:"empty"}} | 1440, 390 | Journal |
| loc.l4.saving | Creating | Submit in flight | LocaleFrame{s:{v:"at",x:"saving"}} | 1440, 390 | Journal |
| loc.dup.readable | Exists, readable | Conflict; Open offered | LocaleFrame{s:{v:"at",x:"dup"}} | 1440, 390 | Journal |
| loc.dup.hidden | Exists, not readable | Neutral error, no Open | LocaleFrame{s:{v:"at",x:"dupHidden"}} | 1440, 390 | Journal |
| loc.l4.fail | Failed | Server error, entries kept | LocaleFrame{s:{v:"at",x:"fail"}} | 1440, 390 | Journal |
| loc.l4.gone | Source deleted | Deleted while the dialog was open | LocaleFrame{s:{v:"at",x:"gone"}} | 1440, 390 | Journal |
| loc.l4.unreadable | Source no longer readable | Access lost between opening and submitting | LocaleFrame{s:{v:"at",x:"unreadable"}} | 1440, 390 | Journal |
| loc.l4.gone | Source deleted | Deleted while the dialog was open: same neutral message as unreadable, no distinction shown | LocaleFrame{s:{v:"at",x:"gone"}} | 1440, 390 | Journal |
| loc.l4.unreadable | Source no longer readable | Access lost between opening and submitting: same neutral message as deleted | LocaleFrame{s:{v:"at",x:"unreadable"}} | 1440, 390 | Journal |
| loc.l4.langOff | Language disabled mid-flow | An owner disabled French while the dialog was open | LocaleFrame{s:{v:"at",x:"langOff"}} | 1440, 390 | Journal |
| loc.l4.long | Long dialog | Long title, many authors and images; body scrolls, footer stays | LocaleFrame{s:{v:"at",x:"long"}} | 1440, 390 | Journal |
@@ -326,6 +327,7 @@ Header priority and the action matrix are in the tables below. One filled primar
| loc.l5.titleLong | Title: too long | Over 200 characters | LocaleFrame{s:{v:"ed",x:"titleLong"}} | 1440, 390 | Journal |
| loc.l5.titleSaving | Title: saving | Save in flight | LocaleFrame{s:{v:"ed",x:"titleSaving"}} | 1440, 390 | Journal |
| loc.l5.failed | Title: save failed | Text kept, still unsaved, Try again | LocaleFrame{s:{v:"ed",x:"failed"}} | 1440, 390 | Journal |
| loc.l5.menuDisabled | Language menu with a disabled language | Spanish is disabled: Disabled tag, version stays readable | LocaleFrame{s:{v:"ed",x:"menuDisabled"}} | 1440, 390 | Journal |
## L5b Permission states (per version)
Route: /article/{id}/edit
@@ -391,6 +393,22 @@ Journal: 8 articles, 17 versions, page size 5. The list header shows the unit: "
| loc.l7.tabsScroll | Tabs scrolled | Not in French reachable; fade and chevrons | LocaleFrame{s:{v:"li",x:"tabsScroll"}} | 1440, 390 | Journal |
| loc.l7.long | Eight languages, long labels, filters | Atelier: chips collapse to +3, +4, +4; Draft and SEO filters | LocaleFrame{s:{v:"li",x:"long"}} | 1440, 1024, 390 | Atelier |
## L7s Article list, single-language site
Route: /articles?site=archive&q&status&page&size
API: GET /v1/article/?page=&size=&q=&status=&site= returns { items, total, page, size, counts }
Replaces the old Articles board frames as the pixel-gate reference for the list. A site with one language has no language filter, no coverage column, no Needs translation or Needs review tabs, and its menu labels name no language. The pager is AppPaginator: summary "Showing 1–5 of 5", rows-per-page select, first, previous, page numbers, next and last (390 px: summary and select, then Previous, "Page N of M", Next). Hidden controls when everything fits on one page; the summary stays.
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l7s.default | Default | Archive: 5 articles on one page, no language filter, coverage column or tabs | LocaleFrame{s:{v:"li",x:"single"}} | 1440, 390 | Archive |
| loc.l7s.draft | Draft filter | Status: Draft | LocaleFrame{s:{v:"li",x:"singleDraft"}} | 1440, 390 | Archive |
| loc.l7s.noResults | No results | Search "zinc" | LocaleFrame{s:{v:"li",x:"singleNoResults"}} | 1440, 390 | Archive |
| loc.l7s.empty | Empty | Gallery: no articles | LocaleFrame{s:{v:"li",x:"singleEmpty"}} | 1440, 390 | Gallery |
| loc.l7s.loading | Loading | Counts shown as – | LocaleFrame{s:{v:"li",x:"singleLoading"}} | 1440, 390 | Archive |
| loc.l7s.fail | Failed | Filters kept | LocaleFrame{s:{v:"li",x:"singleFail"}} | 1440, 390 | Archive |
| loc.l7s.menu | Row menu | Open, Unpublish, Copy API URL, Delete article: no language named | LocaleFrame{s:{v:"li",x:"singleMenu"}} | 1440, 390 | Archive |
## L8 Publishing and deletion
Route: same as the editor
API: existing PATCH /v1/article/{id}/publish and /unpublish and DELETE per article id
@@ -402,16 +420,19 @@ Every action names its language. No bulk publish. With unsaved edits the primary
| loc.l8.publish | Publish French | Confirm, English untouched | LocaleFrame{s:{v:"pd",x:"publish"}} | 1440, 390 | Journal |
| loc.lang.publishDisabled | Publishing blocked: language disabled | French disabled on the site | LocaleFrame{s:{v:"pd",x:"publishDisabled"}} | 1440, 390 | Journal |
| loc.l8.saveAndPublish | Save and publish French | Unsaved edits: saves first, then publishes | LocaleFrame{s:{v:"pd",x:"saveAndPublish"}} | 1440, 390 | Journal |
| loc.l8.saveFailPublish | Save failed, nothing published | Toast; edits kept and retryable | LocaleFrame{s:{v:"pd",x:"saveFailPublish"}} | 1440, 390 | Journal |
| loc.l8.saveFailPublish | Save failed, nothing published | Error banner above the editor and an error toast; Save failed badge, primary Try saving again, edits kept, nothing published | LocaleFrame{s:{v:"pd",x:"saveFailPublish"}} | 1440, 390 | Journal |
| loc.l8.unpublish | Unpublish French | Confirm | LocaleFrame{s:{v:"pd",x:"unpublish"}} | 1440, 390 | Journal |
| loc.l8.unpublishDirty | Unpublish with unsaved edits | Saved version goes offline; edits kept; nothing saved | LocaleFrame{s:{v:"pd",x:"unpublishDirty"}} | 1440, 390 | Journal |
| loc.l8.delete | Delete French version | Confirm | LocaleFrame{s:{v:"pd",x:"delete"}} | 1440, 390 | Journal |
| loc.l8.deleteSource | Delete a source version | Warns about review tracking | LocaleFrame{s:{v:"pd",x:"deleteSource"}} | 1440, 390 | Journal |
| loc.l8.deleteSource | Delete a source version | Names the readable versions that track the source; warns review tracking pauses | LocaleFrame{s:{v:"pd",x:"deleteSource"}} | 1440, 390 | Journal |
| loc.l8.deleteLast | Delete the only version | Deletes the article | LocaleFrame{s:{v:"pd",x:"deleteLast"}} | 1440, 390 | Journal |
| loc.l8.deleteStoryOk | Delete article (all readable versions) | Names readable versions only | LocaleFrame{s:{v:"pd",x:"deleteStoryOk"}} | 1440, 390 | Journal |
| loc.l8.deleteStory | Delete article blocked | Neutral, no version named | LocaleFrame{s:{v:"pd",x:"deleteStory"}} | 1440, 390 | Journal |
| loc.l8.done | Published | Toast | LocaleFrame{s:{v:"pd",x:"done"}} | 1440, 390 | Journal |
| loc.l8.done | Published | Toast "French published" and the online banner; badge Published, status Saved | LocaleFrame{s:{v:"pd",x:"done"}} | 1440, 390 | Journal |
| loc.l8.fail | Publish failed | Toast | LocaleFrame{s:{v:"pd",x:"fail"}} | 1440, 390 | Journal |
| loc.l8.deleteDone | French version deleted | Toast; editor opens English | LocaleFrame{s:{v:"pd",x:"deleteDone"}} | 1440, 390 | Journal |
| loc.l8.deleteStoryFail | Delete article failed | Dialog error; nothing deleted | LocaleFrame{s:{v:"pd",x:"deleteStoryFail"}} | 1440, 390 | Journal |
| loc.l8.deleteStoryDone | Article deleted | Toast on the Articles list | LocaleFrame{s:{v:"li",x:"deleteStoryDone"}} | 1440, 390 | Journal |
## L9 Freshness and review (release 2) (release 2)
Route: editor banner · list badge
@@ -441,11 +462,11 @@ Slug is unique per (site, lang). Exact lookup is by site + language + slug and n
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.seo.noFallback | Search and sharing: French | Localized slug, canonical, alternates, fallback labels, Edit slug | LocaleFrame{s:{v:"seo",x:""}} | 1440, 390 | Journal |
| loc.slug.published | Edit slug | Published version: warning about old links | LocaleFrame{s:{v:"seo",x:"editSlug"}} | 1440, 390 | Journal |
| loc.url.unset | Public URL not configured | No pattern, no fabricated URL | LocaleFrame{s:{v:"seo",x:"unset"}} | 1440, 390 | Journal |
| loc.l10.noSocial | Social locale not set | fr has no region and none is set: og:locale omitted | LocaleFrame{s:{v:"seo",x:"noSocial"}} | 1440, 390 | Journal |
| loc.l10.api | Integration: languages | Exact lookup, resolve by article group, errors | LocaleFrame{s:{v:"api",x:""}} | 1440, 390 | Journal |
| loc.seo.noFallback | Search and sharing: French | Search and sharing slide-over on the editor: localized slug, canonical, alternates, fallback labels, Edit slug | LocaleFrame{s:{v:"seo",x:""}} | 1440, 390 | Journal |
| loc.slug.published | Edit slug | Edit slug dialog over the Search and sharing slide-over: published version warns about old links | LocaleFrame{s:{v:"seo",x:"editSlug"}} | 1440, 390 | Journal |
| loc.url.unset | Public URL not configured | Search and sharing slide-over: no pattern, no fabricated URL | LocaleFrame{s:{v:"seo",x:"unset"}} | 1440, 390 | Journal |
| loc.l10.noSocial | Social locale not set | Search and sharing slide-over: fr has no region and none is set, og:locale omitted | LocaleFrame{s:{v:"seo",x:"noSocial"}} | 1440, 390 | Journal |
| loc.l10.api | Integration: languages | Integration page: exact lookup, resolve by article group, errors | LocaleFrame{s:{v:"api",x:""}} | 1440, 390 | Journal |
## L11 Conflicts and sessions
Route: editor
@@ -455,9 +476,9 @@ The conflict dialog gives four choices. Keep my changes is the only filled butto
| ID | State | Trigger | Selector | Sizes | Fixture |
|---|---|---|---|---|---|
| loc.l11.session | Session expired | Draft kept locally | LocaleFrame{s:{v:"pm",x:"session"}} | 1440, 390 | Journal |
| loc.l11.session | Session expired | Draft kept in this browser; editor behind shows the unsaved changes | LocaleFrame{s:{v:"pm",x:"session"}} | 1440, 390 | Journal |
| loc.l11.restored | Restored after login | Editor shows "Restored unsaved changes from 14:18" | LocaleFrame{s:{v:"pm",x:"restored"}} | 1440, 390 | Journal |
| loc.conflict | Concurrent update | Four choices, local work kept | LocaleFrame{s:{v:"pm",x:"conflict"}} | 1440, 390 | Journal |
| loc.conflict | Concurrent update | Four choices, local work kept; Save over theirs outlined in error colour with a warning icon | LocaleFrame{s:{v:"pm",x:"conflict"}} | 1440, 390 | Journal |
| loc.l11.conflictRestored | Concurrent update after session expiry | Restored changes notice above the choices | LocaleFrame{s:{v:"pm",x:"conflictRestored"}} | 1440, 390 | Journal |
| loc.l11.reloadConfirm | Reload theirs: confirm | "This discards your unsaved changes." | LocaleFrame{s:{v:"pm",x:"reloadConfirm"}} | 1440, 390 | Journal |
| loc.l11.conflict2 | Second conflict | Save over theirs failed again; nothing overwritten | LocaleFrame{s:{v:"pm",x:"conflict2"}} | 1440, 390 | Journal |
@@ -477,8 +498,9 @@ Entry point is the site select in the editor details. Articles move as a whole,
| loc.l11b.moveBusy | In progress | Dialog locked | LocaleFrame{s:{v:"pm",x:"moveBusy"}} | 1440, 390 | Journal |
| loc.l11b.moveDone | Moved | Toast | LocaleFrame{s:{v:"pm",x:"moveDone"}} | 1440, 390 | Journal |
| loc.l11b.move | Language missing in destination | Notes enables English only | LocaleFrame{s:{v:"pm",x:"move"}} | 1440, 390 | Journal, Notes |
| loc.l11b.moveSlug | Address conflict | Names the conflicting readable version only | LocaleFrame{s:{v:"pm",x:"moveSlug"}} | 1440, 390 | Journal, Atelier |
| loc.l11b.moveSlug | Address conflict | Names the conflicting readable version and its address; unreadable ones are never named | LocaleFrame{s:{v:"pm",x:"moveSlug"}} | 1440, 390 | Journal, Atelier |
| loc.l11b.movePerm | No permission | Neutral | LocaleFrame{s:{v:"pm",x:"movePerm"}} | 1440, 390 | Journal |
| loc.l11b.moveFail | Move failed | Server error, nothing moved | LocaleFrame{s:{v:"pm",x:"moveFail"}} | 1440, 390 | Journal |
## L12 Existing surfaces
Route: Home · Activity · Media · editor details
@@ -491,11 +513,12 @@ Single-language sites show none of this. Home counts articles, not versions.
| loc.l12.home | Home: recent articles | Default-language title; code only when not default | LocaleFrame{s:{v:"sf",x:"home"}} | 1440, 1024, 390 | Journal |
| loc.l12.homeFr | Home: reader of French only | Titles in French with code FR | LocaleFrame{s:{v:"sf",x:"homeFr"}} | 1440, 1024, 390 | Journal |
| loc.l12.activity | Activity | Events name the language | LocaleFrame{s:{v:"sf",x:"activity"}} | 1440, 1024, 390 | Journal |
| loc.l12.media | Media usage | Shared image counted once, versions listed | LocaleFrame{s:{v:"sf",x:"media"}} | 1440, 1024, 390 | Journal |
| loc.l12.menu | Article menu labels | Publish and Delete name the language | LocaleFrame{s:{v:"sf",x:"menu"}} | 1440, 390 | Journal |
| loc.l12.apiCopy | API URL copy | Copies the version URL with ?lang= | LocaleFrame{s:{v:"sf",x:"apiCopy"}} | 1440, 390 | Journal |
| loc.l12.seoPreview | SEO preview | Localized address and canonical | LocaleFrame{s:{v:"sf",x:"seoPreview"}} | 1440, 390 | Journal |
| loc.l12.siteFallback | Site text fallback | Used only when its language matches the article | LocaleFrame{s:{v:"sf",x:"siteFallback"}} | 1440, 390 | Journal |
| loc.l12.media | Media usage | Media page with the image detail panel: shared image counted once, versions listed, "Used in 2" | LocaleFrame{s:{v:"sf",x:"media"}} | 1440, 1024, 390 | Journal |
| loc.l12.mediaElsewhere | Media usage: used elsewhere only | Image used only as an author avatar: badge "Used in 1", "Articles" with "No article shows it.", "Used elsewhere" with Author avatar linking to /authors | LocaleFrame{s:{v:"sf",x:"mediaElsewhere"}} | 1440, 390 | Journal |
| loc.l12.menu | Article menu labels | Articles list, French selected: row menu names the language (Publish French, Delete French version). Bottom sheet at 390 | LocaleFrame{s:{v:"li",x:"menuLang"}} | 1440, 390 | Journal |
| loc.l12.apiCopy | API URL copy | Articles list: toast after Copy API URL with the ?lang= address. Bottom toast at 390 | LocaleFrame{s:{v:"li",x:"apiCopy"}} | 1440, 390 | Journal |
| loc.l12.seoPreview | SEO preview | Search and sharing slide-over: localized address and canonical | LocaleFrame{s:{v:"seo",x:"preview"}} | 1440, 390 | Journal |
| loc.l12.siteFallback | Site text fallback | Search and sharing slide-over: site text used only in its own language | LocaleFrame{s:{v:"seo",x:"siteFallback"}} | 1440, 390 | Journal |
### Legacy IDs kept
| Canonical ID | Alias |
@@ -519,6 +542,9 @@ Single-language sites show none of this. Home counts articles, not versions.
| loc.url.unset | loc.l10.unset |
| loc.conflict | loc.l11.conflict |
### Added in the third localization pass
New: L5:ed:menuDisabled, L1b:pu:invalidHost, L8:pd:deleteDone, L8:pd:deleteStoryFail, L8:li:deleteStoryDone, L11b:pm:moveFail, L12:sf:mediaElsewhere, L7s:li (default, draft, noResults, empty, loading, fail, menu). Moved selector, same ID: loc.l12.menu, loc.l12.apiCopy, loc.l12.seoPreview, loc.l12.siteFallback.
### Changes against the previous 109
Added (70): L1:sl:broader, L1:sl:defaultFail, L1:sl:stale, L1:sl:reenableDone, L1:sl:reenableFail, L1:sl:removeFail, L1:sl:social, L1:sl:socialEdit, L1:sl:socialInvalid, L1b:pu:set, L1b:pu:editing, L1b:pu:unknown, L1b:pu:scheme, L1b:pu:encoded, L1b:pu:noSlug, L1b:pu:saved, L1b:pu:unset, L1b:pu:viewOnly, L2:su:noLocale, L2:su:inProgress, L4:at:unreadable, L4:at:langOff, L5:ed:cleanOnline, L5:ed:pubFail, L5:ed:ovf, L5:ed:titleEdit, L5:ed:titleUnsaved, L5:ed:titleShort, L5:ed:titleLong, L5b:perm:ro, L5b:perm:roMenu, L5b:perm:p2, L5b:perm:p3, L5b:perm:p4, L5b:perm:p5, L5b:perm:p6, L5b:perm:noAdd, L5b:perm:withAdd, L6:src:arRef, L7:li:page2, L7:li:review, L7:li:author, L7:li:langFilter, L7:li:rowMenu, L8:pd:publishDisabled, L8:pd:unpublishDirty, L8:pd:deleteStoryOk, L9:li:frReview, L10:seo:editSlug, L11:pm:restored, L11:pm:conflictRestored, L11:pm:reloadConfirm, L11:pm:conflict2, L11:pm:download, L11b:pm:moveSelect, L11b:pm:moveSelectHidden, L11b:pm:moveConfirm, L11b:pm:moveBusy, L11b:pm:moveDone, L11b:pm:move, L11b:pm:moveSlug, L11b:pm:movePerm, L12:sf:home, L12:sf:homeFr, L12:sf:activity, L12:sf:media, L12:sf:menu, L12:sf:apiCopy, L12:sf:seoPreview, L12:sf:siteFallback