docs: consolidate design change logs and refresh stale docs
This commit is contained in:
1 parent
c050db78d2
commit
c92d8740ed
40 files changed
+185
-604
No files matched your search
@@ -27,7 +27,7 @@ Upgrade with `docker compose pull && docker compose up -d`. The image is `git.fl
|
|||||||
|
|
||||||
### Updates from the browser
|
### Updates from the browser
|
||||||
|
|
||||||
On Linux with systemd, install v2.6.0-beta or newer manually, then follow [the browser-update setup guide](docs/deployment.md#browser-updates). It covers finding the actual database directory, installing the helper, testing a restart and troubleshooting. Other platforms use manual upgrades.
|
On Linux with systemd, install the helper manually once, then follow [the browser-update setup guide](docs/deployment.md#browser-updates). It covers finding the actual database directory, installing the helper, testing a restart and troubleshooting. Other platforms use manual upgrades.
|
||||||
|
|
||||||
### Develop
|
### Develop
|
||||||
|
|
||||||
@@ -52,10 +52,6 @@ task dev # API with hot reload + Vite HMR → http://localhost:51
|
|||||||
| [docs/performance.md](docs/performance.md) | Measured costs, such as the `?seo=` article filter |
|
| [docs/performance.md](docs/performance.md) | Measured costs, such as the `?seo=` article filter |
|
||||||
| [docs/api.md](docs/api.md) | API reference: authentication, API keys, routes and the activity log |
|
| [docs/api.md](docs/api.md) | API reference: authentication, API keys, routes and the activity log |
|
||||||
| [docs/api/articles.md](docs/api/articles.md) | Reading articles: pagination, search, `format` and body schemas |
|
| [docs/api/articles.md](docs/api/articles.md) | Reading articles: pagination, search, `format` and body schemas |
|
||||||
| [docs/custom-content-types.md](docs/custom-content-types.md) | Custom content types: layering, phase 1 limitations and their fix path |
|
| [docs/custom-content-types.md](docs/custom-content-types.md) | Custom content types: layering and known limitations |
|
||||||
| [docs/image-variants.md](docs/image-variants.md) | Image variants and their cache |
|
| [docs/image-variants.md](docs/image-variants.md) | Image variants and their cache |
|
||||||
| [docs/release-notes.md](docs/release-notes.md) | Release notes format, the published releases and how to publish one |
|
| [docs/release-notes.md](docs/release-notes.md) | Release notes format, the published releases and how to publish one |
|
||||||
|
|
||||||
## TODOs
|
|
||||||
|
|
||||||
- Cancel button when email validation pending or expired
|
|
||||||
@@ -12,7 +12,7 @@ A content type is a named schema (a key, a name and 1 to 50 fields) stored as on
|
|||||||
|
|
||||||
The domain packages `internal/domain/contenttype` and `internal/domain/entry` own the rules: key, name and field validation, entry validation against a schema, the sort whitelist, the page limits and the value equality used by uniqueness. The use cases orchestrate them through the two repositories. The repositories only store and load rows. Entry counting for the "type still has entries" rule lives on the entry repository.
|
The domain packages `internal/domain/contenttype` and `internal/domain/entry` own the rules: key, name and field validation, entry validation against a schema, the sort whitelist, the page limits and the value equality used by uniqueness. The use cases orchestrate them through the two repositories. The repositories only store and load rows. Entry counting for the "type still has entries" rule lives on the entry repository.
|
||||||
|
|
||||||
## Known limitations of phase 1
|
## Known limitations
|
||||||
|
|
||||||
- Uniqueness and schema re-validation are O(n). Checking a unique field loads every entry of the type and compares values in Go, and updating a type with stored entries loads all of them to re-validate. This keeps the SQL portable across SQLite, MySQL/MariaDB and PostgreSQL. The path to fix it is the filterable-fields phase: project the values of unique and filterable fields into an indexed table, so uniqueness becomes an indexed lookup and re-validation can be restricted to the changed fields.
|
- Uniqueness and schema re-validation are O(n). Checking a unique field loads every entry of the type and compares values in Go, and updating a type with stored entries loads all of them to re-validate. This keeps the SQL portable across SQLite, MySQL/MariaDB and PostgreSQL. The path to fix it is the filterable-fields phase: project the values of unique and filterable fields into an indexed table, so uniqueness becomes an indexed lookup and re-validation can be restricted to the changed fields.
|
||||||
- Unique values are checked without a transaction, so two concurrent writes of the same unique value can both succeed. A content type key is protected by a unique index, and a violation is reported as `content_type_key_taken` (409).
|
- Unique values are checked without a transaction, so two concurrent writes of the same unique value can both succeed. A content type key is protected by a unique index, and a violation is reported as `content_type_key_taken` (409).
|
||||||
|
|||||||
+3
-3
@@ -45,7 +45,7 @@ Once the restart helper is installed (it writes `update-helper.json` next to `DB
|
|||||||
|
|
||||||
The owner's Restart button installs a downloaded update, or just restarts the site, without touching the server by hand. The app itself stays unprivileged: the systemd unit below keeps `ProtectSystem=strict` and only the data directory (the directory that holds `DB_FILE`) is writable. The only privileged code is a small helper, the same binary started as `renewcms apply-update` by a systemd service that runs as root.
|
The owner's Restart button installs a downloaded update, or just restarts the site, without touching the server by hand. The app itself stays unprivileged: the systemd unit below keeps `ProtectSystem=strict` and only the data directory (the directory that holds `DB_FILE`) is writable. The only privileged code is a small helper, the same binary started as `renewcms apply-update` by a systemd service that runs as root.
|
||||||
|
|
||||||
First install v2.6.0-beta or newer manually, back up the database, uploads and current binary, and confirm the app runs as an unprivileged service user. The binary and its parent directory should be controlled by root. Owners can start updates and restarts; admins have read-only update status.
|
First install the helper manually, back up the database, uploads and current binary, and confirm the app runs as an unprivileged service user. The binary and its parent directory should be controlled by root. Owners can start updates and restarts; admins have read-only update status.
|
||||||
|
|
||||||
#### Find the actual database directory
|
#### Find the actual database directory
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ On the latest release you can test only a plain restart. Test installation, forc
|
|||||||
- **Restart is missing:** reload Settings, confirm you are owner, and inspect `GET /v1/system/update` in the browser's Network panel. For an idle owner, `can_apply: false` means the app did not detect the helper. Check that `update-helper.json` is beside the actual database and readable by the service user.
|
- **Restart is missing:** reload Settings, confirm you are owner, and inspect `GET /v1/system/update` in the browser's Network panel. For an idle owner, `can_apply: false` means the app did not detect the helper. Check that `update-helper.json` is beside the actual database and readable by the service user.
|
||||||
- **Wrong helper directory:** disable the helper with the old `--data-dir`, then enable it with the correct directory and port. Do not change `DB_FILE` to compensate.
|
- **Wrong helper directory:** disable the helper with the old `--data-dir`, then enable it with the correct directory and port. Do not change `DB_FILE` to compensate.
|
||||||
- **Overlay does not clear:** inspect the helper journal, `update-result.json` and `update-running.json` in the actual database directory. Check localhost health on the configured port, then public health through the proxy. Do not cache `/healthz` at the proxy.
|
- **Overlay does not clear:** inspect the helper journal, `update-result.json` and `update-running.json` in the actual database directory. Check localhost health on the configured port, then public health through the proxy. Do not cache `/healthz` at the proxy.
|
||||||
- **Old binary panics on `--version`:** older releases may interpret it as normal startup and require server environment variables. v2.6.0-beta and newer print their version without loading configuration. A separately launched process failing this way does not stop the systemd service.
|
- **Old binary panics on `--version`:** releases before v2.6.0-beta treat it as normal startup and require server environment variables. A separately launched process failing this way does not stop the systemd service.
|
||||||
- **Download or verification fails:** inspect the app journal. All assets and redirects must use HTTPS on the configured feed host. The release must include the six binaries, `checksums.txt` and `checksums.txt.sig`.
|
- **Download or verification fails:** inspect the app journal. All assets and redirects must use HTTPS on the configured feed host. The release must include the six binaries, `checksums.txt` and `checksums.txt.sig`.
|
||||||
- **Update rolls back:** check the previous version and helper journal. Automatic rollback restores the binary, not the database; schema changes still require a recovery plan.
|
- **Update rolls back:** check the previous version and helper journal. Automatic rollback restores the binary, not the database; schema changes still require a recovery plan.
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ What is supported and what is not:
|
|||||||
|
|
||||||
#### Verification status
|
#### Verification status
|
||||||
|
|
||||||
A real v2.6.0-beta plain browser restart was confirmed on Linux/systemd on 2026-10-02: the path unit triggered the helper, a new server process started, the result was `succeeded`, and public health returned `ok`. Real signed installation, forced rollback and adversarial filesystem checks remain unverified. The manual cases are listed in [the restart QA procedure](../features/updates_restart_helper.qa.md).
|
Only the plain browser restart has been confirmed on a real Linux/systemd machine. Signed installation, forced rollback and adversarial filesystem checks remain unverified; the manual cases are in [the restart QA procedure](../features/updates_restart_helper.qa.md).
|
||||||
|
|
||||||
## 2. Lay out the server
|
## 2. Lay out the server
|
||||||
|
|
||||||
|
|||||||
+28
-148
@@ -1,156 +1,36 @@
|
|||||||
# RenewCMS design pass 2
|
# RenewCMS design
|
||||||
|
|
||||||
Nuxt UI v4 back-office. Tokens only, no raw hex (brand mark excepted). Light and dark. 1440×900 and 390×844. Radius 0.
|
Nuxt UI v4 back-office. Tokens only, no raw hex (the brand mark excepted). Light and dark, at 1440×900 and 390×844. Radius 0. Behaviour comes from `features/`; where the two disagree, stop and ask.
|
||||||
|
|
||||||
## Boards
|
## Files
|
||||||
- `RenewCMS Screens Back-office.dc.html`: every back-office screen, Settings → Media (variants, cache), and the group "States matrix: controls". Frames: `AppFrame` (shell), `DirectoryFrame` (Sites and Authors), `ActivityFrame`, `MediaFrame`, `VariantsFrame`, `IntegrationPage`, `IntegrationSearch`, `IntegrationImages`, `CodeSnippet`, `StatesMatrix`.
|
|
||||||
- `RenewCMS Screens Auth.dc.html` (7 groups, 50 states), `RenewCMS Screens API keys.dc.html` (12 states).
|
|
||||||
- `RenewCMS Prototype.dc.html`: updated to match (title-only New article, no body limit or counter, neutral count, pagination, link button, AppFrame's mobile drawer).
|
|
||||||
- Unchanged: Foundations, Components, Handoff, Directions and Emails boards.
|
|
||||||
|
|
||||||
## Pass 3: sites and authors (revised)
|
| File | Content |
|
||||||
Roles, decisions and the slice order are in `sites-authors-slices.md` (Sites and Authors) and `seo-slices.md` (SEO).
|
| --- | --- |
|
||||||
|
| `routes.md` | Routes with their auth requirement. |
|
||||||
|
| `components.md` | Component inventory with its Nuxt UI mapping, then the full component specs. |
|
||||||
|
| `states.md` | Every state per screen, plus the HTTP-status-to-copy table. |
|
||||||
|
| `screens/<screen>.md` | Exact copy per state. `visual:check` takes the screen name from the file name. |
|
||||||
|
| `foundations.md` | Tokens, spacing, type and contrast. |
|
||||||
|
| `emails.md` | The mail templates. |
|
||||||
|
| `decisions.md` | Product and interface decisions that are not visible in the specs: search and sharing, sites and authors, roles, account self-service and the interface defaults. |
|
||||||
|
| `media-field.md` | Spec of the Media field type: control, states, copy, accessibility and API. |
|
||||||
|
| `source/` | The raw export. The `.dc.html` boards are the visual reference, to open in a browser. `tokens/` and `emails/` hold the ready-to-use token and template files. |
|
||||||
|
|
||||||
Back-office board groups: Sites, Sites: form, Sites: delete, Authors, Authors: form, Authors: delete, Editor: site and authors, Profile: author profile, Articles: sites and authors. Plus tables: decisions to confirm (13 rows, 5 marked Decided), deviations, API and errors (with the 403 rows), components, accessibility, copy per state.
|
## Boards (in `source/`)
|
||||||
- Roles: only creating, editing, deleting and setting the default site are owner only (no sidebar entry or Publishing card for others, 403 page on a direct link). Anyone who can edit an article can set its site and read the site list. Authors: owner and admin manage; other roles read-only, and any role can create and edit their own author (user link locked, no delete).
|
|
||||||
- The site key is immutable: read-only with a lock in the edit form; the warning state and dialog are gone.
|
|
||||||
- The "Author profile" card is drawn in every existing Profile frame. The "Site and authors" section shows collapsed in every existing Editor frame that has the details sidebar (39 already did; the Loading frame now shows the header with a skeleton summary). The Not found frame has no editor, and on mobile the section is inside the Details sheet. The dedicated groups keep only the feature-specific states.
|
|
||||||
- Shared pieces (list, slide-over, delete-with-reassign dialog, slug field, media field) are defined once in `DirectoryFrame`, used by Sites and Authors. The Profile card is defined once in `AppFrame`.
|
|
||||||
- States trimmed from 87 to 68 (Sites 9, Sites: form 7, Sites: delete 3, Authors 8, Authors: form 11, Authors: delete 6, Editor: site and authors 16, Profile: author profile 5, Articles: sites and authors 3). Dropped: in-progress and done variants (covered by the button loading pattern and the confirm state's trigger), per-error URL variants, the site picker (same picker as the avatar), manual slug override, long names (merged into Many authors), per-field duplicates. The blocked-delete dialog is one frame, "Server fallback (409)".
|
|
||||||
- Only individual states that render a relative time or a countdown carry the "Clock-sensitive" tag (13 states, listed in states.md). No Home or Articles frame is tagged as a whole.
|
|
||||||
- Role-restricted variants drawn: read-only Authors list and detail, hidden Sites entry, Sites 403, own-author edit.
|
|
||||||
|
|
||||||
## Pass 4: custom content types
|
- `RenewCMS Screens Back-office.dc.html`: every back-office screen, with the frames `AppFrame` (shell), `DirectoryFrame` (Sites and Authors), `ActivityFrame`, `MediaFrame`, `VariantsFrame`, `IntegrationPage`, `IntegrationSearch`, `IntegrationImages`, `CodeSnippet` and `StatesMatrix`.
|
||||||
Board `RenewCMS Screens Schema.dc.html` (frames: `SchemaFrame`, mounted in `AppFrame`): 11 groups, 99 states. 1 Schema list 7, 2 Schema builder 19, 3 Review and delete 9, 4 Field types 7, 5 Content and entry list 12, 6 Entry editor 23, 7 Relations 5, 8 Permissions 2, 9 Export and import 3, 10 Media picker for a field 8, 11 Media library usage 4. The Media field type (pass 5) is specified in `media-field-changes.md`; the exact copy and measurements are there, and the boards are the visual reference. Plus tables: scope, routes, new components with their Nuxt UI mapping, errors to copy, needs backend (4 rows), decisions to confirm, accessibility. Specs: screens/schema.md, content.md, entry.md, schema-later.md, media.md, media-picker.md, and media-field-changes.md for the Media field type.
|
- `RenewCMS Screens Auth.dc.html`: log in, register, pending, verify, accept invite, forgot and reset password, verify new email, account deleted.
|
||||||
- AppFrame gains a Schema item (owner and admin) and a Content group (sidebar and mobile drawer). Nothing else in AppFrame changed.
|
- `RenewCMS Screens API keys.dc.html`.
|
||||||
- Backend gaps still open: built-in types and fields, display names for created_by and updated_by, a type icon, per-change entry counts.
|
- `RenewCMS Screens Roles.dc.html`: the role editor, users and ownership (`RolesFrame`, data `roles-spec.js`).
|
||||||
- Not done: Back-office and Prototype boards were not compared visually after the sidebar change (see Checks below).
|
- `RenewCMS Screens Schema.dc.html`: custom content types, entries, relations and the Media field (`SchemaFrame`).
|
||||||
|
- `RenewCMS Screens Account.dc.html`: change email, export your data and delete your account (`AccountCards`, copy in `account-copy.js`).
|
||||||
|
- `RenewCMS Emails.dc.html`, `RenewCMS Foundations.dc.html`, `RenewCMS Components Primitives.dc.html`, `RenewCMS Components Product.dc.html`, `RenewCMS Handoff.dc.html`, `RenewCMS Directions.dc.html` and `RenewCMS Prototype.dc.html`.
|
||||||
|
|
||||||
## Specs
|
Every state has a desktop and a mobile frame, and the theme switch gives light and dark. States that render a relative time or a countdown are tagged clock-sensitive in `states.md` and skipped by `task visual:check`.
|
||||||
routes.md · components.md · states.md · foundations.md · change-list.md · activity-spec.js · screens/*.md (24 files)
|
|
||||||
|
|
||||||
## Status
|
## Known gaps
|
||||||
| Section | Status |
|
|
||||||
|---|---|
|
|
||||||
| A1 Activity export | Done: CSV only, synchronous; progress state removed; JSON proposed |
|
|
||||||
| A2 Pagination `page`/`size` | Done in frames, routes.md, change-list.md |
|
|
||||||
| A3 Unknown variant and unsupported parameter | Done: both 400 bodies in the Responses table |
|
|
||||||
| A4 New tokens | Done: foundations.md table |
|
|
||||||
| A5 screens/*.md | Done: 22 files, copy generated from the board source |
|
|
||||||
| B1 Editor link | Done: 11 states |
|
|
||||||
| B2 Duplicates | Done for sidebar, topbar, drawer, code block, modal geometry; see Not done |
|
|
||||||
| B3 CodeSnippet | Done |
|
|
||||||
| B4 Overlays | Done |
|
|
||||||
| B5 Prototype | Done |
|
|
||||||
| B6 Contrast and states matrix | Done for the pairs listed; matrix drawn |
|
|
||||||
| B7 Consistency sweep | Done for the roles listed |
|
|
||||||
| B8 Copy glossary | Done by search; not every line re-read |
|
|
||||||
| B9 Media and picker | Badge added; Media pagination drawn (9 states); filter, sort and size not folded in |
|
|
||||||
| B10 "proposed" labels | Only article pagination and JSON export |
|
|
||||||
|
|
||||||
## State counts (Back-office board: 341 before pass 3, plus 68 for sites and authors = 413)
|
- The Foundations, Components and Prototype boards do not show the account self-service pieces or every newer token.
|
||||||
| Group | States |
|
- Duplicate implementations that look alike are not merged in code: toasts, badges, empty states and modal shells.
|
||||||
|---|---|
|
- The Back-office board still shows the owner, admin and user navigation. Writer frames use the restricted `user` navigation of `AppFrame`.
|
||||||
| Boot | 1 |
|
- The Schema board group 8 (Permissions) is superseded by the role editor, and `/settings/permissions` redirects to `/settings/roles`.
|
||||||
| Home | 10 |
|
|
||||||
| Articles | 14 |
|
|
||||||
| Articles: pagination | 11 |
|
|
||||||
| New article | 5 |
|
|
||||||
| Editor | 41 |
|
|
||||||
| Media | 56 |
|
|
||||||
| Media picker | 13 |
|
|
||||||
| Integration | 8 |
|
|
||||||
| Activity | 9 |
|
|
||||||
| Activity: filter cards | 10 |
|
|
||||||
| Activity: export | 15 |
|
|
||||||
| Activity details | 6 |
|
|
||||||
| My activity | 3 |
|
|
||||||
| Users | 17 |
|
|
||||||
| Settings | 25 |
|
|
||||||
| Settings: Media cleanup | 9 |
|
|
||||||
| Purge dialog | 8 |
|
|
||||||
| Purge results | 2 |
|
|
||||||
| Media duplicates entry | 2 |
|
|
||||||
| Settings: Retention | 6 |
|
|
||||||
| Profile | 13 |
|
|
||||||
| Global | 4 |
|
|
||||||
| Integration: images | 9 |
|
|
||||||
| Settings navigation | 5 |
|
|
||||||
| Variants list | 12 |
|
|
||||||
| Add edit variant | 11 |
|
|
||||||
| Media library (variants) | 6 |
|
|
||||||
| Image detail | 8 |
|
|
||||||
| Cache panel | 10 |
|
|
||||||
| Editor picker | 5 |
|
|
||||||
|
|
||||||
Back-office total 341 (pass 2 groups, table above) + 68 (sites and authors) + 4 (Media: used elsewhere) = 413. Auth board 50 (Log in 9, Register 6, Pending 6, Verify 6, Accept invite 10, Forgot password 6, Reset password 7). API keys board 12. Total 481 states; every one has a desktop and a mobile frame, and the theme switch gives light and dark. The matrix has 24 control rows, 5 states on desktop and 4 on mobile (no hover). screens/*.md list the states of the pass 2 groups and the 68 pass 3 states.
|
|
||||||
|
|
||||||
## Fixes applied (pass 2)
|
|
||||||
1. Activity export dialog: removed the 42% progress bar, the "large export" state, the server-job copy and the "ready later" toast; added "Preparing download…" (indeterminate), "Download started", failed, nothing to export, 403; CSV only, with one proposed JSON state.
|
|
||||||
2. Export dialog and menu copy now match the implemented endpoint (current view with filters, all with optional range).
|
|
||||||
3. Articles pagination used `per_page`, sizes 10/25/50, default 10, 32 pages. Now `page`/`size`, 5/25/50/100, default 25, 13 pages; frames, route, URL examples and the change list updated.
|
|
||||||
4. The Integration responses table now shows both 400 bodies, `unknown_variant` and `unsupported_parameter`.
|
|
||||||
5. `--rc-sand` and `--rc-on-sand` (and the other `--rc-*` tokens) documented as new tokens with light and dark values and their uses.
|
|
||||||
6. Raw colours removed from frames: crop handles and mask, tooltip, transparency checker, drag-drop text, selected-row text in MediaFrame, hex in the Prototype. Only the brand mark keeps hex.
|
|
||||||
7. ActivityFrame, MediaFrame and VariantsFrame sidebars, topbars and (Variants) mobile drawer deleted; they render inside AppFrame. Activity now follows the sidebar order (Users, Activity, Integration, Settings).
|
|
||||||
8. Prototype's own mobile drawer (Home, Articles, Users, Integration only) replaced by AppFrame's drawer.
|
|
||||||
9. All code blocks on Integration (auth example, endpoint examples, error response), the base URL, the image URL builder and the one-time API key use CodeSnippet; the three older block styles are gone. CodeSnippet tabs changed from sand pills to the underline style used by tabs elsewhere.
|
|
||||||
10. Variant form: mobile full page replaced by a 92% bottom sheet with grabber and close ×; back arrow removed.
|
|
||||||
11. Dialog geometry unified (140 px top offset on desktop, flush bottom sheets with grabber on mobile, header 20 24 0, footer secondary left and primary right, close × 44 px on mobile). Changed: Media and Variants dialogs (mobile inset), Activity export (centered, header bar, 480 wide), API keys dialogs (border, 18 px title, 44 px buttons, 120 px offset).
|
|
||||||
12. Current page in the paginator: border `--ui-primary` instead of sand (was 1.3:1).
|
|
||||||
13. Page padding 24/16 everywhere (was 32 and 24); card padding 16 (was 16 and 20); section gap 16 (was 16, 20, 28); page title 20/28 (was 28/36, 24/32, 20/28); stat figure 24/32; card title 15/24; inputs 32/44 (were 40); one 13 px icon became 14; toast width 356 (was 360, 372).
|
|
||||||
14. Copy: "Removed N …" in activity lines became "Deleted N …"; export titles and footers per the glossary.
|
|
||||||
15. Media screen: variant count badge added to grid card, list row and mobile row, matching the Variants board.
|
|
||||||
16. Editor toolbar on mobile was a cut-down 9-button subset; it now has the full toolbar with sideways scroll and edge fades.
|
|
||||||
17. Editor words and characters show "0 words · 0 characters" for a new empty article (was a fallback sample count).
|
|
||||||
|
|
||||||
## Defaults chosen
|
|
||||||
- Slide-over rule: only forms with a live preview and record details; on mobile always a 92% bottom sheet.
|
|
||||||
- Hover, active, disabled and focus tokens for the matrix (components not specified before): primary hover `--ui-text`, active `--ui-text-toned`; destructive hover opacity 0.85 and active 0.75; disabled 40% (rows 55%).
|
|
||||||
- Article pagination: Shipped shapes: the article list body is {items, total, page, size, counts}; the media list body is an array with the total in `X-Total-Count`. Users and authors follow their own routes (see routes.md).
|
|
||||||
- The Remove action in the link popover has no confirmation; undo restores the link.
|
|
||||||
- The link button is disabled when an image is selected.
|
|
||||||
- The tooltip text is "Insert link (Ctrl+K)" and "Insert link (⌘K)" on macOS.
|
|
||||||
- Mobile toolbar scrolls sideways; no wrap, no overflow menu.
|
|
||||||
- Selected nav items keep the sand fill plus weight 500 and `aria-current` (the fill alone is 1.3:1 in light).
|
|
||||||
- Syntax colouring dropped from code samples (one rendering).
|
|
||||||
- Export "Download started" offers only Close.
|
|
||||||
- New article keeps the title counter "n / 200" (title is limited to 200); the body has no limit and no counter.
|
|
||||||
- Page title 20/28, page padding 24/16, card padding 16 (foundations.md).
|
|
||||||
- Activity position in the sidebar follows AppFrame.
|
|
||||||
- The Prototype shows a toast "Not in this prototype" for sidebar items it has no screen for (Media, Activity, Settings).
|
|
||||||
|
|
||||||
## Checks run
|
|
||||||
- Opened `RenewCMS Screens Back-office.dc.html` in the preview: it loaded, 38 sections rendered, and I read the DOM of the Activity, Export, Media, Variants, Editor, Integration, Pagination, Picker, Editor picker and States matrix groups to confirm the frames render text and the sidebar. I did not read the console.
|
|
||||||
- Computed the contrast table from token values with a script.
|
|
||||||
- Searched frames and boards for raw hex and rgb values (only token definitions on the board roots and the brand mark remain), for exclamation marks, Remove and Delete, Update and Save, and per_page.
|
|
||||||
- Generated screens/*.md and counted states from the board source; the counts equal the DOM counts for the Back-office board.
|
|
||||||
|
|
||||||
## Not done
|
|
||||||
- Console errors on the Auth, API keys, Prototype, Foundations and Components boards were not read by me. The delivery check covers the Back-office board; the other boards use the changed frames (AppFrame, ApiKeysPage, AuthCard, MediaFrame) and may need a look.
|
|
||||||
- Light and dark were checked for the Back-office board by reading DOM only; I did not view the screenshots of each state at 1440 and 390, so visual regressions from the shell change (slot mode) are possible.
|
|
||||||
- Media screen: the "With variants / Without" filter, the "Largest total size" sort and a total-size column exist only on the Variants board states; they are not folded into the Media toolbar.
|
|
||||||
- Media picker tiles and the Editor picker were not compared field by field.
|
|
||||||
- Duplicated implementations that look alike are not merged in code: toasts (5), badges (2), empty states (2), modal shells (4). Only their visuals were aligned.
|
|
||||||
- Hover, active and focus-visible are drawn once in the matrix; screens do not redraw them. Hover and active tokens for destructive buttons use opacity, not a token.
|
|
||||||
- Contrast was not computed for every hover and active background, only the pairs listed in foundations.md.
|
|
||||||
- The glossary pass was a search for known patterns, not a line-by-line read of every string. Titles in Title Case were not searched.
|
|
||||||
- The Components, Handoff and Foundations boards were not updated to mention the new tokens, CodeSnippet or the matrix.
|
|
||||||
- Integration still lists "max 10000" for article bodies, as the backend does today (change-list item 11).
|
|
||||||
- The sandbox cannot export; no files were bundled.
|
|
||||||
|
|
||||||
## Pass 5: roles and permissions
|
|
||||||
Board `RenewCMS Screens Roles.dc.html` (frame `RolesFrame`, data `roles-spec.js`): 16 groups, 96 states, all Available now (pass 6 added groups 12 to 15 and changed groups 5, 8 and 9; see change-log-roles-sync.md). Spec `screens/roles.md`; additions in components.md, states.md, routes.md, change-list.md. The Permissions grid is folded into the role editor and `/settings/permissions` is retired. Writer frames use AppFrame's existing restricted `user` navigation. Not done: AppFrame itself is unchanged, so the Back-office board still shows owner/admin/user nav; Schema board group 8 (Permissions) should be marked superseded.
|
|
||||||
|
|
||||||
The Schema board group 7 (Relations) now has 4 states; the Integration page and the Activity category filter follow the roles sync (see `change-log-roles-sync.md`).
|
|
||||||
|
|
||||||
## Sync notes (app sync)
|
|
||||||
- Back-office board: +4 Media states (Used elsewhere: avatar, site, both, none). Roles board: +6 states (group 12b Remove user).
|
|
||||||
- Pagination: articles return {items, total, page, size, counts}; media returns an array with X-Total-Count.
|
|
||||||
- Event names follow the backend: role.create, role.edit, role.delete, role.reassign, user.change_role, user.transfer_ownership, user.remove.
|
|
||||||
- Counts above include the Media (+4) and Roles (+6) states only. States added this round to the Editor, Articles, Sites, Authors, Profile and Schema groups are on the boards and in the screens files, and are not yet recounted here.
|
|
||||||
|
|
||||||
## Pass 7: account self-service (email, export, delete)
|
|
||||||
Board `source/RenewCMS Screens Account.dc.html` (33 states: change email 14, export 9, delete 10), Auth board groups "Verify new email" (7) and "Account deleted" (1), four email templates in `source/emails/` (previews on the Emails board). Specs: `screens/account.md`, `emails.md`, `routes.md`, `components.md`, `states.md`. Frames: `source/AccountCards.dc.html` (new; copy in `source/account-copy.js`), `AppFrame.dc.html` (Profile hooks, modal list, warning, link and disabled confirm). Every Profile frame on the other boards shows the new cards in their default state. English only.
|
|
||||||
Decided: authored content is anonymized to "Deleted user"; email-change link 2 hours; export link 24 hours, one export per 15 minutes; direct download is the primary export flow, the emailed link is the documented alternative; the export-ready link needs a logged-in session; "last administrator" counts Owner; a change request for an address already in use answers like success; the account-deleted email goes to the address the account had.
|
|
||||||
Not done: Foundations and Components boards and the Prototype are not updated. The 15-minute rate-limit state is clock-sensitive (frozen clock 09:41) and is skipped by `task visual:check`.
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# Change list for implementation (status after pass 2; SEO round drawn; cleanup round drawn)
|
|
||||||
|
|
||||||
## Cleanup round
|
|
||||||
|
|
||||||
1. Editor: site and authors, "Create author" states (4 states, desktop and mobile): the mobile dialog sat under the Details sheet (z-index 50 against 55) and was hidden. It now stacks above the sheet (z-index 60), as the SEO panel does. Full-width bottom sheet with grabber, same fields, Cancel left and "Create author" right (Cancel below on mobile, as the other mobile dialogs). Frame: `DirectoryFrame.dc.html`. The four states share the fix: dialog, "for me", slug taken, failed.
|
|
||||||
2. Integration, "6. Errors": the 403 row gets the muted note "Site SEO defaults are owner only. Article SEO fields need only the right to edit the article." The row below shifts down by the wrapped note height (two 18 px lines desktop, three mobile, plus the 2 px row gap). The old inline parenthetical with the same sentence in the 403 cause text was removed, so it isn't said twice. Frame: `IntegrationPage.dc.html`.
|
|
||||||
3. Integration, sitemap errors: 404 now shows the body `site_not_found` in code style (was "Site not found"); 400 now shows the generic body-mismatch text used by the other routes (was "Invalid page or size.").
|
|
||||||
4. New groups on the Back-office board: "Sites: pagination" and "Authors: pagination", 10 states each (20 states, every one desktop and mobile; light and dark by the theme switch). Back-office total 409 → 429. Same pager as Articles and Media, sizes 5, 25, 50, 100, default 25 (no difference to report). One deviation: on a single page the page-size select is hidden and only "N sites" or "N authors" remains.
|
|
||||||
5. Specs: screens/sites.md, screens/authors.md (pager states, URL rules, out-of-range fallback), screens/integration.md, states.md (pagination states, three HTTP rows), components.md (single-page variant, no new component), routes.md (Sites pagination in the URL).
|
|
||||||
|
|
||||||
**Backend assumptions to confirm.** GET /v1/site/ takes `q`, `page`, `size` and returns `X-Total-Count` (it returns every site today). GET /v1/author/ gets `page`, `size` and `X-Total-Count` (it has `q`).
|
|
||||||
|
|
||||||
**Open points.**
|
|
||||||
- Sites has no search field. The two states that need one (empty after search, search resets to page 1) draw the Authors field on Sites with "Search by name or key". Drop those states if you don't want search on Sites.
|
|
||||||
- Stale copy found, not changed: GET /v1/article/ on Integration still shows the 400 caption "Invalid page or size." while every other route, and now the sitemap, uses the body-mismatch text. The 404 row of the Errors table still lists only the four resource messages and doesn't mention `site_not_found`. The existing Sites and Authors "Default" frames don't draw the count line. README.md state counts and the "Pagination" notes were not updated (outside this round).
|
|
||||||
|
|
||||||
## SEO round: decisions (frames drawn)
|
|
||||||
|
|
||||||
**Placement.** A "Search and sharing" section sits under "Site and authors" in the Editor sidebar and in the mobile Details sheet. It is collapsed by default, shows a one-line summary ("Ready", "2 suggestions" or "Hidden from search") and is remembered per browser like Site and authors. It holds only the slug, the full URL and three actions: "Edit search and sharing", "What the frontend gets" and the suggestion list. The fields and live previews open in a slide-over (desktop) or a 92% bottom sheet stacked over the Details sheet (mobile), because the 280 px sidebar is too narrow for previews and the slide-over rule already covers forms with a live preview. The API preview is a 720 px dialog (desktop) or a bottom sheet (mobile).
|
|
||||||
|
|
||||||
**Principles.** RenewCMS does not render pages or inject meta tags. Every field is optional and the UI shows its fallback. No scores. Suggestions never block saving or publishing; publishing is allowed regardless. Suggestions are dismissible per article.
|
|
||||||
|
|
||||||
**Fallback chain (resolved server-side, shown in the UI).**
|
|
||||||
| Output | Order |
|
|
||||||
|---|---|
|
|
||||||
| title | seo_title, then article title; site title_template applied last |
|
|
||||||
| description | seo_description, then excerpt, then first sentence of the body, then site default_description |
|
|
||||||
| image | og_image_id, then cover_image_id, then site social image |
|
|
||||||
| canonical | canonical_url, then site base_url + "/" + slug |
|
|
||||||
| robots | noindex true gives "noindex, nofollow"; otherwise site default_robots |
|
|
||||||
| og/twitter title and description | social_title, social_description, then the resolved values above |
|
|
||||||
|
|
||||||
**Soft ranges (counters, never blocks).** Meta title 30–60 characters; description 70–160; slug "very long" above 75 characters. Counter turns neutral-warning (`--ui-warning`) outside the range, never red.
|
|
||||||
|
|
||||||
**New article fields (PATCH /v1/article/{id}, all optional, snake_case).** `slug`, `excerpt` (max 300), `seo_title` (max 120), `seo_description` (max 320), `cover_image_id`, `cover_image_alt` (max 200; the cover is "ready" only with alt text), `social_title`, `social_description`, `social_image_id`, `canonical_url` (absolute https URL), `noindex` (boolean; only sets a flag in the API, it doesn't change visibility), `seo_dismissed` (string[] of suggestion codes). Slug: lowercase letters, numbers, hyphens; unique per site; 409 `slug_taken` with `suggestion` ("<slug>-2"); auto-generated from the title until edited.
|
|
||||||
|
|
||||||
**New site fields (PATCH /v1/site/{id}, owner only).** `site_name`, `title_template` (default "%title% | %site%", must contain %title%), `default_description` (max 320), `social_image_id` (existing), `default_robots` ("index" | "noindex"; existing as robots).
|
|
||||||
|
|
||||||
**New endpoints.**
|
|
||||||
- `GET /v1/article/{id}/seo`: resolved object, public for online articles, 404 for drafts to anonymous callers. Shape: `{ title, description, canonical_url, robots, og: { type, title, description, image, image_alt, url, site_name, locale }, twitter: { card, title, description, image, image_alt }, image: { url, alt, source }, authors: [{ name, url, same_as: [] }], published_at, modified_at, fallbacks: { title, description, image, canonical }, json_ld }`. `json_ld` is a ready Article object (headline, description, image, author[] with name, url and sameAs from author profile URL and social links, datePublished, dateModified, mainEntityOfPage).
|
|
||||||
- `GET /v1/site/{key}/sitemap?page=&size=`: published URLs only, total in `X-Total-Count`. Items: `{ url, modified_at, noindex }`. Decision: yes, shown on Integration under "Search and sharing" with a copy button; noindex articles are included with the flag so the frontend can skip them.
|
|
||||||
- `GET /v1/article/?seo=ready|suggestions|hidden`: filter behind "Needs attention" (suggestions) with `seo_status` and `seo_suggestions` (count) added to list items.
|
|
||||||
|
|
||||||
**Articles list.** Quiet indicator inside the Status cell (and under the title on mobile cards): nothing when ready, a lightbulb chip "N" for suggestions, an eye-off chip "Hidden" for noindex. Toolbar gets a "Needs attention" toggle next to search; tabs and counts are unchanged. It behaves the same on a single-site install (independent of the Site filter, which stays hidden).
|
|
||||||
|
|
||||||
**Authors.** No change to the Authors screen. Profile URL (website) and social links feed `authors[].url` and `same_as`, and JSON-LD `author[].sameAs`; the API preview shows them.
|
|
||||||
|
|
||||||
**Permissions.** Anyone who can edit an article edits its SEO fields. Only the owner edits site SEO defaults. 401 and 403 rows join the existing HTTP table: 403 `owner_only` on Site SEO fields, 401 on a missing session.
|
|
||||||
|
|
||||||
**Left out on purpose.** Keyword scoring and focus keywords, readability scores, redirects management, schema types beyond Article, hreflang, robots.txt and sitemap rendering (RenewCMS only provides data), automatic image generation for social cards.
|
|
||||||
|
|
||||||
**Open questions.**
|
|
||||||
1. Is `slug` immutable after first publish, or can it change (and break URLs with no redirects)?
|
|
||||||
2. Does the API return the SEO object for drafts to authenticated callers (for frontend previews)?
|
|
||||||
3. Should noindex articles be excluded from the sitemap endpoint instead of flagged?
|
|
||||||
4. Is title_template applied by the API (resolved title) or only returned for the frontend to apply?
|
|
||||||
5. Backfill: generate slugs for existing articles from their titles at migration, with `-2` suffixes on conflicts?
|
|
||||||
6. Should excerpt be exposed in the list payload as the article teaser?
|
|
||||||
|
|
||||||
**Status.** Frames drawn on the Back-office board (groups: Articles: search status, Editor: search and sharing, Editor: what the frontend gets, Sites: SEO defaults), Integration page group added, specs updated. Placement rationale: the sidebar holds only the summary, address and suggestions because it is 280 px wide; previews and fields need a slide-over.
|
|
||||||
|
|
||||||
|
|
||||||
Ordered by implementation dependency: tokens and shell first, then shared components, then screens. "Frontend only" means no backend work is needed.
|
|
||||||
|
|
||||||
| # | Screen | Change | Done in design? | Needs backend (yes/no, what) | Needs new tokens or components | Frontend only |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| 1 | All | Add the `--rc-*` tokens (sand, on-sand, four soft grounds, overlay, two shadows) to the app CSS, light and dark (foundations.md) | Yes | No | 9 new tokens | Yes |
|
|
||||||
| 2 | All | One sidebar, topbar and mobile drawer; Settings parent and child selected states; Activity sits between Users and Integration | Yes | No | NavItem, NavGroup | Yes |
|
|
||||||
| 3 | All | Overlay pattern: dialog (desktop) and bottom sheet (mobile); slide-over only for the variant form and record details; footer secondary left, primary right | Yes | No | Modal wrapper rules (components.md) | Yes |
|
|
||||||
| 4 | All | CodeSnippet is the only code block (Integration, Images, API keys, base URL, image URL) | Yes | No | CodeSnippet | Yes |
|
|
||||||
| 5 | All | Copy glossary pass (Delete vs Remove, Save vs Update, toast phrasing) | Yes, by search; not every line re-read | No | None | Yes |
|
|
||||||
| 6 | Settings | "Media" card with "Manage image variants" and "Cache usage" | Yes | No | MediaLinksCard | Yes |
|
|
||||||
| 7 | Media, picker | Variant count badge on grid, list and mobile rows | Yes | No (variant counts already come with GET /v1/media?variants=) | Badge "N variants" | Yes |
|
|
||||||
| 8 | Media | "With variants / Without" filter and "Largest total size" sort in the Media toolbar | Drawn on the Variants board only, not folded into the Media screen | No (GET /v1/media?variants=&sort=total_size exists) | None | Yes |
|
|
||||||
| 9 | Image variants | Variant form as slide-over (desktop) and tall sheet (mobile) | Yes | No | None | Yes |
|
|
||||||
| 10 | New article | Title only. Body removed from the modal; the editor opens straight away | Yes | Maybe: confirm `POST /v1/article/` accepts an empty `body` (it is "required" today); otherwise the frontend sends an empty string | NewArticleModal | Yes if empty body is accepted |
|
|
||||||
| 11 | Editor | Remove the 10,000 character limit, counter, limit and over-limit states; neutral count | Yes | Yes: remove the body length validation on `POST /v1/article/` and `PATCH /v1/article/{id}` | EditorStatusBar | No |
|
|
||||||
| 12 | Editor | Insert-link button (default, hover, active, disabled), tooltip, LinkModal, link popover and touch sheet, rendered link, toolbar scrolling at 390 px | Yes | No (editor only) | LinkModal, LinkPopover, ToolbarButton | Yes |
|
|
||||||
| 13 | Articles | Pagination with `page` and `size` (5, 25, 50, 100; default 25), URL state, loading, error, out of range | Yes | **Yes:** `page`, `size` on `GET /v1/article/` (same convention and 400 as Activity); total in `X-Total-Count`, array body unchanged; Integration reference updated | Paginator, PageSizeSelect | No |
|
|
||||||
| 13b | Media | Pagination under the grid and list, same Paginator and PageSizeSelect as Articles; `page` and `size` in the URL; saved size preference | Yes (9 states in the Media group) | **Yes:** `page`, `size` on `GET /v1/media/` (replaces `cursor` and `limit`); total in `X-Total-Count`, array body unchanged; Integration reference | None (reuses Paginator, PageSizeSelect) | No |
|
|
||||||
| 14 | Activity | Failed-action and security cards as toggles, synced chip and segment | Yes | No | FilterCardToggle | Yes |
|
|
||||||
| 15 | Activity | Export menu and dialog, CSV: current view (active filters) and all activity (no filters, optional `from`/`to`). Preparing, started, failed, nothing to export, 403 | Yes | No: `GET /v1/activity/export` is implemented (CSV, synchronous, owner and admin, same filters as the list) | ExportMenu, ExportDialog | Yes |
|
|
||||||
| 16 | Activity | JSON export (format control CSV or JSON) | Yes, one state, labelled proposed | **Yes (proposed):** `format=json` on `GET /v1/activity/export` | Segmented format control | No |
|
|
||||||
| 17 | Integration | Images and variants section, URL builder, response bodies `unknown_variant` and `unsupported_parameter` | Yes | No | UrlBuilder | Yes |
|
|
||||||
| 18 | All | States matrix for controls (hover, focus-visible, active, disabled) | Yes (board "States matrix: controls") | No | None | Yes |
|
|
||||||
| 19 | All | Contrast fixes: paginator current page border, no muted text on sand | Yes | No | None | Yes |
|
|
||||||
| 20 | All | Spacing, type and icon sizes made consistent (foundations.md, "One value per role") | Yes in the frames listed there | No | None | Yes |
|
|
||||||
| 21 | Settings, sidebar | Sites screen under Settings → Sites ("Publishing" group); "Publishing" card on Settings. Owner only: hidden for other roles, 403 page on a direct link. Key immutable after creation | Yes | **Yes:** `site` table, GET/POST/PATCH/DELETE /v1/site/ (writes owner only; PATCH rejects a changed key), `reassign_to`, seed from PUBLIC_URL | SitesTable, SiteForm, SlugField (locked variant), ReassignDialog | No |
|
|
||||||
| 22 | Sidebar | Authors entry between Media and Users, all roles. Owner and admin manage; other roles read-only list and detail | Yes | **Yes:** `author` table, GET /v1/author/ (any role), POST/PATCH/DELETE (owner and admin; a user may PATCH their own linked author), POST /v1/author/me (any role) | AuthorsTable, AuthorForm, SocialLinksRepeater, MediaField | No |
|
|
||||||
| 23 | Editor | "Site and authors" section in every Editor frame (collapsed by default): Site select (any role that can edit the article), reorderable authors, inline create (owner and admin), hint | Yes | **Yes:** `site_id`, `author_ids[]` on PATCH /v1/article/{id} | MetaSection, AuthorList, AuthorSelect, SiteSelect, CreateAuthorHint | No |
|
|
||||||
| 24 | Articles | Authors column; Site column and filter when more than one site | Yes | **Yes:** `site` filter, `site` and `authors` in the list payload | AuthorStack | No |
|
|
||||||
| 25 | Profile | "Author profile" card in every Profile frame, one-click create, edit own author (user link locked) | Yes | **Yes:** POST /v1/author/me | AuthorProfileCard | No |
|
|
||||||
| 26 | Backfill | Existing articles get the default site; authors start empty | n/a | **Yes:** migration | None | No |
|
|
||||||
|
|
||||||
## Roles and permissions: screens that change
|
|
||||||
|
|
||||||
| Screen | Change |
|
|
||||||
|---|---|
|
|
||||||
| Sidebar and mobile drawer (AppFrame) | Items are listed per permission, not per fixed role. Writer sees Home, Articles, Media, Authors, Activity and Content. A Roles entry appears under Settings for people who can view roles. |
|
|
||||||
| Settings navigation (AppFrame, done) | New "Access" group with a "Roles" item (shield icon) under Settings in the sidebar and the mobile drawer, shown to everyone except the restricted `user` role. "Permissions" is removed. |
|
|
||||||
| /settings/permissions | Retired. 301 to /settings/roles. The grid lives in the role editor. |
|
|
||||||
| Users (list, invite, row menu) | Role badges, Change role dialog, role picker in invite (default Writer), pending invites show the role, Transfer ownership, protected owner row. |
|
|
||||||
| Articles list and Editor | Publish hidden for roles without it; status shows Draft with "Ask an editor to publish." Delete and Edit disabled with a reason when scope is Own and the item is someone else's. |
|
|
||||||
| Content entry lists and editor | Same Own rules. View only mark. |
|
|
||||||
| Settings pages | Read-only mode with banner for people who can view but not edit. Owner-only actions disabled with an Owner only tag. |
|
|
||||||
| Schema | Hidden for roles without Schema view. Read-only for view-only roles. |
|
|
||||||
| API keys | Info alert "Keys follow their creator's role", a Follows role column. |
|
|
||||||
| Profile | Your role card and See what I can do. |
|
|
||||||
| Activity and filters | New event types, role diff in the detail panel. |
|
|
||||||
| Error and 403 copy | Two codes and the page-level state added. |
|
|
||||||
|
|
||||||
Decisions to confirm:
|
|
||||||
| # | Decision made | Alternative |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | Content permissions come before General permissions in the editor, because the common case (Contributor) only touches the grid. | General first, as in the brief. |
|
|
||||||
| 2 | Writer gets Authors: view and edit own, and Activity: own. | No Authors or Activity access. |
|
|
||||||
| 3 | A new role starts with nothing granted; presets fill the grid. | Start from a copy of Writer. |
|
|
||||||
| 4 | Own-scope rows others created show "View only" with a lock icon, not a hidden row. | Hide other people's entries. |
|
|
||||||
| 5 | Reset to default exists for built-in roles edited by the owner. | No reset. |
|
|
||||||
| 6 | Activity own/all scope uses the same None / Own / All control. | Toggle plus a separate scope select. |
|
|
||||||
| 7 | Transfer ownership takes effect at once, no email to the new owner. | The new owner must accept. |
|
|
||||||
| 8 | A user with Roles: manage cannot edit their own role, even in a custom role. | Allow narrowing only. |
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
# Change log: roles sync (pass 6)
|
|
||||||
|
|
||||||
Diff this against the code. "Old" is the previous design copy, "New" is the copy now in the design. EN is the design language; FR is in the board table "Final copy: toasts and errors, EN and FR" (screens/roles.md, same title). Every state is drawn light and dark, 1440×900 and 390×844, on `RenewCMS Screens Roles.dc.html` (data `roles-spec.js`, frame `RolesFrame.dc.html`) unless stated. Roles board: 61 → 90 states, 11 → 15 groups.
|
|
||||||
|
|
||||||
## 3. Differences adopted
|
|
||||||
|
|
||||||
### 3.1 Neutral wording ("they")
|
|
||||||
| File · state | Old | New |
|
|
||||||
|---|---|---|
|
|
||||||
| roles-spec.js, screens/roles.md, RolesFrame · Users: Change role dialog and Change role: options blocked | Her API keys follow the new role. | Their API keys follow the new role. |
|
|
||||||
| same · Users: Transfer ownership: confirm | You become an Admin. You can't take ownership back unless he transfers it. | You become an Admin. You can't take ownership back unless they transfer it. |
|
|
||||||
Searched all .md, .js, .dc.html and email files for he, she, his, her, him, hers. No other occurrence.
|
|
||||||
|
|
||||||
### 3.2 Event names
|
|
||||||
| File | Old | New |
|
|
||||||
|---|---|---|
|
|
||||||
| routes.md, screens/roles.md (Needs backend), roles-spec.js | role.created, role.updated, role.deleted, user.role_changed, site.ownership_transferred | role.create, role.update, role.delete, role.reassign (new), user.role_change, site.ownership_transfer |
|
|
||||||
| states.md (new section "Roles sync"), screens/activity.md (new section) | event names and sentences were not written down | table of name, sentence, failed sentence |
|
|
||||||
Sentences themselves are unchanged ("Role Contributor created", and so on).
|
|
||||||
|
|
||||||
### 3.3 Invite endpoint
|
|
||||||
| File | Old | New |
|
|
||||||
|---|---|---|
|
|
||||||
| screens/roles.md group 5, roles-spec.js group 5 | POST /v1/invite | POST /v1/user/invite |
|
|
||||||
routes.md, components.md and the other screen files never cited `/v1/invite`. IntegrationPage already said `/v1/user/invite`.
|
|
||||||
|
|
||||||
## 1. Copy
|
|
||||||
All EN and FR strings: roles board, table "Final copy: toasts and errors, EN and FR", and group 12 (toasts) and 13 (inline errors).
|
|
||||||
| State | EN |
|
|
||||||
|---|---|
|
|
||||||
| Toast: role changed (group 12) | Role changed · Ines Duarte is now Content lead, and their API keys follow the new role. |
|
|
||||||
| Toast: role edited elsewhere, 409 | Role changed elsewhere · Jonas Weber edited Contributor, so nothing was saved. Reload and try again. · Reload |
|
|
||||||
| Toast: role deleted elsewhere | That role no longer exists · It was deleted somewhere else, so we removed it from the list. |
|
|
||||||
| Toast: member's role changed before a change | Role not changed · Ines Duarte's role changed in the meantime, so nothing was saved. Reload and try again. · Reload |
|
|
||||||
| Toast: target's role changed before a transfer | Ownership not transferred · Jonas Weber's role changed in the meantime, so nothing was transferred. Check it and try again. · Reload |
|
|
||||||
| Toast: member's role changed before a removal | User not removed · Ines Duarte's role changed in the meantime, so nothing was removed. Check it and try again. · Reload |
|
|
||||||
| Role editor: save failed (new) | Couldn't save the role · Your changes are still here. Try again in a moment. · Try again |
|
|
||||||
| Change role: failed (new) | Couldn't change the role · Nothing was changed. Try again in a moment. |
|
|
||||||
| Delete role: target deleted (new) | Writer no longer exists · It was deleted somewhere else. Choose another role to move the members to. |
|
|
||||||
| Delete role: role gained members (new) | Role still has members · Intern now has 1 member. Move them to another role first. |
|
|
||||||
| Delete role: move failed (new) | Couldn't move the members · Nothing was changed. Try again in a moment. |
|
|
||||||
| Invite: invalid email (new) | Enter a valid email address. |
|
|
||||||
| Invite: account exists, 409 (new) | This address already has an account. |
|
|
||||||
| Invite: role above yours (new) | Your role can't assign a role with more power than yours. |
|
|
||||||
| Invite: send failed (new) | Couldn't send the invite · Try again in a moment. |
|
|
||||||
| Activity, role.reassign | Moved 3 members from Intern to Writer |
|
|
||||||
| Activity, failed sentences | Failed to {verb} {object}; one per event in the table "Activity events: failed sentences, EN and FR" |
|
|
||||||
| Activity filter, category names | Articles · Media · Users and invites (old: Users & invites) · Roles (new) · API keys · Authentication · System |
|
|
||||||
Unchanged and still correct: Name required, Name already taken, Escalation blocked, Conflict banner, Permission lowered.
|
|
||||||
|
|
||||||
## 2. Profile role card (group 9)
|
|
||||||
Decision: the summary line is removed. "Assigned by Marta Alves" is removed with it, because `access.role` has no such field either. "See what I can do" stays; its summary is computed from the permission list in GET /v1/me.
|
|
||||||
| State | Old | New |
|
|
||||||
|---|---|---|
|
|
||||||
| Role card: custom role | Your role · Contributor · Assigned by Marta Alves · Creates content and edits only their own. · See what I can do · Your API keys follow this role… | Your role · [Contributor badge] [Custom tag] · See what I can do · Your API keys follow this role. If your role changes, they change too. |
|
|
||||||
| Role card: Owner (new) | none | Your role · [Owner badge] [Locked] [Built-in] · See what I can do · same key line |
|
|
||||||
Layout, top to bottom, inside the existing card: title; one row of badges (role badge with icon, then Locked if `locked`, then Built-in or Custom from `built_in`); link row with the list-checks icon; muted key line. Gap 12, as every card.
|
|
||||||
|
|
||||||
## 4. Behaviour
|
|
||||||
### Read = Own (group 14, new, 5 states; also group 2 Role editor and group 3 preview via the same editor)
|
|
||||||
| State | Content |
|
|
||||||
|---|---|
|
|
||||||
| Role editor: Read set to Own | New info alert under the grid: Read: Own · Articles show online articles plus the person's own drafts. Custom content types show only entries the person created. Status counts follow the same rule. The preview sentence for Read = Own is now type-specific: "Can read online articles and their own drafts" (Articles) and "Can read only entries they created" (custom types). Old preview text was "read only their own". |
|
|
||||||
| Articles list: Read = Own | Junior writer: 3 online articles by others and 2 own drafts. Tabs All 5, Online 3, Draft 2. Banner: Drafts by others are hidden. |
|
|
||||||
| Articles list: no drafts of their own | Draft tab, counts All 3, Online 3, Draft 0. You have no drafts · Drafts you create appear here. Your role can't see drafts by other people. |
|
|
||||||
| Entry list: Read = Own | Only own entries, "3 entries", banner Entries by others aren't shown. |
|
|
||||||
| Entry list: no entries of their own | You haven't created an event yet · Your role shows only entries you created, so other people's events aren't listed here. Use New entry to add one. |
|
|
||||||
Decisions table row 4 now says Update and Delete set to Own keep the View only rows, Read set to Own hides them.
|
|
||||||
|
|
||||||
### 409 conflicts (group 15, new, 5 states)
|
|
||||||
| State | Where | Edits kept | Recovery |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Stale save: banner (existing Conflict state, copy unchanged) | Banner above the form | Yes, Save off | Reload role, or Review differences |
|
|
||||||
| Stale save: review differences (new) | Dialog over the editor | Yes | Keep my edits (reload and re-apply edits on top, Save on) or Use the saved role (drop edits) |
|
|
||||||
| Stale save: edits kept after reload (new) | Info toast, form shows saved role plus edits | Yes | Review, then Save |
|
|
||||||
| Change role: member's role changed (new) | Alert inside the dialog; toast if closed | Selection kept; Current mark moves | Change role again |
|
|
||||||
| Transfer ownership: target's role changed (new) | Alert inside the dialog; toast if closed | Typed email kept; primary off | Reload member, then Transfer ownership |
|
|
||||||
|
|
||||||
### Integration page (IntegrationPage.dc.html, Back-office board group Integration)
|
|
||||||
| Item | Old | New |
|
|
||||||
|---|---|---|
|
|
||||||
| Authentication, API key card | …with that user's role (owner or admin). A key never has more rights than its creator. | …with that user's role as it is now. A key never has more rights than its creator. A route that needs a permission answers 403 "missing_permission" to a key whose creator's role lacks it. |
|
|
||||||
| Errors table, 403 | "This action needs a real session.", "Only the owner can do this.", "Only owners and admins can do this." or "You must verify your account to access this resource." | "This action needs a real session.", `missing_permission` (code style, literal body), or "You must verify your account to access this resource." Note: `missing_permission` is the literal body, not a sentence. Site SEO defaults need the permission to manage sites. Article SEO fields need only the right to edit the article. |
|
|
||||||
| Access chip on 9 routes (content types create, update, delete; built-in types list, get; media purge settings get, patch, preview, purge) | Owner or admin | Needs permission. The route description ends with Needs the permission "…". Proposed permissions: Create and edit content types; Delete content types; View content types; Delete files (purge routes). |
|
|
||||||
| Per-route 403 line on those routes | Only owners and admins can do this. | missing_permission (code style) |
|
|
||||||
| Content types section intro | An owner or admin creates a type… | A user whose role can create and edit content types defines a type… |
|
|
||||||
"Owner only" chips are kept for the owner-only routes.
|
|
||||||
|
|
||||||
### Relation picker (SchemaFrame, Schema board group 7, Later)
|
|
||||||
Sample data: 25 venues per page (the design had 3 sample results, never 10), footer "Showing 25 of 62" with Load more. New state "Relation picker: loading more": skeleton rows, Loading… on a disabled button. Group 7: 3 → 4 states.
|
|
||||||
|
|
||||||
## Files touched
|
|
||||||
roles-spec.js, RolesFrame.dc.html, screens/roles.md (regenerated from the spec), routes.md, states.md, screens/activity.md, screens/integration.md, screens/schema-later.md, schema-spec.js, SchemaFrame.dc.html, IntegrationPage.dc.html, README.md.
|
|
||||||
|
|
||||||
## Open items for you
|
|
||||||
See the end of this file's companion list in the chat reply; repeated here.
|
|
||||||
1. Relation picker: the design never had a "Load more" frame or a 10-result sample. Both were added, not updated.
|
|
||||||
2. Activity event names: states.md and screens/activity.md did not contain event names; they only existed in roles.md, roles-spec.js and routes.md. I added the table to the first two.
|
|
||||||
3. `user.role_changed` and `site.ownership_transferred` became `user.role_change` and `site.ownership_transfer`. Your brief said "the same present-tense pattern for the other role events". Confirm those two are included.
|
|
||||||
4. `/v1/invite` appeared only in screens/roles.md and roles-spec.js. routes.md, components.md and the other screens already had `/v1/user/invite` or no citation.
|
|
||||||
5. The 403 row in the design did not list "missing_permission". It listed "Only the owner can do this." and "Only owners and admins can do this.", which I removed. Check that owner-only routes now also answer `missing_permission`.
|
|
||||||
6. Status naming: you say All / Online / Draft; the design already used Online and Draft, so no change.
|
|
||||||
7. "Role deleted by someone else (409)": a deleted role normally answers 404. I drew 404 for the roles list and an inline 409 only where the deleted role was a reassign target. Confirm the codes.
|
|
||||||
8. "Changing a role that changed meanwhile": I read it as the member's role having changed, not the role definition. Confirm.
|
|
||||||
9. Profile: `access.role` has no assigned_by, so "Assigned by" was removed along with the summary. Confirm that the permission summary can be built from /v1/me.
|
|
||||||
10. Activity categories: the filter had six categories. I added Roles and renamed "Users & invites" to "Users and invites" to match the permission group. Authors, Sites and Content types have activity in the product but no category in the filter; I did not add them.
|
|
||||||
11. Purge routes: the catalogue has no permission for media purge. I used Delete files. Pick the real one.
|
|
||||||
12. Not changed, but now out of date: IntegrationPage Entries intro still says "Any verified user can write entries"; screens/schema.md and the Schema frames say "Owner or admin" for Schema; states.md "Sites and authors: roles" and the 403 owner_only rows still describe fixed roles; screens/profile.md Display name copy. Say if you want these aligned.
|
|
||||||
13. Decision 4 in "Decisions to confirm" was narrowed (Read = Own hides rows). Direct link to a hidden item answers the existing 404 page; this is proposed.
|
|
||||||
14. French role names: built-in names (Owner, Admin, Writer) stay English in FR copy until you decide.
|
|
||||||
15. Toast bodies use two short sentences in places; the glossary says one. Kept for clarity.
|
|
||||||
|
|
||||||
## Corrections made while importing into the repo
|
|
||||||
These were checked against the backend and applied to the repo copy of the design (the Claude Design copy still has the old values).
|
|
||||||
1. Event names: the backend emits `role.edit` (not `role.update`), `user.change_role` (not `user.role_change`) and `user.transfer_ownership` (not `site.ownership_transfer`). The other three, `role.create`, `role.delete` and `role.reassign`, match.
|
|
||||||
2. Media purge permissions: `GET /v1/media/purge-settings` needs "View settings", `PATCH /v1/media/purge-settings` needs "Edit general settings", and `GET /v1/media/purge-preview` and `POST /v1/media/purge` need "Delete files".
|
|
||||||
3. Open item 7 (deleted role): the backend answers 404 for a deleted role and 409 for a stale role save, as drawn.
|
|
||||||
4. Open item 8: "changing a role that changed meanwhile" means the member's role, as read.
|
|
||||||
5. Event names now match the backend: role.create, role.edit, role.delete, role.reassign, user.change_role, user.transfer_ownership, user.remove. The living specs (screens/roles.md, roles-spec.js, routes.md, states.md, screens/activity.md) use these names; the history rows above keep the names as they were at the time.
|
|
||||||
@@ -124,7 +124,7 @@ Frames: `SchemaFrame.dc.html` (all schema, content and entry bodies; relation, p
|
|||||||
|
|
||||||
## Media field (pass 5)
|
## Media field (pass 5)
|
||||||
|
|
||||||
Frames: `SchemaFrame.dc.html` (`MCtl`) and `MediaFrame.dc.html` (picker in field mode, usage). Full spec: `media-field-changes.md`.
|
Frames: `SchemaFrame.dc.html` (`MCtl`) and `MediaFrame.dc.html` (picker in field mode, usage). Full spec: `media-field.md`.
|
||||||
|
|
||||||
| Component | Status | Nuxt UI | Notes |
|
| Component | Status | Nuxt UI | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Design decisions
|
||||||
|
|
||||||
|
Product and interface decisions that outlive the design passes. Behaviour comes from `features/`; where the two disagree, stop and ask. Screen copy and states are in `screens/*.md`, `states.md` and `routes.md`.
|
||||||
|
|
||||||
|
## Search and sharing (SEO)
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
**Placement.** A "Search and sharing" section sits under "Site and authors" in the Editor sidebar and in the mobile Details sheet. It is collapsed by default, shows a one-line summary ("Ready", "2 suggestions" or "Hidden from search") and is remembered per browser like Site and authors. It holds only the slug, the full URL and three actions: "Edit search and sharing", "What the frontend gets" and the suggestion list. The fields and live previews open in a slide-over (desktop) or a 92% bottom sheet stacked over the Details sheet (mobile), because the 280 px sidebar is too narrow for previews and the slide-over rule already covers forms with a live preview. The API preview is a 720 px dialog (desktop) or a bottom sheet (mobile).
|
||||||
|
|
||||||
|
**Principles.** RenewCMS does not render pages or inject meta tags. Every field is optional and the UI shows its fallback. No scores. Suggestions never block saving or publishing; publishing is allowed regardless. Suggestions are dismissible per article.
|
||||||
|
|
||||||
|
**Fallback chain (resolved server-side, shown in the UI).**
|
||||||
|
| Output | Order |
|
||||||
|
|---|---|
|
||||||
|
| title | seo_title, then article title; site title_template applied last |
|
||||||
|
| description | seo_description, then excerpt, then first sentence of the body, then site default_description |
|
||||||
|
| image | og_image_id, then cover_image_id, then site social image |
|
||||||
|
| canonical | canonical_url, then site base_url + "/" + slug |
|
||||||
|
| robots | noindex true gives "noindex, nofollow"; otherwise site default_robots |
|
||||||
|
| og/twitter title and description | social_title, social_description, then the resolved values above |
|
||||||
|
|
||||||
|
**Soft ranges (counters, never blocks).** Meta title 30–60 characters; description 70–160; slug "very long" above 75 characters. Counter turns neutral-warning (`--ui-warning`) outside the range, never red.
|
||||||
|
|
||||||
|
**New article fields (PATCH /v1/article/{id}, all optional, snake_case).** `slug`, `excerpt` (max 300), `seo_title` (max 120), `seo_description` (max 320), `cover_image_id`, `cover_image_alt` (max 200; the cover is "ready" only with alt text), `social_title`, `social_description`, `social_image_id`, `canonical_url` (absolute https URL), `noindex` (boolean; only sets a flag in the API, it doesn't change visibility), `seo_dismissed` (string[] of suggestion codes). Slug: lowercase letters, numbers, hyphens; unique per site; 409 `slug_taken` with `suggestion` ("<slug>-2"); auto-generated from the title until edited.
|
||||||
|
|
||||||
|
**New site fields (PATCH /v1/site/{id}, owner only).** `site_name`, `title_template` (default "%title% | %site%", must contain %title%), `default_description` (max 320), `social_image_id` (existing), `default_robots` ("index" | "noindex"; existing as robots).
|
||||||
|
|
||||||
|
**New endpoints.**
|
||||||
|
- `GET /v1/article/{id}/seo`: resolved object, public for online articles, 404 for drafts to anonymous callers. Shape: `{ title, description, canonical_url, robots, og: { type, title, description, image, image_alt, url, site_name, locale }, twitter: { card, title, description, image, image_alt }, image: { url, alt, source }, authors: [{ name, url, same_as: [] }], published_at, modified_at, fallbacks: { title, description, image, canonical }, json_ld }`. `json_ld` is a ready Article object (headline, description, image, author[] with name, url and sameAs from author profile URL and social links, datePublished, dateModified, mainEntityOfPage).
|
||||||
|
- `GET /v1/site/{key}/sitemap?page=&size=`: published URLs only, total in `X-Total-Count`. Items: `{ url, modified_at, noindex }`. Decision: yes, shown on Integration under "Search and sharing" with a copy button; noindex articles are included with the flag so the frontend can skip them.
|
||||||
|
- `GET /v1/article/?seo=ready|suggestions|hidden`: filter behind "Needs attention" (suggestions) with `seo_status` and `seo_suggestions` (count) added to list items.
|
||||||
|
|
||||||
|
**Articles list.** Quiet indicator inside the Status cell (and under the title on mobile cards): nothing when ready, a lightbulb chip "N" for suggestions, an eye-off chip "Hidden" for noindex. Toolbar gets a "Needs attention" toggle next to search; tabs and counts are unchanged. It behaves the same on a single-site install (independent of the Site filter, which stays hidden).
|
||||||
|
|
||||||
|
**Authors.** No change to the Authors screen. Profile URL (website) and social links feed `authors[].url` and `same_as`, and JSON-LD `author[].sameAs`; the API preview shows them.
|
||||||
|
|
||||||
|
**Permissions.** Anyone who can edit an article edits its SEO fields. Only the owner edits site SEO defaults. 401 and 403 rows join the existing HTTP table: 403 `owner_only` on Site SEO fields, 401 on a missing session.
|
||||||
|
|
||||||
|
**Left out on purpose.** Keyword scoring and focus keywords, readability scores, redirects management, schema types beyond Article, hreflang, robots.txt and sitemap rendering (RenewCMS only provides data), automatic image generation for social cards.
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
|
||||||
|
| # | Question | Decision |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | Is `slug` immutable after publication? | No. It stays editable. RenewCMS does not manage redirects, so the frontend owns them. No extra warning copy is added to the design. |
|
||||||
|
| 2 | SEO object for drafts | `GET /v1/article/{id}/seo` answers drafts to authenticated callers who can read the article, and 404 to anonymous callers. The editor preview needs it. |
|
||||||
|
| 3 | Noindex in the sitemap | Included with `noindex: true`, as drawn. The frontend decides. |
|
||||||
|
| 4 | Who applies `title_template` | The API. `title` in the SEO object is resolved. The raw `seo_title` and the site template stay readable on their own routes. |
|
||||||
|
| 5 | Backfill | Migration generates a slug for every existing article from its title (same slug rule as authors), unique per site, with `-2`, `-3` suffixes. A title with no letter or digit becomes `article`, then `article-2`, `article-3`. It runs once and is idempotent. |
|
||||||
|
| 6 | Excerpt in the list payload | Yes. Article items carry `excerpt`. |
|
||||||
|
|
||||||
|
### Implicit decisions
|
||||||
|
|
||||||
|
- Suggestions come from one server-side rule set. The same function feeds `seo_status` and `seo_suggestions` in the article list, the collapsed section summary, and the editor panel.
|
||||||
|
- The editor panel's "checking" states and the "slug taken" state use one non-persisting endpoint: `POST /v1/article/{id}/seo/check`. Its body takes the same optional SEO fields as `PATCH /v1/article/{id}`, plus an optional `body` (never `site_id` or `author_ids`), and overrides the saved values for the check only. It answers `{ status, suggestions: [{ code, field, params }], slug: { available, suggestion } }`. Copy for each code lives in the SPA, with a French entry. `seo_dismissed` codes are filtered out of `suggestions` and are never counted in the list.
|
||||||
|
- The suggestion codes are the ones the board uses: `title_long`, `title_empty`, `desc_short`, `desc_long`, `cover_alt`, `cover_none`, `slug_long`, `no_au`, `no_h`, `body_alt`, `short`.
|
||||||
|
- Cover, social and site social images are media usages: `GET /media/{id}/usage`, `used_in`, the unused filter, merge, purge and delete protection must count them. This also closes the existing gap where author avatars and site social images are missing from `GET /media/{id}/usage`.
|
||||||
|
- Roles: anyone who can edit an article edits its SEO fields and runs the check. Only the owner edits site SEO fields (403 "Only the owner can do this."). `GET /v1/site/{key}/sitemap` is public, like the other read routes that serve online articles. SEO edits are logged as an `articles` event; no field values or body text are stored.
|
||||||
|
- `PATCH /v1/article/{id}` still requires at least one known field and still answers 400 on `null`. The SEO fields join `body`, `site_id` and `author_ids`.
|
||||||
|
|
||||||
|
## Sites and authors
|
||||||
|
|
||||||
|
### Who can do what
|
||||||
|
|
||||||
|
| Action | Owner | Admin | Other roles |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Create, edit, set default, delete a site | yes | no | no |
|
||||||
|
| Read the site list, choose an article's site | yes | yes | yes, if they can edit the article |
|
||||||
|
| Create, edit, delete any author | yes | yes | no |
|
||||||
|
| Read authors, pick authors on an article | yes | yes | yes |
|
||||||
|
| Create and edit their own linked author (`POST /v1/author/me`) | yes | yes | yes |
|
||||||
|
| Create an author from the editor ("Create author…", "This is me") | yes | yes | no |
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- One site per article (`site_id` on the article). No per-site publish state: `is_online` stays the global switch. API keys stay global.
|
||||||
|
- The site key is immutable after creation. Exactly one default site exists and it cannot be deleted. Deleting a site with articles requires `reassign_to`.
|
||||||
|
- Authors are separate from users. An optional, unique link to a user. The slug is globally unique and not tied to a site. Zero authors on an article is allowed and never blocks publishing. Deleting an author with articles requires `reassign_to` (an author id or `none`).
|
||||||
|
- Media used as an author avatar or a site's default social image counts as media usage.
|
||||||
|
- The default site is seeded once from `PUBLIC_URL`. That variable is the CMS's own address (email links), so the seed is only a starting value that the owner edits. The base URL must be https, except for loopback hosts.
|
||||||
|
- Existing articles are backfilled to the default site and get no authors.
|
||||||
|
|
||||||
|
## Roles and permissions
|
||||||
|
|
||||||
|
Event names follow the backend: `role.create`, `role.edit`, `role.delete`, `role.reassign`, `user.change_role`, `user.transfer_ownership`, `user.remove`.
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
|
||||||
|
| # | Decision made | Alternative |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Content permissions come before General permissions in the editor, because the common case (Contributor) only touches the grid. | General first, as in the brief. |
|
||||||
|
| 2 | Writer gets Authors: view and edit own, and Activity: own. | No Authors or Activity access. |
|
||||||
|
| 3 | A new role starts with nothing granted; presets fill the grid. | Start from a copy of Writer. |
|
||||||
|
| 4 | Own-scope rows others created show "View only" with a lock icon, not a hidden row. | Hide other people's entries. |
|
||||||
|
| 5 | Reset to default exists for built-in roles edited by the owner. | No reset. |
|
||||||
|
| 6 | Activity own/all scope uses the same None / Own / All control. | Toggle plus a separate scope select. |
|
||||||
|
| 7 | Transfer ownership takes effect at once, no email to the new owner. | The new owner must accept. |
|
||||||
|
| 8 | A user with Roles: manage cannot edit their own role, even in a custom role. | Allow narrowing only. |
|
||||||
|
|
||||||
|
## Account self-service
|
||||||
|
|
||||||
|
Authored content is anonymized to "Deleted user". The email-change link lasts 2 hours. One export succeeds per user every 15 minutes, and a direct download is the export flow. "Last administrator" counts Owner. A change request for an address already in use answers like success. The account-deleted email goes to the address the account had.
|
||||||
|
|
||||||
|
## Interface defaults
|
||||||
|
- Slide-over rule: only forms with a live preview and record details; on mobile always a 92% bottom sheet.
|
||||||
|
- Hover, active, disabled and focus tokens for the matrix (components not specified before): primary hover `--ui-text`, active `--ui-text-toned`; destructive hover opacity 0.85 and active 0.75; disabled 40% (rows 55%).
|
||||||
|
- Article pagination: Shipped shapes: the article list body is {items, total, page, size, counts}; the media list body is an array with the total in `X-Total-Count`. Users and authors follow their own routes (see routes.md).
|
||||||
|
- The Remove action in the link popover has no confirmation; undo restores the link.
|
||||||
|
- The link button is disabled when an image is selected.
|
||||||
|
- The tooltip text is "Insert link (Ctrl+K)" and "Insert link (⌘K)" on macOS.
|
||||||
|
- Mobile toolbar scrolls sideways; no wrap, no overflow menu.
|
||||||
|
- Selected nav items keep the sand fill plus weight 500 and `aria-current` (the fill alone is 1.3:1 in light).
|
||||||
|
- Syntax colouring dropped from code samples (one rendering).
|
||||||
|
- Export "Download started" offers only Close.
|
||||||
|
- New article keeps the title counter "n / 200" (title is limited to 200); the body has no limit and no counter.
|
||||||
|
- Page title 20/28, page padding 24/16, card padding 16 (foundations.md).
|
||||||
|
- Activity position in the sidebar follows AppFrame.
|
||||||
|
- The Prototype shows a toast "Not in this prototype" for sidebar items it has no screen for (Media, Activity, Settings).
|
||||||
@@ -23,6 +23,5 @@ Go `html/template` files built from `source/emails/verification.html` (same tabl
|
|||||||
| `source/emails/verify-new-email.html` | the new address | Confirm your new RenewCMS email address | `.Host`, `.Username`, `.VerificationCode`, linking to `{{.Host}}/account/email/confirm?c={{.VerificationCode}}` (expires in 2 hours, works once) |
|
| `source/emails/verify-new-email.html` | the new address | Confirm your new RenewCMS email address | `.Host`, `.Username`, `.VerificationCode`, linking to `{{.Host}}/account/email/confirm?c={{.VerificationCode}}` (expires in 2 hours, works once) |
|
||||||
| `source/emails/email-change-notice.html` | the old address, at request time | Your RenewCMS email address change was requested | `.Host`, `.Username`, `.NewEmail`, linking to `{{.Host}}/profile` |
|
| `source/emails/email-change-notice.html` | the old address, at request time | Your RenewCMS email address change was requested | `.Host`, `.Username`, `.NewEmail`, linking to `{{.Host}}/profile` |
|
||||||
| `source/emails/account-deleted.html` | the address the account had, sent once | Your RenewCMS account has been deleted | `.Host`, `.Username`, `.DeletedAt`; no button, generic text, no personal data |
|
| `source/emails/account-deleted.html` | the address the account had, sent once | Your RenewCMS account has been deleted | `.Host`, `.Username`, `.DeletedAt`; no button, generic text, no personal data |
|
||||||
| `source/emails/export-ready.html` | the account address, async flow only | Your RenewCMS data export is ready | `.Host`, `.Username`, `.VerificationCode`, linking to `{{.Host}}/account/export/{{.VerificationCode}}` (expires in 24 hours, requires a logged-in session) |
|
|
||||||
|
|
||||||
`export-ready.html` is only needed if the async export alternative is built. Previews are rows on `source/RenewCMS Emails.dc.html`.
|
Previews are rows on `source/RenewCMS Emails.dc.html`.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Media field type: change list for implementation
|
# Media field type
|
||||||
|
|
||||||
Board: `RenewCMS Screens Schema.dc.html`. Frames changed: `SchemaFrame.dc.html`, `MediaFrame.dc.html`. Spec data: `schema-spec.js` (states) and `media-field-tables.js` (tables, appended to the board). No new tokens, no new colors, radius 0.
|
Board: `RenewCMS Screens Schema.dc.html`. Frames changed: `SchemaFrame.dc.html`, `MediaFrame.dc.html`. Spec data: `schema-spec.js` (states) and `media-field-tables.js` (tables, appended to the board). No new tokens, no new colors, radius 0.
|
||||||
|
|
||||||
@@ -101,21 +101,3 @@ HTTP to copy (picker and field), from states.md: 400 "That request wasn't valid.
|
|||||||
- Focus: Use this image → Change image. Remove or Clear → Pick an image. Cancel or Esc → the opener.
|
- Focus: Use this image → Change image. Remove or Clear → Pick an image. Cancel or Esc → the opener.
|
||||||
- Picker: tiles are buttons with `aria-pressed`, names "stone-steps.jpg, 310 KB, Current". "No image" has `aria-pressed`. "Use this image" uses `aria-disabled`.
|
- Picker: tiles are buttons with `aria-pressed`, names "stone-steps.jpg, 310 KB, Current". "No image" has `aria-pressed`. "Use this image" uses `aria-disabled`.
|
||||||
- Contrast: Current badge and selected rows use `--rc-on-sand`; hover bar 18.9 (inverted pair). 44 px targets on mobile. No hover on mobile.
|
- Contrast: Current badge and selected rows use `--rc-on-sand`; hover bar 18.9 (inverted pair). 44 px targets on mobile. No hover on mobile.
|
||||||
|
|
||||||
## 7. Conflicts to decide
|
|
||||||
1. Row height: design is 48 px (56 px media rows, 68 px cards), not 32 px. Drawn at 48 and 68.
|
|
||||||
2. Thumbnail position: existing form has Description last. Drawn after Description (end of form).
|
|
||||||
3. "Used in N" counted articles. Now articles + entries.
|
|
||||||
4. Delete title: existing in-use title kept; "Delete this image?" stays for unused images.
|
|
||||||
5. "Clear" vs glossary "Remove".
|
|
||||||
6. "1600 × 900 · 240 KB" (design separator) instead of "1600 x 900 - 240 KB".
|
|
||||||
7. Review step disables Apply for the certain refusal; existing review never does.
|
|
||||||
8. Picker button "Use this image" vs article picker "Insert image"; "Remove image" when No image is chosen is my addition.
|
|
||||||
9. "Pick an image" also on optional empty fields.
|
|
||||||
10. No read-only route for custom types.
|
|
||||||
11. Two tab stops for the same picker (thumbnail and Change image).
|
|
||||||
12. Disabled opacity: form 60%, foundations 40%.
|
|
||||||
13. The picker frame shows a stand-in entry form, not the real editor.
|
|
||||||
|
|
||||||
## 8. Not done
|
|
||||||
README.md, states.md and the screens/*.md files are not updated with the new counts (group 2 +3, 3 +2, 4 +1, 5 +3, 6 +10, 1 +1, new groups 10 (8) and 11 (4): 32 states). Light/dark and mobile frames were checked by reading the DOM, not state by state on screen.
|
|
||||||
@@ -51,7 +51,7 @@ Contents listed in the card: Profile (Username, display name, email, role and au
|
|||||||
| Requested | "We're preparing your export" · "This can take a few minutes. We'll email a link to marta@renew.example when it's ready." · "Export requested"; toast "Export requested" / "Check marta@renew.example in a few minutes." |
|
| Requested | "We're preparing your export" · "This can take a few minutes. We'll email a link to marta@renew.example when it's ready." · "Export requested"; toast "Export requested" / "Check marta@renew.example in a few minutes." |
|
||||||
| Already in progress | "An export is already being prepared" · "We'll email a link to marta@renew.example when it's ready." |
|
| Already in progress | "An export is already being prepared" · "We'll email a link to marta@renew.example when it's ready." |
|
||||||
|
|
||||||
Recommendation: direct download; async is the documented alternative and uses the export-ready email.
|
Decision: direct download. An emailed export link is out of scope.
|
||||||
|
|
||||||
## Delete account
|
## Delete account
|
||||||
Danger zone: "Danger zone" · "Delete your account and your personal data. This can't be undone." · "Delete my account".
|
Danger zone: "Danger zone" · "Delete your account and your personal data. This can't be undone." · "Delete my account".
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ The sidebar gets a "Content" group, one item per custom type (a default icon: ty
|
|||||||
| 9 | Type not found | 404 content_type_not_found on /content/unknown. | Content type not found · It may have been deleted, or the address may be mistyped. · Go to Home | Available now |
|
| 9 | Type not found | 404 content_type_not_found on /content/unknown. | Content type not found · It may have been deleted, or the address may be mistyped. · Go to Home | Available now |
|
||||||
|
|
||||||
## Media field (pass 5)
|
## Media field (pass 5)
|
||||||
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field-changes.md`, section 3.5. Board groups: 5 Content and entry list.
|
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field.md`, section 3.5. Board groups: 5 Content and entry list.
|
||||||
@@ -27,4 +27,4 @@ The form is generated from the schema, one control per field in schema order. Re
|
|||||||
| 13 | Delete entry: confirm | Delete. | Delete this entry? · "Lime plaster workshop" will be deleted from Events. This can't be undone. · Cancel · Delete entry | Available now |
|
| 13 | Delete entry: confirm | Delete. | Delete this entry? · "Lime plaster workshop" will be deleted from Events. This can't be undone. · Cancel · Delete entry | Available now |
|
||||||
|
|
||||||
## Media field (pass 5)
|
## Media field (pass 5)
|
||||||
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field-changes.md`, section 3.6. Board groups: 6 Entry editor.
|
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field.md`, section 3.6. Board groups: 6 Entry editor.
|
||||||
@@ -47,4 +47,4 @@ The Library tab of the picker gets a Variant select. Options are Original plus t
|
|||||||
| 5 | Variant disabled | Same block, different cause. Proposed copy. | "Articles" / "How we price a bathroom renovation" / "Edit article" / "Variant 'hero' is disabled." |
|
| 5 | Variant disabled | Same block, different cause. Proposed copy. | "Articles" / "How we price a bathroom renovation" / "Edit article" / "Variant 'hero' is disabled." |
|
||||||
|
|
||||||
## Media field (pass 5)
|
## Media field (pass 5)
|
||||||
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field-changes.md`, section 3.7. Board groups: 10 Media picker for a field.
|
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field.md`, section 3.7. Board groups: 10 Media picker for a field.
|
||||||
@@ -115,7 +115,7 @@ A secondary "Purge duplicates" button (icon merge) in the toolbar next to the so
|
|||||||
| 2 | Duplicates tab: offline | The button is disabled with the existing banner. | |
|
| 2 | Duplicates tab: offline | The button is disabled with the existing banner. | |
|
||||||
|
|
||||||
## Media field (pass 5)
|
## Media field (pass 5)
|
||||||
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field-changes.md`, section 3.8. Board groups: 11 Media library usage.
|
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field.md`, section 3.8. Board groups: 11 Media library usage.
|
||||||
|
|
||||||
## App sync (batch A)
|
## App sync (batch A)
|
||||||
- Detail panel, "Used elsewhere" below the articles and entries lists: rows "Author avatar" (/authors) and "Site default social image" (/settings/sites). States: avatar only, site only, both, none (section absent). The "Used in N" badge includes these, N = articles + entries + elsewhere. "Not used in any article" appears only when N is 0; otherwise the line reads "No article shows it."
|
- Detail panel, "Used elsewhere" below the articles and entries lists: rows "Author avatar" (/authors) and "Site default social image" (/settings/sites). States: avatar only, site only, both, none (section absent). The "Used in N" badge includes these, N = articles + entries + elsewhere. "Not used in any article" appears only when N is 0; otherwise the line reads "No article shows it."
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ One frame per type: inspector options, the entry form control (default and open
|
|||||||
| 6 | Choice | type: "select" | Choice · One option from a list you define · Choices · Choose an option · Choose one of: Talk, Workshop or Meetup. · Another entry already uses this choice for Category. | Available now |
|
| 6 | Choice | type: "select" | Choice · One option from a list you define · Choices · Choose an option · Choose one of: Talk, Workshop or Meetup. · Another entry already uses this choice for Category. | Available now |
|
||||||
|
|
||||||
## Media field (pass 5)
|
## Media field (pass 5)
|
||||||
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field-changes.md`, section 3.1 to 3.4. Board groups: 1 Schema list, 2 Schema builder, 3 Review, 4 Field types.
|
Spec, states, exact copy, measurements, backend needs and accessibility: `media-field.md`, section 3.1 to 3.4. Board groups: 1 Schema list, 2 Schema builder, 3 Review, 4 Field types.
|
||||||
|
|
||||||
## Permissions (app sync)
|
## Permissions (app sync)
|
||||||
Three keys gate Schema: schema.view (list and read types, and the Schema sidebar entry), schema.edit (create and change types and fields), schema.delete (delete a content type). The built-in Owner and Admin roles hold all three. The app still words the 403 as "Only owners and admins can…"; the design keeps that copy until the app changes. A role with schema.view only sees the builder read-only: Save, Add field, Move and Remove are hidden.
|
Three keys gate Schema: schema.view (list and read types, and the Schema sidebar entry), schema.edit (create and change types and fields), schema.delete (delete a content type). The built-in Owner and Admin roles hold all three. The app still words the 403 as "Only owners and admins can…"; the design keeps that copy until the app changes. A role with schema.view only sees the builder read-only: Save, Add field, Move and Remove are hidden.
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# SEO: decisions and slice order
|
|
||||||
|
|
||||||
Spec of record for the design is `change-list.md` (section "SEO round"), `screens/editor.md`, `screens/sites.md`, `screens/articles.md`, `screens/integration.md` and `states.md`. This file records the product decisions that settle the design's open questions, and the order of the implementation slices.
|
|
||||||
|
|
||||||
## Decisions on the open questions
|
|
||||||
|
|
||||||
| # | Question | Decision |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 1 | Is `slug` immutable after publication? | No. It stays editable. RenewCMS does not manage redirects, so the frontend owns them. No extra warning copy is added to the design. |
|
|
||||||
| 2 | SEO object for drafts | `GET /v1/article/{id}/seo` answers drafts to authenticated callers who can read the article, and 404 to anonymous callers. The editor preview needs it. |
|
|
||||||
| 3 | Noindex in the sitemap | Included with `noindex: true`, as drawn. The frontend decides. |
|
|
||||||
| 4 | Who applies `title_template` | The API. `title` in the SEO object is resolved. The raw `seo_title` and the site template stay readable on their own routes. |
|
|
||||||
| 5 | Backfill | Migration generates a slug for every existing article from its title (same slug rule as authors), unique per site, with `-2`, `-3` suffixes. A title with no letter or digit becomes `article`, then `article-2`, `article-3`. It runs once and is idempotent. |
|
|
||||||
| 6 | Excerpt in the list payload | Yes. Article items carry `excerpt`. |
|
|
||||||
|
|
||||||
## Decisions that the design leaves implicit
|
|
||||||
|
|
||||||
- Suggestions come from one server-side rule set. The same function feeds `seo_status` and `seo_suggestions` in the article list, the collapsed section summary, and the editor panel.
|
|
||||||
- The editor panel's "checking" states and the "slug taken" state use one non-persisting endpoint: `POST /v1/article/{id}/seo/check`. Its body takes the same optional SEO fields as `PATCH /v1/article/{id}`, plus an optional `body` (never `site_id` or `author_ids`), and overrides the saved values for the check only. It answers `{ status, suggestions: [{ code, field, params }], slug: { available, suggestion } }`. Copy for each code lives in the SPA, with a French entry. `seo_dismissed` codes are filtered out of `suggestions` and are never counted in the list.
|
|
||||||
- The suggestion codes are the ones the board uses: `title_long`, `title_empty`, `desc_short`, `desc_long`, `cover_alt`, `cover_none`, `slug_long`, `no_au`, `no_h`, `body_alt`, `short`.
|
|
||||||
- Cover, social and site social images are media usages: `GET /media/{id}/usage`, `used_in`, the unused filter, merge, purge and delete protection must count them. This also closes the existing gap where author avatars and site social images are missing from `GET /media/{id}/usage`.
|
|
||||||
- Roles: anyone who can edit an article edits its SEO fields and runs the check. Only the owner edits site SEO fields (403 "Only the owner can do this."). `GET /v1/site/{key}/sitemap` is public, like the other read routes that serve online articles. SEO edits are logged as an `articles` event; no field values or body text are stored.
|
|
||||||
- `PATCH /v1/article/{id}` still requires at least one known field and still answers 400 on `null`. The SEO fields join `body`, `site_id` and `author_ids`.
|
|
||||||
|
|
||||||
## Slices
|
|
||||||
|
|
||||||
1. **SEO data (backend).** Article and site fields, slug rule and uniqueness per site (409 plain-text `slug_taken`; the first free alternative is in the `X-Slug-Suggestion` response header), backfill, `excerpt` in reads and lists, media usage widening. No resolver yet.
|
|
||||||
2. **SEO output (backend).** `GET /v1/article/{id}/seo` (fallback chain, title template, JSON-LD), `POST /v1/article/{id}/seo/check`, `seo_status`, `seo_suggestions` and `?seo=` on the article list, `GET /v1/site/{key}/sitemap`.
|
|
||||||
3. **Editor (SPA).** "Search and sharing" section, slide-over and sheet, live previews, suggestions with debounced check, slug field with Auto and Reset, "What the frontend gets" dialog, draft and Save behaviour.
|
|
||||||
4. **Sites, Articles, Integration (SPA).** Site SEO defaults, status chip and "Needs attention" filter, Integration "Search and sharing" group, French strings, catalogue.
|
|
||||||
|
|
||||||
Run them in order and commit between them: 2 uses the fields of 1, 3 uses the endpoints of 2, and 3 and 4 share the article API types.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Sites and authors: cross-slice context
|
|
||||||
|
|
||||||
The design for Sites and Authors is in `screens/sites.md`, `screens/authors.md`, the "Editor: site and authors", "Profile: author profile" and "Articles: sites and authors" sections of the editor, profile and articles docs, and the board groups listed in `README.md` (pass 3). Behaviour still comes from `features/`; where the two disagree, stop and ask.
|
|
||||||
|
|
||||||
## Roles
|
|
||||||
| Action | Owner | Admin | Other roles |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Create, edit, set default, delete a site | yes | no | no |
|
|
||||||
| Read the site list, choose an article's site | yes | yes | yes, if they can edit the article |
|
|
||||||
| Create, edit, delete any author | yes | yes | no |
|
|
||||||
| Read authors, pick authors on an article | yes | yes | yes |
|
|
||||||
| Create and edit their own linked author (`POST /v1/author/me`) | yes | yes | yes |
|
|
||||||
| Create an author from the editor ("Create author…", "This is me") | yes | yes | no |
|
|
||||||
|
|
||||||
## Decisions already taken
|
|
||||||
- One site per article (`site_id` on the article). No per-site publish state: `is_online` stays the global switch. API keys stay global.
|
|
||||||
- The site key is immutable after creation. Exactly one default site exists and it cannot be deleted. Deleting a site with articles requires `reassign_to`.
|
|
||||||
- Authors are separate from users. An optional, unique link to a user. The slug is globally unique and not tied to a site. Zero authors on an article is allowed and never blocks publishing. Deleting an author with articles requires `reassign_to` (an author id or `none`).
|
|
||||||
- Media used as an author avatar or a site's default social image counts as media usage.
|
|
||||||
- The default site is seeded once from `PUBLIC_URL`. That variable is the CMS's own address (email links), so the seed is only a starting value that the owner edits. The base URL must be https, except for loopback hosts.
|
|
||||||
- Existing articles are backfilled to the default site and get no authors.
|
|
||||||
|
|
||||||
## Slices, in order
|
|
||||||
1. Sites API. 2. Authors API (independent of 1; both touch the article update route and the article response, so merge 1 first and rebase 2).
|
|
||||||
3. Sites and Authors screens plus the shared directory pieces (`DirectoryFrame.dc.html`). Existing Editor, Articles and Profile screens are not changed here.
|
|
||||||
4. Editor, Articles and Profile integration, which also makes every existing frame of those screens pass the visual check.
|
|
||||||
|
|
||||||
## Open point for the first slice
|
|
||||||
`PATCH /v1/article/{id}` is served by `UpdateArticleBody` and only updates the body. Extending it with `site_id` and `author_ids[]` or splitting a metadata endpoint is decided in the specifier step of slices 1 and 2.
|
|
||||||
@@ -38,7 +38,7 @@ class Component extends DCLogic {
|
|||||||
fetch('emails/verification.html').then(x => x.text()).then(v => this.setState({ v }))
|
fetch('emails/verification.html').then(x => x.text()).then(v => this.setState({ v }))
|
||||||
fetch('emails/password-reset.html').then(x => x.text()).then(r => this.setState({ r }))
|
fetch('emails/password-reset.html').then(x => x.text()).then(r => this.setState({ r }))
|
||||||
fetch('emails/invitation.html').then(x => x.text()).then(i => this.setState({ i }))
|
fetch('emails/invitation.html').then(x => x.text()).then(i => this.setState({ i }))
|
||||||
;['verify-new-email','email-change-notice','account-deleted','export-ready'].forEach(n => fetch('emails/' + n + '.html').then(x => x.text()).then(t => this.setState({ ['x_' + n]: t })))
|
;['verify-new-email','email-change-notice','account-deleted'].forEach(n => fetch('emails/' + n + '.html').then(x => x.text()).then(t => this.setState({ ['x_' + n]: t })))
|
||||||
}
|
}
|
||||||
fill(t) {
|
fill(t) {
|
||||||
return t.split('{{.InvitedBy}}').join('marta').split('{{.Email}} It').join('lea.martin@renew.example. It').split('sent to {{.Email}}').join('sent to lea.martin@renew.example').split('{{.Username}}').join('marta').split('{{.NewEmail}}').join('marta.alves@atelier-renew.example').split('{{.DeletedAt}}').join('29 Oct 2026').split('{{.Host}}').join('https://cms.renew.example').split('{{.VerificationCode}}').join('7f3c9a2e4b1d').split('{{.Email}}').join('marta%40renew.example').replace('uses marta%40renew.example', 'uses marta@renew.example')
|
return t.split('{{.InvitedBy}}').join('marta').split('{{.Email}} It').join('lea.martin@renew.example. It').split('sent to {{.Email}}').join('sent to lea.martin@renew.example').split('{{.Username}}').join('marta').split('{{.NewEmail}}').join('marta.alves@atelier-renew.example').split('{{.DeletedAt}}').join('29 Oct 2026').split('{{.Host}}').join('https://cms.renew.example').split('{{.VerificationCode}}').join('7f3c9a2e4b1d').split('{{.Email}}').join('marta%40renew.example').replace('uses marta%40renew.example', 'uses marta@renew.example')
|
||||||
@@ -57,7 +57,6 @@ class Component extends DCLogic {
|
|||||||
{ title: 'Verify new email', vars: '{{.Host}} · {{.Username}} · {{.VerificationCode}} → /account/email/confirm?c=', frames: this.frames(this.state['x_verify-new-email']) },
|
{ title: 'Verify new email', vars: '{{.Host}} · {{.Username}} · {{.VerificationCode}} → /account/email/confirm?c=', frames: this.frames(this.state['x_verify-new-email']) },
|
||||||
{ title: 'Email change notice (to the old address)', vars: '{{.Host}} · {{.Username}} · {{.NewEmail}}', frames: this.frames(this.state['x_email-change-notice']) },
|
{ title: 'Email change notice (to the old address)', vars: '{{.Host}} · {{.Username}} · {{.NewEmail}}', frames: this.frames(this.state['x_email-change-notice']) },
|
||||||
{ title: 'Account deleted', vars: '{{.Host}} · {{.Username}} · {{.DeletedAt}}', frames: this.frames(this.state['x_account-deleted']) },
|
{ title: 'Account deleted', vars: '{{.Host}} · {{.Username}} · {{.DeletedAt}}', frames: this.frames(this.state['x_account-deleted']) },
|
||||||
{ title: 'Export ready (alternative flow)', vars: '{{.Host}} · {{.Username}} · {{.VerificationCode}} → /account/export/', frames: this.frames(this.state['x_export-ready']) }
|
|
||||||
] }
|
] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="color-scheme" content="light dark">
|
|
||||||
<meta name="supported-color-schemes" content="light dark">
|
|
||||||
<title>Your data export is ready</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; padding: 0; width: 100%; -webkit-text-size-adjust: 100%; }
|
|
||||||
table { border-collapse: collapse; }
|
|
||||||
a { color: #111111; }
|
|
||||||
@media (max-width: 620px) {
|
|
||||||
.rc-card { padding: 28px 20px !important; }
|
|
||||||
.rc-btn a { display: block !important; }
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.rc-bg { background-color: #0a0a0a !important; }
|
|
||||||
.rc-card { background-color: #111111 !important; border-color: #333333 !important; }
|
|
||||||
.rc-h1, .rc-word { color: #fafafa !important; }
|
|
||||||
.rc-text { color: #e6e4e0 !important; }
|
|
||||||
.rc-muted { color: #999999 !important; }
|
|
||||||
.rc-mark { border: 1px solid #444444 !important; }
|
|
||||||
.rc-btn td { background-color: #fafafa !important; }
|
|
||||||
.rc-btn a { background-color: #fafafa !important; color: #111111 !important; border-color: #fafafa !important; }
|
|
||||||
.rc-url { color: #e6e4e0 !important; background-color: #1a1a1a !important; }
|
|
||||||
.rc-rule { border-color: #333333 !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="rc-bg" style="margin:0;padding:0;background-color:#fafafa;">
|
|
||||||
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">Your RenewCMS data export is ready to download. The link expires in 24 hours.</div>
|
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="rc-bg" style="background-color:#fafafa;">
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="padding:40px 12px;">
|
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding:0 0 20px 0;">
|
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
|
||||||
<tr>
|
|
||||||
<td class="rc-mark" width="32" height="32" align="center" valign="middle" bgcolor="#111111" style="width:32px;height:32px;background-color:#111111;color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:19px;font-weight:bold;line-height:32px;">R</td>
|
|
||||||
<td class="rc-word" style="padding-left:10px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:17px;font-weight:600;color:#111111;">RenewCMS</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="rc-card" style="background-color:#ffffff;border:1px solid #e6e4e0;padding:40px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;">
|
|
||||||
<h1 class="rc-h1" style="margin:0 0 16px 0;font-size:24px;line-height:32px;font-weight:600;color:#111111;">Your data export is ready</h1>
|
|
||||||
<p class="rc-text" style="margin:0 0 28px 0;font-size:16px;line-height:26px;color:#222222;">The export you requested for the RenewCMS account {{.Username}} at {{.Host}} is ready. It contains your profile, authored content, uploaded media references and account activity. You need to be logged in to download it.</p>
|
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="rc-btn">
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#111111" style="background-color:#111111;">
|
|
||||||
<a href="{{.Host}}/account/export/{{.VerificationCode}}" target="_blank" style="display:inline-block;padding:14px 24px;border:1px solid #111111;background-color:#111111;color:#ffffff;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:16px;font-weight:600;line-height:20px;text-decoration:none;text-align:center;">Download my data</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<p class="rc-muted" style="margin:16px 0 0 0;font-size:14px;line-height:22px;color:#666666;">This link expires in 24 hours and only works while you are logged in.</p>
|
|
||||||
<p class="rc-muted" style="margin:28px 0 8px 0;font-size:14px;line-height:22px;color:#666666;">If the button doesn't work, copy this address into your browser:</p>
|
|
||||||
<p class="rc-url" style="margin:0;padding:10px 12px;background-color:#f3f2f0;font-family:'IBM Plex Mono',Menlo,Consolas,monospace;font-size:13px;line-height:20px;color:#222222;word-break:break-all;">{{.Host}}/account/export/{{.VerificationCode}}</p>
|
|
||||||
<p class="rc-text rc-rule" style="margin:28px 0 0 0;padding-top:20px;border-top:1px solid #e6e4e0;font-size:14px;line-height:22px;color:#222222;">If you didn't request this export, log in and change your password.</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="rc-muted" style="padding:20px 0 0 0;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;color:#666666;">RenewCMS · {{.Host}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -108,7 +108,7 @@ Schema changes with entries: Save opens the review ("Add 2 fields, change 1, rem
|
|||||||
Entry rules, 422 except unique (409): required, type, min, max, options, unique, unknown_field. Messages are per type and drawn in the field-type gallery (group "Field types").
|
Entry rules, 422 except unique (409): required, type, min, max, options, unique, unknown_field. Messages are per type and drawn in the field-type gallery (group "Field types").
|
||||||
|
|
||||||
## Media field (pass 5)
|
## Media field (pass 5)
|
||||||
Copy and states are in `media-field-changes.md` (sections 2 to 6). Entry rules for a media field: required 422 "Pick an image for Thumbnail."; type 422 "Thumbnail must be an image from the library."; min, max, options and unique do not apply. A required media field added to a type with entries is refused client side in the review ("Can't be applied") and by the server as 409 schema_incompatible. Image deleted while entries use it: the control shows "This image is no longer in the library"; saving is refused only when the field is required. HTTP statuses for the picker and the field use the table above (400, 401, 403, 404, 409, 413, 429, 500, network).
|
Copy and states are in `media-field.md` (sections 2 to 6). Entry rules for a media field: required 422 "Pick an image for Thumbnail."; type 422 "Thumbnail must be an image from the library."; min, max, options and unique do not apply. A required media field added to a type with entries is refused client side in the review ("Can't be applied") and by the server as 409 schema_incompatible. Image deleted while entries use it: the control shows "This image is no longer in the library"; saving is refused only when the field is required. HTTP statuses for the picker and the field use the table above (400, 401, 403, 404, 409, 413, 429, 500, network).
|
||||||
|
|
||||||
## Roles and permissions
|
## Roles and permissions
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -21,7 +21,7 @@ Requirements: Docker with Compose v2. [task](https://taskfile.dev) is optional f
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
`docker-compose.yml` uses `image: ${RENEWCMS_IMAGE:-git.floriansylvain.fr/fsylvain/renewcms:beta}`. The default tag is `beta` because `latest` does not exist until the first stable release (the default changes to `latest` then). Set `RENEWCMS_IMAGE` in your shell or in a `.env` file next to the compose file to pin another tag, for example `RENEWCMS_IMAGE=git.floriansylvain.fr/fsylvain/renewcms:v3.2.1-beta`.
|
`docker-compose.yml` uses `image: ${RENEWCMS_IMAGE:-git.floriansylvain.fr/fsylvain/renewcms:beta}`. The default tag is `beta` because `latest` does not exist until the first stable release (the default changes to `latest` then). Set `RENEWCMS_IMAGE` in your shell or in a `.env` file next to the compose file to pin another tag, for example `RENEWCMS_IMAGE=git.floriansylvain.fr/fsylvain/renewcms:v3.7.0-beta`.
|
||||||
|
|
||||||
To build from the local sources instead, see [Building from source](#building-from-source).
|
To build from the local sources instead, see [Building from source](#building-from-source).
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
|
|||||||
| Release | Tags pushed |
|
| Release | Tags pushed |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `vX.Y.Z` (stable) | `vX.Y.Z`, `X.Y.Z`, `X.Y`, `latest` |
|
| `vX.Y.Z` (stable) | `vX.Y.Z`, `X.Y.Z`, `X.Y`, `latest` |
|
||||||
| `vX.Y.Z-<pre>` (for example `v3.2.1-beta`, `v3.2.1-rc.1`) | `vX.Y.Z-<pre>`, `X.Y.Z-<pre>`, and the moving tag `<pre-name>` (`beta`, `rc`) |
|
| `vX.Y.Z-<pre>` (for example `v3.7.0-beta`, `v3.7.0-rc.1`) | `vX.Y.Z-<pre>`, `X.Y.Z-<pre>`, and the moving tag `<pre-name>` (`beta`, `rc`) |
|
||||||
|
|
||||||
A pre-release never moves `latest`. Images are published for `linux/amd64` and `linux/arm64`. `renewcms --version` in the image prints the exact release tag. Images are not signed; the Ed25519 key signs the release binaries only.
|
A pre-release never moves `latest`. Images are published for `linux/amd64` and `linux/arm64`. `renewcms --version` in the image prints the exact release tag. Images are not signed; the Ed25519 key signs the release binaries only.
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ Publishing is a manual, maintainer-run task, like `task release`. There is no CI
|
|||||||
1. Tag the release and keep the work tree clean:
|
1. Tag the release and keep the work tree clean:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git tag v3.2.1-beta
|
git tag v3.7.0-beta
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Log in once to the registry. The task never reads or stores credentials:
|
2. Log in once to the registry. The task never reads or stores credentials:
|
||||||
@@ -226,7 +226,7 @@ Publishing is a manual, maintainer-run task, like `task release`. There is no CI
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
task docker:publish -- --dry-run
|
task docker:publish -- --dry-run
|
||||||
task docker:publish VERSION=v3.2.1-rc.1 -- --dry-run
|
task docker:publish VERSION=v3.7.0-rc.1 -- --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
The dry run builds for the host platform only, loads the image into the local Docker, pushes nothing and needs no login. It prints the tags a real run would push. A `VERSION` override is accepted only with `--dry-run`, and a dirty tree is allowed there. Remove the loaded images afterwards with `docker rmi`.
|
The dry run builds for the host platform only, loads the image into the local Docker, pushes nothing and needs no login. It prints the tags a real run would push. A `VERSION` override is accepted only with `--dry-run`, and a dirty tree is allowed there. Remove the loaded images afterwards with `docker rmi`.
|
||||||
|
|||||||
+2
-9
@@ -4,18 +4,11 @@
|
|||||||
|
|
||||||
`GET /v1/article/` assesses the SEO status of articles in memory, because the status depends on the body, the site template and the authors. Every request assesses every visible article once, to fill `counts.suggestions`, and `?seo=` reuses that assessment to filter before paging. The cost therefore grows with the number of articles, not with the page size.
|
`GET /v1/article/` assesses the SEO status of articles in memory, because the status depends on the body, the site template and the authors. Every request assesses every visible article once, to fill `counts.suggestions`, and `?seo=` reuses that assessment to filter before paging. The cost therefore grows with the number of articles, not with the page size.
|
||||||
|
|
||||||
Measured by `TestSeoFilterPerformance` in `test/seo_filter_performance_test.go` (skipped with `-short`): 5,000 articles of about 5 KB each in one site, on a SQLite file in a temporary directory, every article with suggestions, `?seo=suggestions&size=25`, slowest of 3 runs:
|
`TestSeoFilterPerformance` in `test/seo_filter_performance_test.go` (skipped with `-short`) measures 5,000 articles of about 5 KB each in one site on a temporary SQLite file, every article with suggestions, slowest of 3 runs:
|
||||||
|
|
||||||
| Version | Time |
|
|
||||||
| --- | --- |
|
|
||||||
| `counts.suggestions` added, assessment done twice | about 430 ms |
|
|
||||||
| assessment shared between the counts and the filter | about 270 ms |
|
|
||||||
|
|
||||||
The target is 300 ms for 5,000 articles. The test also measures the default list (`GET /v1/article/` with no `?seo=`), which pays the same assessment for `counts.suggestions`. Latest run on the development machine, slowest of 3 runs:
|
|
||||||
|
|
||||||
| Request | Time |
|
| Request | Time |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `?seo=suggestions&size=25` | 315 to 338 ms |
|
| `?seo=suggestions&size=25` | 315 to 338 ms |
|
||||||
| default page, no `?seo=` | 307 to 329 ms |
|
| default page, no `?seo=` | 307 to 329 ms |
|
||||||
|
|
||||||
Both are slightly above the 300 ms target on this machine (they were about 270 ms in the earlier measurement), so the target is logged by the test but not enforced; no cache or index was added. Re-run the test with `go test -tags nofrontend ./test/... -run TestSeoFilterPerformance -v -count=1` after a change to the assessment rules or the article repository. The time depends on the machine; the test only fails above 500 ms.
|
The target is 300 ms for 5,000 articles. Both requests sit slightly above it on the development machine, so the test logs the target but only fails above 500 ms; no cache or index was added. Re-run it with `go test -tags nofrontend ./test/... -run TestSeoFilterPerformance -v -count=1` after a change to the assessment rules or the article repository.
|
||||||
@@ -6,7 +6,7 @@ Feature: Authors backend, public bylines that articles credit in order
|
|||||||
So that readers see who wrote an article, whether or not the writer has an account
|
So that readers see who wrote an article, whether or not the writer has an account
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/screens/authors.md, docs/design/screens/editor.md (site and authors), docs/design/screens/profile.md, docs/design/states.md (author rows), docs/design/routes.md and docs/design/sites-authors-slices.md
|
Given the spec of record is docs/design/screens/authors.md, docs/design/screens/editor.md (site and authors), docs/design/screens/profile.md, docs/design/states.md (author rows), docs/design/routes.md and docs/design/decisions.md
|
||||||
And this slice is backend only: no SPA, no change to docs/design
|
And this slice is backend only: no SPA, no change to docs/design
|
||||||
And an Author has id (uint32), name, slug, bio, avatar_id (nullable media id), website (nullable), social_links, user_id (nullable and unique), is_linked and article_count, in snake_case JSON
|
And an Author has id (uint32), name, slug, bio, avatar_id (nullable media id), website (nullable), social_links, user_id (nullable and unique), is_linked and article_count, in snake_case JSON
|
||||||
And an article has zero or more authors in a stored order through an ordered article_authors join; the first is the primary byline
|
And an article has zero or more authors in a stored order through an ordered article_authors join; the first is the primary byline
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Feature: Editor: search and sharing, suggestions and "What the frontend gets"
|
|||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the screen is EditorView (/article/:id/edit) inside DashboardShell, per docs/design/routes.md
|
Given the screen is EditorView (/article/:id/edit) inside DashboardShell, per docs/design/routes.md
|
||||||
And the visual spec of record is docs/design/screens/editor.md (groups "Editor: search and sharing" with 24 states and "Editor: what the frontend gets" with 6 states), docs/design/seo-slices.md, docs/design/components.md (SearchSharingSection, SeoPanel, SoftCounter, SearchPreview, SocialCardPreview, SuggestionList, ApiPreviewDialog, SlugField, MediaField) and docs/design/states.md ("SEO round"). Exact copy is read from those files and is not repeated here unless a scenario needs it
|
And the visual spec of record is docs/design/screens/editor.md (groups "Editor: search and sharing" with 24 states and "Editor: what the frontend gets" with 6 states), docs/design/decisions.md, docs/design/components.md (SearchSharingSection, SeoPanel, SoftCounter, SearchPreview, SocialCardPreview, SuggestionList, ApiPreviewDialog, SlugField, MediaField) and docs/design/states.md ("SEO round"). Exact copy is read from those files and is not repeated here unless a scenario needs it
|
||||||
And the board frames of docs/design/source/RenewCMS Screens Back-office.dc.html, AppFrame.dc.html and DirectoryFrame.dc.html are the layout tiebreaker
|
And the board frames of docs/design/source/RenewCMS Screens Back-office.dc.html, AppFrame.dc.html and DirectoryFrame.dc.html are the layout tiebreaker
|
||||||
And this slice is SPA only and reuses the API of features/seo_data_backend.feature and features/seo_output_backend.feature without change
|
And this slice is SPA only and reuses the API of features/seo_data_backend.feature and features/seo_output_backend.feature without change
|
||||||
And the SPA calls, with relative URLs through useApi only: GET /v1/article/{id} and PATCH /v1/article/{id} (SEO fields slug, excerpt, seo_title, seo_description, cover_image_id, cover_image_alt, social_title, social_description, social_image_id, canonical_url, noindex), POST /v1/article/{id}/seo/check, GET /v1/article/{id}/seo, GET /v1/site/ and the media routes of the shared picker
|
And the SPA calls, with relative URLs through useApi only: GET /v1/article/{id} and PATCH /v1/article/{id} (SEO fields slug, excerpt, seo_title, seo_description, cover_image_id, cover_image_alt, social_title, social_description, social_image_id, canonical_url, noindex), POST /v1/article/{id}/seo/check, GET /v1/article/{id}/seo, GET /v1/site/ and the media routes of the shared picker
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Feature: Media field usage in the Media library
|
|||||||
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
|
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
|
||||||
And API errors are plain text
|
And API errors are plain text
|
||||||
And this slice changes only web/ (and web/visual); the backend of features/media_field_type.feature is done and not touched
|
And this slice changes only web/ (and web/visual); the backend of features/media_field_type.feature is done and not touched
|
||||||
And the design is docs/design/media-field-changes.md section 3.8 and section 4, with the board group "11 Media library: usage" (4 states) of "RenewCMS Screens Schema.dc.html", which draws MediaFrame.dc.html with entries counted as uses
|
And the design is docs/design/media-field.md section 3.8 and section 4, with the board group "11 Media library: usage" (4 states) of "RenewCMS Screens Schema.dc.html", which draws MediaFrame.dc.html with entries counted as uses
|
||||||
And GET /v1/media/ items carry "used_in", which counts articles, authors, sites and entries together, and no split
|
And GET /v1/media/ items carry "used_in", which counts articles, authors, sites and entries together, and no split
|
||||||
And GET /v1/media/{id}/usage answers an array ordered articles, authors, sites, then entries by content type name then entry id, with no total and no "first few" wrapper
|
And GET /v1/media/{id}/usage answers an array ordered articles, authors, sites, then entries by content type name then entry id, with no total and no "first few" wrapper
|
||||||
And an article item is {"type":"article","id","title","published","fields"}
|
And an article item is {"type":"article","id","title","published","fields"}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Feature: Media field type in the SPA
|
|||||||
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
|
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
|
||||||
And API errors are plain text
|
And API errors are plain text
|
||||||
And this slice changes only web/ (and web/visual); the backend of features/media_field_type.feature is done
|
And this slice changes only web/ (and web/visual); the backend of features/media_field_type.feature is done
|
||||||
And the design is docs/design/media-field-changes.md sections 1, 2, 3.2 to 3.7, 4, 6 and 7, with the board groups 2 Schema builder, 3 Review and delete, 4 Field types, 5 Content and entry list, 6 Entry editor and 10 Media picker for fields of "RenewCMS Screens Schema.dc.html"
|
And the design is docs/design/media-field.md sections 1, 2, 3.2 to 3.7, 4 and 6, with the board groups 2 Schema builder, 3 Review and delete, 4 Field types, 5 Content and entry list, 6 Entry editor and 10 Media picker for fields of "RenewCMS Screens Schema.dc.html"
|
||||||
And the registry is lib/fieldTypes with lib/entryEditors, the single source for the builder, the list cells and the entry form; no screen branches on the type name "media" where a registry property can say it
|
And the registry is lib/fieldTypes with lib/entryEditors, the single source for the builder, the list cells and the entry form; no screen branches on the type name "media" where a registry property can say it
|
||||||
And a media value is the id of a library image (a uint32 of at least 1) or null
|
And a media value is the id of a library image (a uint32 of at least 1) or null
|
||||||
And every entry answer (POST 201, PUT 200, GET one, each list item) carries a top-level "media" object keyed by media field key: {"id","filename","width","height","size","thumbnail_url"} for an existing image, or {"id","missing":true} when the image is gone; a null value has no key
|
And every entry answer (POST 201, PUT 200, GET one, each list item) carries a top-level "media" object keyed by media field key: {"id","filename","width","height","size","thumbnail_url"} for an existing image, or {"id","missing":true} when the image is gone; a null value has no key
|
||||||
@@ -476,7 +476,7 @@ Feature: Media field type in the SPA
|
|||||||
Given the card or a title needs the text of a media cell
|
Given the card or a title needs the text of a media cell
|
||||||
Then it reads "–" for none and never the numeric id
|
Then it reads "–" for none and never the numeric id
|
||||||
|
|
||||||
# ---------- Accessibility (media-field-changes.md section 6)
|
# ---------- Accessibility (media-field.md section 6)
|
||||||
|
|
||||||
Scenario: Accessibility of the control
|
Scenario: Accessibility of the control
|
||||||
Then the control is role="group" labelled by the field label
|
Then the control is role="group" labelled by the field label
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Helper: decode into {items []map, total, page, size int, counts {all, unused, du
|
|||||||
- `docs/api.md` lines 99-103: replace `cursor` and `limit` with `page` (default 1) and `size` (5, 25, 50, 100; default 25); state the envelope (`items`, `total`, `page`, `size`, `counts`), that `counts` stays library-wide, that invalid values answer 400 "The media query is invalid.", and a "Breaking change" note: `next_cursor` is gone, `cursor` and `limit` are ignored.
|
- `docs/api.md` lines 99-103: replace `cursor` and `limit` with `page` (default 1) and `size` (5, 25, 50, 100; default 25); state the envelope (`items`, `total`, `page`, `size`, `counts`), that `counts` stays library-wide, that invalid values answer 400 "The media query is invalid.", and a "Breaking change" note: `next_cursor` is gone, `cursor` and `limit` are ignored.
|
||||||
- `docs/api/*.md`: grep for `/v1/media/` and `cursor`; update the media listing section if one exists.
|
- `docs/api/*.md`: grep for `/v1/media/` and `cursor`; update the media listing section if one exists.
|
||||||
- `web/src/data/apiCatalogue.ts` `med-list`: add `page`, `size`, `q`, `filter`, `sort`, `variants`, `hash` query entries, the 400 error, and a paged example (`total`, `page`, `size`, no `next_cursor`). Remove the `next_cursor` rule (line ~129, with its French entry in `web/src/i18n/messages.ts` line ~1606) if it is only used by media. Integration specs asserting the catalogue are amended.
|
- `web/src/data/apiCatalogue.ts` `med-list`: add `page`, `size`, `q`, `filter`, `sort`, `variants`, `hash` query entries, the 400 error, and a paged example (`total`, `page`, `size`, no `next_cursor`). Remove the `next_cursor` rule (line ~129, with its French entry in `web/src/i18n/messages.ts` line ~1606) if it is only used by media. Integration specs asserting the catalogue are amended.
|
||||||
- Flag for the user (read-only to agents): `docs/design/screens/integration.md` and `docs/design/source/IntegrationPage.dc.html` (med-list with no params), `docs/design/screens/media.md`, `routes.md`, `change-list.md` item 13b, `README.md` line 92 and `screens/media-picker.md`: all still say "array body" and "X-Total-Count".
|
- Flag for the user (read-only to agents): `docs/design/screens/integration.md` and `docs/design/source/IntegrationPage.dc.html` (med-list with no params), `docs/design/screens/media.md`, `routes.md`, `README.md` line 92 and `screens/media-picker.md`: all still say "array body" and "X-Total-Count".
|
||||||
- A release note under `docs/releases/` is the maintainer's call: flag it.
|
- A release note under `docs/releases/` is the maintainer's call: flag it.
|
||||||
|
|
||||||
## Manual verification
|
## Manual verification
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Feature: Relation picker "loading more" state
|
|||||||
So that pressing "Load more" never loses focus or shows a blank list
|
So that pressing "Load more" never loses focus or shows a blank list
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/screens/schema-later.md group 7 states 2 and 3, and docs/design/change-log-roles-sync.md "Relation picker"
|
Given the spec of record is docs/design/screens/schema-later.md group 7 states 2 and 3
|
||||||
And the picker is RelationField.vue over useRelationSearch.ts, which already requests GET /v1/content-type/{key}/entry?page=N&size=25 and has loading, loadingMore, hasMore and total
|
And the picker is RelationField.vue over useRelationSearch.ts, which already requests GET /v1/content-type/{key}/entry?page=N&size=25 and has loading, loadingMore, hasMore and total
|
||||||
And the sample is 25 venues loaded of 62 for "rue", footer "Showing 25 of 62" with "Load more"
|
And the sample is 25 venues loaded of 62 for "rue", footer "Showing 25 of 62" with "Load more"
|
||||||
And the design marks the group "Later" but the picker exists in the app; this file specifies the missing state only
|
And the design marks the group "Later" but the picker exists in the app; this file specifies the missing state only
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Do not copy the old `Role ==` checks into new code.
|
|||||||
## Design notes for the user
|
## Design notes for the user
|
||||||
|
|
||||||
- docs/design/screens/roles.md "Owner-only items" disables View roles and Manage roles for an admin. This slice follows the user's decision (Admin has 29 keys). The design needs a fix by the user.
|
- docs/design/screens/roles.md "Owner-only items" disables View roles and Manage roles for an admin. This slice follows the user's decision (Admin has 29 keys). The design needs a fix by the user.
|
||||||
- docs/design/change-list.md item 21 ("Sites: owner only, hidden for other roles") and its SEO "Permissions" paragraph ("only the owner edits site SEO defaults", 403 `owner_only`), plus the `owner_only` clause in routes.md "SEO", disagree with this slice. Admin has `settings.manage_sites`, so an admin can now edit sites and their SEO defaults.
|
- the removed change-list.md item 21 ("Sites: owner only, hidden for other roles") and its SEO "Permissions" paragraph ("only the owner edits site SEO defaults", 403 `owner_only`), plus the `owner_only` clause in routes.md "SEO", disagree with this slice. Admin has `settings.manage_sites`, so an admin can now edit sites and their SEO defaults.
|
||||||
- roles.md is inconsistent about Writer and Schema. The "Writer: Articles list" state hides Schema from the sidebar, while the catalogue, the built-in table and change-list ("Schema read-only for view-only roles") give Writer `schema.view`.
|
- roles.md is inconsistent about Writer and Schema. The "Writer: Articles list" state hides Schema from the sidebar, while the catalogue, the built-in table and change-list ("Schema read-only for view-only roles") give Writer `schema.view`.
|
||||||
- change-list line 100 mentions a "restricted `user` role" that does not exist.
|
- change-list line 100 mentions a "restricted `user` role" that does not exist.
|
||||||
- The docs name `GET /v1/me`. The implemented route is `GET /v1/auth/me` (resolved in slice 1).
|
- The docs name `GET /v1/me`. The implemented route is `GET /v1/auth/me` (resolved in slice 1).
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Feature: Roles sync pass 6: final copy, neutral wording and role activity events
|
|||||||
So that every message names what happened, says what was kept and says what to do next
|
So that every message names what happened, says what was kept and says what to do next
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/screens/roles.md groups 8, 12 and 13, the tables "Activity events: sentences", "Activity events: failed sentences", "Activity filter: category names" and "Final copy: toasts and errors, EN and FR", docs/design/states.md "Roles sync (pass 6)", docs/design/screens/activity.md "Roles sync (pass 6)" and docs/design/change-log-roles-sync.md section 1
|
Given the spec of record is docs/design/screens/roles.md groups 8, 12 and 13, the tables "Activity events: sentences", "Activity events: failed sentences", "Activity filter: category names" and "Final copy: toasts and errors, EN and FR", docs/design/states.md "Roles sync (pass 6)", docs/design/screens/activity.md "Roles sync (pass 6)"
|
||||||
And the stale-state toasts, banners and dialog alerts of group 15 are in features/roles_sync_conflicts.feature, not here
|
And the stale-state toasts, banners and dialog alerts of group 15 are in features/roles_sync_conflicts.feature, not here
|
||||||
And the removal of a user (route, dialog, toast, user.remove event) is in features/roles_sync_remove_user.feature
|
And the removal of a user (route, dialog, toast, user.remove event) is in features/roles_sync_remove_user.feature
|
||||||
And the audit actions are role.create, role.edit, role.delete, role.reassign, user.change_role and user.transfer_ownership (internal/domain/audit/auditDomain.go), the outcome is a field and a failed event keeps its name
|
And the audit actions are role.create, role.edit, role.delete, role.reassign, user.change_role and user.transfer_ownership (internal/domain/audit/auditDomain.go), the outcome is a field and a failed event keeps its name
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Feature: Integration page: permission-gated routes and role-aware copy
|
|||||||
So that a key's owner knows which permission a route needs
|
So that a key's owner knows which permission a route needs
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/screens/integration.md (Page copy, EndpointCard states, Error messages) and docs/design/change-log-roles-sync.md "Integration page"
|
Given the spec of record is docs/design/screens/integration.md (Page copy, EndpointCard states, Error messages)
|
||||||
And the catalogue is web/src/data/apiCatalogue.ts and its search index; routes are gated in api/router.go by container.require(...) with the permission keys of internal/domain/role
|
And the catalogue is web/src/data/apiCatalogue.ts and its search index; routes are gated in api/router.go by container.require(...) with the permission keys of internal/domain/role
|
||||||
And the real gating is:
|
And the real gating is:
|
||||||
| route | permission label (lib/permissionNames.ts) |
|
| route | permission label (lib/permissionNames.ts) |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Feature: Profile role card of the roles-sync design (group 9)
|
|||||||
So that I can understand my access without reading who assigned it
|
So that I can understand my access without reading who assigned it
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/screens/roles.md group 9 and docs/design/change-log-roles-sync.md section 2
|
Given the spec of record is docs/design/screens/roles.md group 9
|
||||||
And features/roles_spa_users.feature "Profile, your role" is amended by this file: the summary line and "Assigned by" are removed
|
And features/roles_spa_users.feature "Profile, your role" is amended by this file: the summary line and "Assigned by" are removed
|
||||||
And GET /v1/auth/me answers access.role {id, name, built_in, locked} and access.permissions; the design says GET /v1/me, the real route is /v1/auth/me
|
And GET /v1/auth/me answers access.role {id, name, built_in, locked} and access.permissions; the design says GET /v1/me, the real route is /v1/auth/me
|
||||||
And the card layout, top to bottom, inside the existing card with a gap of 12: the title, one row of badges, the link row with the list-checks icon, the muted key line
|
And the card layout, top to bottom, inside the existing card with a gap of 12: the title, one row of badges, the link row with the list-checks icon, the muted key line
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Feature: Read = Own surfaces of the roles-sync design (group 14)
|
|||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the backend contract of features/roles_permissions_content_scope.feature: a caller with content.read=own gets online articles plus their own drafts, and only the entries they created, with counts, total, page and size computed over the visible set
|
Given the backend contract of features/roles_permissions_content_scope.feature: a caller with content.read=own gets online articles plus their own drafts, and only the entries they created, with counts, total, page and size computed over the visible set
|
||||||
And the spec of record is docs/design/screens/roles.md group 14, its table "Read = Own: what each surface returns", and docs/design/change-log-roles-sync.md section 4
|
And the spec of record is docs/design/screens/roles.md group 14, its table "Read = Own: what each surface returns"
|
||||||
And the SPA never computes visibility: every count, total and pager value shown comes from the API (GET /v1/article counts and total, GET /v1/content-type/{key}/entry total)
|
And the SPA never computes visibility: every count, total and pager value shown comes from the API (GET /v1/article counts and total, GET /v1/content-type/{key}/entry total)
|
||||||
And the SPA knows a person reads Own through scopeOf("content.read", type) of stores/permissions.ts
|
And the SPA knows a person reads Own through scopeOf("content.read", type) of stores/permissions.ts
|
||||||
And Update or Delete set to Own keep the "View only" rows of slice 5; only Read = Own hides rows
|
And Update or Delete set to Own keep the "View only" rows of slice 5; only Read = Own hides rows
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Feature: SEO data backend, article and site search fields, unique slugs and medi
|
|||||||
So that a frontend can later resolve titles, descriptions, images and addresses from RenewCMS
|
So that a frontend can later resolve titles, descriptions, images and addresses from RenewCMS
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/seo-slices.md (slice 1), docs/design/change-list.md ("SEO round"), docs/design/screens/editor.md, docs/design/screens/sites.md and docs/design/states.md
|
Given the spec of record is docs/design/decisions.md (Search and sharing), docs/design/screens/editor.md, docs/design/screens/sites.md and docs/design/states.md
|
||||||
And this slice is backend only: no SPA, no change to docs/design, and no resolver, check endpoint, sitemap or list seo filter
|
And this slice is backend only: no SPA, no change to docs/design, and no resolver, check endpoint, sitemap or list seo filter
|
||||||
And an article gains these snake_case fields: slug, excerpt, seo_title, seo_description, cover_image_id, cover_image_alt, social_title, social_description, social_image_id, canonical_url, noindex, seo_dismissed
|
And an article gains these snake_case fields: slug, excerpt, seo_title, seo_description, cover_image_id, cover_image_alt, social_title, social_description, social_image_id, canonical_url, noindex, seo_dismissed
|
||||||
And in article JSON the text fields are "" when empty, cover_image_id and social_image_id are null when unset, noindex is false by default and seo_dismissed is [] by default
|
And in article JSON the text fields are "" when empty, cover_image_id and social_image_id are null when unset, noindex is false by default and seo_dismissed is [] by default
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Spec: `features/seo_data_backend.feature`. Go tests only, in `test/` (package `t
|
|||||||
|
|
||||||
- **Site image and robots names:** the API uses `default_social_image_id` and `default_robots` (existing site contract). The design writes `social_image_id` for the site and, in places, `robots`. The SPA uses the code names.
|
- **Site image and robots names:** the API uses `default_social_image_id` and `default_robots` (existing site contract). The design writes `social_image_id` for the site and, in places, `robots`. The SPA uses the code names.
|
||||||
- **Non-owner site writes:** the API answers 403 with the existing text "Only the owner can do this." (`apperr.MissingPermissionErrorMessage`). The design mentions a 403 `owner_only` token, which does not exist.
|
- **Non-owner site writes:** the API answers 403 with the existing text "Only the owner can do this." (`apperr.MissingPermissionErrorMessage`). The design mentions a 403 `owner_only` token, which does not exist.
|
||||||
- **Empty-slug fallback:** one rule everywhere. A title with no letter or digit becomes `article`, then `article-2`, `article-3` on conflict, in the API and in the migration. `docs/design/seo-slices.md` (decision 5) says `article-<id>` for the migration; that wording is not followed.
|
- **Empty-slug fallback:** one rule everywhere. A title with no letter or digit becomes `article`, then `article-2`, `article-3` on conflict, in the API and in the migration. `docs/design/decisions.md` (decision 5) says `article-<id>` for the migration; that wording is not followed.
|
||||||
- **Slug conflict transport:** the conflict follows the authors slug conflict (plain text `slug_taken`, 409); the suggestion travels in the `X-Slug-Suggestion` response header, not in a JSON body. The `slug.suggestion` of the slice 2 check endpoint is separate.
|
- **Slug conflict transport:** the conflict follows the authors slug conflict (plain text `slug_taken`, 409); the suggestion travels in the `X-Slug-Suggestion` response header, not in a JSON body. The `slug.suggestion` of the slice 2 check endpoint is separate.
|
||||||
- **Empty slug:** `slug: ""` regenerates the slug from the title (the SPA "Reset to match the title" sends `""`); 409 only if the regenerated slug belongs to another article on the site.
|
- **Empty slug:** `slug: ""` regenerates the slug from the title (the SPA "Reset to match the title" sends `""`); 409 only if the regenerated slug belongs to another article on the site.
|
||||||
- **Status codes:** `invalid_url` is 422 (authors); `invalid_slug`, `invalid_title_template` and `image_not_found` are 400.
|
- **Status codes:** `invalid_url` is 422 (authors); `invalid_slug`, `invalid_title_template` and `image_not_found` are 400.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Feature: SEO output backend, resolved SEO object, non-persisting check, list sta
|
|||||||
So that a frontend can render meta tags and a sitemap without reimplementing the fallback chain
|
So that a frontend can render meta tags and a sitemap without reimplementing the fallback chain
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/seo-slices.md (slice 2), docs/design/change-list.md ("SEO round"), docs/design/screens/editor.md, docs/design/screens/articles.md and docs/design/screens/integration.md
|
Given the spec of record is docs/design/decisions.md (Search and sharing), docs/design/screens/editor.md, docs/design/screens/articles.md and docs/design/screens/integration.md
|
||||||
And this slice is backend only: no SPA screen and no change to docs/design
|
And this slice is backend only: no SPA screen and no change to docs/design
|
||||||
And it reuses the article and site SEO fields of features/seo_data_backend.feature and adds no column and no migration
|
And it reuses the article and site SEO fields of features/seo_data_backend.feature and adds no column and no migration
|
||||||
And RenewCMS renders no page and injects no meta tag: it only returns data
|
And RenewCMS renders no page and injects no meta tag: it only returns data
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Feature: SEO round, slice 4: Sites defaults, Articles search status, Integration
|
|||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the screens are SitesView (/settings/sites), ArticleView (/article, also reached as /articles), IntegrationView (/integration) and the Editor SEO panel (/article/:id/edit), inside DashboardShell, per docs/design/routes.md
|
Given the screens are SitesView (/settings/sites), ArticleView (/article, also reached as /articles), IntegrationView (/integration) and the Editor SEO panel (/article/:id/edit), inside DashboardShell, per docs/design/routes.md
|
||||||
And the visual spec of record is docs/design/screens/sites.md ("Sites: SEO defaults", 5 states), articles.md ("Articles: search status", 3 states), integration.md ("Search and sharing"), editor.md (SuggestionList), docs/design/components.md (SeoStatusChip, SuggestionList), docs/design/seo-slices.md and docs/design/states.md ("SEO round"). Exact copy is read from those files and is not repeated here unless a scenario needs it
|
And the visual spec of record is docs/design/screens/sites.md ("Sites: SEO defaults", 5 states), articles.md ("Articles: search status", 3 states), integration.md ("Search and sharing"), editor.md (SuggestionList), docs/design/components.md (SeoStatusChip, SuggestionList), docs/design/decisions.md and docs/design/states.md ("SEO round"). Exact copy is read from those files and is not repeated here unless a scenario needs it
|
||||||
And the board frames of docs/design/source/DirectoryFrame.dc.html (Sites form, SuggestionList), AppFrame.dc.html (Articles rows, cards and toolbar) and IntegrationPage.dc.html (group and index) are the layout tiebreaker
|
And the board frames of docs/design/source/DirectoryFrame.dc.html (Sites form, SuggestionList), AppFrame.dc.html (Articles rows, cards and toolbar) and IntegrationPage.dc.html (group and index) are the layout tiebreaker
|
||||||
And this slice is SPA only and uses the API of features/seo_data_backend.feature and features/seo_output_backend.feature, plus counts.suggestions added by features/articles_pagination_api.feature. No other Go file changes
|
And this slice is SPA only and uses the API of features/seo_data_backend.feature and features/seo_output_backend.feature, plus counts.suggestions added by features/articles_pagination_api.feature. No other Go file changes
|
||||||
And the SPA calls, with relative URLs through useApi only: GET /v1/site/, POST /v1/site/ and PATCH /v1/site/{id} (site_name, title_template, default_description), GET /v1/article/ with the optional seo=suggestions parameter, whose response also carries counts.suggestions, and PATCH /v1/article/{id} with seo_dismissed. The Integration page makes no request
|
And the SPA calls, with relative URLs through useApi only: GET /v1/site/, POST /v1/site/ and PATCH /v1/site/{id} (site_name, title_template, default_description), GET /v1/article/ with the optional seo=suggestions parameter, whose response also carries counts.suggestions, and PATCH /v1/article/{id} with seo_dismissed. The Integration page makes no request
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Feature: Sites backend, the owner manages sites and every article belongs to exa
|
|||||||
So that a single back-office can hold the content of more than one website
|
So that a single back-office can hold the content of more than one website
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given the spec of record is docs/design/screens/sites.md, docs/design/states.md (site rows), docs/design/routes.md and docs/design/sites-authors-slices.md
|
Given the spec of record is docs/design/screens/sites.md, docs/design/states.md (site rows), docs/design/routes.md and docs/design/decisions.md
|
||||||
And this slice is backend only: no SPA, no change to docs/design
|
And this slice is backend only: no SPA, no change to docs/design
|
||||||
And a Site has id (uint32), name, key, base_url, default_social_image_id (nullable), default_robots ("index" or "noindex"), locale (nullable), is_default and article_count, in snake_case JSON
|
And a Site has id (uint32), name, key, base_url, default_social_image_id (nullable), default_robots ("index" or "noindex"), locale (nullable), is_default and article_count, in snake_case JSON
|
||||||
And exactly one site is the default at any time, enforced by the use case inside a transaction and not by a partial index
|
And exactly one site is the default at any time, enforced by the use case inside a transaction and not by a partial index
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# RenewCMS frontend
|
|
||||||
|
|
||||||
Vue 3 + Vite + PrimeVue + Tailwind SPA, managed with Bun. It is built into `../internal/infrastructure/assets/dist/` and embedded into the Go binary.
|
|
||||||
|
|
||||||
Use the tasks from the repository root rather than running Vite directly:
|
|
||||||
|
|
||||||
- `task dev` runs Vite on http://localhost:5173 and proxies `/v1` to the API.
|
|
||||||
- `task lint:web` runs eslint and vue-tsc.
|
|
||||||
- `task build:web` produces the production build.
|
|
||||||
|
|
||||||
See [../docs/development.md](../docs/development.md).
|
|
||||||
Reference in new issue
Block a user