@actuate-media/cms-core 0.25.0 → 0.26.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/dist/__tests__/api/stats-routes.test.d.ts +2 -0
- package/dist/__tests__/api/stats-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/stats-routes.test.js +251 -0
- package/dist/__tests__/api/stats-routes.test.js.map +1 -0
- package/dist/__tests__/auth/password.test.js +39 -12
- package/dist/__tests__/auth/password.test.js.map +1 -1
- package/dist/__tests__/content-health/scanner.test.d.ts +2 -0
- package/dist/__tests__/content-health/scanner.test.d.ts.map +1 -0
- package/dist/__tests__/content-health/scanner.test.js +151 -0
- package/dist/__tests__/content-health/scanner.test.js.map +1 -0
- package/dist/__tests__/diagnostics/api-metrics.test.d.ts +2 -0
- package/dist/__tests__/diagnostics/api-metrics.test.d.ts.map +1 -0
- package/dist/__tests__/diagnostics/api-metrics.test.js +153 -0
- package/dist/__tests__/diagnostics/api-metrics.test.js.map +1 -0
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +31 -1
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +263 -0
- package/dist/api/handlers.js.map +1 -1
- package/dist/auth/password.d.ts +11 -0
- package/dist/auth/password.d.ts.map +1 -1
- package/dist/auth/password.js +13 -0
- package/dist/auth/password.js.map +1 -1
- package/dist/content-health/cron.d.ts +67 -0
- package/dist/content-health/cron.d.ts.map +1 -0
- package/dist/content-health/cron.js +167 -0
- package/dist/content-health/cron.js.map +1 -0
- package/dist/content-health/scanner.d.ts +118 -0
- package/dist/content-health/scanner.d.ts.map +1 -0
- package/dist/content-health/scanner.js +263 -0
- package/dist/content-health/scanner.js.map +1 -0
- package/dist/diagnostics/api-metrics.d.ts +135 -0
- package/dist/diagnostics/api-metrics.d.ts.map +1 -0
- package/dist/diagnostics/api-metrics.js +374 -0
- package/dist/diagnostics/api-metrics.js.map +1 -0
- package/package.json +1 -1
- package/prisma/migrations/0007_dashboard_metrics/migration.sql +74 -0
- package/prisma/schema.prisma +180 -73
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { LATENCY_BUCKETS, HISTOGRAM_LENGTH, bucketStartFor, estimatePercentile, histogramBucketIndex, makeHistogramFor, mergeHistograms, normalizeRoute, shouldRecord, statusBucketOf, } from '../../diagnostics/api-metrics.js';
|
|
3
|
+
describe('statusBucketOf', () => {
|
|
4
|
+
it('maps standard HTTP families to xx buckets', () => {
|
|
5
|
+
expect(statusBucketOf(200)).toBe('2xx');
|
|
6
|
+
expect(statusBucketOf(204)).toBe('2xx');
|
|
7
|
+
expect(statusBucketOf(301)).toBe('3xx');
|
|
8
|
+
expect(statusBucketOf(304)).toBe('3xx');
|
|
9
|
+
expect(statusBucketOf(400)).toBe('4xx');
|
|
10
|
+
expect(statusBucketOf(404)).toBe('4xx');
|
|
11
|
+
expect(statusBucketOf(429)).toBe('4xx');
|
|
12
|
+
expect(statusBucketOf(500)).toBe('5xx');
|
|
13
|
+
expect(statusBucketOf(503)).toBe('5xx');
|
|
14
|
+
});
|
|
15
|
+
it('puts unknown statuses in the "other" bucket', () => {
|
|
16
|
+
expect(statusBucketOf(99)).toBe('other');
|
|
17
|
+
expect(statusBucketOf(700)).toBe('other');
|
|
18
|
+
expect(statusBucketOf(-1)).toBe('other');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('histogramBucketIndex', () => {
|
|
22
|
+
it('assigns sub-1ms to bucket 0', () => {
|
|
23
|
+
expect(histogramBucketIndex(0)).toBe(0);
|
|
24
|
+
expect(histogramBucketIndex(0.4)).toBe(0);
|
|
25
|
+
expect(histogramBucketIndex(0.99)).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
it('uses log-2 boundaries', () => {
|
|
28
|
+
expect(histogramBucketIndex(1)).toBe(1); // 1 < 2
|
|
29
|
+
expect(histogramBucketIndex(1.5)).toBe(1);
|
|
30
|
+
expect(histogramBucketIndex(2)).toBe(2); // 2 < 4
|
|
31
|
+
expect(histogramBucketIndex(4)).toBe(3);
|
|
32
|
+
expect(histogramBucketIndex(127)).toBe(7); // 64-128
|
|
33
|
+
expect(histogramBucketIndex(128)).toBe(8); // 128-256
|
|
34
|
+
});
|
|
35
|
+
it('puts ≥1024ms in the overflow bucket', () => {
|
|
36
|
+
expect(histogramBucketIndex(1024)).toBe(LATENCY_BUCKETS.length);
|
|
37
|
+
expect(histogramBucketIndex(60_000)).toBe(LATENCY_BUCKETS.length);
|
|
38
|
+
});
|
|
39
|
+
it('handles non-finite input as bucket 0', () => {
|
|
40
|
+
expect(histogramBucketIndex(NaN)).toBe(0);
|
|
41
|
+
expect(histogramBucketIndex(-1)).toBe(0);
|
|
42
|
+
expect(histogramBucketIndex(Infinity)).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('makeHistogramFor + mergeHistograms', () => {
|
|
46
|
+
it('initial histogram has exactly one count in the right bucket', () => {
|
|
47
|
+
const hist = makeHistogramFor(50); // 32-64ms → bucket 6
|
|
48
|
+
expect(hist).toHaveLength(HISTOGRAM_LENGTH);
|
|
49
|
+
expect(hist[6]).toBe(1);
|
|
50
|
+
expect(hist.reduce((s, v) => s + v, 0)).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
it('merges element-wise without mutating inputs', () => {
|
|
53
|
+
const a = makeHistogramFor(50);
|
|
54
|
+
const b = makeHistogramFor(100); // 64-128ms → bucket 7
|
|
55
|
+
const merged = mergeHistograms(a, b);
|
|
56
|
+
expect(merged[6]).toBe(1);
|
|
57
|
+
expect(merged[7]).toBe(1);
|
|
58
|
+
expect(a[7]).toBe(0); // unchanged
|
|
59
|
+
});
|
|
60
|
+
it('tolerates undefined / short prior arrays', () => {
|
|
61
|
+
const b = makeHistogramFor(10);
|
|
62
|
+
expect(() => mergeHistograms(undefined, b)).not.toThrow();
|
|
63
|
+
expect(() => mergeHistograms([1, 2, 3], b)).not.toThrow();
|
|
64
|
+
const merged = mergeHistograms([1, 2, 3], b);
|
|
65
|
+
expect(merged[0]).toBe(1);
|
|
66
|
+
expect(merged[1]).toBe(2);
|
|
67
|
+
expect(merged[2]).toBe(3);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('estimatePercentile', () => {
|
|
71
|
+
it('returns 0 for an empty histogram', () => {
|
|
72
|
+
expect(estimatePercentile(new Array(HISTOGRAM_LENGTH).fill(0), 0.95)).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
it('returns the bucket boundary that crosses the p-watermark', () => {
|
|
75
|
+
// 90% of traffic in bucket 4 (8-16ms), 10% in bucket 7 (64-128ms).
|
|
76
|
+
// p50 should land in bucket 4 (upper bound = 16).
|
|
77
|
+
// p95 must cross into bucket 7 (upper bound = 128).
|
|
78
|
+
const hist = new Array(HISTOGRAM_LENGTH).fill(0);
|
|
79
|
+
hist[4] = 90;
|
|
80
|
+
hist[7] = 10;
|
|
81
|
+
expect(estimatePercentile(hist, 0.5)).toBe(16);
|
|
82
|
+
expect(estimatePercentile(hist, 0.95)).toBe(128);
|
|
83
|
+
});
|
|
84
|
+
it('estimates beyond the last bucket using the overflow heuristic', () => {
|
|
85
|
+
const hist = new Array(HISTOGRAM_LENGTH).fill(0);
|
|
86
|
+
hist[HISTOGRAM_LENGTH - 1] = 10; // overflow bucket
|
|
87
|
+
const result = estimatePercentile(hist, 0.95);
|
|
88
|
+
expect(result).toBeGreaterThan(LATENCY_BUCKETS[LATENCY_BUCKETS.length - 1]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('bucketStartFor', () => {
|
|
92
|
+
it('snaps to the start of the UTC minute', () => {
|
|
93
|
+
const t = new Date('2026-05-28T14:37:42.123Z').getTime();
|
|
94
|
+
const start = bucketStartFor(t);
|
|
95
|
+
expect(start.toISOString()).toBe('2026-05-28T14:37:00.000Z');
|
|
96
|
+
});
|
|
97
|
+
it('is idempotent — calling twice returns the same boundary', () => {
|
|
98
|
+
const t = new Date('2026-05-28T14:37:42.123Z').getTime();
|
|
99
|
+
const a = bucketStartFor(t);
|
|
100
|
+
const b = bucketStartFor(a);
|
|
101
|
+
expect(a.getTime()).toBe(b.getTime());
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('normalizeRoute', () => {
|
|
105
|
+
it('passes static routes through verbatim', () => {
|
|
106
|
+
expect(normalizeRoute('/stats')).toBe('/stats');
|
|
107
|
+
expect(normalizeRoute('/health')).toBe('/health');
|
|
108
|
+
expect(normalizeRoute('/auth/login')).toBe('/auth/login');
|
|
109
|
+
expect(normalizeRoute('/notifications')).toBe('/notifications');
|
|
110
|
+
});
|
|
111
|
+
it('strips the /api/cms prefix if present', () => {
|
|
112
|
+
expect(normalizeRoute('/api/cms/stats')).toBe('/stats');
|
|
113
|
+
expect(normalizeRoute('/api/cms/collections/posts/abc')).toBe('/collections/:slug/:id');
|
|
114
|
+
});
|
|
115
|
+
it('parametrises collection routes by document id', () => {
|
|
116
|
+
expect(normalizeRoute('/collections/posts')).toBe('/collections/:slug');
|
|
117
|
+
expect(normalizeRoute('/collections/posts/abc123')).toBe('/collections/:slug/:id');
|
|
118
|
+
expect(normalizeRoute('/collections/posts/abc123/restore')).toBe('/collections/:slug/:id/restore');
|
|
119
|
+
expect(normalizeRoute('/collections/posts/abc123/versions')).toBe('/collections/:slug/:id/versions');
|
|
120
|
+
});
|
|
121
|
+
it('parametrises globals, media, forms, users, api-keys', () => {
|
|
122
|
+
expect(normalizeRoute('/globals/site-settings')).toBe('/globals/:slug');
|
|
123
|
+
expect(normalizeRoute('/media/abc')).toBe('/media/:id');
|
|
124
|
+
expect(normalizeRoute('/forms/xyz/submit')).toBe('/forms/:id/submit');
|
|
125
|
+
expect(normalizeRoute('/forms/xyz')).toBe('/forms/:id');
|
|
126
|
+
expect(normalizeRoute('/users/u1')).toBe('/users/:id');
|
|
127
|
+
expect(normalizeRoute('/api-keys/k1')).toBe('/api-keys/:id');
|
|
128
|
+
});
|
|
129
|
+
it('collapses unknown long paths into the /other catch-all', () => {
|
|
130
|
+
expect(normalizeRoute('/some/random/path/we/never/added')).toBe('/other');
|
|
131
|
+
expect(normalizeRoute('/x/y/z')).toBe('/other');
|
|
132
|
+
});
|
|
133
|
+
it('strips query strings and trailing slashes', () => {
|
|
134
|
+
expect(normalizeRoute('/stats?from=2026')).toBe('/stats');
|
|
135
|
+
expect(normalizeRoute('/collections/posts/')).toBe('/collections/:slug');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('shouldRecord', () => {
|
|
139
|
+
it('skips the dashboard polling routes', () => {
|
|
140
|
+
expect(shouldRecord('/stats')).toBe(false);
|
|
141
|
+
expect(shouldRecord('/stats/api-delivery')).toBe(false);
|
|
142
|
+
expect(shouldRecord('/stats/content-health')).toBe(false);
|
|
143
|
+
expect(shouldRecord('/health')).toBe(false);
|
|
144
|
+
expect(shouldRecord('/notifications/stream')).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
it('records normal API traffic', () => {
|
|
147
|
+
expect(shouldRecord('/collections/:slug')).toBe(true);
|
|
148
|
+
expect(shouldRecord('/collections/:slug/:id')).toBe(true);
|
|
149
|
+
expect(shouldRecord('/auth/login')).toBe(true);
|
|
150
|
+
expect(shouldRecord('/other')).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=api-metrics.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-metrics.test.js","sourceRoot":"","sources":["../../../src/__tests__/diagnostics/api-metrics.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,cAAc,GACf,MAAM,kCAAkC,CAAA;AAEzC,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACxC,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACzC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACvC,MAAM,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzC,MAAM,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,QAAQ;QAChD,MAAM,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,QAAQ;QAChD,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACvC,MAAM,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,SAAS;QACnD,MAAM,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,UAAU;IACtD,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;QAC/D,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzC,MAAM,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACxC,MAAM,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,IAAI,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAA,CAAC,qBAAqB;QACvD,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACvB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAA;QAC9B,MAAM,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAA,CAAC,sBAAsB;QACtD,MAAM,MAAM,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QACpC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzB,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAC,YAAY;IACnC,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAA;QAC9B,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;QACzD,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;QACzD,MAAM,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3B,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,kBAAkB,CAAC,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC/E,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,mEAAmE;QACnE,kDAAkD;QAClD,oDAAoD;QACpD,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChD,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;QACZ,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;QACZ,MAAM,CAAC,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC9C,MAAM,CAAC,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAClD,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChD,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC,GAAG,EAAE,CAAA,CAAC,kBAAkB;QAClD,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,eAAe,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,CAAA;IAC9E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAA;QACxD,MAAM,KAAK,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;QAC/B,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAA;QACxD,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;QAC3B,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;QAC3B,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC/C,MAAM,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACjD,MAAM,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QACzD,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACvD,MAAM,CAAC,cAAc,CAAC,gCAAgC,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;IACzF,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAA;QACvE,MAAM,CAAC,cAAc,CAAC,2BAA2B,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;QAClF,MAAM,CAAC,cAAc,CAAC,mCAAmC,CAAC,CAAC,CAAC,IAAI,CAC9D,gCAAgC,CACjC,CAAA;QACD,MAAM,CAAC,cAAc,CAAC,oCAAoC,CAAC,CAAC,CAAC,IAAI,CAC/D,iCAAiC,CAClC,CAAA;IACH,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,cAAc,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QACvE,MAAM,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QACvD,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACrE,MAAM,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QACvD,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QACtD,MAAM,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,cAAc,CAAC,kCAAkC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACzE,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACzD,MAAM,CAAC,cAAc,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAA;IAC1E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC1C,MAAM,CAAC,YAAY,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,CAAC,YAAY,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACzD,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3C,MAAM,CAAC,YAAY,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrD,MAAM,CAAC,YAAY,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACzD,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9C,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-factory.d.ts","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"handler-factory.d.ts","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"AAgFA,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5C;AAMD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,IAsFtC,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAmJnE"}
|
|
@@ -8,6 +8,7 @@ import { getClientIp } from '../security/client-ip.js';
|
|
|
8
8
|
import { autoSeedAdmin } from '../setup/index.js';
|
|
9
9
|
import { setStorageAdapter } from '../storage/index.js';
|
|
10
10
|
import { getActuateConfig, setActuateConfig, setActuateCoreVersion } from '../config/runtime.js';
|
|
11
|
+
import { recordApiRequest } from '../diagnostics/api-metrics.js';
|
|
11
12
|
/**
|
|
12
13
|
* Routes that may legitimately receive a state-mutating request without a
|
|
13
14
|
* CSRF token. Matched by exact prefix on the rewritten (CMS-relative) path,
|
|
@@ -145,6 +146,35 @@ export function handleActuateAPI(options = {}) {
|
|
|
145
146
|
registerCMSRoutes(router);
|
|
146
147
|
let dbChecked = false;
|
|
147
148
|
return async function handler(request) {
|
|
149
|
+
// Capture the start *before* the try-block — we want the duration
|
|
150
|
+
// to include DB init / setup wiring too, since those costs are
|
|
151
|
+
// visible to the user as latency.
|
|
152
|
+
const startedAt = performance.now();
|
|
153
|
+
const pathname = (() => {
|
|
154
|
+
try {
|
|
155
|
+
return new URL(request.url).pathname;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return '/';
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
const response = await runHandler(request);
|
|
162
|
+
// Fire-and-forget — must NOT delay the response. Wrapping in a
|
|
163
|
+
// try/catch on the synchronous parts of `recordApiRequest` is
|
|
164
|
+
// belt-and-braces since the function itself swallows DB errors.
|
|
165
|
+
try {
|
|
166
|
+
recordApiRequest({
|
|
167
|
+
pathname,
|
|
168
|
+
status: response.status,
|
|
169
|
+
durationMs: performance.now() - startedAt,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* metric pipeline must never break user requests */
|
|
174
|
+
}
|
|
175
|
+
return response;
|
|
176
|
+
};
|
|
177
|
+
async function runHandler(request) {
|
|
148
178
|
try {
|
|
149
179
|
if (!isDBInitialized()) {
|
|
150
180
|
const client = options.prismaClient ??
|
|
@@ -244,6 +274,6 @@ export function handleActuateAPI(options = {}) {
|
|
|
244
274
|
headers: { 'Content-Type': 'application/json' },
|
|
245
275
|
});
|
|
246
276
|
}
|
|
247
|
-
}
|
|
277
|
+
}
|
|
248
278
|
}
|
|
249
279
|
//# sourceMappingURL=handler-factory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-factory.js","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACpE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"handler-factory.js","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAwnB5C,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AA2ND,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA+lOzD"}
|
package/dist/api/handlers.js
CHANGED
|
@@ -21,6 +21,7 @@ import { publishNotification } from '../realtime/notification-bus.js';
|
|
|
21
21
|
import { createNotificationStreamResponse } from '../realtime/notification-stream.js';
|
|
22
22
|
import { schedulingCronHandler } from '../scheduling/index.js';
|
|
23
23
|
import { isAuthorizedCronRequest, processCleanup, processSeoScan } from '../cron/index.js';
|
|
24
|
+
import { processContentHealthScan } from '../content-health/cron.js';
|
|
24
25
|
import { verifyCaptcha, getCaptchaConfig } from '../security/captcha.js';
|
|
25
26
|
import { checkForUpdates } from '../upgrade/version-check.js';
|
|
26
27
|
import { createUpgradePR } from '../upgrade/upgrade-pr.js';
|
|
@@ -2520,6 +2521,31 @@ export function registerCMSRoutes(router) {
|
|
|
2520
2521
|
statusCounts: {},
|
|
2521
2522
|
recentDocuments: [],
|
|
2522
2523
|
};
|
|
2524
|
+
const EMPTY_DELIVERY_STATS = {
|
|
2525
|
+
hasData: false,
|
|
2526
|
+
requests24h: 0,
|
|
2527
|
+
requestsPrev24h: 0,
|
|
2528
|
+
deltaPercent: null,
|
|
2529
|
+
errorRate: 0,
|
|
2530
|
+
errorCount: 0,
|
|
2531
|
+
avgLatencyMs: 0,
|
|
2532
|
+
p95LatencyMs: 0,
|
|
2533
|
+
maxLatencyMs: 0,
|
|
2534
|
+
webhookCount: 0,
|
|
2535
|
+
webhookActiveCount: 0,
|
|
2536
|
+
};
|
|
2537
|
+
const EMPTY_CONTENT_HEALTH = {
|
|
2538
|
+
score: 100,
|
|
2539
|
+
label: 'Good',
|
|
2540
|
+
counts: {
|
|
2541
|
+
MISSING_META_DESCRIPTION: 0,
|
|
2542
|
+
MISSING_ALT_TEXT: 0,
|
|
2543
|
+
BROKEN_INTERNAL_LINK: 0,
|
|
2544
|
+
OUTDATED_CONTENT: 0,
|
|
2545
|
+
},
|
|
2546
|
+
totalIssues: 0,
|
|
2547
|
+
lastScanAt: null,
|
|
2548
|
+
};
|
|
2523
2549
|
async function safeGroupBy(model, by, where) {
|
|
2524
2550
|
if (!model || typeof model !== 'object')
|
|
2525
2551
|
return [];
|
|
@@ -2650,6 +2676,227 @@ export function registerCMSRoutes(router) {
|
|
|
2650
2676
|
}
|
|
2651
2677
|
});
|
|
2652
2678
|
// ---------------------------------------------------------------------------
|
|
2679
|
+
// Content Health — backs the dashboard "Content Health" issue rows
|
|
2680
|
+
// ---------------------------------------------------------------------------
|
|
2681
|
+
//
|
|
2682
|
+
// Aggregates rows from `actuate_content_issues` (populated nightly by
|
|
2683
|
+
// /cron/content-health) by `type`, returning the totals the dashboard
|
|
2684
|
+
// surfaces. We also compute an overall content-health score so the
|
|
2685
|
+
// big number + progress bar stay consistent with the per-row counts.
|
|
2686
|
+
//
|
|
2687
|
+
// Score model:
|
|
2688
|
+
// - Start at 100.
|
|
2689
|
+
// - Subtract 5 per missing-meta-description issue.
|
|
2690
|
+
// - Subtract 2 per missing-alt image.
|
|
2691
|
+
// - Subtract 10 per broken internal link.
|
|
2692
|
+
// - Subtract 3 per outdated document.
|
|
2693
|
+
// Floor at 0. The weights reflect SEO impact (broken links and
|
|
2694
|
+
// missing meta hit hardest) and are kept here, not in the DB, so we
|
|
2695
|
+
// can tune them without a migration.
|
|
2696
|
+
router.get('/stats/content-health', async (request) => {
|
|
2697
|
+
try {
|
|
2698
|
+
if (!isSecretMissing()) {
|
|
2699
|
+
const auth = await requireAuth(request);
|
|
2700
|
+
if (auth.error)
|
|
2701
|
+
return auth.error;
|
|
2702
|
+
}
|
|
2703
|
+
let d;
|
|
2704
|
+
try {
|
|
2705
|
+
d = db();
|
|
2706
|
+
}
|
|
2707
|
+
catch {
|
|
2708
|
+
return json({ data: EMPTY_CONTENT_HEALTH });
|
|
2709
|
+
}
|
|
2710
|
+
if (!d?.contentIssue) {
|
|
2711
|
+
return json({ data: EMPTY_CONTENT_HEALTH });
|
|
2712
|
+
}
|
|
2713
|
+
let rows = [];
|
|
2714
|
+
try {
|
|
2715
|
+
rows = await d.contentIssue.groupBy({
|
|
2716
|
+
by: ['type'],
|
|
2717
|
+
_sum: { count: true },
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
catch {
|
|
2721
|
+
return json({ data: EMPTY_CONTENT_HEALTH });
|
|
2722
|
+
}
|
|
2723
|
+
const counts = {
|
|
2724
|
+
MISSING_META_DESCRIPTION: 0,
|
|
2725
|
+
MISSING_ALT_TEXT: 0,
|
|
2726
|
+
BROKEN_INTERNAL_LINK: 0,
|
|
2727
|
+
OUTDATED_CONTENT: 0,
|
|
2728
|
+
};
|
|
2729
|
+
for (const r of rows) {
|
|
2730
|
+
counts[r.type] = r._sum.count ?? 0;
|
|
2731
|
+
}
|
|
2732
|
+
// Read-out via locals so TS narrows away the Record<string,..>
|
|
2733
|
+
// indexed-access undefined union — we know the keys are present
|
|
2734
|
+
// because we initialised them above.
|
|
2735
|
+
const cMeta = counts['MISSING_META_DESCRIPTION'] ?? 0;
|
|
2736
|
+
const cAlt = counts['MISSING_ALT_TEXT'] ?? 0;
|
|
2737
|
+
const cLinks = counts['BROKEN_INTERNAL_LINK'] ?? 0;
|
|
2738
|
+
const cOutdated = counts['OUTDATED_CONTENT'] ?? 0;
|
|
2739
|
+
// Penalty model — see comment above.
|
|
2740
|
+
let score = 100;
|
|
2741
|
+
score -= cMeta * 5;
|
|
2742
|
+
score -= cAlt * 2;
|
|
2743
|
+
score -= cLinks * 10;
|
|
2744
|
+
score -= cOutdated * 3;
|
|
2745
|
+
score = Math.max(0, Math.min(100, score));
|
|
2746
|
+
// Last scan timestamp — useful for the dashboard to display
|
|
2747
|
+
// "Last scanned X minutes ago" so editors don't think the
|
|
2748
|
+
// numbers are live.
|
|
2749
|
+
let lastScanAt = null;
|
|
2750
|
+
try {
|
|
2751
|
+
const latest = await d.contentIssue.findFirst({
|
|
2752
|
+
orderBy: { lastSeenAt: 'desc' },
|
|
2753
|
+
select: { lastSeenAt: true },
|
|
2754
|
+
});
|
|
2755
|
+
lastScanAt = latest?.lastSeenAt ?? null;
|
|
2756
|
+
}
|
|
2757
|
+
catch {
|
|
2758
|
+
/* ignore — empty table or stale schema */
|
|
2759
|
+
}
|
|
2760
|
+
return json({
|
|
2761
|
+
data: {
|
|
2762
|
+
score,
|
|
2763
|
+
label: score >= 70 ? 'Good' : score >= 40 ? 'Fair' : 'Poor',
|
|
2764
|
+
counts,
|
|
2765
|
+
totalIssues: cMeta + cAlt + cLinks + cOutdated,
|
|
2766
|
+
lastScanAt: lastScanAt?.toISOString() ?? null,
|
|
2767
|
+
},
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
catch {
|
|
2771
|
+
return json({ data: EMPTY_CONTENT_HEALTH });
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
// ---------------------------------------------------------------------------
|
|
2775
|
+
// Delivery API metrics — backs the dashboard "Delivery API" tiles
|
|
2776
|
+
// ---------------------------------------------------------------------------
|
|
2777
|
+
//
|
|
2778
|
+
// Reads from `actuate_api_request_metrics` (per-minute pre-aggregated
|
|
2779
|
+
// counters populated by `recordApiRequest` in handler-factory.ts) and
|
|
2780
|
+
// folds the last 24h into the four scalars the dashboard surfaces:
|
|
2781
|
+
//
|
|
2782
|
+
// - requests24h, requestsPrev24h (delta = "+12% vs yesterday")
|
|
2783
|
+
// - errorRate (4xx + 5xx as fraction of total)
|
|
2784
|
+
// - avgLatencyMs, p95LatencyMs (latter via histogram merge)
|
|
2785
|
+
// - webhooks (total + active count)
|
|
2786
|
+
//
|
|
2787
|
+
// The endpoint returns a complete, never-undefined object even when
|
|
2788
|
+
// the metrics table is empty — the dashboard distinguishes
|
|
2789
|
+
// "no traffic yet" from "table missing" via the boolean `hasData`
|
|
2790
|
+
// field so it can render an empty state without an extra round-trip.
|
|
2791
|
+
router.get('/stats/api-delivery', async (request) => {
|
|
2792
|
+
try {
|
|
2793
|
+
if (!isSecretMissing()) {
|
|
2794
|
+
const auth = await requireAuth(request);
|
|
2795
|
+
if (auth.error)
|
|
2796
|
+
return auth.error;
|
|
2797
|
+
}
|
|
2798
|
+
let d;
|
|
2799
|
+
try {
|
|
2800
|
+
d = db();
|
|
2801
|
+
}
|
|
2802
|
+
catch {
|
|
2803
|
+
return json({ data: EMPTY_DELIVERY_STATS });
|
|
2804
|
+
}
|
|
2805
|
+
if (!d?.apiRequestMetric) {
|
|
2806
|
+
return json({ data: EMPTY_DELIVERY_STATS });
|
|
2807
|
+
}
|
|
2808
|
+
const now = new Date();
|
|
2809
|
+
const since24h = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
2810
|
+
const sincePrev24h = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
2811
|
+
const [currentResult, prevResult, webhookTotalResult, webhookActiveResult] = await Promise.allSettled([
|
|
2812
|
+
safeFindMany(d.apiRequestMetric, {
|
|
2813
|
+
where: { bucketStart: { gte: since24h } },
|
|
2814
|
+
select: {
|
|
2815
|
+
statusBucket: true,
|
|
2816
|
+
count: true,
|
|
2817
|
+
sumLatencyMs: true,
|
|
2818
|
+
maxLatencyMs: true,
|
|
2819
|
+
latencyHistogram: true,
|
|
2820
|
+
},
|
|
2821
|
+
}),
|
|
2822
|
+
safeFindMany(d.apiRequestMetric, {
|
|
2823
|
+
where: { bucketStart: { gte: sincePrev24h, lt: since24h } },
|
|
2824
|
+
select: { count: true },
|
|
2825
|
+
}),
|
|
2826
|
+
safeCount(d.webhookEndpoint),
|
|
2827
|
+
safeCount(d.webhookEndpoint, { active: true }),
|
|
2828
|
+
]);
|
|
2829
|
+
const currentRows = currentResult.status === 'fulfilled' ? currentResult.value : [];
|
|
2830
|
+
const prevRows = prevResult.status === 'fulfilled' ? prevResult.value : [];
|
|
2831
|
+
let total = 0;
|
|
2832
|
+
let errorTotal = 0; // 4xx + 5xx
|
|
2833
|
+
let sumLatency = 0;
|
|
2834
|
+
let maxLatency = 0;
|
|
2835
|
+
// Histogram merge for p95 — fixed 12-bucket array (see
|
|
2836
|
+
// diagnostics/api-metrics.ts `HISTOGRAM_LENGTH`).
|
|
2837
|
+
const mergedHistogram = new Array(12).fill(0);
|
|
2838
|
+
for (const row of currentRows) {
|
|
2839
|
+
total += row.count;
|
|
2840
|
+
if (row.statusBucket === '4xx' || row.statusBucket === '5xx')
|
|
2841
|
+
errorTotal += row.count;
|
|
2842
|
+
sumLatency += row.sumLatencyMs;
|
|
2843
|
+
if (row.maxLatencyMs > maxLatency)
|
|
2844
|
+
maxLatency = row.maxLatencyMs;
|
|
2845
|
+
const h = row.latencyHistogram;
|
|
2846
|
+
if (Array.isArray(h)) {
|
|
2847
|
+
for (let i = 0; i < mergedHistogram.length; i++) {
|
|
2848
|
+
const v = h[i];
|
|
2849
|
+
const current = mergedHistogram[i] ?? 0;
|
|
2850
|
+
if (typeof v === 'number')
|
|
2851
|
+
mergedHistogram[i] = current + v;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
const prevTotal = prevRows.reduce((s, r) => s + r.count, 0);
|
|
2856
|
+
const delta = prevTotal === 0 ? null : Math.round(((total - prevTotal) / prevTotal) * 1000) / 10; // 1-decimal %
|
|
2857
|
+
const errorRate = total === 0 ? 0 : errorTotal / total;
|
|
2858
|
+
const avgLatency = total === 0 ? 0 : sumLatency / total;
|
|
2859
|
+
// p95: walk the merged histogram cumulatively, return the upper
|
|
2860
|
+
// bound of the bucket that crosses the 0.95 watermark.
|
|
2861
|
+
const LATENCY_BUCKET_UPPER = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024];
|
|
2862
|
+
let p95Latency = 0;
|
|
2863
|
+
if (total > 0) {
|
|
2864
|
+
const target = total * 0.95;
|
|
2865
|
+
let acc = 0;
|
|
2866
|
+
for (let i = 0; i < mergedHistogram.length; i++) {
|
|
2867
|
+
acc += mergedHistogram[i] ?? 0;
|
|
2868
|
+
if (acc >= target) {
|
|
2869
|
+
p95Latency =
|
|
2870
|
+
i < LATENCY_BUCKET_UPPER.length
|
|
2871
|
+
? LATENCY_BUCKET_UPPER[i]
|
|
2872
|
+
: LATENCY_BUCKET_UPPER[LATENCY_BUCKET_UPPER.length - 1] * 2;
|
|
2873
|
+
break;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
const webhookTotal = webhookTotalResult.status === 'fulfilled' ? webhookTotalResult.value : 0;
|
|
2878
|
+
const webhookActive = webhookActiveResult.status === 'fulfilled' ? webhookActiveResult.value : 0;
|
|
2879
|
+
return json({
|
|
2880
|
+
data: {
|
|
2881
|
+
hasData: total > 0,
|
|
2882
|
+
requests24h: total,
|
|
2883
|
+
requestsPrev24h: prevTotal,
|
|
2884
|
+
deltaPercent: delta,
|
|
2885
|
+
errorRate: Math.round(errorRate * 10000) / 10000, // 4-dec precision
|
|
2886
|
+
errorCount: errorTotal,
|
|
2887
|
+
avgLatencyMs: Math.round(avgLatency * 10) / 10,
|
|
2888
|
+
p95LatencyMs: p95Latency,
|
|
2889
|
+
maxLatencyMs: Math.round(maxLatency * 10) / 10,
|
|
2890
|
+
webhookCount: webhookTotal,
|
|
2891
|
+
webhookActiveCount: webhookActive,
|
|
2892
|
+
},
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
catch {
|
|
2896
|
+
return json({ data: EMPTY_DELIVERY_STATS });
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
// ---------------------------------------------------------------------------
|
|
2653
2900
|
// Global search
|
|
2654
2901
|
// ---------------------------------------------------------------------------
|
|
2655
2902
|
router.get('/search/global', async (request) => {
|
|
@@ -5420,6 +5667,22 @@ export function registerCMSRoutes(router) {
|
|
|
5420
5667
|
};
|
|
5421
5668
|
router.get('/cron/seo-scan', cronSeoScan);
|
|
5422
5669
|
router.post('/cron/seo-scan', cronSeoScan);
|
|
5670
|
+
const cronContentHealth = async (request) => {
|
|
5671
|
+
try {
|
|
5672
|
+
if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
|
|
5673
|
+
return errorResponse('Unauthorized', 401);
|
|
5674
|
+
}
|
|
5675
|
+
const config = getActuateConfig();
|
|
5676
|
+
const siteOrigin = config?.site?.url ?? config?.seo?.siteUrl ?? process.env.NEXT_PUBLIC_SITE_URL ?? null;
|
|
5677
|
+
const result = await processContentHealthScan(db(), { siteOrigin });
|
|
5678
|
+
return json({ data: result });
|
|
5679
|
+
}
|
|
5680
|
+
catch (err) {
|
|
5681
|
+
return internalError(err, 'cron content-health');
|
|
5682
|
+
}
|
|
5683
|
+
};
|
|
5684
|
+
router.get('/cron/content-health', cronContentHealth);
|
|
5685
|
+
router.post('/cron/content-health', cronContentHealth);
|
|
5423
5686
|
router.get('/scheduling/calendar', async (request) => {
|
|
5424
5687
|
try {
|
|
5425
5688
|
const auth = await requireAuth(request);
|