@trieb.work/payload-plugin-backup-mongodb 0.1.5 → 0.2.0

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.
Files changed (97) hide show
  1. package/README.md +260 -90
  2. package/dist/backup-dashboard.css +18 -1
  3. package/dist/collections/BackupTasks.js +8 -1
  4. package/dist/collections/BackupTasks.js.map +1 -1
  5. package/dist/components/BackupDashboard/BackupListCollapsible.client.js.map +1 -1
  6. package/dist/components/BackupDashboard/BackupSettingsModal.client.js +203 -115
  7. package/dist/components/BackupDashboard/BackupSettingsModal.client.js.map +1 -1
  8. package/dist/components/BackupDashboard/CollectionBackupPreviewBody.client.js.map +1 -1
  9. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js +1 -1
  10. package/dist/components/BackupDashboard/RestoreBackupDialog.client.js.map +1 -1
  11. package/dist/components/BackupDashboard/TaskActionButton.client.d.ts +1 -1
  12. package/dist/components/BackupDashboard/TaskActionButton.client.js +59 -26
  13. package/dist/components/BackupDashboard/TaskActionButton.client.js.map +1 -1
  14. package/dist/components/BackupDashboard/backupDashboardInlineCss.js +1 -1
  15. package/dist/components/BackupDashboard/backupDashboardInlineCss.js.map +1 -1
  16. package/dist/components/BackupDashboard/index.js +27 -3
  17. package/dist/components/BackupDashboard/index.js.map +1 -1
  18. package/dist/core/archive.js.map +1 -1
  19. package/dist/core/backup.d.ts +4 -3
  20. package/dist/core/backup.js +50 -43
  21. package/dist/core/backup.js.map +1 -1
  22. package/dist/core/backupArchiveRead.d.ts +15 -0
  23. package/dist/core/backupArchiveRead.js +30 -0
  24. package/dist/core/backupArchiveRead.js.map +1 -0
  25. package/dist/core/backupBlobIO.js.map +1 -1
  26. package/dist/core/backupSettings.js +5 -0
  27. package/dist/core/backupSettings.js.map +1 -1
  28. package/dist/core/blobTokenValidate.js.map +1 -1
  29. package/dist/core/restore.d.ts +5 -2
  30. package/dist/core/restore.js +133 -89
  31. package/dist/core/restore.js.map +1 -1
  32. package/dist/core/restorePreview.d.ts +12 -3
  33. package/dist/core/restorePreview.js +17 -13
  34. package/dist/core/restorePreview.js.map +1 -1
  35. package/dist/core/restoreResult.d.ts +34 -0
  36. package/dist/core/restoreResult.js +45 -0
  37. package/dist/core/restoreResult.js.map +1 -0
  38. package/dist/core/storage/config.d.ts +31 -0
  39. package/dist/core/storage/config.js +60 -0
  40. package/dist/core/storage/config.js.map +1 -0
  41. package/dist/core/storage/index.d.ts +23 -0
  42. package/dist/core/storage/index.js +25 -0
  43. package/dist/core/storage/index.js.map +1 -0
  44. package/dist/core/storage/s3.d.ts +35 -0
  45. package/dist/core/storage/s3.js +200 -0
  46. package/dist/core/storage/s3.js.map +1 -0
  47. package/dist/core/storage/types.d.ts +60 -0
  48. package/dist/core/storage/types.js +6 -0
  49. package/dist/core/storage/types.js.map +1 -0
  50. package/dist/core/storage/vercelBlob.d.ts +25 -0
  51. package/dist/core/storage/vercelBlob.js +53 -0
  52. package/dist/core/storage/vercelBlob.js.map +1 -0
  53. package/dist/core/taskProgress.d.ts +10 -4
  54. package/dist/core/taskProgress.js +11 -2
  55. package/dist/core/taskProgress.js.map +1 -1
  56. package/dist/core/vercelBackupCron.js.map +1 -1
  57. package/dist/endpoints/index.js +0 -5
  58. package/dist/endpoints/index.js.map +1 -1
  59. package/dist/endpoints/paths/admin-backup-download.js +8 -7
  60. package/dist/endpoints/paths/admin-backup-download.js.map +1 -1
  61. package/dist/endpoints/paths/admin-delete.js +11 -5
  62. package/dist/endpoints/paths/admin-delete.js.map +1 -1
  63. package/dist/endpoints/paths/admin-manual.js +2 -1
  64. package/dist/endpoints/paths/admin-manual.js.map +1 -1
  65. package/dist/endpoints/paths/admin-preview.js +9 -2
  66. package/dist/endpoints/paths/admin-preview.js.map +1 -1
  67. package/dist/endpoints/paths/admin-restore.js +7 -4
  68. package/dist/endpoints/paths/admin-restore.js.map +1 -1
  69. package/dist/endpoints/paths/admin-settings.js +35 -2
  70. package/dist/endpoints/paths/admin-settings.js.map +1 -1
  71. package/dist/endpoints/paths/cron-list.js +2 -1
  72. package/dist/endpoints/paths/cron-list.js.map +1 -1
  73. package/dist/endpoints/paths/cron-restore.js +14 -4
  74. package/dist/endpoints/paths/cron-restore.js.map +1 -1
  75. package/dist/endpoints/paths/cron-run.js +2 -1
  76. package/dist/endpoints/paths/cron-run.js.map +1 -1
  77. package/dist/endpoints/shared.d.ts +0 -1
  78. package/dist/endpoints/shared.js +0 -6
  79. package/dist/endpoints/shared.js.map +1 -1
  80. package/dist/index.d.ts +5 -3
  81. package/dist/index.js +3 -2
  82. package/dist/index.js.map +1 -1
  83. package/dist/plugin.js.map +1 -1
  84. package/dist/publicApiPaths.d.ts +0 -1
  85. package/dist/publicApiPaths.js +0 -1
  86. package/dist/publicApiPaths.js.map +1 -1
  87. package/dist/types.d.ts +0 -5
  88. package/dist/types.js.map +1 -1
  89. package/dist/utils/blobName.js.map +1 -1
  90. package/dist/utils/dashboardRoleAccess.js.map +1 -1
  91. package/dist/utils/dialogBackdrop.js.map +1 -1
  92. package/dist/utils/hostname.js.map +1 -1
  93. package/dist/utils/maskBlobToken.js.map +1 -1
  94. package/package.json +25 -4
  95. package/dist/endpoints/paths/admin-seed.d.ts +0 -3
  96. package/dist/endpoints/paths/admin-seed.js +0 -47
  97. package/dist/endpoints/paths/admin-seed.js.map +0 -1
package/README.md CHANGED
@@ -1,59 +1,172 @@
1
- > Experimental. Originated in the [Payblocks](https://www.shadcnblocks.com/payload-cms) template, now maintained as a standalone package. Running in production at 20+ projects from TRWK agency. Feedback and PRs welcome.
1
+ > Experimental. Originated in the
2
+ > [Payblocks](https://www.shadcnblocks.com/payload-cms) template, now maintained
3
+ > as a standalone package. Running in production at 20+ projects from TRWK
4
+ > agency. Feedback and PRs welcome.
2
5
 
3
6
  # @trieb.work/payload-plugin-backup-mongodb
4
7
 
5
8
  [![npm](https://img.shields.io/npm/v/@trieb.work/payload-plugin-backup-mongodb)](https://www.npmjs.com/package/@trieb.work/payload-plugin-backup-mongodb)
6
9
 
7
- A **Payload CMS v3** plugin for **MongoDB only** — not Postgres, SQLite, or other database adapters. It handles Mongo + media backup, restore, and scheduled retention with zero meta-database and a built-in admin UI. Backups live directly in Vercel Blob Storage, so a fresh install can list and restore any prior backup without bootstrapping a database first.
10
+ A **Payload CMS v3** plugin for **MongoDB only** — not Postgres, SQLite, or
11
+ other database adapters. It handles Mongo + media backup, restore, and scheduled
12
+ retention with zero meta-database and a built-in admin UI. Backups live in
13
+ **Vercel Blob Storage** (default) or **AWS S3 / S3-compatible stores** (R2,
14
+ MinIO, etc.), so a fresh install can list and restore any prior backup without
15
+ bootstrapping a database first.
8
16
 
9
17
  ![Backup dashboard embedded in the Payload admin panel](./docs/screenshots/dashboard.png)
10
18
 
11
- A first-class **Backups** section lives right below the Payload dashboard: browse every archive
12
- sorted by creation time, see at a glance which host and database it belongs to, whether media is
13
- bundled and how big it is, then download, restore or delete any backup with one click. Read more on how it originated at [TRWK> Case Study](https://trwk.de/case-studies/payload-plugin-mongodb-backup-restore).
19
+ A first-class **Backups** section lives right below the Payload dashboard:
20
+ browse every archive sorted by creation time, see at a glance which host and
21
+ database it belongs to, whether media is bundled and how big it is, then
22
+ download, restore or delete any backup with one click. Read more on how it
23
+ originated at
24
+ [TRWK> Case Study](https://trwk.de/case-studies/payload-plugin-mongodb-backup-restore).
14
25
 
15
26
  ---
16
27
 
17
28
  ## Highlights
18
29
 
19
- - **No meta database.** Every backup is self-describing in its blob name (`type---db---host---{collectionCount}-{timestampMs}.{ext}`). A brand new project can list and restore any backup straight from blob storage.
20
- - **Scheduled (cron) and on-demand backups.** Wire `/api/backup-mongodb/cron/run` to Vercel Cron or any HTTP scheduler. Retention is configurable (N most recent cron backups are kept; older ones are pruned automatically).
21
- - **Full database coverage.** Dumps **every** MongoDB collection via the Payload mongoose adapter — including hidden system collections like `users`, `payload-preferences`, `payload-migrations`. Individual collections can be excluded per backup from the UI or via API.
22
- - **Optional media bundling.** Cron or manual backups can include Payload `media` blobs in a `.tar.gz` archive alongside the MongoDB dump.
23
- - **Restore with filters.** Restore any backup with an optional collection blacklist and `mergeData` upsert mode. Partial restores keep the running admin session / tasks collection intact.
24
- - **Native admin dashboard.** Adds a `BackupDashboard` widget to the Payload admin (`afterDashboard`): list / sort / filter / search backups, trigger manual backups or restores, configure retention and storage, and live-poll long-running tasks.
25
- - **Payload REST, not Next.js route files.** All endpoints are registered as Payload custom endpoints and served by the default `/api/[...slug]` handler — you do not add `app/.../route.ts` files for this plugin.
26
- - **Pluggable blob storage.** Uses `BLOB_READ_WRITE_TOKEN` by default (the same store you often use with `@payloadcms/storage-vercel-blob`), or point backups at a dedicated Vercel Blob store in the admin. Both **public** and **private** access stores are supported; a validation step detects which modes the store accepts.
27
- - **Resumable long-running tasks.** Manual backups, restores, and seed runs are tracked in a hidden `backup-tasks` collection (TTL, 30 min). The UI polls progress with a short-lived `pollSecret` so long jobs stay observable even across reloads.
28
- - **Demo/seed support.** Optional `seedDemoDumpUrl` registers a one-click seed endpoint for templates and starters.
29
- - **Tested.** Vitest covers archive, backup, restore, task progress, blob I/O, endpoint auth, cron parsing, and blob-name helpers.
30
+ - **No meta database.** Every backup is self-describing in its blob name
31
+ (`type---db---host---{collectionCount}-{timestampMs}.{ext}`). A brand new
32
+ project can list and restore any backup straight from blob storage.
33
+ - **Scheduled (cron) and on-demand backups.** Wire
34
+ `/api/backup-mongodb/cron/run` to Vercel Cron or any HTTP scheduler. Retention
35
+ is configurable (N most recent cron backups are kept; older ones are pruned
36
+ automatically).
37
+ - **Full database coverage.** Dumps **every** MongoDB collection via the Payload
38
+ mongoose adapter including hidden system collections like `users`,
39
+ `payload-preferences`, `payload-migrations`. Individual collections can be
40
+ excluded per backup from the UI or via API.
41
+ - **Optional media bundling.** Cron or manual backups can include Payload
42
+ `media` blobs in a `.tar.gz` archive alongside the MongoDB dump.
43
+ - **Restore with filters.** Restore any backup with an optional collection
44
+ blacklist and `mergeData` upsert mode. Partial restores keep the running admin
45
+ session / tasks collection intact.
46
+ - **Native admin dashboard.** Adds a `BackupDashboard` widget to the Payload
47
+ admin (`afterDashboard`): list / sort / filter / search backups, trigger
48
+ manual backups or restores, configure retention and storage, and live-poll
49
+ long-running tasks.
50
+ - **Payload REST, not Next.js route files.** All endpoints are registered as
51
+ Payload custom endpoints and served by the default `/api/[...slug]` handler —
52
+ you do not add `app/.../route.ts` files for this plugin.
53
+ - **Pluggable blob storage.** Uses `BLOB_READ_WRITE_TOKEN` by default (the same
54
+ store you often use with `@payloadcms/storage-vercel-blob`), or point backups
55
+ at a dedicated Vercel Blob store in the admin. Alternatively set
56
+ `BACKUP_STORAGE=s3` and use AWS S3, Cloudflare R2, MinIO, etc. Both **public**
57
+ and **private** access stores are supported; a validation step detects which
58
+ modes the store accepts.
59
+ - **Resumable long-running tasks.** Manual backups and restores are tracked in a
60
+ hidden `backup-tasks` collection (TTL, 30 min). The UI polls progress with a
61
+ short-lived `pollSecret` so long jobs stay observable even across reloads.
62
+ - **Tested.** Vitest covers archive, backup, restore, task progress, blob I/O,
63
+ endpoint auth, cron parsing, and blob-name helpers.
30
64
 
31
65
  ---
32
66
 
33
67
  ## Requirements
34
68
 
69
+ - **Node.js 20.9+** (Node 18 is no longer supported).
35
70
  - Payload **v3+** (same major as your other `@payloadcms/*` packages).
36
71
  - MongoDB with Payload’s mongoose adapter (`@payloadcms/db-mongodb`).
37
72
  - MongoDB server (any version supported by that adapter).
38
- - A Vercel Blob read/write token (`BLOB_READ_WRITE_TOKEN`). Vercel hosting is **not** required — any Node runtime that can reach Vercel Blob works.
73
+ - A backup target:
74
+ - **Vercel Blob:** `BLOB_READ_WRITE_TOKEN` (the default target). Vercel
75
+ hosting is **not** required — any Node runtime that can reach Vercel Blob
76
+ works.
77
+ - **S3:** `BACKUP_STORAGE=s3` plus `BACKUP_S3_BUCKET` (and optionally
78
+ credentials). Requires Node 20+ for the optional AWS SDK peer packages. See
79
+ [Choosing a backup target](#choosing-a-backup-target-vercel-blob-or-s3).
39
80
  - Next.js **15+** and React **19+** (the usual Payload 3 + App Router stack).
40
81
 
41
82
  ### Environment variables
42
83
 
43
- | Variable | Required | Purpose |
44
- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | `MONGODB_URI` | yes | MongoDB connection string (used by Payload and to label backups with the DB name). |
46
- | `BLOB_READ_WRITE_TOKEN` | yes | Default Vercel Blob store for backups and media. Can be overridden in admin settings. |
47
- | `CRON_SECRET` | for cron | Bearer token for every `/api/backup-mongodb/cron/*` call. Vercel Cron can supply this. |
48
- | `NEXT_PUBLIC_SERVER_URL` | optional | Used to label backups with the current host. Falls back to `VERCEL_URL` when set. |
49
- | `BACKUPS_TO_KEEP` | optional | Default retention for cron backups if the settings document has not been edited. Default `10`. |
50
- | `PAYLOAD_BACKUP_ALLOWED_ROLES` | optional | Comma-separated role slugs that may see the **Backups** dashboard (case-insensitive). Use `*` to allow any authenticated user. When unset, the plugin falls back to requiring a `role` with slug `admin`, or allows everyone when the users collection has no `roles` field. Overridden by the `access` plugin option. |
84
+ | Variable | Required | Purpose |
85
+ | ------------------------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `MONGODB_URI` | yes | MongoDB connection string (used by Payload and to label backups with the DB name). |
87
+ | `BACKUP_STORAGE` | optional | Backup target: `vercel-blob` (default) or `s3`. See [Choosing a backup target](#choosing-a-backup-target-vercel-blob-or-s3). |
88
+ | `BLOB_READ_WRITE_TOKEN` | for Vercel Blob | Default Vercel Blob store for backups and media. Required when `BACKUP_STORAGE=vercel-blob` (the default). Can be overridden in admin settings. |
89
+ | `CRON_SECRET` | for cron | Bearer token for every `/api/backup-mongodb/cron/*` call. Vercel Cron can supply this. |
90
+ | `NEXT_PUBLIC_SERVER_URL` | optional | Used to label backups with the current host. Falls back to `VERCEL_URL` when set. |
91
+ | `BACKUPS_TO_KEEP` | optional | Default retention for cron backups if the settings document has not been edited. Default `10`. |
92
+ | `PAYLOAD_BACKUP_ALLOWED_ROLES` | optional | Comma-separated role slugs that may see the **Backups** dashboard (case-insensitive). Use `*` to allow any authenticated user. When unset, the plugin falls back to requiring a `role` with slug `admin`, or allows everyone when the users collection has no `roles` field. Overridden by the `access` plugin option. |
93
+
94
+ ---
95
+
96
+ ## Choosing a backup target (Vercel Blob or S3)
97
+
98
+ Backups can be stored in **Vercel Blob** (default) or **AWS S3**. The S3 target
99
+ also works with any S3-compatible store — **Cloudflare R2**, **MinIO**, etc. —
100
+ because they share the same API; only the configuration differs, never the
101
+ plugin code.
102
+
103
+ Select the target with `BACKUP_STORAGE`. When unset it stays `vercel-blob`, so
104
+ existing deployments are unaffected.
105
+
106
+ ### Using S3
107
+
108
+ S3 is enabled when `BACKUP_STORAGE=s3`. Install the AWS SDK (declared as
109
+ optional peer dependencies, so Vercel-only installs don't pull it in):
110
+
111
+ ```bash
112
+ pnpm add @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
113
+ ```
114
+
115
+ | Variable | Required | Purpose |
116
+ | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
117
+ | `BACKUP_STORAGE` | yes | Set to `s3`. |
118
+ | `BACKUP_S3_BUCKET` | yes | Target bucket name. |
119
+ | `BACKUP_S3_REGION` | optional | Region. Falls back to `AWS_REGION`, then `us-east-1`. |
120
+ | `BACKUP_S3_ENDPOINT` | optional | Custom endpoint for S3-compatible stores (R2, MinIO). Omit for AWS S3. |
121
+ | `BACKUP_S3_FORCE_PATH_STYLE` | optional | Path-style addressing. Defaults to `true` when an endpoint is set (needed for MinIO and some R2 setups). |
122
+ | `BACKUP_S3_PREFIX` | optional | Key namespace prepended to every object (for sharing a bucket). Backups still list/restore by their `backups/…` name. |
123
+ | `BACKUP_S3_ACCESS_KEY_ID` | optional | Access key. Falls back to `AWS_ACCESS_KEY_ID`. Omit to use the AWS default credential chain (IAM roles, etc.). |
124
+ | `BACKUP_S3_SECRET_ACCESS_KEY` | optional | Secret key. Falls back to `AWS_SECRET_ACCESS_KEY`. |
125
+ | `BACKUP_S3_SESSION_TOKEN` | optional | Session token for temporary credentials. Falls back to `AWS_SESSION_TOKEN`. |
126
+
127
+ Credentials are optional: when omitted, the AWS SDK's default provider chain is
128
+ used (environment, shared config, or instance/role credentials — recommended on
129
+ AWS).
130
+
131
+ **AWS S3**
132
+
133
+ ```bash
134
+ BACKUP_STORAGE=s3
135
+ BACKUP_S3_BUCKET=my-payload-backups
136
+ BACKUP_S3_REGION=eu-central-1
137
+ # Credentials via IAM role (recommended) or:
138
+ # BACKUP_S3_ACCESS_KEY_ID=...
139
+ # BACKUP_S3_SECRET_ACCESS_KEY=...
140
+ ```
141
+
142
+ **Cloudflare R2**
143
+
144
+ ```bash
145
+ BACKUP_STORAGE=s3
146
+ BACKUP_S3_BUCKET=my-payload-backups
147
+ BACKUP_S3_REGION=auto
148
+ BACKUP_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
149
+ BACKUP_S3_FORCE_PATH_STYLE=true
150
+ BACKUP_S3_ACCESS_KEY_ID=<r2-access-key-id>
151
+ BACKUP_S3_SECRET_ACCESS_KEY=<r2-secret-access-key>
152
+ ```
153
+
154
+ Backups are stored under the same `backups/…` keys (see
155
+ [Blob naming](#blob-naming)), so listing and restore work identically across
156
+ targets. Downloads stream through the plugin's authenticated endpoint, so the
157
+ bucket does **not** need to be public.
158
+
159
+ > **Note on media:** the "include media" option bundles files from your Payload
160
+ > **Vercel Blob upload store** (`BLOB_READ_WRITE_TOKEN`). The configurable
161
+ > target above governs where backup **archives** live only; media is always read
162
+ > from and restored to the upload store, even when `BACKUP_STORAGE=s3`.
51
163
 
52
164
  ---
53
165
 
54
166
  ## Installation and getting started
55
167
 
56
- Use this flow when **adding the plugin to an existing Payload 3 + Next.js app** that already has MongoDB and (typically) Vercel Blob configured.
168
+ Use this flow when **adding the plugin to an existing Payload 3 + Next.js app**
169
+ that already has MongoDB and a blob/S3 store configured.
57
170
 
58
171
  ### 1. Add the package
59
172
 
@@ -63,7 +176,9 @@ pnpm add @trieb.work/payload-plugin-backup-mongodb
63
176
  # yarn add @trieb.work/payload-plugin-backup-mongodb
64
177
  ```
65
178
 
66
- Peer dependencies: **`@payloadcms/db-mongodb`**, **`payload`**, **`@payloadcms/ui`**, **`next`**, and **`react`**. (all expected in a Payload 3 + MongoDB + Next.js app).
179
+ Peer dependencies: **`@payloadcms/db-mongodb`**, **`payload`**,
180
+ **`@payloadcms/ui`**, **`next`**, and **`react`**. (all expected in a Payload
181
+ 3 + MongoDB + Next.js app).
67
182
 
68
183
  ### 2. Register the plugin
69
184
 
@@ -82,7 +197,10 @@ export default buildConfig({
82
197
  })
83
198
  ```
84
199
 
85
- The plugin will register the backup collections, mount the dashboard, and add `/api/backup-mongodb/*` endpoints. Import paths resolve to a **published or linked** `dist/` build; after local changes to the package, run `pnpm build` in the plugin repo (or reinstall) so `dist/` is up to date.
200
+ The plugin will register the backup collections, mount the dashboard, and add
201
+ `/api/backup-mongodb/*` endpoints. Import paths resolve to a **published or
202
+ linked** `dist/` build; after local changes to the package, run `pnpm build` in
203
+ the plugin repo (or reinstall) so `dist/` is up to date.
86
204
 
87
205
  ### 3. Regenerate types and the import map
88
206
 
@@ -95,7 +213,8 @@ pnpm payload generate:importmap
95
213
 
96
214
  ### 4. (Optional) Schedule cron backups
97
215
 
98
- **Vercel:** add a cron in `vercel.json`. Path must match your API; default is under `/api/…`:
216
+ **Vercel:** add a cron in `vercel.json`. Path must match your API; default is
217
+ under `/api/…`:
99
218
 
100
219
  ```json
101
220
  {
@@ -108,7 +227,9 @@ pnpm payload generate:importmap
108
227
  }
109
228
  ```
110
229
 
111
- Vercel Cron sends header `Authorization: Bearer $CRON_SECRET`. **Other hosts** (k8s, GitHub Actions, etc.): `GET` the same URL on your schedule with that same authorization header.
230
+ Vercel Cron sends header `Authorization: Bearer $CRON_SECRET`. **Other hosts**
231
+ (k8s, GitHub Actions, etc.): `GET` the same URL on your schedule with that same
232
+ authorization header.
112
233
 
113
234
  ### Checklist
114
235
 
@@ -116,33 +237,49 @@ Vercel Cron sends header `Authorization: Bearer $CRON_SECRET`. **Other hosts** (
116
237
  - [ ] `backupMongodbPlugin` in `plugins` array
117
238
  - [ ] `generate:types` and `generate:importmap` run
118
239
  - [ ] `MONGODB_URI` and `BLOB_READ_WRITE_TOKEN` set in the environment
119
- - [ ] (Optional) `CRON_SECRET` and cron job (vercel.json) if you want scheduled backups
240
+ - [ ] (Optional) `CRON_SECRET` and cron job (vercel.json) if you want scheduled
241
+ backups
120
242
 
121
- Start your dev server, open `/admin`, sign in as an admin, and you should see **Backups** below the dashboard.
243
+ Start your dev server, open `/admin`, sign in as an admin, and you should see
244
+ **Backups** below the dashboard.
122
245
 
123
246
  ---
124
247
 
125
248
  ## Admin UI
126
249
 
127
- After the first login as an admin user, a **Backups** section appears below the default Payload
128
- dashboard. Day-to-day tasks — create, download, restore, delete, schedule — all happen in the UI below.
250
+ After the first login as an admin user, a **Backups** section appears below the
251
+ default Payload dashboard. Day-to-day tasks — create, download, restore, delete,
252
+ schedule — all happen in the UI below.
129
253
 
130
254
  **Who can see it?** The visibility rules, in precedence order:
131
255
 
132
- 1. If the plugin is registered with an `access` function, it wins (see [Plugin options](#plugin-options)).
133
- 2. Otherwise, if `PAYLOAD_BACKUP_ALLOWED_ROLES` is set, it is used as a comma-separated allow-list of role slugs (case-insensitive). Use `*` for "any authenticated user". Example: `PAYLOAD_BACKUP_ALLOWED_ROLES=admin,superadmin`.
134
- 3. Otherwise, the historical default applies: users with a `role` whose slug is `admin` see the dashboard; projects that don't use a `roles` field at all get the dashboard for any authenticated user.
256
+ 1. If the plugin is registered with an `access` function, it wins (see
257
+ [Plugin options](#plugin-options)).
258
+ 2. Otherwise, if `PAYLOAD_BACKUP_ALLOWED_ROLES` is set, it is used as a
259
+ comma-separated allow-list of role slugs (case-insensitive). Use `*` for "any
260
+ authenticated user". Example:
261
+ `PAYLOAD_BACKUP_ALLOWED_ROLES=admin,superadmin`.
262
+ 3. Otherwise, the historical default applies: users with a `role` whose slug is
263
+ `admin` see the dashboard; projects that don't use a `roles` field at all get
264
+ the dashboard for any authenticated user.
135
265
 
136
266
  ### Scheduled (cron) vs on-demand (manual) backups
137
267
 
138
- - **Scheduled / cron** backups follow shared settings: retention, media toggle, and collections to skip. Older cron archives are pruned when the keep-count is exceeded. They appear in the list as **CRON BACKUP** (or similar).
139
- - **Manual / on-demand** backups are configured **per run** — per-collection toggles, optional media in a `.tar.gz` archive. Manual backups are not auto-pruned; use them as checkpoints before migrations.
268
+ - **Scheduled / cron** backups follow shared settings: retention, media toggle,
269
+ and collections to skip. Older cron archives are pruned when the keep-count is
270
+ exceeded. They appear in the list as **CRON BACKUP** (or similar).
271
+ - **Manual / on-demand** backups are configured **per run** — per-collection
272
+ toggles, optional media in a `.tar.gz` archive. Manual backups are not
273
+ auto-pruned; use them as checkpoints before migrations.
140
274
 
141
275
  ![Manual backup dialog with per-collection selection and optional media bundling](./docs/screenshots/manual-backup.png)
142
276
 
143
277
  ### Selective restore with per-collection preview
144
278
 
145
- **Restore** opens a preview of the archive before any write. You can restore everything, cherry-pick collections, or skip media from `.tar.gz` when you only need database rows. Restoring from a different host or database is supported; filters help when cloning production into staging.
279
+ **Restore** opens a preview of the archive before any write. You can restore
280
+ everything, cherry-pick collections, or skip media from `.tar.gz` when you only
281
+ need database rows. Restoring from a different host or database is supported;
282
+ filters help when cloning production into staging.
146
283
 
147
284
  ![Restore dialog previewing document counts per collection](./docs/screenshots/restore.png)
148
285
 
@@ -152,10 +289,18 @@ Retention, storage token, and cron collection skip list live in one modal:
152
289
 
153
290
  ![Scheduled backup settings: cron schedule, retention, blob token, collection skip list](./docs/screenshots/settings.png)
154
291
 
155
- - **Schedule:** when `vercel.json` is present, the plugin can show a human-readable cron description (via [cronstrue](https://github.com/bradymholt/cronstrue)).
156
- - **Retention:** how many **cron** archives to keep; manual backups are not pruned by this.
157
- - **Dedicated backup storage (optional):** a separate Vercel Blob read/write token so backups can live in a different store than media. The UI can validate the token and optionally copy existing `backups/*` objects to the new store.
158
- - **Collection selection for cron:** defaults to all collections, with per-collection opt-out.
292
+ - **Schedule:** when `vercel.json` is present, the plugin can show a
293
+ human-readable cron description (via
294
+ [cronstrue](https://github.com/bradymholt/cronstrue)).
295
+ - **Retention:** how many **cron** archives to keep; manual backups are not
296
+ pruned by this.
297
+ - **Dedicated backup storage (optional):** a separate Vercel Blob read/write
298
+ token (when `BACKUP_STORAGE=vercel-blob`) so backups can live in a different
299
+ store than media. The UI can validate the token and optionally copy existing
300
+ `backups/*` objects to the new store. For S3, configure the target bucket via
301
+ environment variables.
302
+ - **Collection selection for cron:** defaults to all collections, with
303
+ per-collection opt-out.
159
304
 
160
305
  ---
161
306
 
@@ -172,12 +317,6 @@ type BackupPluginOptions = {
172
317
  /** Default cron retention when no value is stored in settings. Falls back to `BACKUPS_TO_KEEP` or `10`. */
173
318
  backupsToKeep?: number
174
319
 
175
- /**
176
- * If set, registers `POST /api/backup-mongodb/admin/seed` (demo dump + public seed media where applicable).
177
- * Omit in production unless you need it.
178
- */
179
- seedDemoDumpUrl?: string
180
-
181
320
  /**
182
321
  * Custom access check for admin routes and the dashboard. Overrides the
183
322
  * `PAYLOAD_BACKUP_ALLOWED_ROLES` env var when provided.
@@ -191,24 +330,14 @@ type BackupPluginOptions = {
191
330
  }
192
331
  ```
193
332
 
194
- Example with custom access and seed URL (typical for starters):
195
-
196
- ```ts
197
- backupMongodbPlugin({
198
- access: (user) =>
199
- Array.isArray((user as { roles?: { slug?: string }[] })?.roles) &&
200
- (user as { roles: { slug?: string }[] }).roles.some(
201
- (r) => r?.slug === 'admin' || r?.slug === 'superadmin',
202
- ),
203
- seedDemoDumpUrl: 'https://example.com/seed/demo-db.json',
204
- })
205
- ```
206
-
207
333
  ---
208
334
 
209
335
  ## HTTP API (Payload REST)
210
336
 
211
- All routes are served by Payload’s `/api/[...slug]` handler under **`/api/backup-mongodb/…`**. The `backup-mongodb` prefix avoids clashing with a collection slug. The admin UI uses the same URLs; use the exported `backupPluginPublicApiPaths` helper in client code.
337
+ All routes are served by Payload’s `/api/[...slug]` handler under
338
+ **`/api/backup-mongodb/…`**. The `backup-mongodb` prefix avoids clashing with a
339
+ collection slug. The admin UI uses the same URLs; use the exported
340
+ `backupPluginPublicApiPaths` helper in client code.
212
341
 
213
342
  ### Cron / external (Bearer `CRON_SECRET`)
214
343
 
@@ -221,18 +350,17 @@ Authorization: Bearer <CRON_SECRET>
221
350
 
222
351
  ### Admin (session cookie, or `pollSecret` for `/task/:id` where applicable)
223
352
 
224
- | Method | Path |
225
- | -------------- | -------------------------------------------------------------- |
226
- | `POST` | `/api/backup-mongodb/admin/manual` |
227
- | `POST` | `/api/backup-mongodb/admin/restore` |
228
- | `POST` | `/api/backup-mongodb/admin/backup-preview` |
229
- | `POST` | `/api/backup-mongodb/admin/restore-preview` |
230
- | `POST` | `/api/backup-mongodb/admin/delete` |
231
- | `GET` | `/api/backup-mongodb/admin/backup-download` |
232
- | `GET` | `/api/backup-mongodb/admin/task/:id` |
233
- | `GET` / `POST` | `/api/backup-mongodb/admin/settings` |
234
- | `POST` | `/api/backup-mongodb/admin/validate-blob-token` |
235
- | `POST` | `/api/backup-mongodb/admin/seed` — if `seedDemoDumpUrl` is set |
353
+ | Method | Path |
354
+ | -------------- | ----------------------------------------------- |
355
+ | `POST` | `/api/backup-mongodb/admin/manual` |
356
+ | `POST` | `/api/backup-mongodb/admin/restore` |
357
+ | `POST` | `/api/backup-mongodb/admin/backup-preview` |
358
+ | `POST` | `/api/backup-mongodb/admin/restore-preview` |
359
+ | `POST` | `/api/backup-mongodb/admin/delete` |
360
+ | `GET` | `/api/backup-mongodb/admin/backup-download` |
361
+ | `GET` | `/api/backup-mongodb/admin/task/:id` |
362
+ | `GET` / `POST` | `/api/backup-mongodb/admin/settings` |
363
+ | `POST` | `/api/backup-mongodb/admin/validate-blob-token` |
236
364
 
237
365
  ---
238
366
 
@@ -241,7 +369,11 @@ Authorization: Bearer <CRON_SECRET>
241
369
  For scripts, hooks, and tests:
242
370
 
243
371
  ```ts
244
- import { createBackup, listBackups, restoreBackup } from '@trieb.work/payload-plugin-backup-mongodb'
372
+ import {
373
+ createBackup,
374
+ listBackups,
375
+ restoreBackup,
376
+ } from '@trieb.work/payload-plugin-backup-mongodb'
245
377
 
246
378
  // Example: manual backup from a one-off script
247
379
  await createBackup(payload, { cron: false, includeMedia: true })
@@ -266,16 +398,31 @@ backups/{type}---{dbName}---{hostname}---{collectionCount}-{timestampMs}.{json|t
266
398
  ```
267
399
 
268
400
  - `type`: `cron` or `manual` (and legacy-style labels where applicable).
269
- - `dbName` / `hostname`: derived from your MongoDB URL and public server URL; URL-encoded in the name.
270
- - `collectionCount` + `timestampMs`: sort key for listing; legacy blob names are still supported.
401
+ - `dbName` / `hostname`: derived from your MongoDB URL and public server URL;
402
+ URL-encoded in the name.
403
+ - `collectionCount` + `timestampMs`: sort key for listing; legacy blob names are
404
+ still supported.
271
405
 
272
406
  ### Public vs private stores
273
407
 
274
- A validation step probes the bucket and records whether the store behaves as public or private. New uploads use the right access mode. Restore and preview flows handle both.
408
+ A validation step probes the bucket and records whether the store behaves as
409
+ public or private. New uploads use the right access mode. Restore and preview
410
+ flows handle both.
275
411
 
276
412
  ### Overriding the backup store
277
413
 
278
- To keep backups in a **different** Vercel Blob project than media, open **Backup settings** in the admin, paste a dedicated `BLOB_READ_WRITE_TOKEN`, validate, and optionally migrate existing `backups/*` objects to the new store before switching.
414
+ To keep backups in a **different** Vercel Blob project than media (when
415
+ `BACKUP_STORAGE=vercel-blob`), open **Backup settings** in the admin, paste a
416
+ dedicated `BLOB_READ_WRITE_TOKEN`, validate, and optionally migrate existing
417
+ `backups/*` objects to the new store before switching. For S3, the target is
418
+ controlled entirely by environment variables.
419
+
420
+ ### Removed demo seed API (intentional)
421
+
422
+ Earlier releases exposed an optional demo seed route (`seedDemoDumpUrl`,
423
+ `/backup-mongodb/admin/seed`, and the `restoreSeedMedia` export). That was
424
+ removed on purpose: a backup plugin should not ship a public “seed my database”
425
+ API. Use your own migration/seed tooling instead.
279
426
 
280
427
  ---
281
428
 
@@ -287,29 +434,51 @@ pnpm test:int
287
434
 
288
435
  ### E2E (Playwright)
289
436
 
290
- End-to-end tests live in `tests/e2e/` and target the `dev/` Next + Payload app. **Locally**, start Mongo (or rely on the in-memory replica set when `DATABASE_URL` / `MONGODB_URI` are unset), then either run `pnpm dev` and `pnpm test:e2e` in another shell (Playwright reuses the server when not in `CI`), or run only `pnpm test:e2e` so Playwright starts `pnpm dev` for you.
437
+ End-to-end tests live in `tests/e2e/` and target the `dev/` Next + Payload app.
438
+ **Locally**, start Mongo (or rely on the in-memory replica set when
439
+ `DATABASE_URL` / `MONGODB_URI` are unset), then either run `pnpm dev` and
440
+ `pnpm test:e2e` in another shell (Playwright reuses the server when not in
441
+ `CI`), or run only `pnpm test:e2e` so Playwright starts `pnpm dev` for you.
291
442
 
292
- **CI** (`.github/workflows/e2e.yml`, same pattern as the reference `e2e.yml` in executive-search: MongoDB 7 service, `pnpm run build` then `pnpm run build:dev`, then Playwright against `next start`). Optional secret **`BLOB_READ_WRITE_TOKEN`** for fuller blob behaviour.
443
+ **CI** (`.github/workflows/e2e.yml`, same pattern as the reference `e2e.yml` in
444
+ executive-search: MongoDB 7 service, `pnpm run build` then `pnpm run build:dev`,
445
+ then Playwright against `next start`). Optional secret
446
+ **`BLOB_READ_WRITE_TOKEN`** for fuller blob behaviour.
293
447
 
294
448
  ### Optional: deploy `dev/` to Vercel
295
449
 
296
- `.github/workflows/deploy-dev-vercel.yml` runs when `dev/` (or related paths) change on `main`, or via **workflow_dispatch**, **only if** `VERCEL_TOKEN`, `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` are set. Configure the Vercel project **Root Directory** to `dev` (`dev/vercel.json` wires install/build from the repo root).
450
+ `.github/workflows/deploy-dev-vercel.yml` runs when `dev/` (or related paths)
451
+ change on `main`, or via **workflow_dispatch**, **only if** `VERCEL_TOKEN`,
452
+ `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` are set. Configure the Vercel project
453
+ **Root Directory** to `dev` (`dev/vercel.json` wires install/build from the repo
454
+ root).
297
455
 
298
- The repo includes a `dev/` Payload + Next app (MongoDB Memory Server when no URI is set): `pnpm dev` → `http://localhost:3000/admin` (login in `dev/helpers/credentials.ts`). In unit tests, external services like `@vercel/blob` and `bson` are often mocked with `vi.mock()`.
456
+ The repo includes a `dev/` Payload + Next app (MongoDB Memory Server when no URI
457
+ is set): `pnpm dev` → `http://localhost:3000/admin` (login in
458
+ `dev/helpers/credentials.ts`). In unit tests, external services like
459
+ `@vercel/blob` and `bson` are often mocked with `vi.mock()`.
299
460
 
300
461
  ---
301
462
 
302
463
  ## Publishing (npm)
303
464
 
304
- Versioning uses [Changesets](https://github.com/changesets/changesets): add a file with `pnpm changeset`, open a PR to `main`, and merge. The **Release** workflow opens a “version packages” PR or runs `pnpm run release` (`build` + `changeset publish`) when the set of changesets is ready. PRs need a new `.changeset/*.md` unless you add the **`no-changeset`** label (e.g. docs-only).
465
+ Versioning uses [Changesets](https://github.com/changesets/changesets): add a
466
+ file with `pnpm changeset`, open a PR to `main`, and merge. The **Release**
467
+ workflow opens a “version packages” PR or runs `pnpm run release` (`build` +
468
+ `changeset publish`) when the set of changesets is ready. PRs need a new
469
+ `.changeset/*.md` unless you add the **`no-changeset`** label (e.g. docs-only).
305
470
 
306
471
  ---
307
472
 
308
473
  ## Roadmap / ideas
309
474
 
310
- - Multipart / very large backup uploads (Vercel Blob supports large objects; may need to switch past single-part limits).
311
- - Additional storage adapters (S3, R2, filesystem, etc.).
312
- - Scheduler-agnostic display when not using Vercel (`vercel.json` is currently used for the schedule summary where available).
475
+ - Multipart / very large backup uploads (Vercel Blob supports large objects; may
476
+ need to switch past single-part limits). _(S3 uploads already use multipart.)_
477
+ - Additional storage adapters (filesystem, etc.). _(AWS S3 and S3-compatible
478
+ stores such as R2/MinIO are supported — see
479
+ [Choosing a backup target](#choosing-a-backup-target-vercel-blob-or-s3).)_
480
+ - Scheduler-agnostic display when not using Vercel (`vercel.json` is currently
481
+ used for the schedule summary where available).
313
482
  - Streaming restore for very large databases.
314
483
  - Configurable `backups/` prefix or bucket layout.
315
484
  - add support for tenant plugin and partial backups per tenant
@@ -322,4 +491,5 @@ MIT
322
491
 
323
492
  ---
324
493
 
325
- Built and maintained by [TRWK>](https://trwk.de), formerly [trieb.work](https://trieb.work).
494
+ Built and maintained by [TRWK>](https://trwk.de), formerly
495
+ [trieb.work](https://trieb.work).
@@ -595,14 +595,31 @@
595
595
  .backup-task-status-slot {
596
596
  display: flex;
597
597
  flex-direction: row;
598
- align-items: center;
598
+ align-items: flex-start;
599
599
  gap: 6px;
600
600
  }
601
+ .backup-task-status-slot .backup-task-status__details {
602
+ display: flex;
603
+ flex-direction: column;
604
+ gap: 3px;
605
+ min-width: 0;
606
+ }
601
607
  .backup-task-status-slot .backup-task-status__message {
602
608
  font-size: 12px;
603
609
  line-height: 1.4;
604
610
  color: var(--theme-elevation-700);
605
611
  }
612
+ .backup-task-status-slot .backup-task-status__warnings {
613
+ font-size: 12px;
614
+ line-height: 1.4;
615
+ color: var(--theme-warning-600, #b7791f);
616
+ white-space: pre-wrap;
617
+ }
618
+ .backup-task-status-slot .backup-task-status__error {
619
+ font-size: 12px;
620
+ line-height: 1.4;
621
+ color: var(--theme-error-500, #c0392b);
622
+ }
606
623
 
607
624
  .restore-preview__blob-token-row {
608
625
  display: flex;
@@ -16,7 +16,6 @@ export const BackupTasksCollection = {
16
16
  options: [
17
17
  'backup',
18
18
  'restore',
19
- 'seed',
20
19
  'delete',
21
20
  'blobTransfer'
22
21
  ],
@@ -29,6 +28,7 @@ export const BackupTasksCollection = {
29
28
  'queued',
30
29
  'running',
31
30
  'completed',
31
+ 'completed_with_warnings',
32
32
  'failed'
33
33
  ],
34
34
  required: true
@@ -42,6 +42,13 @@ export const BackupTasksCollection = {
42
42
  name: 'error',
43
43
  type: 'text'
44
44
  },
45
+ {
46
+ name: 'warnings',
47
+ type: 'textarea',
48
+ admin: {
49
+ hidden: true
50
+ }
51
+ },
45
52
  {
46
53
  name: 'pollSecret',
47
54
  type: 'text',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/BackupTasks.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport const BackupTasksCollection: CollectionConfig = {\n slug: 'backup-tasks',\n access: {\n create: () => false,\n delete: () => false,\n read: () => false,\n update: () => false,\n },\n admin: {\n hidden: true,\n },\n fields: [\n {\n name: 'kind',\n type: 'select',\n options: ['backup', 'restore', 'seed', 'delete', 'blobTransfer'],\n required: true,\n },\n {\n name: 'status',\n type: 'select',\n options: ['queued', 'running', 'completed', 'failed'],\n required: true,\n },\n {\n name: 'message',\n type: 'text',\n required: true,\n },\n {\n name: 'error',\n type: 'text',\n },\n {\n name: 'pollSecret',\n type: 'text',\n admin: {\n hidden: true,\n },\n },\n ],\n timestamps: true,\n}\n"],"names":["BackupTasksCollection","slug","access","create","delete","read","update","admin","hidden","fields","name","type","options","required","timestamps"],"mappings":"AAEA,OAAO,MAAMA,wBAA0C;IACrDC,MAAM;IACNC,QAAQ;QACNC,QAAQ,IAAM;QACdC,QAAQ,IAAM;QACdC,MAAM,IAAM;QACZC,QAAQ,IAAM;IAChB;IACAC,OAAO;QACLC,QAAQ;IACV;IACAC,QAAQ;QACN;YACEC,MAAM;YACNC,MAAM;YACNC,SAAS;gBAAC;gBAAU;gBAAW;gBAAQ;gBAAU;aAAe;YAChEC,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;YACNC,SAAS;gBAAC;gBAAU;gBAAW;gBAAa;aAAS;YACrDC,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;YACNE,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;QACR;QACA;YACED,MAAM;YACNC,MAAM;YACNJ,OAAO;gBACLC,QAAQ;YACV;QACF;KACD;IACDM,YAAY;AACd,EAAC"}
1
+ {"version":3,"sources":["../../src/collections/BackupTasks.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport const BackupTasksCollection: CollectionConfig = {\n slug: 'backup-tasks',\n access: {\n create: () => false,\n delete: () => false,\n read: () => false,\n update: () => false,\n },\n admin: {\n hidden: true,\n },\n fields: [\n {\n name: 'kind',\n type: 'select',\n options: ['backup', 'restore', 'delete', 'blobTransfer'],\n required: true,\n },\n {\n name: 'status',\n type: 'select',\n options: ['queued', 'running', 'completed', 'completed_with_warnings', 'failed'],\n required: true,\n },\n {\n name: 'message',\n type: 'text',\n required: true,\n },\n {\n name: 'error',\n type: 'text',\n },\n {\n name: 'warnings',\n type: 'textarea',\n admin: {\n hidden: true,\n },\n },\n {\n name: 'pollSecret',\n type: 'text',\n admin: {\n hidden: true,\n },\n },\n ],\n timestamps: true,\n}\n"],"names":["BackupTasksCollection","slug","access","create","delete","read","update","admin","hidden","fields","name","type","options","required","timestamps"],"mappings":"AAEA,OAAO,MAAMA,wBAA0C;IACrDC,MAAM;IACNC,QAAQ;QACNC,QAAQ,IAAM;QACdC,QAAQ,IAAM;QACdC,MAAM,IAAM;QACZC,QAAQ,IAAM;IAChB;IACAC,OAAO;QACLC,QAAQ;IACV;IACAC,QAAQ;QACN;YACEC,MAAM;YACNC,MAAM;YACNC,SAAS;gBAAC;gBAAU;gBAAW;gBAAU;aAAe;YACxDC,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;YACNC,SAAS;gBAAC;gBAAU;gBAAW;gBAAa;gBAA2B;aAAS;YAChFC,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;YACNE,UAAU;QACZ;QACA;YACEH,MAAM;YACNC,MAAM;QACR;QACA;YACED,MAAM;YACNC,MAAM;YACNJ,OAAO;gBACLC,QAAQ;YACV;QACF;QACA;YACEE,MAAM;YACNC,MAAM;YACNJ,OAAO;gBACLC,QAAQ;YACV;QACF;KACD;IACDM,YAAY;AACd,EAAC"}