@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
@@ -9,11 +9,19 @@
9
9
  //
10
10
  // No provider calls: the editorial calendar is fully first-party D1.
11
11
 
12
+ import type { FoundryDispatch, FoundryJobStatus } from '../providers/types.js'
12
13
  import type { Authz, RouteContext } from './context.js'
13
14
  import { json } from './context.js'
14
15
 
15
16
  export interface CalendarRouteConfig {
16
17
  authz: Authz
18
+ /**
19
+ * W1-08b: when the host supplies its Foundry provider, every scheduled
20
+ * video/podcast item in the range carries its Go publish state
21
+ * (`goPublish`) read through `getJob(contentId)` -- the badge the
22
+ * calendar chip renders. Absent provider → `goPublish` is omitted.
23
+ */
24
+ foundryVideo?: Pick<FoundryDispatch, 'getJob'>
17
25
  }
18
26
 
19
27
  export interface CalendarRouteHandlers {
@@ -29,6 +37,60 @@ export interface CalendarRouteItem {
29
37
  slug: string
30
38
  /** ISO-8601 date string derived from publish_at (scheduled) or published_at (published). */
31
39
  date: string
40
+ /**
41
+ * Go publish state of a scheduled video/podcast item (W1-08b). `null`
42
+ * when the host provider knows nothing about the item; a `state` of
43
+ * `unavailable` with `blockedReason` when the provider itself failed --
44
+ * the badge says so rather than rendering nothing.
45
+ */
46
+ goPublish?: CalendarGoPublish | null
47
+ }
48
+
49
+ export interface CalendarGoPublish {
50
+ state: FoundryJobStatus['state'] | 'unavailable'
51
+ eventKind: string | null
52
+ receiptAt: number | null
53
+ correlationId: string | null
54
+ blockedReason: string | null
55
+ processingState: string | null
56
+ mediaReady: boolean
57
+ }
58
+
59
+ const GO_PUBLISH_TYPES = new Set(['video', 'podcast'])
60
+
61
+ /** Fold a provider answer into the calendar's badge shape. */
62
+ export function toCalendarGoPublish(status: FoundryJobStatus | null): CalendarGoPublish | null {
63
+ if (!status) return null
64
+ return {
65
+ state: status.state,
66
+ eventKind: status.eventKind ?? null,
67
+ receiptAt: status.receiptAt ?? null,
68
+ correlationId: status.correlationId ?? null,
69
+ blockedReason: status.blockedReason ?? null,
70
+ processingState: status.processingState ?? null,
71
+ mediaReady: status.mediaReady ?? false,
72
+ }
73
+ }
74
+
75
+ async function readGoPublish(
76
+ provider: Pick<FoundryDispatch, 'getJob'>,
77
+ contentId: string,
78
+ ): Promise<CalendarGoPublish | null> {
79
+ try {
80
+ return toCalendarGoPublish(await provider.getJob(contentId))
81
+ } catch (error) {
82
+ const reason = error instanceof Error ? error.message : String(error)
83
+ console.warn('calendar: Go publish state unavailable', { contentId, reason })
84
+ return {
85
+ state: 'unavailable',
86
+ eventKind: null,
87
+ receiptAt: null,
88
+ correlationId: null,
89
+ blockedReason: reason,
90
+ processingState: null,
91
+ mediaReady: false,
92
+ }
93
+ }
32
94
  }
33
95
 
34
96
  /**
@@ -121,6 +183,16 @@ export function createCalendarRoutes(config: CalendarRouteConfig): CalendarRoute
121
183
  date: toIso(row.date_unix),
122
184
  }))
123
185
 
186
+ const provider = config.foundryVideo
187
+ if (provider) {
188
+ await Promise.all(
189
+ items.map(async (item) => {
190
+ if (item.status !== 'scheduled' || !GO_PUBLISH_TYPES.has(item.type)) return
191
+ item.goPublish = await readGoPublish(provider, item.id)
192
+ }),
193
+ )
194
+ }
195
+
124
196
  return json({ items })
125
197
  },
126
198
  }
@@ -199,6 +199,8 @@ export interface ContentRouteConfig extends CmsRouteConfig {
199
199
  publishReadiness?: PublishReadinessHook
200
200
  /** Host's existing production rollback control; omitted means enabled. */
201
201
  podcastSourceProductionEnabled?: boolean
202
+ /** The video twin (FRONTS_VIDEO_DISPATCH_TARGET on the host); omitted means enabled. */
203
+ videoSourceProductionEnabled?: boolean
202
204
  /** Internal machine-route constraint: atomically update only in this status. */
203
205
  updateStatusConstraint?: ContentStatus
204
206
  /**
@@ -199,6 +199,7 @@ export function createFrontsPublishRoutes(config: ContentRouteConfig): FrontsPub
199
199
  nowSeconds: now,
200
200
  mediaPublicDomain: resolved.mediaPublicDomain,
201
201
  podcastSourceProductionEnabled: config.podcastSourceProductionEnabled,
202
+ videoSourceProductionEnabled: config.videoSourceProductionEnabled,
202
203
  // Site-RELATIVE paths: the CMS does not know the site origin, and the
203
204
  // consumer joins its own trusted configured origin. A host-supplied
204
205
  // origin is deliberately deferred (packages-docs/cms.md).
@@ -26,6 +26,7 @@ type CmsHooksLocals = ContentRouteConfig['hooks'] & {
26
26
  dispatchWebhook?: DispatchWebhookHook
27
27
  publishReadiness?: PublishReadinessHook
28
28
  podcastSourceProductionEnabled?: boolean
29
+ videoSourceProductionEnabled?: boolean
29
30
  }
30
31
 
31
32
  /**
@@ -84,5 +85,6 @@ export function toContentConfig(c: APIContext): ContentRouteConfig {
84
85
  dispatchWebhook: hooks.dispatchWebhook,
85
86
  publishReadiness: hooks.publishReadiness,
86
87
  podcastSourceProductionEnabled: hooks.podcastSourceProductionEnabled,
88
+ videoSourceProductionEnabled: hooks.videoSourceProductionEnabled,
87
89
  }
88
90
  }
@@ -1,6 +1,7 @@
1
1
  // src/ui/api/calendar.ts — Astro endpoint adapter for GET /admin/api/calendar.
2
2
  // Maps the APIContext onto a RouteContext and delegates to createCalendarRoutes().month.
3
- // The calendar is first-party D1 (no provider injection needed).
3
+ // The calendar is first-party D1; the host's Foundry provider (locals.cmsHooks.foundryVideo)
4
+ // adds the Go publish badge state to scheduled media items when present (W1-08b).
4
5
 
5
6
  import type { APIContext } from 'astro'
6
7
  import { type CalendarRouteConfig, createCalendarRoutes } from '../../routes/calendar.js'
@@ -13,8 +14,11 @@ function toCtx(c: APIContext): RouteContext {
13
14
  }
14
15
 
15
16
  function calendarConfig(c: APIContext): CalendarRouteConfig {
17
+ const hooks = (c.locals as { cmsHooks?: { foundryVideo?: CalendarRouteConfig['foundryVideo'] } })
18
+ .cmsHooks
16
19
  return {
17
20
  authz: buildSiteAuthz(c),
21
+ foundryVideo: hooks?.foundryVideo,
18
22
  }
19
23
  }
20
24
 
@@ -18,6 +18,7 @@
18
18
  import { useCallback, useEffect, useReducer, useState } from 'react'
19
19
  import { Icon } from '../icons.js'
20
20
  import { buildCalendarMonth, type CalendarCell, type CalendarItem } from './calendar-data.js'
21
+ import { goPublishBadge } from './go-publish-badge.js'
21
22
 
22
23
  // ---------------------------------------------------------------------------
23
24
  // Props
@@ -483,6 +484,10 @@ function CalendarItemChip({
483
484
  const border = TYPE_COLOR[item.type] ?? 'var(--accent)'
484
485
  const statusIcon = STATUS_ICON[item.status]
485
486
  const statusColor = STATUS_ICON_COLOR[item.status] ?? 'var(--ink-faint)'
487
+ // W1-08b: the Go publish state of a scheduled media item, from the
488
+ // host's receipts (route: goPublish). Hover shows the receipt and the
489
+ // correlation id; the pill vocabulary is the Foundry tab's.
490
+ const badge = goPublishBadge(item.goPublish)
486
491
 
487
492
  return (
488
493
  <div
@@ -520,6 +525,23 @@ function CalendarItemChip({
520
525
  >
521
526
  {item.title || '(Untitled)'}
522
527
  </button>
528
+ {badge && (
529
+ <span
530
+ className={
531
+ badge.pill === 'ready'
532
+ ? 'pill published'
533
+ : badge.pill === 'failed'
534
+ ? 'pill review'
535
+ : 'pill processing'
536
+ }
537
+ title={badge.tooltip}
538
+ data-go-publish={item.goPublish?.state}
539
+ style={{ flex: 'none', fontSize: 9, padding: '1px 5px', whiteSpace: 'nowrap' }}
540
+ >
541
+ <span className="pdot" />
542
+ {badge.label}
543
+ </span>
544
+ )}
523
545
  {statusIcon &&
524
546
  (item.status === 'scheduled' ? (
525
547
  <button
@@ -13,6 +13,8 @@
13
13
  // Types
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
+ import type { CalendarGoPublish } from '../../routes/calendar.js'
17
+
16
18
  /**
17
19
  * A dated content item as returned by the `/admin/api/calendar?from=&to=` route.
18
20
  * `date` is an ISO-8601 string (date-only or with time component, UTC-assumed).
@@ -24,6 +26,8 @@ export interface CalendarItem {
24
26
  slug: string
25
27
  title: string
26
28
  date: string
29
+ /** Go publish state of a scheduled video/podcast item (W1-08b); see go-publish-badge.ts. */
30
+ goPublish?: CalendarGoPublish | null
27
31
  }
28
32
 
29
33
  /** A leading blank cell before the first day of the month. */
@@ -0,0 +1,55 @@
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
+
13
+ import type { CalendarGoPublish } from '../../routes/calendar.js'
14
+ import type { FoundryPillState } from '../inspector/foundry-stages.js'
15
+
16
+ export interface GoPublishBadge {
17
+ label: string
18
+ pill: FoundryPillState
19
+ tooltip: string
20
+ }
21
+
22
+ const LABELS: Record<string, { label: string; pill: FoundryPillState }> = {
23
+ pending: { label: 'Not captured', pill: 'working' },
24
+ captured: { label: 'Captured', pill: 'working' },
25
+ processing: { label: 'Processing', pill: 'working' },
26
+ media_ready: { label: 'Media ready', pill: 'ready' },
27
+ publishing: { label: 'Publishing', pill: 'working' },
28
+ published: { label: 'Published', pill: 'ready' },
29
+ late: { label: 'Late', pill: 'failed' },
30
+ failed: { label: 'Blocked', pill: 'failed' },
31
+ unavailable: { label: 'State unavailable', pill: 'failed' },
32
+ }
33
+
34
+ /** Format a unix-second receipt time for the tooltip (UTC, to the minute). */
35
+ export function formatReceiptTime(unixSeconds: number): string {
36
+ return `${new Date(unixSeconds * 1000).toISOString().slice(0, 16).replace('T', ' ')} UTC`
37
+ }
38
+
39
+ export function goPublishBadge(
40
+ goPublish: CalendarGoPublish | null | undefined,
41
+ ): GoPublishBadge | null {
42
+ if (!goPublish) return null
43
+ const entry = LABELS[goPublish.state] ?? { label: goPublish.state, pill: 'working' as const }
44
+ const lines = [`Go publish: ${entry.label}`]
45
+ if (goPublish.eventKind && goPublish.receiptAt !== null) {
46
+ lines.push(`Last receipt: ${goPublish.eventKind} at ${formatReceiptTime(goPublish.receiptAt)}`)
47
+ } else if (goPublish.eventKind) {
48
+ lines.push(`Last receipt: ${goPublish.eventKind}`)
49
+ } else {
50
+ lines.push('No receipt yet')
51
+ }
52
+ if (goPublish.correlationId) lines.push(`Correlation: ${goPublish.correlationId}`)
53
+ if (goPublish.blockedReason) lines.push(`Reason: ${goPublish.blockedReason}`)
54
+ return { label: entry.label, pill: entry.pill, tooltip: lines.join('\n') }
55
+ }