Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d7aab5f
feat(core): expose media usage indexing progress
khoinguyenpham04 Aug 16, 2026
b22c22a
feat(admin): add guided media usage setup
khoinguyenpham04 Aug 16, 2026
5af9593
feat(admin): show media usage indexing progress
khoinguyenpham04 Aug 16, 2026
91eeaef
fix(core): start Node media usage reconciliation
khoinguyenpham04 Aug 17, 2026
851e082
feat(core): process one due media usage unit
khoinguyenpham04 Aug 17, 2026
a02ab2f
feat(core): continue media usage maintenance on Node
khoinguyenpham04 Aug 17, 2026
c62935a
feat(cloudflare): continue media usage work through queue wakeups
khoinguyenpham04 Aug 17, 2026
1fcbc86
fix(admin): format media usage progress fallback
khoinguyenpham04 Aug 18, 2026
64279d0
perf(core): enlarge media usage projection admission
khoinguyenpham04 Aug 18, 2026
b704b5a
perf(cloudflare): drain media usage work per queue delivery
khoinguyenpham04 Aug 18, 2026
826de42
docs(media-usage): document paid backfill requirements
khoinguyenpham04 Aug 18, 2026
e2d516b
feat(core): continue media usage activation in background
khoinguyenpham04 Aug 18, 2026
1d0ddd7
feat(admin): show automatic media usage setup progress
khoinguyenpham04 Aug 18, 2026
11361ec
docs(media-usage): document one-action setup
khoinguyenpham04 Aug 18, 2026
f46d11e
fix(core): keep media usage maintenance work-conserving
khoinguyenpham04 Aug 19, 2026
df80594
feat(media-usage): show finalizing setup progress
khoinguyenpham04 Aug 19, 2026
880b4c5
fix(core): wake media usage maintenance after activation
khoinguyenpham04 Aug 19, 2026
28dd347
perf(core): process media usage work in bulk
khoinguyenpham04 Aug 20, 2026
72517f2
perf(media-usage): run work-conserving bulk jobs
khoinguyenpham04 Aug 20, 2026
fae21f9
fix(media-usage): preserve bounded bulk recovery
khoinguyenpham04 Aug 20, 2026
2585ebb
perf(media-usage): sweep reconciliation sources in bulk
khoinguyenpham04 Aug 20, 2026
fa6fbf2
perf(media-usage): reduce maintenance round trips
khoinguyenpham04 Aug 20, 2026
2cdb2ad
fix(media-usage): bound D1 repair source lookups
khoinguyenpham04 Aug 20, 2026
30b6136
fix(cloudflare): keep media recovery independent
khoinguyenpham04 Aug 20, 2026
3b485c0
refactor(media-usage): remove superseded single-row work paths
khoinguyenpham04 Aug 20, 2026
ccecc1e
refactor(media-usage): remove scheduled recovery paths
khoinguyenpham04 Aug 20, 2026
e66bba6
test(media-usage): remove redundant coverage
khoinguyenpham04 Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fast-media-usage-backfill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"emdash": patch
"@emdash-cms/cloudflare": patch
---

Speeds up Media Usage setup and historical indexing on Workers Paid, including entries with up to 500 media occurrences.
7 changes: 7 additions & 0 deletions .changeset/media-usage-activation-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"emdash": minor
"@emdash-cms/admin": minor
"@emdash-cms/cloudflare": minor
---

Adds one-action Media Usage setup with live indexing status and faster background indexing for existing content.
5 changes: 5 additions & 0 deletions .changeset/media-usage-progress-locale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@emdash-cms/admin": patch
---

Fixes Media Usage progress text displaying untranslated plural syntax.
6 changes: 6 additions & 0 deletions .changeset/remove-media-usage-scheduled-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"emdash": minor
"@emdash-cms/cloudflare": minor
---

Removes scheduled Media Usage recovery APIs. Remove any dedicated Media Usage Cron trigger, call `createScheduledHandler()` without `mediaUsageCron` or `resolveMediaUsageQueue`, and update custom Node schedulers to implement the continuous Media Usage callback and wake methods.
14 changes: 12 additions & 2 deletions demos/cloudflare/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@
*/

import handler from "@astrojs/cloudflare/entrypoints/server";
import { createScheduledHandler, PluginBridge } from "@emdash-cms/cloudflare/worker";
import {
createMediaUsageQueueHandler,
createMediaUsageFetchHandler,
createScheduledHandler,
type MediaUsageWakeMessage,
PluginBridge,
} from "@emdash-cms/cloudflare/worker";

export { PluginBridge };

const resolveMediaUsageQueue = (env: Env) => env.MEDIA_USAGE_QUEUE;

export default {
...handler,
fetch: createMediaUsageFetchHandler(handler, resolveMediaUsageQueue),
scheduled: createScheduledHandler(),
} satisfies ExportedHandler<Env>;
queue: createMediaUsageQueueHandler(resolveMediaUsageQueue),
} satisfies ExportedHandler<Env, MediaUsageWakeMessage>;
1 change: 1 addition & 0 deletions demos/cloudflare/worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ declare namespace Cloudflare {
interface Env {
MEDIA: R2Bucket;
DB: D1Database;
MEDIA_USAGE_QUEUE: Queue;
LOADER: WorkerLoader;
AI_SEARCH: AiSearchNamespace;
}
Expand Down
24 changes: 22 additions & 2 deletions demos/cloudflare/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,29 @@
"bucket_name": "emdash-media",
},
],
// Cron trigger drives the AI Search reindex queue flush.
"queues": {
"producers": [
{
"binding": "MEDIA_USAGE_QUEUE",
"queue": "emdash-demo-media-usage",
},
],
"consumers": [
{
"queue": "emdash-demo-media-usage",
"max_batch_size": 1,
"max_batch_timeout": 0,
"max_retries": 3,
"max_concurrency": 1,
},
],
},
"limits": {
"cpu_ms": 300000,
},
// Cron triggers drive general maintenance.
"triggers": {
"crons": ["* * * * *", "*/2 * * * *"],
"crons": ["* * * * *"],
},
// Observability
"observability": {
Expand Down
69 changes: 58 additions & 11 deletions docs/src/content/docs/deployment/cloudflare.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Cloudflare Workers provides a fast, globally distributed runtime for EmDash. Thi
## Prerequisites

- A Cloudflare account
- Wrangler CLI installed (`npm install -g wrangler`)
- Workers Paid when activating automatic Media Usage indexing
- Wrangler 4.123 or newer (`npx wrangler@latest`)
- Authenticated with Cloudflare (`wrangler login`)

## Configure Bindings
Expand Down Expand Up @@ -76,44 +77,90 @@ To change the schema or content model of a site that is already deployed, see [E

## Scheduled Publishing

On Cloudflare Workers, scheduled publishing, plugin cron, and maintenance tasks run from Worker Cron Triggers. New Cloudflare templates include both required schedules automatically. When updating an existing project, configure distinct general and Media Usage lanes:
On Cloudflare Workers, scheduled publishing, plugin cron, and maintenance tasks run from one Worker Cron Trigger. New Cloudflare templates include the required schedule automatically. When updating an existing project, configure the general maintenance handler:

```ts title="src/worker.ts"
import handler, {
createMediaUsageFetchHandler,
createMediaUsageQueueHandler,
createScheduledHandler,
type MediaUsageWakeMessage,
PluginBridge,
} from "@emdash-cms/cloudflare/worker";

export { PluginBridge };

const resolveMediaUsageQueue = (env: Env) => env.MEDIA_USAGE_QUEUE;

export default {
...handler,
fetch: createMediaUsageFetchHandler(handler, resolveMediaUsageQueue),
scheduled: createScheduledHandler(),
} satisfies ExportedHandler;
queue: createMediaUsageQueueHandler(resolveMediaUsageQueue),
} satisfies ExportedHandler<Env, MediaUsageWakeMessage>;
```

By default, `*/2 * * * *` runs Media Usage maintenance and every other expression runs general maintenance. Then add both Cron Triggers to `wrangler.jsonc`:
Add one Cron Trigger for general maintenance, the five-minute CPU allowance, and the automatically provisioned Queue to `wrangler.jsonc`:

```jsonc title="wrangler.jsonc"
{
"triggers": {
"crons": ["* * * * *", "*/2 * * * *"],
"crons": ["* * * * *"],
},
"limits": {
"cpu_ms": 300000,
},
"queues": {
"producers": [
{
"binding": "MEDIA_USAGE_QUEUE",
"queue": "my-emdash-site-media-usage",
},
],
"consumers": [
{
"queue": "my-emdash-site-media-usage",
"max_batch_size": 1,
"max_batch_timeout": 0,
"max_retries": 3,
"max_concurrency": 1,
},
],
},
}
```

To use different schedules, set the corresponding `generalCron` or `mediaUsageCron` option in `createScheduledHandler()` and use the same expression in `wrangler.jsonc`.
Run `wrangler types` after changing the bindings so `Env.MEDIA_USAGE_QUEUE` is available to
TypeScript.

Enabling Media Usage sends the first Queue message. Each batch processes up to 1,000 entries. The
consumer continues with another batch while its invocation has capacity, then sends another message
when work remains. Queue messages contain no content identity, and the existing database work rows
remain authoritative if a message is retried. Concurrency stays at one because D1 processes a database
sequentially.

To use a different general maintenance schedule, set `generalCron` in `createScheduledHandler()` and
use the same expression in `wrangler.jsonc`.

<Aside type="caution">
Without the general trigger, scheduled publishing and plugin cron do not run. Without the dedicated Media Usage trigger, automatic historical reconciliation cannot progress. Local `astro dev` still uses the in-process scheduler.
Keep the Media Usage Queue configured until setup reaches **Ready**. A stopped
Queue chain does not restart from the general Cron.
</Aside>

### Enable automatic media usage indexing

Keep `mediaUsageCron` running while you enable automatic media usage indexing. Pause all application
and direct database writes, follow [Enable automatic media usage
indexing](/reference/rest-api/#enable-automatic-media-usage-indexing), then resume writes when the
endpoint returns `active`. Existing content is indexed in the background.
Pause all application and direct database writes. Open
**Settings → Media Usage**, select **Enable Media Usage**, and confirm the safety requirements once.
EmDash queues the remaining setup work and starts indexing existing content. This work continues if
you close the page while the Queue chain remains active.

Keep writers stopped while the page shows **Setting up**. Resume writers when it shows **Starting
indexing**, **Indexing existing content**, or **Ready**. While the page is visible, it refreshes
progress every two seconds. If **Needs attention** offers **Retry setup**, keep writers stopped, fix
the server error, and retry. An indexing failure after setup is active does not block writers.

The [REST procedure](/reference/rest-api/#enable-automatic-media-usage-indexing) remains available
for API operators.

## Deploy

Expand Down
23 changes: 19 additions & 4 deletions docs/src/content/docs/deployment/nodejs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,30 @@ The server runs on `http://localhost:4321` by default. Migrations are applied on
The built-in scheduler runs only while a Node process is running. It handles scheduled publishing,
plugin tasks, and background media indexing.

While Media Usage work is available, the scheduler continues one bounded indexing unit on each event
loop turn until it catches up. It yields between units so requests and other scheduled work remain
responsive.

Keep at least one Node process running continuously in production. If all processes stop or sleep,
scheduled tasks pause.

### Enable automatic media usage indexing

Keep at least one Node process running while you enable automatic media usage indexing. Pause all
application and direct database writes, follow [Enable automatic media usage
indexing](/reference/rest-api/#enable-automatic-media-usage-indexing), then resume writes when the
endpoint returns `active`. Existing content is indexed in the background.
Keep at least one Node process running, then pause all application and direct database writes. Open
**Settings → Media Usage**, select **Enable Media Usage**, and confirm the safety requirements once.
The Node scheduler prepares the remaining collections and starts indexing existing content. This
work continues if you close the page.

Keep the Node process running until Media Usage reaches **Ready**. An interrupted process does not
restart Media Usage work from the general scheduler heartbeat.

Keep writers stopped while the page shows **Setting up**. Resume writers when it shows **Starting
indexing**, **Indexing existing content**, or **Ready**. While the page is visible, it refreshes
progress every two seconds. If **Needs attention** offers **Retry setup**, keep writers stopped, fix
the server error, and retry. An indexing failure after setup is active does not block writers.

The [REST procedure](/reference/rest-api/#enable-automatic-media-usage-indexing) remains available
for API operators.

## Production Storage

Expand Down
15 changes: 15 additions & 0 deletions docs/src/content/docs/guides/media-library.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ Open the media library from the admin sidebar by clicking **Media**. The library

<img src={mediaLibraryImg.src} alt="EmDash media library showing image grid with upload button" />

## Used in

Open a local media item's details to see known references from EmDash-managed content fields.
Already-found references remain visible while historical content is still indexing.

Administrators see a Media Library notice until automatic indexing is active. Open **Settings →
Media Usage**, select **Enable Media Usage**, and confirm the setup requirements once. Setup continues
in the background if you leave the page. The Settings page shows when indexing starts and how many
content types are ready.

<Aside type="caution">
Used in results are advisory. They do not prove that a media item is universally unused or safe to
delete.
</Aside>

## Uploading Files

### From the Media Library
Expand Down
43 changes: 34 additions & 9 deletions docs/src/content/docs/reference/rest-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ paused while EmDash prepares each collection so that no changes are missed.

Both endpoints require `schema:manage`. Bearer tokens also require the `admin` scope.

Administrators can run the same procedure from **Settings → Media Usage**. The page sends one POST,
then observes server-owned setup and indexing work. It cannot stop external writers or verify
scheduled maintenance.

#### Check the current state

```http
Expand All @@ -385,7 +389,7 @@ This request does not change anything. It returns one of these states:

Status responses do not include internal lock data or raw database errors.

#### Prepare the next collection
#### Start activation

```http
POST /_emdash/api/admin/media-usage/activation
Expand All @@ -398,28 +402,37 @@ X-EmDash-Request: 1
}
```

Each request prepares one collection. Send one request at a time until the state becomes `active`.
The request prepares at most one collection and wakes the configured Cloudflare Queue or Node
scheduler. These standard drivers continue with bounded batches after the response, including when
the client disconnects. Use `GET` to observe the durable state. With a standard driver, do not send
another POST unless the stored status reports a failure that you have fixed.

Custom integrations that provide scheduled maintenance but do not wire the standard wake can
continue sending one POST at a time. This compatibility path retains the same collection bound.

Set both fields to `true`:

- `writersDrained`: Application and direct database writes have stopped, and any writes already in
progress have finished.
- `maintenanceReady`: Scheduled background tasks are running through `mediaUsageCron` on Cloudflare
- `maintenanceReady`: Scheduled background tasks are running through the general Cron on Cloudflare
or the built-in scheduler on Node.js.

#### Enable indexing in production

1. Confirm that scheduled background tasks are running: `mediaUsageCron` on Cloudflare, or the
1. Confirm that scheduled background tasks are running: the general Cron on Cloudflare, or the
built-in scheduler on Node.js.
2. Stop all application and direct database writes. Wait for writes already in progress to finish.
3. Call `GET` to check the current state.
4. Call `POST` one request at a time until the state becomes `active`.
5. Resume writes.
4. With the standard Queue or Node driver, call `POST` once, then use `GET` until the state becomes
`active`. With a custom integration that does not continue activation, alternate one `POST` with
`GET` until the state becomes `active`.
5. Resume writes after the state becomes `active`.
6. Keep scheduled background tasks running while EmDash indexes existing content.

If `POST` times out or returns `409` or `500`, call `GET` before sending another request. If
`lastErrorCode` is set, keep writes stopped, check the application logs, fix the problem, and try
again. Do not edit EmDash's internal database tables.
If `POST` times out or returns `409` or `500`, call `GET` before deciding what to do. If the state is
still `activating` without `lastErrorCode`, the server is continuing or another worker owns the
current batch. If `lastErrorCode` is set, keep writes stopped, check the application logs, fix the
problem, and send one confirmed POST to retry. Do not edit EmDash's internal database tables.

<Aside type="caution">
After this process starts, it cannot be cancelled or reset. Test it in a staging environment and
Expand All @@ -429,6 +442,18 @@ again. Do not edit EmDash's internal database tables.
When the state is `active`, EmDash tracks changes to media references in content. Existing content
may still be indexing in the background.

#### Check historical indexing progress

```http
GET /_emdash/api/admin/media-usage/progress
```

After activation is active, this returns `indexing`, `ready`, or `needs_attention` together with the
number of ready and total current content types. The optional `indexingStarted` field is `false`
between activation finishing and the first current reconciliation starting. Older responses may omit
it. The endpoint does not inspect content rows or return work-item details. It requires
`schema:manage`; bearer tokens also require the `admin` scope.

### List Media Usage Work

```http
Expand Down
4 changes: 3 additions & 1 deletion packages/admin/src/components/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface ConfirmDialogProps {
/** Button variant — defaults to "destructive" */
variant?: "destructive" | "primary";
isPending: boolean;
disabled?: boolean;
/** Error from a mutation — pass mutation.error directly */
error: unknown;
onConfirm: () => void;
Expand All @@ -40,6 +41,7 @@ export function ConfirmDialog({
pendingLabel,
variant = "destructive",
isPending,
disabled = false,
error,
onConfirm,
children,
Expand All @@ -56,7 +58,7 @@ export function ConfirmDialog({
<Button variant="secondary" onClick={onClose}>
{t`Cancel`}
</Button>
<Button variant={variant} disabled={isPending} onClick={onConfirm}>
<Button variant={variant} disabled={disabled || isPending} onClick={onConfirm}>
{isPending ? pendingLabel : confirmLabel}
</Button>
</div>
Expand Down
Loading
Loading