@growth-labs/cms 0.8.4 → 0.8.7

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.
@@ -34,6 +34,7 @@ import {
34
34
  verifyHmac,
35
35
  } from '../engine/foundry-dispatch.js'
36
36
  import {
37
+ parseFrontsChannelTargets,
37
38
  parseFrontsDefaultChannelTargets,
38
39
  setFrontsChannelTargets,
39
40
  } from '../engine/fronts-publish-intent.js'
@@ -47,6 +48,7 @@ import {
47
48
  getContentRelations,
48
49
  getContentTagPairs,
49
50
  publishContent,
51
+ type ScheduleFrontsChannelTargets,
50
52
  scheduleContent,
51
53
  unpublishContent,
52
54
  updateContentItem,
@@ -751,7 +753,7 @@ async function getContentPayload(ctx: RouteContext, type: string, id: string) {
751
753
  return ctx.db
752
754
  .prepare(
753
755
  `SELECT transcript, audio_r2_key, duration_seconds,
754
- processing_source_url, processing_source_kind
756
+ processing_source_url, processing_source_kind, processing_state, processing_error
755
757
  FROM podcast_content WHERE content_id = ? LIMIT 1`,
756
758
  )
757
759
  .bind(id)
@@ -885,6 +887,36 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
885
887
  await setFrontsChannelTargets(ctx.db, contentId, targetDefaults.targets, { onlyIfNull: true })
886
888
  }
887
889
  }
890
+ async function prepareScheduleTargets(
891
+ ctx: RouteContext,
892
+ contentId: string,
893
+ ): Promise<{ targets: ScheduleFrontsChannelTargets | undefined } | { error: Response }> {
894
+ if (!targetDefaults) return { targets: undefined }
895
+ if ('error' in targetDefaults) return { error: json({ error: targetDefaults.error }, 500) }
896
+ const row = await ctx.db
897
+ .prepare(
898
+ 'SELECT fronts_channel_targets FROM content_items WHERE id = ? AND deleted_at IS NULL',
899
+ )
900
+ .bind(contentId)
901
+ .first<{ fronts_channel_targets: string | null }>()
902
+ if (!row) return { error: json({ error: 'Not found', contentId }, 404) }
903
+ const defaults = JSON.stringify(targetDefaults.targets)
904
+ const raw = row.fronts_channel_targets ?? defaults
905
+ const parsed = parseFrontsChannelTargets(raw)
906
+ if ('code' in parsed) {
907
+ return {
908
+ error: json(
909
+ {
910
+ error: parsed.code,
911
+ message: 'Choose at least one valid publication target before scheduling.',
912
+ contentId,
913
+ },
914
+ 400,
915
+ ),
916
+ }
917
+ }
918
+ return { targets: { expected: row.fronts_channel_targets, defaults } }
919
+ }
888
920
 
889
921
  async function loadStoredMediaSource(
890
922
  db: D1Database,
@@ -1362,11 +1394,22 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
1362
1394
  return json({ error: 'Invalid publishAt' }, 400)
1363
1395
  }
1364
1396
  const publishAtSeconds = Math.floor(publishAtMs / 1000)
1397
+ const expectedTargets = new Map<string, ScheduleFrontsChannelTargets | undefined>()
1398
+ for (const id of ids) {
1399
+ const prepared = await prepareScheduleTargets(ctx, id)
1400
+ if ('error' in prepared) return prepared.error
1401
+ expectedTargets.set(id, prepared.targets)
1402
+ }
1403
+ const scheduled: string[] = []
1365
1404
  for (const id of ids) {
1366
- await scheduleContent(ctx.db, id, publishAtSeconds)
1367
- await armDefaultTargets(ctx, id)
1405
+ const targets = expectedTargets.get(id)
1406
+ const changed = await scheduleContent(ctx.db, id, publishAtSeconds, undefined, targets)
1407
+ if (targets !== undefined && !changed) {
1408
+ return json({ error: 'schedule_targets_changed', contentId: id, scheduled }, 409)
1409
+ }
1410
+ scheduled.push(id)
1368
1411
  }
1369
- return json({ scheduled: ids })
1412
+ return json({ scheduled })
1370
1413
  }
1371
1414
 
1372
1415
  // Exhaustive – the discriminated union above covers all ops.
@@ -1739,8 +1782,18 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
1739
1782
  return json({ error: 'Missing publish time' }, 400)
1740
1783
  }
1741
1784
 
1742
- await scheduleContent(ctx.db, id, publishAtSeconds as number, publishTimeZone)
1743
- await armDefaultTargets(ctx, id)
1785
+ const prepared = await prepareScheduleTargets(ctx, id)
1786
+ if ('error' in prepared) return prepared.error
1787
+ const changed = await scheduleContent(
1788
+ ctx.db,
1789
+ id,
1790
+ publishAtSeconds as number,
1791
+ publishTimeZone,
1792
+ prepared.targets,
1793
+ )
1794
+ if (prepared.targets !== undefined && !changed) {
1795
+ return json({ error: 'schedule_targets_changed', contentId: id }, 409)
1796
+ }
1744
1797
 
1745
1798
  // Fire content.scheduled webhook.
1746
1799
  await fireWebhook(ctx, 'content.scheduled', {
@@ -910,6 +910,10 @@ ALTER TABLE podcast_content ADD COLUMN processing_publication_guard TEXT;
910
910
  ALTER TABLE podcast_content ADD COLUMN processing_state TEXT CHECK (processing_state IS NULL OR processing_state IN ('pending','queued','fetching','transcribing','transcoding','ready','failed'));
911
911
  `,
912
912
  },
913
+ {
914
+ id: '0031_podcast_processing_error',
915
+ sql: `ALTER TABLE podcast_content ADD COLUMN processing_error TEXT;`,
916
+ },
913
917
  ]
914
918
 
915
919
  /**
@@ -213,6 +213,8 @@ export interface VideoContentRow {
213
213
 
214
214
  export interface PodcastContentRow {
215
215
  content_id: string
216
+ /** Current preparation cause, cleared by source replacement or a new capture. */
217
+ processing_error?: string | null
216
218
  /** Explicit preparation lifecycle; historical captures remain unqueued. */
217
219
  processing_state?: VideoProcessingState | null
218
220
  /** Capture-time published revision fence; null for historical/unpublished captures. */