@brftech/filex-core 0.43.2 → 0.44.1

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 (55) hide show
  1. package/dist/ArchiveViewer-D_tJxJhJ.js +236 -0
  2. package/dist/ArchiveViewer-D_tJxJhJ.js.map +1 -0
  3. package/dist/{CsvViewer-I0AceHh8.js → CsvViewer-B95fArq0.js} +2 -2
  4. package/dist/{CsvViewer-I0AceHh8.js.map → CsvViewer-B95fArq0.js.map} +1 -1
  5. package/dist/{DrawioViewer-B2vl10hY.js → DrawioViewer-BAaPMTge.js} +2 -2
  6. package/dist/{DrawioViewer-B2vl10hY.js.map → DrawioViewer-BAaPMTge.js.map} +1 -1
  7. package/dist/{EpubViewer-L3rxrx5i.js → EpubViewer-B-B7bRW7.js} +2 -2
  8. package/dist/{EpubViewer-L3rxrx5i.js.map → EpubViewer-B-B7bRW7.js.map} +1 -1
  9. package/dist/{IpynbViewer-Do8n0RzJ.js → IpynbViewer-COOTyOcu.js} +2 -2
  10. package/dist/{IpynbViewer-Do8n0RzJ.js.map → IpynbViewer-COOTyOcu.js.map} +1 -1
  11. package/dist/{MermaidViewer-0JqH6k0f.js → MermaidViewer-CjVCmR3s.js} +2 -2
  12. package/dist/{MermaidViewer-0JqH6k0f.js.map → MermaidViewer-CjVCmR3s.js.map} +1 -1
  13. package/dist/{PsdViewer-BnI-NDr8.js → PsdViewer-xsIYL9Gu.js} +2 -2
  14. package/dist/{PsdViewer-BnI-NDr8.js.map → PsdViewer-xsIYL9Gu.js.map} +1 -1
  15. package/dist/{TiffViewer-CrLDXI_f.js → TiffViewer-CtwnTxqL.js} +2 -2
  16. package/dist/{TiffViewer-CrLDXI_f.js.map → TiffViewer-CtwnTxqL.js.map} +1 -1
  17. package/dist/{Viewer3D-CpB1K0-0.js → Viewer3D-xYMwndEy.js} +2 -2
  18. package/dist/{Viewer3D-CpB1K0-0.js.map → Viewer3D-xYMwndEy.js.map} +1 -1
  19. package/dist/filex-core.js +372 -366
  20. package/dist/filex-core.umd.cjs +62 -62
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/{index-D9_EBsqF.js → index-BBToAjwL.js} +16563 -15727
  23. package/dist/index-BBToAjwL.js.map +1 -0
  24. package/dist/index.d.ts +185 -10
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +363 -23
  28. package/src/components/ContextMenu.vue +17 -0
  29. package/src/components/HomeView.vue +14 -4
  30. package/src/components/InspectorPanel.vue +9 -3
  31. package/src/components/ListView.vue +6 -3
  32. package/src/components/OperationsCenter.vue +6 -0
  33. package/src/components/PendingOpsTray.vue +5 -3
  34. package/src/composables/useFileApi.ts +36 -6
  35. package/src/composables/useLocale.ts +26 -0
  36. package/src/composables/useOperations.ts +7 -1
  37. package/src/composables/usePendingOps.ts +11 -7
  38. package/src/index.ts +15 -0
  39. package/src/lib/actionIcons.ts +6 -0
  40. package/src/lib/archiveFormats.ts +12 -0
  41. package/src/lib/archivePreviewCache.ts +61 -0
  42. package/src/lib/catalogCoverage.ts +137 -0
  43. package/src/locales/en.ts +67 -0
  44. package/src/locales/tr.ts +65 -0
  45. package/src/modals/ArchiveCreateModal.vue +192 -0
  46. package/src/modals/ArchiveExtractModal.vue +57 -0
  47. package/src/modals/ArchivePasswordModal.vue +62 -0
  48. package/src/modals/PreviewModal.vue +15 -1
  49. package/src/styles/base.css +47 -2
  50. package/src/types/ExplorerConfig.ts +9 -0
  51. package/src/types/FileNode.ts +19 -0
  52. package/src/viewers/ArchiveViewer.vue +145 -40
  53. package/dist/ArchiveViewer-Bn-pwgX8.js +0 -149
  54. package/dist/ArchiveViewer-Bn-pwgX8.js.map +0 -1
  55. package/dist/index-D9_EBsqF.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -3507,6 +3507,8 @@ export declare function applyThemeToEl(el: HTMLElement, id: string, dark: boolea
3507
3507
  */
3508
3508
  export declare function applyToAllFolders(view: FolderViewDefault): void;
3509
3509
 
3510
+ declare type ArchiveCreateFormat = 'zip' | '7z' | 'tar' | 'tar.gz' | 'tar.bz2' | 'tar.xz';
3511
+
3510
3512
  export declare interface ArchiveEntry {
3511
3513
  name: string;
3512
3514
  size: number;
@@ -3514,6 +3516,16 @@ export declare interface ArchiveEntry {
3514
3516
  lastModified?: number;
3515
3517
  }
3516
3518
 
3519
+ /**
3520
+ * archiveFormatLabel — how an archive format is written wherever a person
3521
+ * reads it: the create dialog, the admin Archives page, its provider cards.
3522
+ *
3523
+ * Upper case (ZIP, TAR.GZ, RAR), except 7z, which its own project writes in
3524
+ * lower case. One rule in one place: the dialog wrote "7z" while the admin
3525
+ * page wrote "7Z" for the same format (measured, 0.44.0 integration of #48).
3526
+ */
3527
+ export declare function archiveFormatLabel(format: string): string;
3528
+
3517
3529
  /** What the mint returns. Mirrors `archiveDownloadInfo` in the Go handler. */
3518
3530
  export declare interface ArchiveTicket {
3519
3531
  /** Path to navigate to. Server-relative, e.g. `/z/<token>`. */
@@ -3787,6 +3799,16 @@ export declare interface Capabilities {
3787
3799
  /** Longest life a new share link may be given, in days (0 = no ceiling).
3788
3800
  * Read by the share dialogs so they offer only expiries the server keeps. */
3789
3801
  share_max_ttl_days?: number;
3802
+ /** Archive creation policy. Absent on servers older than archive providers.
3803
+ * `allowed_formats` is what this server can actually make (every format
3804
+ * but a plain ZIP needs 7-Zip there); empty means it can make none, and
3805
+ * `encryption` says whether a password can be set. */
3806
+ archive?: {
3807
+ enabled: boolean;
3808
+ default_format: ArchiveCreateFormat | '';
3809
+ allowed_formats: ArchiveCreateFormat[];
3810
+ encryption?: boolean;
3811
+ };
3790
3812
  /** Is the caller a person (`'user'` — a session OR their own API token) or an
3791
3813
  * integration (`'app'` — a host proxy, a bot, an MCP client)? The explorer
3792
3814
  * reads it to decide whether to draw the identity-bearing surfaces; see
@@ -3840,6 +3862,23 @@ export declare interface Capabilities {
3840
3862
  };
3841
3863
  }
3842
3864
 
3865
+ /** The server's `coverage` object (sync.CatalogueCoverage). */
3866
+ export declare interface CatalogCoverage {
3867
+ complete: boolean;
3868
+ reason?: CoverageReason | string;
3869
+ /** `lazy` for a lazily cataloged storage. */
3870
+ mode?: string;
3871
+ /** The lazy behaviour: `background` (A) or `on_open` (B). */
3872
+ fill?: string;
3873
+ catalogued_folders?: number;
3874
+ pending_folders?: number;
3875
+ watched_folders?: number;
3876
+ max_watches?: number;
3877
+ filler?: string;
3878
+ held_back_folders?: number;
3879
+ reconciles?: number;
3880
+ }
3881
+
3843
3882
  export declare const ChoiceButtons: DefineComponent<__VLS_Props_10, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
3844
3883
  "update:modelValue": (v: string | string[]) => any;
3845
3884
  }, string, PublicProps, Readonly<__VLS_Props_10> & Readonly<{
@@ -4108,6 +4147,79 @@ export declare interface ContextAction {
4108
4147
  shortcutId?: string;
4109
4148
  }
4110
4149
 
4150
+ /** `storage_info` → storage name → its incomplete coverage (null = complete). */
4151
+ export declare function coverageByStorage(info: unknown): Record<string, CatalogCoverage | null>;
4152
+
4153
+ /** A coverage object worth telling anybody about, or null. */
4154
+ export declare function coverageIncomplete(c: CatalogCoverage | null | undefined): CatalogCoverage | null;
4155
+
4156
+ /** The sentence one storage's coverage is told in: a catalog key and its values. */
4157
+ export declare function coverageMessage(c: CatalogCoverage): {
4158
+ key: string;
4159
+ vars: Record<string, number>;
4160
+ };
4161
+
4162
+ /** What the banner over a listing or a search says, and whether it offers an action. */
4163
+ export declare interface CoverageNotice {
4164
+ key: string;
4165
+ vars: Record<string, string | number>;
4166
+ /** The storage the notice is about, when it is about one. */
4167
+ storage?: string;
4168
+ /** Offer "Catalog everything" (an administrator, behaviour B). */
4169
+ offerCatalogAll: boolean;
4170
+ }
4171
+
4172
+ /**
4173
+ * The notice for what is on screen.
4174
+ *
4175
+ * - A listing, or a search inside one storage: that storage's coverage.
4176
+ * - A search that spans every storage (the server searches them all when
4177
+ * the query is typed at a storage's root and there is more than one):
4178
+ * the names of the ones not fully cataloged.
4179
+ *
4180
+ * `admin` decides the action only; everybody reads the same sentence.
4181
+ */
4182
+ export declare function coverageNotice(opts: {
4183
+ map: Record<string, CatalogCoverage | null>;
4184
+ adapter: string;
4185
+ searching: boolean;
4186
+ crossStorage: boolean;
4187
+ admin: boolean;
4188
+ }): CoverageNotice | null;
4189
+
4190
+ /**
4191
+ * The share of the folders found so far that are cataloged, 0–99 while any
4192
+ * is still waiting (a floor, so "100%" is never shown beside a notice that
4193
+ * says the work is not done).
4194
+ */
4195
+ export declare function coveragePercent(c: CatalogCoverage): number;
4196
+
4197
+ /**
4198
+ * catalogCoverage — how much of a storage its catalog covers, as the server
4199
+ * says it, and the one set of rules every view draws that by.
4200
+ *
4201
+ * filex keeps a catalog of every storage, and search, folder sizes and drive
4202
+ * usage are answered from it. While it does not cover a storage in full the
4203
+ * server says so (docs/LAZY-CATALOGUE.md → "Coverage in the UI"):
4204
+ *
4205
+ * - `first_scan` — any storage whose first sync has not finished;
4206
+ * - `lazy_filling` — a lazily cataloged storage whose background pass is
4207
+ * still going (behaviour A);
4208
+ * - `lazy_on_open` — a lazily cataloged storage that catalogs only the
4209
+ * folders people open (behaviour B).
4210
+ *
4211
+ * The listing and search responses carry it per storage in `storage_info`,
4212
+ * drive usage per row. A folder row whose own size leaves something out
4213
+ * carries `size_partial` (see useLocale `formatNodeSize`).
4214
+ *
4215
+ * ⚠ One module for the whole explorer: the banner over a listing, the banner
4216
+ * over search results and Home's drive cards read the SAME object through the
4217
+ * SAME rules, so they cannot tell a person two different things about one
4218
+ * storage.
4219
+ */
4220
+ /** Why a storage's catalog is not complete. */
4221
+ export declare type CoverageReason = 'first_scan' | 'lazy_filling' | 'lazy_on_open';
4222
+
4111
4223
  /**
4112
4224
  * A column store over `specs`, reading and writing through `backing`.
4113
4225
  *
@@ -4791,6 +4903,7 @@ export declare interface EndpointMap {
4791
4903
  capabilities: string | null;
4792
4904
  archiveList: string | null;
4793
4905
  archiveExtract: string | null;
4906
+ archiveCreate: string | null;
4794
4907
  archiveAdd: string | null;
4795
4908
  copy: string | null;
4796
4909
  moveAsync: string | null;
@@ -4938,6 +5051,7 @@ export declare interface ExplorerConfig {
4938
5051
  capabilities?: string;
4939
5052
  archiveList?: string;
4940
5053
  archiveExtract?: string;
5054
+ archiveCreate?: string;
4941
5055
  archiveAdd?: string;
4942
5056
  /** Recursive S3-side copy with "-copy" collision suffix (async). */
4943
5057
  copy?: string;
@@ -5460,6 +5574,13 @@ export declare interface ExplorerConfig {
5460
5574
  * about the person under a label about the drive.
5461
5575
  */
5462
5576
  usedBytes?: number;
5577
+ /**
5578
+ * `usedBytes` counts only part of the storage: its catalog does not cover
5579
+ * all of it yet (the server's `coverage` beside the figure —
5580
+ * lib/catalogCoverage). The card then draws it as a lower bound. Absent:
5581
+ * the explorer uses what the last listing said about the storage.
5582
+ */
5583
+ usedPartial?: boolean;
5463
5584
  }>;
5464
5585
  /**
5465
5586
  * Desktop-shell hook — dragging rows OUT of the window onto the OS.
@@ -5629,6 +5750,13 @@ export declare interface FileNode {
5629
5750
  extension?: string;
5630
5751
  /** Bytes. 0 = directory. */
5631
5752
  size?: number;
5753
+ /**
5754
+ * The folder's size leaves something out: the catalog does not cover all of
5755
+ * it yet (a lazily cataloged folder, or any folder while its storage's first
5756
+ * sync runs). `size` is then a lower bound, or unknown when 0. Draw it with
5757
+ * useLocale `formatNodeSize`, never `formatSize` (docs/LAZY-CATALOGUE.md).
5758
+ */
5759
+ size_partial?: boolean;
5632
5760
  /** Unix ms (backend "last_modified"). */
5633
5761
  last_modified?: number;
5634
5762
  /** MIME. */
@@ -6876,6 +7004,10 @@ export declare interface ManagerResponse {
6876
7004
  * list is not the whole answer. Absent on other actions and on servers
6877
7005
  * older than the flag. */
6878
7006
  truncated?: boolean;
7007
+ /** Every storage the caller can open, with how much of it the catalog
7008
+ * covers (`coverage`, absent when all of it — lib/catalogCoverage). Absent
7009
+ * on servers older than the lazy catalog. */
7010
+ storage_info?: StorageInfo[];
6879
7011
  }
6880
7012
 
6881
7013
  /** True when the folder has an operator escrow slot. */
@@ -7237,6 +7369,7 @@ export declare interface Operation {
7237
7369
  error: string | null;
7238
7370
  errorDetail: string | null;
7239
7371
  queued: boolean;
7372
+ cancelling: boolean;
7240
7373
  doneCount: number | null;
7241
7374
  totalCount: number | null;
7242
7375
  uploadedBytes: number | null;
@@ -7276,6 +7409,8 @@ export declare interface OperationInput {
7276
7409
  errorDetail?: string | null;
7277
7410
  /** Queue op accepted but not started yet ("Queued"). */
7278
7411
  queued?: boolean;
7412
+ /** Cancellation was accepted and the worker is stopping/cleaning up. */
7413
+ cancelling?: boolean;
7279
7414
  /** Progress counters for queue ops (3/5 items). */
7280
7415
  doneCount?: number;
7281
7416
  totalCount?: number;
@@ -7326,7 +7461,7 @@ export declare interface OperationInput {
7326
7461
  */
7327
7462
  /** `plugin` — an app-plugin job (docs/APP-PLUGINS-API.md); also what any
7328
7463
  * queue kind this package has no drawing for is shown as. */
7329
- export declare type OperationKind = 'upload' | 'copy' | 'move' | 'delete' | 'convert' | 'archive' | 'plugin' | 'trash';
7464
+ export declare type OperationKind = 'upload' | 'copy' | 'move' | 'delete' | 'convert' | 'archive' | 'archive-create' | 'archive-extract' | 'plugin' | 'trash';
7330
7465
 
7331
7466
  export declare const OperationsCenter: DefineComponent<__VLS_Props_31, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<__VLS_Props_31> & Readonly<{}>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {
7332
7467
  rootEl: HTMLDivElement;
@@ -7604,7 +7739,7 @@ export declare interface PendingOp {
7604
7739
  id: number;
7605
7740
  op_type: PendingOpType;
7606
7741
  /** `cancelled` — somebody stopped it (POST /ops/{id}/cancel). */
7607
- status: 'pending' | 'running' | 'done' | 'error' | 'cancelled';
7742
+ status: 'pending' | 'running' | 'cancelling' | 'done' | 'error' | 'cancelled';
7608
7743
  progress_total: number;
7609
7744
  progress_done: number;
7610
7745
  /** Running cross-storage transfer's bytes (issue #27); absent otherwise. */
@@ -7619,6 +7754,7 @@ export declare interface PendingOp {
7619
7754
  error_code?: string;
7620
7755
  /** The engine a job needed and the server lacks (`error_code: engine_missing`). */
7621
7756
  error_engine?: string;
7757
+ cancellable: boolean;
7622
7758
  started_at: string | null;
7623
7759
  finished_at: string | null;
7624
7760
  created_at: string | null;
@@ -7635,14 +7771,15 @@ export declare interface PendingOp {
7635
7771
  /** Server-side PendingOp DTO (mirror of Modules\FishApp\Models\PendingOp::toApiArray). */
7636
7772
  export declare interface PendingOpDto {
7637
7773
  id: number;
7638
- op_type: 'copy' | 'move' | 'delete';
7639
- status: 'pending' | 'running' | 'done' | 'error';
7774
+ op_type: 'copy' | 'move' | 'delete' | 'archive-create' | 'archive-extract';
7775
+ status: 'pending' | 'running' | 'cancelling' | 'done' | 'error' | 'cancelled';
7640
7776
  progress_total: number;
7641
7777
  progress_done: number;
7642
7778
  target_path: string | null;
7643
7779
  source_dir: string | null;
7644
7780
  source_count: number;
7645
7781
  error_message: string | null;
7782
+ cancellable: boolean;
7646
7783
  started_at: string | null;
7647
7784
  finished_at: string | null;
7648
7785
  created_at: string | null;
@@ -10106,6 +10243,13 @@ export declare const StorageFields: DefineComponent<__VLS_Props_39, {}, {}, {},
10106
10243
  */
10107
10244
  export declare type StorageFieldType = 'string' | 'int' | 'bool' | 'password' | 'select';
10108
10245
 
10246
+ /** One entry of a listing's `storage_info`. */
10247
+ export declare interface StorageInfo {
10248
+ name: string;
10249
+ read_only?: boolean;
10250
+ coverage?: CatalogCoverage | null;
10251
+ }
10252
+
10109
10253
  /** A configured storage, as `GET /api/admin/storages` returns it. */
10110
10254
  export declare interface StorageRow {
10111
10255
  id: number;
@@ -11200,12 +11344,29 @@ export declare function useFileApi(config: ExplorerConfig): {
11200
11344
  revokeShare: (uuid: string) => Promise<{
11201
11345
  success: boolean;
11202
11346
  }>;
11203
- archiveList: (path: string) => Promise<{
11347
+ archiveList: (path: string, password?: string) => Promise<{
11204
11348
  entries: ArchiveEntry[];
11205
11349
  }>;
11206
- archiveExtract: (path: string, members?: string[]) => Promise<{
11207
- keys: string[];
11208
- count: number;
11350
+ archiveExtract: (path: string, options?: {
11351
+ members?: string[];
11352
+ password?: string;
11353
+ dest?: string;
11354
+ }) => Promise<{
11355
+ op?: PendingOpDto;
11356
+ keys?: string[];
11357
+ count?: number;
11358
+ }>;
11359
+ archiveCreate: (payload: {
11360
+ dest: string;
11361
+ sources: string[];
11362
+ format: ArchiveCreateFormat;
11363
+ password?: string;
11364
+ encrypt_filenames?: boolean;
11365
+ compression?: number;
11366
+ solid?: boolean;
11367
+ dictionary_size_mb?: number;
11368
+ }) => Promise<{
11369
+ op: PendingOpDto;
11209
11370
  }>;
11210
11371
  archiveAdd: (path: string, files: Array<{
11211
11372
  name: string;
@@ -11278,6 +11439,14 @@ localeRef: Ref<LocaleCode | string> | (() => LocaleCode | string)): {
11278
11439
  foreign(text: string): string;
11279
11440
  };
11280
11441
  formatSize: (bytes: number | undefined | null) => string;
11442
+ formatNodeSize: (n: {
11443
+ size?: number | null;
11444
+ size_partial?: boolean;
11445
+ } | null | undefined) => string;
11446
+ nodeSizeHint: (n: {
11447
+ size?: number | null;
11448
+ size_partial?: boolean;
11449
+ } | null | undefined) => string | undefined;
11281
11450
  formatDate: (ms: number | undefined | null, opts?: {
11282
11451
  time?: boolean;
11283
11452
  }) => string;
@@ -11398,6 +11567,7 @@ export declare function useOperations(): {
11398
11567
  error: string | null;
11399
11568
  errorDetail: string | null;
11400
11569
  queued: boolean;
11570
+ cancelling: boolean;
11401
11571
  doneCount: number | null;
11402
11572
  totalCount: number | null;
11403
11573
  uploadedBytes: number | null;
@@ -11418,6 +11588,7 @@ export declare function useOperations(): {
11418
11588
  error: string | null;
11419
11589
  errorDetail: string | null;
11420
11590
  queued: boolean;
11591
+ cancelling: boolean;
11421
11592
  doneCount: number | null;
11422
11593
  totalCount: number | null;
11423
11594
  uploadedBytes: number | null;
@@ -11439,6 +11610,7 @@ export declare function useOperations(): {
11439
11610
  error: string | null;
11440
11611
  errorDetail: string | null;
11441
11612
  queued: boolean;
11613
+ cancelling: boolean;
11442
11614
  doneCount: number | null;
11443
11615
  totalCount: number | null;
11444
11616
  uploadedBytes: number | null;
@@ -11459,6 +11631,7 @@ export declare function useOperations(): {
11459
11631
  error: string | null;
11460
11632
  errorDetail: string | null;
11461
11633
  queued: boolean;
11634
+ cancelling: boolean;
11462
11635
  doneCount: number | null;
11463
11636
  totalCount: number | null;
11464
11637
  uploadedBytes: number | null;
@@ -11490,7 +11663,7 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
11490
11663
  ops: Ref< {
11491
11664
  id: number;
11492
11665
  op_type: PendingOpType;
11493
- status: "pending" | "running" | "done" | "error" | "cancelled";
11666
+ status: "pending" | "running" | "cancelling" | "done" | "error" | "cancelled";
11494
11667
  progress_total: number;
11495
11668
  progress_done: number;
11496
11669
  bytes_total?: number | undefined;
@@ -11501,6 +11674,7 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
11501
11674
  error_message: string | null;
11502
11675
  error_code?: string | undefined;
11503
11676
  error_engine?: string | undefined;
11677
+ cancellable: boolean;
11504
11678
  started_at: string | null;
11505
11679
  finished_at: string | null;
11506
11680
  created_at: string | null;
@@ -11514,7 +11688,7 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
11514
11688
  }[], PendingOp[] | {
11515
11689
  id: number;
11516
11690
  op_type: PendingOpType;
11517
- status: "pending" | "running" | "done" | "error" | "cancelled";
11691
+ status: "pending" | "running" | "cancelling" | "done" | "error" | "cancelled";
11518
11692
  progress_total: number;
11519
11693
  progress_done: number;
11520
11694
  bytes_total?: number | undefined;
@@ -11525,6 +11699,7 @@ export declare function usePendingOps(config: ExplorerConfig, api: FileApi, opts
11525
11699
  error_message: string | null;
11526
11700
  error_code?: string | undefined;
11527
11701
  error_engine?: string | undefined;
11702
+ cancellable: boolean;
11528
11703
  started_at: string | null;
11529
11704
  finished_at: string | null;
11530
11705
  created_at: string | null;