domma-cms 0.16.0 → 0.17.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.
- package/admin/css/dashboard.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +4 -4
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +1 -1
- package/admin/js/templates/dashboard/activity-feed.html +3 -0
- package/admin/js/templates/dashboard/health-detail.html +2 -0
- package/admin/js/templates/dashboard/journeys.html +17 -0
- package/admin/js/templates/dashboard/kpi-strip.html +34 -0
- package/admin/js/templates/dashboard/spike-feed.html +3 -0
- package/admin/js/templates/dashboard/top-pages.html +3 -0
- package/admin/js/templates/dashboard/traffic-chart.html +3 -0
- package/admin/js/templates/dashboard.html +22 -44
- package/admin/js/views/dashboard/lib/escape.js +1 -0
- package/admin/js/views/dashboard/widgets/activity-feed.js +1 -0
- package/admin/js/views/dashboard/widgets/health-detail.js +1 -0
- package/admin/js/views/dashboard/widgets/journeys.js +1 -0
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -0
- package/admin/js/views/dashboard/widgets/spike-feed.js +6 -0
- package/admin/js/views/dashboard/widgets/top-pages.js +1 -0
- package/admin/js/views/dashboard/widgets/traffic-chart.js +1 -0
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/config/plugins.json +3 -0
- package/package.json +2 -2
- package/plugins/analytics/daily.json +5 -0
- package/plugins/analytics/journeys.json +10 -0
- package/plugins/analytics/lifetime.json +25 -0
- package/plugins/analytics/plugin.js +231 -16
- package/plugins/analytics/public/inject-body.html +26 -2
- package/server/routes/api/dashboard.js +239 -0
- package/server/server.js +2 -0
- package/server/services/email.js +60 -20
- package/server/services/health.js +282 -0
- package/server/services/plugins.js +37 -5
package/server/services/email.js
CHANGED
|
@@ -4,6 +4,32 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import nodemailer from 'nodemailer';
|
|
6
6
|
|
|
7
|
+
let lastSendResult = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Return the most recent email send result, or null if no send has occurred.
|
|
11
|
+
*
|
|
12
|
+
* @returns {{ ok: boolean, at: string, info: string|null } | null}
|
|
13
|
+
*/
|
|
14
|
+
export function getLastSendResult() {
|
|
15
|
+
return lastSendResult;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Record the outcome of an email send for health reporting.
|
|
20
|
+
*
|
|
21
|
+
* @param {boolean} ok
|
|
22
|
+
* @param {string|null} info
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
function recordSendResult(ok, info) {
|
|
26
|
+
lastSendResult = {
|
|
27
|
+
ok,
|
|
28
|
+
at: new Date().toISOString(),
|
|
29
|
+
info: info || null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
/**
|
|
8
34
|
* Escape HTML special characters for safe use in email bodies.
|
|
9
35
|
*
|
|
@@ -58,17 +84,24 @@ export async function createTransport(smtp) {
|
|
|
58
84
|
* @throws {Error} If sending the email fails.
|
|
59
85
|
*/
|
|
60
86
|
export async function sendEmail(transport, {from, fromName, to, subject, html, text}) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
try {
|
|
88
|
+
const info = await transport.sendMail({
|
|
89
|
+
from: `"${fromName}" <${from}>`,
|
|
90
|
+
to,
|
|
91
|
+
subject,
|
|
92
|
+
text,
|
|
93
|
+
html
|
|
94
|
+
});
|
|
95
|
+
recordSendResult(true, info.messageId);
|
|
68
96
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
const previewUrl = nodemailer.getTestMessageUrl(info);
|
|
98
|
+
if (previewUrl) {
|
|
99
|
+
console.log('[email] Preview URL:', previewUrl);
|
|
100
|
+
}
|
|
101
|
+
return info;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
recordSendResult(false, err.message);
|
|
104
|
+
throw err;
|
|
72
105
|
}
|
|
73
106
|
}
|
|
74
107
|
|
|
@@ -112,16 +145,23 @@ export async function sendFormEmail(transport, { from, fromName, to, subject, fo
|
|
|
112
145
|
|
|
113
146
|
const text = `New form submission: ${formTitle}\n\n${plainRows}`;
|
|
114
147
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
148
|
+
try {
|
|
149
|
+
const info = await transport.sendMail({
|
|
150
|
+
from: `"${fromName}" <${from}>`,
|
|
151
|
+
to,
|
|
152
|
+
subject,
|
|
153
|
+
text,
|
|
154
|
+
html
|
|
155
|
+
});
|
|
156
|
+
recordSendResult(true, info.messageId);
|
|
122
157
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
158
|
+
const previewUrl = nodemailer.getTestMessageUrl(info);
|
|
159
|
+
if (previewUrl) {
|
|
160
|
+
console.log('[email] Preview URL:', previewUrl);
|
|
161
|
+
}
|
|
162
|
+
return info;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
recordSendResult(false, err.message);
|
|
165
|
+
throw err;
|
|
126
166
|
}
|
|
127
167
|
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Service
|
|
3
|
+
* Aggregates server + site health checks for the dashboard.
|
|
4
|
+
* Each check returns { id, level: 'ok'|'warn'|'fail', label, value, detail? }.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {fileURLToPath} from 'url';
|
|
9
|
+
import {config} from '../config.js';
|
|
10
|
+
import {getLastSendResult} from './email.js';
|
|
11
|
+
import {listPages} from './content.js';
|
|
12
|
+
import {getPluginLoadFailures} from './plugins.js';
|
|
13
|
+
import {FORMS_DIR} from './forms.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
17
|
+
|
|
18
|
+
const CACHE_TTL_MS = 30 * 1000;
|
|
19
|
+
const STALE_PAGE_DAYS = 90;
|
|
20
|
+
const FORM_WARN_DAYS = 30;
|
|
21
|
+
let cached = { at: 0, value: null };
|
|
22
|
+
|
|
23
|
+
const LEVELS = { ok: 0, warn: 1, fail: 2 };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute the worst level across a set of checks.
|
|
27
|
+
*
|
|
28
|
+
* @param {Array<{ level: string }>} checks
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function worstLevel(checks) {
|
|
32
|
+
let worst = 'ok';
|
|
33
|
+
for (const c of checks) if (LEVELS[c.level] > LEVELS[worst]) worst = c.level;
|
|
34
|
+
return worst;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format a byte count as a human-readable string.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} n
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function formatBytes(n) {
|
|
44
|
+
if (n < 1024) return n + ' B';
|
|
45
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
|
46
|
+
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB';
|
|
47
|
+
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format a duration in seconds as a human-readable string.
|
|
52
|
+
*
|
|
53
|
+
* @param {number} seconds
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function formatDuration(seconds) {
|
|
57
|
+
const d = Math.floor(seconds / 86400);
|
|
58
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
59
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
60
|
+
const s = Math.floor(seconds % 60);
|
|
61
|
+
if (d > 0) return `${d}d ${h}h`;
|
|
62
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
63
|
+
if (m > 0) return `${m}m`;
|
|
64
|
+
return `${s}s`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Recursively compute the size of a directory in bytes.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} dir
|
|
71
|
+
* @returns {Promise<number>}
|
|
72
|
+
*/
|
|
73
|
+
async function dirSize(dir) {
|
|
74
|
+
let total = 0;
|
|
75
|
+
let entries;
|
|
76
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); }
|
|
77
|
+
catch { return 0; }
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
const full = path.join(dir, e.name);
|
|
80
|
+
if (e.isDirectory()) total += await dirSize(full);
|
|
81
|
+
else if (e.isFile()) {
|
|
82
|
+
try { total += (await fs.stat(full)).size; } catch { /* ignore */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return total;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Individual checks
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
async function checkUptime() {
|
|
93
|
+
return {
|
|
94
|
+
id: 'uptime',
|
|
95
|
+
level: 'ok',
|
|
96
|
+
label: 'Uptime',
|
|
97
|
+
value: formatDuration(Math.floor(process.uptime()))
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function checkMemory() {
|
|
102
|
+
const mem = process.memoryUsage();
|
|
103
|
+
const usedMb = mem.heapUsed / 1024 / 1024;
|
|
104
|
+
const totalMb = mem.heapTotal / 1024 / 1024;
|
|
105
|
+
const pct = totalMb === 0 ? 0 : usedMb / totalMb;
|
|
106
|
+
return {
|
|
107
|
+
id: 'memory',
|
|
108
|
+
level: pct > 0.9 ? 'warn' : 'ok',
|
|
109
|
+
label: 'Heap',
|
|
110
|
+
value: `${usedMb.toFixed(0)} / ${totalMb.toFixed(0)} MB`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function checkContentDisk() {
|
|
115
|
+
const contentDir = path.isAbsolute(config.content.contentDir)
|
|
116
|
+
? config.content.contentDir
|
|
117
|
+
: path.join(ROOT, config.content.contentDir);
|
|
118
|
+
const size = await dirSize(contentDir);
|
|
119
|
+
return {
|
|
120
|
+
id: 'disk-content',
|
|
121
|
+
level: 'ok',
|
|
122
|
+
label: 'Content disk',
|
|
123
|
+
value: formatBytes(size)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function checkStalePages() {
|
|
128
|
+
const pages = await listPages().catch(() => []);
|
|
129
|
+
const cutoff = Date.now() - STALE_PAGE_DAYS * 24 * 60 * 60 * 1000;
|
|
130
|
+
const stale = pages.filter(p => {
|
|
131
|
+
const updated = p.updatedAt ? new Date(p.updatedAt).getTime() : 0;
|
|
132
|
+
return updated > 0 && updated < cutoff;
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
id: 'stale-pages',
|
|
136
|
+
level: stale.length === 0 ? 'ok' : 'warn',
|
|
137
|
+
label: `Stale pages (>${STALE_PAGE_DAYS}d)`,
|
|
138
|
+
value: stale.length,
|
|
139
|
+
detail: stale.slice(0, 10).map(p => p.urlPath)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function checkMissingMeta() {
|
|
144
|
+
const pages = await listPages().catch(() => []);
|
|
145
|
+
const missing = pages.filter(p => !p.description || String(p.description).trim() === '');
|
|
146
|
+
return {
|
|
147
|
+
id: 'missing-meta',
|
|
148
|
+
level: missing.length === 0 ? 'ok' : 'warn',
|
|
149
|
+
label: 'Pages missing description',
|
|
150
|
+
value: missing.length,
|
|
151
|
+
detail: missing.slice(0, 10).map(p => p.urlPath)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function checkLastForm() {
|
|
156
|
+
let formsExist = false;
|
|
157
|
+
try {
|
|
158
|
+
const formFiles = await fs.readdir(FORMS_DIR);
|
|
159
|
+
formsExist = formFiles.some(f => f.endsWith('.json'));
|
|
160
|
+
} catch { /* no forms dir */ }
|
|
161
|
+
|
|
162
|
+
const collectionsDir = path.join(ROOT, 'content', 'collections');
|
|
163
|
+
let newestIso = '';
|
|
164
|
+
let dirs = [];
|
|
165
|
+
try { dirs = await fs.readdir(collectionsDir, { withFileTypes: true }); } catch { /* none */ }
|
|
166
|
+
for (const d of dirs) {
|
|
167
|
+
if (!d.isDirectory()) continue;
|
|
168
|
+
const dataFile = path.join(collectionsDir, d.name, 'data.json');
|
|
169
|
+
let entries = [];
|
|
170
|
+
try { entries = JSON.parse(await fs.readFile(dataFile, 'utf8')); }
|
|
171
|
+
catch { continue; }
|
|
172
|
+
if (!Array.isArray(entries)) continue;
|
|
173
|
+
for (const e of entries) {
|
|
174
|
+
const at = e?.meta?.createdAt;
|
|
175
|
+
if (typeof at === 'string' && at > newestIso) newestIso = at;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!formsExist) {
|
|
180
|
+
return { id: 'last-form', level: 'ok', label: 'Last form submission', value: 'no forms defined' };
|
|
181
|
+
}
|
|
182
|
+
if (!newestIso) {
|
|
183
|
+
return { id: 'last-form', level: 'warn', label: 'Last form submission', value: 'never' };
|
|
184
|
+
}
|
|
185
|
+
const ageDays = (Date.now() - new Date(newestIso).getTime()) / (24 * 60 * 60 * 1000);
|
|
186
|
+
return {
|
|
187
|
+
id: 'last-form',
|
|
188
|
+
level: ageDays > FORM_WARN_DAYS ? 'warn' : 'ok',
|
|
189
|
+
label: 'Last form submission',
|
|
190
|
+
value: newestIso
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function checkSmtp() {
|
|
195
|
+
const r = getLastSendResult();
|
|
196
|
+
if (!r) return { id: 'smtp', level: 'ok', label: 'Last email send', value: 'never sent' };
|
|
197
|
+
return {
|
|
198
|
+
id: 'smtp',
|
|
199
|
+
level: r.ok ? 'ok' : 'fail',
|
|
200
|
+
label: 'Last email send',
|
|
201
|
+
value: r.at,
|
|
202
|
+
detail: r.info ? [r.info] : []
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function checkPluginLoad() {
|
|
207
|
+
const failures = getPluginLoadFailures();
|
|
208
|
+
return {
|
|
209
|
+
id: 'plugin-load',
|
|
210
|
+
level: failures.length === 0 ? 'ok' : 'fail',
|
|
211
|
+
label: 'Plugin failures',
|
|
212
|
+
value: failures.length,
|
|
213
|
+
detail: failures.slice(0, 10).map(f => `${f.name}: ${f.reason}`)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Orchestrator
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Run a single health check, isolating any thrown errors.
|
|
223
|
+
*
|
|
224
|
+
* If the check function throws, the failure is degraded to a `fail`-level
|
|
225
|
+
* result for that check rather than propagating and killing the aggregate.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} label
|
|
228
|
+
* @param {() => Promise<object>} fn
|
|
229
|
+
* @returns {Promise<object>}
|
|
230
|
+
*/
|
|
231
|
+
async function safeCheck(label, fn) {
|
|
232
|
+
try {
|
|
233
|
+
return await fn();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return {
|
|
236
|
+
id: label,
|
|
237
|
+
level: 'fail',
|
|
238
|
+
label,
|
|
239
|
+
value: 'check failed',
|
|
240
|
+
detail: [String(err.message || err)]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Aggregate site + server health checks.
|
|
247
|
+
*
|
|
248
|
+
* Returns the cached result if it's within the 30 s TTL; otherwise runs all eight
|
|
249
|
+
* checks in parallel and caches the result. Per-check errors are isolated — an
|
|
250
|
+
* unexpected throw degrades only that check to `fail`, never the whole call.
|
|
251
|
+
*
|
|
252
|
+
* Note: no in-flight deduplication. Two concurrent cache misses will both run the
|
|
253
|
+
* eight checks. Acceptable for the dashboard's traffic profile; revisit if the
|
|
254
|
+
* checks become heavy or the cache TTL drops.
|
|
255
|
+
*
|
|
256
|
+
* @returns {Promise<{status: 'ok'|'warn'|'fail', checks: Array}>}
|
|
257
|
+
*/
|
|
258
|
+
export async function getHealth() {
|
|
259
|
+
if (cached.value && Date.now() - cached.at < CACHE_TTL_MS) return cached.value;
|
|
260
|
+
const checks = await Promise.all([
|
|
261
|
+
safeCheck('uptime', checkUptime),
|
|
262
|
+
safeCheck('memory', checkMemory),
|
|
263
|
+
safeCheck('disk-content', checkContentDisk),
|
|
264
|
+
safeCheck('stale-pages', checkStalePages),
|
|
265
|
+
safeCheck('missing-meta', checkMissingMeta),
|
|
266
|
+
safeCheck('last-form', checkLastForm),
|
|
267
|
+
safeCheck('smtp', checkSmtp),
|
|
268
|
+
safeCheck('plugin-load', checkPluginLoad)
|
|
269
|
+
]);
|
|
270
|
+
const value = { status: worstLevel(checks), checks };
|
|
271
|
+
cached = { at: Date.now(), value };
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { worstLevel };
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Reset the in-memory cache. Test-only helper.
|
|
279
|
+
*
|
|
280
|
+
* @returns {void}
|
|
281
|
+
*/
|
|
282
|
+
export function _resetCache() { cached = { at: 0, value: null }; }
|
|
@@ -23,6 +23,28 @@ const PLUGINS_DIR = path.resolve('plugins');
|
|
|
23
23
|
|
|
24
24
|
const REQUIRED_MANIFEST_FIELDS = ['name', 'displayName', 'version', 'description', 'author', 'date', 'icon'];
|
|
25
25
|
|
|
26
|
+
const failedLoads = [];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record a plugin load failure for health reporting.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} name
|
|
32
|
+
* @param {string} reason
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
export function recordPluginLoadFailure(name, reason) {
|
|
36
|
+
failedLoads.push({ name, reason, at: new Date().toISOString() });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Return a snapshot of all recorded plugin load failures.
|
|
41
|
+
*
|
|
42
|
+
* @returns {Array<{ name: string, reason: string, at: string }>}
|
|
43
|
+
*/
|
|
44
|
+
export function getPluginLoadFailures() {
|
|
45
|
+
return failedLoads.slice();
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
/**
|
|
27
49
|
* In-memory registry of loaded plugins, populated by registerPlugins().
|
|
28
50
|
* Keyed by plugin name; each entry mirrors the manifest plus runtime metadata.
|
|
@@ -71,7 +93,9 @@ export async function discoverPlugins() {
|
|
|
71
93
|
let valid = true;
|
|
72
94
|
for (const field of REQUIRED_MANIFEST_FIELDS) {
|
|
73
95
|
if (!manifest[field]) {
|
|
74
|
-
|
|
96
|
+
const reason = `missing required field "${field}" in plugin.json`;
|
|
97
|
+
console.warn(`Plugin "${dir}": ${reason} — skipping`);
|
|
98
|
+
recordPluginLoadFailure(dir, reason);
|
|
75
99
|
valid = false;
|
|
76
100
|
break;
|
|
77
101
|
}
|
|
@@ -85,14 +109,18 @@ export async function discoverPlugins() {
|
|
|
85
109
|
try {
|
|
86
110
|
await fs.access(pluginJsPath);
|
|
87
111
|
} catch {
|
|
88
|
-
|
|
112
|
+
const reason = 'missing required file plugin.js';
|
|
113
|
+
console.warn(`Plugin "${dir}": ${reason} — skipping`);
|
|
114
|
+
recordPluginLoadFailure(dir, reason);
|
|
89
115
|
continue;
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
try {
|
|
93
119
|
await fs.access(configJsPath);
|
|
94
120
|
} catch {
|
|
95
|
-
|
|
121
|
+
const reason = 'missing required file config.js';
|
|
122
|
+
console.warn(`Plugin "${dir}": ${reason} — skipping`);
|
|
123
|
+
recordPluginLoadFailure(dir, reason);
|
|
96
124
|
continue;
|
|
97
125
|
}
|
|
98
126
|
|
|
@@ -118,7 +146,9 @@ async function loadConfigDefaults(name) {
|
|
|
118
146
|
const {default: exported} = await import(configJsPath);
|
|
119
147
|
defaults = exported || {};
|
|
120
148
|
} catch (err) {
|
|
121
|
-
|
|
149
|
+
const reason = `failed to load config.js — ${err.message}`;
|
|
150
|
+
console.warn(`Plugin "${name}": ${reason}`);
|
|
151
|
+
recordPluginLoadFailure(name, reason);
|
|
122
152
|
}
|
|
123
153
|
_configDefaultsCache.set(name, defaults);
|
|
124
154
|
return defaults;
|
|
@@ -194,7 +224,9 @@ export async function registerPlugins(fastify) {
|
|
|
194
224
|
// no public entry — that's fine
|
|
195
225
|
}
|
|
196
226
|
} catch (err) {
|
|
197
|
-
|
|
227
|
+
const reason = `server failed to load: ${err.message}`;
|
|
228
|
+
fastify.log.error(`Plugin "${manifest.name}" ${reason}`);
|
|
229
|
+
recordPluginLoadFailure(manifest.name, reason);
|
|
198
230
|
}
|
|
199
231
|
}
|
|
200
232
|
|