@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
package/src/routes/calendar.ts
CHANGED
|
@@ -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
|
}
|
package/src/routes/content.ts
CHANGED
|
@@ -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
|
}
|
package/src/ui/api/calendar.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|