domma-cms 0.75.0 → 0.77.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 (150) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/action-editor-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
  6. package/admin/js/lib/api-tokens-arrange.js +3 -0
  7. package/admin/js/lib/block-editor-arrange.js +2 -0
  8. package/admin/js/lib/collection-editor-arrange.js +3 -0
  9. package/admin/js/lib/collection-entries-arrange.js +1 -0
  10. package/admin/js/lib/component-editor-arrange.js +27 -0
  11. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  12. package/admin/js/lib/dashboard-arrange.js +1 -0
  13. package/admin/js/lib/effects-arrange.js +3 -0
  14. package/admin/js/lib/form-editor-arrange.js +1 -0
  15. package/admin/js/lib/form-submissions-arrange.js +2 -0
  16. package/admin/js/lib/layouts-arrange.js +1 -0
  17. package/admin/js/lib/menu-editor-arrange.js +2 -0
  18. package/admin/js/lib/my-profile-arrange.js +1 -0
  19. package/admin/js/lib/notifications-arrange.js +1 -0
  20. package/admin/js/lib/page-editor-arrange.js +1 -0
  21. package/admin/js/lib/plugin-code-arrange.js +1 -0
  22. package/admin/js/lib/plugins-arrange.js +2 -0
  23. package/admin/js/lib/project-page-arrange.js +1 -0
  24. package/admin/js/lib/role-editor-arrange.js +3 -0
  25. package/admin/js/lib/roles-arrange.js +1 -0
  26. package/admin/js/lib/search-settings-arrange.js +1 -0
  27. package/admin/js/lib/sidebar-renderer.js +2 -2
  28. package/admin/js/lib/site-settings-arrange.js +1 -0
  29. package/admin/js/lib/user-editor-arrange.js +1 -0
  30. package/admin/js/lib/users-arrange.js +1 -0
  31. package/admin/js/lib/view-editor-arrange.js +14 -0
  32. package/admin/js/templates/action-editor.html +223 -147
  33. package/admin/js/templates/api-endpoint-editor.html +172 -117
  34. package/admin/js/templates/api-tokens.html +83 -10
  35. package/admin/js/templates/block-editor.html +194 -203
  36. package/admin/js/templates/collection-editor.html +191 -113
  37. package/admin/js/templates/collection-entries.html +99 -36
  38. package/admin/js/templates/component-editor.html +128 -126
  39. package/admin/js/templates/context-menu-editor.html +208 -148
  40. package/admin/js/templates/dashboard.html +184 -22
  41. package/admin/js/templates/effects.html +155 -72
  42. package/admin/js/templates/form-editor.html +41 -26
  43. package/admin/js/templates/form-submissions.html +78 -26
  44. package/admin/js/templates/layouts.html +126 -43
  45. package/admin/js/templates/menu-editor.html +57 -17
  46. package/admin/js/templates/menu-locations.html +54 -11
  47. package/admin/js/templates/my-profile.html +100 -12
  48. package/admin/js/templates/notifications.html +70 -10
  49. package/admin/js/templates/page-editor.html +47 -26
  50. package/admin/js/templates/plugin-code.html +91 -73
  51. package/admin/js/templates/plugins.html +100 -28
  52. package/admin/js/templates/project-detail.html +83 -145
  53. package/admin/js/templates/project-settings.html +88 -53
  54. package/admin/js/templates/role-editor.html +158 -83
  55. package/admin/js/templates/roles.html +78 -7
  56. package/admin/js/templates/search.html +193 -216
  57. package/admin/js/templates/settings.html +404 -472
  58. package/admin/js/templates/user-editor.html +150 -14
  59. package/admin/js/templates/users.html +69 -9
  60. package/admin/js/templates/view-editor.html +203 -180
  61. package/admin/js/templates/view-preview.html +45 -45
  62. package/admin/js/views/action-editor.js +7 -1
  63. package/admin/js/views/api-endpoint-editor.js +2 -3
  64. package/admin/js/views/api-tokens.js +62 -8
  65. package/admin/js/views/block-editor.js +14 -8
  66. package/admin/js/views/collection-editor.js +64 -5
  67. package/admin/js/views/collection-entries.js +50 -1
  68. package/admin/js/views/component-editor.js +2 -28
  69. package/admin/js/views/context-menu-editor.js +54 -55
  70. package/admin/js/views/dashboard.js +7 -1
  71. package/admin/js/views/effects.js +31 -1
  72. package/admin/js/views/form-editor.js +7 -7
  73. package/admin/js/views/form-submissions.js +11 -1
  74. package/admin/js/views/index.js +1 -1
  75. package/admin/js/views/layouts.js +81 -1
  76. package/admin/js/views/login.js +2 -2
  77. package/admin/js/views/menu-editor.js +21 -43
  78. package/admin/js/views/menu-locations.js +16 -1
  79. package/admin/js/views/my-profile.js +1 -1
  80. package/admin/js/views/notifications.js +18 -1
  81. package/admin/js/views/page-editor.js +26 -26
  82. package/admin/js/views/plugin-code.js +23 -4
  83. package/admin/js/views/plugins.js +97 -39
  84. package/admin/js/views/project-detail.js +9 -1
  85. package/admin/js/views/project-settings.js +1 -1
  86. package/admin/js/views/role-editor.js +11 -3
  87. package/admin/js/views/roles.js +49 -4
  88. package/admin/js/views/search.js +7 -1
  89. package/admin/js/views/settings.js +13 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +13 -4
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/admin/js/views/view-preview.js +1 -1
  94. package/package.json +1 -1
  95. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  96. package/plugins/_lib/admin/mail/contacts.js +4 -1
  97. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  98. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  99. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  100. package/plugins/_lib/admin/mail/mail.css +1 -1
  101. package/plugins/_lib/admin/mail/mail.html +1 -1
  102. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  103. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  104. package/plugins/_lib/mail/readRoutes.js +43 -1
  105. package/plugins/_lib/mail/unreadBadge.js +42 -0
  106. package/plugins/free-tier.lock.json +5 -5
  107. package/plugins/mail-reader/CLAUDE.md +1 -0
  108. package/plugins/mail-reader/plugin.js +3 -1
  109. package/plugins/mail-reader/plugin.json +5 -4
  110. package/server/routes/api/api-tokens.js +56 -0
  111. package/server/routes/api/auth.js +7 -1
  112. package/server/routes/api/collections.js +13 -1
  113. package/server/routes/api/dashboard.js +32 -4
  114. package/server/routes/api/effects.js +68 -13
  115. package/server/routes/api/forms.js +104 -13
  116. package/server/routes/api/layouts.js +93 -2
  117. package/server/routes/api/notifications.js +140 -14
  118. package/server/routes/api/plugins.js +135 -4
  119. package/server/routes/api/roles.js +111 -0
  120. package/server/routes/api/search.js +38 -4
  121. package/server/routes/api/settings.js +52 -0
  122. package/server/routes/api/users.js +65 -3
  123. package/server/routes/api/views.js +55 -1
  124. package/server/server.js +12 -0
  125. package/server/services/components.js +9 -0
  126. package/server/services/plugins.js +7 -0
  127. package/server/services/roles.js +3 -0
  128. package/server/services/search.js +58 -0
  129. package/server/services/users.js +16 -2
  130. package/server/services/views.js +14 -0
  131. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  132. package/admin/js/templates/dashboard/cache.html +0 -32
  133. package/admin/js/templates/dashboard/health-detail.html +0 -2
  134. package/admin/js/templates/dashboard/journeys.html +0 -17
  135. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  136. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  137. package/admin/js/templates/dashboard/top-pages.html +0 -3
  138. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  139. package/admin/js/templates/plugin-marketplace.html +0 -29
  140. package/admin/js/views/block-editor-enhance.js +0 -1
  141. package/admin/js/views/dashboard/lib/escape.js +0 -1
  142. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  143. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  144. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  145. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  146. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  147. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  148. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  149. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
  150. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -303,6 +303,15 @@ export async function getComponent(name) {
303
303
  } catch { /* return malformed source for editing; UI flags it */ }
304
304
  return {name, source, props, bundled, ...(meta && {meta})};
305
305
  } catch (err) {
306
+ // A plugin's component has no file: return its source, read-only
307
+ // (saveComponent/deleteComponent refuse it as PLUGIN_OWNED). The list's
308
+ // "View source" opened the editor on it and got a 404.
309
+ const pc = err.code === 'ENOENT' && getPluginComponents().find(c => c.name === name);
310
+ if (pc) {
311
+ let props = {};
312
+ try { props = validatePropsJson(parseDmcSource(pc.source).props); } catch { /* flagged by the UI */ }
313
+ return {name, source: pc.source, props, bundled: true, origin: 'plugin', readOnly: true};
314
+ }
306
315
  if (err.code === 'ENOENT') {
307
316
  const nf = new Error('Component not found');
308
317
  nf.code = 'ENOENT';
@@ -266,6 +266,13 @@ async function loadConfigDefaults(name) {
266
266
  if (_configDefaultsCache.has(name)) return _configDefaultsCache.get(name);
267
267
  const configJsPath = path.join(PLUGINS_DIR, name, 'config.js');
268
268
  let defaults = {};
269
+ // No config.js is "no defaults", not a failure: the Marketplace asks for every
270
+ // installed plugin's settings (enabled or not), and recording a load failure for
271
+ // one without the file would put it on the health report for being looked at.
272
+ try { await fs.access(configJsPath); } catch {
273
+ _configDefaultsCache.set(name, defaults);
274
+ return defaults;
275
+ }
269
276
  try {
270
277
  const {default: exported} = await import(configJsPath);
271
278
  defaults = exported || {};
@@ -26,6 +26,9 @@ const USERS_DIR = path.resolve(path.dirname(COLLECTIONS_DIR), 'users');
26
26
  /** Base role slugs that should always exist */
27
27
  const BASE_ROLE_NAMES = ['super-admin', 'admin', 'user'];
28
28
 
29
+ /** The seeded roles every site relies on - never deletable, whatever route is asked. */
30
+ export const isBaseRole = (name) => BASE_ROLE_NAMES.includes(name);
31
+
29
32
  /** Role slugs that were seeded by the old 4-role system - used to detect legacy data */
30
33
  const DEFUNCT_ROLE_NAMES = ['manager', 'editor', 'subscriber'];
31
34
 
@@ -411,3 +411,61 @@ export function searchPages(pages, query, {maxResults = 10, onError = () => {}}
411
411
  results.sort((a, b) => b.score - a.score);
412
412
  return results.slice(0, maxResults);
413
413
  }
414
+
415
+ // ---------------------------------------------------------------------------
416
+ // The admin screen: coverage, and the sidebar badge
417
+ // ---------------------------------------------------------------------------
418
+
419
+ /**
420
+ * Which pages search can return, and which it leaves out - the same rule
421
+ * searchPages() applies (drafts and private pages are never results).
422
+ *
423
+ * @param {object[]} pages
424
+ * @returns {{searchable: number, drafts: number, private: number, total: number}}
425
+ */
426
+ export function searchCoverage(pages = []) {
427
+ let drafts = 0;
428
+ let priv = 0;
429
+ for (const p of pages) {
430
+ if (p?.status === 'draft') drafts++;
431
+ else if (p?.visibility === 'private') priv++;
432
+ }
433
+ return {searchable: pages.length - drafts - priv, drafts, private: priv, total: pages.length};
434
+ }
435
+
436
+ /**
437
+ * The sidebar badge on System > Search (its countUrl, GET /api/search/_count):
438
+ * how many pages a visitor's search can find, with whether search is on at all
439
+ * and how it is reached in the popover. An inventory, not a signal - plain text
440
+ * throughout, the popover sets it with textContent.
441
+ *
442
+ * @param {object} settings - normalised search settings
443
+ * @param {object[]} pages
444
+ */
445
+ export function searchBadge(settings = SEARCH_DEFAULTS, pages = []) {
446
+ const s = normaliseSearchSettings(settings);
447
+ const c = searchCoverage(pages);
448
+ const pl = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
449
+ const trigger = s.displayMode === 'floating'
450
+ ? `Floating button, ${s.position.replace('-', ' ')}`
451
+ : 'Icon in the navigation bar';
452
+ return {
453
+ count: c.searchable,
454
+ label: s.enabled
455
+ ? `Search is on - ${pl(c.searchable, 'page')} searchable`
456
+ : `Search is off - ${pl(c.searchable, 'page')} would be searchable`,
457
+ tone: 'info',
458
+ details: {
459
+ title: 'Site search',
460
+ rows: [
461
+ {label: 'Status', value: s.enabled ? 'On' : 'Off'},
462
+ {label: 'Trigger', value: s.enabled ? trigger : '-'},
463
+ {label: 'Keyboard shortcut', value: s.enabled && s.keyboardShortcut ? 'Ctrl+K / ⌘K' : 'Off'},
464
+ {label: 'Searchable pages', value: String(c.searchable)},
465
+ {label: 'Left out (drafts, private)', value: String(c.drafts + c.private)}
466
+ ],
467
+ items: [],
468
+ empty: s.enabled ? 'Visitors can search every published, public page.' : 'Search is switched off - nothing is added to public pages.'
469
+ }
470
+ };
471
+ }
@@ -124,7 +124,7 @@ export async function getUserByEmail(email) {
124
124
  * @param {object} data - { name, email, password, role }
125
125
  * @returns {Promise<object>} Created user (password stripped)
126
126
  */
127
- export async function createUser({ name, email, password, role = 'user', additionalRoles = [], meta, projects }) {
127
+ export async function createUser({ name, email, password, role = 'user', additionalRoles = [], meta, projects, isActive = true }) {
128
128
  const existing = await getUserByEmail(email);
129
129
  if (existing) throw new Error('A user with that email already exists');
130
130
 
@@ -144,7 +144,7 @@ export async function createUser({ name, email, password, role = 'user', additio
144
144
  // Project access-scope: when non-empty, the user is restricted to artefacts
145
145
  // tagged with one of these project slugs (see canSeeArtefact in projects.js).
146
146
  projects: Array.isArray(projects) ? projects.filter(Boolean) : [],
147
- isActive: true,
147
+ isActive: isActive !== false,
148
148
  createdAt: now,
149
149
  updatedAt: now,
150
150
  lastLogin: null,
@@ -168,6 +168,20 @@ export async function updateUser(id, updates) {
168
168
  const user = await readUserFile(id);
169
169
  if (!user) throw new Error('User not found');
170
170
 
171
+ // An email is how an account signs in: two accounts must never share one.
172
+ // createUser always refused a taken address; an update did not check at all.
173
+ if (typeof updates.email === 'string' && updates.email.trim()) {
174
+ updates.email = updates.email.toLowerCase().trim();
175
+ if (updates.email !== String(user.email || '').toLowerCase()) {
176
+ const other = await getUserByEmail(updates.email);
177
+ if (other && other.id !== user.id) {
178
+ const err = new Error('A user with that email already exists');
179
+ err.code = 'EMAIL_TAKEN';
180
+ throw err;
181
+ }
182
+ }
183
+ }
184
+
171
185
  if (updates.password) {
172
186
  updates.password = await bcrypt.hash(updates.password, config.auth.bcryptRounds);
173
187
  }
@@ -452,7 +452,21 @@ async function executeFileView(view, { page, limit, user }) {
452
452
  export async function executeView(slug, {page = 1, limit = 25, user = null} = {}) {
453
453
  const view = await getView(slug);
454
454
  if (!view) throw new Error(`View "${slug}" not found`);
455
+ return executeViewConfig(view, {page, limit, user});
456
+ }
455
457
 
458
+ /**
459
+ * Execute a view config that need not be saved - the view editor's live
460
+ * Results run the pipeline being edited. Same validation and the same two
461
+ * engines (native aggregation for a MongoDB source, the JS evaluator
462
+ * otherwise) as executeView.
463
+ *
464
+ * @param {{connection?: string, pipeline: {source: string, stages?: object[]}, access?: object}} view
465
+ * @param {{page?: number, limit?: number, user?: object|null}} [opts]
466
+ * @returns {Promise<{ results: object[], total: number, page: number, limit: number }>}
467
+ */
468
+ export async function executeViewConfig(view, {page = 1, limit = 25, user = null} = {}) {
469
+ if (!view?.pipeline?.source) throw new Error('pipeline.source is required');
456
470
  validateStages(view.pipeline?.stages || []);
457
471
 
458
472
  // Dispatch on the SOURCE collection's storage adapter. A mongodb-declared
@@ -1,3 +0,0 @@
1
- <h3>Recent activity</h3>
2
- <div data-field="rows"></div>
3
- <p class="dash-empty" hidden>No recent activity.</p>
@@ -1,32 +0,0 @@
1
- <div class="dash-card-header">
2
- <h3>Response Cache</h3>
3
- <label class="dash-cache-toggle" title="Enable / disable cache">
4
- <input type="checkbox" data-field="toggle">
5
- <span data-field="toggle-label">…</span>
6
- </label>
7
- </div>
8
- <div class="dash-cache-grid">
9
- <div class="dash-cache-stat">
10
- <div class="dash-cache-stat-label">Hit rate</div>
11
- <div class="dash-cache-stat-value" data-field="hit-rate">-</div>
12
- <div class="dash-cache-stat-sub" data-field="hits-misses">no traffic yet</div>
13
- </div>
14
- <div class="dash-cache-stat">
15
- <div class="dash-cache-stat-label">Entries</div>
16
- <div class="dash-cache-stat-value" data-field="size">-</div>
17
- <div class="dash-cache-stat-sub" data-field="size-bar"><span data-field="size-fill"></span></div>
18
- </div>
19
- <div class="dash-cache-stat">
20
- <div class="dash-cache-stat-label">Writes</div>
21
- <div class="dash-cache-stat-value" data-field="writes">0</div>
22
- <div class="dash-cache-stat-sub" data-field="invalidations">0 invalidations</div>
23
- </div>
24
- <div class="dash-cache-stat">
25
- <div class="dash-cache-stat-label">Last cleared</div>
26
- <div class="dash-cache-stat-value" data-field="last-cleared">never</div>
27
- <div class="dash-cache-stat-sub" data-field="driver">driver: -</div>
28
- </div>
29
- </div>
30
- <div class="dash-cache-actions">
31
- <button class="btn btn-sm btn-ghost" data-field="clear-btn"><span data-icon="trash"></span> Clear cache</button>
32
- </div>
@@ -1,2 +0,0 @@
1
- <h3>Health</h3>
2
- <div data-field="rows"></div>
@@ -1,17 +0,0 @@
1
- <h3>User journeys - last 7 days</h3>
2
- <div class="dash-journey-grid">
3
- <div>
4
- <h4>Top entry pages</h4>
5
- <div data-field="entry"></div>
6
- </div>
7
- <div>
8
- <h4>Top exit pages</h4>
9
- <div data-field="exit"></div>
10
- </div>
11
- <div>
12
- <h4>Top paths</h4>
13
- <div data-field="paths"></div>
14
- </div>
15
- </div>
16
- <p class="dash-bounce">Bounce rate: <strong data-field="bounce">-</strong></p>
17
- <p class="dash-empty" hidden>No journey data yet - encourage some traffic and check back.</p>
@@ -1,34 +0,0 @@
1
- <div class="dash-card dash-kpi">
2
- <span class="dash-kpi-icon" data-icon="trending-up"></span>
3
- <div>
4
- <div class="dash-kpi-value" data-field="hits-today">-</div>
5
- <div class="dash-kpi-label">Hits today</div>
6
- <div class="dash-kpi-delta" data-field="hits-delta"></div>
7
- </div>
8
- </div>
9
- <div class="dash-card dash-kpi">
10
- <span class="dash-kpi-icon" data-icon="users"></span>
11
- <div>
12
- <div class="dash-kpi-value" data-field="active-sessions">-</div>
13
- <div class="dash-kpi-label">Active sessions (5m)</div>
14
- </div>
15
- </div>
16
- <div class="dash-card dash-kpi">
17
- <span class="dash-kpi-icon" data-icon="inbox"></span>
18
- <div>
19
- <div class="dash-kpi-value" data-field="submissions-week">-</div>
20
- <div class="dash-kpi-label">Submissions this week</div>
21
- </div>
22
- </div>
23
- <div class="dash-card dash-kpi">
24
- <span class="dash-kpi-icon" data-icon="activity"></span>
25
- <div>
26
- <div class="dash-kpi-value">
27
- <span class="dash-health-pill" data-field="health-pill" data-level="ok">
28
- <span data-icon="circle"></span>
29
- <span data-field="health-label">OK</span>
30
- </span>
31
- </div>
32
- <div class="dash-kpi-label">Site health</div>
33
- </div>
34
- </div>
@@ -1,3 +0,0 @@
1
- <h3>Spikes (last hour)</h3>
2
- <div data-field="spike-list"></div>
3
- <p class="dash-empty" hidden>No anomalies detected.</p>
@@ -1,3 +0,0 @@
1
- <h3>Top pages - last 7 days</h3>
2
- <div data-field="rows"></div>
3
- <p class="dash-empty" hidden>No traffic recorded yet.</p>
@@ -1,3 +0,0 @@
1
- <h3>Traffic - last 30 days</h3>
2
- <div class="dash-chart-wrap"><canvas id="dash-traffic-canvas"></canvas></div>
3
- <p class="dash-empty" hidden>No traffic recorded yet.</p>
@@ -1,29 +0,0 @@
1
- <div class="view-header">
2
- <h2><span data-icon="store"></span> Browse</h2>
3
- <p class="text-muted">Plugins available to this site.</p>
4
- </div>
5
-
6
- <!--
7
- Shown when this site is not managed. Telling an unmanaged customer to set
8
- MANAGER_URL is telling them to configure something they will never have: a
9
- standalone purchase arrives as a file, and Install from File is the whole
10
- procedure. The managed path is mentioned second because it is not theirs to
11
- set up.
12
- -->
13
- <div id="marketplace-unavailable" style="display:none">
14
- <div class="card">
15
- <div class="card-body">
16
- <p><strong>This site installs plugins from a file.</strong></p>
17
- <p class="text-muted">Buy a plugin and you are sent a single <code>.dcmsplugin</code> file. Use
18
- <strong>Install from File</strong> above to install it - no connection to anything is needed, and the
19
- same button takes an update later.</p>
20
- <p class="text-muted" style="margin-bottom:0">Sites managed for you browse and install here instead, over their
21
- manager connection.</p>
22
- </div>
23
- </div>
24
- </div>
25
-
26
- <div id="marketplace-available" style="display:none">
27
- <p id="marketplace-site" class="text-muted" style="display:none;font-size:.85rem;margin:0 0 .75rem"></p>
28
- <div id="marketplace-grid" class="card-grid"></div>
29
- </div>
@@ -1 +0,0 @@
1
- import{blockEditorView as r}from"./block-editor.js";import{createSimpleEditor as u}from"../lib/simple-editor.js";const p=r.onMount;r.onMount=async function(e){await p.call(this,e);const n=e.find(".editor-pane--write").get(0),l=e.find("#block-line-numbers").get(0),t=e.find("#block-content").get(0);if(!n||!t||n.dataset.dmEnhanced==="1")return;n.dataset.dmEnhanced="1",l&&(l.style.display="none"),t.style.display="none",n.style.display="block",n.style.position="relative",n.style.overflow="hidden";const i=document.createElement("div");i.style.cssText="width:100%;height:100%;",n.appendChild(i);const a=u(i,{initialValue:t.value,onChange:o=>{t.value!==o&&(t.value=o,t.dispatchEvent(new Event("input",{bubbles:!0})))}}),d=Object.getPrototypeOf(t),c=Object.getOwnPropertyDescriptor(d,"value");Object.defineProperty(t,"value",{configurable:!0,get(){return c.get.call(this)},set(o){c.set.call(this,o),a.getValue()!==o&&a.setValue(o??"")}});const s=e.find(".card.editor-card").get(0);s&&(s.style.height="min(78vh, 900px)",s.style.minHeight="520px"),g(e)};const f={icon:"box","facebook-icon":"facebook","instagram-icon":"instagram"};function g(e){const n=e.get?e.get(0):e;if(!n)return;const l=n.querySelectorAll("input[data-placeholder]");for(const t of l){const i=(t.dataset.placeholder||"").toLowerCase(),a=f[i];a&&(t.value&&!/^Sample /i.test(t.value)||(t.value=a,t.dispatchEvent(new Event("input",{bubbles:!0}))))}}
@@ -1 +0,0 @@
1
- export function escapeHtml(t){return t==null?"":String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export function safeHref(t){if(t==null)return"#";const r=String(t).trim();if(r==="")return"#";if(r.startsWith("/"))return escapeHtml(r);const e=r.toLowerCase();return e.startsWith("http://")||e.startsWith("https://")||e.startsWith("mailto:")?escapeHtml(r):"#"}
@@ -1 +0,0 @@
1
- import{escapeHtml as s}from"../lib/escape.js";const p={edit:"edited",publish:"published",form:"received submission"};function f(e,n){e&&(e.innerHTML=n)}export default function m(e,n){const r=Array.isArray(n?.activity)?n.activity:[];r.length===0?e.find(".dash-empty").removeAttr("hidden"):e.find(".dash-empty").attr("hidden","hidden");const i=r.map(t=>{const o=t.at?D(t.at).fromNow():"",a=t.target||t.form||"",d=t.actor?"<strong>"+s(t.actor)+"</strong> ":"",c=s(p[t.type]||t.type||"");return'<div class="dash-spike-row"><span>'+d+c+" <code>"+s(a)+"</code></span><span>"+s(o)+"</span></div>"});f(e.find('[data-field="rows"]').get(0),i.join(""))}
@@ -1 +0,0 @@
1
- import{apiRequest as r}from"../../../api.js";function h(t){if(!t)return"never";const e=Date.now()-new Date(t).getTime();if(e<0)return"just now";const a=Math.floor(e/1e3);if(a<60)return`${a}s ago`;const i=Math.floor(a/60);if(i<60)return`${i}m ago`;const n=Math.floor(i/60);return n<24?`${n}h ago`:`${Math.floor(n/24)}d ago`}function d(t,e){const a=e.hits+e.misses,i=e.hitRate;t.find('[data-field="hit-rate"]').text(i===null?"-":(i*100).toFixed(1)+"%"),t.find('[data-field="hits-misses"]').text(a===0?"no traffic yet":`${e.hits.toLocaleString()} hits \xB7 ${e.misses.toLocaleString()} misses`);const n=e.size??0,c=e.maxItems??0;t.find('[data-field="size"]').text(c?`${n} / ${c}`:String(n));const l=c?Math.min(100,n/c*100):0,o=t.find('[data-field="size-fill"]').get(0);o&&(o.style.width=l+"%"),t.find('[data-field="writes"]').text(e.writes.toLocaleString()),t.find('[data-field="invalidations"]').text(`${e.invalidations.toLocaleString()} invalidations`),t.find('[data-field="last-cleared"]').text(h(e.lastClearedAt)),t.find('[data-field="driver"]').text("driver: "+e.driver);const f=t.find('[data-field="toggle"]').get(0);f&&(f.checked=!!e.enabled),t.find('[data-field="toggle-label"]').text(e.enabled?"On":"Off")}async function s(t){try{const e=await r("/cache/status");d(t,e),t.data("cache-stats",e)}catch(e){console.warn("[cache widget] failed to refresh:",e)}}export default async function g(t){await s(t);const e=t.find('[data-field="toggle"]');e.on("change",async()=>{const a=e.get(0).checked;try{const i=await r("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:a})});d(t,i),E.toast(`Cache ${i.enabled?"enabled":"disabled"}.`,{type:"success"})}catch(i){e.get(0).checked=!a,E.toast(i.message||"Failed to toggle cache.",{type:"error"})}}),t.find('[data-field="clear-btn"]').on("click",async()=>{if(await E.confirm("Clear the entire response cache?"))try{const a=await r("/cache/clear",{method:"POST"});d(t,a),E.toast("Cache cleared.",{type:"success"})}catch(a){E.toast(a.message||"Failed to clear cache.",{type:"error"})}}),window.Domma&&Domma.icons&&Domma.icons.scan()}export{s as refreshCacheWidget};
@@ -1 +0,0 @@
1
- const p={ok:"check-circle",warn:"warning",fail:"x-circle"};export default function h(o,s){const r=s?.health?.checks||[],l=o.find('[data-field="rows"]');l.empty();const a=l.get(0);if(a){for(const e of r){const n=document.createElement("div");n.className="dash-spike-row dash-health-pill",n.setAttribute("data-level",e.level);const c=document.createElement("span"),i=document.createElement("span");i.setAttribute("data-icon",p[e.level]||"circle"),c.appendChild(i),c.appendChild(document.createTextNode(" "+(e.label||e.id)));const d=document.createElement("span");if(d.textContent=String(e.value),n.appendChild(c),n.appendChild(d),a.appendChild(n),Array.isArray(e.detail)&&e.detail.length>0){const t=document.createElement("div");t.className="dash-health-detail",t.style.fontSize="12px",t.style.opacity="0.75",t.style.padding="4px 0 8px",t.textContent=e.detail.slice(0,5).join(", "),a.appendChild(t)}}window.Domma&&Domma.icons&&Domma.icons.scan()}}
@@ -1 +0,0 @@
1
- import{escapeHtml as r}from"../lib/escape.js";function s(n,a){return n.map(e=>'<div class="dash-spike-row">'+a(e)+"</div>").join("")}function i(n,a){n&&(n.innerHTML=a)}export default function f(n,a){const e=a?.journeys,d=n.find(".dash-empty"),o=n.find('[data-field="entry"]'),p=n.find('[data-field="exit"]'),u=n.find('[data-field="paths"]'),c=n.find('[data-field="bounce"]');if(!e||!e.totalSessions){d.removeAttr("hidden"),o.empty(),p.empty(),u.empty(),c.text("-");return}d.attr("hidden","hidden"),i(o.get(0),s(e.entry||[],t=>"<span>"+r(t.url)+"</span><span>"+(Number(t.count)|0)+"</span>")),i(p.get(0),s(e.exit||[],t=>"<span>"+r(t.url)+"</span><span>"+(Number(t.count)|0)+"</span>")),i(u.get(0),s(e.paths||[],t=>"<span>"+r(t.from)+" &rarr; "+r(t.to)+"</span><span>"+(Number(t.count)|0)+"</span>")),c.text((Number(e.bounceRate)*100).toFixed(1)+"%")}
@@ -1 +0,0 @@
1
- export default function n(e,t){const d=t?.traffic?.today??0,a=t?.traffic?.deltaPct;e.find('[data-field="hits-today"]').text(d);const s=e.find('[data-field="hits-delta"]');a==null?s.text("").removeClass("up down"):a>=0?s.text("+"+a+"% vs yesterday").removeClass("down").addClass("up"):s.text(a+"% vs yesterday").removeClass("up").addClass("down"),e.find('[data-field="active-sessions"]').text(t?.realtime?.activeSessions??0),e.find('[data-field="submissions-week"]').text(t?._submissionsWeek??"-");const i=t?.health?.status||"ok",l={ok:"OK",warn:"Attention",fail:"Failing"};e.find('[data-field="health-pill"]').attr("data-level",i),e.find('[data-field="health-label"]').text(l[i])}
@@ -1,6 +0,0 @@
1
- import{escapeHtml as a,safeHref as d}from"../lib/escape.js";export default function p(i,r){const t=Array.isArray(r?.spikes)?r.spikes:[];t.length===0?i.find(".dash-empty").removeAttr("hidden"):i.find(".dash-empty").attr("hidden","hidden");const s=t.map(e=>`
2
- <div class="dash-spike-row">
3
- <span><a href="${d(e.url)}">${a(e.url)}</a></span>
4
- <span><strong>${Number(e.hits)|0}</strong> \xB7 \xD7${Number(e.ratio)||0}</span>
5
- </div>
6
- `).join(""),n=i.find('[data-field="spike-list"]').get(0);n&&(n.innerHTML=s)}
@@ -1 +0,0 @@
1
- import{escapeHtml as l,safeHref as i}from"../lib/escape.js";function c(n,s,e){if(s=s||80,e=e||24,!n||n.length===0)return"";const a=n.map(p=>Number(p)||0),o=Math.max.apply(null,a.concat([1])),t=s/Math.max(a.length-1,1),r=a.map((p,d)=>(d*t).toFixed(1)+","+(e-p/o*e).toFixed(1)).join(" ");return'<svg class="dash-spark" viewBox="0 0 '+s+" "+e+'" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none"><polyline fill="none" stroke="currentColor" stroke-width="1.5" points="'+r+'"/></svg>'}export default function m(n,s){const e=Array.isArray(s?.topPages)?s.topPages:[];e.length===0?n.find(".dash-empty").removeAttr("hidden"):n.find(".dash-empty").attr("hidden","hidden");const a=e.map(t=>{const r=t.deltaPct==null?"":'<span class="'+(t.deltaPct>=0?"up":"down")+' dash-kpi-delta">'+(t.deltaPct>=0?"+":"")+(Number(t.deltaPct)||0)+"%</span>";return'<div class="dash-spike-row"><span><a href="'+i(t.url)+'">'+l(t.url)+"</a> "+c(t.spark)+"</span><span><strong>"+(Number(t.hits)|0)+"</strong> "+r+"</span></div>"}).join(""),o=n.find('[data-field="rows"]').get(0);o&&(o.innerHTML=a)}
@@ -1 +0,0 @@
1
- const p="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js";let s=null;function u(){return window.Chart?Promise.resolve(window.Chart):s||(s=new Promise((r,n)=>{const e=document.createElement("script");e.src=p,e.onload=()=>r(window.Chart),e.onerror=n,document.head.appendChild(e)}),s)}let i=null;export default async function m(r,n){const e=await u(),o=30,a=[];for(let t=o-1;t>=0;t-=1)a.push({label:D().subtract(t,"days").format("MM-DD"),hits:0});if(Array.isArray(n?.topPages))for(const t of n.topPages){if(!Array.isArray(t.spark))continue;const l=o-t.spark.length;t.spark.forEach((f,h)=>{a[l+h].hits+=f})}const d=a.every(t=>t.hits===0);d?r.find(".dash-empty").removeAttr("hidden"):r.find(".dash-empty").attr("hidden","hidden");const c=r.find("#dash-traffic-canvas").get(0);c&&(i&&(i.destroy(),i=null),!d&&(i=new e(c,{type:"line",data:{labels:a.map(t=>t.label),datasets:[{data:a.map(t=>t.hits),borderColor:"rgba(37, 99, 235, 1)",backgroundColor:"rgba(37, 99, 235, 0.15)",fill:!0,tension:.3,pointRadius:0}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8}},y:{beginAtZero:!0}}}})))}
@@ -1 +0,0 @@
1
- import{api as u}from"../api.js";import{pluginAccent as P,pluginWatermark as S}from"../lib/plugin-accent.js";export const pluginMarketplaceView={templateUrl:"/admin/js/templates/plugin-marketplace.html",async onMount(e){const a=E.loader(e.get(0),{type:"dots"});let t;try{t=await u.marketplace.catalogue()}catch(s){E.toast(`Failed to load marketplace: ${s.message}`,{type:"error"}),a.destroy();return}if(a.destroy(),!t.available){e.find("#marketplace-unavailable").show();return}e.find("#marketplace-available").show(),t.site&&e.find("#marketplace-site").text(`Licensed to ${t.site}.`).show(),U(e,t.catalogue),Domma.icons.scan()}};function U(e,a){const t=e.find("#marketplace-grid").get(0);if(t){for(;t.firstChild;)t.removeChild(t.firstChild);if(!a||a.length===0){const s=document.createElement("p");s.className="text-muted",s.textContent="No plugins available in the marketplace.",t.appendChild(s);return}a.forEach(s=>{const i=R(s,e);t.appendChild(i)})}}function F(e){if(e.tier!=="paid")return"Free";const a=e.terms?.managed?.monthly;return a==null||!Number.isFinite(Number(a))?"Paid":`${e.terms?.currency==="GBP"?"\xA3":(e.terms?.currency||"")+" "}${(Number(a)/100).toFixed(2)}/mo`}function R(e,a){const{slug:t,version:s,displayName:i,description:y,entitled:o,installed:l,installedVersion:p,updatable:b}=e,r=document.createElement("div");r.className="card plugin-card",r.dataset.slug=t,r.style.setProperty("--plugin-accent",P(t)),r.appendChild(S(e.icon));const h=document.createElement("div");h.className="card-body";const m=document.createElement("div");m.className="plugin-header";const N=document.createElement("div");N.className="plugin-icon";const I=document.createElement("span");I.dataset.icon=e.icon||"package",N.appendChild(I);const g=document.createElement("div");g.className="plugin-meta";const x=document.createElement("div");x.className="plugin-name",x.textContent=i||t;const k=document.createElement("div");k.className="plugin-version",k.textContent=l&&p&&p!==s?`v${p} \u2192 v${s}`:`v${s}`,g.appendChild(x),g.appendChild(k);const f=document.createElement("div");f.style.cssText="display:flex;gap:.35rem;align-items:center;";const w=document.createElement("span");if(w.className="badge badge-outline",w.textContent=F(e),f.appendChild(w),o&&e.tier==="paid"){const c=document.createElement("span");c.className="badge badge-success",c.textContent=e.licenceStatus==="grace"?"Renew":"Licensed",c.title=e.licenceStatus==="grace"?"The rental term has ended - still running during the grace period":"This site holds a live licence",f.appendChild(c)}m.appendChild(N),m.appendChild(g),m.appendChild(f);const $=document.createElement("p");$.className="plugin-desc",$.textContent=y||"",h.appendChild(m),h.appendChild($);const C=document.createElement("div");C.className="plugin-footer";const v=document.createElement("span");v.className="text-muted",v.style.fontSize=".8rem",v.textContent="Restart server after changes",C.appendChild(v);const d=document.createElement("div");d.className="plugin-footer-actions";const n=document.createElement("button");if(n.className="btn btn-sm",l&&b&&o){n.className+=" btn-primary",n.textContent="Update",n.onclick=()=>T(e,a,{update:!0}),d.appendChild(n);const c=document.createElement("button");c.className="btn btn-sm btn-ghost",c.textContent="Uninstall",c.onclick=()=>D(t,a),d.appendChild(c)}else l?(n.className+=" btn-ghost",n.textContent="Uninstall",n.onclick=()=>D(t,a),d.appendChild(n)):o?(n.className+=" btn-primary",n.textContent="Install",n.onclick=()=>T(e,a),d.appendChild(n)):(n.className+=" btn-ghost",n.textContent=e.licenceStatus==="expired"?"Licence expired":"Not licensed",n.disabled=!0,n.title="Contact us to add this to your site.",d.appendChild(n));return C.appendChild(d),r.appendChild(h),r.appendChild(C),r}async function T(e,a,{update:t=!1}={}){const{slug:s,version:i,installedVersion:y}=e,o=t?`<p>Update ${s} from ${y} to ${i}?</p><p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`:`<p>Install ${e.displayName||s} v${i}?</p><p class="text-muted">It arrives licensed to this site and takes effect after a restart.</p>`;if(await E.confirm(o,{title:t?"Update plugin":"Install plugin",confirmText:t?"Update":"Install",cancelText:"Cancel",html:!0}))try{const l=await u.marketplace.install(s,i,{update:t}),p=l?.action==="upgrade"?`${s} updated${l.from?` from ${l.from}`:""} to ${l.version}.`:`${s} ${l?.version??i} installed.`;E.toast(`${p} Restart the server to activate it.`,{type:"success"});const b=await u.marketplace.catalogue();U(a,b.catalogue),Domma.icons.scan()}catch(l){E.toast(l.message||"Install failed.",{type:"error"})}}async function D(e,a){if(await E.confirm(`Uninstall ${e}? This will remove its files.`))try{await u.marketplace.uninstall(e),E.toast(`${e} uninstalled. Restart to apply.`,{type:"success"});const t=await u.marketplace.catalogue();U(a,t.catalogue),Domma.icons.scan()}catch(t){E.toast(`Uninstall failed: ${t.message}`,{type:"error"})}}