@brftech/filex-core 0.42.1 → 0.43.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 (233) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5241 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1207 -343
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +47 -17
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -56
  76. package/src/components/SSHKeysPanel.vue +79 -26
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -27
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +121 -0
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +522 -119
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +539 -155
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2895 -241
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +58 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-CLNevfPE.js +0 -124
  216. package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
  217. package/dist/CsvViewer-K8XA3z7N.js +0 -141
  218. package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
  219. package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
  220. package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
  221. package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
  222. package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
  223. package/dist/MermaidViewer-DqucH_io.js +0 -137
  224. package/dist/MermaidViewer-DqucH_io.js.map +0 -1
  225. package/dist/PsdViewer-CwiVENmq.js +0 -122
  226. package/dist/PsdViewer-CwiVENmq.js.map +0 -1
  227. package/dist/TiffViewer-CpewSVmQ.js +0 -142
  228. package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
  229. package/dist/Viewer3D-CIrOn63I.js.map +0 -1
  230. package/dist/index-Dx0IbBcI.js +0 -26831
  231. package/dist/index-Dx0IbBcI.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
@@ -23,6 +23,13 @@
23
23
 
24
24
  import type { ExplorerConfig, AuthConfig, EndpointMap } from '../types/ExplorerConfig';
25
25
  import { resolveLocale } from '../locales/resolve';
26
+ import { listingAddress } from '../lib/internalPaths';
27
+ import { localeTag } from './useLocale';
28
+ import { networkFailure, requestFailure } from '../lib/errorWords';
29
+ // ⚠ The same folding rule the web app's axios layer applies, applied by the
30
+ // request layer an EMBED uses — one notice for a connection that is down, and
31
+ // no surface-specific copy of the decision (lib/connection).
32
+ import { kindOfMethod, noteRequestFailed, noteRequestSucceeded } from '../lib/connection';
26
33
  import type {
27
34
  FileNode,
28
35
  ShareInfo,
@@ -31,6 +38,13 @@ import type {
31
38
  ArchiveEntry,
32
39
  TrashEntry,
33
40
  } from '../types/FileNode';
41
+ import type {
42
+ PluginActionsResponse,
43
+ PluginRunResult,
44
+ PluginSurface,
45
+ PluginUsersResponse,
46
+ PluginViewEventBody,
47
+ } from '../types/Plugins';
34
48
 
35
49
  /** Server-side PendingOp DTO (mirror of Modules\FishApp\Models\PendingOp::toApiArray). */
36
50
  export interface PendingOpDto {
@@ -75,6 +89,11 @@ export interface ManagerResponse {
75
89
  e2e?: boolean;
76
90
  e2e_root?: string;
77
91
  files: FileNode[];
92
+ /** `action=search` only: more rows matched than came back — the index
93
+ * filled its page, or the index-less fallback filled its window — so the
94
+ * list is not the whole answer. Absent on other actions and on servers
95
+ * older than the flag. */
96
+ truncated?: boolean;
78
97
  }
79
98
 
80
99
  /** A single ACL grant row (RBAC permissions panel). */
@@ -86,6 +105,8 @@ export interface Grant {
86
105
  level: 'viewer' | 'editor' | 'owner';
87
106
  user_email?: string;
88
107
  user_display_name?: string;
108
+ /** The person as every screen names them (server model.PersonLabel). */
109
+ user_name?: string;
89
110
  inherited?: boolean;
90
111
  }
91
112
 
@@ -107,13 +128,14 @@ export interface PermissionsResponse {
107
128
 
108
129
  export interface ResolveEmailResponse {
109
130
  found: boolean;
110
- user?: { id: number; email: string; display_name: string; role: string };
131
+ user?: { id: number; email: string; display_name: string; username?: string; role: string };
111
132
  }
112
133
 
113
134
  export interface UserSuggestion {
114
135
  id: number;
115
136
  email: string;
116
137
  display_name: string;
138
+ username?: string;
117
139
  role: string;
118
140
  }
119
141
 
@@ -249,6 +271,13 @@ export function resolveEndpoints(config: ExplorerConfig): EndpointMap {
249
271
  /* wiring:e2 — escrow proof-of-possession, then the owner is told. */
250
272
  e2eEscrowChallenge: derive(config.e2eEscrowChallenge, '/api/files/e2e/escrow/challenge'),
251
273
  e2eEscrowUsed: derive(config.e2eEscrowUsed, '/api/files/e2e/escrow/used'),
274
+ /* App plugins — docs/APP-PLUGINS-API.md. */
275
+ pluginActions: derive(config.pluginActions, '/api/files/plugins/actions'),
276
+ pluginActionRun: derive(config.pluginActionRun, '/api/files/plugins/actions/{plugin}/{action}/run'),
277
+ pluginView: derive(config.pluginView, '/api/files/plugins/views/{plugin}/{view}'),
278
+ pluginViewEvent: derive(config.pluginViewEvent, '/api/files/plugins/views/{plugin}/{view}/event'),
279
+ pluginUsers: derive(config.pluginUsers, '/api/files/plugins/users'),
280
+ opsCancel: derive(config.opsCancel, '/api/files/ops/{id}/cancel'),
252
281
  };
253
282
  }
254
283
 
@@ -346,47 +375,52 @@ export function useFileApi(config: ExplorerConfig) {
346
375
  return authConf.kind === 'csrf' ? 'include' : 'same-origin';
347
376
  }
348
377
 
349
- // Map an HTTP status to a short, human-readable message in the explorer's
350
- // locale. The raw JSON body is attached as `.detail` for debugging but never
351
- // shown in the toast (Ada, translated from Turkish: "when it gives a 404/403
352
- // or whatever, I see raw json").
353
- function statusMessage(status: number): string {
354
- const tr = resolveLocale(config.locale) !== 'en';
355
- const m: Record<number, [string, string]> = {
356
- 400: ['Geçersiz istek', 'Bad request'],
357
- 401: ['Oturum gerekli, tekrar giriş yapın', 'Sign-in required'],
358
- 403: ['Bu işlem için yetkiniz yok', 'You are not allowed to do this'],
359
- 404: ['Bulunamadı', 'Not found'],
360
- 409: ['Zaten var / çakışma', 'Already exists / conflict'],
361
- 413: ['Dosya çok büyük', 'File too large'],
362
- 415: ['Bu dosya türü desteklenmiyor', 'Unsupported file type'],
363
- 422: ['Geçersiz veri', 'Invalid data'],
364
- 429: ['Çok fazla istek, biraz bekleyin', 'Too many requests'],
365
- 500: ['Sunucu hatası', 'Server error'],
366
- 501: ['Bu işlem desteklenmiyor', 'Not supported'],
367
- 503: ['Servis şu an kullanılamıyor', 'Service unavailable'],
368
- };
369
- const e = m[status];
370
- if (e) return tr ? e[0] : e[1];
371
- return tr ? `Hata (${status})` : `Error (${status})`;
372
- }
378
+ /* ⚠⚠ Every refusal this client meets is said by lib/errorWords — ONE table
379
+ of status words and refusal codes for every screen (a read-only drive,
380
+ a missing encryption key, a quota, the statuses). It used to live here
381
+ as an inline table and was copied, differently, into each component that
382
+ called `fetch` itself; those printed "save failed: 500 {…}" and
383
+ "Config fetch 503: {…}". The raw body rides along as `.detail`, for an
384
+ administrator's second line and for `lockedRefusal`, never the sentence. */
385
+ const lang = () => resolveLocale(config.locale);
373
386
 
374
387
  async function jsonFetch<T>(url: string, init: RequestInit = {}): Promise<T> {
375
388
  const headers = {
389
+ // ⚠⚠ The language on SCREEN, not the one the browser was installed in.
390
+ // Everything the server writes for a person — a plugin's surface, the
391
+ // labels inside it, the message on a queued job — is rendered with the
392
+ // request's `Accept-Language`. Chrome sends `en-US` whatever filex is
393
+ // set to, so a Turkish window asked a signing wizard for its screens
394
+ // and got "Identity" and an English help line in the middle of Turkish
395
+ // ones. The window's own language is the only right answer here.
396
+ 'Accept-Language': localeTag(resolveLocale(config.locale)),
376
397
  ...(await authHeaders()),
377
398
  ...((init.headers as Record<string, string> | undefined) ?? {}),
378
399
  };
379
- const res = await fetch(url, {
380
- ...init,
381
- headers,
382
- credentials: credentialsMode(),
383
- });
400
+ let res: Response;
401
+ try {
402
+ res = await fetch(url, {
403
+ ...init,
404
+ headers,
405
+ credentials: credentialsMode(),
406
+ });
407
+ } catch (e) {
408
+ // An abort is the caller's own decision — handed back untouched, callers
409
+ // branch on `name === 'AbortError'`.
410
+ if ((e as Error)?.name === 'AbortError') throw e;
411
+ // No answer at all: said as a network failure, not as the browser's
412
+ // "TypeError: Failed to fetch".
413
+ // ⚠ Told to the shared notice as well, so an embedded explorer folds a
414
+ // storm of failed listings and thumbnails the same way the panel does.
415
+ noteRequestFailed(kindOfMethod(init.method));
416
+ throw networkFailure(lang(), e);
417
+ }
418
+ // Any answer — a 404 and a 500 included — means the connection is not
419
+ // what is wrong, and the shared notice must not keep saying it is.
420
+ noteRequestSucceeded();
384
421
  if (!res.ok) {
385
422
  const text = await res.text().catch(() => '');
386
- const err = new Error(statusMessage(res.status)) as Error & { status?: number; detail?: string };
387
- err.status = res.status;
388
- err.detail = text.slice(0, 300);
389
- throw err;
423
+ throw requestFailure(res.status, text, lang());
390
424
  }
391
425
  // ⚠⚠ A 204 carries NO BODY, and several endpoints answer with one (every
392
426
  // delete does). Parsing it throws "Unexpected end of JSON input" AFTER the
@@ -454,12 +488,22 @@ export function useFileApi(config: ExplorerConfig) {
454
488
  return `${endpoints.manager}${sep}${qs({ q: action, action, ...params })}`;
455
489
  }
456
490
 
491
+ // ⚠⚠ A listing of one of filex's own directories is answered with the
492
+ // storage's root instead (lib/internalPaths → listingAddress). The server
493
+ // refuses the trash/versions/thumbs trees outright, but it has to keep
494
+ // serving `.filex-open` by exact path — every desktop release since 0.29.0
495
+ // reads its working copies that way — so the explorer is the only place
496
+ // that can tell "a person navigated here" from "the desktop is checking
497
+ // its copy". The explorer arrives wherever the response's `dirname` says,
498
+ // so a stale hash, an old tab or a notification from before this release
499
+ // lands in the storage instead of in the machinery (measured 2026-09-21:
500
+ // `#docs/.filex-open` opened a folder of working copies).
457
501
  async function index(path: string): Promise<ManagerResponse> {
458
- return jsonFetch<ManagerResponse>(managerUrl('index', { path }));
502
+ return jsonFetch<ManagerResponse>(managerUrl('index', { path: listingAddress(path) }));
459
503
  }
460
504
 
461
505
  async function search(path: string, filter: string): Promise<ManagerResponse> {
462
- return jsonFetch<ManagerResponse>(managerUrl('search', { path, filter }));
506
+ return jsonFetch<ManagerResponse>(managerUrl('search', { path: listingAddress(path), filter }));
463
507
  }
464
508
 
465
509
  /* === bul:s3 — global "search everywhere" ===
@@ -675,6 +719,7 @@ export function useFileApi(config: ExplorerConfig) {
675
719
  }
676
720
  };
677
721
  xhr.onload = () => {
722
+ noteRequestSucceeded();
678
723
  if (xhr.status >= 200 && xhr.status < 300) {
679
724
  try {
680
725
  resolve(JSON.parse(xhr.responseText));
@@ -682,10 +727,16 @@ export function useFileApi(config: ExplorerConfig) {
682
727
  reject(e);
683
728
  }
684
729
  } else {
685
- reject(new Error(`${xhr.status} ${xhr.statusText}: ${xhr.responseText.slice(0, 200)}`));
730
+ reject(requestFailure(xhr.status, xhr.responseText || '', lang()));
686
731
  }
687
732
  };
688
- xhr.onerror = () => reject(new Error('Network error'));
733
+ // ⚠ `action`, not `background`: an upload is something the person
734
+ // started and is waiting on, so it is NEVER folded into the shared
735
+ // notice — it says for itself that it did not go.
736
+ xhr.onerror = () => {
737
+ noteRequestFailed('action');
738
+ reject(networkFailure(lang()));
739
+ };
689
740
  xhr.send(fd);
690
741
  });
691
742
  }
@@ -729,9 +780,7 @@ export function useFileApi(config: ExplorerConfig) {
729
780
  });
730
781
  if (!res.ok) {
731
782
  const text = await res.text().catch(() => '');
732
- throw new Error(
733
- `${res.status} ${res.statusText}${text ? ' — ' + text.slice(0, 200) : ''}`,
734
- );
783
+ throw requestFailure(res.status, text, lang());
735
784
  }
736
785
  const blob = await res.blob();
737
786
  const mime = blob.type || res.headers.get('content-type') || '';
@@ -752,9 +801,7 @@ export function useFileApi(config: ExplorerConfig) {
752
801
  });
753
802
  if (!res.ok) {
754
803
  const text = await res.text().catch(() => '');
755
- throw new Error(
756
- `${res.status} ${res.statusText}${text ? ' — ' + text.slice(0, 200) : ''}`,
757
- );
804
+ throw requestFailure(res.status, text, lang());
758
805
  }
759
806
  return res.arrayBuffer();
760
807
  }
@@ -784,6 +831,91 @@ export function useFileApi(config: ExplorerConfig) {
784
831
  return jsonFetch<Capabilities>(endpoints.capabilities);
785
832
  }
786
833
 
834
+ /* ── App plugins (docs/APP-PLUGINS-API.md) ─────────────────────────── */
835
+
836
+ /** Fill `{name}` placeholders of an endpoint template. */
837
+ function fillTemplate(tpl: string, vars: Record<string, string | number>): string {
838
+ return Object.entries(vars).reduce(
839
+ (acc, [k, v]) => acc.replaceAll(`{${k}}`, encodeURIComponent(String(v))),
840
+ tpl,
841
+ );
842
+ }
843
+
844
+ /** `GET /api/files/plugins/actions` — what applies to the caller. */
845
+ async function pluginActions(): Promise<PluginActionsResponse> {
846
+ if (!endpoints.pluginActions) return { actions: [], views: [] };
847
+ const res = await jsonFetch<Partial<PluginActionsResponse>>(endpoints.pluginActions);
848
+ return { actions: res?.actions ?? [], views: res?.views ?? [] };
849
+ }
850
+
851
+ /**
852
+ * `POST …/actions/{plugin}/{action}/run`. `paths` are adapter-qualified
853
+ * wire paths (`docs://reports/nda.pdf`) — the same form copy/move send.
854
+ * Answers `{op}` (queued) or `{surface}` (the action opens a view first).
855
+ */
856
+ async function pluginActionRun(
857
+ plugin: string,
858
+ action: string,
859
+ body: { paths: string[]; params?: Record<string, unknown> },
860
+ ): Promise<PluginRunResult> {
861
+ if (!endpoints.pluginActionRun) throw new Error('pluginActionRun endpoint not configured');
862
+ return jsonFetch<PluginRunResult>(fillTemplate(endpoints.pluginActionRun, { plugin, action }), {
863
+ method: 'POST',
864
+ headers: { 'Content-Type': 'application/json' },
865
+ body: JSON.stringify(body),
866
+ });
867
+ }
868
+
869
+ /** `GET …/views/{plugin}/{view}?path=` — the initial surface (event `open`). */
870
+ /**
871
+ * `GET …/views/{plugin}/{view}` — a view's opening surface. `section` opens
872
+ * a home page at one of its sections (`surface.sections`): the frame keeps
873
+ * it in its own address, so Back and a link land where they were.
874
+ */
875
+ async function pluginView(
876
+ plugin: string,
877
+ view: string,
878
+ path?: string,
879
+ section?: string,
880
+ ): Promise<{ surface: PluginSurface }> {
881
+ if (!endpoints.pluginView) throw new Error('pluginView endpoint not configured');
882
+ const url = fillTemplate(endpoints.pluginView, { plugin, view });
883
+ const q = [
884
+ path ? `path=${encodeURIComponent(path)}` : '',
885
+ section ? `section=${encodeURIComponent(section)}` : '',
886
+ ].filter(Boolean);
887
+ return jsonFetch<{ surface: PluginSurface }>(url + (q.length ? `?${q.join('&')}` : ''));
888
+ }
889
+
890
+ /** `POST …/views/{plugin}/{view}/event` — answers a surface, or `{op}` when it enqueued a job. */
891
+ async function pluginViewEvent(plugin: string, view: string, body: PluginViewEventBody): Promise<PluginRunResult> {
892
+ if (!endpoints.pluginViewEvent) throw new Error('pluginViewEvent endpoint not configured');
893
+ return jsonFetch<PluginRunResult>(fillTemplate(endpoints.pluginViewEvent, { plugin, view }), {
894
+ method: 'POST',
895
+ headers: { 'Content-Type': 'application/json' },
896
+ body: JSON.stringify(body),
897
+ });
898
+ }
899
+
900
+ /**
901
+ * `GET …/plugins/users?plugin=<name>&q=` — the people-picker's lookup of
902
+ * internal users. Answers only for a plugin that holds `users:lookup`;
903
+ * 403 (and 404 on an older server) mean "free e-mail entry only" and the
904
+ * caller reads the status off the thrown error.
905
+ */
906
+ async function pluginUsers(plugin: string, q: string): Promise<PluginUsersResponse> {
907
+ if (!endpoints.pluginUsers) throw Object.assign(new Error('pluginUsers endpoint not configured'), { status: 404 });
908
+ const url = `${endpoints.pluginUsers}?plugin=${encodeURIComponent(plugin)}&q=${encodeURIComponent(q)}`;
909
+ const res = await jsonFetch<Partial<PluginUsersResponse>>(url);
910
+ return { users: res?.users ?? [] };
911
+ }
912
+
913
+ /** `POST /api/files/ops/{id}/cancel`. */
914
+ async function opsCancel(id: number): Promise<void> {
915
+ if (!endpoints.opsCancel) throw new Error('opsCancel endpoint not configured');
916
+ await jsonFetch<unknown>(fillTemplate(endpoints.opsCancel, { id }), { method: 'POST' });
917
+ }
918
+
787
919
  /* wiring:e2 — escrow use is announced, not merely performed.
788
920
  *
789
921
  * Ask the server for a nonce sealed to the escrow public key; only the
@@ -1003,6 +1135,13 @@ export function useFileApi(config: ExplorerConfig) {
1003
1135
  /* wiring:e2 */
1004
1136
  e2eEscrowChallenge,
1005
1137
  e2eEscrowUsed,
1138
+ /* App plugins */
1139
+ pluginActions,
1140
+ pluginActionRun,
1141
+ pluginView,
1142
+ pluginViewEvent,
1143
+ pluginUsers,
1144
+ opsCancel,
1006
1145
  createShare,
1007
1146
  listShares,
1008
1147
  revokeShare,