Feature: Sites and Authors screens As a RenewCMS owner or admin I want /settings/sites and /authors to list, add, edit and delete sites and authors So that every article belongs to a frontend and credits the right bylines Background: Given /settings/sites is a verified route rendered as SitesView and /authors is a verified route rendered as AuthorsView, both inside DashboardShell, per docs/design/routes.md And both screens are built from the shared directory pieces, defined once and not redrawn: DirectoryList, DirectoryPanel (the slide-over), ReassignDialog, SlugField and MediaField And the screens call, with relative URLs through useApi only, GET, POST, PATCH and DELETE on /v1/site/ and /v1/author/, PATCH /v1/site/{id}/default and GET /v1/user/ And POST /v1/author/me is not exposed in the UI by this slice And a site has the fields id, name, key, base_url, default_social_image_id, default_robots ("index" or "noindex"), locale, is_default and article_count And an author has the fields id, name, slug, bio, avatar_id, website, social_links, user_id, is_linked, article_count and, for owner and admin only, user_email And an error answered by the API is a plain-text code, and the SPA maps the code to the copy of docs/design/states.md, never showing the code itself And the sites fixture is, in list order: | name | key | base_url | articles | default | | Renew | renew | https://renew.example | 268 | yes | | Journal | journal | https://journal.example | 38 | no | | Projects | projects | https://projects.example | 6 | no | | Archive | archive | https://archive.example | 0 | no | And the authors fixture includes "Inès Duarte" (slug ines-duarte, linked, 9 articles), "Camille Roche" (slug camille-roche, guest, 3 articles), "Léa Fontaine" (slug lea-fontaine, guest, 0 articles) and one author with 1 article And every visible string goes through t(...) with the English text as the key and its French entry in web/src/i18n/messages.ts And the screens use Nuxt UI components and design tokens only, with no raw colors And the board frames of docs/design/source/DirectoryFrame.dc.html are the layout tiebreaker Rule: Sites list Scenario: Sites: default (owner) Given the session role is "owner" And GET /v1/site/ answers 200 with the sites fixture When the user navigates to /settings/sites Then the page shows "Sites", "The frontends that display your content." and a primary "Add site" button And the columns are "Site", "Base URL" and "Articles" And each row shows the name with its key and the base URL And the counts read "268 articles", "38 articles", "6 articles" and "No articles" And "Renew" carries a "Default" badge And a site whose default_robots is "noindex" carries a "Noindex" badge And below md the rows are cards and a full-width "Add site" button is pinned to the bottom Scenario: Sites: only the default site Given GET /v1/site/ answers 200 with only the default site When the owner navigates to /settings/sites Then the row is listed and the hint "Add another site if your content is shown on more than one frontend. Each article belongs to one site." appears under the list And no empty-state wall is drawn Scenario: Sites: loading Given GET /v1/site/ is pending When the owner navigates to /settings/sites Then no skeleton shows before 150 ms And after 150 ms skeleton rows are drawn and the list has aria-busy "true" Scenario Outline: Sites: error on load Given GET /v1/site/ fails with When the owner navigates to /settings/sites Then an alert shows "Couldn't load sites", "Check your connection and try again." and a "Try again" action And no rows are drawn When the user activates "Try again" Then GET /v1/site/ is requested again and, on 200, the list replaces the alert Examples: | failure | | status 500 | | a network error | Scenario: Sites: row menu Given the sites list is shown When the user activates the ellipsis button of "Journal", or presses Enter on it Then a menu offers "Edit", "Set as default" and "Delete…" And below md the menu is a bottom sheet titled "Journal" Scenario: Sites: delete unavailable on the default site Given the sites list is shown When the user opens the menu of "Renew" Then "Delete…" has aria-disabled "true" and stays focusable And a tooltip on desktop, and text in the sheet on touch, reads "The default site can't be deleted. Make another site the default first." And activating it opens no dialog and sends no request Scenario: Sites: set as default, confirm and success Given the sites list is shown When the user chooses "Set as default" on "Journal" Then a dialog titled "Make Journal the default site?" reads "New articles start on this site. Articles that are already on other sites stay where they are." with "Cancel" and "Make default" When the user activates "Make default" Then exactly one PATCH /v1/site/2/default is sent with no body And the button reads "Saving…" while it runs And on 200 the "Default" badge moves to "Journal" and a toast shows "Default site changed" and "\"Journal\" is now the default site." Scenario Outline: Sites: set as default, failures Given the "Make Journal the default site?" dialog is open When PATCH /v1/site/2/default answers Then Examples: | status | outcome | | 500 | the dialog stays open with the notice "Couldn't save the site. Your changes are still here. Try again in a moment." | | 404 | the dialog closes, the row leaves the list and an info toast shows "That site no longer exists" and "It was deleted somewhere else, so we removed it from the list." | | 403 | the dialog closes and an error toast shows "Only the owner can manage sites" and "Ask the owner of this site." | Rule: Sites access Scenario: Sites: hidden entry for an admin Given the session role is "admin" When the user opens /settings Then the sidebar has no "Publishing" group and no "Sites" entry And the page has no "Publishing" card and no "Manage sites" row Scenario: Sites: the owner sees the entry Given the session role is "owner" When the user opens /settings Then the sidebar has a "Publishing" group with a "Sites" entry linking to /settings/sites And Settings has a "Publishing" card with a "Manage sites" row linking to /settings/sites Scenario Outline: Sites: forbidden (403 page) Given the session role is "" When the user opens Then the page shows "You don't have access to Sites", "Only the owner can manage sites. Ask the owner of this site if you need a change." and a "Back to Settings" button And the user is not redirected, no slide-over opens and the sidebar has no Sites entry When the user activates "Back to Settings" Then the user lands on /settings Examples: | role | path | | admin | /settings/sites | | member | /settings/sites | | admin | /settings/sites?edit=journal | Scenario: Sites: the API answers 403 where the SPA expected the owner Given the session role is "owner" but the API answers 403 "Only the owner can do this." When the answer is for a page load Then the 403 page shows When the answer is for an action Then an error toast shows "Only the owner can manage sites" and "Ask the owner of this site." Rule: Sites form Scenario: Sites form: add, empty Given the sites list is shown and the default site is "Renew" When the user activates "Add site" (the page header button, or the pinned bottom button below md) Then a slide-over titled "Add site" opens, a 92 % bottom sheet below md And it has the fields Name (placeholder "e.g. Journal"), Key (placeholder "Generated from the name"), Base URL (placeholder "https://blog.example.com", help "The public address of the frontend. Use https and no path."), "Default social image (optional)" with "Choose image" and "Used when an article has no image of its own.", "Default robots policy" with "Index" and "Noindex" and "Search engines may list this site's pages.", "Locale (optional)" (placeholder "e.g. en or fr-CH", help "A language tag. Leave empty if the site has no single language."), and the switch "Make this the default site" with "New articles start here. The current default is Renew." And a "Canonical URL preview" is shown, plus "Cancel" and "Add site" And Name has focus Scenario: Sites form: key from the name Given the Add site slide-over is open When the user types "Journal" in Name Then Key shows "journal" with an "Auto" badge and the help "Lowercase letters, numbers and hyphens. Used in API paths. It can't be changed after the site is added." When the user activates the pencil Then Key becomes editable and no longer follows the name Scenario Outline: Sites form: base URL not valid Given the Add site slide-over is open with a valid name and key When the user enters base URL "" and leaves the field or submits Then no request is sent And the Base URL error reads "" Examples: | value | message | | http://blog.example.com | Use https. For example https://blog.example.com. | | https://blog.example.com/en | Remove the path. Use only the host, like https://blog.example.com. | | blog | Enter a full URL, like https://blog.example.com. | Scenario: Sites form: base URL refused by the server Given the form passed the client checks When POST or PATCH answers 400 invalid_site_url Then Base URL shows "Enter a full URL, like https://blog.example.com." Scenario Outline: Sites form: loopback hosts accept http Given the Add site slide-over is open When the user enters base URL "" Then no base URL error is shown Examples: | value | | http://localhost:5173 | | http://127.0.0.1:8081 | | http://[::1]:5173 | Scenario: Sites form: add succeeds Given the Add site slide-over is filled with name "Journal", key "journal", base URL "https://journal.example", robots "Noindex", empty locale and the default switch off When the user activates "Add site" Then exactly one POST /v1/site/ is sent with name, key, base_url, default_robots "noindex", no locale, no default_social_image_id and is_default false And the button reads "Saving…" while the request runs and the fields are disabled And on 201 the slide-over closes and the new site is listed Scenario: Sites form: duplicate key Given the Add site slide-over has key "journal" and a site with that key is in the loaded list When the user leaves Key, or submits and POST /v1/site/ answers 409 site_key_taken Then Key shows "A site with the key \"journal\" already exists. Choose another key." and a suggestion "Use journal-2" And activating the suggestion sets Key to "journal-2" Scenario Outline: Sites form: save failed Given the Add site slide-over is filled When the save fails with Then a notice in the panel reads "Couldn't save the site. Your changes are still here. Try again in a moment." And all the values are kept and the form is enabled again Examples: | failure | | status 500 | | a network error | Scenario: Sites form: invalid locale from the server Given the form has locale "english" When the save answers 400 invalid_locale Then Locale shows "Use a language tag like en or fr-CH." and the panel stays open Scenario: Sites form: edit the default site When the owner chooses "Edit" on "Renew" Then a slide-over titled "Edit Renew" opens with the fields filled And Key shows "renew" read-only with a lock and "Used in API paths, so it can't change." And "Default site" is on and locked with "New articles start on the default site. To change it, make another site the default." And the submit reads "Save changes" Scenario: Sites form: edit another site, noindex When the owner chooses "Edit" on "Projects" whose robots is "noindex" Then the slide-over is titled "Edit Projects", Key is read-only and locked, "Noindex" is selected with "Asks search engines not to list this site's pages." and the switch "Make this the default site" is available When the user changes the name and activates "Save changes" Then exactly one PATCH /v1/site/3 is sent without the key field Scenario: Sites form: make another site the default from the form Given the "Projects" edit form has the default switch turned on When the user activates "Save changes" Then the SPA sends PATCH /v1/site/3 for the changed fields, then PATCH /v1/site/3/default And is_default is never sent in the first PATCH Scenario: Sites form: discard changes Given a slide-over is open with a changed field When the user closes it with "Cancel", Escape or the backdrop Then the existing "Discard changes?" dialog is asked And an unchanged form closes without it Scenario: Sites form: deep link Given the session role is "owner" When the user opens /settings/sites?edit=projects Then the "Edit Projects" slide-over opens When the user opens /settings/sites?edit=nope Then an info toast shows "That site no longer exists" and the slide-over stays closed Rule: Sites delete Scenario: Sites delete: site with articles Given the sites list is shown When the owner chooses "Delete…" on "Journal" Then a dialog titled "Delete this site?" reads "\"Journal\" will be deleted. 38 articles belong to it, so choose a site to move them to. This can't be undone." And "Move articles to" defaults to the default site "Renew" shown with "renew.example" And the text "38 articles will move to Renew. Their canonical host changes to renew.example." is shown, with "Cancel" and "Move 38 articles and delete" Scenario: Sites delete: choose destination Given the "Delete this site?" dialog is open for "Journal" When the user opens the select Then "Journal" is not offered and each option shows the name and the host Scenario: Sites delete: with articles succeeds Given the dialog is open for "Journal" (id 2) with destination "Renew" (id 1) When the user activates "Move 38 articles and delete" Then exactly one DELETE /v1/site/2?reassign_to=1 is sent And the button reads "Moving articles…" while it runs And on 204 the dialog closes, the row leaves the list and a toast shows "Site deleted" and "38 articles moved to Renew." Scenario: Sites delete: no articles When the owner chooses "Delete…" on "Archive" Then a dialog titled "Delete this site?" reads "\"Archive\" will be deleted. No articles belong to it. This can't be undone." with "Cancel" and "Delete site" and no select When the user activates "Delete site" Then DELETE /v1/site/4 is sent with no reassign_to and, on 204, the row leaves the list Scenario Outline: Sites delete: failures Given the delete dialog is open for "Journal" When DELETE answers Then Examples: | status | outcome | | 500 or network | the dialog stays open with the notice titled "Couldn't delete the site" and text "Nothing was changed. Try again in a moment." | | 409 site_has_articles | the dialog stays open and shows "Choose where the articles go. This site has 38 articles." | | 403 default_site | the dialog closes and an error toast shows "The default site can't be deleted. Make another site the default first." | | 404 | the dialog closes, the row leaves the list and the info toast "That site no longer exists" shows | Rule: Authors list Scenario Outline: Authors: default Given the session role is "" And GET /v1/author/ answers 200 with the authors fixture When the user navigates to /authors Then the sidebar entry "Authors" sits between "Media" and "Users" and carries aria-current "page" And the page shows "Authors", "The names readers see on your articles. An author doesn't need an account.", a primary "Add author" button and the search field "Search by name or slug" And the columns are "Author", "Slug", "Account" and "Articles" And each row shows the avatar and name, the slug, a "Linked" or "Guest" badge, and "9 articles", "1 article" or "No articles" And a long name is truncated and shown in full on hover And the first request is GET /v1/author/?page=1&size=25 Examples: | role | | owner | | admin | Scenario: Authors: loading Given GET /v1/author/ is pending When the user navigates to /authors Then skeleton rows are drawn and the list has aria-busy "true" Scenario Outline: Authors: error on load Given GET /v1/author/ fails with When the user navigates to /authors Then an alert shows "Couldn't load authors", "Check your connection and try again." and "Try again" When the user activates "Try again" Then the list is requested again Examples: | failure | | status 500 | | a network error | Scenario Outline: Authors: empty, first run Given GET /v1/author/ answers 200 with total 0 and no search And the session role is "" When the user navigates to /authors Then the toolbar is hidden and the empty state shows "No authors yet" and "Authors are the names readers see on articles. They don't need an account." And