domma-cms 0.92.1 → 0.94.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 (177) hide show
  1. package/CLAUDE.md +5 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/app.js +2 -2
  4. package/admin/js/lib/action-editor-arrange.js +1 -1
  5. package/admin/js/lib/api-tokens-arrange.js +2 -2
  6. package/admin/js/lib/block-editor-arrange.js +1 -1
  7. package/admin/js/lib/blocks-arrange.js +1 -1
  8. package/admin/js/lib/collection-entries-arrange.js +1 -1
  9. package/admin/js/lib/components-arrange.js +1 -1
  10. package/admin/js/lib/dashboard-arrange.js +1 -1
  11. package/admin/js/lib/dates.js +1 -0
  12. package/admin/js/lib/forms-arrange.js +1 -1
  13. package/admin/js/lib/media-arrange.js +1 -1
  14. package/admin/js/lib/notifications-arrange.js +1 -1
  15. package/admin/js/lib/pages-arrange.js +1 -1
  16. package/admin/js/lib/related.js +1 -1
  17. package/admin/js/lib/timeline-builder.js +2 -2
  18. package/admin/js/templates/action-editor.html +6 -5
  19. package/admin/js/templates/actions-list.html +1 -1
  20. package/admin/js/templates/contacts.html +1 -1
  21. package/admin/js/templates/docs/api-actions.html +86 -60
  22. package/admin/js/templates/docs/api-authentication.html +159 -123
  23. package/admin/js/templates/docs/api-builder.html +197 -0
  24. package/admin/js/templates/docs/api-collections.html +199 -259
  25. package/admin/js/templates/docs/api-external.html +225 -0
  26. package/admin/js/templates/docs/api-forms.html +268 -0
  27. package/admin/js/templates/docs/api-layouts.html +70 -45
  28. package/admin/js/templates/docs/api-media.html +57 -80
  29. package/admin/js/templates/docs/api-navigation.html +66 -22
  30. package/admin/js/templates/docs/api-pages.html +109 -129
  31. package/admin/js/templates/docs/api-plugins.html +123 -61
  32. package/admin/js/templates/docs/api-scaffold.html +185 -0
  33. package/admin/js/templates/docs/api-settings.html +72 -64
  34. package/admin/js/templates/docs/api-users.html +74 -107
  35. package/admin/js/templates/docs/api-views.html +68 -54
  36. package/admin/js/templates/docs/components-howto.html +20 -17
  37. package/admin/js/templates/docs/components-reference.html +13 -16
  38. package/admin/js/templates/docs/components-rules.html +7 -6
  39. package/admin/js/templates/docs/components-walkthrough.html +19 -19
  40. package/admin/js/templates/docs/tutorial-crud.html +71 -40
  41. package/admin/js/templates/docs/tutorial-forms.html +51 -35
  42. package/admin/js/templates/docs/tutorial-plugin.html +132 -56
  43. package/admin/js/templates/docs/usage-actions.html +61 -15
  44. package/admin/js/templates/docs/usage-collections.html +108 -0
  45. package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
  46. package/admin/js/templates/docs/usage-dconfig.html +0 -3
  47. package/admin/js/templates/docs/usage-editions.html +213 -0
  48. package/admin/js/templates/docs/usage-media.html +22 -6
  49. package/admin/js/templates/docs/usage-navigation.html +74 -18
  50. package/admin/js/templates/docs/usage-pages.html +60 -20
  51. package/admin/js/templates/docs/usage-plugins.html +89 -17
  52. package/admin/js/templates/docs/usage-shortcodes.html +123 -70
  53. package/admin/js/templates/docs/usage-site-settings.html +50 -18
  54. package/admin/js/templates/docs/usage-tools.html +73 -0
  55. package/admin/js/templates/docs/usage-users-roles.html +99 -20
  56. package/admin/js/templates/docs/usage-views.html +36 -19
  57. package/admin/js/templates/documentation.html +153 -32
  58. package/admin/js/templates/page-editor.html +0 -5
  59. package/admin/js/templates/plugin-guide.html +15 -0
  60. package/admin/js/templates/plugin-guides.html +21 -0
  61. package/admin/js/templates/pro-docs.html +53 -234
  62. package/admin/js/templates/tutorials.html +5 -4
  63. package/admin/js/views/actions-list.js +3 -3
  64. package/admin/js/views/analytics.js +5 -5
  65. package/admin/js/views/api-endpoint-editor.js +2 -2
  66. package/admin/js/views/block-editor.js +4 -4
  67. package/admin/js/views/blocks.js +4 -4
  68. package/admin/js/views/collection-editor.js +4 -4
  69. package/admin/js/views/collection-entries.js +7 -7
  70. package/admin/js/views/component-editor.js +2 -2
  71. package/admin/js/views/contacts.js +22 -20
  72. package/admin/js/views/context-menu-editor.js +5 -5
  73. package/admin/js/views/doc-pages.js +1 -1
  74. package/admin/js/views/form-editor.js +4 -4
  75. package/admin/js/views/form-submissions.js +2 -2
  76. package/admin/js/views/index.js +1 -1
  77. package/admin/js/views/media.js +3 -3
  78. package/admin/js/views/menu-editor.js +13 -13
  79. package/admin/js/views/menu-locations.js +2 -2
  80. package/admin/js/views/my-profile.js +1 -1
  81. package/admin/js/views/page-editor.js +8 -8
  82. package/admin/js/views/plugin-guides.js +5 -0
  83. package/admin/js/views/project-detail.js +2 -2
  84. package/admin/js/views/project-settings.js +1 -1
  85. package/admin/js/views/role-editor.js +4 -4
  86. package/admin/js/views/search.js +2 -2
  87. package/admin/js/views/seo.js +17 -17
  88. package/admin/js/views/settings.js +3 -3
  89. package/admin/js/views/theme.js +3 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +2 -2
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/bin/cli.js +13 -13
  94. package/bin/lib/node-version.js +29 -0
  95. package/package.json +1 -1
  96. package/plugins/_lib/admin/mail/compose-window.js +3 -2
  97. package/plugins/_lib/admin/mail/reader-view.js +7 -6
  98. package/plugins/_lib/admin/mail/scheduling.js +4 -2
  99. package/plugins/_lib/admin/mail/templates.js +4 -4
  100. package/plugins/_lib/admin/ui/dates.js +85 -0
  101. package/plugins/blog/CLAUDE.md +31 -22
  102. package/plugins/blog/admin/views/blog.js +3 -2
  103. package/plugins/blog/admin/views/comments.js +2 -1
  104. package/plugins/blog/admin/views/post-editor.js +4 -4
  105. package/plugins/blog/blocks/blog-card-row.html +1 -1
  106. package/plugins/blog/blocks/blog-card.html +2 -2
  107. package/plugins/blog/blocks/blog-post-classic.html +2 -2
  108. package/plugins/blog/blocks/blog-post-essay.html +2 -2
  109. package/plugins/blog/blocks/blog-post-feature.html +2 -2
  110. package/plugins/blog/blocks/blog-post-minimal.html +2 -2
  111. package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
  112. package/plugins/blog/blocks/blog-post-split.html +2 -2
  113. package/plugins/blog/docs/guide.md +205 -0
  114. package/plugins/blog/lib/layouts.js +3 -3
  115. package/plugins/blog/lib/page.js +2 -1
  116. package/plugins/blog/plugin.js +3 -3
  117. package/plugins/blog/plugin.json +4 -4
  118. package/plugins/blog/tests/layouts.test.js +6 -0
  119. package/plugins/feedback/CLAUDE.md +22 -3
  120. package/plugins/feedback/admin/lib/kit.js +6 -7
  121. package/plugins/feedback/admin/views/feedback.js +79 -10
  122. package/plugins/feedback/admin/views/send.js +28 -6
  123. package/plugins/feedback/docs/guide.md +95 -0
  124. package/plugins/feedback/lib/receiver.js +9 -2
  125. package/plugins/feedback/lib/sender.js +3 -2
  126. package/plugins/feedback/plugin.js +54 -6
  127. package/plugins/feedback/plugin.json +4 -4
  128. package/plugins/feedback/tests/api.test.js +74 -2
  129. package/plugins/free-tier.lock.json +49 -44
  130. package/plugins/mail-reader/CLAUDE.md +33 -18
  131. package/plugins/mail-reader/docs/guide.md +147 -0
  132. package/plugins/mail-reader/plugin.json +1 -1
  133. package/plugins/security/CLAUDE.md +4 -1
  134. package/plugins/security/admin/views/security.js +5 -5
  135. package/plugins/security/docs/guide.md +170 -0
  136. package/plugins/security/plugin.js +2 -1
  137. package/plugins/security/plugin.json +2 -1
  138. package/plugins/shopping-cart/CLAUDE.md +7 -1
  139. package/plugins/shopping-cart/admin/lib/kit.js +5 -2
  140. package/plugins/shopping-cart/admin/views/orders.js +4 -4
  141. package/plugins/shopping-cart/admin/views/overview.js +2 -2
  142. package/plugins/shopping-cart/docs/guide.md +191 -0
  143. package/plugins/shopping-cart/lib/render.js +2 -1
  144. package/plugins/shopping-cart/plugin.json +3 -3
  145. package/public/js/collection-browser.js +2 -2
  146. package/public/js/site.js +1 -1
  147. package/scripts/gen-instance-secret.js +3 -1
  148. package/scripts/setup.js +3 -1
  149. package/server/middleware/auth.js +2 -1
  150. package/server/routes/api/actions.js +47 -27
  151. package/server/routes/api/blocks.js +2 -1
  152. package/server/routes/api/collections.js +16 -52
  153. package/server/routes/api/contacts.js +66 -3
  154. package/server/routes/api/documentation.js +42 -0
  155. package/server/routes/api/notifications.js +3 -2
  156. package/server/routes/api/users.js +10 -6
  157. package/server/server.js +16 -1
  158. package/server/services/actions.js +110 -34
  159. package/server/services/adapterRegistry.js +169 -16
  160. package/server/services/adapters/FileAdapter.js +25 -0
  161. package/server/services/adapters/MongoAdapter.js +23 -0
  162. package/server/services/collections.js +104 -1
  163. package/server/services/connectionManager.js +12 -0
  164. package/server/services/dates.js +81 -0
  165. package/server/services/docs.js +13 -2
  166. package/server/services/markdown.js +75 -26
  167. package/server/services/notification-sources.js +6 -5
  168. package/server/services/passwordReset.js +2 -1
  169. package/server/services/permissionRegistry.js +3 -2
  170. package/server/services/pluginGuides.js +255 -0
  171. package/server/services/pluginInstaller.js +54 -13
  172. package/server/services/plugins.js +29 -1
  173. package/server/services/presetCollections.js +31 -5
  174. package/server/services/renderer.js +2 -2
  175. package/server/services/sidebarBadges.js +3 -1
  176. package/server/services/tools.js +4 -2
  177. package/server/templates/page.html +2 -2
@@ -0,0 +1,170 @@
1
+ ---
2
+ title: Guide
3
+ order: 1
4
+ ---
5
+
6
+ Security protects your site with no set-up. It checks what is weak and tells you how to fix it, locks out repeated wrong passwords, enforces sensible password rules, logs every sign-in and turns away scanners. It is free and comes with Domma CMS, switched on.
7
+
8
+ ## What it does
9
+
10
+ - **Health check**: a list of checks, each with why it matters and how to fix it.
11
+ - **Sign-in lockout**: after repeated wrong passwords, the account or the address is locked for a while, longer each time.
12
+ - **Password rules**: new passwords must be long enough, not a common password, and not contain the person's name or email.
13
+ - **Sign-in log**: every sign-in, failed or successful, and every "Forgot your password?" request.
14
+ - **Scanner guard**: requests for things no Domma site has (WordPress, `.env` files, database dumps and the like) get an empty "not found". An address that keeps probing is blocked for a while.
15
+ - **Blocks**: block an address or range by hand, and see and clear lockouts and blocks.
16
+ - A badge on the Security sidebar entry: the number of health checks needing attention. It turns red only when a failing check is critical.
17
+
18
+ ## Getting started
19
+
20
+ Security is on from the start. To review it:
21
+
22
+ 1. Open [Security](#/plugins/security) in the sidebar.
23
+ 2. Read the **Health** card on the Overview tab. Click a check to see why it matters and how to fix it.
24
+ 3. Fix what you can. Use **Go there** in a check's menu where it offers one.
25
+ 4. Click the cog in the banner to review **Security settings**.
26
+
27
+ ## Screens
28
+
29
+ The screen has three tabs. The banner has **Run the checks again** and **Security settings** (the cog).
30
+
31
+ ### Overview
32
+
33
+ - **Health**: a score and the list of checks, grouped under People, Sign-in, Server, Web and Plugins.
34
+ - Tiles: **Need attention**, **Failed sign-ins, 24h**, **Blocked now**, **Probes refused today** and **Reset requests, 24h**.
35
+
36
+ Right-click a check for **Why, and how to fix it**, **Go there**, **Run the checks again** and **Copy**.
37
+
38
+ The checks include:
39
+
40
+ | Group | Checks |
41
+ |---|---|
42
+ | People | Unused admin accounts, How many admins, Accounts never used |
43
+ | Sign-in | Sign-in lockout, Password rules, Two-factor sign-in, Email for resets and alerts |
44
+ | Server | Domma CMS up to date, Production mode, Visitor addresses, Listening address |
45
+ | Web | HTTPS certificate, Security headers, HTTPS, Private files not served |
46
+ | Plugins | Plugin licences |
47
+
48
+ A check the plugin cannot confirm shows as information, never as a pass. Results are kept for 10 minutes; **Run the checks again** refreshes them.
49
+
50
+ ### Sign-ins
51
+
52
+ - Filters: **All**, **Failed**, **Succeeded** and **Resets**.
53
+ - Period: **Last 24 hours**, **Last 7 days**, **Last 30 days** or **All kept**.
54
+ - A search box for email, address or browser.
55
+ - **CSV** downloads what is shown.
56
+
57
+ Right-click an entry for **Only this address**, **Unlock this account**, **Block this address...**, **Copy the address** and **Copy the browser**.
58
+
59
+ The Resets filter lists "Forgot your password?" requests and what happened to each.
60
+
61
+ ### Blocked
62
+
63
+ - **Locked after wrong passwords**: current lockouts. **Unlock all** clears every one. Right-click one to **Unlock** it.
64
+ - **Blocked for probing**: addresses blocked by the scanner guard. Right-click for **Unblock** or **Block for good**.
65
+ - **Blocked by hand**: your own blocks. Enter an **Address or range** and an optional reason, then click **Block**. Right-click a block for **Remove the block**.
66
+
67
+ An address can be written as:
68
+
69
+ ```text
70
+ 81.2.69.142 one address
71
+ 192.168.1.* a trailing wildcard
72
+ 10.0.0.0/8 an IPv4 range
73
+ 2a00:23c7:* an IPv6 prefix
74
+ ```
75
+
76
+ If a block would cover your own address, you are warned first. The server itself cannot be blocked. Blocked addresses get an empty "forbidden" for every request.
77
+
78
+ ## Settings
79
+
80
+ Click the cog in the banner to open **Security settings**. Click **Save** (or press Ctrl+Enter).
81
+
82
+ ### Sign-in lockout
83
+
84
+ | Setting | Default |
85
+ |---|---|
86
+ | Lock after repeated wrong passwords | On |
87
+ | Wrong passwords on one account | 5 |
88
+ | Wrong passwords from one address | 20 |
89
+ | Counted over (minutes) | 15 |
90
+ | First lock (minutes) | 1. Each lock after that is twice as long |
91
+ | Longest lock (minutes) | 60 |
92
+
93
+ While locked, even the right password does not get in. The message never says whether the account exists. A successful sign-in clears the account's count.
94
+
95
+ ### Password rules
96
+
97
+ | Setting | Default |
98
+ |---|---|
99
+ | Check new passwords | On |
100
+ | Shortest password | 10 (8 to 64) |
101
+ | Refuse the 10,000 most common passwords | On |
102
+ | Refuse passwords containing the person's name or email | On |
103
+
104
+ Passwords are checked when they are set or changed. Existing passwords keep working until then. While the rules are on, a password that uses only one or two different characters (such as `aaaaaaaaaa` or `abababababab`) is always refused.
105
+
106
+ ### Scanners
107
+
108
+ | Setting | Default |
109
+ |---|---|
110
+ | Refuse probes, and block addresses that keep probing | On |
111
+ | Probes before a block | 5 |
112
+ | Counted over (minutes) | 10 |
113
+ | Block for (minutes) | 60 |
114
+ | Also treat as probes | Your own extra paths, one per line, starting with `/` |
115
+ | This site does serve | Paths that look like probes but are real on your site, for example `/legacy/` |
116
+
117
+ An address someone has signed in from in the last 7 days is never blocked automatically. Requests from the server itself are never touched.
118
+
119
+ ### Keeping and telling
120
+
121
+ | Setting | Default |
122
+ |---|---|
123
+ | Keep the sign-in log (days) | 30 (7 to 365) |
124
+ | Admin unused after (days) | 90. The health check flags admins who have not signed in for this long |
125
+ | Notify when an account or address is locked | On |
126
+ | Notify when someone signs in after many wrong passwords | On |
127
+ | Notify when a scanner is blocked | Off |
128
+ | Notify when password resets look like someone trying addresses | On |
129
+
130
+ The last one fires when one address asks about 3 different accounts within an hour, or when 10 requests in an hour name accounts that do not exist.
131
+
132
+ ## Locked out yourself
133
+
134
+ If you cannot sign in because of a lockout or a block, someone with access to the server can clear it. In the site's folder, run:
135
+
136
+ ```text
137
+ node plugins/security/bin/unlock.js --list
138
+ node plugins/security/bin/unlock.js --all
139
+ node plugins/security/bin/unlock.js --ip 81.2.69.142
140
+ node plugins/security/bin/unlock.js --user sam@example.com
141
+ ```
142
+
143
+ `--list` shows what is locked or blocked. `--all` clears everything. `--ip` clears one address and `--user` one account. The site notices within a few seconds, with no restart.
144
+
145
+ ## Permissions and roles
146
+
147
+ In System > Roles, Security adds the permission **Security** with two actions:
148
+
149
+ | Action | Label | Meaning | Granted by default to |
150
+ |---|---|---|---|
151
+ | read | View | See the health check, sign-ins and blocks | Admin |
152
+ | manage | Manage | Change settings; block, unblock and unlock | Admin |
153
+
154
+ Super Admins always have both. Someone with View only sees the screens, but the unlock and block actions are switched off.
155
+
156
+ Security adds no roles.
157
+
158
+ ## Tips
159
+
160
+ - Keep the health check clear. The sidebar badge tells you when something needs attention.
161
+ - Give only a few people admin rights. The health check flags admin accounts that are not used.
162
+ - Set up outgoing email under Settings > Email, so password resets and alerts can be sent.
163
+ - On a site behind a proxy, check **Visitor addresses** in the health check. If the site cannot see real visitor addresses, lockouts and blocks apply to the proxy's address.
164
+ - The CSV export is safe to open in a spreadsheet: cells that could run as formulas are neutralised.
165
+
166
+ ## Limitations
167
+
168
+ - Lockout and password rules need Domma CMS 0.83 or later. On older versions the screen says so and only the scanner guard and health check work.
169
+ - Reset requests are only logged on Domma CMS 0.88 or later.
170
+ - No two-factor sign-in. **Security Pro** adds it, along with more protection. When Security Pro is installed it takes over from Security.
@@ -24,6 +24,7 @@ import path from 'node:path';
24
24
  import {fileURLToPath} from 'node:url';
25
25
  import fp from 'fastify-plugin';
26
26
  import defaultConfig from './config.js';
27
+ import {fmtDateTime} from '../_lib/admin/ui/dates.js';
27
28
  import {createState} from './server/state.js';
28
29
  import {gatherFacts} from './server/facts.js';
29
30
  import {evaluate} from './admin/lib/health.js';
@@ -155,7 +156,7 @@ async function security(fastify, options = {}) {
155
156
  const who = locks.account ? `the account ${e.email}` : `the address ${ip}`;
156
157
  notify({source: 'lockouts', severity: 'warning', title: `Sign-in locked: ${locks.account ? e.email : ip}`,
157
158
  dedupeKey: `lock:${locks.account ? e.email : ip}`,
158
- body: `Too many wrong passwords for ${who} (last from ${ip || 'an unknown address'}). Locked until ${new Date(locks.account || locks.address).toLocaleTimeString('en-GB')}.`,
159
+ body: `Too many wrong passwords for ${who} (last from ${ip || 'an unknown address'}). Locked until ${fmtDateTime(locks.account || locks.address)}.`,
159
160
  link: '#/plugins/security/blocked'});
160
161
  }
161
162
  } catch (err) {
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "security",
3
3
  "displayName": "Security",
4
- "version": "1.1.0",
4
+ "version": "1.1.3",
5
5
  "tier": "free",
6
6
  "description": "Keeps a Domma CMS site safe: a health check of what is weak and how to fix it, sign-in lockout after repeated wrong passwords, password rules, a log of every sign-in, and blocking of the scanners that probe every site for WordPress, .env files and the like.",
7
7
  "author": "Domma CMS",
8
8
  "date": "2026-09-27",
9
+ "minCmsVersion": "0.83.0",
9
10
  "icon": "shield",
10
11
  "permissions": [
11
12
  {
@@ -8,11 +8,17 @@ pages with a gallery; a basket kept in the shopper's browser that follows them r
8
8
  button + drawer on every page); one-page checkout; Stripe (hosted Checkout), PayPal (Orders v2) or payment
9
9
  offline; a receipt page that waits for the payment to be confirmed; orders in the admin with fulfil (tracking),
10
10
  refund (through the provider), cancel, packing slips; an Overview with takings and what needs doing; a sidebar
11
- badge for paid orders to fulfil; sample products with bundled pictures.
11
+ badge for paid orders to fulfil; sample products with bundled pictures. 1.0.2 (2026-09-27): every date a person sees
12
+ (order lists, packing slips, the receipt page, the Overview chart) reads dd/mm/yyyy through
13
+ `plugins/_lib/admin/ui/dates.js`.
12
14
 
13
15
  `tier: free`. Shopping Cart Pro (plugins/shopping-cart-pro) is an ADD-ON that plugs into this one - it does not
14
16
  supersede it (see Extensions).
15
17
 
18
+ **The free Cart sends no email at all** - no order confirmation to the shopper, no dispatch notice, nothing to the
19
+ shop. The receipt page (its token URL) is the shopper's record. Order and dispatch emails come with Shopping Cart
20
+ Pro. Say so to a site owner who expects them.
21
+
16
22
  ## The one rule about money
17
23
 
18
24
  Every amount is an integer in MINOR units (pence) - products, orders, settings (`shippingFlat: 395` = £3.95).
@@ -8,6 +8,9 @@
8
8
  */
9
9
 
10
10
  import {formatMoney, fromMinor, toMinor} from '/plugins/shopping-cart/public/lib/money.js';
11
+ import {fmtDate, fmtDateTime} from '/plugins/_lib/admin/ui/dates.js';
12
+
13
+ export {fmtDate, fmtDateTime};
11
14
 
12
15
  export const SIDEBAR_URL = '#/plugins/shopping-cart';
13
16
  export const BASE = '/api/plugins/shopping-cart';
@@ -102,7 +105,7 @@ export const moneyOut = (text) => {
102
105
  return m === null ? NaN : m;
103
106
  };
104
107
 
105
- /** "3 min ago", "26 Sep". */
108
+ /** "3 min ago", "26/09/2026". */
106
109
  export function when(iso) {
107
110
  const t = new Date(iso).getTime();
108
111
  if (!Number.isFinite(t)) return '';
@@ -111,7 +114,7 @@ export function when(iso) {
111
114
  if (s < 3600) return `${Math.round(s / 60)} min ago`;
112
115
  if (s < 86400) return `${Math.round(s / 3600)} h ago`;
113
116
  if (s < 86400 * 6) return `${Math.round(s / 86400)} d ago`;
114
- return new Date(t).toLocaleDateString(undefined, {day: 'numeric', month: 'short', ...(s > 86400 * 300 ? {year: 'numeric'} : {})});
117
+ return fmtDate(t);
115
118
  }
116
119
 
117
120
  // ---- Slideovers ---------------------------------------------------------------------
@@ -9,7 +9,7 @@
9
9
  * slip. A notification's link (#/plugins/shopping-cart/orders/<id>) opens one.
10
10
  */
11
11
 
12
- import {api, copy, entriesOf, esc, money, openPanel, refreshBadge, store, when, wireRows} from '../lib/kit.js';
12
+ import {api, copy, entriesOf, esc, fmtDate, fmtDateTime, money, openPanel, refreshBadge, store, when, wireRows} from '../lib/kit.js';
13
13
  import {NEXT, STATUS_LABEL, STATUS_TONE, addressLines, orderLabel} from '/plugins/shopping-cart/public/lib/orders.js';
14
14
 
15
15
  const FILTERS = [
@@ -217,7 +217,7 @@ export async function mount(root, ctx) {
217
217
  <style>body{font:14px/1.5 system-ui,sans-serif;margin:2.5rem;color:#111}h1{font-size:1.5rem;margin:0}table{width:100%;border-collapse:collapse;margin:1.5rem 0}
218
218
  th,td{text-align:left;padding:.5rem;border-bottom:1px solid #ddd}td.q{width:4rem;text-align:center;font-weight:700}.box{display:flex;justify-content:space-between;gap:2rem}
219
219
  .muted{color:#666}.note{border:1px dashed #999;padding:.75rem;margin-top:1rem}</style></head><body>
220
- <div class="box"><div><h1>${esc(settings.shopTitle || 'Shop')}</h1><p class="muted">Packing slip · ${esc(orderLabel(d))} · ${esc(new Date(o.meta?.createdAt).toLocaleDateString())}</p></div>
220
+ <div class="box"><div><h1>${esc(settings.shopTitle || 'Shop')}</h1><p class="muted">Packing slip · ${esc(orderLabel(d))} · ${esc(fmtDate(o.meta?.createdAt))}</p></div>
221
221
  <div><strong>${esc(d.customer?.name || '')}</strong><br>${d.delivery === 'collect' ? 'Collecting in person' : addr}</div></div>
222
222
  <table><thead><tr><th>Qty</th><th>Item</th><th>SKU</th></tr></thead><tbody>
223
223
  ${(d.lines || []).map(l => `<tr><td class="q">${l.qty}</td><td>${esc(l.name)}${l.variantName ? ` - ${esc(l.variantName)}` : ''}</td><td class="muted">${esc(l.sku || '')}</td></tr>`).join('')}
@@ -275,7 +275,7 @@ ${(d.lines || []).map(l => `<tr><td class="q">${l.qty}</td><td>${esc(l.name)}${l
275
275
  const html = `<div class="shp-order">
276
276
  <div class="shp-order-head">
277
277
  <span class="shp-chip is-${STATUS_TONE[d.status]}" data-part="status">${esc(STATUS_LABEL[d.status])}</span>
278
- <span class="shp-muted">${esc(new Date(o.meta?.createdAt).toLocaleString())} · ${esc(PROVIDER[d.provider] || d.provider)}</span>
278
+ <span class="shp-muted">${esc(fmtDateTime(o.meta?.createdAt))} · ${esc(PROVIDER[d.provider] || d.provider)}</span>
279
279
  <span class="shp-spacer"></span>
280
280
  <div class="shp-order-acts" data-part="acts"></div>
281
281
  <button type="button" class="shp-kebab" data-part="more" aria-label="More">⋮</button>
@@ -303,7 +303,7 @@ ${(d.lines || []).map(l => `<tr><td class="q">${l.qty}</td><td>${esc(l.name)}${l
303
303
  ${ext.length ? `<div class="shp-ext">${ext.map(x => `<span class="shp-chip is-muted">${esc(x.k)}: ${esc(String(x.v))}</span>`).join('')}</div>` : ''}
304
304
  <h4>History</h4>
305
305
  <ol class="shp-history">${(d.history || []).slice().reverse().map(h => `<li><span class="shp-chip is-${STATUS_TONE[h.status] || 'muted'}">${esc(STATUS_LABEL[h.status] || h.status)}</span>
306
- <span>${esc(new Date(h.at).toLocaleString())} · ${esc(h.by || '')}${h.note ? ` - ${esc(h.note)}` : ''}</span></li>`).join('')}</ol>
306
+ <span>${esc(fmtDateTime(h.at))} · ${esc(h.by || '')}${h.note ? ` - ${esc(h.note)}` : ''}</span></li>`).join('')}</ol>
307
307
  </div>`;
308
308
  panel = await openPanel({
309
309
  title: `Order ${orderLabel(d)}`,
@@ -7,7 +7,7 @@
7
7
  * steps to its first sale instead.
8
8
  */
9
9
 
10
- import {api, esc, money, when} from '../lib/kit.js';
10
+ import {api, esc, fmtDate, money, when} from '../lib/kit.js';
11
11
  import {STATUS_LABEL, STATUS_TONE} from '/plugins/shopping-cart/public/lib/orders.js';
12
12
 
13
13
  export async function mount(root, ctx) {
@@ -167,7 +167,7 @@ function drawChart(el, series, scope) {
167
167
  const bw = Math.max(2, slot - 2);
168
168
  const y = (v) => padT + (H - padT - padB) * (1 - v / top);
169
169
  const ticks = [0, top / 2, top];
170
- const day = (k) => new Date(`${k}T12:00:00`).toLocaleDateString(undefined, {day: 'numeric', month: 'short'});
170
+ const day = (k) => fmtDate(k);
171
171
  const bars = series.map((d, i) => {
172
172
  const x = padL + i * slot + 1;
173
173
  const h = Math.max(0, H - padB - y(d.revenue));
@@ -0,0 +1,191 @@
1
+ ---
2
+ title: Guide
3
+ order: 1
4
+ ---
5
+
6
+ Sell from your own site. Add products, choose how you get paid, and run orders from the Shop in the admin sidebar. Shopping Cart is free.
7
+
8
+ ## What it does
9
+
10
+ - A storefront at `/shop` (you can move it) with category filters, and search and sort once there are more than four products, and a page for each product.
11
+ - Products with pictures, a "was" price, stock counts and options (for example sizes or colours), each option with its own price, SKU and stock.
12
+ - A basket that follows shoppers round the whole site, with a floating basket button.
13
+ - A one-page checkout at `/shop/cart` that takes payment by Stripe, PayPal or offline (for example bank transfer).
14
+ - A receipt page for each order that the shopper can come back to.
15
+ - Orders in the admin: mark paid, fulfil with tracking, refund, cancel, print a packing slip.
16
+ - A badge on the Shop sidebar item showing paid orders waiting to be sent.
17
+
18
+ The free Shopping Cart sends no emails at all - not to the shopper and not to you. The receipt page is the shopper's record of the order. Order confirmation and dispatch emails come with Shopping Cart Pro.
19
+
20
+ ## Getting started
21
+
22
+ 1. Open [Shop](#/plugins/shopping-cart) in the sidebar. A new shop shows "Open your shop" with three steps.
23
+ 2. Click **Add a product**, or **or try samples** to load six sample products with pictures, options and stock. You can edit or delete the samples later.
24
+ 3. Click **Payment settings** and turn on at least one way to pay (see Settings below). Pay offline is on by default.
25
+ 4. Click **See the shop** to check it, then link to `/shop` from your menu or put `[shop-products /]` on any page.
26
+ 5. If your site sits behind a proxy or on an unusual port, set your site's address first: [Site Settings](#/settings) > General > **Site URL** (for example `https://example.com`). Stripe and PayPal send shoppers back to this address after they pay. Without it the Shop guesses the address from the request, which can be wrong behind a proxy.
27
+
28
+ ## Screens
29
+
30
+ The Shop has three tabs under the banner: Overview, Orders and Products. Shopping Cart Pro adds tabs of its own to this row. The banner has three buttons: the cog (Shop settings), open the shop in a new tab, and + (Add a product, on the Products tab).
31
+
32
+ ### Overview
33
+
34
+ - **Today**, **Last 30 days** (compared with the 30 days before), **Average order** and **To fulfil**. Click To fulfil to go to the orders.
35
+ - **Takings, last 30 days** - a bar per day. Hover a bar to see the day, the takings and the number of orders. Only paid and fulfilled orders count; refunded and cancelled orders are left out.
36
+ - **Needs you** - paid orders to send, orders awaiting payment, products low in stock or sold out, checkouts started this week and not paid, and a reminder if you only take offline payment.
37
+ - **Latest orders** and **Best sellers** (by takings over the last 30 days).
38
+
39
+ ### Orders
40
+
41
+ 1. Pick a filter: **To do**, **To fulfil**, **Awaiting payment**, **Fulfilled**, **Started**, **Cancelled & refunded** or **All**. Each shows a count.
42
+ 2. Search by order number, name, email or item.
43
+ 3. Click an order to open it. Right-click a row (or use the ⋮ button, or Shift+F10) for its actions.
44
+
45
+ An open order shows the items and totals, the shopper, the delivery address and tracking, the shopper's note, a **Staff note** only you can see (saved when you click away), the payment reference and the order's history.
46
+
47
+ Order statuses:
48
+
49
+ | Status | Meaning |
50
+ |---|---|
51
+ | Checkout started | The shopper went to pay and has not finished. No stock is held. |
52
+ | Awaiting payment | An offline order. Nothing has been charged. |
53
+ | Paid - to fulfil | Paid. Stock has been taken. Ready to send. |
54
+ | Fulfilled | Sent, delivered or collected. |
55
+ | Cancelled | Never paid, and closed. |
56
+ | Refunded | Paid, then given back. |
57
+
58
+ Actions:
59
+
60
+ - **Mark paid** - for an offline order when the money arrives. Stock is taken at this point.
61
+ - **Mark fulfilled** (or **Mark collected**) - add a carrier, tracking number and tracking link if you have them. The shopper sees the tracking on their receipt page.
62
+ - **Back to "to fulfil"** - reopen a fulfilled order.
63
+ - **Refund** - see below.
64
+ - **Cancel order** - only for orders that were never paid.
65
+ - **Packing slip** - opens a printable slip with the items, SKUs, address and the shopper's note. Allow pop-ups for your site if nothing opens.
66
+ - **Copy receipt link** - the shopper's receipt page address.
67
+ - **Email the shopper** - opens your own email program.
68
+ - **Delete** - only for Checkout started, Awaiting payment and Cancelled orders. A paid or refunded order cannot be deleted.
69
+
70
+ Refunds:
71
+
72
+ - Refunds are full refunds only. You cannot refund part of an order.
73
+ - A Stripe or PayPal order is refunded through Stripe or PayPal first. The order is only marked refunded once they accept it.
74
+ - An offline order is only marked refunded. Give the money back yourself, the way it came.
75
+ - Tick **Put the items back in stock** to return the stock.
76
+
77
+ ### Products
78
+
79
+ 1. Pick a filter: **All**, **On sale**, **Drafts**, **Low stock** or **Archived**.
80
+ 2. Click a product to edit it, or click **Product** (or + in the banner) to add one.
81
+ 3. Right-click a row (or ⋮) to **Edit**, **View in the shop**, **Duplicate**, **Put on sale**, **Back to draft**, **Feature**, set stock, **Archive** or **Delete**.
82
+
83
+ Only products **On sale** appear in the shop. **Archive** hides a product but keeps it. Deleting a product does not change past orders; they keep their own copy.
84
+
85
+ The product form:
86
+
87
+ - **Name**, **Address** (the last part of the product's web address), **Pictures** (from Media; the first is shown on the card, the second on hover; drag to reorder).
88
+ - **Price** and **Was** (an earlier, higher price, shown struck through with a Sale flag).
89
+ - **Summary** (on the card and the top of the product page) and **Description** (Markdown and shortcodes work).
90
+ - **Category**, **Tags** (comma-separated), **Status** (On sale, Draft - hidden, Archived) and **Featured** (shown first, with a Featured flag).
91
+ - **Options & stock** - tick **Count stock** to track stock; it then sells out at 0. Add options with **Add an option**, each with a name, price (empty uses the product's price), SKU and stock. **Options are** names them (for example Size).
92
+ - **Delivery & tax** - untick **Needs delivering** for downloads, services and tickets (no delivery charge, no address asked). **Weight (g)** and **Tax class** are used by Shopping Cart Pro.
93
+
94
+ Ctrl+Enter saves the form.
95
+
96
+ ## Settings
97
+
98
+ Click the cog in the Shop banner to open **Shop settings**. It has four sections. Click **Save** when done. Changing the currency or the shop address reloads the page.
99
+
100
+ ### Storefront
101
+
102
+ - **Shop name**, **Address** (default `/shop`; takes effect at once, and old links stop working), **Introduction**.
103
+ - **Currency** - changing it does not convert prices you have entered.
104
+ - **Products per row** (2, 3 or 4).
105
+ - **Category filters**, **Search box** (shown once there are more than four products), **Basket button on every page**, **Say "Only 3 left"**.
106
+ - **Basket button side** (Right or Left), **Low stock at**.
107
+ - **Terms of sale page** - if set, shoppers must tick "I agree to the terms of sale" before paying.
108
+
109
+ ### Delivery
110
+
111
+ - **We deliver to** - the countries checkout offers. Products that need no delivery can be bought from anywhere.
112
+ - **Delivery charge** - one charge per order, and what it is **Called**.
113
+ - **Free over** - orders at or above this amount go free. 0 means never.
114
+ - **Offer collection** - shoppers can collect in person, with no charge and no address.
115
+
116
+ ### Tax
117
+
118
+ - **Tax rate %** - one rate for every product. Leave at 0 for no tax.
119
+ - **Called** (for example VAT).
120
+ - **My prices include tax** - on: the price is what the shopper pays, and the receipt shows the tax inside it. Off: tax is added at checkout.
121
+ - **Tax the delivery charge too**.
122
+
123
+ ### Payment
124
+
125
+ Turn on one or more:
126
+
127
+ - **Stripe** - cards, Apple Pay and Google Pay on Stripe's own page.
128
+ 1. In Stripe, go to Developers > API keys and copy the secret key (`sk_test_` for testing, `sk_live_` for real).
129
+ 2. Paste it in **Secret key**.
130
+ 3. Optional but recommended: in Stripe, go to Developers > Webhooks, add the **Webhook** address shown in the settings (use the copy button) for the event `checkout.session.completed`, then paste its signing secret in **Webhook signing secret**. This confirms a payment even if the shopper closes the tab before coming back.
131
+ 4. Click **Test the key**.
132
+ - **PayPal** - needs a PayPal Business account and a REST app.
133
+ 1. Choose **Mode**: Sandbox (testing) or Live.
134
+ 2. Paste the **Client ID** and **Secret** from developer.paypal.com > Apps & Credentials > your app.
135
+ 3. Click **Test the keys**.
136
+ - **Pay offline** - bank transfer or cash on collection. The order is placed as Awaiting payment and nothing is charged. Set what it is **Called** and **What to tell the shopper** (for example your bank details). This shows on the shopper's receipt with the order number and total.
137
+
138
+ Keys are kept on the server. Once saved they show as dots and are never shown again. Leave the dots alone to keep the saved key.
139
+
140
+ ## Shortcodes
141
+
142
+ Put products on any page.
143
+
144
+ A grid of products (all options are optional; `limit` is 1 to 48, default 8):
145
+
146
+ ```text
147
+ [shop-products /]
148
+ [shop-products category="Mugs" limit="8" columns="4" /]
149
+ [shop-products tag="gift" /]
150
+ [shop-products featured="true" /]
151
+ ```
152
+
153
+ One product card:
154
+
155
+ ```text
156
+ [shop-product slug="blue-mug" /]
157
+ ```
158
+
159
+ An add-to-basket button with the price. For a product with options it links to the product page instead, so the shopper can choose:
160
+
161
+ ```text
162
+ [buy-button slug="blue-mug" label="Buy now" /]
163
+ ```
164
+
165
+ Only products On sale are shown. A page built from these can show out-of-date stock until it is next rendered, but checkout always checks stock and prices again.
166
+
167
+ ## Permissions and roles
168
+
169
+ Set in System > Roles, under Plugins:
170
+
171
+ | Permission | Action | Lets someone |
172
+ |---|---|---|
173
+ | Shop | See | Open the Shop and read its orders and products |
174
+ | Shop | Manage | Edit products, move and refund orders, change the Shop's settings |
175
+
176
+ The admin role has both by default.
177
+
178
+ ## Tips
179
+
180
+ - Test with Stripe's `sk_test_` key or PayPal Sandbox before going live.
181
+ - Notifications: a notice arrives for each new paid order and for each offline order awaiting payment. A Super Admin can switch the "Shop orders" source off in the Notifications settings.
182
+ - Stock is taken when an order is paid, not when checkout starts. Started checkouts hold no stock and can be deleted.
183
+ - Order numbers start at 1001.
184
+ - If a shopper's basket changes while they check out (something sold out, fewer left), checkout stops and shows the new total before taking payment.
185
+
186
+ ## Limitations
187
+
188
+ - No emails. Shopping Cart Pro adds order and dispatch emails.
189
+ - Refunds are full refunds only.
190
+ - One flat delivery charge and one tax rate. Shopping Cart Pro adds shipping zones, rates by weight or order value, tax by country and tax class, and discount codes.
191
+ - Behind a proxy, set Site URL in Site Settings or Stripe and PayPal may send shoppers back to the wrong address.
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import {formatMoney} from '../public/lib/money.js';
14
+ import {fmtDateTime} from '../../_lib/admin/ui/dates.js';
14
15
  import {available, priceRange, safeImage} from '../public/lib/products.js';
15
16
  import {COUNTRIES, STATUS_PUBLIC, addressLines} from '../public/lib/orders.js';
16
17
 
@@ -274,7 +275,7 @@ export function receiptHtml({order, settings, base, problem = ''}) {
274
275
  <section><h2>Items</h2><ul class="shop-r-lines">${lines}</ul>${totals}</section>
275
276
  <section class="shop-r-side">
276
277
  ${addr.length ? `<h2>${order.delivery === 'collect' ? 'Collection' : 'Delivering to'}</h2><address>${addr.map(esc).join('<br>')}</address>` : order.delivery === 'collect' ? `<h2>Collection</h2><p>${esc(settings.collectionLabel || 'Collect in person')}</p>` : ''}
277
- <h2>Placed</h2><p>${esc(new Date(order.placedAt).toLocaleString('en-GB', {dateStyle: 'long', timeStyle: 'short'}))}</p>
278
+ <h2>Placed</h2><p>${esc(fmtDateTime(order.placedAt))}</p>
278
279
  <p><a class="shop-linkbtn" href="${esc(base)}">Continue shopping</a> · <button type="button" class="shop-linkbtn" data-shop-print>Print</button></p>
279
280
  </section>
280
281
  </div>
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "shopping-cart",
3
3
  "displayName": "Shopping Cart",
4
- "version": "1.0.0",
4
+ "version": "1.0.3",
5
5
  "tier": "free",
6
6
  "description": "Sell from your site: products with options, stock and pictures, a storefront and a cart that follows shoppers from page to page, and checkout with Stripe, PayPal or payment offline. Orders to fulfil in the sidebar, refunds from the order.",
7
7
  "author": "Domma CMS",
8
- "date": "2026-09-26",
8
+ "date": "2026-09-27",
9
9
  "icon": "shopping-bag",
10
10
  "permissions": [
11
11
  {
@@ -44,7 +44,7 @@
44
44
  ],
45
45
  "views": {
46
46
  "plugin-shopping-cart": {
47
- "entry": "shopping-cart/admin/views/shop.js?v=1.0.0",
47
+ "entry": "shopping-cart/admin/views/shop.js?v=1.0.2",
48
48
  "exportName": "shopView"
49
49
  }
50
50
  }