@growth-labs/cms 0.6.8 → 0.8.1
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/engine/fronts-publish-intent.d.ts +25 -1
- package/dist/engine/fronts-publish-intent.d.ts.map +1 -1
- package/dist/engine/fronts-publish-intent.js +64 -11
- package/dist/engine/fronts-publish-intent.js.map +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/publication.d.ts.map +1 -1
- package/dist/engine/publication.js +7 -3
- package/dist/engine/publication.js.map +1 -1
- package/dist/engine/published-content.d.ts +10 -0
- package/dist/engine/published-content.d.ts.map +1 -1
- package/dist/engine/published-content.js +3 -0
- package/dist/engine/published-content.js.map +1 -1
- package/dist/engine/publisher.d.ts +26 -2
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +113 -6
- package/dist/engine/publisher.js.map +1 -1
- package/dist/engine/video-source.d.ts +26 -0
- package/dist/engine/video-source.d.ts.map +1 -0
- package/dist/engine/video-source.js +31 -0
- package/dist/engine/video-source.js.map +1 -0
- package/dist/providers/types.d.ts +40 -3
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +17 -0
- package/dist/providers/types.js.map +1 -1
- package/dist/routes/calendar.d.ts +26 -0
- package/dist/routes/calendar.d.ts.map +1 -1
- package/dist/routes/calendar.js +41 -0
- package/dist/routes/calendar.js.map +1 -1
- package/dist/routes/content.d.ts +2 -0
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/fronts-publish.d.ts.map +1 -1
- package/dist/routes/fronts-publish.js +1 -0
- package/dist/routes/fronts-publish.js.map +1 -1
- package/dist/ui/api/_content-config.d.ts.map +1 -1
- package/dist/ui/api/_content-config.js +1 -0
- package/dist/ui/api/_content-config.js.map +1 -1
- package/dist/ui/api/calendar.d.ts.map +1 -1
- package/dist/ui/api/calendar.js +5 -1
- package/dist/ui/api/calendar.js.map +1 -1
- package/dist/ui/screens/CalendarScreen.d.ts.map +1 -1
- package/dist/ui/screens/CalendarScreen.js +10 -1
- package/dist/ui/screens/CalendarScreen.js.map +1 -1
- package/dist/ui/screens/calendar-data.d.ts +3 -0
- package/dist/ui/screens/calendar-data.d.ts.map +1 -1
- package/dist/ui/screens/calendar-data.js.map +1 -1
- package/dist/ui/screens/go-publish-badge.d.ts +11 -0
- package/dist/ui/screens/go-publish-badge.d.ts.map +1 -0
- package/dist/ui/screens/go-publish-badge.js +47 -0
- package/dist/ui/screens/go-publish-badge.js.map +1 -0
- package/package.json +1 -1
- package/src/engine/fronts-publish-intent.ts +104 -15
- package/src/engine/index.ts +5 -0
- package/src/engine/publication.ts +7 -1
- package/src/engine/published-content.ts +13 -0
- package/src/engine/publisher.ts +119 -4
- package/src/engine/video-source.ts +45 -0
- package/src/providers/types.ts +51 -1
- package/src/routes/calendar.ts +72 -0
- package/src/routes/content.ts +2 -0
- package/src/routes/fronts-publish.ts +1 -0
- package/src/ui/api/_content-config.ts +2 -0
- package/src/ui/api/calendar.ts +5 -1
- package/src/ui/screens/CalendarScreen.tsx +22 -0
- package/src/ui/screens/calendar-data.ts +4 -0
- package/src/ui/screens/go-publish-badge.ts +55 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar-data.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/calendar-data.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"calendar-data.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/calendar-data.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAEjE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,4FAA4F;IAC5F,SAAS,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACpC;AAED,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,IAAI,CAAA;IACX,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,KAAK,CAAC,EAAE,SAAS,CAAA;CACjB;AAED,mDAAmD;AACnD,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,KAAK,CAAA;IACZ,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,qDAAqD;IACrD,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,YAAY,EAAE,CAAA;CACrB;AAED,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,CAAA;AAM9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,YAAY,EAAE,EACrB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EAAE,UAAU;AACzB,GAAG,GAAE,MAAmB,GACtB,YAAY,EAAE,CAuDhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar-data.js","sourceRoot":"","sources":["../../../src/ui/screens/calendar-data.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,oBAAoB;AACpB,EAAE;AACF,WAAW;AACX,kEAAkE;AAClE,gFAAgF;AAChF,gFAAgF;
|
|
1
|
+
{"version":3,"file":"calendar-data.js","sourceRoot":"","sources":["../../../src/ui/screens/calendar-data.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,oBAAoB;AACpB,EAAE;AACF,WAAW;AACX,kEAAkE;AAClE,gFAAgF;AAChF,gFAAgF;AA2ChF,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CACjC,KAAqB,EACrB,IAAY,EACZ,KAAa,EAAE,UAAU;AACzB,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,6EAA6E;IAE7E,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA,CAAC,sBAAsB;IAE7E,2DAA2D;IAC3D,0EAA0E;IAC1E,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;IACtD,MAAM,aAAa,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IAExC,6EAA6E;IAE7E,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;IAC7B,MAAM,QAAQ,GACb,OAAO,CAAC,WAAW,EAAE,KAAK,IAAI,IAAI,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAE9F,6EAA6E;IAE7E,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAA;IAEjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,0EAA0E;QAC1E,+DAA+D;QAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAA;QACrB,0DAA0D;QAC1D,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA,CAAC,eAAe;QACjD,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAElD,IAAI,CAAC,KAAK,IAAI,IAAI,EAAE,KAAK,KAAK;YAAE,SAAQ,CAAC,qBAAqB;QAE9D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACnC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACvB,CAAC;IAED,6EAA6E;IAE7E,MAAM,KAAK,GAAmB,EAAE,CAAA;IAEhC,sBAAsB;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5B,CAAC;IAED,YAAY;IACZ,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,IAAI,WAAW,EAAE,GAAG,EAAE,EAAE,CAAC;QAC7C,KAAK,CAAC,IAAI,CAAC;YACV,KAAK,EAAE,KAAK;YACZ,GAAG;YACH,KAAK,EAAE,GAAG,KAAK,QAAQ;YACvB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE;SAC7B,CAAC,CAAA;IACH,CAAC;IAED,OAAO,KAAK,CAAA;AACb,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CalendarGoPublish } from '../../routes/calendar.js';
|
|
2
|
+
import type { FoundryPillState } from '../inspector/foundry-stages.js';
|
|
3
|
+
export interface GoPublishBadge {
|
|
4
|
+
label: string;
|
|
5
|
+
pill: FoundryPillState;
|
|
6
|
+
tooltip: string;
|
|
7
|
+
}
|
|
8
|
+
/** Format a unix-second receipt time for the tooltip (UTC, to the minute). */
|
|
9
|
+
export declare function formatReceiptTime(unixSeconds: number): string;
|
|
10
|
+
export declare function goPublishBadge(goPublish: CalendarGoPublish | null | undefined): GoPublishBadge | null;
|
|
11
|
+
//# sourceMappingURL=go-publish-badge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"go-publish-badge.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/go-publish-badge.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AAEtE,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,gBAAgB,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CACf;AAcD,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,cAAc,CAC7B,SAAS,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,GAC7C,cAAc,GAAG,IAAI,CAcvB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/ui/screens/go-publish-badge.ts — pure badge model for a scheduled media
|
|
2
|
+
// item's Go publish state (W1-08b).
|
|
3
|
+
//
|
|
4
|
+
// goPublishBadge(goPublish) → { label, pill, tooltip } | null
|
|
5
|
+
// label — the word the chip shows ("Captured", "Processing", …)
|
|
6
|
+
// pill — the FoundryPillState bucket the broadsheet pill classes render
|
|
7
|
+
// (working / ready / failed), reused from foundry-stages
|
|
8
|
+
// tooltip — the hover text: label, the newest receipt and its time, the
|
|
9
|
+
// correlation id, and the blocked reason when there is one
|
|
10
|
+
//
|
|
11
|
+
// Pure — no DOM, no React, fully vitest-testable.
|
|
12
|
+
const LABELS = {
|
|
13
|
+
pending: { label: 'Not captured', pill: 'working' },
|
|
14
|
+
captured: { label: 'Captured', pill: 'working' },
|
|
15
|
+
processing: { label: 'Processing', pill: 'working' },
|
|
16
|
+
media_ready: { label: 'Media ready', pill: 'ready' },
|
|
17
|
+
publishing: { label: 'Publishing', pill: 'working' },
|
|
18
|
+
published: { label: 'Published', pill: 'ready' },
|
|
19
|
+
late: { label: 'Late', pill: 'failed' },
|
|
20
|
+
failed: { label: 'Blocked', pill: 'failed' },
|
|
21
|
+
unavailable: { label: 'State unavailable', pill: 'failed' },
|
|
22
|
+
};
|
|
23
|
+
/** Format a unix-second receipt time for the tooltip (UTC, to the minute). */
|
|
24
|
+
export function formatReceiptTime(unixSeconds) {
|
|
25
|
+
return `${new Date(unixSeconds * 1000).toISOString().slice(0, 16).replace('T', ' ')} UTC`;
|
|
26
|
+
}
|
|
27
|
+
export function goPublishBadge(goPublish) {
|
|
28
|
+
if (!goPublish)
|
|
29
|
+
return null;
|
|
30
|
+
const entry = LABELS[goPublish.state] ?? { label: goPublish.state, pill: 'working' };
|
|
31
|
+
const lines = [`Go publish: ${entry.label}`];
|
|
32
|
+
if (goPublish.eventKind && goPublish.receiptAt !== null) {
|
|
33
|
+
lines.push(`Last receipt: ${goPublish.eventKind} at ${formatReceiptTime(goPublish.receiptAt)}`);
|
|
34
|
+
}
|
|
35
|
+
else if (goPublish.eventKind) {
|
|
36
|
+
lines.push(`Last receipt: ${goPublish.eventKind}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
lines.push('No receipt yet');
|
|
40
|
+
}
|
|
41
|
+
if (goPublish.correlationId)
|
|
42
|
+
lines.push(`Correlation: ${goPublish.correlationId}`);
|
|
43
|
+
if (goPublish.blockedReason)
|
|
44
|
+
lines.push(`Reason: ${goPublish.blockedReason}`);
|
|
45
|
+
return { label: entry.label, pill: entry.pill, tooltip: lines.join('\n') };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=go-publish-badge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"go-publish-badge.js","sourceRoot":"","sources":["../../../src/ui/screens/go-publish-badge.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,oCAAoC;AACpC,EAAE;AACF,8DAA8D;AAC9D,oEAAoE;AACpE,6EAA6E;AAC7E,qEAAqE;AACrE,0EAA0E;AAC1E,uEAAuE;AACvE,EAAE;AACF,kDAAkD;AAWlD,MAAM,MAAM,GAA8D;IACzE,OAAO,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE;IACnD,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE;IAChD,UAAU,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE;IACpD,WAAW,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE;IACpD,UAAU,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE;IACpD,SAAS,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE;IAChD,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE;IACvC,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC5C,WAAW,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,QAAQ,EAAE;CAC3D,CAAA;AAED,8EAA8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACpD,OAAO,GAAG,IAAI,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAA;AAC1F,CAAC;AAED,MAAM,UAAU,cAAc,CAC7B,SAA+C;IAE/C,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAA;IAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,SAAkB,EAAE,CAAA;IAC7F,MAAM,KAAK,GAAG,CAAC,eAAe,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;IAC5C,IAAI,SAAS,CAAC,SAAS,IAAI,SAAS,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QACzD,KAAK,CAAC,IAAI,CAAC,iBAAiB,SAAS,CAAC,SAAS,OAAO,iBAAiB,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;IAChG,CAAC;SAAM,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,iBAAiB,SAAS,CAAC,SAAS,EAAE,CAAC,CAAA;IACnD,CAAC;SAAM,CAAC;QACP,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IAC7B,CAAC;IACD,IAAI,SAAS,CAAC,aAAa;QAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,SAAS,CAAC,aAAa,EAAE,CAAC,CAAA;IAClF,IAAI,SAAS,CAAC,aAAa;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,SAAS,CAAC,aAAa,EAAE,CAAC,CAAA;IAC7E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;AAC3E,CAAC"}
|
package/package.json
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
podcastPublishBlocker,
|
|
47
47
|
validPodcastSourceUrl,
|
|
48
48
|
} from './podcast-source.js'
|
|
49
|
+
import { frontsVideoSourceGeneration, videoSourceIngestableOverHttps } from './video-source.js'
|
|
49
50
|
|
|
50
51
|
/** The CMS's own name for the record this intent describes. */
|
|
51
52
|
export const FRONTS_PUBLISH_INTENT_SOURCE = 'masthead'
|
|
@@ -68,6 +69,7 @@ export type FrontsPublishIntentErrorCode =
|
|
|
68
69
|
| 'missing_trigger_token'
|
|
69
70
|
| 'missing_article_body'
|
|
70
71
|
| 'video_media_not_ready'
|
|
72
|
+
| 'video_source_duration_unavailable'
|
|
71
73
|
| 'missing_podcast_audio'
|
|
72
74
|
| PodcastPublishBlockerCode
|
|
73
75
|
|
|
@@ -142,6 +144,27 @@ export interface FrontsPublishPodcastSourceRef {
|
|
|
142
144
|
duration_seconds: number
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The video twin of `FrontsPublishPodcastSourceRef` (W1-02): the captured
|
|
149
|
+
* source of a scheduled video whose HLS is not ready, for the Go
|
|
150
|
+
* `media.pipeline` to produce from. `correlation_id` is
|
|
151
|
+
* `video_content.processing_correlation_id`, the id the Foundry's
|
|
152
|
+
* `hls.ready`/`transcript.ready` callbacks are applied under, and
|
|
153
|
+
* `visibility` is `content_items.visibility` at capture time, selecting the
|
|
154
|
+
* delivery profile (the delivery re-reads it at every boundary). Present only
|
|
155
|
+
* while the video is incomplete. Contract: `fronts-video-source.v1`.
|
|
156
|
+
*/
|
|
157
|
+
export interface FrontsPublishVideoSourceRef {
|
|
158
|
+
kind: 'https'
|
|
159
|
+
url: string
|
|
160
|
+
/** Immutable media identity; rescheduling alone does not change it. */
|
|
161
|
+
generation: string
|
|
162
|
+
/** Positive finite duration measured for the current captured source. */
|
|
163
|
+
duration_seconds: number
|
|
164
|
+
correlation_id: string
|
|
165
|
+
visibility: 'free' | 'premium'
|
|
166
|
+
}
|
|
167
|
+
|
|
145
168
|
export interface FrontsPublishLinkRef {
|
|
146
169
|
rel: 'canonical' | 'public'
|
|
147
170
|
url: string
|
|
@@ -155,6 +178,8 @@ export interface FrontsPublishContentRefs {
|
|
|
155
178
|
podcast_audio_hls: FrontsPublishPodcastAudioRef | null
|
|
156
179
|
/** Present only while media is incomplete and a valid source was captured. */
|
|
157
180
|
podcast_source: FrontsPublishPodcastSourceRef | null
|
|
181
|
+
/** Present only while a video's HLS is not ready and a valid source was captured. */
|
|
182
|
+
video_source: FrontsPublishVideoSourceRef | null
|
|
158
183
|
/** `[]` is valid. */
|
|
159
184
|
links: FrontsPublishLinkRef[]
|
|
160
185
|
}
|
|
@@ -319,11 +344,12 @@ interface IntentCandidateRow {
|
|
|
319
344
|
canonical_url: string | null
|
|
320
345
|
hero_image_id: string | null
|
|
321
346
|
social_image_id: string | null
|
|
347
|
+
visibility: 'free' | 'premium'
|
|
322
348
|
}
|
|
323
349
|
|
|
324
350
|
const CANDIDATE_COLUMNS = `id, type, slug, title, channel, publish_at, fronts_publish_status,
|
|
325
351
|
canonical_version, fronts_channel_targets, fronts_publish_trigger_token,
|
|
326
|
-
canonical_url, hero_image_id, social_image_id`
|
|
352
|
+
canonical_url, hero_image_id, social_image_id, visibility`
|
|
327
353
|
|
|
328
354
|
export interface HydrateFrontsPublishIntentsOptions {
|
|
329
355
|
/** Window upper bound, unix epoch seconds. Items due at/before this are considered. */
|
|
@@ -342,6 +368,8 @@ export interface HydrateFrontsPublishIntentsOptions {
|
|
|
342
368
|
mediaPublicDomain?: string
|
|
343
369
|
/** False keeps source-only readiness failures visible without requesting production. */
|
|
344
370
|
podcastSourceProductionEnabled?: boolean
|
|
371
|
+
/** The video twin: false keeps `video_media_not_ready` visible without emitting `video_source`. */
|
|
372
|
+
videoSourceProductionEnabled?: boolean
|
|
345
373
|
}
|
|
346
374
|
|
|
347
375
|
/** One due item that could NOT form a complete intent, with the exact reason. */
|
|
@@ -396,6 +424,10 @@ interface VideoRefRow {
|
|
|
396
424
|
hls_poster_url: string | null
|
|
397
425
|
duration_seconds: number | null
|
|
398
426
|
thumbnail_image_id: string | null
|
|
427
|
+
processing_source_url: string | null
|
|
428
|
+
processing_source_kind: string | null
|
|
429
|
+
processing_trigger_token: string | null
|
|
430
|
+
processing_correlation_id: string | null
|
|
399
431
|
}
|
|
400
432
|
|
|
401
433
|
interface PodcastRefRow {
|
|
@@ -477,7 +509,10 @@ export async function hydrateFrontsPublishIntents(
|
|
|
477
509
|
mediaPublicDomain,
|
|
478
510
|
resolve: options.resolveContentUrl,
|
|
479
511
|
})
|
|
480
|
-
if (
|
|
512
|
+
if (
|
|
513
|
+
(options.podcastSourceProductionEnabled !== false || !intent.content_refs.podcast_source) &&
|
|
514
|
+
(options.videoSourceProductionEnabled !== false || !intent.content_refs.video_source)
|
|
515
|
+
) {
|
|
481
516
|
intents.push(intent)
|
|
482
517
|
}
|
|
483
518
|
// A podcast with a source but incomplete media is visible to the
|
|
@@ -535,7 +570,8 @@ async function loadContentRefSources(
|
|
|
535
570
|
const res = await db
|
|
536
571
|
.prepare(
|
|
537
572
|
`SELECT content_id, hls_ready, hls_manifest_url, hls_poster_url, duration_seconds,
|
|
538
|
-
thumbnail_image_id
|
|
573
|
+
thumbnail_image_id, processing_source_url, processing_source_kind,
|
|
574
|
+
processing_trigger_token, processing_correlation_id
|
|
539
575
|
FROM video_content WHERE content_id IN (${placeholders(videoIds.length)})`,
|
|
540
576
|
)
|
|
541
577
|
.bind(...videoIds)
|
|
@@ -685,22 +721,75 @@ async function buildContentRefs(
|
|
|
685
721
|
}
|
|
686
722
|
|
|
687
723
|
// --- video_hls: required for video, and only when genuinely ready.
|
|
724
|
+
// A video whose HLS is not ready but whose captured source the Go
|
|
725
|
+
// media.pipeline can ingest (an https URL, a Dropbox link) with a
|
|
726
|
+
// correlation id and a measured duration is the dual-signal case the
|
|
727
|
+
// podcast branch below established: not publishable (reported in
|
|
728
|
+
// incomplete[] under the same code as before) but carried as
|
|
729
|
+
// video_source so the pull consumer can start production.
|
|
688
730
|
let videoHls: FrontsPublishVideoHlsRef | null = null
|
|
731
|
+
let videoSource: FrontsPublishVideoSourceRef | null = null
|
|
732
|
+
let incomplete: FrontsPublishIntentFailure | null = null
|
|
689
733
|
if (row.type === 'video') {
|
|
690
734
|
const found = refs.videos.get(row.id)
|
|
691
735
|
const manifest = found?.hls_manifest_url?.trim()
|
|
692
736
|
if (!found || found.hls_ready !== 1 || !manifest) {
|
|
693
|
-
|
|
694
|
-
'video_media_not_ready',
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
737
|
+
const failure = {
|
|
738
|
+
code: 'video_media_not_ready' as const,
|
|
739
|
+
message:
|
|
740
|
+
'a video intent requires video_content.hls_ready = 1 and a non-empty hls_manifest_url',
|
|
741
|
+
}
|
|
742
|
+
const sourceUrl = found?.processing_source_url
|
|
743
|
+
const correlationId = found?.processing_correlation_id?.trim()
|
|
744
|
+
if (
|
|
745
|
+
found &&
|
|
746
|
+
validPodcastSourceUrl(sourceUrl) &&
|
|
747
|
+
videoSourceIngestableOverHttps(found.processing_source_kind) &&
|
|
748
|
+
correlationId &&
|
|
749
|
+
/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(correlationId)
|
|
750
|
+
) {
|
|
751
|
+
if (
|
|
752
|
+
typeof found.duration_seconds !== 'number' ||
|
|
753
|
+
!Number.isFinite(found.duration_seconds) ||
|
|
754
|
+
found.duration_seconds <= 0
|
|
755
|
+
) {
|
|
756
|
+
throw new FrontsPublishIntentError(
|
|
757
|
+
'video_source_duration_unavailable',
|
|
758
|
+
row.id,
|
|
759
|
+
'Video source duration is not verified.',
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
videoSource = {
|
|
763
|
+
kind: 'https',
|
|
764
|
+
url: sourceUrl,
|
|
765
|
+
duration_seconds: found.duration_seconds,
|
|
766
|
+
generation: await frontsVideoSourceGeneration({
|
|
767
|
+
content_id: row.id,
|
|
768
|
+
slug: row.slug,
|
|
769
|
+
canonical_version: row.canonical_version as number,
|
|
770
|
+
processing_source_url: found.processing_source_url,
|
|
771
|
+
processing_source_kind: found.processing_source_kind,
|
|
772
|
+
processing_trigger_token: found.processing_trigger_token,
|
|
773
|
+
}),
|
|
774
|
+
correlation_id: correlationId,
|
|
775
|
+
visibility: row.visibility === 'premium' ? 'premium' : 'free',
|
|
776
|
+
}
|
|
777
|
+
incomplete = {
|
|
778
|
+
content_id: row.id,
|
|
779
|
+
code: failure.code,
|
|
780
|
+
message: failure.message,
|
|
781
|
+
publish_at: Math.floor(row.publish_at ?? 0),
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
throw new FrontsPublishIntentError(failure.code, row.id, failure.message)
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
videoHls = {
|
|
788
|
+
kind: 'hls-manifest',
|
|
789
|
+
url: manifest,
|
|
790
|
+
poster_url: found.hls_poster_url?.trim() || null,
|
|
791
|
+
duration_seconds: found.duration_seconds ?? null,
|
|
792
|
+
}
|
|
704
793
|
}
|
|
705
794
|
}
|
|
706
795
|
|
|
@@ -720,7 +809,6 @@ async function buildContentRefs(
|
|
|
720
809
|
// else.
|
|
721
810
|
let podcastAudio: FrontsPublishPodcastAudioRef | null = null
|
|
722
811
|
let podcastSource: FrontsPublishPodcastSourceRef | null = null
|
|
723
|
-
let incomplete: FrontsPublishIntentFailure | null = null
|
|
724
812
|
if (row.type === 'podcast') {
|
|
725
813
|
const found = refs.podcasts.get(row.id)
|
|
726
814
|
const key = found?.audio_r2_key?.trim()
|
|
@@ -806,6 +894,7 @@ async function buildContentRefs(
|
|
|
806
894
|
video_hls: videoHls,
|
|
807
895
|
podcast_audio_hls: podcastAudio,
|
|
808
896
|
podcast_source: podcastSource,
|
|
897
|
+
video_source: videoSource,
|
|
809
898
|
links,
|
|
810
899
|
},
|
|
811
900
|
incomplete,
|
package/src/engine/index.ts
CHANGED
|
@@ -409,6 +409,11 @@ export {
|
|
|
409
409
|
validateArticleBody,
|
|
410
410
|
warnErrors,
|
|
411
411
|
} from './validator/index.js'
|
|
412
|
+
export {
|
|
413
|
+
type FrontsVideoSourceSnapshot,
|
|
414
|
+
frontsVideoSourceGeneration,
|
|
415
|
+
videoSourceIngestableOverHttps,
|
|
416
|
+
} from './video-source.js'
|
|
412
417
|
// Webhook signer + default dispatch impl — HMAC-SHA256 via Web Crypto (P7 Task 9).
|
|
413
418
|
export {
|
|
414
419
|
buildDefaultDispatchWebhook,
|
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
ensureUniqueSlug,
|
|
24
24
|
getContentItem,
|
|
25
25
|
getContentSnapshot,
|
|
26
|
+
PUBLISH_METADATA_STAMP_SQL,
|
|
26
27
|
PublishContentValidationError,
|
|
28
|
+
publishMetadataStampBinds,
|
|
27
29
|
} from './publisher.js'
|
|
28
30
|
import { getRevision } from './revisions.js'
|
|
29
31
|
import { slugify } from './slug.js'
|
|
@@ -1637,7 +1639,8 @@ async function movePointerAndBeginCommit(
|
|
|
1637
1639
|
.prepare(
|
|
1638
1640
|
`UPDATE content_items
|
|
1639
1641
|
SET status = 'published', published_at = ?, publish_at = NULL,
|
|
1640
|
-
published_revision_id = ?, updated_at = unixepoch()
|
|
1642
|
+
published_revision_id = ?, updated_at = unixepoch(),
|
|
1643
|
+
${PUBLISH_METADATA_STAMP_SQL}
|
|
1641
1644
|
WHERE id = ?
|
|
1642
1645
|
AND COALESCE(published_revision_id, '') = ?
|
|
1643
1646
|
AND EXISTS (
|
|
@@ -1649,6 +1652,9 @@ async function movePointerAndBeginCommit(
|
|
|
1649
1652
|
.bind(
|
|
1650
1653
|
attempt.targetPublishedAt,
|
|
1651
1654
|
attempt.revisionId,
|
|
1655
|
+
// Wall clock, NOT targetPublishedAt — the latter is routinely backdated
|
|
1656
|
+
// to the source video's date on the channel sites.
|
|
1657
|
+
...publishMetadataStampBinds(now),
|
|
1652
1658
|
attempt.contentId,
|
|
1653
1659
|
attempt.previousRevisionId ?? '',
|
|
1654
1660
|
attempt.id,
|
|
@@ -54,6 +54,16 @@ export interface PublishedContent {
|
|
|
54
54
|
type: ContentType
|
|
55
55
|
publishedAt: number
|
|
56
56
|
updatedAt: number
|
|
57
|
+
/**
|
|
58
|
+
* Row creation time (unix seconds), from the published revision snapshot.
|
|
59
|
+
*
|
|
60
|
+
* Distinct from `publishedAt`, which is the date the article *claims* and is
|
|
61
|
+
* deliberately backdated to the source video's date on the channel sites.
|
|
62
|
+
* Consumers pass this to `resolveAdaptedArticleDates` as the `createdAt`
|
|
63
|
+
* fallback for `datePublished`; without it that fallback is dead code.
|
|
64
|
+
* `undefined` only for a revision snapshot predating the column.
|
|
65
|
+
*/
|
|
66
|
+
createdAt: number | undefined
|
|
57
67
|
payload: ContentRevisionPayload
|
|
58
68
|
metadata: ContentMetadata
|
|
59
69
|
}
|
|
@@ -358,6 +368,9 @@ function decodePublishedContent(row: PublishedContentRow, requireBodies = true):
|
|
|
358
368
|
type: item.type as ContentType,
|
|
359
369
|
publishedAt: row.canonical_published_at,
|
|
360
370
|
updatedAt: item.updated_at as number,
|
|
371
|
+
// getContentSnapshot selects * from content_items, so created_at rides
|
|
372
|
+
// along in the revision payload; no extra column selection needed.
|
|
373
|
+
createdAt: Number.isSafeInteger(item.created_at) ? (item.created_at as number) : undefined,
|
|
361
374
|
payload,
|
|
362
375
|
metadata,
|
|
363
376
|
}
|
package/src/engine/publisher.ts
CHANGED
|
@@ -1464,16 +1464,129 @@ export async function assertContentBodyValidForPublish(
|
|
|
1464
1464
|
}
|
|
1465
1465
|
}
|
|
1466
1466
|
|
|
1467
|
+
/**
|
|
1468
|
+
* Publish-time date stamping on `content_items.metadata_json`.
|
|
1469
|
+
*
|
|
1470
|
+
* Adapted articles (an article written from a YouTube video script) need two
|
|
1471
|
+
* dates the row did not previously carry:
|
|
1472
|
+
*
|
|
1473
|
+
* - `adaptedAt` — the date the article first went live **on the site**. This is
|
|
1474
|
+
* what `@growth-labs/seo`'s `resolveAdaptedArticleDates` turns into JSON-LD
|
|
1475
|
+
* `datePublished`.
|
|
1476
|
+
* - `updatedAt` — the date of the most recent re-publish, which becomes
|
|
1477
|
+
* `dateModified`.
|
|
1478
|
+
*
|
|
1479
|
+
* ## The distinction that matters: claimed date vs. wall clock
|
|
1480
|
+
*
|
|
1481
|
+
* `published_at` is the date the article *claims*, and on all ten channel sites
|
|
1482
|
+
* it is deliberately backdated to the **YouTube video's** publish date — that is
|
|
1483
|
+
* the whole reason these two keys exist. `adaptedAt` and `updatedAt` are the
|
|
1484
|
+
* opposite: the wall-clock moment the publish transition actually committed.
|
|
1485
|
+
*
|
|
1486
|
+
* Deriving either of them from the publish timestamp would collapse
|
|
1487
|
+
* `datePublished` straight back onto the video date and make the feature a
|
|
1488
|
+
* no-op for every future article. So the stamp takes its own `now` value,
|
|
1489
|
+
* distinct from `publishedAt`, at every call site.
|
|
1490
|
+
*
|
|
1491
|
+
* Invariants:
|
|
1492
|
+
* - `adaptedAt` is written once and **never rewritten**. It is the article's
|
|
1493
|
+
* identity date; rewriting it would walk `datePublished` forward on every edit.
|
|
1494
|
+
* - A re-publish stamps `updatedAt` at wall clock. A first publish does not —
|
|
1495
|
+
* `dateModified` defaults to `datePublished` in the consumer. Wall clock also
|
|
1496
|
+
* means a backdated re-publish cannot emit a `dateModified` before
|
|
1497
|
+
* `datePublished`, and a scheduled forward-dated publish cannot emit a
|
|
1498
|
+
* *future* `dateModified` (a Discover quality signal problem that the seo
|
|
1499
|
+
* helper's clamp does not catch).
|
|
1500
|
+
* - A row already published before this shipped has no `adaptedAt`. It is
|
|
1501
|
+
* backfilled from `COALESCE(created_at, published_at)` — `created_at` is the
|
|
1502
|
+
* real row-creation moment, whereas `published_at` is the claimed (possibly
|
|
1503
|
+
* backdated) date. The two are equal on exactly the rows that need the
|
|
1504
|
+
* backfill, so this is a no-op on current data and correct by construction
|
|
1505
|
+
* rather than by coincidence. `content_items.created_at` is NOT NULL, so the
|
|
1506
|
+
* `published_at` arm of that COALESCE is unreachable by schema and kept only
|
|
1507
|
+
* as defence in depth — there is deliberately no test for it, because no test
|
|
1508
|
+
* can construct the state.
|
|
1509
|
+
*
|
|
1510
|
+
* Expressed as SQL rather than read-modify-write so the stamp lands in the same
|
|
1511
|
+
* atomic statement as the publish transition. A separate UPDATE would open a
|
|
1512
|
+
* window where an item is published but unstamped, and in the 2PC kernel it
|
|
1513
|
+
* would not be covered by the pointer CAS. In SQLite the right-hand side of a
|
|
1514
|
+
* SET clause sees the row's **pre-update** values, so `published_at` and
|
|
1515
|
+
* `created_at` below are the previous values, which is what the backfill needs.
|
|
1516
|
+
*
|
|
1517
|
+
* Malformed or absent `metadata_json` degrades to `{}` rather than failing the
|
|
1518
|
+
* publish; `json_set` on invalid JSON would otherwise throw and block the item.
|
|
1519
|
+
*/
|
|
1520
|
+
const METADATA_OBJECT_SQL = `CASE
|
|
1521
|
+
WHEN metadata_json IS NOT NULL AND json_valid(metadata_json) AND json_type(metadata_json) = 'object'
|
|
1522
|
+
THEN metadata_json
|
|
1523
|
+
ELSE '{}'
|
|
1524
|
+
END`
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* `metadata_json` with `adaptedAt` resolved:
|
|
1528
|
+
* already set → untouched; previously published → backfilled from the row's own
|
|
1529
|
+
* creation time; otherwise a genuine first publish → the wall clock `?`.
|
|
1530
|
+
*/
|
|
1531
|
+
const METADATA_WITH_ADAPTED_AT_SQL = `CASE
|
|
1532
|
+
WHEN json_extract(${METADATA_OBJECT_SQL}, '$.adaptedAt') IS NOT NULL
|
|
1533
|
+
THEN ${METADATA_OBJECT_SQL}
|
|
1534
|
+
WHEN published_at IS NOT NULL
|
|
1535
|
+
THEN json_set(${METADATA_OBJECT_SQL}, '$.adaptedAt', date(COALESCE(created_at, published_at), 'unixepoch'))
|
|
1536
|
+
ELSE json_set(${METADATA_OBJECT_SQL}, '$.adaptedAt', date(?, 'unixepoch'))
|
|
1537
|
+
END`
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* The full `metadata_json = ...` assignment. Append to a publish UPDATE's SET
|
|
1541
|
+
* clause and bind with {@link publishMetadataStampBinds}.
|
|
1542
|
+
*
|
|
1543
|
+
* "Already published" is `published_at IS NOT NULL OR adaptedAt IS NOT NULL` —
|
|
1544
|
+
* the second disjunct keeps unpublish→republish correct, since `unpublishContent`
|
|
1545
|
+
* clears `published_at` but the article did go live once.
|
|
1546
|
+
*/
|
|
1547
|
+
export const PUBLISH_METADATA_STAMP_SQL = `metadata_json = CASE
|
|
1548
|
+
WHEN published_at IS NOT NULL
|
|
1549
|
+
OR json_extract(${METADATA_OBJECT_SQL}, '$.adaptedAt') IS NOT NULL
|
|
1550
|
+
THEN json_set(${METADATA_WITH_ADAPTED_AT_SQL}, '$.updatedAt', date(?, 'unixepoch'))
|
|
1551
|
+
ELSE ${METADATA_WITH_ADAPTED_AT_SQL}
|
|
1552
|
+
END`
|
|
1553
|
+
|
|
1554
|
+
/** Placeholder count in {@link PUBLISH_METADATA_STAMP_SQL}, derived so it cannot drift. */
|
|
1555
|
+
const PUBLISH_METADATA_STAMP_PLACEHOLDERS = (PUBLISH_METADATA_STAMP_SQL.match(/\?/g) ?? []).length
|
|
1556
|
+
|
|
1557
|
+
/** Wall-clock unix seconds — the moment a publish transition commits. */
|
|
1558
|
+
export function nowSeconds(): number {
|
|
1559
|
+
return Math.floor(Date.now() / 1000)
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Binds for {@link PUBLISH_METADATA_STAMP_SQL}. Every placeholder is the same
|
|
1564
|
+
* wall-clock `now` (unix seconds) — deliberately NOT the publish timestamp,
|
|
1565
|
+
* which is routinely backdated. The count is derived from the SQL itself so
|
|
1566
|
+
* editing the expression cannot silently desynchronize the bind list.
|
|
1567
|
+
*/
|
|
1568
|
+
export function publishMetadataStampBinds(now: number): number[] {
|
|
1569
|
+
return new Array(PUBLISH_METADATA_STAMP_PLACEHOLDERS).fill(now)
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* @param publishedAt the date the article claims — may be deliberately backdated.
|
|
1574
|
+
* @param now wall-clock unix seconds for the date stamps; defaults to real now.
|
|
1575
|
+
* Kept separate from `publishedAt` on purpose (see PUBLISH_METADATA_STAMP_SQL)
|
|
1576
|
+
* and injectable so tests stay deterministic.
|
|
1577
|
+
*/
|
|
1467
1578
|
export async function publishContent(
|
|
1468
1579
|
db: D1Database,
|
|
1469
1580
|
id: string,
|
|
1470
1581
|
publishedAt: number,
|
|
1582
|
+
now: number = nowSeconds(),
|
|
1471
1583
|
): Promise<void> {
|
|
1472
1584
|
await db
|
|
1473
1585
|
.prepare(
|
|
1474
|
-
`UPDATE content_items SET status = 'published', published_at = ?, publish_at = NULL, updated_at = unixepoch()
|
|
1586
|
+
`UPDATE content_items SET status = 'published', published_at = ?, publish_at = NULL, updated_at = unixepoch(),
|
|
1587
|
+
${PUBLISH_METADATA_STAMP_SQL} WHERE id = ?`,
|
|
1475
1588
|
)
|
|
1476
|
-
.bind(publishedAt, id)
|
|
1589
|
+
.bind(publishedAt, ...publishMetadataStampBinds(now), id)
|
|
1477
1590
|
.run()
|
|
1478
1591
|
}
|
|
1479
1592
|
|
|
@@ -1493,14 +1606,16 @@ export async function claimScheduledForPublish(
|
|
|
1493
1606
|
db: D1Database,
|
|
1494
1607
|
id: string,
|
|
1495
1608
|
publishedAt: number,
|
|
1609
|
+
now: number = nowSeconds(),
|
|
1496
1610
|
): Promise<boolean> {
|
|
1497
1611
|
const res = await db
|
|
1498
1612
|
.prepare(
|
|
1499
1613
|
`UPDATE content_items
|
|
1500
|
-
SET status = 'published', published_at = ?, publish_at = NULL, updated_at = unixepoch()
|
|
1614
|
+
SET status = 'published', published_at = ?, publish_at = NULL, updated_at = unixepoch(),
|
|
1615
|
+
${PUBLISH_METADATA_STAMP_SQL}
|
|
1501
1616
|
WHERE id = ? AND status = 'scheduled'`,
|
|
1502
1617
|
)
|
|
1503
|
-
.bind(publishedAt, id)
|
|
1618
|
+
.bind(publishedAt, ...publishMetadataStampBinds(now), id)
|
|
1504
1619
|
.run()
|
|
1505
1620
|
const changes = (res.meta as { changes?: number } | undefined)?.changes ?? 0
|
|
1506
1621
|
return changes > 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Current CMS evidence used by the signed schedule and media callback guards. */
|
|
2
|
+
export interface FrontsVideoSourceSnapshot {
|
|
3
|
+
content_id: string
|
|
4
|
+
slug: string
|
|
5
|
+
canonical_version: number
|
|
6
|
+
processing_source_url: string | null
|
|
7
|
+
processing_source_kind: string | null
|
|
8
|
+
processing_trigger_token: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stable over polling, machine media callbacks and rescheduling; canonical
|
|
13
|
+
* editor/source changes invalidate the old production. The raw trigger token
|
|
14
|
+
* and URL never need to appear in callback or notification identities. The
|
|
15
|
+
* Go consumer's contract is `fronts-video-source.v1` (foundry
|
|
16
|
+
* `publishplane/masthead/videosource.schema.json`, exported by
|
|
17
|
+
* `foundry-capability-probe --contract`).
|
|
18
|
+
*/
|
|
19
|
+
export async function frontsVideoSourceGeneration(
|
|
20
|
+
input: FrontsVideoSourceSnapshot,
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const bytes = new TextEncoder().encode(
|
|
23
|
+
JSON.stringify([
|
|
24
|
+
'fronts-video-source.v1',
|
|
25
|
+
input.content_id,
|
|
26
|
+
input.slug,
|
|
27
|
+
input.canonical_version,
|
|
28
|
+
input.processing_source_url,
|
|
29
|
+
input.processing_source_kind,
|
|
30
|
+
input.processing_trigger_token,
|
|
31
|
+
]),
|
|
32
|
+
)
|
|
33
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes)
|
|
34
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Source kinds the Go media.pipeline can ingest over plain HTTPS
|
|
39
|
+
* (`ingest.https.verified`): a direct URL and a Dropbox share link, both of
|
|
40
|
+
* which are fetched as-is. A Trello attachment needs the board credential
|
|
41
|
+
* and stays on the host's own dispatch path.
|
|
42
|
+
*/
|
|
43
|
+
export function videoSourceIngestableOverHttps(kind: string | null | undefined): boolean {
|
|
44
|
+
return kind === 'http_url' || kind === 'dropbox'
|
|
45
|
+
}
|
package/src/providers/types.ts
CHANGED
|
@@ -402,9 +402,59 @@ export interface FoundryDispatchResult {
|
|
|
402
402
|
/** Return source evidence; the CMS owns its atomic capture with the token. */
|
|
403
403
|
podcastSource?: { url: string; kind: 'https' }
|
|
404
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* The Go publish state of one media item as the host reads it from its own
|
|
407
|
+
* receipts (W1-08b): the state bucket the schedule badge renders plus what
|
|
408
|
+
* the badge shows on hover. Every field but `state` is optional so a host
|
|
409
|
+
* that still answers `{ state }` keeps working; `state` is one of
|
|
410
|
+
* `goPublishStateValues` for a Go-plane host and free text otherwise.
|
|
411
|
+
*/
|
|
412
|
+
export const goPublishStateValues = [
|
|
413
|
+
'pending',
|
|
414
|
+
'captured',
|
|
415
|
+
'processing',
|
|
416
|
+
'media_ready',
|
|
417
|
+
'publishing',
|
|
418
|
+
'published',
|
|
419
|
+
'late',
|
|
420
|
+
'failed',
|
|
421
|
+
] as const
|
|
422
|
+
export type GoPublishState = (typeof goPublishStateValues)[number]
|
|
423
|
+
|
|
424
|
+
export interface FoundryJobStatus {
|
|
425
|
+
state: GoPublishState | (string & {})
|
|
426
|
+
kind?: 'video' | 'podcast'
|
|
427
|
+
contentId?: string
|
|
428
|
+
contentStatus?: string
|
|
429
|
+
/** The id callbacks arrive under; null before the source is captured. */
|
|
430
|
+
correlationId?: string | null
|
|
431
|
+
/** The host's raw processing state (video_content.processing_state), when it has one. */
|
|
432
|
+
processingState?: string | null
|
|
433
|
+
/** True when the media the site serves exists (HLS ready / audio present). */
|
|
434
|
+
mediaReady?: boolean
|
|
435
|
+
/** Latest production or publish receipt kind, e.g. `hls.ready`, `publish:live`. */
|
|
436
|
+
eventKind?: string | null
|
|
437
|
+
/** Unix seconds of that latest receipt. */
|
|
438
|
+
receiptAt?: number | null
|
|
439
|
+
publishStatus?: 'queued' | 'publishing' | 'live' | 'late' | 'failed' | null
|
|
440
|
+
/** Why the run is blocked (a production error, the last publish error). */
|
|
441
|
+
blockedReason?: string | null
|
|
442
|
+
placement?: {
|
|
443
|
+
storageClass: 'public' | 'private'
|
|
444
|
+
verified: boolean
|
|
445
|
+
verifiedAt: number
|
|
446
|
+
} | null
|
|
447
|
+
}
|
|
448
|
+
|
|
405
449
|
export interface FoundryDispatch {
|
|
406
450
|
dispatchVideo(jobSpec: FoundryJobSpec): Promise<FoundryDispatchResult>
|
|
407
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Read a media item's processing / Go publish state. `reference` is the
|
|
453
|
+
* production correlation id the host handed back from dispatchVideo, or
|
|
454
|
+
* a content id (video or podcast) so the schedule views can ask per row.
|
|
455
|
+
* Null when the host knows nothing about the reference.
|
|
456
|
+
*/
|
|
457
|
+
getJob(reference: string): Promise<FoundryJobStatus | null>
|
|
408
458
|
}
|
|
409
459
|
|
|
410
460
|
export type MediaSourceKind = 'video' | 'podcast'
|