@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.
Files changed (69) hide show
  1. package/dist/engine/fronts-publish-intent.d.ts +25 -1
  2. package/dist/engine/fronts-publish-intent.d.ts.map +1 -1
  3. package/dist/engine/fronts-publish-intent.js +64 -11
  4. package/dist/engine/fronts-publish-intent.js.map +1 -1
  5. package/dist/engine/index.d.ts +1 -0
  6. package/dist/engine/index.d.ts.map +1 -1
  7. package/dist/engine/index.js +1 -0
  8. package/dist/engine/index.js.map +1 -1
  9. package/dist/engine/publication.d.ts.map +1 -1
  10. package/dist/engine/publication.js +7 -3
  11. package/dist/engine/publication.js.map +1 -1
  12. package/dist/engine/published-content.d.ts +10 -0
  13. package/dist/engine/published-content.d.ts.map +1 -1
  14. package/dist/engine/published-content.js +3 -0
  15. package/dist/engine/published-content.js.map +1 -1
  16. package/dist/engine/publisher.d.ts +26 -2
  17. package/dist/engine/publisher.d.ts.map +1 -1
  18. package/dist/engine/publisher.js +113 -6
  19. package/dist/engine/publisher.js.map +1 -1
  20. package/dist/engine/video-source.d.ts +26 -0
  21. package/dist/engine/video-source.d.ts.map +1 -0
  22. package/dist/engine/video-source.js +31 -0
  23. package/dist/engine/video-source.js.map +1 -0
  24. package/dist/providers/types.d.ts +40 -3
  25. package/dist/providers/types.d.ts.map +1 -1
  26. package/dist/providers/types.js +17 -0
  27. package/dist/providers/types.js.map +1 -1
  28. package/dist/routes/calendar.d.ts +26 -0
  29. package/dist/routes/calendar.d.ts.map +1 -1
  30. package/dist/routes/calendar.js +41 -0
  31. package/dist/routes/calendar.js.map +1 -1
  32. package/dist/routes/content.d.ts +2 -0
  33. package/dist/routes/content.d.ts.map +1 -1
  34. package/dist/routes/content.js.map +1 -1
  35. package/dist/routes/fronts-publish.d.ts.map +1 -1
  36. package/dist/routes/fronts-publish.js +1 -0
  37. package/dist/routes/fronts-publish.js.map +1 -1
  38. package/dist/ui/api/_content-config.d.ts.map +1 -1
  39. package/dist/ui/api/_content-config.js +1 -0
  40. package/dist/ui/api/_content-config.js.map +1 -1
  41. package/dist/ui/api/calendar.d.ts.map +1 -1
  42. package/dist/ui/api/calendar.js +5 -1
  43. package/dist/ui/api/calendar.js.map +1 -1
  44. package/dist/ui/screens/CalendarScreen.d.ts.map +1 -1
  45. package/dist/ui/screens/CalendarScreen.js +10 -1
  46. package/dist/ui/screens/CalendarScreen.js.map +1 -1
  47. package/dist/ui/screens/calendar-data.d.ts +3 -0
  48. package/dist/ui/screens/calendar-data.d.ts.map +1 -1
  49. package/dist/ui/screens/calendar-data.js.map +1 -1
  50. package/dist/ui/screens/go-publish-badge.d.ts +11 -0
  51. package/dist/ui/screens/go-publish-badge.d.ts.map +1 -0
  52. package/dist/ui/screens/go-publish-badge.js +47 -0
  53. package/dist/ui/screens/go-publish-badge.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/engine/fronts-publish-intent.ts +104 -15
  56. package/src/engine/index.ts +5 -0
  57. package/src/engine/publication.ts +7 -1
  58. package/src/engine/published-content.ts +13 -0
  59. package/src/engine/publisher.ts +119 -4
  60. package/src/engine/video-source.ts +45 -0
  61. package/src/providers/types.ts +51 -1
  62. package/src/routes/calendar.ts +72 -0
  63. package/src/routes/content.ts +2 -0
  64. package/src/routes/fronts-publish.ts +1 -0
  65. package/src/ui/api/_content-config.ts +2 -0
  66. package/src/ui/api/calendar.ts +5 -1
  67. package/src/ui/screens/CalendarScreen.tsx +22 -0
  68. package/src/ui/screens/calendar-data.ts +4 -0
  69. 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;CACZ;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
+ {"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;AAuChF,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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/cms",
3
- "version": "0.6.8",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "growth-labs-cms-migrations": "./dist/cli/migrations.js"
@@ -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 (options.podcastSourceProductionEnabled !== false || !intent.content_refs.podcast_source) {
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
- throw new FrontsPublishIntentError(
694
- 'video_media_not_ready',
695
- row.id,
696
- 'a video intent requires video_content.hls_ready = 1 and a non-empty hls_manifest_url',
697
- )
698
- }
699
- videoHls = {
700
- kind: 'hls-manifest',
701
- url: manifest,
702
- poster_url: found.hls_poster_url?.trim() || null,
703
- duration_seconds: found.duration_seconds ?? null,
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,
@@ -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
  }
@@ -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() WHERE id = ?`,
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
+ }
@@ -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
- getJob(correlationId: string): Promise<{ state: string } | null>
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'