@actuate-media/cms-core 0.25.1 → 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__/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/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
package/prisma/schema.prisma
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
generator client {
|
|
2
|
-
provider
|
|
3
|
-
output
|
|
2
|
+
provider = "prisma-client"
|
|
3
|
+
output = "../generated"
|
|
4
4
|
// `relationJoins` is the Preview-feature flag that makes
|
|
5
5
|
// `relationLoadStrategy: 'join'` a recognised argument on
|
|
6
6
|
// findUnique / findMany / findFirst. Without it the option is
|
|
@@ -73,6 +73,26 @@ enum AuditEvent {
|
|
|
73
73
|
import_completed
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
enum ContentIssueType {
|
|
77
|
+
// Document is published but missing a `metaDescription` /
|
|
78
|
+
// `seoDescription` field — the dashboard surfaces these so editors
|
|
79
|
+
// can fix the SERP snippet before a competitor outranks them.
|
|
80
|
+
MISSING_META_DESCRIPTION
|
|
81
|
+
// Document HTML contains <img> tags without alt attributes. Hurts
|
|
82
|
+
// both a11y and SEO. Counted per *document* not per image — so a
|
|
83
|
+
// page with 5 alt-less images is 1 row, not 5, with `details.count`
|
|
84
|
+
// capturing the per-document count.
|
|
85
|
+
MISSING_ALT_TEXT
|
|
86
|
+
// Internal link (same-origin URL or relative path) returned 404 /
|
|
87
|
+
// 410 / DNS-failure when the link checker probed it. `details.url`
|
|
88
|
+
// holds the offending URL.
|
|
89
|
+
BROKEN_INTERNAL_LINK
|
|
90
|
+
// Published document not touched in >90 days. Encourages refresh
|
|
91
|
+
// of stale content — particularly important for "best of YEAR"
|
|
92
|
+
// posts that decay rapidly.
|
|
93
|
+
OUTDATED_CONTENT
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
enum NotificationType {
|
|
77
97
|
DOCUMENT_PUBLISHED
|
|
78
98
|
DOCUMENT_ASSIGNED
|
|
@@ -143,31 +163,31 @@ model User {
|
|
|
143
163
|
createdAt DateTime @default(now())
|
|
144
164
|
updatedAt DateTime @updatedAt
|
|
145
165
|
|
|
146
|
-
sessions
|
|
147
|
-
oauthAccounts
|
|
148
|
-
apiKeys
|
|
149
|
-
documents
|
|
150
|
-
updatedDocuments
|
|
151
|
-
versions
|
|
152
|
-
media
|
|
153
|
-
contentLocks
|
|
154
|
-
notifications
|
|
155
|
-
contentTemplates
|
|
156
|
-
workflowAssigned
|
|
157
|
-
passwordResetTokens
|
|
166
|
+
sessions Session[]
|
|
167
|
+
oauthAccounts OAuthAccount[]
|
|
168
|
+
apiKeys ApiKey[]
|
|
169
|
+
documents Document[] @relation("CreatedDocuments")
|
|
170
|
+
updatedDocuments Document[] @relation("UpdatedDocuments")
|
|
171
|
+
versions Version[]
|
|
172
|
+
media Media[]
|
|
173
|
+
contentLocks ContentLock[]
|
|
174
|
+
notifications InAppNotification[]
|
|
175
|
+
contentTemplates ContentTemplate[]
|
|
176
|
+
workflowAssigned WorkflowState[]
|
|
177
|
+
passwordResetTokens PasswordResetToken[]
|
|
158
178
|
|
|
159
179
|
@@map("actuate_users")
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
model OAuthAccount {
|
|
163
|
-
id String
|
|
183
|
+
id String @id @default(cuid())
|
|
164
184
|
userId String
|
|
165
185
|
provider String
|
|
166
186
|
providerAccountId String
|
|
167
187
|
accessToken String?
|
|
168
188
|
refreshToken String?
|
|
169
189
|
expiresAt DateTime?
|
|
170
|
-
createdAt DateTime
|
|
190
|
+
createdAt DateTime @default(now())
|
|
171
191
|
|
|
172
192
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
173
193
|
|
|
@@ -247,40 +267,41 @@ model Folder {
|
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
model Document {
|
|
250
|
-
id
|
|
251
|
-
collection
|
|
252
|
-
title
|
|
253
|
-
slug
|
|
254
|
-
data
|
|
255
|
-
status
|
|
256
|
-
locale
|
|
257
|
-
localeGroupId
|
|
258
|
-
createdById
|
|
259
|
-
updatedById
|
|
260
|
-
publishedAt
|
|
261
|
-
scheduledAt
|
|
262
|
-
scheduledUnpublishAt
|
|
263
|
-
deletedAt
|
|
264
|
-
searchVector
|
|
265
|
-
plainText
|
|
266
|
-
contentHash
|
|
267
|
-
structuredData
|
|
268
|
-
contentGraph
|
|
269
|
-
siteId
|
|
270
|
-
templateId
|
|
271
|
-
workflowStage
|
|
272
|
-
reviewerId
|
|
273
|
-
reviewNote
|
|
274
|
-
folderId
|
|
275
|
-
createdAt
|
|
276
|
-
updatedAt
|
|
277
|
-
|
|
278
|
-
createdBy
|
|
279
|
-
updatedBy
|
|
280
|
-
folder
|
|
281
|
-
versions
|
|
282
|
-
mediaUsages
|
|
283
|
-
contentLocks
|
|
270
|
+
id String @id @default(cuid())
|
|
271
|
+
collection String
|
|
272
|
+
title String?
|
|
273
|
+
slug String?
|
|
274
|
+
data Json
|
|
275
|
+
status DocumentStatus @default(DRAFT)
|
|
276
|
+
locale String?
|
|
277
|
+
localeGroupId String?
|
|
278
|
+
createdById String
|
|
279
|
+
updatedById String
|
|
280
|
+
publishedAt DateTime?
|
|
281
|
+
scheduledAt DateTime?
|
|
282
|
+
scheduledUnpublishAt DateTime?
|
|
283
|
+
deletedAt DateTime?
|
|
284
|
+
searchVector String?
|
|
285
|
+
plainText String?
|
|
286
|
+
contentHash String?
|
|
287
|
+
structuredData Json?
|
|
288
|
+
contentGraph Json?
|
|
289
|
+
siteId String?
|
|
290
|
+
templateId String?
|
|
291
|
+
workflowStage WorkflowStage @default(DRAFT)
|
|
292
|
+
reviewerId String?
|
|
293
|
+
reviewNote String?
|
|
294
|
+
folderId String?
|
|
295
|
+
createdAt DateTime @default(now())
|
|
296
|
+
updatedAt DateTime @updatedAt
|
|
297
|
+
|
|
298
|
+
createdBy User @relation("CreatedDocuments", fields: [createdById], references: [id])
|
|
299
|
+
updatedBy User @relation("UpdatedDocuments", fields: [updatedById], references: [id])
|
|
300
|
+
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
|
301
|
+
versions Version[]
|
|
302
|
+
mediaUsages MediaUsage[]
|
|
303
|
+
contentLocks ContentLock[]
|
|
304
|
+
contentIssues ContentIssue[]
|
|
284
305
|
|
|
285
306
|
@@unique([collection, slug])
|
|
286
307
|
@@index([collection])
|
|
@@ -310,25 +331,25 @@ model Version {
|
|
|
310
331
|
}
|
|
311
332
|
|
|
312
333
|
model Media {
|
|
313
|
-
id
|
|
314
|
-
filename
|
|
315
|
-
storageKey
|
|
316
|
-
mimeType
|
|
317
|
-
fileSize
|
|
318
|
-
width
|
|
319
|
-
height
|
|
320
|
-
altText
|
|
321
|
-
title
|
|
322
|
-
uploadedById
|
|
323
|
-
focalPointX
|
|
324
|
-
focalPointY
|
|
325
|
-
blurHash
|
|
326
|
-
aiAltText
|
|
327
|
-
aiTags
|
|
334
|
+
id String @id @default(cuid())
|
|
335
|
+
filename String
|
|
336
|
+
storageKey String @unique
|
|
337
|
+
mimeType String
|
|
338
|
+
fileSize Int
|
|
339
|
+
width Int?
|
|
340
|
+
height Int?
|
|
341
|
+
altText String?
|
|
342
|
+
title String?
|
|
343
|
+
uploadedById String
|
|
344
|
+
focalPointX Float?
|
|
345
|
+
focalPointY Float?
|
|
346
|
+
blurHash String?
|
|
347
|
+
aiAltText String?
|
|
348
|
+
aiTags Json?
|
|
328
349
|
aiDescription String?
|
|
329
|
-
folderId
|
|
330
|
-
createdAt
|
|
331
|
-
updatedAt
|
|
350
|
+
folderId String?
|
|
351
|
+
createdAt DateTime @default(now())
|
|
352
|
+
updatedAt DateTime @updatedAt
|
|
332
353
|
|
|
333
354
|
uploadedBy User @relation(fields: [uploadedById], references: [id])
|
|
334
355
|
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
|
@@ -478,18 +499,18 @@ model WebhookEndpoint {
|
|
|
478
499
|
}
|
|
479
500
|
|
|
480
501
|
model WebhookDeliveryLog {
|
|
481
|
-
id String
|
|
502
|
+
id String @id @default(cuid())
|
|
482
503
|
endpointId String
|
|
483
504
|
event String
|
|
484
505
|
payload Json
|
|
485
|
-
status String
|
|
486
|
-
attempts Int
|
|
487
|
-
maxAttempts Int
|
|
506
|
+
status String @default("pending")
|
|
507
|
+
attempts Int @default(0)
|
|
508
|
+
maxAttempts Int @default(3)
|
|
488
509
|
lastAttemptAt DateTime?
|
|
489
510
|
nextRetryAt DateTime?
|
|
490
511
|
responseStatus Int?
|
|
491
512
|
responseBody String?
|
|
492
|
-
createdAt DateTime
|
|
513
|
+
createdAt DateTime @default(now())
|
|
493
514
|
|
|
494
515
|
endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade)
|
|
495
516
|
|
|
@@ -561,3 +582,89 @@ model SavedSection {
|
|
|
561
582
|
@@index([category])
|
|
562
583
|
@@map("actuate_saved_sections")
|
|
563
584
|
}
|
|
585
|
+
|
|
586
|
+
// ─── Dashboard metrics ─────────────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
/// Per-minute pre-aggregated request counters that power the
|
|
589
|
+
/// "Delivery API" dashboard tiles (request volume, error rate,
|
|
590
|
+
/// average latency, p95). One row per (bucketStart × route × statusBucket).
|
|
591
|
+
///
|
|
592
|
+
/// Why pre-aggregated minute buckets and not per-request rows:
|
|
593
|
+
/// - 24h of traffic at 10 req/s on 20 routes ≈ 17M raw rows; the
|
|
594
|
+
/// equivalent minute-bucketed table is ≤ 28,800 rows (1440 × 20).
|
|
595
|
+
/// - Vercel Functions are concurrency-bounded; an O(1) UPSERT per
|
|
596
|
+
/// request is cheaper than O(write+query) per dashboard load.
|
|
597
|
+
/// - 7-day retention is enforced by the
|
|
598
|
+
/// `/api/cms/cron/api-metrics-prune` cron, so the table stays
|
|
599
|
+
/// bounded.
|
|
600
|
+
///
|
|
601
|
+
/// `latencyHistogram` is a fixed 12-bucket log-scale array (ms):
|
|
602
|
+
/// [<1, 1-2, 2-4, 4-8, 8-16, 16-32, 32-64, 64-128, 128-256,
|
|
603
|
+
/// 256-512, 512-1024, >1024]
|
|
604
|
+
/// Cumulative-sum to estimate p95 with ~10% accuracy without
|
|
605
|
+
/// per-request samples. The `delivery-stats` reader merges
|
|
606
|
+
/// histograms across buckets before estimating.
|
|
607
|
+
model ApiRequestMetric {
|
|
608
|
+
id String @id @default(cuid())
|
|
609
|
+
/// UTC minute boundary (`floor(time, '1 minute')`). The unique
|
|
610
|
+
/// constraint on `(bucketStart, route, statusBucket)` lets the
|
|
611
|
+
/// request logger UPSERT in one round-trip per request.
|
|
612
|
+
bucketStart DateTime
|
|
613
|
+
/// Normalised request route (e.g. `/collections/posts` — never
|
|
614
|
+
/// the per-document `/collections/posts/abc123`). Normalisation
|
|
615
|
+
/// happens in `instrumentation.ts` so the cardinality stays
|
|
616
|
+
/// bounded.
|
|
617
|
+
route String
|
|
618
|
+
/// HTTP status family — one of `2xx`, `3xx`, `4xx`, `5xx`,
|
|
619
|
+
/// `other`. Storing the bucket rather than the raw status lets
|
|
620
|
+
/// us answer "error rate?" with a single GROUP BY.
|
|
621
|
+
statusBucket String
|
|
622
|
+
count Int @default(0)
|
|
623
|
+
/// Sum of `responseMs` across `count` requests. Average latency
|
|
624
|
+
/// = `sum(sumLatencyMs) / sum(count)` over the window.
|
|
625
|
+
sumLatencyMs Float @default(0)
|
|
626
|
+
/// Max single-request latency in this bucket. Useful as a
|
|
627
|
+
/// fast-path tail-latency indicator and as a sanity check on
|
|
628
|
+
/// the histogram estimator.
|
|
629
|
+
maxLatencyMs Float @default(0)
|
|
630
|
+
/// 12-bucket log-scale histogram (see model doc). Stored as
|
|
631
|
+
/// `Json` so the schema doesn't have to change when we revise
|
|
632
|
+
/// bucket boundaries.
|
|
633
|
+
latencyHistogram Json
|
|
634
|
+
updatedAt DateTime @updatedAt
|
|
635
|
+
|
|
636
|
+
@@unique([bucketStart, route, statusBucket])
|
|
637
|
+
@@index([bucketStart])
|
|
638
|
+
@@map("actuate_api_request_metrics")
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/// Persisted content-quality issues surfaced by the nightly
|
|
642
|
+
/// content-health scan. The Dashboard's "Content Health" card
|
|
643
|
+
/// aggregates these rows by `type` to produce the issue list.
|
|
644
|
+
///
|
|
645
|
+
/// Upsert key is `(documentId, type)` so the scanner is
|
|
646
|
+
/// idempotent — re-running yields the same row count, and a row
|
|
647
|
+
/// "self-resolves" by being absent from the next scan (the
|
|
648
|
+
/// scanner uses a `firstScanAt`-watermark + delete pattern).
|
|
649
|
+
model ContentIssue {
|
|
650
|
+
id String @id @default(cuid())
|
|
651
|
+
documentId String
|
|
652
|
+
type ContentIssueType
|
|
653
|
+
/// Per-issue count (e.g. 5 alt-less images = `count: 5` even
|
|
654
|
+
/// though it's 1 row). Lets the dashboard show real numbers
|
|
655
|
+
/// without re-scanning the document HTML on each page load.
|
|
656
|
+
count Int @default(1)
|
|
657
|
+
/// Type-specific payload — the broken URL, the page's
|
|
658
|
+
/// `updatedAt`, the offending image src, etc. JSON so each
|
|
659
|
+
/// issue type can carry its own shape without schema churn.
|
|
660
|
+
details Json?
|
|
661
|
+
firstSeenAt DateTime @default(now())
|
|
662
|
+
lastSeenAt DateTime @default(now())
|
|
663
|
+
|
|
664
|
+
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
|
665
|
+
|
|
666
|
+
@@unique([documentId, type])
|
|
667
|
+
@@index([type])
|
|
668
|
+
@@index([documentId])
|
|
669
|
+
@@map("actuate_content_issues")
|
|
670
|
+
}
|