@trieb.work/payload-plugin-backup-mongodb 0.1.5 → 0.1.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.
package/README.md CHANGED
@@ -1,32 +1,67 @@
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 directly
13
+ in Vercel Blob Storage, so a fresh install can list and restore any prior backup
14
+ without bootstrapping a database first.
8
15
 
9
16
  ![Backup dashboard embedded in the Payload admin panel](./docs/screenshots/dashboard.png)
10
17
 
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).
18
+ A first-class **Backups** section lives right below the Payload dashboard:
19
+ browse every archive sorted by creation time, see at a glance which host and
20
+ database it belongs to, whether media is bundled and how big it is, then
21
+ download, restore or delete any backup with one click. Read more on how it
22
+ originated at
23
+ [TRWK> Case Study](https://trwk.de/case-studies/payload-plugin-mongodb-backup-restore).
14
24
 
15
25
  ---
16
26
 
17
27
  ## Highlights
18
28
 
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.
29
+ - **No meta database.** Every backup is self-describing in its blob name
30
+ (`type---db---host---{collectionCount}-{timestampMs}.{ext}`). A brand new
31
+ project can list and restore any backup straight from blob storage.
32
+ - **Scheduled (cron) and on-demand backups.** Wire
33
+ `/api/backup-mongodb/cron/run` to Vercel Cron or any HTTP scheduler. Retention
34
+ is configurable (N most recent cron backups are kept; older ones are pruned
35
+ automatically).
36
+ - **Full database coverage.** Dumps **every** MongoDB collection via the Payload
37
+ mongoose adapter including hidden system collections like `users`,
38
+ `payload-preferences`, `payload-migrations`. Individual collections can be
39
+ excluded per backup from the UI or via API.
40
+ - **Optional media bundling.** Cron or manual backups can include Payload
41
+ `media` blobs in a `.tar.gz` archive alongside the MongoDB dump.
42
+ - **Restore with filters.** Restore any backup with an optional collection
43
+ blacklist and `mergeData` upsert mode. Partial restores keep the running admin
44
+ session / tasks collection intact.
45
+ - **Native admin dashboard.** Adds a `BackupDashboard` widget to the Payload
46
+ admin (`afterDashboard`): list / sort / filter / search backups, trigger
47
+ manual backups or restores, configure retention and storage, and live-poll
48
+ long-running tasks.
49
+ - **Payload REST, not Next.js route files.** All endpoints are registered as
50
+ Payload custom endpoints and served by the default `/api/[...slug]` handler —
51
+ you do not add `app/.../route.ts` files for this plugin.
52
+ - **Pluggable blob storage.** Uses `BLOB_READ_WRITE_TOKEN` by default (the same
53
+ store you often use with `@payloadcms/storage-vercel-blob`), or point backups
54
+ at a dedicated Vercel Blob store in the admin. Both **public** and **private**
55
+ access stores are supported; a validation step detects which modes the store
56
+ accepts.
57
+ - **Resumable long-running tasks.** Manual backups, restores, and seed runs are
58
+ tracked in a hidden `backup-tasks` collection (TTL, 30 min). The UI polls
59
+ progress with a short-lived `pollSecret` so long jobs stay observable even
60
+ across reloads.
61
+ - **Demo/seed support.** Optional `seedDemoDumpUrl` registers a one-click seed
62
+ endpoint for templates and starters.
63
+ - **Tested.** Vitest covers archive, backup, restore, task progress, blob I/O,
64
+ endpoint auth, cron parsing, and blob-name helpers.
30
65
 
31
66
  ---
32
67
 
@@ -35,7 +70,8 @@ bundled and how big it is, then download, restore or delete any backup with one
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 Vercel Blob read/write token (`BLOB_READ_WRITE_TOKEN`). Vercel hosting is
74
+ **not** required — any Node runtime that can reach Vercel Blob works.
39
75
  - Next.js **15+** and React **19+** (the usual Payload 3 + App Router stack).
40
76
 
41
77
  ### Environment variables
@@ -53,7 +89,8 @@ bundled and how big it is, then download, restore or delete any backup with one
53
89
 
54
90
  ## Installation and getting started
55
91
 
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.
92
+ Use this flow when **adding the plugin to an existing Payload 3 + Next.js app**
93
+ that already has MongoDB and (typically) Vercel Blob configured.
57
94
 
58
95
  ### 1. Add the package
59
96
 
@@ -63,7 +100,9 @@ pnpm add @trieb.work/payload-plugin-backup-mongodb
63
100
  # yarn add @trieb.work/payload-plugin-backup-mongodb
64
101
  ```
65
102
 
66
- Peer dependencies: **`@payloadcms/db-mongodb`**, **`payload`**, **`@payloadcms/ui`**, **`next`**, and **`react`**. (all expected in a Payload 3 + MongoDB + Next.js app).
103
+ Peer dependencies: **`@payloadcms/db-mongodb`**, **`payload`**,
104
+ **`@payloadcms/ui`**, **`next`**, and **`react`**. (all expected in a Payload
105
+ 3 + MongoDB + Next.js app).
67
106
 
68
107
  ### 2. Register the plugin
69
108
 
@@ -82,7 +121,10 @@ export default buildConfig({
82
121
  })
83
122
  ```
84
123
 
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.
124
+ The plugin will register the backup collections, mount the dashboard, and add
125
+ `/api/backup-mongodb/*` endpoints. Import paths resolve to a **published or
126
+ linked** `dist/` build; after local changes to the package, run `pnpm build` in
127
+ the plugin repo (or reinstall) so `dist/` is up to date.
86
128
 
87
129
  ### 3. Regenerate types and the import map
88
130
 
@@ -95,7 +137,8 @@ pnpm payload generate:importmap
95
137
 
96
138
  ### 4. (Optional) Schedule cron backups
97
139
 
98
- **Vercel:** add a cron in `vercel.json`. Path must match your API; default is under `/api/…`:
140
+ **Vercel:** add a cron in `vercel.json`. Path must match your API; default is
141
+ under `/api/…`:
99
142
 
100
143
  ```json
101
144
  {
@@ -108,7 +151,9 @@ pnpm payload generate:importmap
108
151
  }
109
152
  ```
110
153
 
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.
154
+ Vercel Cron sends header `Authorization: Bearer $CRON_SECRET`. **Other hosts**
155
+ (k8s, GitHub Actions, etc.): `GET` the same URL on your schedule with that same
156
+ authorization header.
112
157
 
113
158
  ### Checklist
114
159
 
@@ -116,33 +161,49 @@ Vercel Cron sends header `Authorization: Bearer $CRON_SECRET`. **Other hosts** (
116
161
  - [ ] `backupMongodbPlugin` in `plugins` array
117
162
  - [ ] `generate:types` and `generate:importmap` run
118
163
  - [ ] `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
164
+ - [ ] (Optional) `CRON_SECRET` and cron job (vercel.json) if you want scheduled
165
+ backups
120
166
 
121
- Start your dev server, open `/admin`, sign in as an admin, and you should see **Backups** below the dashboard.
167
+ Start your dev server, open `/admin`, sign in as an admin, and you should see
168
+ **Backups** below the dashboard.
122
169
 
123
170
  ---
124
171
 
125
172
  ## Admin UI
126
173
 
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.
174
+ After the first login as an admin user, a **Backups** section appears below the
175
+ default Payload dashboard. Day-to-day tasks — create, download, restore, delete,
176
+ schedule — all happen in the UI below.
129
177
 
130
178
  **Who can see it?** The visibility rules, in precedence order:
131
179
 
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.
180
+ 1. If the plugin is registered with an `access` function, it wins (see
181
+ [Plugin options](#plugin-options)).
182
+ 2. Otherwise, if `PAYLOAD_BACKUP_ALLOWED_ROLES` is set, it is used as a
183
+ comma-separated allow-list of role slugs (case-insensitive). Use `*` for "any
184
+ authenticated user". Example:
185
+ `PAYLOAD_BACKUP_ALLOWED_ROLES=admin,superadmin`.
186
+ 3. Otherwise, the historical default applies: users with a `role` whose slug is
187
+ `admin` see the dashboard; projects that don't use a `roles` field at all get
188
+ the dashboard for any authenticated user.
135
189
 
136
190
  ### Scheduled (cron) vs on-demand (manual) backups
137
191
 
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.
192
+ - **Scheduled / cron** backups follow shared settings: retention, media toggle,
193
+ and collections to skip. Older cron archives are pruned when the keep-count is
194
+ exceeded. They appear in the list as **CRON BACKUP** (or similar).
195
+ - **Manual / on-demand** backups are configured **per run** — per-collection
196
+ toggles, optional media in a `.tar.gz` archive. Manual backups are not
197
+ auto-pruned; use them as checkpoints before migrations.
140
198
 
141
199
  ![Manual backup dialog with per-collection selection and optional media bundling](./docs/screenshots/manual-backup.png)
142
200
 
143
201
  ### Selective restore with per-collection preview
144
202
 
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.
203
+ **Restore** opens a preview of the archive before any write. You can restore
204
+ everything, cherry-pick collections, or skip media from `.tar.gz` when you only
205
+ need database rows. Restoring from a different host or database is supported;
206
+ filters help when cloning production into staging.
146
207
 
147
208
  ![Restore dialog previewing document counts per collection](./docs/screenshots/restore.png)
148
209
 
@@ -152,10 +213,16 @@ Retention, storage token, and cron collection skip list live in one modal:
152
213
 
153
214
  ![Scheduled backup settings: cron schedule, retention, blob token, collection skip list](./docs/screenshots/settings.png)
154
215
 
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.
216
+ - **Schedule:** when `vercel.json` is present, the plugin can show a
217
+ human-readable cron description (via
218
+ [cronstrue](https://github.com/bradymholt/cronstrue)).
219
+ - **Retention:** how many **cron** archives to keep; manual backups are not
220
+ pruned by this.
221
+ - **Dedicated backup storage (optional):** a separate Vercel Blob read/write
222
+ token so backups can live in a different store than media. The UI can validate
223
+ the token and optionally copy existing `backups/*` objects to the new store.
224
+ - **Collection selection for cron:** defaults to all collections, with
225
+ per-collection opt-out.
159
226
 
160
227
  ---
161
228
 
@@ -208,7 +275,10 @@ backupMongodbPlugin({
208
275
 
209
276
  ## HTTP API (Payload REST)
210
277
 
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.
278
+ All routes are served by Payload’s `/api/[...slug]` handler under
279
+ **`/api/backup-mongodb/…`**. The `backup-mongodb` prefix avoids clashing with a
280
+ collection slug. The admin UI uses the same URLs; use the exported
281
+ `backupPluginPublicApiPaths` helper in client code.
212
282
 
213
283
  ### Cron / external (Bearer `CRON_SECRET`)
214
284
 
@@ -241,7 +311,11 @@ Authorization: Bearer <CRON_SECRET>
241
311
  For scripts, hooks, and tests:
242
312
 
243
313
  ```ts
244
- import { createBackup, listBackups, restoreBackup } from '@trieb.work/payload-plugin-backup-mongodb'
314
+ import {
315
+ createBackup,
316
+ listBackups,
317
+ restoreBackup,
318
+ } from '@trieb.work/payload-plugin-backup-mongodb'
245
319
 
246
320
  // Example: manual backup from a one-off script
247
321
  await createBackup(payload, { cron: false, includeMedia: true })
@@ -266,16 +340,23 @@ backups/{type}---{dbName}---{hostname}---{collectionCount}-{timestampMs}.{json|t
266
340
  ```
267
341
 
268
342
  - `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.
343
+ - `dbName` / `hostname`: derived from your MongoDB URL and public server URL;
344
+ URL-encoded in the name.
345
+ - `collectionCount` + `timestampMs`: sort key for listing; legacy blob names are
346
+ still supported.
271
347
 
272
348
  ### Public vs private stores
273
349
 
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.
350
+ A validation step probes the bucket and records whether the store behaves as
351
+ public or private. New uploads use the right access mode. Restore and preview
352
+ flows handle both.
275
353
 
276
354
  ### Overriding the backup store
277
355
 
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.
356
+ To keep backups in a **different** Vercel Blob project than media, open **Backup
357
+ settings** in the admin, paste a dedicated `BLOB_READ_WRITE_TOKEN`, validate,
358
+ and optionally migrate existing `backups/*` objects to the new store before
359
+ switching.
279
360
 
280
361
  ---
281
362
 
@@ -287,29 +368,49 @@ pnpm test:int
287
368
 
288
369
  ### E2E (Playwright)
289
370
 
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.
371
+ End-to-end tests live in `tests/e2e/` and target the `dev/` Next + Payload app.
372
+ **Locally**, start Mongo (or rely on the in-memory replica set when
373
+ `DATABASE_URL` / `MONGODB_URI` are unset), then either run `pnpm dev` and
374
+ `pnpm test:e2e` in another shell (Playwright reuses the server when not in
375
+ `CI`), or run only `pnpm test:e2e` so Playwright starts `pnpm dev` for you.
291
376
 
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.
377
+ **CI** (`.github/workflows/e2e.yml`, same pattern as the reference `e2e.yml` in
378
+ executive-search: MongoDB 7 service, `pnpm run build` then `pnpm run build:dev`,
379
+ then Playwright against `next start`). Optional secret
380
+ **`BLOB_READ_WRITE_TOKEN`** for fuller blob behaviour.
293
381
 
294
382
  ### Optional: deploy `dev/` to Vercel
295
383
 
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).
384
+ `.github/workflows/deploy-dev-vercel.yml` runs when `dev/` (or related paths)
385
+ change on `main`, or via **workflow_dispatch**, **only if** `VERCEL_TOKEN`,
386
+ `VERCEL_ORG_ID`, and `VERCEL_PROJECT_ID` are set. Configure the Vercel project
387
+ **Root Directory** to `dev` (`dev/vercel.json` wires install/build from the repo
388
+ root).
297
389
 
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()`.
390
+ The repo includes a `dev/` Payload + Next app (MongoDB Memory Server when no URI
391
+ is set): `pnpm dev` → `http://localhost:3000/admin` (login in
392
+ `dev/helpers/credentials.ts`). In unit tests, external services like
393
+ `@vercel/blob` and `bson` are often mocked with `vi.mock()`.
299
394
 
300
395
  ---
301
396
 
302
397
  ## Publishing (npm)
303
398
 
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).
399
+ Versioning uses [Changesets](https://github.com/changesets/changesets): add a
400
+ file with `pnpm changeset`, open a PR to `main`, and merge. The **Release**
401
+ workflow opens a “version packages” PR or runs `pnpm run release` (`build` +
402
+ `changeset publish`) when the set of changesets is ready. PRs need a new
403
+ `.changeset/*.md` unless you add the **`no-changeset`** label (e.g. docs-only).
305
404
 
306
405
  ---
307
406
 
308
407
  ## Roadmap / ideas
309
408
 
310
- - Multipart / very large backup uploads (Vercel Blob supports large objects; may need to switch past single-part limits).
409
+ - Multipart / very large backup uploads (Vercel Blob supports large objects; may
410
+ need to switch past single-part limits).
311
411
  - 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).
412
+ - Scheduler-agnostic display when not using Vercel (`vercel.json` is currently
413
+ used for the schedule summary where available).
313
414
  - Streaming restore for very large databases.
314
415
  - Configurable `backups/` prefix or bucket layout.
315
416
  - add support for tenant plugin and partial backups per tenant
@@ -322,4 +423,5 @@ MIT
322
423
 
323
424
  ---
324
425
 
325
- Built and maintained by [TRWK>](https://trwk.de), formerly [trieb.work](https://trieb.work).
426
+ Built and maintained by [TRWK>](https://trwk.de), formerly
427
+ [trieb.work](https://trieb.work).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/BackupDashboard/BackupListCollapsible.client.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Collapsible } from '@payloadcms/ui'\nimport { useCallback, useId, useMemo, useRef, useState } from 'react'\n\nimport type { TransformBlobNameResult } from '../../utils/index'\n\nimport { closeNativeDialogOnBackdropPointer } from '../../utils/dialogBackdrop'\nimport { formatBytes, getBackupSortTimeMs, transformBlobName } from '../../utils/index'\nimport { BackupItemActions } from './BackupItemActions.client'\n\nexport interface SerializableBackupBlob {\n downloadUrl: string\n pathname: string\n size: number\n uploadedAt: string\n url: string\n}\n\nexport interface BackupListCollapsibleProps {\n blobs: SerializableBackupBlob[]\n countOtherDb: number\n countOtherHostname: number\n currentDbName: string\n currentHostname: string\n i18nLanguage: string\n}\n\ninterface BackupListFilterState {\n dateFrom: string\n dateTo: string\n label: string\n media: 'all' | 'with' | 'without'\n showOtherDb: boolean\n showOtherHostname: boolean\n source: 'all' | 'cron' | 'manual'\n}\n\nconst defaultFilterState: BackupListFilterState = {\n dateFrom: '',\n dateTo: '',\n label: '',\n media: 'all',\n showOtherDb: true,\n showOtherHostname: false,\n source: 'all',\n}\n\nfunction parseDateInputStartMs(isoDate: string): null | number {\n const parts = isoDate.split('-').map(Number)\n if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) {\n return null\n }\n const [y, m, d] = parts\n const t = new Date(y, m - 1, d, 0, 0, 0, 0).getTime()\n return Number.isFinite(t) ? t : null\n}\n\nfunction parseDateInputEndMs(isoDate: string): null | number {\n const parts = isoDate.split('-').map(Number)\n if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) {\n return null\n }\n const [y, m, d] = parts\n const t = new Date(y, m - 1, d, 23, 59, 59, 999).getTime()\n return Number.isFinite(t) ? t : null\n}\n\nfunction passesDateMediaSource(\n parsed: TransformBlobNameResult,\n displayMs: number,\n filters: BackupListFilterState,\n): boolean {\n if (filters.dateFrom) {\n const from = parseDateInputStartMs(filters.dateFrom)\n if (from != null && displayMs < from) {\n return false\n }\n }\n if (filters.dateTo) {\n const to = parseDateInputEndMs(filters.dateTo)\n if (to != null && displayMs > to) {\n return false\n }\n }\n if (filters.media === 'with' && parsed.fileType !== 'tar.gz') {\n return false\n }\n if (filters.media === 'without' && parsed.fileType !== 'json') {\n return false\n }\n if (filters.source === 'manual' && parsed.type !== 'manual') {\n return false\n }\n if (filters.source === 'cron' && parsed.type !== 'cron') {\n return false\n }\n const labelQuery = filters.label.trim().toLowerCase()\n if (labelQuery) {\n const blobLabel = (parsed.label ?? '').toLowerCase()\n if (!blobLabel.includes(labelQuery)) {\n return false\n }\n }\n return true\n}\n\nfunction hasNonDefaultFilters(f: BackupListFilterState): boolean {\n return (\n f.dateFrom !== defaultFilterState.dateFrom ||\n f.dateTo !== defaultFilterState.dateTo ||\n f.label !== defaultFilterState.label ||\n f.media !== defaultFilterState.media ||\n f.source !== defaultFilterState.source ||\n f.showOtherDb !== defaultFilterState.showOtherDb ||\n f.showOtherHostname !== defaultFilterState.showOtherHostname\n )\n}\n\nexport const BackupListCollapsible: React.FC<BackupListCollapsibleProps> = ({\n blobs,\n countOtherDb,\n countOtherHostname,\n currentDbName,\n currentHostname,\n i18nLanguage,\n}) => {\n const uid = useId()\n const idDb = `${uid}-show-other-db`\n const idHost = `${uid}-show-other-host`\n const dialogRef = useRef<HTMLDialogElement>(null)\n const [filters, setFilters] = useState<BackupListFilterState>(defaultFilterState)\n\n const visibleRows = useMemo(() => {\n const rows: Array<{\n blob: SerializableBackupBlob\n displayAt: Date\n displayMs: number\n parsed: TransformBlobNameResult\n }> = []\n\n for (const blob of blobs) {\n const parsed = transformBlobName(blob.pathname)\n const { dbName, hostname } = parsed\n const isCurrentDb = currentDbName === dbName\n const isCurrentHostname = currentHostname === hostname\n if (!(filters.showOtherDb || isCurrentDb)) {\n continue\n }\n if (!(filters.showOtherHostname || isCurrentHostname)) {\n continue\n }\n\n const uploadedAt = new Date(blob.uploadedAt)\n const displayMs = getBackupSortTimeMs(parsed, uploadedAt)\n if (!passesDateMediaSource(parsed, displayMs, filters)) {\n continue\n }\n\n rows.push({\n blob,\n displayAt: new Date(displayMs),\n displayMs,\n parsed,\n })\n }\n\n return rows\n }, [blobs, currentDbName, currentHostname, filters])\n\n const openFilterDialog = useCallback(() => {\n dialogRef.current?.showModal()\n }, [])\n\n const closeFilterDialog = useCallback(() => {\n dialogRef.current?.close()\n }, [])\n\n const clearFilters = useCallback(() => {\n setFilters(defaultFilterState)\n }, [])\n\n const hasScopeOptions = countOtherDb > 0 || countOtherHostname > 0\n const filtersActive = hasNonDefaultFilters(filters)\n\n const filtersButton = (\n <Button\n buttonStyle=\"secondary\"\n className={\n filtersActive\n ? 'backup-dashboard__filter-btn backup-dashboard__filter-btn--active'\n : 'backup-dashboard__filter-btn'\n }\n onClick={openFilterDialog}\n type=\"button\"\n >\n Filters\n </Button>\n )\n\n return (\n <>\n <Collapsible\n header={<span className=\"backup-dashboard__collapsible-title\">Backup list</span>}\n initCollapsed={true}\n >\n <div className=\"backup-dashboard__backup-list-toolbar\">{filtersButton}</div>\n {blobs.length === 0 ? (\n <p className=\"backup-dashboard__list-empty\">No backups yet</p>\n ) : visibleRows.length === 0 ? (\n <p className=\"backup-dashboard__list-empty\">No backups match the current filters</p>\n ) : (\n visibleRows.map(({ blob, displayAt, parsed }) => {\n const { type, collectionCount, dbName, fileType, hostname, label } = parsed\n const isCurrentDb = currentDbName === dbName\n const isCurrentHostname = currentHostname === hostname\n\n return (\n <div className=\"backup-item\" key={blob.pathname}>\n <div className=\"backup-item__main\">\n <div className=\"backup-item__primary\">\n <time\n className=\"backup-item__time\"\n dateTime={displayAt.toISOString()}\n suppressHydrationWarning\n >\n {displayAt.toLocaleString(i18nLanguage)}\n </time>\n <span\n className={`backup-item__pill backup-item__pill--kind backup-item__pill--${type === 'cron' ? 'cron' : 'manual'}`}\n >\n {type === 'cron' ? 'Cron backup' : 'Manual backup'}\n </span>\n {label ? (\n <span\n className=\"backup-item__pill backup-item__pill--label\"\n title={`Label: ${label}`}\n >\n <span className=\"backup-item__pill-key\">Label</span>\n {label}\n </span>\n ) : null}\n </div>\n <div className=\"backup-item__meta\">\n <span\n className={`backup-item__pill backup-item__pill--db${isCurrentDb ? '' : ' backup-item__pill--warn'}`}\n >\n <span className=\"backup-item__pill-key\">\n {isCurrentDb ? 'DB' : 'other DB'}\n </span>\n {dbName || 'Unknown'}\n </span>\n <span\n className={`backup-item__pill backup-item__pill--host${isCurrentHostname ? '' : ' backup-item__pill--warn'}`}\n >\n <span className=\"backup-item__pill-key\">Host</span>\n {hostname || 'Unknown'}\n </span>\n {typeof collectionCount === 'number' ? (\n <span className=\"backup-item__pill backup-item__pill--collections\">\n <span className=\"backup-item__pill-key\">Collections</span>\n {collectionCount}\n </span>\n ) : null}\n {fileType === 'tar.gz' ? (\n <span className=\"backup-item__pill backup-item__pill--media\">With media</span>\n ) : fileType === 'json' ? (\n <span className=\"backup-item__pill backup-item__pill--media\">\n Without media\n </span>\n ) : null}\n <span\n className=\"backup-item__pill backup-item__pill--size\"\n title={`${blob.size.toLocaleString(i18nLanguage)} bytes`}\n >\n <span className=\"backup-item__pill-key\">Size</span>\n <span className=\"backup-item__size-value\">{formatBytes(blob.size)}</span>\n </span>\n </div>\n </div>\n <BackupItemActions\n downloadUrl={blob.downloadUrl}\n pathname={blob.pathname}\n url={blob.url}\n />\n </div>\n )\n })\n )}\n </Collapsible>\n\n <dialog\n className=\"backup-confirm-dialog backup-confirm-dialog--backup-list-filters\"\n onPointerDown={(e) => closeNativeDialogOnBackdropPointer(e, dialogRef)}\n ref={dialogRef}\n >\n <h3 className=\"backup-confirm-dialog__title\">Filter backups</h3>\n <p className=\"backup-confirm-dialog__subtitle\">\n All filters apply in the browser only. Current DB and hostname are always shown; enable\n the options below to include backups from other databases or hosts.\n </p>\n <div className=\"backup-confirm-dialog__body backup-list-filters__body\">\n {hasScopeOptions ? (\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Scope</legend>\n {countOtherDb > 0 ? (\n <div className=\"field-type checkbox backup-dashboard__collapsible-checkbox backup-list-filters__scope-row\">\n <input\n aria-label={`Show other databases (${countOtherDb} in list)`}\n checked={filters.showOtherDb}\n className=\"checkbox-input__input\"\n id={idDb}\n onChange={(e) => setFilters((f) => ({ ...f, showOtherDb: e.target.checked }))}\n type=\"checkbox\"\n />\n <label className=\"field-label\" htmlFor={idDb}>\n Show other DBs\n <span className=\"backup-list-filters__hint\"> ({countOtherDb} in list)</span>\n </label>\n </div>\n ) : null}\n {countOtherHostname > 0 ? (\n <div className=\"field-type checkbox backup-dashboard__collapsible-checkbox backup-list-filters__scope-row\">\n <input\n aria-label={`Show other hostnames (${countOtherHostname} in list)`}\n checked={filters.showOtherHostname}\n className=\"checkbox-input__input\"\n id={idHost}\n onChange={(e) =>\n setFilters((f) => ({ ...f, showOtherHostname: e.target.checked }))\n }\n type=\"checkbox\"\n />\n <label className=\"field-label\" htmlFor={idHost}>\n Show other Hostnames\n <span className=\"backup-list-filters__hint\">\n {' '}\n ({countOtherHostname} in list)\n </span>\n </label>\n </div>\n ) : null}\n </fieldset>\n ) : null}\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Label</legend>\n <div className=\"backup-list-filters__row backup-list-filters__row--label\">\n <label className=\"backup-list-filters__label\" htmlFor={`${uid}-label`}>\n Search\n </label>\n <input\n aria-label=\"Filter backups by label text\"\n autoComplete=\"off\"\n className=\"backup-list-filters__text\"\n id={`${uid}-label`}\n onChange={(e) => setFilters((f) => ({ ...f, label: e.target.value }))}\n placeholder=\"Filter by label text\"\n spellCheck={false}\n type=\"text\"\n value={filters.label}\n />\n </div>\n </fieldset>\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Date range</legend>\n <div className=\"backup-list-filters__row\">\n <label className=\"backup-list-filters__label\" htmlFor={`${uid}-from`}>\n From\n </label>\n <input\n aria-label=\"Filter from date\"\n className=\"backup-list-filters__date\"\n id={`${uid}-from`}\n onChange={(e) => setFilters((f) => ({ ...f, dateFrom: e.target.value }))}\n type=\"date\"\n value={filters.dateFrom}\n />\n </div>\n <div className=\"backup-list-filters__row\">\n <label className=\"backup-list-filters__label\" htmlFor={`${uid}-to`}>\n To\n </label>\n <input\n aria-label=\"Filter to date\"\n className=\"backup-list-filters__date\"\n id={`${uid}-to`}\n onChange={(e) => setFilters((f) => ({ ...f, dateTo: e.target.value }))}\n type=\"date\"\n value={filters.dateTo}\n />\n </div>\n </fieldset>\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Media</legend>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Media filter: all backups\"\n checked={filters.media === 'all'}\n className=\"radio-input__input\"\n id={`${uid}-media-all`}\n name={`${uid}-media`}\n onChange={() => setFilters((f) => ({ ...f, media: 'all' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-media-all`}>\n All\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Media filter: with media\"\n checked={filters.media === 'with'}\n className=\"radio-input__input\"\n id={`${uid}-media-with`}\n name={`${uid}-media`}\n onChange={() => setFilters((f) => ({ ...f, media: 'with' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-media-with`}>\n With media\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Media filter: without media\"\n checked={filters.media === 'without'}\n className=\"radio-input__input\"\n id={`${uid}-media-without`}\n name={`${uid}-media`}\n onChange={() => setFilters((f) => ({ ...f, media: 'without' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-media-without`}>\n Without media\n </label>\n </div>\n </fieldset>\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Source</legend>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Source filter: all\"\n checked={filters.source === 'all'}\n className=\"radio-input__input\"\n id={`${uid}-source-all`}\n name={`${uid}-source`}\n onChange={() => setFilters((f) => ({ ...f, source: 'all' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-source-all`}>\n All\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Source filter: manual backups\"\n checked={filters.source === 'manual'}\n className=\"radio-input__input\"\n id={`${uid}-source-manual`}\n name={`${uid}-source`}\n onChange={() => setFilters((f) => ({ ...f, source: 'manual' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-source-manual`}>\n Manual\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Source filter: cron backups\"\n checked={filters.source === 'cron'}\n className=\"radio-input__input\"\n id={`${uid}-source-cron`}\n name={`${uid}-source`}\n onChange={() => setFilters((f) => ({ ...f, source: 'cron' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-source-cron`}>\n Cron\n </label>\n </div>\n </fieldset>\n </div>\n <div className=\"backup-confirm-dialog__actions\">\n <Button buttonStyle=\"secondary\" onClick={clearFilters} type=\"button\">\n Clear filters\n </Button>\n <Button onClick={closeFilterDialog} type=\"button\">\n Done\n </Button>\n </div>\n </dialog>\n </>\n )\n}\n"],"names":["Button","Collapsible","useCallback","useId","useMemo","useRef","useState","closeNativeDialogOnBackdropPointer","formatBytes","getBackupSortTimeMs","transformBlobName","BackupItemActions","defaultFilterState","dateFrom","dateTo","label","media","showOtherDb","showOtherHostname","source","parseDateInputStartMs","isoDate","parts","split","map","Number","length","some","n","isFinite","y","m","d","t","Date","getTime","parseDateInputEndMs","passesDateMediaSource","parsed","displayMs","filters","from","to","fileType","type","labelQuery","trim","toLowerCase","blobLabel","includes","hasNonDefaultFilters","f","BackupListCollapsible","blobs","countOtherDb","countOtherHostname","currentDbName","currentHostname","i18nLanguage","uid","idDb","idHost","dialogRef","setFilters","visibleRows","rows","blob","pathname","dbName","hostname","isCurrentDb","isCurrentHostname","uploadedAt","push","displayAt","openFilterDialog","current","showModal","closeFilterDialog","close","clearFilters","hasScopeOptions","filtersActive","filtersButton","buttonStyle","className","onClick","header","span","initCollapsed","div","p","collectionCount","time","dateTime","toISOString","suppressHydrationWarning","toLocaleString","title","size","downloadUrl","url","dialog","onPointerDown","e","ref","h3","fieldset","legend","input","aria-label","checked","id","onChange","target","htmlFor","autoComplete","value","placeholder","spellCheck","name"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/BackupDashboard/BackupListCollapsible.client.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Collapsible } from '@payloadcms/ui'\nimport { useCallback, useId, useMemo, useRef, useState } from 'react'\n\nimport type { TransformBlobNameResult } from '../../utils/index'\n\nimport { closeNativeDialogOnBackdropPointer } from '../../utils/dialogBackdrop'\nimport { formatBytes, getBackupSortTimeMs, transformBlobName } from '../../utils/index'\nimport { BackupItemActions } from './BackupItemActions.client'\n\nexport interface SerializableBackupBlob {\n downloadUrl: string\n pathname: string\n size: number\n uploadedAt: string\n url: string\n}\n\nexport interface BackupListCollapsibleProps {\n blobs: SerializableBackupBlob[]\n countOtherDb: number\n countOtherHostname: number\n currentDbName: string\n currentHostname: string\n i18nLanguage: string\n}\n\ninterface BackupListFilterState {\n dateFrom: string\n dateTo: string\n label: string\n media: 'all' | 'with' | 'without'\n showOtherDb: boolean\n showOtherHostname: boolean\n source: 'all' | 'cron' | 'manual'\n}\n\nconst defaultFilterState: BackupListFilterState = {\n dateFrom: '',\n dateTo: '',\n label: '',\n media: 'all',\n showOtherDb: true,\n showOtherHostname: false,\n source: 'all',\n}\n\nfunction parseDateInputStartMs(isoDate: string): null | number {\n const parts = isoDate.split('-').map(Number)\n if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) {\n return null\n }\n const [y, m, d] = parts\n const t = new Date(y, m - 1, d, 0, 0, 0, 0).getTime()\n return Number.isFinite(t) ? t : null\n}\n\nfunction parseDateInputEndMs(isoDate: string): null | number {\n const parts = isoDate.split('-').map(Number)\n if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) {\n return null\n }\n const [y, m, d] = parts\n const t = new Date(y, m - 1, d, 23, 59, 59, 999).getTime()\n return Number.isFinite(t) ? t : null\n}\n\nfunction passesDateMediaSource(\n parsed: TransformBlobNameResult,\n displayMs: number,\n filters: BackupListFilterState,\n): boolean {\n if (filters.dateFrom) {\n const from = parseDateInputStartMs(filters.dateFrom)\n if (from != null && displayMs < from) {\n return false\n }\n }\n if (filters.dateTo) {\n const to = parseDateInputEndMs(filters.dateTo)\n if (to != null && displayMs > to) {\n return false\n }\n }\n if (filters.media === 'with' && parsed.fileType !== 'tar.gz') {\n return false\n }\n if (filters.media === 'without' && parsed.fileType !== 'json') {\n return false\n }\n if (filters.source === 'manual' && parsed.type !== 'manual') {\n return false\n }\n if (filters.source === 'cron' && parsed.type !== 'cron') {\n return false\n }\n const labelQuery = filters.label.trim().toLowerCase()\n if (labelQuery) {\n const blobLabel = (parsed.label ?? '').toLowerCase()\n if (!blobLabel.includes(labelQuery)) {\n return false\n }\n }\n return true\n}\n\nfunction hasNonDefaultFilters(f: BackupListFilterState): boolean {\n return (\n f.dateFrom !== defaultFilterState.dateFrom ||\n f.dateTo !== defaultFilterState.dateTo ||\n f.label !== defaultFilterState.label ||\n f.media !== defaultFilterState.media ||\n f.source !== defaultFilterState.source ||\n f.showOtherDb !== defaultFilterState.showOtherDb ||\n f.showOtherHostname !== defaultFilterState.showOtherHostname\n )\n}\n\nexport const BackupListCollapsible: React.FC<BackupListCollapsibleProps> = ({\n blobs,\n countOtherDb,\n countOtherHostname,\n currentDbName,\n currentHostname,\n i18nLanguage,\n}) => {\n const uid = useId()\n const idDb = `${uid}-show-other-db`\n const idHost = `${uid}-show-other-host`\n const dialogRef = useRef<HTMLDialogElement>(null)\n const [filters, setFilters] = useState<BackupListFilterState>(defaultFilterState)\n\n const visibleRows = useMemo(() => {\n const rows: Array<{\n blob: SerializableBackupBlob\n displayAt: Date\n displayMs: number\n parsed: TransformBlobNameResult\n }> = []\n\n for (const blob of blobs) {\n const parsed = transformBlobName(blob.pathname)\n const { dbName, hostname } = parsed\n const isCurrentDb = currentDbName === dbName\n const isCurrentHostname = currentHostname === hostname\n if (!(filters.showOtherDb || isCurrentDb)) {\n continue\n }\n if (!(filters.showOtherHostname || isCurrentHostname)) {\n continue\n }\n\n const uploadedAt = new Date(blob.uploadedAt)\n const displayMs = getBackupSortTimeMs(parsed, uploadedAt)\n if (!passesDateMediaSource(parsed, displayMs, filters)) {\n continue\n }\n\n rows.push({\n blob,\n displayAt: new Date(displayMs),\n displayMs,\n parsed,\n })\n }\n\n return rows\n }, [blobs, currentDbName, currentHostname, filters])\n\n const openFilterDialog = useCallback(() => {\n dialogRef.current?.showModal()\n }, [])\n\n const closeFilterDialog = useCallback(() => {\n dialogRef.current?.close()\n }, [])\n\n const clearFilters = useCallback(() => {\n setFilters(defaultFilterState)\n }, [])\n\n const hasScopeOptions = countOtherDb > 0 || countOtherHostname > 0\n const filtersActive = hasNonDefaultFilters(filters)\n\n const filtersButton = (\n <Button\n buttonStyle=\"secondary\"\n className={\n filtersActive ?\n 'backup-dashboard__filter-btn backup-dashboard__filter-btn--active'\n : 'backup-dashboard__filter-btn'\n }\n onClick={openFilterDialog}\n type=\"button\"\n >\n Filters\n </Button>\n )\n\n return (\n <>\n <Collapsible\n header={<span className=\"backup-dashboard__collapsible-title\">Backup list</span>}\n initCollapsed={true}\n >\n <div className=\"backup-dashboard__backup-list-toolbar\">{filtersButton}</div>\n {blobs.length === 0 ?\n <p className=\"backup-dashboard__list-empty\">No backups yet</p>\n : visibleRows.length === 0 ?\n <p className=\"backup-dashboard__list-empty\">No backups match the current filters</p>\n : visibleRows.map(({ blob, displayAt, parsed }) => {\n const { type, collectionCount, dbName, fileType, hostname, label } = parsed\n const isCurrentDb = currentDbName === dbName\n const isCurrentHostname = currentHostname === hostname\n\n return (\n <div className=\"backup-item\" key={blob.pathname}>\n <div className=\"backup-item__main\">\n <div className=\"backup-item__primary\">\n <time\n className=\"backup-item__time\"\n dateTime={displayAt.toISOString()}\n suppressHydrationWarning\n >\n {displayAt.toLocaleString(i18nLanguage)}\n </time>\n <span\n className={`backup-item__pill backup-item__pill--kind backup-item__pill--${type === 'cron' ? 'cron' : 'manual'}`}\n >\n {type === 'cron' ? 'Cron backup' : 'Manual backup'}\n </span>\n {label ?\n <span\n className=\"backup-item__pill backup-item__pill--label\"\n title={`Label: ${label}`}\n >\n <span className=\"backup-item__pill-key\">Label</span>\n {label}\n </span>\n : null}\n </div>\n <div className=\"backup-item__meta\">\n <span\n className={`backup-item__pill backup-item__pill--db${isCurrentDb ? '' : ' backup-item__pill--warn'}`}\n >\n <span className=\"backup-item__pill-key\">\n {isCurrentDb ? 'DB' : 'other DB'}\n </span>\n {dbName || 'Unknown'}\n </span>\n <span\n className={`backup-item__pill backup-item__pill--host${isCurrentHostname ? '' : ' backup-item__pill--warn'}`}\n >\n <span className=\"backup-item__pill-key\">Host</span>\n {hostname || 'Unknown'}\n </span>\n {typeof collectionCount === 'number' ?\n <span className=\"backup-item__pill backup-item__pill--collections\">\n <span className=\"backup-item__pill-key\">Collections</span>\n {collectionCount}\n </span>\n : null}\n {fileType === 'tar.gz' ?\n <span className=\"backup-item__pill backup-item__pill--media\">With media</span>\n : fileType === 'json' ?\n <span className=\"backup-item__pill backup-item__pill--media\">\n Without media\n </span>\n : null}\n <span\n className=\"backup-item__pill backup-item__pill--size\"\n title={`${blob.size.toLocaleString(i18nLanguage)} bytes`}\n >\n <span className=\"backup-item__pill-key\">Size</span>\n <span className=\"backup-item__size-value\">{formatBytes(blob.size)}</span>\n </span>\n </div>\n </div>\n <BackupItemActions\n downloadUrl={blob.downloadUrl}\n pathname={blob.pathname}\n url={blob.url}\n />\n </div>\n )\n })\n }\n </Collapsible>\n\n <dialog\n className=\"backup-confirm-dialog backup-confirm-dialog--backup-list-filters\"\n onPointerDown={(e) => closeNativeDialogOnBackdropPointer(e, dialogRef)}\n ref={dialogRef}\n >\n <h3 className=\"backup-confirm-dialog__title\">Filter backups</h3>\n <p className=\"backup-confirm-dialog__subtitle\">\n All filters apply in the browser only. Current DB and hostname are always shown; enable\n the options below to include backups from other databases or hosts.\n </p>\n <div className=\"backup-confirm-dialog__body backup-list-filters__body\">\n {hasScopeOptions ?\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Scope</legend>\n {countOtherDb > 0 ?\n <div className=\"field-type checkbox backup-dashboard__collapsible-checkbox backup-list-filters__scope-row\">\n <input\n aria-label={`Show other databases (${countOtherDb} in list)`}\n checked={filters.showOtherDb}\n className=\"checkbox-input__input\"\n id={idDb}\n onChange={(e) => setFilters((f) => ({ ...f, showOtherDb: e.target.checked }))}\n type=\"checkbox\"\n />\n <label className=\"field-label\" htmlFor={idDb}>\n Show other DBs\n <span className=\"backup-list-filters__hint\"> ({countOtherDb} in list)</span>\n </label>\n </div>\n : null}\n {countOtherHostname > 0 ?\n <div className=\"field-type checkbox backup-dashboard__collapsible-checkbox backup-list-filters__scope-row\">\n <input\n aria-label={`Show other hostnames (${countOtherHostname} in list)`}\n checked={filters.showOtherHostname}\n className=\"checkbox-input__input\"\n id={idHost}\n onChange={(e) =>\n setFilters((f) => ({ ...f, showOtherHostname: e.target.checked }))\n }\n type=\"checkbox\"\n />\n <label className=\"field-label\" htmlFor={idHost}>\n Show other Hostnames\n <span className=\"backup-list-filters__hint\">\n {' '}\n ({countOtherHostname} in list)\n </span>\n </label>\n </div>\n : null}\n </fieldset>\n : null}\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Label</legend>\n <div className=\"backup-list-filters__row backup-list-filters__row--label\">\n <label className=\"backup-list-filters__label\" htmlFor={`${uid}-label`}>\n Search\n </label>\n <input\n aria-label=\"Filter backups by label text\"\n autoComplete=\"off\"\n className=\"backup-list-filters__text\"\n id={`${uid}-label`}\n onChange={(e) => setFilters((f) => ({ ...f, label: e.target.value }))}\n placeholder=\"Filter by label text\"\n spellCheck={false}\n type=\"text\"\n value={filters.label}\n />\n </div>\n </fieldset>\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Date range</legend>\n <div className=\"backup-list-filters__row\">\n <label className=\"backup-list-filters__label\" htmlFor={`${uid}-from`}>\n From\n </label>\n <input\n aria-label=\"Filter from date\"\n className=\"backup-list-filters__date\"\n id={`${uid}-from`}\n onChange={(e) => setFilters((f) => ({ ...f, dateFrom: e.target.value }))}\n type=\"date\"\n value={filters.dateFrom}\n />\n </div>\n <div className=\"backup-list-filters__row\">\n <label className=\"backup-list-filters__label\" htmlFor={`${uid}-to`}>\n To\n </label>\n <input\n aria-label=\"Filter to date\"\n className=\"backup-list-filters__date\"\n id={`${uid}-to`}\n onChange={(e) => setFilters((f) => ({ ...f, dateTo: e.target.value }))}\n type=\"date\"\n value={filters.dateTo}\n />\n </div>\n </fieldset>\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Media</legend>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Media filter: all backups\"\n checked={filters.media === 'all'}\n className=\"radio-input__input\"\n id={`${uid}-media-all`}\n name={`${uid}-media`}\n onChange={() => setFilters((f) => ({ ...f, media: 'all' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-media-all`}>\n All\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Media filter: with media\"\n checked={filters.media === 'with'}\n className=\"radio-input__input\"\n id={`${uid}-media-with`}\n name={`${uid}-media`}\n onChange={() => setFilters((f) => ({ ...f, media: 'with' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-media-with`}>\n With media\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Media filter: without media\"\n checked={filters.media === 'without'}\n className=\"radio-input__input\"\n id={`${uid}-media-without`}\n name={`${uid}-media`}\n onChange={() => setFilters((f) => ({ ...f, media: 'without' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-media-without`}>\n Without media\n </label>\n </div>\n </fieldset>\n\n <fieldset className=\"backup-list-filters__fieldset\">\n <legend className=\"backup-list-filters__legend\">Source</legend>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Source filter: all\"\n checked={filters.source === 'all'}\n className=\"radio-input__input\"\n id={`${uid}-source-all`}\n name={`${uid}-source`}\n onChange={() => setFilters((f) => ({ ...f, source: 'all' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-source-all`}>\n All\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Source filter: manual backups\"\n checked={filters.source === 'manual'}\n className=\"radio-input__input\"\n id={`${uid}-source-manual`}\n name={`${uid}-source`}\n onChange={() => setFilters((f) => ({ ...f, source: 'manual' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-source-manual`}>\n Manual\n </label>\n </div>\n <div className=\"field-type radio backup-list-filters__radio-row\">\n <input\n aria-label=\"Source filter: cron backups\"\n checked={filters.source === 'cron'}\n className=\"radio-input__input\"\n id={`${uid}-source-cron`}\n name={`${uid}-source`}\n onChange={() => setFilters((f) => ({ ...f, source: 'cron' }))}\n type=\"radio\"\n />\n <label className=\"field-label\" htmlFor={`${uid}-source-cron`}>\n Cron\n </label>\n </div>\n </fieldset>\n </div>\n <div className=\"backup-confirm-dialog__actions\">\n <Button buttonStyle=\"secondary\" onClick={clearFilters} type=\"button\">\n Clear filters\n </Button>\n <Button onClick={closeFilterDialog} type=\"button\">\n Done\n </Button>\n </div>\n </dialog>\n </>\n )\n}\n"],"names":["Button","Collapsible","useCallback","useId","useMemo","useRef","useState","closeNativeDialogOnBackdropPointer","formatBytes","getBackupSortTimeMs","transformBlobName","BackupItemActions","defaultFilterState","dateFrom","dateTo","label","media","showOtherDb","showOtherHostname","source","parseDateInputStartMs","isoDate","parts","split","map","Number","length","some","n","isFinite","y","m","d","t","Date","getTime","parseDateInputEndMs","passesDateMediaSource","parsed","displayMs","filters","from","to","fileType","type","labelQuery","trim","toLowerCase","blobLabel","includes","hasNonDefaultFilters","f","BackupListCollapsible","blobs","countOtherDb","countOtherHostname","currentDbName","currentHostname","i18nLanguage","uid","idDb","idHost","dialogRef","setFilters","visibleRows","rows","blob","pathname","dbName","hostname","isCurrentDb","isCurrentHostname","uploadedAt","push","displayAt","openFilterDialog","current","showModal","closeFilterDialog","close","clearFilters","hasScopeOptions","filtersActive","filtersButton","buttonStyle","className","onClick","header","span","initCollapsed","div","p","collectionCount","time","dateTime","toISOString","suppressHydrationWarning","toLocaleString","title","size","downloadUrl","url","dialog","onPointerDown","e","ref","h3","fieldset","legend","input","aria-label","checked","id","onChange","target","htmlFor","autoComplete","value","placeholder","spellCheck","name"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,WAAW,QAAQ,iBAAgB;AACpD,SAASC,WAAW,EAAEC,KAAK,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAIrE,SAASC,kCAAkC,QAAQ,6BAA4B;AAC/E,SAASC,WAAW,EAAEC,mBAAmB,EAAEC,iBAAiB,QAAQ,oBAAmB;AACvF,SAASC,iBAAiB,QAAQ,6BAA4B;AA6B9D,MAAMC,qBAA4C;IAChDC,UAAU;IACVC,QAAQ;IACRC,OAAO;IACPC,OAAO;IACPC,aAAa;IACbC,mBAAmB;IACnBC,QAAQ;AACV;AAEA,SAASC,sBAAsBC,OAAe;IAC5C,MAAMC,QAAQD,QAAQE,KAAK,CAAC,KAAKC,GAAG,CAACC;IACrC,IAAIH,MAAMI,MAAM,KAAK,KAAKJ,MAAMK,IAAI,CAAC,CAACC,IAAM,CAACH,OAAOI,QAAQ,CAACD,KAAK;QAChE,OAAO;IACT;IACA,MAAM,CAACE,GAAGC,GAAGC,EAAE,GAAGV;IAClB,MAAMW,IAAI,IAAIC,KAAKJ,GAAGC,IAAI,GAAGC,GAAG,GAAG,GAAG,GAAG,GAAGG,OAAO;IACnD,OAAOV,OAAOI,QAAQ,CAACI,KAAKA,IAAI;AAClC;AAEA,SAASG,oBAAoBf,OAAe;IAC1C,MAAMC,QAAQD,QAAQE,KAAK,CAAC,KAAKC,GAAG,CAACC;IACrC,IAAIH,MAAMI,MAAM,KAAK,KAAKJ,MAAMK,IAAI,CAAC,CAACC,IAAM,CAACH,OAAOI,QAAQ,CAACD,KAAK;QAChE,OAAO;IACT;IACA,MAAM,CAACE,GAAGC,GAAGC,EAAE,GAAGV;IAClB,MAAMW,IAAI,IAAIC,KAAKJ,GAAGC,IAAI,GAAGC,GAAG,IAAI,IAAI,IAAI,KAAKG,OAAO;IACxD,OAAOV,OAAOI,QAAQ,CAACI,KAAKA,IAAI;AAClC;AAEA,SAASI,sBACPC,MAA+B,EAC/BC,SAAiB,EACjBC,OAA8B;IAE9B,IAAIA,QAAQ3B,QAAQ,EAAE;QACpB,MAAM4B,OAAOrB,sBAAsBoB,QAAQ3B,QAAQ;QACnD,IAAI4B,QAAQ,QAAQF,YAAYE,MAAM;YACpC,OAAO;QACT;IACF;IACA,IAAID,QAAQ1B,MAAM,EAAE;QAClB,MAAM4B,KAAKN,oBAAoBI,QAAQ1B,MAAM;QAC7C,IAAI4B,MAAM,QAAQH,YAAYG,IAAI;YAChC,OAAO;QACT;IACF;IACA,IAAIF,QAAQxB,KAAK,KAAK,UAAUsB,OAAOK,QAAQ,KAAK,UAAU;QAC5D,OAAO;IACT;IACA,IAAIH,QAAQxB,KAAK,KAAK,aAAasB,OAAOK,QAAQ,KAAK,QAAQ;QAC7D,OAAO;IACT;IACA,IAAIH,QAAQrB,MAAM,KAAK,YAAYmB,OAAOM,IAAI,KAAK,UAAU;QAC3D,OAAO;IACT;IACA,IAAIJ,QAAQrB,MAAM,KAAK,UAAUmB,OAAOM,IAAI,KAAK,QAAQ;QACvD,OAAO;IACT;IACA,MAAMC,aAAaL,QAAQzB,KAAK,CAAC+B,IAAI,GAAGC,WAAW;IACnD,IAAIF,YAAY;QACd,MAAMG,YAAY,AAACV,CAAAA,OAAOvB,KAAK,IAAI,EAAC,EAAGgC,WAAW;QAClD,IAAI,CAACC,UAAUC,QAAQ,CAACJ,aAAa;YACnC,OAAO;QACT;IACF;IACA,OAAO;AACT;AAEA,SAASK,qBAAqBC,CAAwB;IACpD,OACEA,EAAEtC,QAAQ,KAAKD,mBAAmBC,QAAQ,IAC1CsC,EAAErC,MAAM,KAAKF,mBAAmBE,MAAM,IACtCqC,EAAEpC,KAAK,KAAKH,mBAAmBG,KAAK,IACpCoC,EAAEnC,KAAK,KAAKJ,mBAAmBI,KAAK,IACpCmC,EAAEhC,MAAM,KAAKP,mBAAmBO,MAAM,IACtCgC,EAAElC,WAAW,KAAKL,mBAAmBK,WAAW,IAChDkC,EAAEjC,iBAAiB,KAAKN,mBAAmBM,iBAAiB;AAEhE;AAEA,OAAO,MAAMkC,wBAA8D,CAAC,EAC1EC,KAAK,EACLC,YAAY,EACZC,kBAAkB,EAClBC,aAAa,EACbC,eAAe,EACfC,YAAY,EACb;IACC,MAAMC,MAAMxD;IACZ,MAAMyD,OAAO,GAAGD,IAAI,cAAc,CAAC;IACnC,MAAME,SAAS,GAAGF,IAAI,gBAAgB,CAAC;IACvC,MAAMG,YAAYzD,OAA0B;IAC5C,MAAM,CAACmC,SAASuB,WAAW,GAAGzD,SAAgCM;IAE9D,MAAMoD,cAAc5D,QAAQ;QAC1B,MAAM6D,OAKD,EAAE;QAEP,KAAK,MAAMC,QAAQb,MAAO;YACxB,MAAMf,SAAS5B,kBAAkBwD,KAAKC,QAAQ;YAC9C,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAG/B;YAC7B,MAAMgC,cAAcd,kBAAkBY;YACtC,MAAMG,oBAAoBd,oBAAoBY;YAC9C,IAAI,CAAE7B,CAAAA,QAAQvB,WAAW,IAAIqD,WAAU,GAAI;gBACzC;YACF;YACA,IAAI,CAAE9B,CAAAA,QAAQtB,iBAAiB,IAAIqD,iBAAgB,GAAI;gBACrD;YACF;YAEA,MAAMC,aAAa,IAAItC,KAAKgC,KAAKM,UAAU;YAC3C,MAAMjC,YAAY9B,oBAAoB6B,QAAQkC;YAC9C,IAAI,CAACnC,sBAAsBC,QAAQC,WAAWC,UAAU;gBACtD;YACF;YAEAyB,KAAKQ,IAAI,CAAC;gBACRP;gBACAQ,WAAW,IAAIxC,KAAKK;gBACpBA;gBACAD;YACF;QACF;QAEA,OAAO2B;IACT,GAAG;QAACZ;QAAOG;QAAeC;QAAiBjB;KAAQ;IAEnD,MAAMmC,mBAAmBzE,YAAY;QACnC4D,UAAUc,OAAO,EAAEC;IACrB,GAAG,EAAE;IAEL,MAAMC,oBAAoB5E,YAAY;QACpC4D,UAAUc,OAAO,EAAEG;IACrB,GAAG,EAAE;IAEL,MAAMC,eAAe9E,YAAY;QAC/B6D,WAAWnD;IACb,GAAG,EAAE;IAEL,MAAMqE,kBAAkB3B,eAAe,KAAKC,qBAAqB;IACjE,MAAM2B,gBAAgBhC,qBAAqBV;IAE3C,MAAM2C,8BACJ,KAACnF;QACCoF,aAAY;QACZC,WACEH,gBACE,sEACA;QAEJI,SAASX;QACT/B,MAAK;kBACN;;IAKH,qBACE;;0BACE,MAAC3C;gBACCsF,sBAAQ,KAACC;oBAAKH,WAAU;8BAAsC;;gBAC9DI,eAAe;;kCAEf,KAACC;wBAAIL,WAAU;kCAAyCF;;oBACvD9B,MAAM3B,MAAM,KAAK,kBAChB,KAACiE;wBAAEN,WAAU;kCAA+B;yBAC5CrB,YAAYtC,MAAM,KAAK,kBACvB,KAACiE;wBAAEN,WAAU;kCAA+B;yBAC5CrB,YAAYxC,GAAG,CAAC,CAAC,EAAE0C,IAAI,EAAEQ,SAAS,EAAEpC,MAAM,EAAE;wBAC1C,MAAM,EAAEM,IAAI,EAAEgD,eAAe,EAAExB,MAAM,EAAEzB,QAAQ,EAAE0B,QAAQ,EAAEtD,KAAK,EAAE,GAAGuB;wBACrE,MAAMgC,cAAcd,kBAAkBY;wBACtC,MAAMG,oBAAoBd,oBAAoBY;wBAE9C,qBACE,MAACqB;4BAAIL,WAAU;;8CACb,MAACK;oCAAIL,WAAU;;sDACb,MAACK;4CAAIL,WAAU;;8DACb,KAACQ;oDACCR,WAAU;oDACVS,UAAUpB,UAAUqB,WAAW;oDAC/BC,wBAAwB;8DAEvBtB,UAAUuB,cAAc,CAACvC;;8DAE5B,KAAC8B;oDACCH,WAAW,CAAC,6DAA6D,EAAEzC,SAAS,SAAS,SAAS,UAAU;8DAE/GA,SAAS,SAAS,gBAAgB;;gDAEpC7B,sBACC,MAACyE;oDACCH,WAAU;oDACVa,OAAO,CAAC,OAAO,EAAEnF,OAAO;;sEAExB,KAACyE;4DAAKH,WAAU;sEAAwB;;wDACvCtE;;qDAEH;;;sDAEJ,MAAC2E;4CAAIL,WAAU;;8DACb,MAACG;oDACCH,WAAW,CAAC,uCAAuC,EAAEf,cAAc,KAAK,4BAA4B;;sEAEpG,KAACkB;4DAAKH,WAAU;sEACbf,cAAc,OAAO;;wDAEvBF,UAAU;;;8DAEb,MAACoB;oDACCH,WAAW,CAAC,yCAAyC,EAAEd,oBAAoB,KAAK,4BAA4B;;sEAE5G,KAACiB;4DAAKH,WAAU;sEAAwB;;wDACvChB,YAAY;;;gDAEd,OAAOuB,oBAAoB,yBAC1B,MAACJ;oDAAKH,WAAU;;sEACd,KAACG;4DAAKH,WAAU;sEAAwB;;wDACvCO;;qDAEH;gDACDjD,aAAa,yBACZ,KAAC6C;oDAAKH,WAAU;8DAA6C;qDAC7D1C,aAAa,uBACb,KAAC6C;oDAAKH,WAAU;8DAA6C;qDAG7D;8DACF,MAACG;oDACCH,WAAU;oDACVa,OAAO,GAAGhC,KAAKiC,IAAI,CAACF,cAAc,CAACvC,cAAc,MAAM,CAAC;;sEAExD,KAAC8B;4DAAKH,WAAU;sEAAwB;;sEACxC,KAACG;4DAAKH,WAAU;sEAA2B7E,YAAY0D,KAAKiC,IAAI;;;;;;;;8CAItE,KAACxF;oCACCyF,aAAalC,KAAKkC,WAAW;oCAC7BjC,UAAUD,KAAKC,QAAQ;oCACvBkC,KAAKnC,KAAKmC,GAAG;;;2BAjEiBnC,KAAKC,QAAQ;oBAqEnD;;;0BAIJ,MAACmC;gBACCjB,WAAU;gBACVkB,eAAe,CAACC,IAAMjG,mCAAmCiG,GAAG1C;gBAC5D2C,KAAK3C;;kCAEL,KAAC4C;wBAAGrB,WAAU;kCAA+B;;kCAC7C,KAACM;wBAAEN,WAAU;kCAAkC;;kCAI/C,MAACK;wBAAIL,WAAU;;4BACZJ,gCACC,MAAC0B;gCAAStB,WAAU;;kDAClB,KAACuB;wCAAOvB,WAAU;kDAA8B;;oCAC/C/B,eAAe,kBACd,MAACoC;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAY,CAAC,sBAAsB,EAAExD,aAAa,SAAS,CAAC;gDAC5DyD,SAASvE,QAAQvB,WAAW;gDAC5BoE,WAAU;gDACV2B,IAAIpD;gDACJqD,UAAU,CAACT,IAAMzC,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAElC,aAAauF,EAAEU,MAAM,CAACH,OAAO;wDAAC,CAAA;gDAC1EnE,MAAK;;0DAEP,MAAC7B;gDAAMsE,WAAU;gDAAc8B,SAASvD;;oDAAM;kEAE5C,MAAC4B;wDAAKH,WAAU;;4DAA4B;4DAAG/B;4DAAa;;;;;;yCAGhE;oCACDC,qBAAqB,kBACpB,MAACmC;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAY,CAAC,sBAAsB,EAAEvD,mBAAmB,SAAS,CAAC;gDAClEwD,SAASvE,QAAQtB,iBAAiB;gDAClCmE,WAAU;gDACV2B,IAAInD;gDACJoD,UAAU,CAACT,IACTzC,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEjC,mBAAmBsF,EAAEU,MAAM,CAACH,OAAO;wDAAC,CAAA;gDAEjEnE,MAAK;;0DAEP,MAAC7B;gDAAMsE,WAAU;gDAAc8B,SAAStD;;oDAAQ;kEAE9C,MAAC2B;wDAAKH,WAAU;;4DACb;4DAAI;4DACH9B;4DAAmB;;;;;;yCAI3B;;iCAEJ;0CAEF,MAACoD;gCAAStB,WAAU;;kDAClB,KAACuB;wCAAOvB,WAAU;kDAA8B;;kDAChD,MAACK;wCAAIL,WAAU;;0DACb,KAACtE;gDAAMsE,WAAU;gDAA6B8B,SAAS,GAAGxD,IAAI,MAAM,CAAC;0DAAE;;0DAGvE,KAACkD;gDACCC,cAAW;gDACXM,cAAa;gDACb/B,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,MAAM,CAAC;gDAClBsD,UAAU,CAACT,IAAMzC,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEpC,OAAOyF,EAAEU,MAAM,CAACG,KAAK;wDAAC,CAAA;gDAClEC,aAAY;gDACZC,YAAY;gDACZ3E,MAAK;gDACLyE,OAAO7E,QAAQzB,KAAK;;;;;;0CAK1B,MAAC4F;gCAAStB,WAAU;;kDAClB,KAACuB;wCAAOvB,WAAU;kDAA8B;;kDAChD,MAACK;wCAAIL,WAAU;;0DACb,KAACtE;gDAAMsE,WAAU;gDAA6B8B,SAAS,GAAGxD,IAAI,KAAK,CAAC;0DAAE;;0DAGtE,KAACkD;gDACCC,cAAW;gDACXzB,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,KAAK,CAAC;gDACjBsD,UAAU,CAACT,IAAMzC,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEtC,UAAU2F,EAAEU,MAAM,CAACG,KAAK;wDAAC,CAAA;gDACrEzE,MAAK;gDACLyE,OAAO7E,QAAQ3B,QAAQ;;;;kDAG3B,MAAC6E;wCAAIL,WAAU;;0DACb,KAACtE;gDAAMsE,WAAU;gDAA6B8B,SAAS,GAAGxD,IAAI,GAAG,CAAC;0DAAE;;0DAGpE,KAACkD;gDACCC,cAAW;gDACXzB,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,GAAG,CAAC;gDACfsD,UAAU,CAACT,IAAMzC,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAErC,QAAQ0F,EAAEU,MAAM,CAACG,KAAK;wDAAC,CAAA;gDACnEzE,MAAK;gDACLyE,OAAO7E,QAAQ1B,MAAM;;;;;;0CAK3B,MAAC6F;gCAAStB,WAAU;;kDAClB,KAACuB;wCAAOvB,WAAU;kDAA8B;;kDAChD,MAACK;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAW;gDACXC,SAASvE,QAAQxB,KAAK,KAAK;gDAC3BqE,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,UAAU,CAAC;gDACtB6D,MAAM,GAAG7D,IAAI,MAAM,CAAC;gDACpBsD,UAAU,IAAMlD,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEnC,OAAO;wDAAM,CAAA;gDACxD4B,MAAK;;0DAEP,KAAC7B;gDAAMsE,WAAU;gDAAc8B,SAAS,GAAGxD,IAAI,UAAU,CAAC;0DAAE;;;;kDAI9D,MAAC+B;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAW;gDACXC,SAASvE,QAAQxB,KAAK,KAAK;gDAC3BqE,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,WAAW,CAAC;gDACvB6D,MAAM,GAAG7D,IAAI,MAAM,CAAC;gDACpBsD,UAAU,IAAMlD,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEnC,OAAO;wDAAO,CAAA;gDACzD4B,MAAK;;0DAEP,KAAC7B;gDAAMsE,WAAU;gDAAc8B,SAAS,GAAGxD,IAAI,WAAW,CAAC;0DAAE;;;;kDAI/D,MAAC+B;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAW;gDACXC,SAASvE,QAAQxB,KAAK,KAAK;gDAC3BqE,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,cAAc,CAAC;gDAC1B6D,MAAM,GAAG7D,IAAI,MAAM,CAAC;gDACpBsD,UAAU,IAAMlD,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEnC,OAAO;wDAAU,CAAA;gDAC5D4B,MAAK;;0DAEP,KAAC7B;gDAAMsE,WAAU;gDAAc8B,SAAS,GAAGxD,IAAI,cAAc,CAAC;0DAAE;;;;;;0CAMpE,MAACgD;gCAAStB,WAAU;;kDAClB,KAACuB;wCAAOvB,WAAU;kDAA8B;;kDAChD,MAACK;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAW;gDACXC,SAASvE,QAAQrB,MAAM,KAAK;gDAC5BkE,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,WAAW,CAAC;gDACvB6D,MAAM,GAAG7D,IAAI,OAAO,CAAC;gDACrBsD,UAAU,IAAMlD,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEhC,QAAQ;wDAAM,CAAA;gDACzDyB,MAAK;;0DAEP,KAAC7B;gDAAMsE,WAAU;gDAAc8B,SAAS,GAAGxD,IAAI,WAAW,CAAC;0DAAE;;;;kDAI/D,MAAC+B;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAW;gDACXC,SAASvE,QAAQrB,MAAM,KAAK;gDAC5BkE,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,cAAc,CAAC;gDAC1B6D,MAAM,GAAG7D,IAAI,OAAO,CAAC;gDACrBsD,UAAU,IAAMlD,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEhC,QAAQ;wDAAS,CAAA;gDAC5DyB,MAAK;;0DAEP,KAAC7B;gDAAMsE,WAAU;gDAAc8B,SAAS,GAAGxD,IAAI,cAAc,CAAC;0DAAE;;;;kDAIlE,MAAC+B;wCAAIL,WAAU;;0DACb,KAACwB;gDACCC,cAAW;gDACXC,SAASvE,QAAQrB,MAAM,KAAK;gDAC5BkE,WAAU;gDACV2B,IAAI,GAAGrD,IAAI,YAAY,CAAC;gDACxB6D,MAAM,GAAG7D,IAAI,OAAO,CAAC;gDACrBsD,UAAU,IAAMlD,WAAW,CAACZ,IAAO,CAAA;4DAAE,GAAGA,CAAC;4DAAEhC,QAAQ;wDAAO,CAAA;gDAC1DyB,MAAK;;0DAEP,KAAC7B;gDAAMsE,WAAU;gDAAc8B,SAAS,GAAGxD,IAAI,YAAY,CAAC;0DAAE;;;;;;;;kCAMpE,MAAC+B;wBAAIL,WAAU;;0CACb,KAACrF;gCAAOoF,aAAY;gCAAYE,SAASN;gCAAcpC,MAAK;0CAAS;;0CAGrE,KAAC5C;gCAAOsF,SAASR;gCAAmBlC,MAAK;0CAAS;;;;;;;;AAO5D,EAAC"}