domma-cms 0.55.1 → 0.69.3
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/CLAUDE.md +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -1,35 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Analytics
|
|
3
|
-
* Tracks page hits with per-day buckets so we can chart trends and
|
|
4
|
-
* filter by time range.
|
|
2
|
+
* Analytics - page-hit counters, trends and journeys.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Was the `analytics` plugin until 0.67.0, where it was a CORE_PLUGINS member:
|
|
5
|
+
* always loaded, never switchable off. A feature nobody may turn off has no
|
|
6
|
+
* business being listed as an add-on, so it is simply part of the CMS now.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* **Counters live in `content/analytics/`, not under plugins/.** They are real
|
|
9
|
+
* traffic history that cannot be regenerated, and the retirement migration
|
|
10
|
+
* deletes the plugin directory - so the data had to move out from under it. The
|
|
11
|
+
* migration copies and verifies before anything is removed.
|
|
12
|
+
*
|
|
13
|
+
* This is the CLIENT-SIDE beacon's store: a browser posts to /api/analytics/hit,
|
|
14
|
+
* which means bots, blocked scripts and Do-Not-Track visitors are absent by
|
|
15
|
+
* design. `analytics-pro` counts the same site server-side from an onRequest
|
|
16
|
+
* hook and will always report more. They are different instruments, not two
|
|
17
|
+
* versions of one, which is why neither supersedes the other.
|
|
18
|
+
*
|
|
19
|
+
* Endpoints (mounted at /api/analytics):
|
|
20
|
+
* POST /hit public record a hit { url, sid, ref }
|
|
12
21
|
* GET /stats admin summary + top pages (?range=today|7d|30d|90d|all|custom&from=&to=)
|
|
13
22
|
* GET /series admin per-day totals series (?days=N or ?from=&to=)
|
|
23
|
+
* GET /journeys admin entry/exit/path aggregates
|
|
24
|
+
* GET /realtime admin sessions active in the last 5 minutes
|
|
25
|
+
* GET /spikes admin days that stand out from the trend
|
|
14
26
|
* GET /export.csv admin CSV export of top pages for the active range
|
|
15
|
-
* DELETE /stats admin reset all
|
|
27
|
+
* DELETE /stats admin reset all counters
|
|
16
28
|
*/
|
|
29
|
+
import {clip, plural} from '../../services/sidebarBadges.js';
|
|
17
30
|
import fs from 'fs/promises';
|
|
18
31
|
import path from 'path';
|
|
19
|
-
import {
|
|
32
|
+
import {authenticate as defaultAuthenticate, requireAdmin as defaultRequireAdmin} from '../../middleware/auth.js';
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Where the counters live. Resolved per call rather than frozen at import so a
|
|
36
|
+
* test can run against a temporary install, the way the migrations do.
|
|
37
|
+
*/
|
|
38
|
+
export function analyticsDir() {
|
|
39
|
+
return path.resolve('content', 'analytics');
|
|
40
|
+
}
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
// preserve (siteCmsUpdater's copyPlugin backs up data/; `domma-cms update`
|
|
25
|
-
// keeps 'data' in its preserve set). Pre-2.1 they sat in the plugin root and
|
|
26
|
-
// were overwritten by the manager's copy on every plugin push. On first boot
|
|
27
|
-
// migrateToDataDir() moves any pre-2.1 root files across.
|
|
28
|
-
const DATA_DIR = path.join(__dirname, 'data');
|
|
42
|
+
const DATA_DIR = analyticsDir();
|
|
29
43
|
const LIFETIME_FILE = path.join(DATA_DIR, 'lifetime.json');
|
|
30
44
|
const DAILY_FILE = path.join(DATA_DIR, 'daily.json');
|
|
31
45
|
const LEGACY_FILE = path.join(DATA_DIR, 'stats.json');
|
|
32
46
|
const JOURNEYS_FILE = path.join(DATA_DIR, 'journeys.json');
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What never gets counted, server side.
|
|
50
|
+
*
|
|
51
|
+
* These were the plugin's `config.js` defaults and no install ever changed one,
|
|
52
|
+
* so they are constants rather than a config block nobody would open. The other
|
|
53
|
+
* two defaults it carried - honourDnt and sessionDedup - were always decided in
|
|
54
|
+
* the browser, where the facts are, and still are: see public/js/analytics.js.
|
|
55
|
+
*/
|
|
56
|
+
const EXCLUDE_ADMIN = true;
|
|
57
|
+
const EXCLUDE_PATHS = ['/api'];
|
|
33
58
|
const JOURNEY_DAILY_CAP = 5000;
|
|
34
59
|
const REALTIME_WINDOW_MS = 5 * 60 * 1000;
|
|
35
60
|
|
|
@@ -48,23 +73,6 @@ async function writeJson(file, data) {
|
|
|
48
73
|
await fs.rename(tmp, file);
|
|
49
74
|
}
|
|
50
75
|
|
|
51
|
-
/**
|
|
52
|
-
* One-shot relocation of pre-2.1 counters from the plugin root into data/.
|
|
53
|
-
* Runs before migrateLegacy() so the rest of the plugin only ever sees data/.
|
|
54
|
-
*/
|
|
55
|
-
async function migrateToDataDir() {
|
|
56
|
-
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
57
|
-
for (const name of ['lifetime.json', 'daily.json', 'journeys.json', 'stats.json']) {
|
|
58
|
-
const dest = path.join(DATA_DIR, name);
|
|
59
|
-
try { await fs.access(dest); continue; } catch { /* dest absent - maybe move */ }
|
|
60
|
-
const oldPath = path.join(__dirname, name);
|
|
61
|
-
try {
|
|
62
|
-
await fs.access(oldPath);
|
|
63
|
-
await fs.rename(oldPath, dest);
|
|
64
|
-
} catch { /* no pre-2.1 file to move */ }
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
76
|
/**
|
|
69
77
|
* One-shot migration from the legacy flat stats.json.
|
|
70
78
|
* Preserves existing totals as lifetime numbers; daily history starts empty.
|
|
@@ -274,11 +282,17 @@ function shouldRecordJourney(dayEntries, cap = JOURNEY_DAILY_CAP) {
|
|
|
274
282
|
return Array.isArray(dayEntries) && dayEntries.length < cap;
|
|
275
283
|
}
|
|
276
284
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Auth is injectable, the shape dashboard.js and plugins.js use, so a test can
|
|
287
|
+
* drive these routes without registering @fastify/jwt.
|
|
288
|
+
*
|
|
289
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
290
|
+
* @param {object} [opts]
|
|
291
|
+
*/
|
|
292
|
+
export async function analyticsRoutes(fastify, opts = {}) {
|
|
293
|
+
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
294
|
+
const requireAdmin = opts.requireAdmin || defaultRequireAdmin;
|
|
280
295
|
|
|
281
|
-
await migrateToDataDir();
|
|
282
296
|
await migrateLegacy();
|
|
283
297
|
|
|
284
298
|
fastify.decorate('analytics', {
|
|
@@ -333,11 +347,11 @@ export default async function analyticsPlugin(fastify, options) {
|
|
|
333
347
|
}
|
|
334
348
|
|
|
335
349
|
const normalised = url.split('?')[0].split('#')[0].replace(/\/+$/, '') || '/';
|
|
336
|
-
if (
|
|
350
|
+
if (EXCLUDE_ADMIN && normalised.startsWith('/admin')) {
|
|
337
351
|
return { ok: true, ignored: 'admin' };
|
|
338
352
|
}
|
|
339
|
-
if (Array.isArray(
|
|
340
|
-
for (const prefix of
|
|
353
|
+
if (Array.isArray(EXCLUDE_PATHS)) {
|
|
354
|
+
for (const prefix of EXCLUDE_PATHS) {
|
|
341
355
|
if (typeof prefix === 'string' && prefix && normalised.startsWith(prefix)) {
|
|
342
356
|
return { ok: true, ignored: 'excluded' };
|
|
343
357
|
}
|
|
@@ -441,6 +455,40 @@ export default async function analyticsPlugin(fastify, options) {
|
|
|
441
455
|
return { activeSessions: sids.size, windowMinutes: REALTIME_WINDOW_MS / 60000 };
|
|
442
456
|
});
|
|
443
457
|
|
|
458
|
+
/** GET /api/analytics/active-count - the sidebar badge: visitors on the site right now. */
|
|
459
|
+
fastify.get('/active-count', { preHandler: [authenticate, requireAdmin] }, async () => {
|
|
460
|
+
const [journeys, daily] = await Promise.all([readJson(JOURNEYS_FILE, {}), readJson(DAILY_FILE, {})]);
|
|
461
|
+
const today = todayKey();
|
|
462
|
+
const cutoff = Date.now() - REALTIME_WINDOW_MS;
|
|
463
|
+
const recent = (journeys[today] || []).filter(ev => ev.t >= cutoff);
|
|
464
|
+
const n = new Set(recent.map(ev => ev.sid)).size;
|
|
465
|
+
const mins = REALTIME_WINDOW_MS / 60000;
|
|
466
|
+
|
|
467
|
+
// What each active visitor is on now: their latest page.
|
|
468
|
+
const latest = new Map();
|
|
469
|
+
for (const ev of recent) if (!latest.has(ev.sid) || latest.get(ev.sid).t < ev.t) latest.set(ev.sid, ev);
|
|
470
|
+
const onPage = {};
|
|
471
|
+
for (const ev of latest.values()) onPage[ev.url] = (onPage[ev.url] || 0) + 1;
|
|
472
|
+
const viewsToday = Object.values(daily[today] || {}).reduce((a, b) => a + b, 0);
|
|
473
|
+
const sessionsToday = new Set((journeys[today] || []).map(ev => ev.sid)).size;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
count: n, label: `${plural(n, 'visitor')} on the site in the last ${mins} minutes`, tone: 'live',
|
|
477
|
+
details: {
|
|
478
|
+
title: 'Analytics',
|
|
479
|
+
rows: [
|
|
480
|
+
{label: 'Active now', value: String(n)},
|
|
481
|
+
{label: 'Views today', value: viewsToday.toLocaleString('en-GB')},
|
|
482
|
+
{label: 'Sessions today', value: sessionsToday.toLocaleString('en-GB')}
|
|
483
|
+
],
|
|
484
|
+
// URLs come from the PUBLIC hit counter - plain text only, set with textContent.
|
|
485
|
+
items: Object.entries(onPage).sort((a, b) => b[1] - a[1]).slice(0, 5)
|
|
486
|
+
.map(([url, c]) => ({text: clip(url, 48), meta: `${plural(c, 'visitor')} here now`})),
|
|
487
|
+
empty: `Nobody on the site in the last ${mins} minutes.`
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
|
|
444
492
|
fastify.get('/spikes', { preHandler: [authenticate, requireAdmin] }, async () => {
|
|
445
493
|
const daily = await readJson(DAILY_FILE, {});
|
|
446
494
|
return { items: detectSpikes(daily) };
|
|
@@ -359,6 +359,40 @@ export async function collectionsRoutes(fastify) {
|
|
|
359
359
|
}
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
+
/*
|
|
363
|
+
* Flag one entry as spam without disturbing the rest of it.
|
|
364
|
+
*
|
|
365
|
+
* `updateEntry` REPLACES `data` wholesale, so the obvious client-side PUT
|
|
366
|
+
* of {spam: true} would wipe every other field on the record. The merge
|
|
367
|
+
* has to happen server-side, which is the whole reason this endpoint
|
|
368
|
+
* exists rather than the context menu calling the generic update route.
|
|
369
|
+
*
|
|
370
|
+
* The field defaults to `spam` and need not be declared in the schema -
|
|
371
|
+
* validateEntryData only checks required fields, so an undeclared key is
|
|
372
|
+
* carried through untouched. Guarded by the same collections.update
|
|
373
|
+
* permission as any other write, so a public visitor cannot reach it.
|
|
374
|
+
*/
|
|
375
|
+
fastify.post('/collections/:slug/entries/:id/spam', canUpdate, async (request, reply) => {
|
|
376
|
+
const {slug, id} = request.params;
|
|
377
|
+
const field = typeof request.body?.field === 'string' && request.body.field.trim()
|
|
378
|
+
? request.body.field.trim()
|
|
379
|
+
: 'spam';
|
|
380
|
+
const value = request.body?.spam === false ? false : true;
|
|
381
|
+
try {
|
|
382
|
+
const existing = await getEntry(slug, id);
|
|
383
|
+
if (!existing) return reply.code(404).send({error: 'Entry not found'});
|
|
384
|
+
|
|
385
|
+
const entry = await updateEntry(slug, id, {...(existing.data || {}), [field]: value});
|
|
386
|
+
hooks.emit('collection:entryUpdated', {
|
|
387
|
+
slug, entryId: entry.id, data: entry.data, userId: request.user?.id || null
|
|
388
|
+
});
|
|
389
|
+
return {entry, field, spam: value};
|
|
390
|
+
} catch (err) {
|
|
391
|
+
const code = /not found/i.test(err.message) ? 404 : 400;
|
|
392
|
+
return reply.code(code).send({error: err.message});
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
362
396
|
fastify.put('/collections/:slug/entries/:id', canUpdate, async (request, reply) => {
|
|
363
397
|
try {
|
|
364
398
|
const entry = await updateEntry(request.params.slug, request.params.id, request.body?.data || {});
|