@peers-app/peers-ui 0.27.3 → 0.27.5

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 (311) hide show
  1. package/dist/screens/network-viewer/group-details-modal.d.ts +4 -0
  2. package/dist/screens/network-viewer/group-details-modal.js +6 -46
  3. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -2
  4. package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
  5. package/dist/screens/network-viewer/sync-recovery.js +103 -1
  6. package/dist/screens/variables/variable-details.js +2 -1
  7. package/dist/tabs-layout/network-status-indicator.js +26 -12
  8. package/dist/tabs-layout/use-network-status.d.ts +6 -1
  9. package/dist/tabs-layout/use-network-status.js +27 -4
  10. package/package.json +9 -3
  11. package/.claude/settings.local.json +0 -34
  12. package/.github/README.md +0 -52
  13. package/.github/workflows/delete-all-artifacts.yml +0 -49
  14. package/.github/workflows/publish.yml +0 -51
  15. package/babel.config.js +0 -7
  16. package/biome.json +0 -198
  17. package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
  18. package/dist/app-updates/app-updates-consumer.test.js +0 -56
  19. package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
  20. package/dist/app-updates/app-updates-settings.test.js +0 -93
  21. package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
  22. package/dist/app-updates/remote-app-update-panel.test.js +0 -165
  23. package/dist/components/access-level-field.test.d.ts +0 -1
  24. package/dist/components/access-level-field.test.js +0 -28
  25. package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
  26. package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
  27. package/dist/components/responsive-section-nav.test.d.ts +0 -1
  28. package/dist/components/responsive-section-nav.test.js +0 -45
  29. package/dist/globals-hash.test.d.ts +0 -1
  30. package/dist/globals-hash.test.js +0 -16
  31. package/dist/hooks/use-observable-state.test.d.ts +0 -1
  32. package/dist/hooks/use-observable-state.test.js +0 -64
  33. package/dist/hooks/use-observable.test.d.ts +0 -1
  34. package/dist/hooks/use-observable.test.js +0 -191
  35. package/dist/hooks/use-rows.test.d.ts +0 -1
  36. package/dist/hooks/use-rows.test.js +0 -175
  37. package/dist/hooks/use-tool.test.d.ts +0 -1
  38. package/dist/hooks/use-tool.test.js +0 -190
  39. package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
  40. package/dist/host-shell/host-shell-consumer.test.js +0 -28
  41. package/dist/root-layout/root-layout-host.test.d.ts +0 -1
  42. package/dist/root-layout/root-layout-host.test.js +0 -86
  43. package/dist/screens/account/account-screen.test.d.ts +0 -1
  44. package/dist/screens/account/account-screen.test.js +0 -51
  45. package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
  46. package/dist/screens/assistants/assistant-config.test.js +0 -57
  47. package/dist/screens/contacts/contact-list.test.d.ts +0 -1
  48. package/dist/screens/contacts/contact-list.test.js +0 -48
  49. package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
  50. package/dist/screens/device-pairing/device-pairing.test.js +0 -119
  51. package/dist/screens/devices/devices-list.test.d.ts +0 -1
  52. package/dist/screens/devices/devices-list.test.js +0 -53
  53. package/dist/screens/groups/group-list.test.d.ts +0 -1
  54. package/dist/screens/groups/group-list.test.js +0 -38
  55. package/dist/screens/identity/identity-routes.test.d.ts +0 -1
  56. package/dist/screens/identity/identity-routes.test.js +0 -51
  57. package/dist/screens/identity/identity-screen.test.d.ts +0 -1
  58. package/dist/screens/identity/identity-screen.test.js +0 -103
  59. package/dist/screens/invites/accept-invite.test.d.ts +0 -1
  60. package/dist/screens/invites/accept-invite.test.js +0 -103
  61. package/dist/screens/invites/activity-view.test.d.ts +0 -1
  62. package/dist/screens/invites/activity-view.test.js +0 -108
  63. package/dist/screens/invites/invite-classify.test.d.ts +0 -1
  64. package/dist/screens/invites/invite-classify.test.js +0 -115
  65. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
  66. package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
  67. package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
  68. package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
  69. package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
  70. package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
  71. package/dist/screens/packages/package-helpers.test.d.ts +0 -1
  72. package/dist/screens/packages/package-helpers.test.js +0 -53
  73. package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
  74. package/dist/screens/packages/package-runtime-status.test.js +0 -50
  75. package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
  76. package/dist/screens/packages/packages-ipc.test.js +0 -43
  77. package/dist/screens/settings/settings-page.test.d.ts +0 -1
  78. package/dist/screens/settings/settings-page.test.js +0 -19
  79. package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
  80. package/dist/screens/tools/tool-access-ipc.test.js +0 -40
  81. package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
  82. package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
  83. package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
  84. package/dist/tabs-layout/network-status-indicator.test.js +0 -665
  85. package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
  86. package/dist/tabs-layout/tabs-state.test.js +0 -110
  87. package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
  88. package/dist/tabs-layout/use-network-status.test.js +0 -45
  89. package/dist/ui-router/ui-loader.test.d.ts +0 -1
  90. package/dist/ui-router/ui-loader.test.js +0 -91
  91. package/jest.config.js +0 -25
  92. package/release-guard.js +0 -10
  93. package/src/app-updates/app-update-controls.tsx +0 -73
  94. package/src/app-updates/app-updates-consumer.test.ts +0 -83
  95. package/src/app-updates/app-updates-consumer.ts +0 -121
  96. package/src/app-updates/app-updates-settings.test.tsx +0 -109
  97. package/src/app-updates/app-updates-settings.tsx +0 -136
  98. package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
  99. package/src/app-updates/remote-app-update-panel.tsx +0 -210
  100. package/src/app-updates/use-app-update-status.ts +0 -16
  101. package/src/command-palette/command-palette-ui.tsx +0 -564
  102. package/src/command-palette/command-palette.ts +0 -242
  103. package/src/components/access-level-field.test.tsx +0 -32
  104. package/src/components/access-level-field.tsx +0 -68
  105. package/src/components/checkbox.tsx +0 -25
  106. package/src/components/group-switcher.tsx +0 -526
  107. package/src/components/input-date.tsx +0 -29
  108. package/src/components/input-number.tsx +0 -83
  109. package/src/components/input.tsx +0 -25
  110. package/src/components/inverse-lazy-list.tsx +0 -107
  111. package/src/components/io-schema-values.tsx +0 -103
  112. package/src/components/io-schema.tsx +0 -259
  113. package/src/components/lazy-list.tsx +0 -115
  114. package/src/components/list-screen.tsx +0 -126
  115. package/src/components/loading-indicator.tsx +0 -7
  116. package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
  117. package/src/components/markdown-editor/editor-inline.tsx +0 -9
  118. package/src/components/markdown-editor/editor.tsx +0 -158
  119. package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
  120. package/src/components/markdown-editor/mention-node.ts +0 -204
  121. package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
  122. package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
  123. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
  124. package/src/components/markdown-editor/theme.ts +0 -46
  125. package/src/components/markdown-editor/toolbar.tsx +0 -269
  126. package/src/components/markdown-with-mentions.tsx +0 -198
  127. package/src/components/message-logs/message-logs.tsx +0 -523
  128. package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
  129. package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
  130. package/src/components/messages/avatar.tsx +0 -171
  131. package/src/components/messages/channel-message-list.tsx +0 -189
  132. package/src/components/messages/channel-view.tsx +0 -92
  133. package/src/components/messages/message-compose.tsx +0 -192
  134. package/src/components/messages/message-display.tsx +0 -231
  135. package/src/components/messages/thread-message-list.tsx +0 -135
  136. package/src/components/qr-scanner.tsx +0 -180
  137. package/src/components/responsive-section-nav.test.tsx +0 -88
  138. package/src/components/responsive-section-nav.tsx +0 -92
  139. package/src/components/router.tsx +0 -175
  140. package/src/components/save-button.tsx +0 -134
  141. package/src/components/sortable-list.tsx +0 -130
  142. package/src/components/tabs.tsx +0 -76
  143. package/src/components/tooltip.tsx +0 -43
  144. package/src/components/trust-level-badge.tsx +0 -44
  145. package/src/components/trust-level-dropdown.tsx +0 -136
  146. package/src/components/typeahead.tsx +0 -357
  147. package/src/globals-hash.test.ts +0 -19
  148. package/src/globals.tsx +0 -226
  149. package/src/hooks/index.ts +0 -8
  150. package/src/hooks/use-host-contract.ts +0 -75
  151. package/src/hooks/use-observable-state.test.tsx +0 -101
  152. package/src/hooks/use-observable-state.ts +0 -29
  153. package/src/hooks/use-observable.test.tsx +0 -258
  154. package/src/hooks/use-observable.ts +0 -67
  155. package/src/hooks/use-on-screen.ts +0 -24
  156. package/src/hooks/use-promise.ts +0 -34
  157. package/src/hooks/use-rows.test.tsx +0 -196
  158. package/src/hooks/use-rows.ts +0 -120
  159. package/src/hooks/use-subscription.ts +0 -24
  160. package/src/hooks/use-tool.test.tsx +0 -225
  161. package/src/hooks/use-tool.ts +0 -116
  162. package/src/host-shell/host-shell-consumer.test.ts +0 -32
  163. package/src/host-shell/host-shell-consumer.ts +0 -53
  164. package/src/index.tsx +0 -42
  165. package/src/mention-configs.ts +0 -185
  166. package/src/operator-console/operator-console-state.ts +0 -119
  167. package/src/operator-console/operator-console.tsx +0 -233
  168. package/src/peers-services-registration.ts +0 -6
  169. package/src/root-layout/global-overlays.ts +0 -50
  170. package/src/root-layout/layout-picker.tsx +0 -47
  171. package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
  172. package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
  173. package/src/root-layout/root-layout-host.test.tsx +0 -108
  174. package/src/root-layout/root-layout-host.tsx +0 -147
  175. package/src/root-layout/root-layout-registry.ts +0 -51
  176. package/src/root-layout/root-layout-state.ts +0 -55
  177. package/src/root-layout/root-layout.ts +0 -32
  178. package/src/screens/account/account-screen.test.tsx +0 -65
  179. package/src/screens/account/account-screen.tsx +0 -942
  180. package/src/screens/assistants/assistant-config.test.tsx +0 -54
  181. package/src/screens/assistants/assistant-config.tsx +0 -121
  182. package/src/screens/assistants/assistant-details.tsx +0 -89
  183. package/src/screens/assistants/assistant-info.tsx +0 -66
  184. package/src/screens/assistants/assistant-list.tsx +0 -145
  185. package/src/screens/assistants/assistant-tools.tsx +0 -82
  186. package/src/screens/console-logs/console-logs-list.tsx +0 -374
  187. package/src/screens/console-logs/log-display.tsx +0 -137
  188. package/src/screens/console-logs/log-filters.tsx +0 -64
  189. package/src/screens/console-logs/mobile-log-card.tsx +0 -161
  190. package/src/screens/console-logs/resizable-table-header.tsx +0 -110
  191. package/src/screens/contacts/contact-details.tsx +0 -123
  192. package/src/screens/contacts/contact-list.test.tsx +0 -59
  193. package/src/screens/contacts/contact-list.tsx +0 -286
  194. package/src/screens/contacts/index.ts +0 -2
  195. package/src/screens/data-explorer/data-explorer.tsx +0 -633
  196. package/src/screens/data-explorer/index.ts +0 -6
  197. package/src/screens/data-explorer/query-executor.tsx +0 -520
  198. package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
  199. package/src/screens/device-pairing/device-pairing.tsx +0 -461
  200. package/src/screens/device-pairing/identity-ipc.ts +0 -25
  201. package/src/screens/device-pairing/index.ts +0 -2
  202. package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
  203. package/src/screens/devices/devices-list.test.tsx +0 -68
  204. package/src/screens/devices/devices-list.tsx +0 -365
  205. package/src/screens/devices/index.ts +0 -1
  206. package/src/screens/groups/group-details.tsx +0 -160
  207. package/src/screens/groups/group-invite-panel.tsx +0 -311
  208. package/src/screens/groups/group-list.test.tsx +0 -44
  209. package/src/screens/groups/group-list.tsx +0 -317
  210. package/src/screens/groups/group-members.tsx +0 -343
  211. package/src/screens/groups/index.ts +0 -3
  212. package/src/screens/identity/identity-routes.test.ts +0 -70
  213. package/src/screens/identity/identity-routes.ts +0 -301
  214. package/src/screens/identity/identity-screen.test.tsx +0 -138
  215. package/src/screens/identity/identity-screen.tsx +0 -282
  216. package/src/screens/identity/index.ts +0 -2
  217. package/src/screens/identity/invite-contact-panel.tsx +0 -45
  218. package/src/screens/identity/profile-invite.tsx +0 -93
  219. package/src/screens/invites/accept-invite.test.tsx +0 -115
  220. package/src/screens/invites/accept-invite.tsx +0 -318
  221. package/src/screens/invites/activity-view.test.tsx +0 -127
  222. package/src/screens/invites/activity-view.tsx +0 -162
  223. package/src/screens/invites/index.ts +0 -6
  224. package/src/screens/invites/invite-capture.ts +0 -104
  225. package/src/screens/invites/invite-classify.test.ts +0 -129
  226. package/src/screens/invites/invite-share.tsx +0 -104
  227. package/src/screens/invites/invites-ipc.ts +0 -25
  228. package/src/screens/invites/pending-invites-list.tsx +0 -256
  229. package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
  230. package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
  231. package/src/screens/network-viewer/device-details-modal.tsx +0 -339
  232. package/src/screens/network-viewer/group-details-modal.tsx +0 -421
  233. package/src/screens/network-viewer/index.ts +0 -8
  234. package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
  235. package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
  236. package/src/screens/network-viewer/network-viewer-section.ts +0 -12
  237. package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
  238. package/src/screens/network-viewer/network-viewer.tsx +0 -708
  239. package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
  240. package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
  241. package/src/screens/network-viewer/sync-recovery.tsx +0 -461
  242. package/src/screens/network-viewer/usage-graph.tsx +0 -455
  243. package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
  244. package/src/screens/packages/local-folders-ipc.ts +0 -28
  245. package/src/screens/packages/package-details.tsx +0 -166
  246. package/src/screens/packages/package-helpers.test.ts +0 -57
  247. package/src/screens/packages/package-helpers.ts +0 -137
  248. package/src/screens/packages/package-info.tsx +0 -354
  249. package/src/screens/packages/package-list.tsx +0 -519
  250. package/src/screens/packages/package-network-panel.tsx +0 -82
  251. package/src/screens/packages/package-new-local.tsx +0 -100
  252. package/src/screens/packages/package-runtime-status.test.tsx +0 -58
  253. package/src/screens/packages/package-runtime-status.tsx +0 -68
  254. package/src/screens/packages/package-versions.tsx +0 -298
  255. package/src/screens/packages/packages-ipc.test.ts +0 -47
  256. package/src/screens/packages/packages-ipc.ts +0 -125
  257. package/src/screens/peer-types/peer-type-details.tsx +0 -172
  258. package/src/screens/peer-types/peer-type-list.tsx +0 -64
  259. package/src/screens/search/global-search.tsx +0 -406
  260. package/src/screens/settings/color-mode-dropdown.tsx +0 -82
  261. package/src/screens/settings/settings-page.test.tsx +0 -24
  262. package/src/screens/settings/settings-page.tsx +0 -426
  263. package/src/screens/setup-user.tsx +0 -355
  264. package/src/screens/tools/tool-access-ipc.test.ts +0 -55
  265. package/src/screens/tools/tool-access-ipc.ts +0 -27
  266. package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
  267. package/src/screens/tools/tool-catalog-ipc.ts +0 -51
  268. package/src/screens/tools/tool-details.tsx +0 -52
  269. package/src/screens/tools/tool-info.tsx +0 -45
  270. package/src/screens/tools/tool-list.tsx +0 -106
  271. package/src/screens/tools/tool-schema.tsx +0 -44
  272. package/src/screens/tools/tool-test-details.tsx +0 -89
  273. package/src/screens/tools/tool-test-list.tsx +0 -73
  274. package/src/screens/variables/variable-details.tsx +0 -289
  275. package/src/screens/variables/variable-list.tsx +0 -110
  276. package/src/screens/welcome-modal.tsx +0 -294
  277. package/src/screens/workflows/workflow-details.tsx +0 -115
  278. package/src/screens/workflows/workflow-info.tsx +0 -22
  279. package/src/screens/workflows/workflow-instructions.tsx +0 -134
  280. package/src/screens/workflows/workflow-list.tsx +0 -117
  281. package/src/setupTests.ts +0 -38
  282. package/src/system-apps/assistants.app.ts +0 -7
  283. package/src/system-apps/console-logs.app.ts +0 -7
  284. package/src/system-apps/data-explorer.app.ts +0 -8
  285. package/src/system-apps/identity.app.ts +0 -9
  286. package/src/system-apps/index.ts +0 -77
  287. package/src/system-apps/mobile-settings.app.ts +0 -7
  288. package/src/system-apps/network-viewer.app.ts +0 -8
  289. package/src/system-apps/packages.app.ts +0 -7
  290. package/src/system-apps/search.app.ts +0 -8
  291. package/src/system-apps/settings.app.ts +0 -7
  292. package/src/system-apps/threads.app.ts +0 -7
  293. package/src/system-apps/tools.app.ts +0 -7
  294. package/src/system-apps/types.app.ts +0 -7
  295. package/src/system-apps/variables.app.ts +0 -7
  296. package/src/system-apps/workflows.app.ts +0 -7
  297. package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
  298. package/src/tabs-layout/network-status-indicator.tsx +0 -756
  299. package/src/tabs-layout/tabs-layout.tsx +0 -857
  300. package/src/tabs-layout/tabs-state.test.ts +0 -152
  301. package/src/tabs-layout/tabs-state.ts +0 -587
  302. package/src/tabs-layout/use-network-status.test.tsx +0 -58
  303. package/src/tabs-layout/use-network-status.ts +0 -68
  304. package/src/ui-defaults/index.ts +0 -2
  305. package/src/ui-defaults/list-screen.tsx +0 -94
  306. package/src/ui-defaults/markdown-field.tsx +0 -87
  307. package/src/ui-router/routes-loader.ts +0 -193
  308. package/src/ui-router/ui-loader.test.tsx +0 -108
  309. package/src/ui-router/ui-loader.tsx +0 -606
  310. package/src/utils.ts +0 -95
  311. package/tsconfig.json +0 -18
@@ -2,5 +2,9 @@ interface IGroupDetailsModalProps {
2
2
  groupId: string;
3
3
  onClose: () => void;
4
4
  }
5
+ /**
6
+ * Network Viewer dialog for one personal or group context: its details and members, and the
7
+ * Sync now and Do full sync actions.
8
+ */
5
9
  export declare function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps): import("react/jsx-runtime").JSX.Element;
6
10
  export {};
@@ -7,26 +7,22 @@ const react_1 = require("react");
7
7
  const loading_indicator_1 = require("../../components/loading-indicator");
8
8
  const trust_level_badge_1 = require("../../components/trust-level-badge");
9
9
  const network_viewer_ipc_1 = require("./network-viewer-ipc");
10
+ /**
11
+ * Network Viewer dialog for one personal or group context: its details and members, and the
12
+ * Sync now and Do full sync actions.
13
+ */
10
14
  function GroupDetailsModal({ groupId, onClose }) {
11
15
  const [group, setGroup] = (0, react_1.useState)(null);
12
- const [syncInfo, setSyncInfo] = (0, react_1.useState)(null);
13
16
  const [loading, setLoading] = (0, react_1.useState)(true);
14
17
  const [notFound, setNotFound] = (0, react_1.useState)(false);
15
- const [downloading, setDownloading] = (0, react_1.useState)(false);
16
- const [downloadProgress, setDownloadProgress] = (0, react_1.useState)(null);
17
- const [downloadResult, setDownloadResult] = (0, react_1.useState)(null);
18
18
  const [fullSyncing, setFullSyncing] = (0, react_1.useState)(false);
19
19
  const [fullSyncResult, setFullSyncResult] = (0, react_1.useState)(null);
20
20
  const [syncingNow, setSyncingNow] = (0, react_1.useState)(false);
21
21
  const [syncNowResult, setSyncNowResult] = (0, react_1.useState)(null);
22
- const platformApi = (0, network_viewer_ipc_1.getNetworkViewerPlatformApi)();
23
22
  const loadData = (0, react_1.useCallback)(async () => {
24
23
  try {
25
24
  const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
26
- const [{ group: groupDetails }, { groups }] = await Promise.all([
27
- diagnostics.tools.getGroupDetails({ groupId }),
28
- diagnostics.tools.getGroupSyncStatus({ groupId }),
29
- ]);
25
+ const { group: groupDetails } = await diagnostics.tools.getGroupDetails({ groupId });
30
26
  if (!groupDetails) {
31
27
  setNotFound(true);
32
28
  }
@@ -34,9 +30,6 @@ function GroupDetailsModal({ groupId, onClose }) {
34
30
  setGroup(groupDetails);
35
31
  setNotFound(false);
36
32
  }
37
- if (groups.length > 0) {
38
- setSyncInfo(groups[0]);
39
- }
40
33
  }
41
34
  catch (error) {
42
35
  console.error("Error loading group details:", error);
@@ -46,37 +39,6 @@ function GroupDetailsModal({ groupId, onClose }) {
46
39
  setLoading(false);
47
40
  }
48
41
  }, [groupId]);
49
- const handleDownloadDatabase = async (deviceId) => {
50
- try {
51
- setDownloading(true);
52
- setDownloadProgress("Preparing download...");
53
- setDownloadResult(null);
54
- if (!platformApi?.downloadRemoteDatabase) {
55
- setDownloadResult({
56
- success: false,
57
- message: "Download API not available",
58
- });
59
- return;
60
- }
61
- setDownloadProgress("Downloading database...");
62
- const result = (await platformApi.downloadRemoteDatabase(groupId, deviceId));
63
- setDownloadResult(result);
64
- if (result.success) {
65
- setDownloadProgress(null);
66
- }
67
- }
68
- catch (error) {
69
- console.error("Error downloading database:", error);
70
- setDownloadResult({
71
- success: false,
72
- message: error instanceof Error ? error.message : "Download failed",
73
- });
74
- }
75
- finally {
76
- setDownloading(false);
77
- setDownloadProgress(null);
78
- }
79
- };
80
42
  const handleSyncNow = async () => {
81
43
  setSyncingNow(true);
82
44
  setSyncNowResult(null);
@@ -131,7 +93,5 @@ function GroupDetailsModal({ groupId, onClose }) {
131
93
  overflowY: "auto",
132
94
  paddingTop: "2rem",
133
95
  paddingBottom: "2rem",
134
- }, onClick: onClose, children: (0, jsx_runtime_1.jsxs)("div", { className: "card", style: { maxWidth: "900px", width: "90%", maxHeight: "90vh" }, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "card-header d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0", children: loading ? "Loading..." : group?.groupName || "Group Details" }), group && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: (0, jsx_runtime_1.jsx)("code", { children: group.groupId }) }))] }), (0, jsx_runtime_1.jsx)("button", { className: "btn-close", onClick: onClose, "aria-label": "Close" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "card-body", style: { overflowY: "auto", maxHeight: "calc(90vh - 120px)" }, children: [loading && (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}), !loading && (notFound || !group) && ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning", children: "Group not found." })), !loading && group && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { className: "border-bottom pb-2 mb-3", children: "Group Information" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Group ID:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: group.groupId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Group Name:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: group.groupName })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Founder:" }), (0, jsx_runtime_1.jsx)("br", {}), group.founderUserName ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { children: group.founderUserName }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: group.founderUserId })] })) : ((0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: group.founderUserId }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Total Members:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { className: "h5", children: group.members.length })] })] })] }), group.members.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "border-bottom pb-2 mb-3", children: ["Members (", group.members.length, ")"] }), (0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-sm table-hover", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "User" }), (0, jsx_runtime_1.jsx)("th", { children: "User ID" }), (0, jsx_runtime_1.jsx)("th", { children: "Role" }), (0, jsx_runtime_1.jsx)("th", { children: "Trust Level" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: group.members.map((member) => ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { children: member.userName || (0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "-" }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: member.userId }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("span", { className: "badge bg-primary", children: member.role }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: member.trustLevel || peers_sdk_1.TrustLevel.Unknown }) })] }, member.userId))) })] }) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "border-bottom pb-2 mb-3", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-repeat me-2" }), "Synchronization"] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-muted small mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Sync now" }), " notifies connected peers that this context may have changes. ", (0, jsx_runtime_1.jsx)("strong", { children: "Do full sync" }), " resets change tracking for this context and rebuilds synchronization state; that may transfer substantial data and should be used only to repair suspected drift."] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary", onClick: () => void handleSyncNow(), disabled: syncingNow || fullSyncing, children: syncingNow ? "Sending sync notification..." : "Sync now" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-warning", onClick: () => void handleFullSync(), disabled: fullSyncing || syncingNow, children: fullSyncing ? "Starting full sync..." : "Do full sync" })] }), syncNowResult && ((0, jsx_runtime_1.jsx)("div", { className: `alert mt-3 mb-0 ${syncNowResult.success ? "alert-success" : "alert-danger"}`, role: "status", children: syncNowResult.message })), fullSyncResult && ((0, jsx_runtime_1.jsx)("div", { className: `alert mt-3 mb-0 ${fullSyncResult.success ? "alert-success" : "alert-danger"}`, role: "status", children: fullSyncResult.message }))] }), platformApi?.downloadRemoteDatabase ? ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "border-bottom pb-2 mb-3", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-download me-2" }), "Download Database (Fast Sync)"] }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted small mb-3", children: "Download the entire database from a connected device. This is useful for fast initial sync on a new device. The database will be saved as a backup file." }), syncInfo && syncInfo.connectedDevices.length > 0 ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label", children: "Select a connected device:" }), (0, jsx_runtime_1.jsx)("div", { className: "d-flex flex-wrap gap-2", children: syncInfo.connectedDevices.map((device) => ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-primary btn-sm", onClick: () => handleDownloadDatabase(device.deviceId), disabled: downloading, children: downloading ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1", role: "status", "aria-hidden": "true" }), "Downloading..."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-cloud-download me-1" }), device.deviceId.substring(0, 8), "...", (0, jsx_runtime_1.jsxs)("span", { className: "badge bg-secondary ms-1", children: [Math.round(device.latencyMs), "ms"] })] })) }, device.deviceId))) })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-info mb-0", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-info-circle me-2" }), "No devices are currently connected to this group. Connect to a device first to download its database."] })), downloadProgress && ((0, jsx_runtime_1.jsx)("div", { className: "mt-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2", role: "status", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: downloadProgress })] }) })), downloadResult && ((0, jsx_runtime_1.jsx)("div", { className: `alert mt-3 mb-0 ${downloadResult.success ? "alert-success" : "alert-danger"}`, children: downloadResult.success ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle me-2" }), (0, jsx_runtime_1.jsx)("strong", { children: "Download complete!" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["File: ", (0, jsx_runtime_1.jsx)("code", { children: downloadResult.filePath }), (0, jsx_runtime_1.jsx)("br", {}), "Size:", " ", downloadResult.size
135
- ? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB`
136
- : "Unknown"] })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-circle me-2" }), (0, jsx_runtime_1.jsx)("strong", { children: "Download failed:" }), " ", downloadResult.message] })) }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-info mb-4", children: "Raw database download is available only in the Electron desktop app." }))] }))] })] }) }));
96
+ }, onClick: onClose, children: (0, jsx_runtime_1.jsxs)("div", { className: "card", style: { maxWidth: "900px", width: "90%", maxHeight: "90vh" }, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "card-header d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0", children: loading ? "Loading..." : group?.groupName || "Group Details" }), group && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: (0, jsx_runtime_1.jsx)("code", { children: group.groupId }) }))] }), (0, jsx_runtime_1.jsx)("button", { className: "btn-close", onClick: onClose, "aria-label": "Close" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "card-body", style: { overflowY: "auto", maxHeight: "calc(90vh - 120px)" }, children: [loading && (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}), !loading && (notFound || !group) && ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning", children: "Group not found." })), !loading && group && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { className: "border-bottom pb-2 mb-3", children: "Group Information" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Group ID:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: group.groupId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Group Name:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: group.groupName })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Founder:" }), (0, jsx_runtime_1.jsx)("br", {}), group.founderUserName ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { children: group.founderUserName }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: group.founderUserId })] })) : ((0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: group.founderUserId }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Total Members:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { className: "h5", children: group.members.length })] })] })] }), group.members.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "border-bottom pb-2 mb-3", children: ["Members (", group.members.length, ")"] }), (0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-sm table-hover", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "User" }), (0, jsx_runtime_1.jsx)("th", { children: "User ID" }), (0, jsx_runtime_1.jsx)("th", { children: "Role" }), (0, jsx_runtime_1.jsx)("th", { children: "Trust Level" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: group.members.map((member) => ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { children: member.userName || (0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "-" }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: member.userId }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("span", { className: "badge bg-primary", children: member.role }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: member.trustLevel || peers_sdk_1.TrustLevel.Unknown }) })] }, member.userId))) })] }) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "border-bottom pb-2 mb-3", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-repeat me-2" }), "Synchronization"] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-muted small mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Sync now" }), " notifies connected peers that this context may have changes. ", (0, jsx_runtime_1.jsx)("strong", { children: "Do full sync" }), " resets change tracking for this context and rebuilds synchronization state; that may transfer substantial data and should be used only to repair suspected drift."] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary", onClick: () => void handleSyncNow(), disabled: syncingNow || fullSyncing, children: syncingNow ? "Sending sync notification..." : "Sync now" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-warning", onClick: () => void handleFullSync(), disabled: fullSyncing || syncingNow, children: fullSyncing ? "Starting full sync..." : "Do full sync" })] }), syncNowResult && ((0, jsx_runtime_1.jsx)("div", { className: `alert mt-3 mb-0 ${syncNowResult.success ? "alert-success" : "alert-danger"}`, role: "status", children: syncNowResult.message })), fullSyncResult && ((0, jsx_runtime_1.jsx)("div", { className: `alert mt-3 mb-0 ${fullSyncResult.success ? "alert-success" : "alert-danger"}`, role: "status", children: fullSyncResult.message }))] })] }))] })] }) }));
137
97
  }
@@ -1,12 +1,13 @@
1
- import { type INetworkControlConsumer, type INetworkDiagnosticsConsumer } from "@peers-app/peers-sdk";
1
+ import { type INetworkControlConsumer, type INetworkDiagnosticsConsumer, type ISyncCheckConsumer } from "@peers-app/peers-sdk";
2
2
  /** Electron-only capabilities intentionally excluded from portable network contracts. */
3
3
  export type NetworkViewerPlatformApi = {
4
- downloadRemoteDatabase?: (groupId: string, deviceId: string) => Promise<unknown>;
5
4
  runDiagnostics: () => Promise<unknown>;
6
5
  };
7
6
  /** Return a typed diagnostics consumer bound to the current local host transport. */
8
7
  export declare function getNetworkDiagnosticsConsumer(): Promise<INetworkDiagnosticsConsumer>;
9
8
  /** Return a typed control consumer bound to the current local host transport. */
10
9
  export declare function getNetworkControlConsumer(): Promise<INetworkControlConsumer>;
10
+ /** Return a typed System Sync Check consumer bound to the current local host transport. */
11
+ export declare function getSyncCheckConsumer(): Promise<ISyncCheckConsumer>;
11
12
  /** Return the context-isolated desktop-only capability bridge, when available. */
12
13
  export declare function getNetworkViewerPlatformApi(): NetworkViewerPlatformApi | undefined;
@@ -2,10 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getNetworkDiagnosticsConsumer = getNetworkDiagnosticsConsumer;
4
4
  exports.getNetworkControlConsumer = getNetworkControlConsumer;
5
+ exports.getSyncCheckConsumer = getSyncCheckConsumer;
5
6
  exports.getNetworkViewerPlatformApi = getNetworkViewerPlatformApi;
6
7
  const peers_sdk_1 = require("@peers-app/peers-sdk");
7
8
  const diagnostics = (0, peers_sdk_1.hostContractConsumer)((0, peers_sdk_1.getNetworkDiagnosticsContractDefinition)());
8
9
  const control = (0, peers_sdk_1.hostContractConsumer)((0, peers_sdk_1.getNetworkControlContractDefinition)());
10
+ const syncCheck = (0, peers_sdk_1.hostContractConsumer)((0, peers_sdk_1.getSyncCheckContractDefinition)());
9
11
  /** Return a typed diagnostics consumer bound to the current local host transport. */
10
12
  async function getNetworkDiagnosticsConsumer() {
11
13
  return diagnostics.get();
@@ -14,6 +16,10 @@ async function getNetworkDiagnosticsConsumer() {
14
16
  async function getNetworkControlConsumer() {
15
17
  return control.get();
16
18
  }
19
+ /** Return a typed System Sync Check consumer bound to the current local host transport. */
20
+ async function getSyncCheckConsumer() {
21
+ return syncCheck.get();
22
+ }
17
23
  /** Return the context-isolated desktop-only capability bridge, when available. */
18
24
  function getNetworkViewerPlatformApi() {
19
25
  return window.electronAPI?.networkViewer;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SyncRecoveryScreen = SyncRecoveryScreen;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
5
6
  const react_1 = require("react");
6
7
  const react_tiny_popover_1 = require("react-tiny-popover");
7
8
  const loading_indicator_1 = require("../../components/loading-indicator");
@@ -76,6 +77,107 @@ function RecoveryRows({ entry, busy, onRun, }) {
76
77
  : "This device has no row to keep", onClick: () => onRun(entry, "keepLocal"), children: "Keep this device's version" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-danger", disabled: busy, onClick: () => onRun(entry, "deleteEverywhere"), children: "Delete everywhere" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", disabled: busy, title: "Removes the entry on this device only. Other devices keep theirs.", onClick: () => onRun(entry, "ignoreLocal"), children: "Ignore on this device" })] }) })] }), (errorOpen || recordOpen) && ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsxs)("td", { colSpan: 7, className: "bg-body-tertiary", children: [errorOpen && ((0, jsx_runtime_1.jsx)("pre", { className: "small mb-0 font-monospace", style: detailsBlockStyle, children: formatError(entry.latestError) })), recordOpen && ((0, jsx_runtime_1.jsxs)("div", { className: errorOpen ? "mt-3" : undefined, children: [entry.changeCreatedAt != null && ((0, jsx_runtime_1.jsxs)("p", { className: "small mb-2", children: ["Change written ", formatWhen(entry.changeCreatedAt)] })), entry.conflictingRecordId && ((0, jsx_runtime_1.jsxs)("p", { className: "small mb-2", children: ["Another record holds the same unique value:", " ", (0, jsx_runtime_1.jsx)("code", { children: entry.conflictingRecordId })] })), entry.tombstoneRecordId &&
77
78
  entry.tombstoneRecordId === entry.conflictingRecordId && ((0, jsx_runtime_1.jsxs)("p", { className: "small mb-2", children: ["Keep deletes ", (0, jsx_runtime_1.jsx)("code", { children: entry.tombstoneRecordId }), " on every device."] })), entry.tombstoneRecordId && entry.tombstoneRecordId === entry.recordId && ((0, jsx_runtime_1.jsx)("p", { className: "small mb-2", children: "Keep snapshots the other record and tombstones this queued id." })), entry.keptRecordPreview && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "small mb-1", children: "This device's version" }), (0, jsx_runtime_1.jsx)("pre", { className: "small mb-2 font-monospace", style: detailsBlockStyle, children: entry.keptRecordPreview })] })), entry.queuedRecordPreview && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "small mb-1", children: "Queued change" }), (0, jsx_runtime_1.jsx)("pre", { className: "small mb-0 font-monospace", style: detailsBlockStyle, children: entry.queuedRecordPreview })] })), !entry.keptRecordPreview && !entry.queuedRecordPreview && entry.recordPreview && ((0, jsx_runtime_1.jsx)("pre", { className: "small mb-0 font-monospace", style: detailsBlockStyle, children: entry.recordPreview })), !entry.keptRecordPreview && !entry.queuedRecordPreview && !entry.recordPreview && ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-0", children: "This device has no stored copy of the record. The latest error may still quote the rejected value." }))] }))] }) }))] }));
78
79
  }
80
+ function MissingChangeRows({ gap }) {
81
+ const [open, setOpen] = (0, react_1.useState)(false);
82
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-link btn-sm p-0", "aria-label": open ? "Hide missing changes" : "Show missing changes", onClick: () => setOpen((current) => !current), children: open ? "Hide changes" : "Show changes" }), open && ((0, jsx_runtime_1.jsxs)("table", { className: "table table-sm small mb-0 mt-1", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Table" }), (0, jsx_runtime_1.jsx)("th", { children: "Record" }), (0, jsx_runtime_1.jsx)("th", { children: "Change" }), (0, jsx_runtime_1.jsx)("th", { children: "Written" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: gap.missing.map((change) => ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { children: change.tableName }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("code", { children: change.recordId }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsxs)("code", { title: change.changeId, children: [change.op, " ", change.path] }) }), (0, jsx_runtime_1.jsx)("td", { className: "text-nowrap", children: formatWhen(change.createdAt) })] }, change.changeId))) })] })), open && gap.missingCount > gap.missing.length && ((0, jsx_runtime_1.jsxs)("div", { className: "small text-muted", children: ["Showing ", gap.missing.length, " of ", gap.missingCount, "."] }))] }));
83
+ }
84
+ /** What the daily sync gap check found missing on this device, with Check now and Repair. */
85
+ function MissingChangesSection() {
86
+ const [gaps, setGaps] = (0, react_1.useState)([]);
87
+ const [availability, setAvailability] = (0, react_1.useState)("loading");
88
+ const loadGeneration = (0, react_1.useRef)(0);
89
+ const [busyKey, setBusyKey] = (0, react_1.useState)(null);
90
+ const [result, setResult] = (0, react_1.useState)(null);
91
+ const load = (0, react_1.useCallback)(async () => {
92
+ const generation = ++loadGeneration.current;
93
+ try {
94
+ const syncCheck = await (0, network_viewer_ipc_1.getSyncCheckConsumer)();
95
+ const { gaps } = await syncCheck.tools.getSyncGaps();
96
+ if (generation !== loadGeneration.current)
97
+ return;
98
+ setGaps(gaps);
99
+ setAvailability("ready");
100
+ }
101
+ catch (loadError) {
102
+ if (generation !== loadGeneration.current)
103
+ return;
104
+ setAvailability("unavailable");
105
+ setGaps([]);
106
+ setResult({
107
+ success: false,
108
+ message: loadError instanceof Error ? loadError.message : String(loadError),
109
+ });
110
+ }
111
+ }, []);
112
+ (0, react_1.useEffect)(() => {
113
+ let active = true;
114
+ let generation = 0;
115
+ let subscription;
116
+ const connect = () => {
117
+ const currentGeneration = ++generation;
118
+ subscription?.dispose();
119
+ subscription = undefined;
120
+ setAvailability("loading");
121
+ setGaps([]);
122
+ setResult(null);
123
+ void load();
124
+ void (0, network_viewer_ipc_1.getSyncCheckConsumer)()
125
+ .then((syncCheck) => {
126
+ if (active && generation === currentGeneration) {
127
+ subscription = syncCheck.observables?.status.subscribe(() => void load());
128
+ }
129
+ })
130
+ .catch(() => {
131
+ // `load` shows the unavailable state, including hosts without System Sync Check.
132
+ });
133
+ };
134
+ connect();
135
+ const unsubscribe = (0, peers_sdk_1.subscribeHostContractTransport)((transport) => {
136
+ if (transport)
137
+ connect();
138
+ else {
139
+ generation++;
140
+ loadGeneration.current++;
141
+ subscription?.dispose();
142
+ subscription = undefined;
143
+ setAvailability("unavailable");
144
+ setGaps([]);
145
+ }
146
+ }, { emitCurrent: false });
147
+ return () => {
148
+ active = false;
149
+ loadGeneration.current++;
150
+ subscription?.dispose();
151
+ unsubscribe();
152
+ };
153
+ }, [load]);
154
+ const run = async (key, action) => {
155
+ setBusyKey(key);
156
+ setResult(null);
157
+ try {
158
+ setResult({ success: true, message: (await action()).message });
159
+ }
160
+ catch (actionError) {
161
+ setResult({
162
+ success: false,
163
+ message: actionError instanceof Error ? actionError.message : String(actionError),
164
+ });
165
+ }
166
+ finally {
167
+ setBusyKey(null);
168
+ await load();
169
+ }
170
+ };
171
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "card mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "card-header d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsxs)("span", { children: ["Missing changes", (0, jsx_runtime_1.jsxs)(InfoTip, { label: "About missing changes", children: ["Once a day this device compares its changes with each connected device in every group. Listed here are changes another device has that this device should have received but doesn't. ", (0, jsx_runtime_1.jsx)("strong", { children: "Repair" }), " fetches them from that device and applies them the same way sync does. If changes keep going missing, that points to a sync problem worth reporting."] })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary", disabled: busyKey !== null || availability !== "ready", onClick: () => void run("check", async () => (await (0, network_viewer_ipc_1.getSyncCheckConsumer)()).tools.checkSyncGaps()), children: busyKey === "check" ? "Checking…" : "Check now" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "card-body py-2", children: [result && ((0, jsx_runtime_1.jsx)("div", { className: `alert ${result.success ? "alert-info" : "alert-danger"} py-1 px-2 small mb-2`, children: result.message })), availability !== "ready" ? ((0, jsx_runtime_1.jsx)("div", { className: "small text-muted", children: availability === "loading"
172
+ ? "Loading missing change check…"
173
+ : "Missing change check is unavailable on this host." })) : gaps.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "small text-muted", children: "No missing changes found. Checks run once a day." })) : (gaps.map((gap) => {
174
+ const key = `${gap.dataContextId}:${gap.deviceId}`;
175
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "border-bottom py-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-start gap-2", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: gap.dataContextName }), ": ", gap.missingCount, gap.truncated ? "+" : "", " ", gap.missingCount === 1 ? "change" : "changes", " on", " ", (0, jsx_runtime_1.jsx)("span", { title: gap.deviceId, children: gap.deviceName || gap.deviceId }), " missing here", (0, jsx_runtime_1.jsxs)("div", { className: "small text-muted", children: ["Found ", formatWhen(gap.checkedAt)] })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-warning text-nowrap", disabled: busyKey !== null, onClick: () => void run(key, async () => (await (0, network_viewer_ipc_1.getSyncCheckConsumer)()).tools.repairSyncGaps({
176
+ dataContextId: gap.dataContextId,
177
+ deviceId: gap.deviceId,
178
+ })), children: busyKey === key ? "Repairing…" : "Repair" })] }), (0, jsx_runtime_1.jsx)(MissingChangeRows, { gap: gap })] }, key));
179
+ }))] })] }));
180
+ }
79
181
  /** Lists recovery-queue entries and the actions that resolve them. */
80
182
  function SyncRecoveryScreen({ onBack }) {
81
183
  const [entries, setEntries] = (0, react_1.useState)([]);
@@ -178,5 +280,5 @@ function SyncRecoveryScreen({ onBack }) {
178
280
  group.push(entry);
179
281
  groups.set(entry.dataContextId, group);
180
282
  }
181
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-3", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", onClick: onBack, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-left me-1" }), "Back"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h2", { className: "mb-1", children: "Sync recovery" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted small mb-0", children: "These records have change history, but this device could not write the row. Parked entries are not asked of other devices until you retry them." })] })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-primary", onClick: () => void load(), children: "Refresh" })] }), error && (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2 small", children: error }), entries.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-success", children: "No records are waiting in the recovery queue." })) : ([...groups.entries()].map(([dataContextId, groupEntries]) => ((0, jsx_runtime_1.jsxs)("div", { className: "card mb-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "card-header", children: groupEntries[0]?.dataContextName || dataContextId }), (0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-sm align-middle mb-0", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Table" }), (0, jsx_runtime_1.jsx)("th", { children: "Record" }), (0, jsx_runtime_1.jsx)("th", { children: "First failed" }), (0, jsx_runtime_1.jsx)("th", { children: "Attempts" }), (0, jsx_runtime_1.jsx)("th", { children: "Status" }), (0, jsx_runtime_1.jsx)("th", { children: "Latest error" }), (0, jsx_runtime_1.jsxs)("th", { children: ["Actions", (0, jsx_runtime_1.jsx)(InfoTip, { label: "About these actions", children: (0, jsx_runtime_1.jsxs)("ul", { className: "mb-0 ps-3", children: [(0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Retry" }), " writes the record again from this device's history, then asks another device once. If a schema or unique-key entry is still queued after that, it is parked again. Use it after an upgrade that might accept a row this device rejected before."] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Keep this device's version" }), " snapshots the row this device will keep. That is the queued row when it is already stored here, and otherwise the other row that holds the same unique value. When both rows are stored, the other record is deleted everywhere. When only the other row is stored, that row is kept and this queued id is tombstoned. Disabled when this device has no row to keep."] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Delete everywhere" }), " deletes this record on every device, including devices that already have it."] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Ignore on this device" }), " removes the entry here only. Other devices keep their own copy of the problem."] })] }) })] })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: groupEntries.map((entry) => ((0, jsx_runtime_1.jsx)(RecoveryRows, { entry: entry, busy: Boolean(busyKey?.startsWith(`${entry.dataContextId}:${entry.syncRecoveryId}:`)), onRun: (item, action) => void run(item, action) }, entry.syncRecoveryId))) })] }) })] }, dataContextId))))] }));
283
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-3", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", onClick: onBack, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-left me-1" }), "Back"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h2", { className: "mb-1", children: "Sync recovery" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted small mb-0", children: "These records have change history, but this device could not write the row. Parked entries are not asked of other devices until you retry them." })] })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-primary", onClick: () => void load(), children: "Refresh" })] }), error && (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2 small", children: error }), (0, jsx_runtime_1.jsx)(MissingChangesSection, {}), entries.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-success", children: "No records are waiting in the recovery queue." })) : ([...groups.entries()].map(([dataContextId, groupEntries]) => ((0, jsx_runtime_1.jsxs)("div", { className: "card mb-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "card-header", children: groupEntries[0]?.dataContextName || dataContextId }), (0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-sm align-middle mb-0", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Table" }), (0, jsx_runtime_1.jsx)("th", { children: "Record" }), (0, jsx_runtime_1.jsx)("th", { children: "First failed" }), (0, jsx_runtime_1.jsx)("th", { children: "Attempts" }), (0, jsx_runtime_1.jsx)("th", { children: "Status" }), (0, jsx_runtime_1.jsx)("th", { children: "Latest error" }), (0, jsx_runtime_1.jsxs)("th", { children: ["Actions", (0, jsx_runtime_1.jsx)(InfoTip, { label: "About these actions", children: (0, jsx_runtime_1.jsxs)("ul", { className: "mb-0 ps-3", children: [(0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Retry" }), " writes the record again from this device's history, then asks another device once. If a schema or unique-key entry is still queued after that, it is parked again. Use it after an upgrade that might accept a row this device rejected before."] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Keep this device's version" }), " snapshots the row this device will keep. That is the queued row when it is already stored here, and otherwise the other row that holds the same unique value. When both rows are stored, the other record is deleted everywhere. When only the other row is stored, that row is kept and this queued id is tombstoned. Disabled when this device has no row to keep."] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Delete everywhere" }), " deletes this record on every device, including devices that already have it."] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Ignore on this device" }), " removes the entry here only. Other devices keep their own copy of the problem."] })] }) })] })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: groupEntries.map((entry) => ((0, jsx_runtime_1.jsx)(RecoveryRows, { entry: entry, busy: Boolean(busyKey?.startsWith(`${entry.dataContextId}:${entry.syncRecoveryId}:`)), onRun: (item, action) => void run(item, action) }, entry.syncRecoveryId))) })] }) })] }, dataContextId))))] }));
182
284
  }
@@ -99,7 +99,8 @@ const CopyValueButton = (props) => {
99
99
  async function onCopy() {
100
100
  try {
101
101
  const userContext = await (0, peers_sdk_1.getUserContext)();
102
- const groupId = userContext.currentlyActiveGroupId();
102
+ // Personal can be "" here; leave it out so the provider uses the personal context.
103
+ const groupId = userContext.currentlyActiveGroupId() || undefined;
103
104
  const tools = await (0, host_shell_consumer_1.hostShellTools)();
104
105
  const result = await tools.copyVariableToClipboard({
105
106
  persistentVarId: pvar.persistentVarId,
@@ -37,13 +37,25 @@ function disconnectedLabelSuffix(disconnectedContextCount) {
37
37
  function connectedLabelSuffix(connectedContextCount) {
38
38
  return `; ${connectedContextCount} ${connectedContextCount === 1 ? "group" : "groups"} connected`;
39
39
  }
40
- function withStuckAttention(presentation, stuckRecoveryCount) {
41
- if (stuckRecoveryCount <= 0) {
40
+ function missingChangesLabel(missingChangeCount) {
41
+ return missingChangeCount === 1
42
+ ? "1 change missing from another device"
43
+ : `${missingChangeCount} changes missing from another device`;
44
+ }
45
+ function withStuckAttention(presentation, stuckRecoveryCount, missingChangeCount) {
46
+ if (stuckRecoveryCount <= 0 && missingChangeCount <= 0) {
42
47
  return presentation;
43
48
  }
44
- const attention = stuckRecoveryCount === 1
45
- ? "1 record needs attention"
46
- : `${stuckRecoveryCount} records need attention`;
49
+ const attention = [
50
+ stuckRecoveryCount === 1
51
+ ? "1 record needs attention"
52
+ : stuckRecoveryCount > 1
53
+ ? `${stuckRecoveryCount} records need attention`
54
+ : "",
55
+ missingChangeCount > 0 ? missingChangesLabel(missingChangeCount) : "",
56
+ ]
57
+ .filter(Boolean)
58
+ .join("; ");
47
59
  const labeled = { ...presentation, label: `${presentation.label}; ${attention}` };
48
60
  if (presentation.colorClass === "text-danger") {
49
61
  return labeled;
@@ -79,6 +91,7 @@ function getNetworkStatusPresentation(viewState, opts) {
79
91
  };
80
92
  }
81
93
  const { connectedContextCount, connectionCount, disconnectedContextCount, errorCount, state, stuckRecoveryCount, syncingContextCount, } = viewState.status;
94
+ const missingChangeCount = viewState.missingChangeCount ?? 0;
82
95
  const disconnectedOverlay = disconnectedContextCount > 0 && state !== "offline"
83
96
  ? { count: disconnectedContextCount }
84
97
  : undefined;
@@ -95,7 +108,7 @@ function getNetworkStatusPresentation(viewState, opts) {
95
108
  mark: "solid",
96
109
  disconnectedGroupsOverlay: disconnectedOverlay,
97
110
  connectedGroupsOverlay: groupsOverlay,
98
- }, stuckRecoveryCount);
111
+ }, stuckRecoveryCount, missingChangeCount);
99
112
  }
100
113
  if (state === "syncing" || opts?.holdSyncingVisual) {
101
114
  const longSync = state === "syncing" && opts?.syncingLongerThanGreenMs === true;
@@ -109,7 +122,7 @@ function getNetworkStatusPresentation(viewState, opts) {
109
122
  mark: longSync ? "indeterminate" : "pulse",
110
123
  disconnectedGroupsOverlay: disconnectedOverlay,
111
124
  connectedGroupsOverlay: groupsOverlay,
112
- }, stuckRecoveryCount);
125
+ }, stuckRecoveryCount, missingChangeCount);
113
126
  }
114
127
  if (state === "offline") {
115
128
  return withStuckAttention({
@@ -117,7 +130,7 @@ function getNetworkStatusPresentation(viewState, opts) {
117
130
  shortLabel: "Offline",
118
131
  iconClass: "bi-cloud-slash-fill",
119
132
  colorClass: "text-secondary",
120
- }, stuckRecoveryCount);
133
+ }, stuckRecoveryCount, missingChangeCount);
121
134
  }
122
135
  return withStuckAttention({
123
136
  label: `Network synced; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}${connectedSuffix}${disconnectedSuffix}`,
@@ -127,7 +140,7 @@ function getNetworkStatusPresentation(viewState, opts) {
127
140
  compactIcon: true,
128
141
  disconnectedGroupsOverlay: disconnectedOverlay,
129
142
  connectedGroupsOverlay: groupsOverlay,
130
- }, stuckRecoveryCount);
143
+ }, stuckRecoveryCount, missingChangeCount);
131
144
  }
132
145
  /**
133
146
  * Add the app-update badge and label to a network status presentation. Applies in every
@@ -233,10 +246,11 @@ function StatusCountRow({ label, value, className, }) {
233
246
  const tone = className ? ` ${className}` : "";
234
247
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: `col-7${tone}`, children: label }), (0, jsx_runtime_1.jsx)("dd", { className: `col-5 text-end${tone}`, children: value })] }));
235
248
  }
236
- function NetworkStatusDetails({ detailsId, status, appUpdate, unavailable, connectionsLoadState, onUseInvite, onOpenIdentity, onOpenViewer, onOpenRecovery, }) {
249
+ function NetworkStatusDetails({ detailsId, status, missingChangeCount, appUpdate, unavailable, connectionsLoadState, onUseInvite, onOpenIdentity, onOpenViewer, onOpenRecovery, }) {
250
+ const needsRecovery = !!status && (status.stuckRecoveryCount > 0 || missingChangeCount > 0);
237
251
  return ((0, jsx_runtime_1.jsx)("div", { id: detailsId, role: "dialog", "aria-label": "Network status details", className: "card shadow position-absolute end-0 mt-1", style: { top: "100%", width: "320px", zIndex: 1100 }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 mb-3", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-primary flex-fill", onClick: onUseInvite, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-qr-code me-1", "aria-hidden": "true" }), "Use Invite"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-primary flex-fill", onClick: onOpenIdentity, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-badge me-1", "aria-hidden": "true" }), "Identity"] })] }), (0, jsx_runtime_1.jsx)(app_update_controls_1.AppUpdatePopoverSection, { status: appUpdate }), (0, jsx_runtime_1.jsx)("h6", { className: "card-title mb-2", children: "Network status" }), unavailable || !status ? ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-3", children: "Network diagnostics are temporarily unavailable. The local host may still be starting or reconnecting." })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("dl", { className: "row small mb-2", children: [(0, jsx_runtime_1.jsx)(StatusCountRow, { label: "Connections", value: status.connectionCount }), (0, jsx_runtime_1.jsx)(StatusCountRow, { label: "Groups connected", value: status.connectedContextCount }), status.syncingContextCount > 0 && ((0, jsx_runtime_1.jsx)(StatusCountRow, { label: "Syncing contexts", value: status.syncingContextCount })), status.disconnectedContextCount > 0 && ((0, jsx_runtime_1.jsx)(StatusCountRow, { label: "Groups offline", value: status.disconnectedContextCount, className: "text-warning" })), status.errorCount > 0 && ((0, jsx_runtime_1.jsx)(StatusCountRow, { label: "Sync errors", value: status.errorCount })), status.lastSuccessfulSyncAt != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Last sync" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end text-truncate", title: new Date(status.lastSuccessfulSyncAt).toLocaleString(), children: formatRelativeSyncTime(status.lastSuccessfulSyncAt) })] }))] }), status.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-2 small mb-2", children: [status.errors[0].message, status.errors.length > 1 && ` (+${status.errors.length - 1} more)`] })), status.stuckRecoveryCount > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning py-2 px-2 small mb-2", children: status.stuckRecoveryCount === 1
238
252
  ? "1 record needs attention"
239
- : `${status.stuckRecoveryCount} records need attention` })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Connected Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: `btn btn-sm btn-outline-primary w-100${status && status.stuckRecoveryCount > 0 ? " mb-2" : ""}`, onClick: onOpenViewer, children: "Open Network Viewer" }), status && status.stuckRecoveryCount > 0 && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-warning w-100", onClick: onOpenRecovery, children: "Sync recovery" }))] }) }));
253
+ : `${status.stuckRecoveryCount} records need attention` })), missingChangeCount > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning py-2 px-2 small mb-2", children: missingChangesLabel(missingChangeCount) })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Connected Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: `btn btn-sm btn-outline-primary w-100${needsRecovery ? " mb-2" : ""}`, onClick: onOpenViewer, children: "Open Network Viewer" }), needsRecovery && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-warning w-100", onClick: onOpenRecovery, children: "Sync recovery" }))] }) }));
240
254
  }
241
255
  /** Accessible reactive network status button and details popover. */
242
256
  function NetworkStatusIndicator({ compact = false }) {
@@ -363,7 +377,7 @@ function NetworkStatusIndicator({ compact = false }) {
363
377
  .network-status-update-badge.bi::before {
364
378
  vertical-align: 0;
365
379
  }
366
- ` })] }))] }), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, appUpdate: appUpdate, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState, onUseInvite: () => {
380
+ ` })] }))] }), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, missingChangeCount: viewState.missingChangeCount ?? 0, appUpdate: appUpdate, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState, onUseInvite: () => {
367
381
  setOpen(false);
368
382
  (0, tabs_state_1.goToTabPath)(identity_routes_1.IDENTITY_ACCEPT_PATH);
369
383
  }, onOpenIdentity: () => {
@@ -3,6 +3,11 @@ import { type INetworkStatusSummary } from "@peers-app/peers-sdk";
3
3
  export interface INetworkStatusViewState {
4
4
  availability: "loading" | "ready" | "unavailable";
5
5
  status?: INetworkStatusSummary;
6
+ /** Changes the daily sync gap check found on another device but not on this one. */
7
+ missingChangeCount?: number;
6
8
  }
7
- /** Subscribe to the current host's System Network Diagnostics status observable. */
9
+ /**
10
+ * Subscribe to the current host's System Network Diagnostics status observable, and to System
11
+ * Sync Check's missing-change count.
12
+ */
8
13
  export declare function useNetworkStatus(): INetworkStatusViewState;
@@ -4,7 +4,10 @@ exports.useNetworkStatus = useNetworkStatus;
4
4
  const peers_sdk_1 = require("@peers-app/peers-sdk");
5
5
  const react_1 = require("react");
6
6
  const network_viewer_ipc_1 = require("../screens/network-viewer/network-viewer-ipc");
7
- /** Subscribe to the current host's System Network Diagnostics status observable. */
7
+ /**
8
+ * Subscribe to the current host's System Network Diagnostics status observable, and to System
9
+ * Sync Check's missing-change count.
10
+ */
8
11
  function useNetworkStatus() {
9
12
  const [viewState, setViewState] = (0, react_1.useState)({
10
13
  availability: "loading",
@@ -13,12 +16,15 @@ function useNetworkStatus() {
13
16
  let active = true;
14
17
  let generation = 0;
15
18
  let statusSubscription;
19
+ let syncCheckSubscription;
16
20
  let hadTransport = false;
17
21
  const unsubscribeTransport = (0, peers_sdk_1.subscribeHostContractTransport)((transport) => {
18
22
  generation += 1;
19
23
  const currentGeneration = generation;
20
24
  statusSubscription?.dispose();
21
25
  statusSubscription = undefined;
26
+ syncCheckSubscription?.dispose();
27
+ syncCheckSubscription = undefined;
22
28
  if (!transport) {
23
29
  setViewState({ availability: hadTransport ? "unavailable" : "loading" });
24
30
  return;
@@ -30,13 +36,14 @@ function useNetworkStatus() {
30
36
  if (!active || generation !== currentGeneration) {
31
37
  return;
32
38
  }
33
- setViewState({
39
+ setViewState((current) => ({
40
+ ...current,
34
41
  availability: "ready",
35
42
  status: diagnostics.observables.status(),
36
- });
43
+ }));
37
44
  statusSubscription = diagnostics.observables.status.subscribe((status) => {
38
45
  if (active && generation === currentGeneration) {
39
- setViewState({ availability: "ready", status });
46
+ setViewState((current) => ({ ...current, availability: "ready", status }));
40
47
  }
41
48
  });
42
49
  })
@@ -45,11 +52,27 @@ function useNetworkStatus() {
45
52
  setViewState({ availability: "unavailable" });
46
53
  }
47
54
  });
55
+ // A missing count only adds a warning; without it the indicator still works.
56
+ void (0, network_viewer_ipc_1.getSyncCheckConsumer)()
57
+ .then((syncCheck) => {
58
+ if (!active || generation !== currentGeneration) {
59
+ return;
60
+ }
61
+ const update = ({ missingChangeCount }) => {
62
+ if (active && generation === currentGeneration) {
63
+ setViewState((current) => ({ ...current, missingChangeCount }));
64
+ }
65
+ };
66
+ update(syncCheck.observables.status());
67
+ syncCheckSubscription = syncCheck.observables.status.subscribe(update);
68
+ })
69
+ .catch(() => { });
48
70
  });
49
71
  return () => {
50
72
  active = false;
51
73
  generation += 1;
52
74
  statusSubscription?.dispose();
75
+ syncCheckSubscription?.dispose();
53
76
  unsubscribeTransport();
54
77
  };
55
78
  }, []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.27.3",
3
+ "version": "0.27.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -9,6 +9,12 @@
9
9
  "license": "MIT",
10
10
  "main": "dist/index.js",
11
11
  "types": "dist/index.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "!dist/**/*.test.*",
15
+ "!dist/**/__tests__",
16
+ "README.md"
17
+ ],
12
18
  "publishConfig": {
13
19
  "access": "public"
14
20
  },
@@ -30,7 +36,7 @@
30
36
  "lint:fix": "biome check --write ."
31
37
  },
32
38
  "peerDependencies": {
33
- "@peers-app/peers-sdk": "^0.27.3",
39
+ "@peers-app/peers-sdk": "^0.27.5",
34
40
  "bootstrap": "^5.3.3",
35
41
  "react": "^18.0.0",
36
42
  "react-dom": "^18.0.0"
@@ -41,7 +47,7 @@
41
47
  "@babel/preset-react": "^7.24.1",
42
48
  "@babel/preset-typescript": "^7.27.1",
43
49
  "@biomejs/biome": "2.3.11",
44
- "@peers-app/peers-sdk": "0.27.3",
50
+ "@peers-app/peers-sdk": "0.27.5",
45
51
  "@testing-library/dom": "^10.4.0",
46
52
  "@testing-library/jest-dom": "^6.6.3",
47
53
  "@testing-library/react": "^16.3.0",
@@ -1,34 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(mkdir:*)",
5
- "Bash(yarn add:*)",
6
- "Bash(yarn test:*)",
7
- "Bash(npm test)",
8
- "Bash(yarn build)",
9
- "Bash(yarn jest:*)",
10
- "Bash(mv:*)",
11
- "Bash(npm run build:*)",
12
- "Bash(cd:*)",
13
- "Bash(npm test:*)",
14
- "Bash(npm run test:*)",
15
- "Bash(yarn tsc:*)",
16
- "WebFetch(domain:developer.mozilla.org)",
17
- "Read(/Users/mark.archer/peers-device/src/**)",
18
- "Read(/Users/mark.archer/peers-packages/tasks/**)",
19
- "Read(/Users/mark.archer/peers-packages/tasks/**)",
20
- "Read(/Users/mark.archer/peers-packages/tasks/**)",
21
- "Read(/Users/mark.archer/peers-packages/tasks/**)",
22
- "Read(/Users/mark.archer/peers-packages/tasks/src/**)",
23
- "Read(/Users/mark.archer/peers-packages/tasks/dist/**)",
24
- "Read(/Users/mark.archer/peers-packages/tasks/dist/**)",
25
- "Read(/Users/mark.archer/peers-packages/tasks/dist/**)",
26
- "Read(/Users/mark.archer/peers-packages/tasks/src/**)",
27
- "Read(/Users/mark.archer/peers-packages/tasks/src/**)"
28
- ],
29
- "deny": [],
30
- "additionalDirectories": [
31
- "/Users/mark.archer/peers-packages/tasks/dist"
32
- ]
33
- }
34
- }