@peers-app/peers-ui 0.27.2 → 0.27.4

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 (309) hide show
  1. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -1
  2. package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
  3. package/dist/screens/network-viewer/sync-recovery.js +103 -1
  4. package/dist/screens/variables/variable-details.js +2 -1
  5. package/dist/tabs-layout/network-status-indicator.js +26 -12
  6. package/dist/tabs-layout/use-network-status.d.ts +6 -1
  7. package/dist/tabs-layout/use-network-status.js +27 -4
  8. package/package.json +9 -3
  9. package/.claude/settings.local.json +0 -34
  10. package/.github/README.md +0 -52
  11. package/.github/workflows/delete-all-artifacts.yml +0 -49
  12. package/.github/workflows/publish.yml +0 -51
  13. package/babel.config.js +0 -7
  14. package/biome.json +0 -198
  15. package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
  16. package/dist/app-updates/app-updates-consumer.test.js +0 -56
  17. package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
  18. package/dist/app-updates/app-updates-settings.test.js +0 -93
  19. package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
  20. package/dist/app-updates/remote-app-update-panel.test.js +0 -165
  21. package/dist/components/access-level-field.test.d.ts +0 -1
  22. package/dist/components/access-level-field.test.js +0 -28
  23. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
  24. package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
  25. package/dist/components/responsive-section-nav.test.d.ts +0 -1
  26. package/dist/components/responsive-section-nav.test.js +0 -45
  27. package/dist/globals-hash.test.d.ts +0 -1
  28. package/dist/globals-hash.test.js +0 -16
  29. package/dist/hooks/use-observable-state.test.d.ts +0 -1
  30. package/dist/hooks/use-observable-state.test.js +0 -64
  31. package/dist/hooks/use-observable.test.d.ts +0 -1
  32. package/dist/hooks/use-observable.test.js +0 -191
  33. package/dist/hooks/use-rows.test.d.ts +0 -1
  34. package/dist/hooks/use-rows.test.js +0 -175
  35. package/dist/hooks/use-tool.test.d.ts +0 -1
  36. package/dist/hooks/use-tool.test.js +0 -190
  37. package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
  38. package/dist/host-shell/host-shell-consumer.test.js +0 -28
  39. package/dist/root-layout/root-layout-host.test.d.ts +0 -1
  40. package/dist/root-layout/root-layout-host.test.js +0 -86
  41. package/dist/screens/account/account-screen.test.d.ts +0 -1
  42. package/dist/screens/account/account-screen.test.js +0 -51
  43. package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
  44. package/dist/screens/assistants/assistant-config.test.js +0 -57
  45. package/dist/screens/contacts/contact-list.test.d.ts +0 -1
  46. package/dist/screens/contacts/contact-list.test.js +0 -48
  47. package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
  48. package/dist/screens/device-pairing/device-pairing.test.js +0 -119
  49. package/dist/screens/devices/devices-list.test.d.ts +0 -1
  50. package/dist/screens/devices/devices-list.test.js +0 -53
  51. package/dist/screens/groups/group-list.test.d.ts +0 -1
  52. package/dist/screens/groups/group-list.test.js +0 -38
  53. package/dist/screens/identity/identity-routes.test.d.ts +0 -1
  54. package/dist/screens/identity/identity-routes.test.js +0 -51
  55. package/dist/screens/identity/identity-screen.test.d.ts +0 -1
  56. package/dist/screens/identity/identity-screen.test.js +0 -103
  57. package/dist/screens/invites/accept-invite.test.d.ts +0 -1
  58. package/dist/screens/invites/accept-invite.test.js +0 -103
  59. package/dist/screens/invites/activity-view.test.d.ts +0 -1
  60. package/dist/screens/invites/activity-view.test.js +0 -108
  61. package/dist/screens/invites/invite-classify.test.d.ts +0 -1
  62. package/dist/screens/invites/invite-classify.test.js +0 -115
  63. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
  64. package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
  65. package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
  66. package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
  67. package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
  68. package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
  69. package/dist/screens/packages/package-helpers.test.d.ts +0 -1
  70. package/dist/screens/packages/package-helpers.test.js +0 -53
  71. package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
  72. package/dist/screens/packages/package-runtime-status.test.js +0 -50
  73. package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
  74. package/dist/screens/packages/packages-ipc.test.js +0 -43
  75. package/dist/screens/settings/settings-page.test.d.ts +0 -1
  76. package/dist/screens/settings/settings-page.test.js +0 -19
  77. package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
  78. package/dist/screens/tools/tool-access-ipc.test.js +0 -40
  79. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
  80. package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
  81. package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
  82. package/dist/tabs-layout/network-status-indicator.test.js +0 -665
  83. package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
  84. package/dist/tabs-layout/tabs-state.test.js +0 -110
  85. package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
  86. package/dist/tabs-layout/use-network-status.test.js +0 -45
  87. package/dist/ui-router/ui-loader.test.d.ts +0 -1
  88. package/dist/ui-router/ui-loader.test.js +0 -91
  89. package/jest.config.js +0 -25
  90. package/release-guard.js +0 -10
  91. package/src/app-updates/app-update-controls.tsx +0 -73
  92. package/src/app-updates/app-updates-consumer.test.ts +0 -83
  93. package/src/app-updates/app-updates-consumer.ts +0 -121
  94. package/src/app-updates/app-updates-settings.test.tsx +0 -109
  95. package/src/app-updates/app-updates-settings.tsx +0 -136
  96. package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
  97. package/src/app-updates/remote-app-update-panel.tsx +0 -210
  98. package/src/app-updates/use-app-update-status.ts +0 -16
  99. package/src/command-palette/command-palette-ui.tsx +0 -564
  100. package/src/command-palette/command-palette.ts +0 -242
  101. package/src/components/access-level-field.test.tsx +0 -32
  102. package/src/components/access-level-field.tsx +0 -68
  103. package/src/components/checkbox.tsx +0 -25
  104. package/src/components/group-switcher.tsx +0 -526
  105. package/src/components/input-date.tsx +0 -29
  106. package/src/components/input-number.tsx +0 -83
  107. package/src/components/input.tsx +0 -25
  108. package/src/components/inverse-lazy-list.tsx +0 -107
  109. package/src/components/io-schema-values.tsx +0 -103
  110. package/src/components/io-schema.tsx +0 -259
  111. package/src/components/lazy-list.tsx +0 -115
  112. package/src/components/list-screen.tsx +0 -126
  113. package/src/components/loading-indicator.tsx +0 -7
  114. package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
  115. package/src/components/markdown-editor/editor-inline.tsx +0 -9
  116. package/src/components/markdown-editor/editor.tsx +0 -158
  117. package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
  118. package/src/components/markdown-editor/mention-node.ts +0 -204
  119. package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
  120. package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
  121. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
  122. package/src/components/markdown-editor/theme.ts +0 -46
  123. package/src/components/markdown-editor/toolbar.tsx +0 -269
  124. package/src/components/markdown-with-mentions.tsx +0 -198
  125. package/src/components/message-logs/message-logs.tsx +0 -523
  126. package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
  127. package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
  128. package/src/components/messages/avatar.tsx +0 -171
  129. package/src/components/messages/channel-message-list.tsx +0 -189
  130. package/src/components/messages/channel-view.tsx +0 -92
  131. package/src/components/messages/message-compose.tsx +0 -192
  132. package/src/components/messages/message-display.tsx +0 -231
  133. package/src/components/messages/thread-message-list.tsx +0 -135
  134. package/src/components/qr-scanner.tsx +0 -180
  135. package/src/components/responsive-section-nav.test.tsx +0 -88
  136. package/src/components/responsive-section-nav.tsx +0 -92
  137. package/src/components/router.tsx +0 -175
  138. package/src/components/save-button.tsx +0 -134
  139. package/src/components/sortable-list.tsx +0 -130
  140. package/src/components/tabs.tsx +0 -76
  141. package/src/components/tooltip.tsx +0 -43
  142. package/src/components/trust-level-badge.tsx +0 -44
  143. package/src/components/trust-level-dropdown.tsx +0 -136
  144. package/src/components/typeahead.tsx +0 -357
  145. package/src/globals-hash.test.ts +0 -19
  146. package/src/globals.tsx +0 -226
  147. package/src/hooks/index.ts +0 -8
  148. package/src/hooks/use-host-contract.ts +0 -75
  149. package/src/hooks/use-observable-state.test.tsx +0 -101
  150. package/src/hooks/use-observable-state.ts +0 -29
  151. package/src/hooks/use-observable.test.tsx +0 -258
  152. package/src/hooks/use-observable.ts +0 -67
  153. package/src/hooks/use-on-screen.ts +0 -24
  154. package/src/hooks/use-promise.ts +0 -34
  155. package/src/hooks/use-rows.test.tsx +0 -196
  156. package/src/hooks/use-rows.ts +0 -120
  157. package/src/hooks/use-subscription.ts +0 -24
  158. package/src/hooks/use-tool.test.tsx +0 -225
  159. package/src/hooks/use-tool.ts +0 -116
  160. package/src/host-shell/host-shell-consumer.test.ts +0 -32
  161. package/src/host-shell/host-shell-consumer.ts +0 -53
  162. package/src/index.tsx +0 -42
  163. package/src/mention-configs.ts +0 -185
  164. package/src/operator-console/operator-console-state.ts +0 -119
  165. package/src/operator-console/operator-console.tsx +0 -233
  166. package/src/peers-services-registration.ts +0 -6
  167. package/src/root-layout/global-overlays.ts +0 -50
  168. package/src/root-layout/layout-picker.tsx +0 -47
  169. package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
  170. package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
  171. package/src/root-layout/root-layout-host.test.tsx +0 -108
  172. package/src/root-layout/root-layout-host.tsx +0 -147
  173. package/src/root-layout/root-layout-registry.ts +0 -51
  174. package/src/root-layout/root-layout-state.ts +0 -55
  175. package/src/root-layout/root-layout.ts +0 -32
  176. package/src/screens/account/account-screen.test.tsx +0 -65
  177. package/src/screens/account/account-screen.tsx +0 -942
  178. package/src/screens/assistants/assistant-config.test.tsx +0 -54
  179. package/src/screens/assistants/assistant-config.tsx +0 -121
  180. package/src/screens/assistants/assistant-details.tsx +0 -89
  181. package/src/screens/assistants/assistant-info.tsx +0 -66
  182. package/src/screens/assistants/assistant-list.tsx +0 -145
  183. package/src/screens/assistants/assistant-tools.tsx +0 -82
  184. package/src/screens/console-logs/console-logs-list.tsx +0 -374
  185. package/src/screens/console-logs/log-display.tsx +0 -137
  186. package/src/screens/console-logs/log-filters.tsx +0 -64
  187. package/src/screens/console-logs/mobile-log-card.tsx +0 -161
  188. package/src/screens/console-logs/resizable-table-header.tsx +0 -110
  189. package/src/screens/contacts/contact-details.tsx +0 -123
  190. package/src/screens/contacts/contact-list.test.tsx +0 -59
  191. package/src/screens/contacts/contact-list.tsx +0 -286
  192. package/src/screens/contacts/index.ts +0 -2
  193. package/src/screens/data-explorer/data-explorer.tsx +0 -633
  194. package/src/screens/data-explorer/index.ts +0 -6
  195. package/src/screens/data-explorer/query-executor.tsx +0 -520
  196. package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
  197. package/src/screens/device-pairing/device-pairing.tsx +0 -461
  198. package/src/screens/device-pairing/identity-ipc.ts +0 -25
  199. package/src/screens/device-pairing/index.ts +0 -2
  200. package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
  201. package/src/screens/devices/devices-list.test.tsx +0 -68
  202. package/src/screens/devices/devices-list.tsx +0 -365
  203. package/src/screens/devices/index.ts +0 -1
  204. package/src/screens/groups/group-details.tsx +0 -160
  205. package/src/screens/groups/group-invite-panel.tsx +0 -311
  206. package/src/screens/groups/group-list.test.tsx +0 -44
  207. package/src/screens/groups/group-list.tsx +0 -317
  208. package/src/screens/groups/group-members.tsx +0 -343
  209. package/src/screens/groups/index.ts +0 -3
  210. package/src/screens/identity/identity-routes.test.ts +0 -70
  211. package/src/screens/identity/identity-routes.ts +0 -301
  212. package/src/screens/identity/identity-screen.test.tsx +0 -138
  213. package/src/screens/identity/identity-screen.tsx +0 -282
  214. package/src/screens/identity/index.ts +0 -2
  215. package/src/screens/identity/invite-contact-panel.tsx +0 -45
  216. package/src/screens/identity/profile-invite.tsx +0 -93
  217. package/src/screens/invites/accept-invite.test.tsx +0 -115
  218. package/src/screens/invites/accept-invite.tsx +0 -318
  219. package/src/screens/invites/activity-view.test.tsx +0 -127
  220. package/src/screens/invites/activity-view.tsx +0 -162
  221. package/src/screens/invites/index.ts +0 -6
  222. package/src/screens/invites/invite-capture.ts +0 -104
  223. package/src/screens/invites/invite-classify.test.ts +0 -129
  224. package/src/screens/invites/invite-share.tsx +0 -104
  225. package/src/screens/invites/invites-ipc.ts +0 -25
  226. package/src/screens/invites/pending-invites-list.tsx +0 -256
  227. package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
  228. package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
  229. package/src/screens/network-viewer/device-details-modal.tsx +0 -339
  230. package/src/screens/network-viewer/group-details-modal.tsx +0 -421
  231. package/src/screens/network-viewer/index.ts +0 -8
  232. package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
  233. package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
  234. package/src/screens/network-viewer/network-viewer-section.ts +0 -12
  235. package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
  236. package/src/screens/network-viewer/network-viewer.tsx +0 -708
  237. package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
  238. package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
  239. package/src/screens/network-viewer/sync-recovery.tsx +0 -461
  240. package/src/screens/network-viewer/usage-graph.tsx +0 -455
  241. package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
  242. package/src/screens/packages/local-folders-ipc.ts +0 -28
  243. package/src/screens/packages/package-details.tsx +0 -166
  244. package/src/screens/packages/package-helpers.test.ts +0 -57
  245. package/src/screens/packages/package-helpers.ts +0 -137
  246. package/src/screens/packages/package-info.tsx +0 -354
  247. package/src/screens/packages/package-list.tsx +0 -519
  248. package/src/screens/packages/package-network-panel.tsx +0 -82
  249. package/src/screens/packages/package-new-local.tsx +0 -100
  250. package/src/screens/packages/package-runtime-status.test.tsx +0 -58
  251. package/src/screens/packages/package-runtime-status.tsx +0 -68
  252. package/src/screens/packages/package-versions.tsx +0 -298
  253. package/src/screens/packages/packages-ipc.test.ts +0 -47
  254. package/src/screens/packages/packages-ipc.ts +0 -125
  255. package/src/screens/peer-types/peer-type-details.tsx +0 -172
  256. package/src/screens/peer-types/peer-type-list.tsx +0 -64
  257. package/src/screens/search/global-search.tsx +0 -406
  258. package/src/screens/settings/color-mode-dropdown.tsx +0 -82
  259. package/src/screens/settings/settings-page.test.tsx +0 -24
  260. package/src/screens/settings/settings-page.tsx +0 -426
  261. package/src/screens/setup-user.tsx +0 -355
  262. package/src/screens/tools/tool-access-ipc.test.ts +0 -55
  263. package/src/screens/tools/tool-access-ipc.ts +0 -27
  264. package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
  265. package/src/screens/tools/tool-catalog-ipc.ts +0 -51
  266. package/src/screens/tools/tool-details.tsx +0 -52
  267. package/src/screens/tools/tool-info.tsx +0 -45
  268. package/src/screens/tools/tool-list.tsx +0 -106
  269. package/src/screens/tools/tool-schema.tsx +0 -44
  270. package/src/screens/tools/tool-test-details.tsx +0 -89
  271. package/src/screens/tools/tool-test-list.tsx +0 -73
  272. package/src/screens/variables/variable-details.tsx +0 -289
  273. package/src/screens/variables/variable-list.tsx +0 -110
  274. package/src/screens/welcome-modal.tsx +0 -294
  275. package/src/screens/workflows/workflow-details.tsx +0 -115
  276. package/src/screens/workflows/workflow-info.tsx +0 -22
  277. package/src/screens/workflows/workflow-instructions.tsx +0 -134
  278. package/src/screens/workflows/workflow-list.tsx +0 -117
  279. package/src/setupTests.ts +0 -38
  280. package/src/system-apps/assistants.app.ts +0 -7
  281. package/src/system-apps/console-logs.app.ts +0 -7
  282. package/src/system-apps/data-explorer.app.ts +0 -8
  283. package/src/system-apps/identity.app.ts +0 -9
  284. package/src/system-apps/index.ts +0 -77
  285. package/src/system-apps/mobile-settings.app.ts +0 -7
  286. package/src/system-apps/network-viewer.app.ts +0 -8
  287. package/src/system-apps/packages.app.ts +0 -7
  288. package/src/system-apps/search.app.ts +0 -8
  289. package/src/system-apps/settings.app.ts +0 -7
  290. package/src/system-apps/threads.app.ts +0 -7
  291. package/src/system-apps/tools.app.ts +0 -7
  292. package/src/system-apps/types.app.ts +0 -7
  293. package/src/system-apps/variables.app.ts +0 -7
  294. package/src/system-apps/workflows.app.ts +0 -7
  295. package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
  296. package/src/tabs-layout/network-status-indicator.tsx +0 -756
  297. package/src/tabs-layout/tabs-layout.tsx +0 -857
  298. package/src/tabs-layout/tabs-state.test.ts +0 -152
  299. package/src/tabs-layout/tabs-state.ts +0 -587
  300. package/src/tabs-layout/use-network-status.test.tsx +0 -58
  301. package/src/tabs-layout/use-network-status.ts +0 -68
  302. package/src/ui-defaults/index.ts +0 -2
  303. package/src/ui-defaults/list-screen.tsx +0 -94
  304. package/src/ui-defaults/markdown-field.tsx +0 -87
  305. package/src/ui-router/routes-loader.ts +0 -193
  306. package/src/ui-router/ui-loader.test.tsx +0 -108
  307. package/src/ui-router/ui-loader.tsx +0 -606
  308. package/src/utils.ts +0 -95
  309. package/tsconfig.json +0 -18
@@ -1,461 +0,0 @@
1
- import type { INetworkSyncRecoveryEntry, SyncRecoveryResolution } from "@peers-app/peers-sdk";
2
- import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
3
- import { Popover } from "react-tiny-popover";
4
- import { LoadingIndicator } from "../../components/loading-indicator";
5
- import { getNetworkControlConsumer, getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
6
-
7
- /** Props for {@link SyncRecoveryScreen}. */
8
- interface ISyncRecoveryScreenProps {
9
- /** Return to the Network Viewer overview. */
10
- onBack: () => void;
11
- }
12
-
13
- function formatWhen(timestamp: number): string {
14
- return new Date(timestamp).toLocaleString();
15
- }
16
-
17
- function statusLabel(entry: INetworkSyncRecoveryEntry): string {
18
- if (entry.status === "retrying") {
19
- return "Retrying";
20
- }
21
- if (entry.parkedReason === "schema") {
22
- return "Parked, schema";
23
- }
24
- if (entry.parkedReason === "unique-conflict") {
25
- return "Parked, conflict";
26
- }
27
- if (entry.parkedReason === "unresolvable") {
28
- return "Parked, needs a decision";
29
- }
30
- return "Parked";
31
- }
32
-
33
- function statusExplanation(entry: INetworkSyncRecoveryEntry): string {
34
- if (entry.status === "retrying") {
35
- return "This device is still asking others for a copy it can write. The wait starts at one minute and doubles after each try, up to one hour. It is marked as needing attention after 5 tries spanning at least 10 minutes.";
36
- }
37
- if (entry.parkedReason === "schema") {
38
- return "The saved change does not match this device's rules for the table — a missing field, an empty id, or an id in the wrong format. Every device has the same change, so asking them again will not help. It is tried again from history once each time this device starts, when a newer change arrives, and when you press Retry. After an upgrade that accepts the row, the next startup or Retry can write it.";
39
- }
40
- if (entry.parkedReason === "unique-conflict") {
41
- return "Two records want the same unique value, such as one variable name. This table has no automatic way to choose a winner, so nothing is deleted until you pick an action.";
42
- }
43
- if (entry.parkedReason === "unresolvable") {
44
- return "An automatic fix could not be applied safely — the other record was missing, more than one row matched, or the write failed. The rows were left as they are. Choose an action when you know which version should remain.";
45
- }
46
- return "This entry is waiting and is not being sent to other devices. Retry tries it again. The other actions clear it.";
47
- }
48
-
49
- /** Show a validation payload as indented JSON when the error ends with one. */
50
- function formatError(error: string): string {
51
- const marker = error.search(/[[{]/);
52
- if (marker < 0) {
53
- return error;
54
- }
55
- const head = error.slice(0, marker).trimEnd();
56
- try {
57
- return `${head}\n${JSON.stringify(JSON.parse(error.slice(marker)), null, 2)}`;
58
- } catch {
59
- return error;
60
- }
61
- }
62
-
63
- const detailsBlockStyle = {
64
- maxHeight: "16.8em",
65
- lineHeight: 1.4,
66
- overflow: "auto",
67
- whiteSpace: "pre-wrap",
68
- wordBreak: "break-word",
69
- } as const;
70
-
71
- function errorSummary(error: string): string {
72
- const line = error.split("\n")[0] ?? error;
73
- return line.length > 80 ? `${line.slice(0, 77)}…` : line;
74
- }
75
-
76
- function InfoTip({ label, children }: { label: string; children: ReactNode }) {
77
- const [open, setOpen] = useState(false);
78
- return (
79
- <Popover
80
- isOpen={open}
81
- positions={["bottom", "top", "left", "right"]}
82
- align="start"
83
- onClickOutside={() => setOpen(false)}
84
- content={() => (
85
- <div
86
- className="card p-2 shadow small"
87
- style={{ maxWidth: 420, zIndex: 1000001, textAlign: "left" }}
88
- >
89
- {children}
90
- </div>
91
- )}
92
- >
93
- <button
94
- type="button"
95
- className="btn btn-link btn-sm p-0 ms-1 align-baseline text-info"
96
- aria-label={label}
97
- onClick={() => setOpen((current) => !current)}
98
- >
99
- <i className="bi bi-info-circle" />
100
- </button>
101
- </Popover>
102
- );
103
- }
104
-
105
- function RecoveryRows({
106
- entry,
107
- busy,
108
- onRun,
109
- }: {
110
- entry: INetworkSyncRecoveryEntry;
111
- busy: boolean;
112
- onRun: (entry: INetworkSyncRecoveryEntry, action: "retry" | SyncRecoveryResolution) => void;
113
- }) {
114
- const [recordOpen, setRecordOpen] = useState(false);
115
- const [errorOpen, setErrorOpen] = useState(false);
116
- return (
117
- <>
118
- <tr>
119
- <td>{entry.tableName}</td>
120
- <td>
121
- <code title={entry.recordId}>{entry.recordId}</code>
122
- {entry.recordSummary && (
123
- <div className="small text-muted" style={{ maxWidth: 220 }}>
124
- {entry.recordSummary}
125
- </div>
126
- )}
127
- <button
128
- type="button"
129
- className="btn btn-link btn-sm p-0"
130
- aria-label={recordOpen ? "Hide record details" : "Show record details"}
131
- onClick={() => setRecordOpen((current) => !current)}
132
- >
133
- {recordOpen ? "Hide details" : "Details"}
134
- </button>
135
- </td>
136
- <td className="text-nowrap small">{formatWhen(entry.firstFailedAt)}</td>
137
- <td>{entry.attemptCount}</td>
138
- <td className="text-nowrap">
139
- {statusLabel(entry)}
140
- <InfoTip label={`About status ${statusLabel(entry)}`}>{statusExplanation(entry)}</InfoTip>
141
- </td>
142
- <td>
143
- <div className="small text-muted" style={{ maxWidth: 220 }}>
144
- {errorSummary(entry.latestError)}
145
- </div>
146
- <button
147
- type="button"
148
- className="btn btn-link btn-sm p-0"
149
- aria-label={errorOpen ? "Hide error details" : "Show error details"}
150
- onClick={() => setErrorOpen((current) => !current)}
151
- >
152
- {errorOpen ? "Hide details" : "Details"}
153
- </button>
154
- </td>
155
- <td>
156
- <div className="d-flex flex-column gap-1">
157
- <button
158
- type="button"
159
- className="btn btn-sm btn-outline-primary"
160
- disabled={busy}
161
- onClick={() => onRun(entry, "retry")}
162
- >
163
- Retry
164
- </button>
165
- <button
166
- type="button"
167
- className="btn btn-sm btn-outline-primary"
168
- disabled={busy || !entry.localRowExists}
169
- title={
170
- entry.localRowExists
171
- ? "Write this device's row as a snapshot every device will adopt"
172
- : "This device has no row to keep"
173
- }
174
- onClick={() => onRun(entry, "keepLocal")}
175
- >
176
- Keep this device's version
177
- </button>
178
- <button
179
- type="button"
180
- className="btn btn-sm btn-outline-danger"
181
- disabled={busy}
182
- onClick={() => onRun(entry, "deleteEverywhere")}
183
- >
184
- Delete everywhere
185
- </button>
186
- <button
187
- type="button"
188
- className="btn btn-sm btn-outline-secondary"
189
- disabled={busy}
190
- title="Removes the entry on this device only. Other devices keep theirs."
191
- onClick={() => onRun(entry, "ignoreLocal")}
192
- >
193
- Ignore on this device
194
- </button>
195
- </div>
196
- </td>
197
- </tr>
198
- {(errorOpen || recordOpen) && (
199
- <tr>
200
- <td colSpan={7} className="bg-body-tertiary">
201
- {errorOpen && (
202
- <pre className="small mb-0 font-monospace" style={detailsBlockStyle}>
203
- {formatError(entry.latestError)}
204
- </pre>
205
- )}
206
- {recordOpen && (
207
- <div className={errorOpen ? "mt-3" : undefined}>
208
- {entry.changeCreatedAt != null && (
209
- <p className="small mb-2">Change written {formatWhen(entry.changeCreatedAt)}</p>
210
- )}
211
- {entry.conflictingRecordId && (
212
- <p className="small mb-2">
213
- Another record holds the same unique value:{" "}
214
- <code>{entry.conflictingRecordId}</code>
215
- </p>
216
- )}
217
- {entry.tombstoneRecordId &&
218
- entry.tombstoneRecordId === entry.conflictingRecordId && (
219
- <p className="small mb-2">
220
- Keep deletes <code>{entry.tombstoneRecordId}</code> on every device.
221
- </p>
222
- )}
223
- {entry.tombstoneRecordId && entry.tombstoneRecordId === entry.recordId && (
224
- <p className="small mb-2">
225
- Keep snapshots the other record and tombstones this queued id.
226
- </p>
227
- )}
228
- {entry.keptRecordPreview && (
229
- <>
230
- <p className="small mb-1">This device's version</p>
231
- <pre className="small mb-2 font-monospace" style={detailsBlockStyle}>
232
- {entry.keptRecordPreview}
233
- </pre>
234
- </>
235
- )}
236
- {entry.queuedRecordPreview && (
237
- <>
238
- <p className="small mb-1">Queued change</p>
239
- <pre className="small mb-0 font-monospace" style={detailsBlockStyle}>
240
- {entry.queuedRecordPreview}
241
- </pre>
242
- </>
243
- )}
244
- {!entry.keptRecordPreview && !entry.queuedRecordPreview && entry.recordPreview && (
245
- <pre className="small mb-0 font-monospace" style={detailsBlockStyle}>
246
- {entry.recordPreview}
247
- </pre>
248
- )}
249
- {!entry.keptRecordPreview && !entry.queuedRecordPreview && !entry.recordPreview && (
250
- <p className="small text-muted mb-0">
251
- This device has no stored copy of the record. The latest error may still quote
252
- the rejected value.
253
- </p>
254
- )}
255
- </div>
256
- )}
257
- </td>
258
- </tr>
259
- )}
260
- </>
261
- );
262
- }
263
-
264
- /** Lists recovery-queue entries and the actions that resolve them. */
265
- export function SyncRecoveryScreen({ onBack }: ISyncRecoveryScreenProps) {
266
- const [entries, setEntries] = useState<INetworkSyncRecoveryEntry[]>([]);
267
- const [loading, setLoading] = useState(true);
268
- const [busyKey, setBusyKey] = useState<string | null>(null);
269
- const [error, setError] = useState<string | null>(null);
270
- const loadGeneration = useRef(0);
271
-
272
- const load = useCallback(async (preserveError = false) => {
273
- const generation = ++loadGeneration.current;
274
- try {
275
- const diagnostics = await getNetworkDiagnosticsConsumer();
276
- const result = await diagnostics.tools.getSyncRecoveryEntries();
277
- if (generation !== loadGeneration.current) {
278
- return;
279
- }
280
- setEntries(result.entries);
281
- if (!preserveError) {
282
- setError(null);
283
- }
284
- } catch (loadError) {
285
- if (generation !== loadGeneration.current) {
286
- return;
287
- }
288
- setError(loadError instanceof Error ? loadError.message : String(loadError));
289
- } finally {
290
- if (generation === loadGeneration.current) {
291
- setLoading(false);
292
- }
293
- }
294
- }, []);
295
-
296
- useEffect(() => {
297
- void load();
298
- }, [load]);
299
-
300
- useEffect(() => {
301
- let active = true;
302
- let subscription: { dispose(): void } | undefined;
303
- void getNetworkDiagnosticsConsumer().then((diagnostics) => {
304
- if (!active) {
305
- return;
306
- }
307
- subscription = diagnostics.observables?.status.subscribe(() => {
308
- void load();
309
- });
310
- });
311
- return () => {
312
- active = false;
313
- subscription?.dispose();
314
- };
315
- }, [load]);
316
-
317
- const run = async (
318
- entry: INetworkSyncRecoveryEntry,
319
- action: "retry" | SyncRecoveryResolution,
320
- ) => {
321
- if (action === "deleteEverywhere") {
322
- const confirmed = window.confirm(
323
- `Delete ${entry.tableName}/${entry.recordId} on every device? This writes a synced delete.`,
324
- );
325
- if (!confirmed) {
326
- return;
327
- }
328
- }
329
- const key = `${entry.dataContextId}:${entry.syncRecoveryId}:${action}`;
330
- setBusyKey(key);
331
- setError(null);
332
- try {
333
- const control = await getNetworkControlConsumer();
334
- const result =
335
- action === "retry"
336
- ? await control.tools.retrySyncRecovery({
337
- dataContextId: entry.dataContextId,
338
- tableName: entry.tableName,
339
- recordId: entry.recordId,
340
- syncRecoveryId: entry.syncRecoveryId,
341
- })
342
- : await control.tools.resolveSyncRecovery({
343
- dataContextId: entry.dataContextId,
344
- tableName: entry.tableName,
345
- recordId: entry.recordId,
346
- resolution: action,
347
- syncRecoveryId: entry.syncRecoveryId,
348
- });
349
- if (!result.success) {
350
- setError(result.message);
351
- await load(true);
352
- } else {
353
- setError(null);
354
- await load();
355
- }
356
- } catch (actionError) {
357
- setError(actionError instanceof Error ? actionError.message : String(actionError));
358
- } finally {
359
- setBusyKey(null);
360
- }
361
- };
362
-
363
- if (loading) {
364
- return <LoadingIndicator />;
365
- }
366
-
367
- const groups = new Map<string, INetworkSyncRecoveryEntry[]>();
368
- for (const entry of entries) {
369
- const group = groups.get(entry.dataContextId) ?? [];
370
- group.push(entry);
371
- groups.set(entry.dataContextId, group);
372
- }
373
-
374
- return (
375
- <div className="container-fluid p-4">
376
- <div className="d-flex justify-content-between align-items-center mb-3">
377
- <div className="d-flex align-items-center gap-3">
378
- <button type="button" className="btn btn-sm btn-outline-secondary" onClick={onBack}>
379
- <i className="bi bi-arrow-left me-1" />
380
- Back
381
- </button>
382
- <div>
383
- <h2 className="mb-1">Sync recovery</h2>
384
- <p className="text-muted small mb-0">
385
- These records have change history, but this device could not write the row. Parked
386
- entries are not asked of other devices until you retry them.
387
- </p>
388
- </div>
389
- </div>
390
- <button type="button" className="btn btn-sm btn-primary" onClick={() => void load()}>
391
- Refresh
392
- </button>
393
- </div>
394
- {error && <div className="alert alert-danger py-2 small">{error}</div>}
395
- {entries.length === 0 ? (
396
- <div className="alert alert-success">No records are waiting in the recovery queue.</div>
397
- ) : (
398
- [...groups.entries()].map(([dataContextId, groupEntries]) => (
399
- <div key={dataContextId} className="card mb-3">
400
- <div className="card-header">{groupEntries[0]?.dataContextName || dataContextId}</div>
401
- <div className="table-responsive">
402
- <table className="table table-sm align-middle mb-0">
403
- <thead>
404
- <tr>
405
- <th>Table</th>
406
- <th>Record</th>
407
- <th>First failed</th>
408
- <th>Attempts</th>
409
- <th>Status</th>
410
- <th>Latest error</th>
411
- <th>
412
- Actions
413
- <InfoTip label="About these actions">
414
- <ul className="mb-0 ps-3">
415
- <li>
416
- <strong>Retry</strong> writes the record again from this device's
417
- history, then asks another device once. If a schema or unique-key entry
418
- is still queued after that, it is parked again. Use it after an upgrade
419
- that might accept a row this device rejected before.
420
- </li>
421
- <li>
422
- <strong>Keep this device's version</strong> snapshots the row this
423
- device will keep. That is the queued row when it is already stored here,
424
- and otherwise the other row that holds the same unique value. When both
425
- rows are stored, the other record is deleted everywhere. When only the
426
- other row is stored, that row is kept and this queued id is tombstoned.
427
- Disabled when this device has no row to keep.
428
- </li>
429
- <li>
430
- <strong>Delete everywhere</strong> deletes this record on every device,
431
- including devices that already have it.
432
- </li>
433
- <li>
434
- <strong>Ignore on this device</strong> removes the entry here only.
435
- Other devices keep their own copy of the problem.
436
- </li>
437
- </ul>
438
- </InfoTip>
439
- </th>
440
- </tr>
441
- </thead>
442
- <tbody>
443
- {groupEntries.map((entry) => (
444
- <RecoveryRows
445
- key={entry.syncRecoveryId}
446
- entry={entry}
447
- busy={Boolean(
448
- busyKey?.startsWith(`${entry.dataContextId}:${entry.syncRecoveryId}:`),
449
- )}
450
- onRun={(item, action) => void run(item, action)}
451
- />
452
- ))}
453
- </tbody>
454
- </table>
455
- </div>
456
- </div>
457
- ))
458
- )}
459
- </div>
460
- );
461
- }