domma-cms 1.6.37 → 1.6.38
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.
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/package.json +2 -2
- package/plugins/blog/CLAUDE.md +11 -1
- package/plugins/blog/lib/cache.js +43 -0
- package/plugins/blog/lib/page.js +28 -9
- package/plugins/blog/plugin.js +14 -13
- package/plugins/blog/plugin.json +1 -1
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/cache.test.js +65 -0
- package/plugins/booking/CLAUDE.md +14 -2
- package/plugins/booking/admin/lib/diary-layout.js +18 -0
- package/plugins/booking/admin/views/diary.js +153 -152
- package/plugins/booking/admin/views/people.js +64 -66
- package/plugins/booking/admin/views/settings.js +31 -32
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/admin-diary.test.js +9 -1
- package/plugins/feedback/CLAUDE.md +55 -0
- package/plugins/feedback/admin/css/index.css +16 -1
- package/plugins/feedback/admin/lib/kit.js +106 -0
- package/plugins/feedback/admin/lib/settings.js +107 -0
- package/plugins/feedback/admin/views/feedback.js +4 -1
- package/plugins/feedback/admin/views/inbox.js +11 -2
- package/plugins/feedback/admin/views/send.js +74 -7
- package/plugins/feedback/docs/guide.md +9 -9
- package/plugins/feedback/lib/attachments.js +133 -0
- package/plugins/feedback/lib/cache.js +43 -0
- package/plugins/feedback/lib/receiver.js +36 -6
- package/plugins/feedback/lib/sender.js +70 -6
- package/plugins/feedback/lib/shape.js +111 -6
- package/plugins/feedback/plugin.js +171 -17
- package/plugins/feedback/plugin.json +2 -2
- package/plugins/feedback/tests/api.test.js +125 -3
- package/plugins/feedback/tests/settings.test.js +59 -0
- package/plugins/feedback/tests/shape.test.js +64 -0
- package/plugins/free-tier.lock.json +73 -61
- package/plugins/mail-reader/CLAUDE.md +15 -4
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/shopping-cart/CLAUDE.md +28 -2
- package/plugins/shopping-cart/admin/css/index.css +3 -0
- package/plugins/shopping-cart/admin/views/orders.js +42 -10
- package/plugins/shopping-cart/admin/views/shop.js +1 -1
- package/plugins/shopping-cart/docs/guide.md +7 -3
- package/plugins/shopping-cart/lib/payments.js +15 -2
- package/plugins/shopping-cart/lib/refunds.js +51 -0
- package/plugins/shopping-cart/lib/sales.js +15 -8
- package/plugins/shopping-cart/lib/shop.js +38 -3
- package/plugins/shopping-cart/plugin.js +47 -19
- package/plugins/shopping-cart/plugin.json +2 -2
- package/plugins/shopping-cart/tests/api.test.js +52 -0
- package/plugins/shopping-cart/tests/commission.test.js +1 -1
- package/plugins/shopping-cart/tests/refunds.test.js +42 -0
- package/plugins/support/CLAUDE.md +51 -0
- package/plugins/support/admin/css/index.css +16 -1
- package/plugins/support/admin/lib/kit.js +192 -8
- package/plugins/support/admin/lib/settings.js +123 -0
- package/plugins/support/admin/views/desk.js +55 -12
- package/plugins/support/admin/views/inbox.js +17 -4
- package/plugins/support/admin/views/support.js +4 -1
- package/plugins/support/docs/guide.md +7 -9
- package/plugins/support/lib/attachments.js +133 -0
- package/plugins/support/lib/cache.js +43 -0
- package/plugins/support/lib/receiver.js +45 -10
- package/plugins/support/lib/sender.js +81 -10
- package/plugins/support/lib/shape.js +153 -12
- package/plugins/support/plugin.js +159 -29
- package/plugins/support/plugin.json +2 -2
- package/plugins/support/tests/api.test.js +125 -3
- package/plugins/support/tests/settings.test.js +68 -0
- package/plugins/support/tests/shape.test.js +51 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Domma Tools CSS v0.51.
|
|
2
|
+
* Domma Tools CSS v0.51.12
|
|
3
3
|
* Dynamic Object Manipulation & Modeling API
|
|
4
4
|
* (c) 2026 Darryl Waterhouse & DCBW-IT
|
|
5
|
-
* Built: 2026-10-
|
|
6
|
-
* Commit:
|
|
5
|
+
* Built: 2026-10-13T13:46:21.895Z
|
|
6
|
+
* Commit: ce9f5f1
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/*!
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Domma Tools v0.51.
|
|
2
|
+
* Domma Tools v0.51.12
|
|
3
3
|
* Developer tools: Theme Roller & Page Roller
|
|
4
4
|
* (c) 2026 Darryl Waterhouse & DCBW-IT
|
|
5
|
-
* Built: 2026-10-
|
|
6
|
-
* Commit:
|
|
5
|
+
* Built: 2026-10-13T13:46:07.193Z
|
|
6
|
+
* Commit: ce9f5f1
|
|
7
7
|
*
|
|
8
8
|
* Requires: domma.min.js
|
|
9
9
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domma-cms",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.38",
|
|
4
4
|
"description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/server.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@fastify/rate-limit": "^10.3.0",
|
|
85
85
|
"@fastify/static": "9.1.1",
|
|
86
86
|
"bcryptjs": "^3.0.3",
|
|
87
|
-
"domma-js": "0.51.
|
|
87
|
+
"domma-js": "0.51.12",
|
|
88
88
|
"dotenv": "^17.2.3",
|
|
89
89
|
"fastify": "5.8.5",
|
|
90
90
|
"gray-matter": "^4.0.3",
|
package/plugins/blog/CLAUDE.md
CHANGED
|
@@ -20,7 +20,8 @@ Public-facing blog for the CMS. Supports posts with categories, tags, and option
|
|
|
20
20
|
- `blocks/*.html|css` - the shipped layouts, copied into content/blocks/ once
|
|
21
21
|
- `public/blog.js`, `public/blog.css` - the browser half (listing, right-click menu, post extras)
|
|
22
22
|
- `public/samples/*.svg` - sample covers
|
|
23
|
-
- `tests/access.test.js`, `tests/comment-notices.test.js`, `tests/layouts.test.js`, `tests/nav-link.test.js`, `tests/notices.test.js`, `tests/public.test.js`
|
|
23
|
+
- `tests/access.test.js`, `tests/comment-notices.test.js`, `tests/layouts.test.js`, `tests/nav-link.test.js`, `tests/notices.test.js`, `tests/public.test.js`, `tests/cache.test.js`
|
|
24
|
+
- `lib/cache.js` - PURE: the 30 s read cache (forgotten on every write route)
|
|
24
25
|
- `admin/views/blog.js` - post list (KPIs + status/category/search filters)
|
|
25
26
|
- `admin/views/post-editor.js` - create / edit a post
|
|
26
27
|
- `admin/views/categories.js`, `admin/views/comments.js`, `admin/views/settings.js`
|
|
@@ -329,3 +330,12 @@ Declared in plugin.json `permissions` (granted once to `admin`), checked on EVER
|
|
|
329
330
|
tested), needs a plausible email, and refuses a post that does not exist or is not visible (404) - each of those
|
|
330
331
|
used to raise a staff notification.
|
|
331
332
|
- `ASSET_V` / the preview `assetV` are 1.9.6 (they were 1.6.x while the plugin was 1.9.5 - keep them with the version).
|
|
333
|
+
|
|
334
|
+
## 1.9.7 (2026-10-13 Tool sweep - caching)
|
|
335
|
+
|
|
336
|
+
- **Whole-collection reads are cached 30 s** (`lib/cache.js`, pure, `tests/cache.test.js`): `loadEntries()` in
|
|
337
|
+
lib/page.js (public pages, shortcodes, the sitemap source, the `/comments/pending-count` badge) and
|
|
338
|
+
`resolveAuthorName()` (one user lookup per author per window). The promise is kept, so concurrent misses share one
|
|
339
|
+
read. **Every non-GET route of this plugin forgets it in an `onSend` hook** - before the writer's response leaves -
|
|
340
|
+
so the admin's own next read, and the public POST /comments, are fresh. The TTL only covers writes made outside
|
|
341
|
+
the plugin (Collections tool, Blog Pro). Callers share the cached array: filter/sort copies, never mutate it.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A short memory for whole-collection reads.
|
|
3
|
+
*
|
|
4
|
+
* Every public page view and every sidebar badge poll (once a minute from
|
|
5
|
+
* every open admin tab) read ALL posts, categories and comments from the
|
|
6
|
+
* adapter. The data changes only when someone writes, so a read is kept for a
|
|
7
|
+
* little while and every write route of this plugin throws the memory away
|
|
8
|
+
* (`forget()`, from an onSend hook in plugin.js - before the writer's own
|
|
9
|
+
* response leaves, so their next read is fresh). The TTL is the safety net
|
|
10
|
+
* for writes this plugin never sees (the Collections tool, a Pro take-over).
|
|
11
|
+
*
|
|
12
|
+
* PURE - no I/O of its own; the loader is handed in. Tested in tests/cache.test.js.
|
|
13
|
+
*
|
|
14
|
+
* @module blog/lib/cache
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const CACHE_MS = 30_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{ttl?: number, clock?: () => number}} [opts]
|
|
21
|
+
* @returns {{get: (key: string, load: () => Promise<any>) => Promise<any>, forget: () => void, size: number}}
|
|
22
|
+
*/
|
|
23
|
+
export function createCache({ttl = CACHE_MS, clock = Date.now} = {}) {
|
|
24
|
+
// key -> {at, value: Promise}. The PROMISE is kept, not the result, so
|
|
25
|
+
// concurrent misses share one read, and a write landing while a read is in
|
|
26
|
+
// flight clears the map before that read can be stored - nothing stale
|
|
27
|
+
// survives a forget().
|
|
28
|
+
const kept = new Map();
|
|
29
|
+
return {
|
|
30
|
+
get(key, load) {
|
|
31
|
+
const hit = kept.get(key);
|
|
32
|
+
if (hit && clock() - hit.at < ttl) return hit.value;
|
|
33
|
+
const value = Promise.resolve().then(load);
|
|
34
|
+
const entry = {at: clock(), value};
|
|
35
|
+
kept.set(key, entry);
|
|
36
|
+
// A failed read is not remembered: the next caller tries again.
|
|
37
|
+
value.catch(() => { if (kept.get(key) === entry) kept.delete(key); });
|
|
38
|
+
return value;
|
|
39
|
+
},
|
|
40
|
+
forget() { kept.clear(); },
|
|
41
|
+
get size() { return kept.size; }
|
|
42
|
+
};
|
|
43
|
+
}
|
package/plugins/blog/lib/page.js
CHANGED
|
@@ -13,6 +13,13 @@ import defaultConfig from '../config.js';
|
|
|
13
13
|
import {fmtDate} from '../../_lib/admin/ui/dates.js';
|
|
14
14
|
import {buildToc, byNewest, cardVars, cleanBasePath, DEFAULT_POST_LAYOUT, DISPLAY_DEFAULTS, escapeHtml, interpolate, pickPostLayout, relatedPosts, safeImageUrl} from './layouts.js';
|
|
15
15
|
import {catalogue, loadBlock, renderCards, styleTag} from './render.js';
|
|
16
|
+
import {createCache} from './cache.js';
|
|
17
|
+
|
|
18
|
+
// Whole-collection reads and author names, kept a short while (lib/cache.js):
|
|
19
|
+
// every public page view and every badge poll read all posts, categories and
|
|
20
|
+
// comments. plugin.js calls forgetEntries() on every write route of this plugin.
|
|
21
|
+
const remembered = createCache();
|
|
22
|
+
export const forgetEntries = () => remembered.forget();
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Whether readers see a post. Blog Pro files posts under several blogs
|
|
@@ -69,12 +76,21 @@ export function liveBase(registered) {
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
/** Always returns the actual array of entries, regardless of adapter shape. */
|
|
72
|
-
|
|
79
|
+
async function readEntries(slug) {
|
|
73
80
|
const result = await listEntries(slug, {limit: 100000, sort: 'createdAt', order: 'desc'});
|
|
74
81
|
const arr = Array.isArray(result) ? result : (Array.isArray(result?.entries) ? result.entries : []);
|
|
75
82
|
return arr.map((entry) => ({id: entry.id, ...(entry.data ?? entry)}));
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A collection's entries, newest first - from the cache when a read is fresh.
|
|
87
|
+
* Callers get the SAME array, so none may change it in place (they filter and
|
|
88
|
+
* sort copies, as before).
|
|
89
|
+
*/
|
|
90
|
+
export function loadEntries(slug) {
|
|
91
|
+
return remembered.get(`entries:${slug}`, () => readEntries(slug));
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
/** Render a post body through the full Markdown + shortcode pipeline. */
|
|
79
95
|
export async function renderPostBody(content) {
|
|
80
96
|
if (!content) return {html: '', usedComponents: []};
|
|
@@ -87,14 +103,17 @@ export async function renderPostBody(content) {
|
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
/** A user's display name - one lookup per author per cache window, not per card. */
|
|
107
|
+
export function resolveAuthorName(authorId) {
|
|
108
|
+
if (!authorId) return Promise.resolve('Unknown');
|
|
109
|
+
return remembered.get(`author:${authorId}`, async () => {
|
|
110
|
+
try {
|
|
111
|
+
const user = await getUserById(authorId);
|
|
112
|
+
return user?.name || user?.email || 'Unknown';
|
|
113
|
+
} catch {
|
|
114
|
+
return 'Unknown';
|
|
115
|
+
}
|
|
116
|
+
});
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
/** One lookup per author, however many posts they wrote. */
|
package/plugins/blog/plugin.js
CHANGED
|
@@ -5,7 +5,7 @@ import {getPluginStates, savePluginState} from '../../server/services/plugins.js
|
|
|
5
5
|
import {renderBlogPage} from '../../server/services/renderer.js';
|
|
6
6
|
import {cleanBasePath, DISPLAY_DEFAULTS, pickDisplaySettings, safeImageUrl} from './lib/layouts.js';
|
|
7
7
|
import {catalogue, seedBlocks} from './lib/render.js';
|
|
8
|
-
import {liveBase, liveSettings, loadEntries as loadAll, renderPostPage, slugHolder} from './lib/page.js';
|
|
8
|
+
import {forgetEntries, liveBase, liveSettings, loadEntries as loadAll, renderPostPage, slugHolder} from './lib/page.js';
|
|
9
9
|
import {addNavLink, moveNavLink, removeNavLink} from './lib/nav-link.js';
|
|
10
10
|
import {SAMPLE_CATEGORIES, samplePosts} from './lib/samples.js';
|
|
11
11
|
import {commentKey, commentNotice} from './lib/notices.js';
|
|
@@ -167,17 +167,9 @@ function isVisible(post) {
|
|
|
167
167
|
|| (post.status === 'scheduled' && post.publishedAt && new Date(post.publishedAt) <= new Date());
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return arr.map((e) => ({ id: e.id, ...(e.data ?? e) }));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function loadCategories() {
|
|
177
|
-
const result = await listEntries(CATEGORIES_SLUG, { limit: 10000 });
|
|
178
|
-
const arr = Array.isArray(result) ? result : (result?.entries || []);
|
|
179
|
-
return arr.map((e) => ({ id: e.id, ...(e.data ?? e) }));
|
|
180
|
-
}
|
|
170
|
+
// The shortcodes run on public page views: the same cached reads as the pages.
|
|
171
|
+
const loadPosts = () => loadAll(POSTS_SLUG);
|
|
172
|
+
const loadCategories = () => loadAll(CATEGORIES_SLUG);
|
|
181
173
|
|
|
182
174
|
function selectPosts(posts, { limit, categorySlug, categoryMap }) {
|
|
183
175
|
let filtered = posts.filter(isVisible);
|
|
@@ -337,6 +329,15 @@ export default async function blogPlugin(fastify, options) {
|
|
|
337
329
|
});
|
|
338
330
|
}
|
|
339
331
|
|
|
332
|
+
// Every write route of this plugin (posts, categories, comments - the public
|
|
333
|
+
// POST /comments too - samples) retires the cached reads behind the public
|
|
334
|
+
// pages, the shortcodes and the badge. onSend, not onResponse: the memory is
|
|
335
|
+
// gone BEFORE the writer's response leaves, so their very next read is fresh.
|
|
336
|
+
// The hook is encapsulated - only this plugin's routes reach it.
|
|
337
|
+
fastify.addHook('onSend', async (request) => {
|
|
338
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') forgetEntries();
|
|
339
|
+
});
|
|
340
|
+
|
|
340
341
|
// The layouts are blocks; put the shipped ones where the Blocks tool can
|
|
341
342
|
// edit them. Once each, never over a site's own copy (lib/render.js).
|
|
342
343
|
try {
|
|
@@ -392,7 +393,7 @@ export default async function blogPlugin(fastify, options) {
|
|
|
392
393
|
});
|
|
393
394
|
const html = await renderBlogPage(
|
|
394
395
|
fileURLToPath(new URL('./templates/post.html', import.meta.url)),
|
|
395
|
-
{ post: page.html, assetV: '1.9.
|
|
396
|
+
{ post: page.html, assetV: '1.9.7' },
|
|
396
397
|
{ title: post.title || 'Preview', description: post.excerpt || '', ogImage: safeImageUrl(post.featuredImage),
|
|
397
398
|
usedComponents: page.usedComponents, urlPath: `${basePath}/${post.slug}`, noindex: true }
|
|
398
399
|
);
|
package/plugins/blog/plugin.json
CHANGED
|
@@ -24,7 +24,7 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
|
24
24
|
const TEMPLATES_DIR = path.join(__dirname, 'templates');
|
|
25
25
|
|
|
26
26
|
/** Bumped with the plugin, so a release is never served a stale script. */
|
|
27
|
-
const ASSET_V = '1.9.
|
|
27
|
+
const ASSET_V = '1.9.7';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Above this many posts a listing pages on the server instead: the browser
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog - the short memory behind the public pages and the badge (lib/cache.js),
|
|
3
|
+
* and the rule that matters: a write is seen by the very next read.
|
|
4
|
+
*/
|
|
5
|
+
import {test} from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import Fastify from 'fastify';
|
|
8
|
+
import {createCache} from '../lib/cache.js';
|
|
9
|
+
|
|
10
|
+
test('a read is shared for the cache window, then read again', async () => {
|
|
11
|
+
let now = 0;
|
|
12
|
+
let reads = 0;
|
|
13
|
+
const c = createCache({ttl: 30_000, clock: () => now});
|
|
14
|
+
const load = async () => { reads++; return ['post']; };
|
|
15
|
+
const [a, b] = await Promise.all([c.get('posts', load), c.get('posts', load)]);
|
|
16
|
+
assert.equal(reads, 1, 'concurrent misses share one read');
|
|
17
|
+
assert.equal(a, b, 'the same array - callers must not change it in place');
|
|
18
|
+
now = 29_999;
|
|
19
|
+
await c.get('posts', load);
|
|
20
|
+
assert.equal(reads, 1);
|
|
21
|
+
now = 30_000;
|
|
22
|
+
await c.get('posts', load);
|
|
23
|
+
assert.equal(reads, 2, 'the window passed: read again');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('forget() retires every key, and a read in flight when the write landed is not kept', async () => {
|
|
27
|
+
const c = createCache({ttl: 60_000});
|
|
28
|
+
let value = 'old';
|
|
29
|
+
let release;
|
|
30
|
+
const slow = () => new Promise(r => { release = r; });
|
|
31
|
+
const inFlight = c.get('posts', slow);
|
|
32
|
+
await c.get('categories', async () => ['cat']);
|
|
33
|
+
assert.equal(c.size, 2);
|
|
34
|
+
// The write finishes while the read is still on its way.
|
|
35
|
+
value = 'new';
|
|
36
|
+
c.forget();
|
|
37
|
+
release('old');
|
|
38
|
+
assert.equal(await inFlight, 'old', 'the caller who started before the write still gets what they read');
|
|
39
|
+
assert.equal(await c.get('posts', async () => value), 'new', 'the next read is fresh, not the stale one');
|
|
40
|
+
assert.deepEqual(await c.get('categories', async () => ['fresh']), ['fresh']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a failed read is not remembered', async () => {
|
|
44
|
+
const c = createCache();
|
|
45
|
+
await assert.rejects(() => c.get('posts', async () => { throw new Error('adapter down'); }), /adapter down/);
|
|
46
|
+
assert.equal(c.size, 0);
|
|
47
|
+
assert.deepEqual(await c.get('posts', async () => ['ok']), ['ok']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the plugin forgets on every write route, before the writer\'s response leaves', async () => {
|
|
51
|
+
// The hook as plugin.js registers it, on a stand-in app: the point under test
|
|
52
|
+
// is the method rule and that it fires in onSend (the writer's next read is fresh).
|
|
53
|
+
const c = createCache();
|
|
54
|
+
const app = Fastify();
|
|
55
|
+
app.addHook('onSend', async (request) => { if (request.method !== 'GET' && request.method !== 'HEAD') c.forget(); });
|
|
56
|
+
app.get('/posts', async () => c.get('posts', async () => ({at: Date.now()})));
|
|
57
|
+
app.put('/posts/1', async () => ({ok: true}));
|
|
58
|
+
await c.get('posts', async () => 'stale');
|
|
59
|
+
assert.equal(c.size, 1);
|
|
60
|
+
await app.inject({method: 'GET', url: '/posts'});
|
|
61
|
+
assert.equal(c.size, 1, 'a GET keeps the memory');
|
|
62
|
+
await app.inject({method: 'PUT', url: '/posts/1', payload: {}});
|
|
63
|
+
assert.equal(c.size, 0, 'a write retires it');
|
|
64
|
+
await app.close();
|
|
65
|
+
});
|
|
@@ -17,8 +17,8 @@ Calendar events block their times (read from `calendar-occurrences` / `calendar-
|
|
|
17
17
|
service - no import of the Calendar, which is a separate paid Tool).
|
|
18
18
|
|
|
19
19
|
`tier: free`, ships with the engine, disabled on arrival. Plan and phases: `dcms-marketplace/docs/booking-plan.md`.
|
|
20
|
-
Booking Pro (
|
|
21
|
-
pattern - it does not supersede it.
|
|
20
|
+
Booking Pro (marketplace `booking-pro`, 0.4.x) is an ADD-ON that registers into this one (`lib/extensions.js`), the
|
|
21
|
+
Cart / Cart Pro pattern - it does not supersede it. Neither takes payment yet.
|
|
22
22
|
|
|
23
23
|
1.6.0 (2026-09-30): "Add people" in an online booking's call (core with `server/services/callInvites.js` and
|
|
24
24
|
`_lib/call/invite.js`; both imported dynamically - lib/call.js by top-level `await import`, plugin.js beside
|
|
@@ -168,3 +168,15 @@ only to roles that hold it.
|
|
|
168
168
|
the run (restored after) and forges a Host to prove it.
|
|
169
169
|
- The API tests create contacts in domma-cms's `content/collections/contacts-contacts/data.json` (tracked) - they
|
|
170
170
|
snapshot and restore it; a crashed run can leave it changed: `git checkout` it in domma-cms.
|
|
171
|
+
|
|
172
|
+
## 1.6.4 (2026-10-13 Tool sweep, upgrades)
|
|
173
|
+
- The Diary is domma-reactive end to end: heads, hour gutter and columns are keyed `data-each` lists (`HEAD`, `HOUR`,
|
|
174
|
+
`COL` in diary.js; each column's open bands and blocks are `{{#each}}` inside it). One `draw` effect works out every
|
|
175
|
+
measure as a CSS length (`blockRow`) and the markup only binds; the empty state, "add a service" tip and first-run
|
|
176
|
+
checklist (`STEP`) are bound too. Kebab / right-click / Shift+F10 still come from `wireRows(gridBox, {match: '.bk-block'})`
|
|
177
|
+
(delegated, so re-rendered blocks need no rewiring). `mergeWindows` moved to admin/lib/diary-layout.js (tested).
|
|
178
|
+
- Settings > Closed days (`CLOSURE_ROW`) and a person's opening hours / days off (`DAY_ROW` with a nested
|
|
179
|
+
`{{#each windows}}`, `OFF_ROW`) are keyed lists of per-field observables; `hoursOut` / `daysOffOut` / `closuresOut`
|
|
180
|
+
turn them back into plain data for the dirty snapshot and the save. A "from" after the "to" still pulls "to" along.
|
|
181
|
+
- Icons in rows added after binding are drawn by `I.scan` after a flush (`rescan`), because new rows arrive after
|
|
182
|
+
the panel's first scan.
|
|
@@ -138,3 +138,21 @@ export function columnItems(bookings, {bookableId = ''} = {}) {
|
|
|
138
138
|
}
|
|
139
139
|
return [...groups.values()].map(g => ({...g, session: g.bookings.length > 1}));
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Opening windows (minutes) merged where they touch or overlap - the week view's one band per day for everyone.
|
|
144
|
+
* Moved here from diary.js (1.6.4) so it is tested with the rest of the geometry.
|
|
145
|
+
*
|
|
146
|
+
* @param {{from: number, to: number}[]} list
|
|
147
|
+
* @returns {{from: number, to: number}[]} sorted, non-overlapping copies
|
|
148
|
+
*/
|
|
149
|
+
export function mergeWindows(list) {
|
|
150
|
+
const sorted = [...(list || [])].sort((a, b) => a.from - b.from);
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const w of sorted) {
|
|
153
|
+
const last = out[out.length - 1];
|
|
154
|
+
if (last && w.from <= last.to) last.to = Math.max(last.to, w.to);
|
|
155
|
+
else out.push({...w});
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|