@growth-labs/cms 0.8.20 → 0.8.24
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/cli/foundry-emit.d.ts +14 -0
- package/dist/cli/foundry-emit.d.ts.map +1 -0
- package/dist/cli/foundry-emit.js +69 -0
- package/dist/cli/foundry-emit.js.map +1 -0
- package/dist/engine/foundry-dispatch.d.ts +1 -0
- package/dist/engine/foundry-dispatch.d.ts.map +1 -1
- package/dist/engine/foundry-dispatch.js +1 -1
- package/dist/engine/foundry-dispatch.js.map +1 -1
- package/dist/engine/fronts-publish-intent.d.ts +1 -1
- package/dist/engine/fronts-publish-intent.d.ts.map +1 -1
- package/dist/engine/fronts-publish-intent.js +12 -1
- 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 +2 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/publication-receipt/receipt.d.ts +20 -20
- package/dist/engine/publisher.d.ts +16 -1
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +55 -4
- package/dist/engine/publisher.js.map +1 -1
- package/dist/engine/tags.d.ts.map +1 -1
- package/dist/engine/tags.js +18 -1
- package/dist/engine/tags.js.map +1 -1
- package/dist/engine/topics.d.ts.map +1 -1
- package/dist/engine/topics.js +4 -1
- package/dist/engine/topics.js.map +1 -1
- package/dist/engine/video-source.d.ts +21 -0
- package/dist/engine/video-source.d.ts.map +1 -1
- package/dist/engine/video-source.js +40 -0
- package/dist/engine/video-source.js.map +1 -1
- package/dist/foundry-emit.d.ts +146 -0
- package/dist/foundry-emit.d.ts.map +1 -0
- package/dist/foundry-emit.js +189 -0
- package/dist/foundry-emit.js.map +1 -0
- package/dist/integration/index.d.ts.map +1 -1
- package/dist/integration/index.js +9 -0
- package/dist/integration/index.js.map +1 -1
- package/dist/integration/options.d.ts +24 -0
- package/dist/integration/options.d.ts.map +1 -1
- package/dist/integration/options.js +22 -0
- package/dist/integration/options.js.map +1 -1
- package/dist/providers/null.d.ts.map +1 -1
- package/dist/providers/null.js +6 -0
- package/dist/providers/null.js.map +1 -1
- package/dist/providers/types.d.ts +56 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/routes/ai.d.ts.map +1 -1
- package/dist/routes/ai.js +21 -13
- package/dist/routes/ai.js.map +1 -1
- package/dist/routes/analytics.d.ts +1 -0
- package/dist/routes/analytics.d.ts.map +1 -1
- package/dist/routes/analytics.js +20 -0
- package/dist/routes/analytics.js.map +1 -1
- package/dist/routes/content.d.ts +27 -0
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +58 -12
- package/dist/routes/content.js.map +1 -1
- package/dist/routes/foundry-publish.d.ts +50 -0
- package/dist/routes/foundry-publish.d.ts.map +1 -1
- package/dist/routes/foundry-publish.js +540 -27
- package/dist/routes/foundry-publish.js.map +1 -1
- package/dist/routes/machine-context.d.ts +6 -0
- package/dist/routes/machine-context.d.ts.map +1 -0
- package/dist/routes/machine-context.js +20 -0
- package/dist/routes/machine-context.js.map +1 -0
- package/dist/routes/surveys.d.ts +14 -1
- package/dist/routes/surveys.d.ts.map +1 -1
- package/dist/routes/surveys.js +515 -10
- package/dist/routes/surveys.js.map +1 -1
- package/dist/schema/insights-ingest.d.ts +8 -8
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +15 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/tables.d.ts +1 -1
- package/dist/schema/tables.d.ts.map +1 -1
- package/dist/schema/tables.js +2 -0
- package/dist/schema/tables.js.map +1 -1
- package/dist/surveys/access.d.ts +1 -0
- package/dist/surveys/access.d.ts.map +1 -1
- package/dist/surveys/access.js +13 -4
- package/dist/surveys/access.js.map +1 -1
- package/dist/surveys/auto-label.d.ts +100 -0
- package/dist/surveys/auto-label.d.ts.map +1 -0
- package/dist/surveys/auto-label.js +325 -0
- package/dist/surveys/auto-label.js.map +1 -0
- package/dist/surveys/schema.d.ts +38 -12
- package/dist/surveys/schema.d.ts.map +1 -1
- package/dist/surveys/schema.js +18 -1
- package/dist/surveys/schema.js.map +1 -1
- package/dist/ui/api/_content-config.d.ts.map +1 -1
- package/dist/ui/api/_content-config.js +4 -0
- package/dist/ui/api/_content-config.js.map +1 -1
- package/dist/ui/api/analytics.d.ts.map +1 -1
- package/dist/ui/api/analytics.js +5 -2
- package/dist/ui/api/analytics.js.map +1 -1
- package/dist/ui/api/foundry/articles/[external_id]/publish.d.ts +3 -0
- package/dist/ui/api/foundry/articles/[external_id]/publish.d.ts.map +1 -0
- package/dist/ui/api/foundry/articles/[external_id]/publish.js +14 -0
- package/dist/ui/api/foundry/articles/[external_id]/publish.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.d.ts +3 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.d.ts.map +1 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.js +17 -0
- package/dist/ui/api/surveys/[surveyId]/answers/label.js.map +1 -0
- package/dist/ui/api/surveys/[surveyId].d.ts.map +1 -1
- package/dist/ui/api/surveys/[surveyId].js +28 -1
- package/dist/ui/api/surveys/[surveyId].js.map +1 -1
- package/dist/ui/commands.d.ts.map +1 -1
- package/dist/ui/commands.js +2 -0
- package/dist/ui/commands.js.map +1 -1
- package/dist/ui/hash-router.d.ts +1 -1
- package/dist/ui/hash-router.d.ts.map +1 -1
- package/dist/ui/hash-router.js +1 -0
- package/dist/ui/hash-router.js.map +1 -1
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +1 -0
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/screens/AnalyticsScreen.d.ts +6 -1
- package/dist/ui/screens/AnalyticsScreen.d.ts.map +1 -1
- package/dist/ui/screens/AnalyticsScreen.js +13 -8
- package/dist/ui/screens/AnalyticsScreen.js.map +1 -1
- package/dist/ui/screens/PodcastListeningScreen.d.ts +6 -0
- package/dist/ui/screens/PodcastListeningScreen.d.ts.map +1 -0
- package/dist/ui/screens/PodcastListeningScreen.js +244 -0
- package/dist/ui/screens/PodcastListeningScreen.js.map +1 -0
- package/dist/ui/screens/SurveyResponsesScreen.d.ts +4 -2
- package/dist/ui/screens/SurveyResponsesScreen.d.ts.map +1 -1
- package/dist/ui/screens/SurveyResponsesScreen.js +71 -12
- package/dist/ui/screens/SurveyResponsesScreen.js.map +1 -1
- package/dist/ui/screens/analytics-data.d.ts +14 -0
- package/dist/ui/screens/analytics-data.d.ts.map +1 -1
- package/dist/ui/screens/analytics-data.js +16 -0
- package/dist/ui/screens/analytics-data.js.map +1 -1
- package/dist/ui/screens/podcast-listening-data.d.ts +19 -0
- package/dist/ui/screens/podcast-listening-data.d.ts.map +1 -0
- package/dist/ui/screens/podcast-listening-data.js +98 -0
- package/dist/ui/screens/podcast-listening-data.js.map +1 -0
- package/dist/ui/screens/registry.d.ts.map +1 -1
- package/dist/ui/screens/registry.js +2 -0
- package/dist/ui/screens/registry.js.map +1 -1
- package/docs/foundry-publish.md +208 -0
- package/migrations/0033_survey_answer_labels.sql +15 -0
- package/package.json +9 -3
- package/src/cli/foundry-emit.ts +85 -0
- package/src/engine/foundry-dispatch.ts +1 -1
- package/src/engine/fronts-publish-intent.ts +23 -1
- package/src/engine/index.ts +5 -0
- package/src/engine/publisher.ts +80 -4
- package/src/engine/tags.ts +21 -1
- package/src/engine/topics.ts +4 -1
- package/src/engine/video-source.ts +44 -0
- package/src/foundry-emit.ts +255 -0
- package/src/integration/index.ts +9 -0
- package/src/integration/options.ts +23 -0
- package/src/providers/null.ts +6 -0
- package/src/providers/types.ts +52 -0
- package/src/routes/ai.ts +29 -17
- package/src/routes/analytics.ts +21 -0
- package/src/routes/content.ts +81 -15
- package/src/routes/foundry-publish.ts +746 -29
- package/src/routes/machine-context.ts +24 -0
- package/src/routes/surveys.ts +587 -12
- package/src/schema/migrations.ts +15 -0
- package/src/schema/tables.ts +2 -0
- package/src/surveys/access.ts +14 -4
- package/src/surveys/auto-label.ts +420 -0
- package/src/surveys/schema.ts +25 -1
- package/src/ui/api/_content-config.ts +6 -0
- package/src/ui/api/analytics.ts +5 -2
- package/src/ui/api/foundry/articles/[external_id]/publish.ts +18 -0
- package/src/ui/api/surveys/[surveyId]/answers/label.ts +23 -0
- package/src/ui/api/surveys/[surveyId].ts +37 -3
- package/src/ui/commands.ts +2 -0
- package/src/ui/hash-router.ts +1 -0
- package/src/ui/nav.ts +1 -0
- package/src/ui/screens/AnalyticsScreen.tsx +34 -6
- package/src/ui/screens/PodcastListeningScreen.tsx +529 -0
- package/src/ui/screens/SurveyResponsesScreen.tsx +179 -13
- package/src/ui/screens/analytics-data.ts +18 -0
- package/src/ui/screens/podcast-listening-data.ts +114 -0
- package/src/ui/screens/registry.tsx +2 -0
package/src/routes/content.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { invalidateMediaCaptureStatements, softDeleteContent } from '../engine/s
|
|
|
66
66
|
import { normalizeSourceSections } from '../engine/source-sections.js'
|
|
67
67
|
import { TagInputError } from '../engine/taxonomy.js'
|
|
68
68
|
import type { ValidationError } from '../engine/validator/index.js'
|
|
69
|
+
import { VideoSourceNotIngestableError } from '../engine/video-source.js'
|
|
69
70
|
import {
|
|
70
71
|
isValidTimeZone,
|
|
71
72
|
PUBLISH_TIMEZONE_FALLBACK,
|
|
@@ -79,6 +80,8 @@ import { type ContentAction, canPerformContentAction } from './authz-matrix.js'
|
|
|
79
80
|
import type { CmsRouteConfig } from './config.js'
|
|
80
81
|
import { resolveConfig } from './config.js'
|
|
81
82
|
import { json, type RouteContext, requireId } from './context.js'
|
|
83
|
+
import type { FoundryPublishOptions } from './foundry-publish.js'
|
|
84
|
+
import { isFoundryMachineContext } from './machine-context.js'
|
|
82
85
|
import type { DispatchWebhookHook } from './webhooks.js'
|
|
83
86
|
|
|
84
87
|
export type { DispatchWebhookHook }
|
|
@@ -146,6 +149,11 @@ function mediaSourceProviderErrorResponse(error: unknown): Response | null {
|
|
|
146
149
|
return json({ error: failure.message, reason: failure.code }, failure.status)
|
|
147
150
|
}
|
|
148
151
|
|
|
152
|
+
function videoSourceNotIngestableResponse(error: unknown): Response | null {
|
|
153
|
+
if (!(error instanceof VideoSourceNotIngestableError)) return null
|
|
154
|
+
return json({ error: error.message, reason: 'video_source_not_ingestable' }, 400)
|
|
155
|
+
}
|
|
156
|
+
|
|
149
157
|
function verifiedMediaDurationConflictResponse(error: unknown): Response | null {
|
|
150
158
|
if (!(error instanceof VerifiedMediaDurationConflictError)) return null
|
|
151
159
|
return json(
|
|
@@ -231,6 +239,14 @@ export interface ContentRouteConfig extends CmsRouteConfig {
|
|
|
231
239
|
videoSourceProductionEnabled?: boolean
|
|
232
240
|
/** Internal machine-route constraint: atomically update only in this status. */
|
|
233
241
|
updateStatusConstraint?: ContentStatus
|
|
242
|
+
/**
|
|
243
|
+
* Internal (Foundry machine route, M3-18): replaces ONLY the publish action's
|
|
244
|
+
* commit step (publishContent + createRevision) with the caller's — publishOne
|
|
245
|
+
* with the publication's live surfaces. Every other step of the native publish
|
|
246
|
+
* action (readiness, body guard, dek, tag check, activity log, takeaways, FAQ,
|
|
247
|
+
* the content.published webhook) runs unchanged, through the same handler.
|
|
248
|
+
*/
|
|
249
|
+
publishCommit?: (ctx: RouteContext, input: { contentId: string; now: number }) => Promise<void>
|
|
234
250
|
/**
|
|
235
251
|
* Optional webhook dispatcher hook (P7 Task 9). Fired at each content
|
|
236
252
|
* lifecycle transition (publish, unpublish, schedule, archive, create, etc.).
|
|
@@ -245,6 +261,21 @@ export interface ContentRouteConfig extends CmsRouteConfig {
|
|
|
245
261
|
* when absent so callers without a workspace concept still produce a scoped row.
|
|
246
262
|
*/
|
|
247
263
|
activeWorkspaceId?: string
|
|
264
|
+
/**
|
|
265
|
+
* Per-publication opt-in for the Foundry machine route's publish transition
|
|
266
|
+
* (M3-18). Omitted, or `autoPublish` anything but `true`, keeps that route
|
|
267
|
+
* draft-only and byte-identical to the v1 route. Read only by routes/foundry-publish.ts.
|
|
268
|
+
*/
|
|
269
|
+
foundryPublish?: FoundryPublishOptions
|
|
270
|
+
/**
|
|
271
|
+
* This publication's canonical host for the Foundry route family, declared
|
|
272
|
+
* statically (the integration's `foundryCanonicalHost`, so it reaches every
|
|
273
|
+
* request whatever the host's per-request hooks set). Once a host is declared
|
|
274
|
+
* here or in `foundryPublish.canonicalHost`, the Foundry route verifies only
|
|
275
|
+
* the host-bound `foundry-cms-v2` string and never the legacy dot form.
|
|
276
|
+
* Read only by routes/foundry-publish.ts.
|
|
277
|
+
*/
|
|
278
|
+
foundryCanonicalHost?: string
|
|
248
279
|
}
|
|
249
280
|
|
|
250
281
|
type ContentType = 'article' | 'video' | 'podcast' | 'newsletter' | 'page'
|
|
@@ -422,6 +453,8 @@ const NEXTCLOUD_SHARE_HOSTS = ['drive.fulcrum-portal.com']
|
|
|
422
453
|
const NEXTCLOUD_SHARE_PATH_RE = /^\/s\/([A-Za-z0-9_-]+)(?:\/download)?\/?$/
|
|
423
454
|
const NEXTCLOUD_DIRECT_PATH_RE = /^\/public\.php\/dav\/files\/[A-Za-z0-9_-]+$/
|
|
424
455
|
|
|
456
|
+
const NEXTCLOUD_SHARE_NOISE_PARAMS: ReadonlySet<string> = new Set(['dir', 'editing', 'openfile'])
|
|
457
|
+
|
|
425
458
|
export function normaliseNextcloudShareUrl(value: string): string {
|
|
426
459
|
let url: URL
|
|
427
460
|
try {
|
|
@@ -440,13 +473,19 @@ export function normaliseNextcloudShareUrl(value: string): string {
|
|
|
440
473
|
) {
|
|
441
474
|
return value
|
|
442
475
|
}
|
|
443
|
-
|
|
444
|
-
|
|
476
|
+
// Nextcloud's copy-link output adds page-navigation noise -- `?dir=/` from a
|
|
477
|
+
// folder listing, `?dir=/&editing=false&openfile=true` from the viewer --
|
|
478
|
+
// none of which changes the file a share serves. Any other parameter may
|
|
479
|
+
// select a different resource, so the URL is kept and the publisher's
|
|
480
|
+
// ingest guard refuses it (HomeFronts 2026-10-01T18:05Z).
|
|
481
|
+
const onlyNoiseQuery =
|
|
482
|
+
url.search === '' ||
|
|
483
|
+
[...url.searchParams.keys()].every((key) => NEXTCLOUD_SHARE_NOISE_PARAMS.has(key))
|
|
445
484
|
if (NEXTCLOUD_DIRECT_PATH_RE.test(url.pathname)) {
|
|
446
|
-
return url.search === '' ? value :
|
|
485
|
+
return url.search === '' ? value : onlyNoiseQuery ? `https://${host}${url.pathname}` : value
|
|
447
486
|
}
|
|
448
487
|
const share = NEXTCLOUD_SHARE_PATH_RE.exec(url.pathname)
|
|
449
|
-
if (!share || !
|
|
488
|
+
if (!share || !onlyNoiseQuery) return value
|
|
450
489
|
return `https://${host}/public.php/dav/files/${share[1]}`
|
|
451
490
|
}
|
|
452
491
|
|
|
@@ -1182,6 +1221,15 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1182
1221
|
* — identical guard pattern to dispatchTakeaways / logActivity.
|
|
1183
1222
|
* When no hook is injected this is a no-op (no 501, no error).
|
|
1184
1223
|
*/
|
|
1224
|
+
/**
|
|
1225
|
+
* Only a context the HMAC-verified Foundry route marked is the machine
|
|
1226
|
+
* (routes/machine-context.ts); a subject or userId string never decides it,
|
|
1227
|
+
* so no person can act as the machine. Any other actor is a person.
|
|
1228
|
+
*/
|
|
1229
|
+
async function isFoundryActor(ctx: RouteContext): Promise<boolean> {
|
|
1230
|
+
return isFoundryMachineContext(ctx)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1185
1233
|
async function fireWebhook(
|
|
1186
1234
|
ctx: RouteContext,
|
|
1187
1235
|
eventType: string,
|
|
@@ -1425,12 +1473,14 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1425
1473
|
if (op === 'topic') {
|
|
1426
1474
|
const { topic } = parsed.data
|
|
1427
1475
|
for (const id of ids) {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1476
|
+
// Through the editor's own save path, so it is a canonical edit; the
|
|
1477
|
+
// existing category is passed through so the save changes nothing else.
|
|
1478
|
+
const existing = await getContentItem(ctx.db, id)
|
|
1479
|
+
if (!existing || (existing as { deleted_at?: number | null }).deleted_at != null) continue
|
|
1480
|
+
await updateContentItem(ctx.db, id, {
|
|
1481
|
+
primaryTopic: topic,
|
|
1482
|
+
primaryCategory: existing.channel,
|
|
1483
|
+
})
|
|
1434
1484
|
}
|
|
1435
1485
|
return json({ updated: ids })
|
|
1436
1486
|
}
|
|
@@ -1532,6 +1582,8 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1532
1582
|
created = await createContent(ctx.db, parsed.data)
|
|
1533
1583
|
} catch (error) {
|
|
1534
1584
|
if (error instanceof TagInputError) return tagInputErrorResponse(error)
|
|
1585
|
+
const notIngestable = videoSourceNotIngestableResponse(error)
|
|
1586
|
+
if (notIngestable) return notIngestable
|
|
1535
1587
|
const conflict = verifiedMediaDurationConflictResponse(error)
|
|
1536
1588
|
if (conflict) return conflict
|
|
1537
1589
|
throw error
|
|
@@ -1778,6 +1830,8 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1778
1830
|
})
|
|
1779
1831
|
} catch (error) {
|
|
1780
1832
|
if (error instanceof TagInputError) return tagInputErrorResponse(error)
|
|
1833
|
+
const notIngestable = videoSourceNotIngestableResponse(error)
|
|
1834
|
+
if (notIngestable) return notIngestable
|
|
1781
1835
|
const conflict = verifiedMediaDurationConflictResponse(error)
|
|
1782
1836
|
if (conflict) return conflict
|
|
1783
1837
|
throw error
|
|
@@ -2034,8 +2088,12 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
2034
2088
|
throw error
|
|
2035
2089
|
}
|
|
2036
2090
|
try {
|
|
2037
|
-
|
|
2038
|
-
|
|
2091
|
+
if (config.publishCommit) {
|
|
2092
|
+
await config.publishCommit(ctx, { contentId: id, now })
|
|
2093
|
+
} else {
|
|
2094
|
+
await publishContent(ctx.db, id, publishedAt, now)
|
|
2095
|
+
await createRevision(ctx.db, id, ctx.userId || null)
|
|
2096
|
+
}
|
|
2039
2097
|
} catch (error) {
|
|
2040
2098
|
if (error instanceof TagInputError) return tagInputErrorResponse(error)
|
|
2041
2099
|
throw error
|
|
@@ -2127,7 +2185,10 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
2127
2185
|
}
|
|
2128
2186
|
|
|
2129
2187
|
if (action === 'unpublish') {
|
|
2130
|
-
|
|
2188
|
+
// A person's unpublish sticks: it is a canonical edit, so a machine
|
|
2189
|
+
// publisher cannot put the item back without a new write. The
|
|
2190
|
+
// machine's own unpublish is not, so it can republish its item.
|
|
2191
|
+
await unpublishContent(ctx.db, id, { canonicalEdit: !(await isFoundryActor(ctx)) })
|
|
2131
2192
|
|
|
2132
2193
|
// Fire content.unpublished webhook.
|
|
2133
2194
|
await fireWebhook(ctx, 'content.unpublished', {
|
|
@@ -2163,9 +2224,11 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
2163
2224
|
if (action === 'unschedule') {
|
|
2164
2225
|
// publish_tz is NOT NULL (DEFAULT 'Europe/Paris'); reset it to the
|
|
2165
2226
|
// default rather than nulling it (a NULL violates the constraint).
|
|
2227
|
+
// A person pulling a scheduled item is a canonical edit (see unpublish).
|
|
2228
|
+
const pulledByPerson = !(await isFoundryActor(ctx))
|
|
2166
2229
|
await ctx.db
|
|
2167
2230
|
.prepare(
|
|
2168
|
-
|
|
2231
|
+
`UPDATE content_items SET status = 'draft', publish_at = NULL, publish_tz = 'Europe/Paris', updated_at = unixepoch()${pulledByPerson ? ', canonical_version = canonical_version + 1' : ''} WHERE id = ?`,
|
|
2169
2232
|
)
|
|
2170
2233
|
.bind(id)
|
|
2171
2234
|
.run()
|
|
@@ -2204,9 +2267,12 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
2204
2267
|
// publish_at so a de-scheduled item doesn't carry a phantom publish time.
|
|
2205
2268
|
// publish_tz is NOT NULL (DEFAULT 'Europe/Paris'), so it is reset to the
|
|
2206
2269
|
// default rather than nulled.
|
|
2270
|
+
// A person moving the item back to draft or review is a canonical
|
|
2271
|
+
// edit (see unpublish); the machine never calls set-status.
|
|
2272
|
+
const movedByPerson = !(await isFoundryActor(ctx))
|
|
2207
2273
|
await ctx.db
|
|
2208
2274
|
.prepare(
|
|
2209
|
-
|
|
2275
|
+
`UPDATE content_items SET status = ?, publish_at = NULL, publish_tz = 'Europe/Paris', updated_at = unixepoch()${movedByPerson ? ', canonical_version = canonical_version + 1' : ''} WHERE id = ?`,
|
|
2210
2276
|
)
|
|
2211
2277
|
.bind(status, id)
|
|
2212
2278
|
.run()
|