Feature: Settings page and GET /v1/settings As a verified RenewCMS user in the shared workspace I want a Settings page that shows the version of the RenewCMS build I am running So that I can tell which build is deployed when I report a problem or check an upgrade Background: Given GET /v1/settings is mounted under /v1 in api/router.go in its own router, behind jwtauth.Verifier, jwtauth.Authenticator, middleware.RequireCurrentToken and middleware.RequireVerifiedUser, the same chain as NewUserRouter And the response of a verified caller is 200 with the JSON body {"version": ""}, one snake_case field and nothing else, built from the domain entity settings.Settings in internal/domain/settings And GetSettingsUseCase (internal/infrastructure/useCases) is built with the version string and depends only on the domain package; the handler in api/handlers/settings receives it through NewHandler and never reads config, os or the environment And config.Settings gains a Version field; cmd/server/main.go sets it from its own main.version variable ("dev" unless set with -ldflags -X main.version) right after config.NewSettingsFromEnv, and api/dependecyInjection.go passes settings.Version to the use case; no os.Getenv or os.LookupEnv for the version appears outside cmd/server/main.go and internal/infrastructure/config, and the version is captured once at startup And /settings is a verified route (meta.auth "verified", meta.title "Settings") rendered as SettingsPage inside DashboardShell, per docs/design/routes.md and docs/design/screens/settings.md And SettingsPage calls GET /v1/settings once per navigation to it And the PageHeader shows breadcrumb "Settings" (mobile title "Settings"), the page title "Settings" and the subtitle "Workspace information." And the page is at most 720 px wide, with padding 32 px on desktop and 20px 16px 88px on mobile, and 20 px between the header and the card And the page holds one card, --ui-bg with a 1 px --ui-border, 20 px padding on desktop and 16 px on mobile, containing a single row laid out as the board frame draws it: the label at the left, the value at the right, vertically centred, 16 px apart And the label is "Version" (15 px semibold, --ui-text-highlighted) and the value is the server's version string in IBM Plex Mono 14 px, --ui-text-toned, with no "v" added by the SPA And the page has no action, toggle, link or input; every verified user sees it, owner and admin alike And DashboardShell's navigation gains "Settings" (icon settings, i-lucide-settings, linking to /settings) after "Integration", in the sidebar and in the mobile drawer, so it lists five destinations in this order: Home, Articles, Users, Integration, Settings And skeleton placeholders show only after 150 ms of loading, so a fast response never flashes them And the Loading and Error states are not drawn in docs/design; they are decided in this spec and covered by Vitest only (no visual state) Scenario Outline: A verified caller gets the running build's version Given the process was started with a config.Settings whose Version is "" And a verified user is logged in with a valid jwt cookie When the client sends GET /v1/settings Then the response status is 200 And the response body is exactly the JSON object {"version": ""} Examples: | version | | v2.2.0-beta | | dev | Scenario: The use case returns the version it was built with Given GetSettingsUseCase is built with the version "v2.2.0-beta" When GetSettings is called Then it returns a settings.Settings whose Version is "v2.2.0-beta" Scenario: A request without a jwt cookie is refused When the client sends GET /v1/settings with no jwt cookie Then the response status is 401 And the use case is not consulted for the response Scenario: A token whose version no longer matches the account is refused Given user id 7 exists with token version 1 And the jwt cookie was minted for user 7 with token version 0 When the client sends GET /v1/settings with that cookie Then the response status is 401 And the response body is apperr.UnauthorizedErrorMessage Scenario: An unverified account is refused Given user id 8 exists with is_verified false and a valid jwt cookie When the client sends GET /v1/settings with that cookie Then the response status is 403 And the response body is apperr.VerificationRequiredErrorMessage And the response does not contain the version Scenario: Default — the Version row shows the server's version Given GET /v1/settings resolves 200 with {"version": "v2.2.0-beta"} When the user navigates to /settings Then the breadcrumb reads "Settings" (mobile title "Settings") And the header shows the title "Settings" and the subtitle "Workspace information." And the card shows one row: the label "Version" and the value "v2.2.0-beta" And the "Settings" nav item has aria-current="page" and no other nav item does Scenario: The value is shown exactly as reported, with no prefix added Given GET /v1/settings resolves 200 with {"version": "dev"} When the user navigates to /settings Then the value reads "dev" Scenario: Navigation — "Settings" sits after Integration Given the user is on /home When DashboardShell mounts Then the sidebar and the mobile drawer list Home, Articles, Users, Integration, Settings in that order And "Settings" has the settings icon and links to /settings Scenario: Loading — the value becomes a skeleton after 150 ms while the request is in flight Given GET /v1/settings has not yet resolved When the user navigates to /settings Then the label "Version" is visible at once, and nothing stands in for the value during the first 150 ms And after 150 ms a skeleton replaces the value only, and the card is aria-busy="true" When GET /v1/settings then resolves 200 with {"version": "v2.2.0-beta"} Then the skeleton is replaced by "v2.2.0-beta" and aria-busy is no longer set Scenario Outline: Error — a 500 or a network failure replaces the row with an inline alert and a retry Given GET /v1/settings When the user navigates to /settings Then the card shows an error alert with no close button and no title, reading "Couldn't reach RenewCMS. Check your connection and try again.", with an action "Try again" (icon rotate-cw) And the "Version" row is not shown And the header (title and subtitle) is still shown When the user activates "Try again" Then the Loading state shows and GET /v1/settings is sent again When it then resolves 200 with {"version": "v2.2.0-beta"} Then the Default state's row replaces the alert Examples: | failure | | resolves with a 500 response | | never reaches the server | Scenario: A 401 or an unverified 403 is handled globally, not by the page Given GET /v1/settings resolves 401 When the user navigates to /settings Then useApi's global handling redirects to "/login?redirect=/settings&reason=expired" And SettingsPage shows no error alert of its own