import type { Kysely } from 'kysely'; import type { TaprootDb } from '../db/client.js'; import type { ContentStatus, Database } from '../db/schema.js'; import type { ContentItem } from './items.js'; import { type ReleaseProblem } from './releases.js'; /** * Scheduled publishing. * * `scheduled` has been a real status with a colour and a filter option since Phase 1, and nothing * ever flipped one live — the seed included one specifically so the gap would be visible rather * than theoretical. * * Two halves, and both are needed for different reasons: * * - **Visibility is computed on read.** A scheduled item whose time has passed is served to * visitors whether or not a sweep has run. That is what makes "goes live at 9am" true on a * deployment where nobody wired up a cron — which is every deployment on its first day, and * most small ones forever. * - **Status is swept on a timer.** The read rule alone would leave the admin saying "scheduled" * about a page the public can already see, and leave `published_at` empty for something that is * demonstrably published. The sweep makes the record agree with reality. * * Doing only the sweep would mean a missed cron silently holds a launch. Doing only the read rule * would mean the CMS lies about its own content. Neither alone is the feature. */ /** Items whose scheduled time has arrived but whose status has not caught up. */ export declare function dueForPublishing(db: Kysely, limit?: number): Promise; export interface SchedulerStatus { /** Items waiting for their moment, whether or not it has arrived. */ waiting: number; /** Of those, the ones whose moment has passed and whose status has not caught up. */ due: number; /** Releases waiting for their moment, whether or not it has arrived. */ releasesWaiting: number; /** Of those, the ones whose moment has passed and which nothing has published. */ releasesDue: number; /** * Releases a sweep reached and refused. * * A number an operator has to act on rather than watch: unlike `due`, which clears itself once a * working cron catches up, a blocked release stays blocked until somebody fixes the content. */ releasesBlocked: number; /** * When the sweep last published something, or `null` if it never has. * * Read from the audit log rather than a stored heartbeat: `item.published` with no actor can only * have come from `publishDueItems`, because every other path to that action has a person on it. */ lastSweptAt: string | null; } /** * What an operator needs to answer "is scheduled publishing actually running here". * * `due` is the load-bearing number and the only reliable one. A sweep that finds nothing writes * nothing, so on a site that schedules content rarely `lastSweptAt` can be months old with a * perfectly healthy cron — it says when the sweep last *did* something, never when it last ran. * A non-zero `due` that persists is the signal that nothing is sweeping, which is why the admin * leads with it. */ export declare function schedulerStatus(db: Kysely): Promise; export interface SweepResult { published: { id: string; title: string; path: string; }[]; } /** * Publish everything whose time has come. * * Deliberately **not** routed through `updateItem`. That path recomputes paths, cascades a subtree, * writes redirects, and appends a revision — all correct for an edit, and all wrong here: nothing * about the content changed, only the status, and appending a revision per scheduled item would * fill the history with entries nobody wrote. The status and `published_at` are the whole change. * * Each item is its own update rather than one bulk statement, so `published_at` is the moment the * sweep reached it and one failure cannot take the batch with it. */ export declare function publishDueItems(db: Kysely, limit?: number): Promise; export interface ReleaseSweepResult { published: { id: string; name: string; itemCount: number; /** * The items that went live, and the status each came from. * * Carried because the sweep is the one publish with no request behind it — nothing calls * `taproot.emit`, so whatever this does not report cannot be told to an integration at all. The * `from` is knowable only inside `publishRelease`'s loop, and it is what separates a page that * became public from a copy edit to one that already was. */ items: { id: string; from: ContentStatus; }[]; }[]; /** Releases the sweep reached and refused, with the reasons it refused them. */ blocked: { id: string; name: string; problems: ReleaseProblem[]; }[]; } /** * Publish every release whose scheduled time has come. * * The second half of Content Releases, and the reason the feature is worth having: "all going live * at 9am on the same day" is a promise the CMS makes on somebody's behalf while they are asleep. * * Note what this deliberately does *not* have: the read-time counterpart that scheduled items get. * A scheduled item is visible the moment its time passes whether or not a sweep runs, because its * content is already in `content_items` and visibility is one predicate away. A release's content * lives in `release_items` and has to be *applied* — paths cascade, redirects are written, revisions * are appended — so there is nothing a read could do. A release genuinely needs the timer, which is * why `schedulerStatus` reports its own counts and the system screen leads with them. */ export declare function publishDueReleases(handle: TaprootDb, limit?: number): Promise;