domma-cms 1.6.60 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +4344 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/page-editor-reference.js +5 -5
  8. package/admin/js/lib/shortcode-tags.js +1 -1
  9. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  10. package/docs/DommaFeatures.md +3005 -0
  11. package/docs/MultiTenant.md +176 -0
  12. package/docs/api-reference.md +1667 -0
  13. package/docs/cache.md +96 -0
  14. package/docs/configuration.md +1811 -0
  15. package/docs/editions-and-licences.md +190 -0
  16. package/docs/getting-started.md +201 -0
  17. package/docs/guides/form-triggers.md +260 -0
  18. package/docs/guides/mongodb-storage-adapter.md +282 -0
  19. package/docs/guides/multi-page-forms.md +121 -0
  20. package/docs/images/scribe-composer.png +0 -0
  21. package/docs/markdown-shortcodes.md +1738 -0
  22. package/docs/pentest-report-2026-04-03.md +181 -0
  23. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  24. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  25. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  26. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  27. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  28. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  29. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  30. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  31. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  32. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  33. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  34. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  35. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  36. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  37. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  38. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  39. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  40. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  41. package/docs/plugin-development.md +827 -0
  42. package/docs/recruitment-recipe.md +501 -0
  43. package/docs/scaffolding.md +312 -0
  44. package/docs/screenshots/calculator-floating.png +0 -0
  45. package/docs/screenshots/calculator-standard.png +0 -0
  46. package/docs/screenshots/email-settings.png +0 -0
  47. package/docs/screenshots/messages-count.png +0 -0
  48. package/docs/structure/user-types.md +107 -0
  49. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  50. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  51. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  52. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  53. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  54. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  55. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  56. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  57. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  58. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  59. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  60. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  61. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  62. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  63. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  64. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  65. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  66. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  67. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  68. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  69. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  70. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  71. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  72. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  73. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  74. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  75. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  76. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  77. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  78. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  79. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  80. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  81. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  82. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  83. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  84. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  85. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  86. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  87. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  88. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  89. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  90. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  91. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  92. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  93. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  94. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  95. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  96. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  97. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  98. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  99. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  100. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  101. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  102. package/docs/theming.md +320 -0
  103. package/package.json +2 -2
  104. package/public/js/celebrations/core/canvas.js +1 -1
  105. package/public/js/celebrations/index.js +1 -1
  106. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  107. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  108. package/public/js/celebrations/message-attrs.mjs +1 -0
  109. package/public/js/celebrations/themes/anniversary.js +1 -0
  110. package/public/js/celebrations/themes/autumn.js +1 -0
  111. package/public/js/celebrations/themes/birthday.js +1 -0
  112. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  113. package/public/js/celebrations/themes/calendar.js +1 -0
  114. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  115. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  116. package/public/js/celebrations/themes/christmas.js +1 -1
  117. package/public/js/celebrations/themes/easter.js +1 -0
  118. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  119. package/public/js/celebrations/themes/garden.js +1 -0
  120. package/public/js/celebrations/themes/graduation.js +1 -0
  121. package/public/js/celebrations/themes/greeting.js +1 -0
  122. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  123. package/public/js/celebrations/themes/money-rain.js +1 -0
  124. package/public/js/celebrations/themes/new-year.js +1 -0
  125. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  126. package/public/js/celebrations/themes/occasions.js +1 -0
  127. package/public/js/celebrations/themes/rain-control.js +1 -0
  128. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  129. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  130. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  131. package/public/js/celebrations/themes/summer-garden.js +1 -0
  132. package/public/js/celebrations/themes/winter.js +1 -0
  133. package/public/js/celebrations/trait-data.mjs +1 -1
  134. package/public/js/effects.js +1 -1
  135. package/scripts/build.js +4 -0
  136. package/server/services/markdown.js +5 -2
  137. package/server/templates/page.html +1 -1
@@ -0,0 +1,181 @@
1
+ # Penetration Test Report - Domma CMS
2
+
3
+ **Date:** 2026-04-03
4
+ **Scope:** domma-cms (localhost:4096) and domma-cms-manager (localhost:8192)
5
+ **Tester:** Claude Code (authorised, internal pen test)
6
+ **Method:** Black-box HTTP + grey-box (source review where required)
7
+
8
+ ---
9
+
10
+ ## Executive Summary
11
+
12
+ The application has a solid authentication and authorisation foundation. All unauthenticated access to protected
13
+ resources was correctly blocked. Path traversal is detected and rejected server-side. However, four actionable findings
14
+ were identified that should be addressed before the application is exposed to the public internet.
15
+
16
+ ---
17
+
18
+ ## Findings
19
+
20
+ ### FIND-01 - Stored XSS in Page Title (Reflected in Public `<title>` tag)
21
+
22
+ **Severity:** High
23
+ **Affects:** Both 4096 and 8192 (shared codebase)
24
+
25
+ **Endpoint:** `POST /api/pages`
26
+ **Method:** POST (authenticated)
27
+ **Payload:**
28
+
29
+ ```json
30
+ {
31
+ "urlPath": "xss-pentest2",
32
+ "frontmatter": { "title": "<script>alert(1)</script>", "status": "published" },
33
+ "body": "<img src=x onerror=alert(2)>"
34
+ }
35
+ ```
36
+
37
+ **Response (API):** 201 - title stored verbatim: `"title":"<script>alert(1)</script>"`
38
+ **Response (public render):** 200 - raw script tag appears in `<title>` element of SSR HTML:
39
+
40
+ ```html
41
+ <title><script>alert(1)</script> | My Nice Little Super Blog!</title>
42
+ ```
43
+
44
+ **Impact:** Any admin/editor who creates a page with a script tag in the title causes that script to execute in the
45
+ browser of any visitor to that page. The body `onerror` payload is also stored unescaped in the API response, though the
46
+ SSR renderer may escape it in the body context (only the `<title>` injection was confirmed to execute).
47
+
48
+ **Recommendation:**
49
+
50
+ - HTML-encode page titles before inserting into the `<title>` tag in `server/templates/page.html` and
51
+ `server/services/renderer.js`. Use `title.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')`.
52
+ - Sanitise all frontmatter string fields on write in `server/services/content.js` before persisting.
53
+ - Consider a Content Security Policy (`Content-Security-Policy: default-src 'self'`) header on all public responses.
54
+
55
+ ---
56
+
57
+ ### FIND-02 - No Rate Limiting on Login Endpoint
58
+
59
+ **Severity:** High
60
+ **Affects:** Both 4096 and 8192
61
+
62
+ **Endpoint:** `POST /api/auth/login`
63
+ **Method:** POST
64
+ **Payload:** `{"email":"admin@dwit.uk","password":"wrongpassword"}` × 20 rapid requests
65
+ **Response:** 20 × 401 - no 429, no delay, no lockout observed.
66
+
67
+ **Impact:** The login endpoint accepts unlimited password attempts. A targeted brute-force or credential-stuffing attack
68
+ against known email addresses (which are enumerable if an attacker has admin access, or guessable) faces no automated
69
+ defence.
70
+
71
+ **Recommendation:**
72
+
73
+ - Add `@fastify/rate-limit` to the Fastify instance, scoped to `POST /api/auth/login` (e.g. max 10 attempts per IP per
74
+ 15 minutes).
75
+ - Optionally implement account lockout after N consecutive failures per email, with unlock by admin or timed expiry.
76
+ - Log and alert on repeated login failures.
77
+
78
+ ---
79
+
80
+ ### FIND-03 - Content-Type Spoofing on Media Upload (HTML stored as JPEG)
81
+
82
+ **Severity:** Medium
83
+ **Affects:** Both 4096 and 8192
84
+
85
+ **Endpoint:** `POST /api/media`
86
+ **Method:** POST (authenticated)
87
+ **Payload:** Multipart upload - filename `evil.jpg`, `Content-Type: image/jpeg`, body is
88
+ `<html><body><script>alert('xss')</script></body></html>`
89
+ **Response:** 201 `{"name":"evil.jpg","url":"/media/evil.jpg"}`
90
+ **Follow-up:** `GET /media/evil.jpg` → 200, `Content-Type: image/jpeg`, body is the raw HTML.
91
+
92
+ **Impact:** The server trusts the client-supplied `Content-Type` and file extension without inspecting the actual file
93
+ content (magic bytes / file signature). An authenticated user can store an HTML file disguised as an image. When another
94
+ user or browser fetches `/media/evil.jpg`, modern browsers will sniff the content and may render it as HTML (especially
95
+ if `X-Content-Type-Options: nosniff` is absent), enabling stored XSS via the media path.
96
+
97
+ **Note:** `.html` files with `Content-Type: text/html` are correctly blocked (400). Only the extension/MIME-spoof path
98
+ is vulnerable.
99
+
100
+ **Recommendation:**
101
+
102
+ - Inspect file magic bytes on upload (e.g. using the `file-type` npm package) and reject files whose content does not
103
+ match their declared type.
104
+ - Add the `X-Content-Type-Options: nosniff` response header to all static media responses - this is the primary
105
+ mitigation and is a single-line Fastify config change.
106
+ - Enforce an allowlist of magic-byte signatures for accepted image types.
107
+
108
+ ---
109
+
110
+ ### FIND-04 - Internal Path Disclosed in 500 Error (Long Filename)
111
+
112
+ **Severity:** Low
113
+ **Affects:** Both 4096 and 8192
114
+
115
+ **Endpoint:** `POST /api/media`
116
+ **Method:** POST (authenticated)
117
+ **Payload:** Multipart upload with filename of 500 `A` characters + `.jpg`
118
+ **Response:** 500
119
+
120
+ ```json
121
+ {"error":"ENAMETOOLONG: name too long, open 'content/media/AAAA...'"}
122
+ ```
123
+
124
+ **Impact:** The raw Node.js filesystem error is passed directly to the client, disclosing the relative server-side
125
+ storage path (`content/media/`). This aids an attacker in understanding the directory structure if combined with other
126
+ vulnerabilities.
127
+
128
+ **Recommendation:**
129
+
130
+ - Validate filename length before attempting filesystem operations (e.g. reject filenames over 200 characters).
131
+ - Catch filesystem errors in the media upload handler and return a generic `{"error":"Upload failed"}` without including
132
+ the raw error message.
133
+
134
+ ---
135
+
136
+ ## Findings Requiring No Action (Passed)
137
+
138
+ | Test | Result |
139
+ |----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
140
+ | Unauthenticated access to `/api/users`, `/api/pages`, `/api/collections`, `/api/media` | Correctly blocked - 401 |
141
+ | `POST /api/auth/setup` after setup complete | Correctly blocked - 403 |
142
+ | Expired JWT | Correctly rejected - 401 |
143
+ | JWT with `alg:none` | Correctly rejected - 401 |
144
+ | Refresh token used as access token | Correctly rejected - 401 (`Invalid token type`) |
145
+ | Path traversal in page GET/POST (`..%2F..%2Fconfig`, `../../../etc/passwd`) | Detected and rejected - 500 `Invalid path: outside content directory` |
146
+ | SQL injection in login fields | No effect - 401 |
147
+ | Very long strings in login | No effect - 401 |
148
+ | NoSQL injection in collection query (`?filter[$where]=1==1`) | No effect - empty result set returned |
149
+ | Prototype pollution in POST body (`__proto__`) | Rejected - 400 invalid JSON |
150
+ | Subscriber role blocked from admin endpoints | Correctly blocked - 403 |
151
+ | Password hashes exposed in `/api/users` list | Not present (good) |
152
+ | `GET /.env` | 404 (not statically served) |
153
+ | `GET /config/server.json` | 404 (not statically served) |
154
+ | `GET /admin/../../../package.json` | 403 Forbidden |
155
+ | CORS - arbitrary origin reflected | Not reflected (no `Access-Control-Allow-Origin` header set for unknown origins) |
156
+ | TRACE method | 404 (method not registered, not echoed) |
157
+ | `.html` file upload | Correctly blocked - 400 |
158
+ | SSTI (`{{7*7}}`, `${7*7}`) in content | Not evaluated - stored/rendered as literal text |
159
+
160
+ ---
161
+
162
+ ## Prioritised Summary Table
163
+
164
+ | # | ID | Severity | Category | Finding | Effort to Fix |
165
+ |---|---------|------------|-----------------|-----------------------------------------------------------------|---------------|
166
+ | 1 | FIND-01 | **High** | XSS | Script tag in page title rendered unescaped in public `<title>` | Low |
167
+ | 2 | FIND-02 | **High** | Auth | No rate limiting on login endpoint - brute-force possible | Low |
168
+ | 3 | FIND-03 | **Medium** | Upload | HTML file uploaded as JPEG, served from media path | Low-Medium |
169
+ | 4 | FIND-04 | **Low** | Info Disclosure | Internal filesystem path leaked in 500 error on long filename | Low |
170
+
171
+ ---
172
+
173
+ ## Notes
174
+
175
+ - **JWT secret in `.env`:** The JWT secret is stored in `.env` which is not statically served (correctly). Ensure `.env`
176
+ is in `.gitignore` and never committed.
177
+ - **`type: 'access'` check in JWT:** The middleware correctly rejects tokens that lack `type: 'access'`, which prevents
178
+ refresh tokens being used as access tokens. This is good practice.
179
+ - **8192 (domma-cms-manager):** Spot-checked and behaves identically to 4096 on auth, CORS, and rate limiting - all
180
+ findings above apply equally to both instances.
181
+ - **Test artefacts:** All pages, media, and collections created during testing have been deleted.
@@ -0,0 +1,349 @@
1
+ # Admin Topbar & Login Form Refactor - Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Replace the hacky Domma navbar with a proper custom `<header>` topbar showing brand icon + user info on the left and Settings/Sign out on the right; also rewrite both login and setup forms to use Domma's `F.render()` blueprint-driven API.
6
+
7
+ **Architecture:** The admin panel already has a sidebar for navigation, so the topbar is pure chrome - brand identity + session info. We replace `Domma.elements.navbar()` with a hand-crafted `<header id="admin-topbar">` rendered via admin.css flexbox. The login/setup forms are replaced with `F.render()` calls using blueprints; the onboarding wizard panels (site identity, theme picker) stay as custom HTML since they are not standard forms.
8
+
9
+ **Tech Stack:** Domma `F.render()`, `I.scan()`, `E.toast()`, admin.css custom styles, ESM view modules.
10
+
11
+ ---
12
+
13
+ ### Task 1: Update `admin/index.html` - replace nav with header
14
+
15
+ **Files:**
16
+ - Modify: `admin/index.html`
17
+
18
+ The current `<nav id="admin-navbar">` uses `Domma.elements.navbar()` and gets a chip appended after auth. Replace it with a structured `<header>` that has two permanent sub-regions JS will populate.
19
+
20
+ **Step 1: Replace the nav element**
21
+
22
+ Change:
23
+ ```html
24
+ <!-- Top Navbar -->
25
+ <nav id="admin-navbar"></nav>
26
+ ```
27
+
28
+ To:
29
+ ```html
30
+ <!-- Top Bar -->
31
+ <header id="admin-topbar">
32
+ <div class="topbar-brand">
33
+ <span data-icon="layout"></span>
34
+ <span class="topbar-brand-text">Domma CMS</span>
35
+ </div>
36
+ <div class="topbar-user" id="topbar-user">
37
+ <!-- Populated after auth: name + role badge -->
38
+ </div>
39
+ <div class="topbar-actions" id="topbar-actions">
40
+ <!-- Populated after auth: Settings link + Sign out -->
41
+ </div>
42
+ </header>
43
+ ```
44
+
45
+ **Step 2: Verify**
46
+
47
+ Open `admin/index.html` and confirm the `<nav>` is gone and the `<header>` has the three child divs with correct IDs.
48
+
49
+ ---
50
+
51
+ ### Task 2: Update `admin/js/app.js` - remove navbar init, rewrite user injection
52
+
53
+ **Files:**
54
+ - Modify: `admin/js/app.js`
55
+
56
+ **Step 1: Remove the Domma navbar call**
57
+
58
+ Delete the entire `Domma.elements.navbar(...)` block (lines ~16-22 currently). It is no longer needed.
59
+
60
+ **Step 2: Rewrite `injectUserMenu()`**
61
+
62
+ Replace the current `injectUserMenu()` function with one that targets the new `#topbar-user` and `#topbar-actions` divs properly, and is fully idempotent:
63
+
64
+ ```javascript
65
+ function injectUserMenu() {
66
+ const user = getUser();
67
+ if (!user) return;
68
+ if ($('#topbar-user-name').length) return; // already injected
69
+
70
+ const roleLabelMap = {
71
+ admin: 'Admin', manager: 'Manager', editor: 'Editor', subscriber: 'Subscriber'
72
+ };
73
+ const roleLabel = roleLabelMap[user.role] || user.role;
74
+
75
+ $('#topbar-user').html(`
76
+ <span id="topbar-user-name" class="topbar-user-name">${escapeHtml(user.name)}</span>
77
+ <span class="topbar-role-badge topbar-role-badge--${user.role}">${roleLabel}</span>
78
+ `);
79
+
80
+ $('#topbar-actions').html(`
81
+ <a href="#/settings" class="topbar-action-link">
82
+ <span data-icon="settings"></span>
83
+ <span>Settings</span>
84
+ </a>
85
+ <button id="topbar-logout-btn" class="btn btn-sm btn-ghost topbar-signout">
86
+ <span data-icon="log-out"></span>
87
+ <span>Sign out</span>
88
+ </button>
89
+ `);
90
+
91
+ $('#topbar-logout-btn').on('click', () => logout());
92
+ Domma.icons.scan('#admin-topbar');
93
+ }
94
+ ```
95
+
96
+ **Step 3: Update the login page `onEnter` hide/show references**
97
+
98
+ Currently: `$('#admin-navbar').hide()` / `$('#admin-navbar').show()`
99
+ Change both to: `$('#admin-topbar').hide()` / `$('#admin-topbar').show()`
100
+
101
+ **Step 4: Verify**
102
+
103
+ Check `app.js` has no remaining references to `admin-navbar` or `Domma.elements.navbar`.
104
+
105
+ ---
106
+
107
+ ### Task 3: Add topbar CSS to `admin/css/admin.css`
108
+
109
+ **Files:**
110
+ - Modify: `admin/css/admin.css`
111
+
112
+ Add the following styles before the `/* Login / setup page */` section. The topbar must be exactly 60px tall - the sidebar already uses `top: '60px'` as its offset.
113
+
114
+ ```css
115
+ /* ── Admin Top Bar ──────────────────────────────── */
116
+ #admin-topbar {
117
+ height: 60px;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 1rem;
121
+ padding: 0 1.25rem;
122
+ background: var(--navbar-bg, rgba(0,0,0,.3));
123
+ border-bottom: 1px solid var(--border-color, rgba(255,255,255,.08));
124
+ position: sticky;
125
+ top: 0;
126
+ z-index: 200;
127
+ flex-shrink: 0;
128
+ }
129
+
130
+ .topbar-brand {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: .5rem;
134
+ font-weight: 700;
135
+ font-size: .95rem;
136
+ text-decoration: none;
137
+ color: inherit;
138
+ flex-shrink: 0;
139
+ margin-right: .5rem;
140
+ }
141
+
142
+ .topbar-brand span[data-icon],
143
+ .topbar-brand svg {
144
+ color: var(--primary, #5b8cff);
145
+ width: 22px;
146
+ height: 22px;
147
+ }
148
+
149
+ .topbar-brand-text {
150
+ opacity: .85;
151
+ }
152
+
153
+ /* User info - sits immediately after brand */
154
+ .topbar-user {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: .6rem;
158
+ flex: 1; /* pushes actions to RHS */
159
+ }
160
+
161
+ .topbar-user-name {
162
+ font-size: .875rem;
163
+ font-weight: 500;
164
+ }
165
+
166
+ .topbar-role-badge {
167
+ font-size: .7rem;
168
+ font-weight: 600;
169
+ letter-spacing: .04em;
170
+ text-transform: uppercase;
171
+ padding: .15rem .45rem;
172
+ border-radius: 4px;
173
+ background: var(--primary-alpha, rgba(91,140,255,.15));
174
+ color: var(--primary, #5b8cff);
175
+ }
176
+
177
+ /* Role-specific badge colours */
178
+ .topbar-role-badge--admin { background: rgba(91,140,255,.15); color: #5b8cff; }
179
+ .topbar-role-badge--manager { background: rgba(52,211,153,.15); color: #34d399; }
180
+ .topbar-role-badge--editor { background: rgba(251,191,36,.15); color: #fbbf24; }
181
+ .topbar-role-badge--subscriber { background: rgba(148,163,184,.15); color: #94a3b8; }
182
+
183
+ /* Actions - RHS */
184
+ .topbar-actions {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: .5rem;
188
+ flex-shrink: 0;
189
+ }
190
+
191
+ .topbar-action-link {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: .35rem;
195
+ font-size: .875rem;
196
+ color: var(--text-muted, #888);
197
+ text-decoration: none;
198
+ padding: .4rem .65rem;
199
+ border-radius: 6px;
200
+ transition: color .15s, background .15s;
201
+ }
202
+
203
+ .topbar-action-link:hover {
204
+ color: var(--text, #eee);
205
+ background: rgba(255,255,255,.06);
206
+ }
207
+
208
+ .topbar-action-link svg,
209
+ .topbar-action-link span[data-icon] {
210
+ width: 16px;
211
+ height: 16px;
212
+ }
213
+
214
+ .topbar-signout {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: .35rem;
218
+ font-size: .875rem;
219
+ }
220
+
221
+ .topbar-signout svg,
222
+ .topbar-signout span[data-icon] {
223
+ width: 16px;
224
+ height: 16px;
225
+ }
226
+ ```
227
+
228
+ **Step 5: Verify**
229
+
230
+ No visual verification needed at this step - just check CSS was added without breaking existing selectors.
231
+
232
+ ---
233
+
234
+ ### Task 4: Rewrite login/setup forms to use `F.render()`
235
+
236
+ **Files:**
237
+ - Modify: `admin/js/templates/login.html`
238
+ - Modify: `admin/js/views/login.js`
239
+
240
+ #### 4a - Simplify `login.html`
241
+
242
+ Replace the manual form inputs in `#setup-panel` and `#login-panel` with bare containers. The onboarding panels (site identity, theme picker, done) are **not** touched - they are wizard UI, not forms.
243
+
244
+ `#setup-panel` becomes:
245
+ ```html
246
+ <div id="setup-panel" style="display:none">
247
+ <h2 class="login-heading">Welcome - create your admin account</h2>
248
+ <p class="text-muted mb-4">First-time setup. Create your administrator account to get started.</p>
249
+ <div id="setup-form-container"></div>
250
+ </div>
251
+ ```
252
+
253
+ `#login-panel` becomes:
254
+ ```html
255
+ <div id="login-panel" style="display:none">
256
+ <h2 class="login-heading">Sign in</h2>
257
+ <div id="login-form-container"></div>
258
+ </div>
259
+ ```
260
+
261
+ #### 4b - Rewrite `login.js` - define blueprints and use `F.render()`
262
+
263
+ Define both blueprints at the module level (above the view export):
264
+
265
+ ```javascript
266
+ const setupBlueprint = {
267
+ name: { type: 'string', required: true, minLength: 1, label: 'Full name',
268
+ formConfig: { placeholder: 'Your name', autocomplete: 'name' } },
269
+ email: { type: 'email', required: true, label: 'Email address',
270
+ formConfig: { placeholder: 'admin@example.com', autocomplete: 'email' } },
271
+ password: { type: 'password', required: true, minLength: 8, label: 'Password (min. 8 characters)',
272
+ formConfig: { placeholder: '••••••••', autocomplete: 'new-password' } }
273
+ };
274
+
275
+ const loginBlueprint = {
276
+ email: { type: 'email', required: true, label: 'Email address',
277
+ formConfig: { placeholder: 'you@example.com', autocomplete: 'email' } },
278
+ password: { type: 'password', required: true, label: 'Password',
279
+ formConfig: { placeholder: '••••••••', autocomplete: 'current-password' } }
280
+ };
281
+ ```
282
+
283
+ Replace `bindSetup()` with an `F.render()` call:
284
+
285
+ ```javascript
286
+ function bindSetup($container) {
287
+ F.render('#setup-form-container', setupBlueprint, {}, {
288
+ layout: 'stacked',
289
+ submitText: 'Create admin account',
290
+ onSubmit: async (data) => {
291
+ try {
292
+ const result = await api.auth.setup(data);
293
+ setAuthData(result);
294
+ showStep($container, 'onboarding-site');
295
+ bindOnboardingSite($container);
296
+ } catch (err) {
297
+ E.toast(err.message || 'Setup failed. Please try again.', { type: 'danger' });
298
+ }
299
+ return false; // prevent default form reset
300
+ }
301
+ });
302
+ }
303
+ ```
304
+
305
+ Replace `bindLogin()` with an `F.render()` call:
306
+
307
+ ```javascript
308
+ function bindLogin($container) {
309
+ F.render('#login-form-container', loginBlueprint, {}, {
310
+ layout: 'stacked',
311
+ submitText: 'Sign in',
312
+ onSubmit: async (data) => {
313
+ try {
314
+ const result = await api.auth.login(data);
315
+ setAuthData(result);
316
+ R.navigate('/');
317
+ } catch (err) {
318
+ E.toast(err.message || 'Invalid credentials. Please try again.', { type: 'danger' });
319
+ }
320
+ return false;
321
+ }
322
+ });
323
+ }
324
+ ```
325
+
326
+ Remove the now-unused `showError()` helper and all manual `$container.find('#...-error')` / `val()` / `prop('disabled')` code - `F.render()` handles validation display and button state internally.
327
+
328
+ ---
329
+
330
+ ### Task 5: Check the `log-out` icon exists in Domma
331
+
332
+ **Files:** Read-only check.
333
+
334
+ The topbar actions use `data-icon="log-out"`. Domma's icon set (200+ icons) includes `log-out` in the Security category. If running and icon doesn't render, fall back to `data-icon="x-circle"` or `data-icon="power"`.
335
+
336
+ No code change needed unless icon is missing - verify visually after boot.
337
+
338
+ ---
339
+
340
+ ### Verification Checklist
341
+
342
+ 1. Start server: `npm start`
343
+ 2. Visit `http://localhost:4096/admin/`
344
+ 3. Redirects to `#/login` - login panel shows (F.render form, no raw inputs)
345
+ 4. Log in - topbar appears: `[layout icon] [Domma CMS] [name] [admin badge] ──── [⚙ Settings] [↩ Sign out]`
346
+ 5. Role badge colour matches role (admin = blue)
347
+ 6. Settings link navigates to `#/settings`
348
+ 7. Sign out clears tokens and returns to `#/login`
349
+ 8. On first run (no users): setup form via F.render → onboarding wizard → dashboard with topbar