@codemem/server 0.22.1 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/static/app.js CHANGED
@@ -148,6 +148,11 @@
148
148
  lastSyncJoinRequests: [],
149
149
  lastTeamInvite: null,
150
150
  lastTeamJoin: null,
151
+ syncJoinFlowFeedback: null,
152
+ syncPeerFeedbackById: /* @__PURE__ */ new Map(),
153
+ syncPeersSectionFeedback: null,
154
+ syncJoinRequestsFeedback: null,
155
+ syncDiscoveredFeedback: null,
151
156
  lastSyncAttempts: [],
152
157
  lastSyncLegacyDevices: [],
153
158
  lastSyncViewModel: null,
@@ -235,6 +240,19 @@
235
240
  if (!resp.ok) throw new Error(`${url}: ${resp.status} ${resp.statusText}`);
236
241
  return resp.json();
237
242
  }
243
+ async function pingViewerReady(timeoutMs = 1200) {
244
+ const controller = new AbortController();
245
+ const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
246
+ try {
247
+ const resp = await fetch("/api/stats", {
248
+ cache: "no-store",
249
+ signal: controller.signal
250
+ });
251
+ if (!resp.ok) throw new Error(`/api/stats: ${resp.status} ${resp.statusText}`);
252
+ } finally {
253
+ window.clearTimeout(timeoutId);
254
+ }
255
+ }
238
256
  async function readJsonPayload(resp) {
239
257
  const text = await resp.text();
240
258
  try {
@@ -2511,18 +2529,18 @@
2511
2529
  priority: 10,
2512
2530
  title: `${device.name} needs repair`,
2513
2531
  summary: device.summary,
2514
- actionLabel: "Review device",
2532
+ actionLabel: "Open device",
2515
2533
  deviceId: device.id
2516
2534
  };
2517
2535
  }
2518
2536
  function createReviewItem(device) {
2519
2537
  return {
2520
- id: `review:${device.id}`,
2538
+ id: `review:${device.id}:${device.key || "default"}`,
2521
2539
  kind: "review-team-device",
2522
2540
  priority: 20,
2523
2541
  title: `${device.name} is available to review`,
2524
2542
  summary: device.summary,
2525
- actionLabel: "Review device",
2543
+ actionLabel: "Open device",
2526
2544
  deviceId: device.id
2527
2545
  };
2528
2546
  }
@@ -2533,7 +2551,7 @@
2533
2551
  priority: 30,
2534
2552
  title: `Name ${device.name}`,
2535
2553
  summary: device.summary,
2536
- actionLabel: "Name device",
2554
+ actionLabel: "Go to name field",
2537
2555
  deviceId: device.id
2538
2556
  };
2539
2557
  }
@@ -2581,7 +2599,7 @@
2581
2599
  priority: candidate.includesLocal ? 5 : 15,
2582
2600
  title: `Possible duplicate person: ${candidate.displayName}`,
2583
2601
  summary: candidate.includesLocal ? "At least one entry is marked as you. Confirm whether these records represent the same person." : "Multiple people share this name. Confirm whether they should stay separate or be combined.",
2584
- actionLabel: "Review people",
2602
+ actionLabel: "Go to people",
2585
2603
  actorIds: candidate.actorIds
2586
2604
  });
2587
2605
  });
@@ -2617,9 +2635,22 @@
2617
2635
  }));
2618
2636
  if (device.peer && trustSummary?.state === "trusted-by-you") attentionItems.push(createReviewItem({
2619
2637
  id: device.deviceId,
2638
+ key: "other-device-trust",
2620
2639
  name,
2621
2640
  summary: "You accepted this device. Finish onboarding on the other device so it trusts this one too."
2622
2641
  }));
2642
+ if (!device.peer && device.discovered?.stale) attentionItems.push(createReviewItem({
2643
+ id: device.deviceId,
2644
+ key: "stale-discovery",
2645
+ name,
2646
+ summary: "This device is no longer advertising fresh coordinator presence. Wait for it to check in again before connecting it here."
2647
+ }));
2648
+ if (!device.peer && Array.isArray(device.discovered?.groups) && device.discovered.groups.length > 1) attentionItems.push(createReviewItem({
2649
+ id: device.deviceId,
2650
+ key: "ambiguous-groups",
2651
+ name,
2652
+ summary: "This device appears in multiple coordinator groups. Review the team setup before approving it here."
2653
+ }));
2623
2654
  if (device.peer && deviceNeedsFriendlyName({
2624
2655
  localName: device.localName,
2625
2656
  coordinatorName: device.coordinatorName,
@@ -3947,6 +3978,11 @@
3947
3978
  placeholder: "Invite will appear here",
3948
3979
  readOnly: true,
3949
3980
  hidden: true
3981
+ }),
3982
+ /* @__PURE__ */ u("div", {
3983
+ className: "peer-meta",
3984
+ id: "syncInviteWarnings",
3985
+ hidden: true
3950
3986
  })
3951
3987
  ]
3952
3988
  });
@@ -4029,6 +4065,12 @@
4029
4065
  function PairingText({ text }) {
4030
4066
  return /* @__PURE__ */ u(k$2, { children: text });
4031
4067
  }
4068
+ function SyncEmptyState({ title, detail }) {
4069
+ return /* @__PURE__ */ u("div", {
4070
+ class: "sync-empty-state",
4071
+ children: [/* @__PURE__ */ u("strong", { children: title }), /* @__PURE__ */ u("span", { children: detail })]
4072
+ });
4073
+ }
4032
4074
  function renderDiagnosticsGrid(mount, items) {
4033
4075
  if (!items.length) {
4034
4076
  clearSyncMount(mount);
@@ -4043,6 +4085,9 @@
4043
4085
  }
4044
4086
  renderIntoSyncMount(mount, /* @__PURE__ */ u(AttemptsList, { attempts }));
4045
4087
  }
4088
+ function renderSyncEmptyState(mount, view) {
4089
+ renderIntoSyncMount(mount, /* @__PURE__ */ u(SyncEmptyState, { ...view }));
4090
+ }
4046
4091
  function renderPairingView(payloadMount, hintMount, view) {
4047
4092
  renderIntoSyncMount(payloadMount, /* @__PURE__ */ u(PairingText, { text: view.payloadText }));
4048
4093
  if (hintMount) renderIntoSyncMount(hintMount, /* @__PURE__ */ u(PairingText, { text: view.hintText }));
@@ -4259,6 +4304,13 @@
4259
4304
  //#region src/tabs/sync/diagnostics.ts
4260
4305
  var SYNC_REDACT_MOUNT_ID = "syncRedactMount";
4261
4306
  var SYNC_REDACT_LABEL_ID = "syncRedactLabel";
4307
+ function newestPeerPing(peers) {
4308
+ const timestamps = Object.values(peers || {}).map((peer) => {
4309
+ if (!peer || typeof peer !== "object") return "";
4310
+ return String(peer.last_ping_at || "").trim();
4311
+ }).filter(Boolean).sort();
4312
+ return timestamps.length ? timestamps[timestamps.length - 1] : null;
4313
+ }
4262
4314
  var _renderSyncPeers = () => {};
4263
4315
  function setRenderSyncPeers(fn) {
4264
4316
  _renderSyncPeers = fn;
@@ -4295,6 +4347,31 @@
4295
4347
  hintText: "Copy this command and run it on the other device. Use --include/--exclude to control which projects sync."
4296
4348
  };
4297
4349
  }
4350
+ function diagnosticsLoadingState() {
4351
+ return {
4352
+ title: "Diagnostics still loading.",
4353
+ detail: "Wait a moment for local sync status. If it stays blank, refresh the page or check whether sync is enabled on this device."
4354
+ };
4355
+ }
4356
+ function diagnosticsUnavailableState() {
4357
+ return {
4358
+ title: "Diagnostics unavailable right now.",
4359
+ detail: "The viewer could not load sync status. Refresh this page, or check that the local codemem sync service is reachable before retrying."
4360
+ };
4361
+ }
4362
+ function noAttemptsState() {
4363
+ const syncStatus = state.lastSyncStatus;
4364
+ return {
4365
+ title: "No recent sync attempts yet.",
4366
+ detail: syncStatus?.daemon_state === "disabled" || syncStatus?.enabled === false ? "Turn on sync in Settings → Device Sync first. Recent attempts will appear here after this device can actually run sync work." : "Trigger a sync pass or pair another device to generate activity here when you need low-level troubleshooting."
4367
+ };
4368
+ }
4369
+ function unavailableAttemptsState() {
4370
+ return {
4371
+ title: "Recent attempts unavailable right now.",
4372
+ detail: "Attempt history could not be loaded because sync diagnostics failed. Refresh the page after local sync status is reachable again."
4373
+ };
4374
+ }
4298
4375
  function renderSyncStatus() {
4299
4376
  const syncStatusGrid = document.getElementById("syncStatusGrid");
4300
4377
  const syncMeta = document.getElementById("syncMeta");
@@ -4303,16 +4380,16 @@
4303
4380
  hideSkeleton("syncDiagSkeleton");
4304
4381
  const status = state.lastSyncStatus;
4305
4382
  if (!status) {
4306
- clearSyncMount(syncStatusGrid);
4383
+ renderSyncEmptyState(syncStatusGrid, diagnosticsLoadingState());
4307
4384
  renderActionList(syncActions, []);
4308
- if (syncMeta) syncMeta.textContent = "Loading sync status…";
4385
+ if (syncMeta) syncMeta.textContent = "Loading advanced sync diagnostics…";
4309
4386
  return;
4310
4387
  }
4311
4388
  const peers = status.peers || {};
4312
4389
  const pingPayload = status.ping || {};
4313
4390
  const syncPayload = status.sync || {};
4314
4391
  const lastSync = status.last_sync_at || status.last_sync_at_utc || null;
4315
- const lastPing = pingPayload.last_ping_at || status.last_ping_at || null;
4392
+ const lastPing = pingPayload.last_ping_at || status.last_ping_at || newestPeerPing(peers) || null;
4316
4393
  const syncError = status.last_sync_error || "";
4317
4394
  const pingError = status.last_ping_error || "";
4318
4395
  const pending = Number(status.pending || 0);
@@ -4328,8 +4405,8 @@
4328
4405
  const peerCount = Object.keys(peers).length;
4329
4406
  const syncNoPeers = !syncDisabled && peerCount === 0;
4330
4407
  if (syncMeta) {
4331
- const parts = syncDisabled ? ["State: Disabled", "Sync is optional and currently off"] : syncNoPeers ? ["State: No peers", "Add peers to enable replication"] : [
4332
- `State: ${daemonStateLabel}`,
4408
+ const parts = syncDisabled ? ["Advanced sync is off on this device", "Turn on sync in Settings → Device Sync when you want pairing payloads, peer status, and recent attempt details here"] : syncNoPeers ? ["Advanced sync is ready but idle", "Use Show pairing to connect another device, then this panel will start showing live peer status and recent attempts"] : [
4409
+ `Advanced state: ${daemonStateLabel}`,
4333
4410
  `Peers: ${peerCount}`,
4334
4411
  lastSync ? `Last sync: ${formatAgeShort(secondsSince(lastSync))} ago` : "Last sync: never"
4335
4412
  ];
@@ -4354,7 +4431,7 @@
4354
4431
  },
4355
4432
  {
4356
4433
  label: "Last sync",
4357
- value: "n/a"
4434
+ value: "Not running"
4358
4435
  }
4359
4436
  ] : syncNoPeers ? [
4360
4437
  {
@@ -4363,7 +4440,7 @@
4363
4440
  },
4364
4441
  {
4365
4442
  label: "Mode",
4366
- value: "Idle"
4443
+ value: "Ready to pair"
4367
4444
  },
4368
4445
  {
4369
4446
  label: "Pending events",
@@ -4371,7 +4448,7 @@
4371
4448
  },
4372
4449
  {
4373
4450
  label: "Last sync",
4374
- value: "n/a"
4451
+ value: "Waiting for first peer"
4375
4452
  }
4376
4453
  ] : [
4377
4454
  {
@@ -4387,7 +4464,7 @@
4387
4464
  value: lastSync ? `${formatAgeShort(secondsSince(lastSync))} ago` : "never"
4388
4465
  },
4389
4466
  {
4390
- label: "Last ping",
4467
+ label: "Last peer ping",
4391
4468
  value: lastPing ? `${formatAgeShort(secondsSince(lastPing))} ago` : "never"
4392
4469
  },
4393
4470
  {
@@ -4404,7 +4481,7 @@
4404
4481
  value: `${syncPayload.seconds_since_last}s`
4405
4482
  });
4406
4483
  if (!syncDisabled && !syncNoPeers && pingPayload.seconds_since_last) items.push({
4407
- label: "Since last ping",
4484
+ label: "Since last peer ping",
4408
4485
  value: `${pingPayload.seconds_since_last}s`
4409
4486
  });
4410
4487
  if (!syncDisabled && retentionEnabled && retentionLastError) items.push({
@@ -4419,7 +4496,7 @@
4419
4496
  command: "codemem sync start"
4420
4497
  });
4421
4498
  actions.push({
4422
- label: "Then run one immediate sync pass.",
4499
+ label: "Run one sync pass now.",
4423
4500
  command: "codemem sync once"
4424
4501
  });
4425
4502
  } else if (daemonState === "needs_attention") actions.push({
@@ -4436,7 +4513,7 @@
4436
4513
  command: "codemem sync restart && codemem sync once"
4437
4514
  });
4438
4515
  actions.push({
4439
- label: "Then run doctor for root cause.",
4516
+ label: "Run doctor for the root cause.",
4440
4517
  command: "codemem sync doctor"
4441
4518
  });
4442
4519
  } else if (!syncDisabled && !syncNoPeers && pending > 0) actions.push({
@@ -4450,7 +4527,7 @@
4450
4527
  if (!syncAttempts) return;
4451
4528
  const attempts = state.lastSyncAttempts;
4452
4529
  if (!Array.isArray(attempts) || !attempts.length) {
4453
- clearSyncMount(syncAttempts);
4530
+ renderSyncEmptyState(syncAttempts, noAttemptsState());
4454
4531
  return;
4455
4532
  }
4456
4533
  renderAttemptsList(syncAttempts, attempts.slice(0, 5).map((attempt) => {
@@ -4462,6 +4539,16 @@
4462
4539
  };
4463
4540
  }));
4464
4541
  }
4542
+ function renderSyncDiagnosticsUnavailable() {
4543
+ const syncStatusGrid = document.getElementById("syncStatusGrid");
4544
+ const syncAttempts = document.getElementById("syncAttempts");
4545
+ const syncMeta = document.getElementById("syncMeta");
4546
+ const syncActions = document.getElementById("syncActions");
4547
+ if (syncStatusGrid) renderSyncEmptyState(syncStatusGrid, diagnosticsUnavailableState());
4548
+ if (syncAttempts) renderSyncEmptyState(syncAttempts, unavailableAttemptsState());
4549
+ if (syncMeta) syncMeta.textContent = "Advanced diagnostics are unavailable right now. Refresh the page, or verify the local sync service before retrying.";
4550
+ renderActionList(syncActions, []);
4551
+ }
4465
4552
  function renderPairingCollapsible() {
4466
4553
  const mount = document.getElementById("syncPairingDisclosureMount");
4467
4554
  const contentHost = document.getElementById("syncPairingPanelMount");
@@ -4575,7 +4662,10 @@
4575
4662
  className: "sync-action",
4576
4663
  children: [/* @__PURE__ */ u("div", {
4577
4664
  className: "sync-action-text",
4578
- children: "Generate an invite to add another teammate to this team."
4665
+ children: ["Add another teammate.", /* @__PURE__ */ u("span", {
4666
+ className: "sync-action-command",
4667
+ children: "Generate an invite when you are ready to bring another device into this team."
4668
+ })]
4579
4669
  }), /* @__PURE__ */ u("button", {
4580
4670
  type: "button",
4581
4671
  className: "settings-button",
@@ -4593,9 +4683,9 @@
4593
4683
  className: "sync-action",
4594
4684
  children: [/* @__PURE__ */ u("div", {
4595
4685
  className: "sync-action-text",
4596
- children: ["No devices are paired yet.", /* @__PURE__ */ u("span", {
4686
+ children: ["Pair another device.", /* @__PURE__ */ u("span", {
4597
4687
  className: "sync-action-command",
4598
- children: "codemem sync pair --payload-only"
4688
+ children: "Use the pairing command in Advanced diagnostics when you are ready to connect one."
4599
4689
  })]
4600
4690
  }), /* @__PURE__ */ u("button", {
4601
4691
  type: "button",
@@ -4607,26 +4697,34 @@
4607
4697
  }
4608
4698
  function SyncInviteJoinPanels({ invitePanel, invitePanelOpen, inviteRestoreParent, joinPanel, joinRestoreParent, onToggleInvitePanel, pairedPeerCount, presenceStatus }) {
4609
4699
  return /* @__PURE__ */ u(k$2, { children: [
4610
- /* @__PURE__ */ u(InviteToggleRow, {
4700
+ presenceStatus === "not_enrolled" ? /* @__PURE__ */ u(k$2, { children: [
4701
+ joinPanel ? /* @__PURE__ */ u(ExistingElementSlot$1, {
4702
+ element: joinPanel,
4703
+ hidden: false,
4704
+ restoreParent: joinRestoreParent
4705
+ }) : null,
4706
+ /* @__PURE__ */ u("div", {
4707
+ className: "peer-meta",
4708
+ id: "syncJoinFeedback",
4709
+ hidden: true
4710
+ }),
4711
+ /* @__PURE__ */ u("div", {
4712
+ className: "sync-action",
4713
+ children: /* @__PURE__ */ u("div", {
4714
+ className: "sync-action-text",
4715
+ children: ["This device is not on the team yet.", /* @__PURE__ */ u("span", {
4716
+ className: "sync-action-command",
4717
+ children: "Import an invite or ask an admin to enroll it first."
4718
+ })]
4719
+ })
4720
+ })
4721
+ ] }) : null,
4722
+ presenceStatus !== "not_enrolled" ? /* @__PURE__ */ u(InviteToggleRow, {
4611
4723
  invitePanel,
4612
4724
  invitePanelOpen,
4613
4725
  inviteRestoreParent,
4614
4726
  onToggle: onToggleInvitePanel
4615
- }),
4616
- presenceStatus === "not_enrolled" ? /* @__PURE__ */ u(k$2, { children: [joinPanel ? /* @__PURE__ */ u(ExistingElementSlot$1, {
4617
- element: joinPanel,
4618
- hidden: false,
4619
- restoreParent: joinRestoreParent
4620
- }) : null, /* @__PURE__ */ u("div", {
4621
- className: "sync-action",
4622
- children: /* @__PURE__ */ u("div", {
4623
- className: "sync-action-text",
4624
- children: ["This device is not connected to the team yet.", /* @__PURE__ */ u("span", {
4625
- className: "sync-action-command",
4626
- children: "Import a team invite or ask your admin to enroll this device"
4627
- })]
4628
- })
4629
- })] }) : null,
4727
+ }) : null,
4630
4728
  !pairedPeerCount && presenceStatus === "posted" ? /* @__PURE__ */ u(PairingCopyRow, {}) : null
4631
4729
  ] });
4632
4730
  }
@@ -4671,7 +4769,30 @@
4671
4769
  }, `${item.peerName}:${item.actorId}:${item.scopeLabel}`)) });
4672
4770
  }
4673
4771
  //#endregion
4772
+ //#region src/tabs/sync/components/sync-inline-feedback.tsx
4773
+ function SyncInlineFeedback({ feedback }) {
4774
+ if (!feedback?.message) return null;
4775
+ return /* @__PURE__ */ u("div", {
4776
+ className: `sync-inline-feedback ${feedback.tone}`,
4777
+ role: feedback.tone === "warning" ? "alert" : "status",
4778
+ "aria-live": feedback.tone === "warning" ? "assertive" : "polite",
4779
+ children: feedback.message
4780
+ });
4781
+ }
4782
+ //#endregion
4674
4783
  //#region src/tabs/sync/components/team-sync-panel.tsx
4784
+ function SectionHeading({ count, label }) {
4785
+ return /* @__PURE__ */ u("div", {
4786
+ className: "sync-section-heading",
4787
+ children: [/* @__PURE__ */ u("div", {
4788
+ className: "sync-action-text sync-section-label",
4789
+ children: label
4790
+ }), count ? /* @__PURE__ */ u("span", {
4791
+ className: "badge actor-badge sync-section-count",
4792
+ children: count
4793
+ }) : null]
4794
+ });
4795
+ }
4675
4796
  function AttentionRow({ item, onAction }) {
4676
4797
  const [busy, setBusy] = d(false);
4677
4798
  return /* @__PURE__ */ u("div", {
@@ -4700,228 +4821,256 @@
4700
4821
  }
4701
4822
  function PendingJoinRequestRow({ request, onApprove, onDeny }) {
4702
4823
  const [busyAction, setBusyAction] = d(null);
4824
+ const [feedback, setFeedback] = d(null);
4703
4825
  const [approveLabel, setApproveLabel] = d("Approve");
4704
4826
  const [denyLabel, setDenyLabel] = d("Deny");
4705
4827
  return /* @__PURE__ */ u("div", {
4706
4828
  className: "actor-row",
4707
- children: [/* @__PURE__ */ u("div", {
4708
- className: "actor-details",
4709
- children: [/* @__PURE__ */ u("div", {
4710
- className: "actor-title",
4711
- children: request.displayName
4712
- }), /* @__PURE__ */ u("div", {
4713
- className: "peer-meta",
4714
- children: ["request: ", request.requestId]
4715
- })]
4716
- }), /* @__PURE__ */ u("div", {
4717
- className: "actor-actions",
4718
- children: [/* @__PURE__ */ u("button", {
4719
- type: "button",
4720
- className: "settings-button",
4721
- disabled: busyAction !== null,
4722
- onClick: async () => {
4723
- setBusyAction("approve");
4724
- setApproveLabel("Approving…");
4725
- try {
4726
- await onApprove(request);
4727
- setApproveLabel("Approve");
4728
- } catch {
4729
- setApproveLabel("Retry");
4730
- } finally {
4731
- setBusyAction(null);
4732
- }
4733
- },
4734
- children: approveLabel
4735
- }), /* @__PURE__ */ u("button", {
4736
- type: "button",
4737
- className: "settings-button",
4738
- disabled: busyAction !== null,
4739
- onClick: async () => {
4740
- setBusyAction("deny");
4741
- setDenyLabel("Denying…");
4742
- try {
4743
- await onDeny(request);
4744
- setDenyLabel("Deny");
4745
- } catch {
4746
- setDenyLabel("Retry deny");
4747
- } finally {
4748
- setBusyAction(null);
4749
- }
4750
- },
4751
- children: denyLabel
4752
- })]
4753
- })]
4754
- });
4755
- }
4756
- function DiscoveredDeviceRow({ row, onInspectConflict, onRemoveConflict, onReview }) {
4757
- const [busy, setBusy] = d(null);
4758
- const [reviewLabel, setReviewLabel] = d(row.actionLabel || "Review device");
4759
- const [removeLabel, setRemoveLabel] = d("Remove broken device record");
4760
- return /* @__PURE__ */ u("div", {
4761
- className: "actor-row",
4762
- "data-discovered-device-id": row.deviceId,
4763
- children: [/* @__PURE__ */ u("div", {
4764
- className: "actor-details",
4765
- children: [/* @__PURE__ */ u("div", {
4766
- className: "actor-title",
4767
- children: [
4768
- /* @__PURE__ */ u("strong", {
4769
- title: row.displayTitle || void 0,
4770
- children: row.displayName
4771
- }),
4772
- /* @__PURE__ */ u("span", {
4773
- className: `badge actor-badge${row.availabilityLabel === "Offline" ? "" : " local"}`,
4774
- children: row.availabilityLabel
4775
- }),
4776
- /* @__PURE__ */ u("span", {
4777
- className: "badge actor-badge",
4778
- children: row.connectionLabel
4779
- }),
4780
- row.approvalBadgeLabel ? /* @__PURE__ */ u("span", {
4781
- className: "badge actor-badge",
4782
- children: row.approvalBadgeLabel
4783
- }) : null
4784
- ]
4785
- }), /* @__PURE__ */ u("div", {
4786
- className: "peer-meta",
4787
- children: row.note
4788
- })]
4789
- }), /* @__PURE__ */ u("div", {
4790
- className: "actor-actions",
4791
- children: [
4792
- row.mode === "accept" ? /* @__PURE__ */ u("button", {
4829
+ children: [
4830
+ /* @__PURE__ */ u("div", {
4831
+ className: "actor-details",
4832
+ children: /* @__PURE__ */ u("div", {
4833
+ className: "actor-title",
4834
+ title: request.requestId || void 0,
4835
+ children: request.displayName
4836
+ })
4837
+ }),
4838
+ /* @__PURE__ */ u("div", {
4839
+ className: "actor-actions",
4840
+ children: [/* @__PURE__ */ u("button", {
4793
4841
  type: "button",
4794
4842
  className: "settings-button",
4795
- disabled: busy !== null,
4843
+ disabled: busyAction !== null,
4796
4844
  onClick: async () => {
4797
- setBusy("review");
4798
- setReviewLabel("Opening…");
4845
+ setBusyAction("approve");
4846
+ setApproveLabel("Approving…");
4799
4847
  try {
4800
- await onReview(row);
4801
- setReviewLabel(row.actionLabel || "Review device");
4848
+ setFeedback(await onApprove(request) || null);
4849
+ setApproveLabel("Approve");
4802
4850
  } catch {
4803
- setReviewLabel("Retry review");
4851
+ setApproveLabel("Retry");
4804
4852
  } finally {
4805
- setBusy(null);
4853
+ setBusyAction(null);
4806
4854
  }
4807
4855
  },
4808
- children: reviewLabel
4809
- }) : null,
4810
- row.mode === "stale" || row.mode === "ambiguous" || row.mode === "scope-pending" ? /* @__PURE__ */ u("div", {
4811
- className: "peer-meta",
4812
- children: row.actionMessage
4813
- }) : null,
4814
- row.mode === "paired" && row.pairedMessage ? /* @__PURE__ */ u("div", {
4815
- className: "peer-meta",
4816
- children: row.pairedMessage
4817
- }) : null,
4818
- row.mode === "conflict" ? /* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u("button", {
4819
- type: "button",
4820
- className: "settings-button",
4821
- disabled: busy !== null,
4822
- onClick: () => onInspectConflict(row),
4823
- children: "Open device details"
4856
+ children: approveLabel
4824
4857
  }), /* @__PURE__ */ u("button", {
4825
4858
  type: "button",
4826
4859
  className: "settings-button",
4827
- disabled: busy !== null,
4860
+ disabled: busyAction !== null,
4828
4861
  onClick: async () => {
4829
- setBusy("remove");
4830
- setRemoveLabel("Removing…");
4862
+ setBusyAction("deny");
4863
+ setDenyLabel("Denying…");
4831
4864
  try {
4832
- await onRemoveConflict(row);
4833
- setRemoveLabel("Remove broken device record");
4865
+ setFeedback(await onDeny(request) || null);
4866
+ setDenyLabel("Deny");
4834
4867
  } catch {
4835
- setRemoveLabel("Retry remove");
4868
+ setDenyLabel("Retry");
4836
4869
  } finally {
4837
- setBusy(null);
4870
+ setBusyAction(null);
4838
4871
  }
4839
4872
  },
4840
- children: removeLabel
4841
- })] }) : null
4842
- ]
4843
- })]
4873
+ children: denyLabel
4874
+ })]
4875
+ }),
4876
+ /* @__PURE__ */ u(SyncInlineFeedback, { feedback })
4877
+ ]
4878
+ });
4879
+ }
4880
+ function DiscoveredDeviceRow({ row, onInspectConflict, onRemoveConflict, onReview }) {
4881
+ const [busy, setBusy] = d(null);
4882
+ const [feedback, setFeedback] = d(null);
4883
+ const [reviewLabel, setReviewLabel] = d(row.actionLabel || "Review device");
4884
+ const [removeLabel, setRemoveLabel] = d("Remove broken device record");
4885
+ return /* @__PURE__ */ u("div", {
4886
+ className: "actor-row",
4887
+ "data-discovered-device-id": row.deviceId,
4888
+ children: [
4889
+ /* @__PURE__ */ u("div", {
4890
+ className: "actor-details",
4891
+ children: [/* @__PURE__ */ u("div", {
4892
+ className: "actor-title",
4893
+ children: [
4894
+ /* @__PURE__ */ u("strong", {
4895
+ title: row.displayTitle || void 0,
4896
+ children: row.displayName
4897
+ }),
4898
+ /* @__PURE__ */ u("span", {
4899
+ className: `badge actor-badge${row.availabilityLabel === "Offline" ? "" : " local"}`,
4900
+ children: row.availabilityLabel
4901
+ }),
4902
+ /* @__PURE__ */ u("span", {
4903
+ className: "badge actor-badge",
4904
+ children: row.connectionLabel
4905
+ }),
4906
+ row.approvalBadgeLabel ? /* @__PURE__ */ u("span", {
4907
+ className: "badge actor-badge",
4908
+ children: row.approvalBadgeLabel
4909
+ }) : null
4910
+ ]
4911
+ }), /* @__PURE__ */ u("div", {
4912
+ className: "peer-meta",
4913
+ children: row.note
4914
+ })]
4915
+ }),
4916
+ /* @__PURE__ */ u("div", {
4917
+ className: "actor-actions",
4918
+ children: [
4919
+ row.mode === "accept" ? /* @__PURE__ */ u("button", {
4920
+ type: "button",
4921
+ className: "settings-button",
4922
+ disabled: busy !== null,
4923
+ onClick: async () => {
4924
+ setBusy("review");
4925
+ setReviewLabel("Reviewing…");
4926
+ try {
4927
+ setFeedback(await onReview(row) || null);
4928
+ setReviewLabel(row.actionLabel || "Review device");
4929
+ } catch {
4930
+ setReviewLabel("Retry");
4931
+ } finally {
4932
+ setBusy(null);
4933
+ }
4934
+ },
4935
+ children: reviewLabel
4936
+ }) : null,
4937
+ (row.mode === "stale" || row.mode === "ambiguous" || row.mode === "scope-pending") && row.actionMessage ? /* @__PURE__ */ u("div", {
4938
+ className: "peer-meta",
4939
+ children: row.actionMessage
4940
+ }) : null,
4941
+ row.mode === "paired" && row.pairedMessage ? /* @__PURE__ */ u("div", {
4942
+ className: "peer-meta",
4943
+ children: row.pairedMessage
4944
+ }) : null,
4945
+ row.mode === "conflict" ? /* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u("button", {
4946
+ type: "button",
4947
+ className: "settings-button",
4948
+ disabled: busy !== null,
4949
+ onClick: () => onInspectConflict(row),
4950
+ children: "Open device details"
4951
+ }), /* @__PURE__ */ u("button", {
4952
+ type: "button",
4953
+ className: "settings-button",
4954
+ disabled: busy !== null,
4955
+ onClick: async () => {
4956
+ setBusy("remove");
4957
+ setRemoveLabel("Removing…");
4958
+ try {
4959
+ setFeedback(await onRemoveConflict(row) || null);
4960
+ setRemoveLabel("Remove broken device record");
4961
+ } catch {
4962
+ setRemoveLabel("Retry");
4963
+ } finally {
4964
+ setBusy(null);
4965
+ }
4966
+ },
4967
+ children: removeLabel
4968
+ })] }) : null
4969
+ ]
4970
+ }),
4971
+ /* @__PURE__ */ u(SyncInlineFeedback, { feedback })
4972
+ ]
4844
4973
  });
4845
4974
  }
4846
4975
  function ActionContent(props) {
4847
4976
  const hasAttentionItems = props.actionItems.length > 0;
4977
+ const hasOtherActionableWork = props.actionableCount > props.actionItems.length;
4848
4978
  return /* @__PURE__ */ u(k$2, { children: [
4849
- hasAttentionItems ? /* @__PURE__ */ u("div", {
4850
- className: "sync-action-text",
4851
- children: "Needs attention"
4852
- }) : null,
4853
- hasAttentionItems ? props.actionItems.slice(0, 4).map((item) => /* @__PURE__ */ u(AttentionRow, {
4979
+ hasAttentionItems || hasOtherActionableWork || props.presenceStatus !== "posted" ? /* @__PURE__ */ u(SectionHeading, { label: "Next steps" }) : null,
4980
+ hasAttentionItems ? props.actionItems.map((item) => /* @__PURE__ */ u(AttentionRow, {
4854
4981
  item,
4855
4982
  onAction: props.onAttentionAction
4856
4983
  }, item.id)) : null,
4857
- !hasAttentionItems && props.presenceStatus === "posted" ? /* @__PURE__ */ u("div", {
4984
+ !hasAttentionItems && !hasOtherActionableWork && props.presenceStatus === "posted" ? /* @__PURE__ */ u("div", {
4858
4985
  className: "sync-action",
4859
4986
  children: /* @__PURE__ */ u("div", {
4860
4987
  className: "sync-action-text",
4861
- children: ["No immediate issues", /* @__PURE__ */ u("span", {
4862
- className: "sync-action-command",
4863
- children: "Your devices and team records do not currently need review."
4864
- })]
4988
+ children: "Everything is healthy."
4989
+ })
4990
+ }) : null,
4991
+ !hasAttentionItems && hasOtherActionableWork && props.presenceStatus === "posted" ? /* @__PURE__ */ u("div", {
4992
+ className: "sync-action",
4993
+ children: /* @__PURE__ */ u("div", {
4994
+ className: "sync-action-text",
4995
+ children: "Review the team items below when you are ready."
4865
4996
  })
4866
4997
  }) : null,
4867
4998
  !hasAttentionItems && props.presenceStatus === "not_enrolled" ? /* @__PURE__ */ u("div", {
4868
4999
  className: "sync-action",
4869
5000
  children: /* @__PURE__ */ u("div", {
4870
5001
  className: "sync-action-text",
4871
- children: ["This device still needs team enrollment", /* @__PURE__ */ u("span", {
5002
+ children: ["This device needs team enrollment", /* @__PURE__ */ u("span", {
4872
5003
  className: "sync-action-command",
4873
- children: "Import an invite or ask your admin to enroll this device before expecting sync activity here."
5004
+ children: "Import an invite or ask an admin to enroll it."
4874
5005
  })]
4875
5006
  })
4876
- }) : null,
4877
- props.children
5007
+ }) : null
4878
5008
  ] });
4879
5009
  }
5010
+ function TeamActionsContent({ children }) {
5011
+ if (!children) return null;
5012
+ return /* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u(SectionHeading, { label: "Keep the team moving" }), children] });
5013
+ }
4880
5014
  function TeamStatusPortal({ mount, statusSummary }) {
4881
5015
  return $(/* @__PURE__ */ u("div", {
4882
5016
  className: "sync-team-summary",
4883
- children: [/* @__PURE__ */ u("div", {
4884
- className: "sync-team-status-row",
4885
- children: [/* @__PURE__ */ u("span", {
4886
- className: "sync-team-status-label",
4887
- children: "Status"
4888
- }), /* @__PURE__ */ u("span", {
4889
- className: statusSummary.badgeClassName,
4890
- children: statusSummary.presenceLabel
4891
- })]
4892
- }), /* @__PURE__ */ u("div", {
4893
- className: "sync-team-metrics",
4894
- children: statusSummary.metricsText
4895
- })]
5017
+ children: [
5018
+ /* @__PURE__ */ u("div", {
5019
+ className: "sync-team-status-row",
5020
+ children: [/* @__PURE__ */ u("span", {
5021
+ className: "sync-team-status-label",
5022
+ children: "Team status"
5023
+ }), /* @__PURE__ */ u("span", {
5024
+ className: statusSummary.badgeClassName,
5025
+ children: statusSummary.presenceLabel
5026
+ })]
5027
+ }),
5028
+ statusSummary.headline ? /* @__PURE__ */ u("div", {
5029
+ className: "sync-team-headline",
5030
+ children: statusSummary.headline
5031
+ }) : null,
5032
+ /* @__PURE__ */ u("div", {
5033
+ className: "sync-team-metrics sync-team-metrics-secondary",
5034
+ children: statusSummary.metricsText
5035
+ })
5036
+ ]
4896
5037
  }), mount);
4897
5038
  }
4898
5039
  function DiscoveredPortal({ mount, rows, onInspectConflict, onRemoveConflict, onReview }) {
4899
- if (!mount) return null;
4900
- return $(/* @__PURE__ */ u(k$2, { children: rows.map((row) => /* @__PURE__ */ u(DiscoveredDeviceRow, {
4901
- row,
4902
- onInspectConflict,
4903
- onRemoveConflict,
4904
- onReview
4905
- }, row.deviceId)) }), mount);
5040
+ if (!mount || !rows.length && !state.syncDiscoveredFeedback) return null;
5041
+ return $(/* @__PURE__ */ u(k$2, { children: [
5042
+ /* @__PURE__ */ u(SectionHeading, {
5043
+ count: rows.length || void 0,
5044
+ label: "Devices seen on team"
5045
+ }),
5046
+ /* @__PURE__ */ u(SyncInlineFeedback, { feedback: state.syncDiscoveredFeedback }),
5047
+ rows.map((row) => /* @__PURE__ */ u(DiscoveredDeviceRow, {
5048
+ row,
5049
+ onInspectConflict,
5050
+ onRemoveConflict,
5051
+ onReview
5052
+ }, row.deviceId))
5053
+ ] }), mount);
4906
5054
  }
4907
5055
  function PendingRequestsPortal({ mount, requests, onApprove, onDeny }) {
4908
- if (!mount || !requests.length) return null;
4909
- return $(/* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u("div", {
4910
- className: "peer-meta",
4911
- children: [
4912
- requests.length,
4913
- " pending join request",
4914
- requests.length === 1 ? "" : "s"
4915
- ]
4916
- }), requests.map((request) => /* @__PURE__ */ u(PendingJoinRequestRow, {
4917
- request,
4918
- onApprove,
4919
- onDeny
4920
- }, request.requestId))] }), mount);
5056
+ if (!mount || !requests.length && !state.syncJoinRequestsFeedback) return null;
5057
+ return $(/* @__PURE__ */ u(k$2, { children: [
5058
+ /* @__PURE__ */ u(SectionHeading, {
5059
+ count: requests.length || void 0,
5060
+ label: "Pending join requests"
5061
+ }),
5062
+ /* @__PURE__ */ u(SyncInlineFeedback, { feedback: state.syncJoinRequestsFeedback }),
5063
+ requests.map((request) => /* @__PURE__ */ u(PendingJoinRequestRow, {
5064
+ request,
5065
+ onApprove,
5066
+ onDeny
5067
+ }, request.requestId))
5068
+ ] }), mount);
4921
5069
  }
4922
5070
  function TeamSyncPanel(props) {
4923
5071
  return /* @__PURE__ */ u(k$2, { children: [
4924
5072
  /* @__PURE__ */ u(ActionContent, { ...props }),
5073
+ /* @__PURE__ */ u(TeamActionsContent, { children: props.children }),
4925
5074
  /* @__PURE__ */ u(TeamStatusPortal, {
4926
5075
  mount: props.listMount,
4927
5076
  statusSummary: props.statusSummary
@@ -9869,14 +10018,43 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9869
10018
  }
9870
10019
  function setInviteOutputVisibility() {
9871
10020
  const syncInviteOutput = document.getElementById("syncInviteOutput");
10021
+ const syncInviteWarnings = document.getElementById("syncInviteWarnings");
9872
10022
  if (!syncInviteOutput) return;
9873
10023
  const encoded = String(state.lastTeamInvite?.encoded || "").trim();
9874
10024
  syncInviteOutput.value = encoded;
9875
10025
  syncInviteOutput.hidden = !encoded;
10026
+ if (syncInviteWarnings) {
10027
+ const warnings = Array.isArray(state.lastTeamInvite?.warnings) ? state.lastTeamInvite.warnings : [];
10028
+ syncInviteWarnings.textContent = warnings.join(" · ");
10029
+ syncInviteWarnings.hidden = warnings.length === 0;
10030
+ }
10031
+ }
10032
+ function setJoinFeedbackVisibility() {
10033
+ const syncJoinFeedback = document.getElementById("syncJoinFeedback");
10034
+ if (!syncJoinFeedback) return;
10035
+ const feedback = state.syncJoinFlowFeedback;
10036
+ syncJoinFeedback.textContent = feedback?.message || "";
10037
+ syncJoinFeedback.hidden = !feedback?.message;
10038
+ syncJoinFeedback.setAttribute("role", feedback?.tone === "warning" ? "alert" : "status");
10039
+ syncJoinFeedback.setAttribute("aria-live", feedback?.tone === "warning" ? "assertive" : "polite");
10040
+ syncJoinFeedback.className = `peer-meta${feedback ? ` ${feedback.tone === "warning" ? "sync-inline-feedback warning" : "sync-inline-feedback success"}` : ""}`;
9876
10041
  }
9877
10042
  function clearContent(node) {
9878
10043
  if (node) node.textContent = "";
9879
10044
  }
10045
+ function pulseAttentionTarget(target) {
10046
+ if (!(target instanceof HTMLElement)) return;
10047
+ target.classList.remove("sync-attention-target");
10048
+ target.offsetWidth;
10049
+ target.classList.add("sync-attention-target");
10050
+ window.setTimeout(() => target.classList.remove("sync-attention-target"), 900);
10051
+ }
10052
+ function prefersReducedMotion() {
10053
+ return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
10054
+ }
10055
+ function syncScrollBehavior() {
10056
+ return prefersReducedMotion() ? "auto" : "smooth";
10057
+ }
9880
10058
  function renderInvitePolicySelect() {
9881
10059
  const mount = document.getElementById("syncInvitePolicyMount");
9882
10060
  if (!mount) return;
@@ -9944,11 +10122,13 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9944
10122
  const meta = document.getElementById("syncTeamMeta");
9945
10123
  const setupPanel = document.getElementById("syncSetupPanel");
9946
10124
  const list = document.getElementById("syncTeamStatus");
10125
+ const listHeading = list?.previousElementSibling;
9947
10126
  const actions = document.getElementById("syncTeamActions");
9948
10127
  if (!meta || !setupPanel || !list || !actions) return;
9949
10128
  renderAdminSetupDisclosure();
9950
10129
  renderInvitePolicySelect();
9951
10130
  setInviteOutputVisibility();
10131
+ setJoinFeedbackVisibility();
9952
10132
  const invitePanel = document.getElementById("syncInvitePanel");
9953
10133
  const inviteRestoreParent = document.getElementById("syncAdminSection");
9954
10134
  const joinPanel = document.getElementById("syncJoinPanel");
@@ -9973,10 +10153,13 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9973
10153
  const focusAttentionTarget = (item) => {
9974
10154
  if (item.kind === "possible-duplicate-person") {
9975
10155
  const actorList = document.getElementById("syncActorsList");
9976
- if (actorList instanceof HTMLElement) actorList.scrollIntoView({
9977
- block: "center",
9978
- behavior: "smooth"
9979
- });
10156
+ if (actorList instanceof HTMLElement) {
10157
+ actorList.scrollIntoView({
10158
+ block: "center",
10159
+ behavior: syncScrollBehavior()
10160
+ });
10161
+ pulseAttentionTarget(actorList);
10162
+ }
9980
10163
  return;
9981
10164
  }
9982
10165
  const deviceId = String(item.deviceId || "").trim();
@@ -9986,10 +10169,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9986
10169
  if (renameInput instanceof HTMLInputElement) {
9987
10170
  renameInput.scrollIntoView({
9988
10171
  block: "center",
9989
- behavior: "smooth"
10172
+ behavior: syncScrollBehavior()
9990
10173
  });
9991
10174
  renameInput.focus();
9992
10175
  renameInput.select();
10176
+ pulseAttentionTarget(renameInput);
9993
10177
  return;
9994
10178
  }
9995
10179
  }
@@ -9997,21 +10181,25 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9997
10181
  if (peerCard instanceof HTMLElement) {
9998
10182
  peerCard.scrollIntoView({
9999
10183
  block: "center",
10000
- behavior: "smooth"
10184
+ behavior: syncScrollBehavior()
10001
10185
  });
10186
+ pulseAttentionTarget(peerCard);
10002
10187
  return;
10003
10188
  }
10004
10189
  const discoveredRow = document.querySelector(`[data-discovered-device-id="${CSS.escape(deviceId)}"]`);
10005
- if (discoveredRow instanceof HTMLElement) discoveredRow.scrollIntoView({
10006
- block: "center",
10007
- behavior: "smooth"
10008
- });
10190
+ if (discoveredRow instanceof HTMLElement) {
10191
+ discoveredRow.scrollIntoView({
10192
+ block: "center",
10193
+ behavior: syncScrollBehavior()
10194
+ });
10195
+ pulseAttentionTarget(discoveredRow);
10196
+ }
10009
10197
  };
10010
10198
  const reviewDuplicatePeople = async (item) => {
10011
10199
  const actorIds = Array.isArray(item.actorIds) ? item.actorIds.map((value) => String(value || "").trim()).filter(Boolean) : [];
10012
10200
  const actors = (Array.isArray(state.lastSyncActors) ? state.lastSyncActors : []).filter((actor) => actorIds.includes(String(actor?.actor_id || "").trim()));
10013
10201
  if (actors.length < 2) {
10014
- showGlobalNotice("This possible duplicate no longer has enough people to review.", "warning");
10202
+ showGlobalNotice("This duplicate review is outdated. Refresh the card and review the remaining people entries.", "warning");
10015
10203
  return;
10016
10204
  }
10017
10205
  const result = await openDuplicatePersonDialog({
@@ -10033,7 +10221,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10033
10221
  const primary = actors.find((actor) => String(actor?.actor_id || "") === result.primaryActorId);
10034
10222
  const secondary = actors.find((actor) => String(actor?.actor_id || "") === result.secondaryActorId);
10035
10223
  if (!primary?.actor_id || !secondary?.actor_id) {
10036
- showGlobalNotice("Could not determine which people to combine.", "warning");
10224
+ showGlobalNotice("Could not determine which people to combine. Refresh People & devices and try the review again.", "warning");
10037
10225
  return;
10038
10226
  }
10039
10227
  try {
@@ -10060,7 +10248,8 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10060
10248
  });
10061
10249
  };
10062
10250
  const configured = Boolean(coordinator && coordinator.configured);
10063
- meta.textContent = configured ? `Connected to ${String(coordinator.coordinator_url || "")} · group: ${(coordinator.groups || []).join(", ") || "none"}` : "Start here: join an existing team or create a new one before connecting devices and people.";
10251
+ meta.textContent = configured ? `Team: ${(coordinator.groups || []).join(", ") || "none"}` : "Start by joining an existing team or creating one, then connect people and devices.";
10252
+ meta.title = configured ? String(coordinator.coordinator_url || "").trim() : "";
10064
10253
  if (!configured) {
10065
10254
  teardownTeamSyncRender(actions, [
10066
10255
  list,
@@ -10069,6 +10258,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10069
10258
  ]);
10070
10259
  setupPanel.hidden = false;
10071
10260
  list.hidden = true;
10261
+ if (listHeading) listHeading.hidden = true;
10072
10262
  actions.hidden = true;
10073
10263
  if (joinRequests) joinRequests.hidden = true;
10074
10264
  if (discoveredPanel) discoveredPanel.hidden = true;
@@ -10076,18 +10266,17 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10076
10266
  }
10077
10267
  setupPanel.hidden = true;
10078
10268
  list.hidden = false;
10269
+ if (listHeading) listHeading.hidden = false;
10079
10270
  actions.hidden = false;
10080
10271
  const presenceStatus = String(coordinator.presence_status || "");
10081
- const presenceLabel = presenceStatus === "posted" ? "Connected" : presenceStatus === "not_enrolled" ? "Not connected — import an invite or ask your admin to enroll this device" : "Connection error";
10082
- const metricParts = [`Connected devices: ${Number(syncView.summary?.connectedDeviceCount || 0)}`, `Seen on team: ${Number(syncView.summary?.seenOnTeamCount || 0)}`];
10083
- if (Number(syncView.summary?.offlineTeamDeviceCount || 0) > 0) metricParts.push(`Offline on team: ${Number(syncView.summary?.offlineTeamDeviceCount || 0)}`);
10084
- const statusSummary = {
10085
- badgeClassName: `pill ${presenceStatus === "posted" ? "pill-success" : presenceStatus === "not_enrolled" ? "pill-warning" : "pill-error"}`,
10086
- metricsText: metricParts.join(" · "),
10087
- presenceLabel
10088
- };
10272
+ const presenceLabel = presenceStatus === "posted" ? "Connected" : presenceStatus === "not_enrolled" ? "Needs enrollment" : "Connection error";
10089
10273
  const localPeers = Array.isArray(state.lastSyncPeers) ? state.lastSyncPeers : [];
10090
10274
  const attentionItems = Array.isArray(syncView.attentionItems) ? syncView.attentionItems : [];
10275
+ const connectedCount = Number(syncView.summary?.connectedDeviceCount || 0);
10276
+ const seenOnTeamCount = Number(syncView.summary?.seenOnTeamCount || 0);
10277
+ const offlineTeamDeviceCount = Number(syncView.summary?.offlineTeamDeviceCount || 0);
10278
+ const metricParts = [`Connected ${connectedCount}`, `Team ${seenOnTeamCount}`];
10279
+ if (offlineTeamDeviceCount > 0) metricParts.push(`Offline ${offlineTeamDeviceCount}`);
10091
10280
  const discoveredRows = (Array.isArray(coordinator.discovered_devices) ? coordinator.discovered_devices : []).map((device) => {
10092
10281
  const deviceId = String(device.device_id || "").trim();
10093
10282
  const displayName = resolveFriendlyDeviceName({
@@ -10107,33 +10296,28 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10107
10296
  const canAccept = Boolean(deviceId) && Boolean(fingerprint) && !pairedPeer && !device.stale && !hasAmbiguousCoordinatorGroup;
10108
10297
  const addresses = Array.isArray(device.addresses) ? device.addresses : [];
10109
10298
  const noteParts = [addresses.length ? addresses.map((address) => isSyncRedactionEnabled() ? redactAddress(String(address || "")) : String(address || "")).filter(Boolean).join(" · ") : "No fresh addresses"];
10110
- if (displayTitle) noteParts.push(`device id: ${deviceId}`);
10299
+ if (!addresses.length && displayTitle) noteParts.push(`device id: ${deviceId}`);
10111
10300
  let actionMessage = null;
10112
10301
  let mode = canAccept ? "accept" : "none";
10113
10302
  let pairedMessage = null;
10114
- if (hasConflict) {
10115
- noteParts.push("repair the local peer before accepting this discovered device");
10116
- mode = "conflict";
10117
- } else if (hasAmbiguousCoordinatorGroup) {
10118
- noteParts.push("this device appears in multiple coordinator groups; review team setup before approving it here");
10119
- actionMessage = "This device is visible through multiple coordinator groups. Fix that team setup first, then approve it here.";
10303
+ if (hasConflict) mode = "conflict";
10304
+ else if (hasAmbiguousCoordinatorGroup) {
10305
+ actionMessage = "This device appears in multiple coordinator groups. Review team setup first or ask an admin to clean up the duplicate enrollment before approving it here.";
10120
10306
  mode = "ambiguous";
10121
10307
  } else if (pairedPeer && isPeerScopeReviewPending(deviceId)) {
10122
- noteParts.push("scope review pending in People");
10123
- actionMessage = `Review this device's scope in People & devices next.`;
10308
+ actionMessage = `Finish this device's scope review in People & devices before you sync it.`;
10124
10309
  mode = "scope-pending";
10125
10310
  } else if (pairedPeer?.last_error) {
10126
- noteParts.push(`paired error: ${String(pairedPeer.last_error)}`);
10311
+ noteParts.push(`error: ${String(pairedPeer.last_error)}`);
10127
10312
  mode = "paired";
10128
10313
  } else if (pairedPeer?.status?.peer_state) {
10129
- noteParts.push(`paired status: ${String(pairedPeer.status.peer_state)}`);
10314
+ noteParts.push(`status: ${String(pairedPeer.status.peer_state)}`);
10130
10315
  mode = "paired";
10131
10316
  } else if (!pairedPeer && device.stale) {
10132
- actionMessage = "Wait for a fresh coordinator presence update.";
10317
+ actionMessage = "Wait for a fresh coordinator presence update, then review this device again here.";
10133
10318
  mode = "stale";
10134
10319
  } else if (pairedPeer) mode = "paired";
10135
- if (approvalSummary.description) noteParts.push(approvalSummary.description);
10136
- if (mode === "paired") pairedMessage = approvalSummary.state === "waiting-for-other-device" ? approvalSummary.description || "Waiting on the other device." : String(pairedPeer?.last_error || "").toLowerCase().includes("401") && String(pairedPeer?.last_error || "").toLowerCase().includes("unauthorized") ? "Waiting for the other device to trust this one before sync can work." : "Manage this device in People & devices.";
10320
+ if (mode === "paired") pairedMessage = approvalSummary.state === "waiting-for-other-device" ? approvalSummary.description || "Waiting on the other device." : String(pairedPeer?.last_error || "").toLowerCase().includes("401") && String(pairedPeer?.last_error || "").toLowerCase().includes("unauthorized") ? "Waiting for the other device to trust this one before sync can work." : null;
10137
10321
  return {
10138
10322
  actionMessage,
10139
10323
  actionLabel: approvalSummary.actionLabel || "Review device",
@@ -10153,9 +10337,19 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10153
10337
  displayName: String(request.display_name || request.device_id || "Pending device"),
10154
10338
  requestId: String(request.request_id || "")
10155
10339
  }));
10156
- if (discoveredPanel) discoveredPanel.hidden = discoveredRows.length === 0;
10157
- if (discoveredMeta) discoveredMeta.textContent = discoveredRows.length ? "These devices are visible through team discovery. Review them before connecting them on this machine." : "";
10158
- if (joinRequests) joinRequests.hidden = pendingJoinRequests.length === 0;
10340
+ const discoveredActionableCount = discoveredRows.filter((row) => row.mode === "accept" || row.mode === "scope-pending").length;
10341
+ const actionableCount = attentionItems.length + pendingJoinRequests.length + discoveredActionableCount;
10342
+ const teamLabel = (coordinator.groups || []).join(", ") || "none";
10343
+ const statusSummary = {
10344
+ badgeClassName: `pill ${presenceStatus === "posted" ? "pill-success" : presenceStatus === "not_enrolled" ? "pill-warning" : "pill-error"}`,
10345
+ headline: presenceStatus === "posted" ? actionableCount > 0 ? `${actionableCount} item${actionableCount === 1 ? "" : "s"} need attention` : "Everything is healthy" : presenceStatus === "not_enrolled" ? "This device is not enrolled in the team yet" : "Sync needs attention",
10346
+ metricsText: metricParts.join(" · "),
10347
+ presenceLabel
10348
+ };
10349
+ meta.textContent = presenceStatus === "posted" ? actionableCount > 0 ? `Team: ${teamLabel}. Start with the next step below, then scan the current team status.` : `Team: ${teamLabel}. Team status and device details are below.` : presenceStatus === "not_enrolled" ? `Team: ${teamLabel}. Enroll this device first, then return here to review the rest of the team.` : `Team: ${teamLabel}. Fix the current sync issue first, then use the rest of this card to verify the team state.`;
10350
+ if (discoveredPanel) discoveredPanel.hidden = discoveredRows.length === 0 && !state.syncDiscoveredFeedback;
10351
+ if (discoveredMeta) discoveredMeta.textContent = discoveredRows.length ? "Review anything here that still needs trust, repair, or approval." : "";
10352
+ if (joinRequests) joinRequests.hidden = pendingJoinRequests.length === 0 && !state.syncJoinRequestsFeedback;
10159
10353
  teardownTeamSyncRender(actions, [
10160
10354
  list,
10161
10355
  joinRequests,
@@ -10166,6 +10360,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10166
10360
  actions.appendChild(actionMount);
10167
10361
  renderIntoSyncMount(actionMount, _$1(TeamSyncPanel, {
10168
10362
  actionItems: attentionItems,
10363
+ actionableCount,
10169
10364
  children: _$1(SyncInviteJoinPanels, {
10170
10365
  invitePanel,
10171
10366
  invitePanelOpen: teamInvitePanelOpen,
@@ -10187,11 +10382,18 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10187
10382
  onApproveJoinRequest: async (request) => {
10188
10383
  try {
10189
10384
  await reviewJoinRequest(request.requestId, "approve");
10190
- showGlobalNotice(`Approved ${request.displayName}. They can now sync with the team.`);
10385
+ const feedback = {
10386
+ message: `Approved ${request.displayName}. They can now sync with the team.`,
10387
+ tone: "success"
10388
+ };
10389
+ state.syncJoinRequestsFeedback = feedback;
10191
10390
  await _loadSyncData$1();
10391
+ return feedback;
10192
10392
  } catch (error) {
10193
- showGlobalNotice(friendlyError(error, "Failed to approve join request."), "warning");
10194
- throw error;
10393
+ return {
10394
+ message: friendlyError(error, "Failed to approve join request. Keep it pending and try again after the coordinator refreshes."),
10395
+ tone: "warning"
10396
+ };
10195
10397
  }
10196
10398
  },
10197
10399
  onAttentionAction: async (item) => {
@@ -10208,14 +10410,21 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10208
10410
  confirmLabel: "Deny request",
10209
10411
  cancelLabel: "Keep request pending",
10210
10412
  tone: "danger"
10211
- })) return;
10413
+ })) return null;
10212
10414
  try {
10213
10415
  await reviewJoinRequest(request.requestId, "deny");
10214
- showGlobalNotice(`Denied join request from ${request.displayName}.`);
10416
+ const feedback = {
10417
+ message: `Denied join request from ${request.displayName}.`,
10418
+ tone: "success"
10419
+ };
10420
+ state.syncJoinRequestsFeedback = feedback;
10215
10421
  await _loadSyncData$1();
10422
+ return feedback;
10216
10423
  } catch (error) {
10217
- showGlobalNotice(friendlyError(error, "Failed to deny join request."), "warning");
10218
- throw error;
10424
+ return {
10425
+ message: friendlyError(error, "Failed to deny join request. Leave it pending for now, then retry after the coordinator refreshes."),
10426
+ tone: "warning"
10427
+ };
10219
10428
  }
10220
10429
  },
10221
10430
  onInspectConflict: (row) => {
@@ -10223,7 +10432,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10223
10432
  if (peerCard instanceof HTMLElement) {
10224
10433
  peerCard.scrollIntoView({
10225
10434
  block: "center",
10226
- behavior: "smooth"
10435
+ behavior: syncScrollBehavior()
10227
10436
  });
10228
10437
  showGlobalNotice(`Opened the conflicting local device record for ${row.displayName}.`, "warning");
10229
10438
  return;
@@ -10237,20 +10446,27 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10237
10446
  confirmLabel: "Remove device record",
10238
10447
  cancelLabel: "Keep device record",
10239
10448
  tone: "danger"
10240
- })) return;
10449
+ })) return null;
10241
10450
  try {
10242
10451
  await deletePeer(row.deviceId);
10243
- showGlobalNotice(`Removed the broken local device record for ${row.displayName}. If it is still available, you can review it again from Needs attention.`);
10452
+ const feedback = {
10453
+ message: `Removed the broken local device record for ${row.displayName}. If it is still available, review it again from Next steps or Devices seen on team.`,
10454
+ tone: "success"
10455
+ };
10456
+ state.syncDiscoveredFeedback = feedback;
10244
10457
  await _loadSyncData$1();
10458
+ return feedback;
10245
10459
  } catch (error) {
10246
- showGlobalNotice(friendlyError(error, "Failed to remove the broken local device record."), "warning");
10247
- throw error;
10460
+ return {
10461
+ message: friendlyError(error, "Failed to remove the broken local device record. The old local record is still present in People & devices."),
10462
+ tone: "warning"
10463
+ };
10248
10464
  }
10249
10465
  },
10250
10466
  onReviewDiscoveredDevice: async (row) => {
10251
10467
  try {
10252
10468
  const reviewedName = await reviewDiscoveredDeviceName(row.displayName);
10253
- if (!reviewedName) return;
10469
+ if (!reviewedName) return null;
10254
10470
  const result = await acceptDiscoveredPeer(row.deviceId, row.fingerprint);
10255
10471
  const optimisticName = String(result?.name || row.displayName || "").trim() || row.displayName;
10256
10472
  state.pendingAcceptedSyncPeers = [...Array.isArray(state.pendingAcceptedSyncPeers) ? state.pendingAcceptedSyncPeers.filter((peer) => String(peer?.peer_device_id || "").trim() !== row.deviceId) : [], {
@@ -10263,23 +10479,44 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10263
10479
  last_error: "Waiting for the other device to approve this one."
10264
10480
  }];
10265
10481
  requestPeerScopeReview(row.deviceId);
10266
- showGlobalNotice(row.approvalBadgeLabel === "Needs your approval" ? `Approved ${row.displayName} on this device. Two-way trust should be ready once both devices refresh.` : `Step 1 complete on this device for ${row.displayName}. Finish onboarding on the other device so both sides trust each other for sync.`);
10482
+ let feedback = {
10483
+ message: row.approvalBadgeLabel === "Needs your approval" ? `Approved ${row.displayName} on this device. Two-way trust should be ready once both devices refresh.` : `Step 1 complete on this device for ${row.displayName}. Finish onboarding on the other device so both sides trust each other for sync.`,
10484
+ tone: "success"
10485
+ };
10267
10486
  try {
10268
10487
  if (reviewedName.trim() !== optimisticName.trim()) {
10269
10488
  await renamePeer(row.deviceId, reviewedName.trim());
10270
- showGlobalNotice(`Saved device name: ${reviewedName.trim()}.`);
10489
+ state.pendingAcceptedSyncPeers = state.pendingAcceptedSyncPeers.map((peer) => String(peer?.peer_device_id || "").trim() === row.deviceId ? {
10490
+ ...peer,
10491
+ name: reviewedName.trim()
10492
+ } : peer);
10493
+ feedback = {
10494
+ message: `Connected ${reviewedName.trim()} and saved its name.`,
10495
+ tone: "success"
10496
+ };
10271
10497
  }
10272
10498
  } catch (error) {
10273
- showGlobalNotice(friendlyError(error, "Device connected, but naming did not finish."), "warning");
10499
+ feedback = {
10500
+ message: friendlyError(error, "Device connected, but naming did not finish."),
10501
+ tone: "warning"
10502
+ };
10274
10503
  }
10504
+ state.syncDiscoveredFeedback = feedback;
10275
10505
  try {
10276
10506
  await _loadSyncData$1();
10277
10507
  } catch (error) {
10278
- showGlobalNotice(friendlyError(error, "Device connected, but the screen did not refresh yet."), "warning");
10508
+ feedback = {
10509
+ message: friendlyError(error, "Device connected, but the screen did not refresh yet. Refresh this page before trying the next step."),
10510
+ tone: "warning"
10511
+ };
10512
+ state.syncDiscoveredFeedback = feedback;
10279
10513
  }
10514
+ return feedback;
10280
10515
  } catch (error) {
10281
- showGlobalNotice(friendlyError(error, "Failed to review this device."), "warning");
10282
- throw error;
10516
+ return {
10517
+ message: friendlyError(error, "Failed to review this device. Wait for a fresh presence update and try again."),
10518
+ tone: "warning"
10519
+ };
10283
10520
  }
10284
10521
  },
10285
10522
  pendingJoinRequests,
@@ -10316,17 +10553,23 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10316
10553
  ttl_hours: ttlValue || 24
10317
10554
  });
10318
10555
  state.lastTeamInvite = result;
10556
+ setInviteOutputVisibility();
10319
10557
  syncInviteOutput.value = String(result.encoded || "");
10320
10558
  syncInviteOutput.hidden = false;
10321
10559
  syncInviteOutput.focus();
10322
10560
  syncInviteOutput.select();
10323
- showGlobalNotice("Invite created. Copy the text above and share it with your teammate.");
10324
- (Array.isArray(result.warnings) ? result.warnings : []).forEach((warning) => showGlobalNotice(String(warning), "warning"));
10561
+ const warnings = Array.isArray(result.warnings) ? result.warnings : [];
10562
+ showGlobalNotice(warnings.length ? `Invite created. Copy it above and review ${warnings.length === 1 ? "1 warning" : `${warnings.length} warnings`}.` : "Invite created. Copy the text above and share it with your teammate.", warnings.length ? "warning" : "success");
10325
10563
  } catch (error) {
10326
- showGlobalNotice(friendlyError(error, "Failed to create invite."), "warning");
10327
- } finally {
10564
+ showGlobalNotice(friendlyError(error, "Failed to create invite. Check the team name, invite lifetime, and coordinator reachability, then try again."), "warning");
10565
+ syncCreateInviteButton.textContent = "Retry";
10328
10566
  syncCreateInviteButton.disabled = false;
10329
- syncCreateInviteButton.textContent = "Create invite";
10567
+ return;
10568
+ } finally {
10569
+ if (syncCreateInviteButton.disabled) {
10570
+ syncCreateInviteButton.disabled = false;
10571
+ syncCreateInviteButton.textContent = "Create invite";
10572
+ }
10330
10573
  }
10331
10574
  });
10332
10575
  syncJoinButton?.addEventListener("click", async () => {
@@ -10342,14 +10585,37 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10342
10585
  try {
10343
10586
  const result = await importCoordinatorInvite(inviteValue);
10344
10587
  state.lastTeamJoin = result;
10345
- showGlobalNotice(result.status === "pending" ? "Join request submitted — waiting for admin approval." : "Joined team successfully.");
10588
+ let feedback = {
10589
+ message: result.status === "pending" ? "Join request sent. Waiting for admin approval." : "Joined the team.",
10590
+ tone: "success"
10591
+ };
10592
+ state.syncJoinFlowFeedback = feedback;
10593
+ setJoinFeedbackVisibility();
10346
10594
  syncJoinInvite.value = "";
10347
- await loadSyncData();
10595
+ try {
10596
+ await loadSyncData();
10597
+ } catch (error) {
10598
+ feedback = {
10599
+ message: friendlyError(error, "Joined the team, but this view has not refreshed yet."),
10600
+ tone: "warning"
10601
+ };
10602
+ state.syncJoinFlowFeedback = feedback;
10603
+ setJoinFeedbackVisibility();
10604
+ }
10348
10605
  } catch (error) {
10349
- showGlobalNotice(friendlyError(error, "Failed to import invite."), "warning");
10350
- } finally {
10606
+ state.syncJoinFlowFeedback = {
10607
+ message: friendlyError(error, "Failed to import invite. Check that the invite is complete, current, and meant for this team, then try again."),
10608
+ tone: "warning"
10609
+ };
10610
+ setJoinFeedbackVisibility();
10611
+ syncJoinButton.textContent = "Retry";
10351
10612
  syncJoinButton.disabled = false;
10352
- syncJoinButton.textContent = "Join team";
10613
+ return;
10614
+ } finally {
10615
+ if (syncJoinButton.disabled) {
10616
+ syncJoinButton.disabled = false;
10617
+ syncJoinButton.textContent = "Join team";
10618
+ }
10353
10619
  }
10354
10620
  });
10355
10621
  syncNowButton?.addEventListener("click", async () => {
@@ -10360,7 +10626,10 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10360
10626
  const summary = summarizeSyncRunResult(await triggerSync());
10361
10627
  showGlobalNotice(summary.message, summary.warning ? "warning" : void 0);
10362
10628
  } catch (error) {
10363
- showGlobalNotice(friendlyError(error, "Failed to start sync."), "warning");
10629
+ showGlobalNotice(friendlyError(error, "Failed to start sync. Retry once, then run codemem sync doctor if the problem keeps coming back."), "warning");
10630
+ syncNowButton.textContent = "Retry";
10631
+ syncNowButton.disabled = false;
10632
+ return;
10364
10633
  }
10365
10634
  syncNowButton.disabled = false;
10366
10635
  syncNowButton.textContent = "Sync now";
@@ -10448,12 +10717,17 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10448
10717
  className: "actor-title",
10449
10718
  children: [/* @__PURE__ */ u("strong", { children: actorDisplayLabel(actor) }), /* @__PURE__ */ u("span", {
10450
10719
  className: `badge actor-badge${actor.is_local ? " local" : ""}`,
10720
+ title: actor.is_local ? localActorNote(hiddenLocalDuplicateCount) : void 0,
10451
10721
  children: actor.is_local ? "You" : `${count} device${count === 1 ? "" : "s"}`
10452
10722
  })]
10453
- }), /* @__PURE__ */ u("div", {
10723
+ }), !actor.is_local ? /* @__PURE__ */ u("div", {
10454
10724
  className: "peer-meta",
10455
- children: actor.is_local ? localActorNote(hiddenLocalDuplicateCount) : `${count} assigned device${count === 1 ? "" : "s"}`
10456
- })]
10725
+ children: [
10726
+ count,
10727
+ " assigned device",
10728
+ count === 1 ? "" : "s"
10729
+ ]
10730
+ }) : null]
10457
10731
  }), /* @__PURE__ */ u("div", {
10458
10732
  className: "actor-actions",
10459
10733
  children: actor.is_local ? /* @__PURE__ */ u("div", {
@@ -10509,7 +10783,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10509
10783
  function SyncActorsList({ actors, hiddenLocalDuplicateCount, onRename, onMerge }) {
10510
10784
  if (!actors.length) return /* @__PURE__ */ u("div", {
10511
10785
  className: "sync-empty-state",
10512
- children: "No people yet. Create one to represent yourself or a teammate."
10786
+ children: [/* @__PURE__ */ u("strong", { children: "No people yet." }), /* @__PURE__ */ u("span", { children: "Create a named person here, then assign each device below to keep sync ownership readable." })]
10513
10787
  });
10514
10788
  return /* @__PURE__ */ u(k$2, { children: actors.map((actor) => {
10515
10789
  const actorId = String(actor.actor_id || actor.display_name || actor.is_local || "");
@@ -10595,6 +10869,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10595
10869
  const cardRef = A$1(null);
10596
10870
  const [scopeHost, setScopeHost] = d(null);
10597
10871
  const [renameValue, setRenameValue] = d(displayName);
10872
+ const [feedback, setFeedback] = d(() => state.syncPeerFeedbackById.get(peerId) ?? null);
10598
10873
  const [renameBusy, setRenameBusy] = d(false);
10599
10874
  const [renameLabel, setRenameLabel] = d("Save name");
10600
10875
  const [syncBusy, setSyncBusy] = d(false);
@@ -10627,6 +10902,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10627
10902
  const excludeEditor = T$1(() => createChipEditor(excludeList, "Add excluded project", "No exclusions"), [peerId, excludeList.join("|")]);
10628
10903
  y(() => {
10629
10904
  setRenameValue(displayName);
10905
+ setFeedback(state.syncPeerFeedbackById.get(peerId) ?? null);
10630
10906
  setRenameBusy(false);
10631
10907
  setRenameLabel("Save name");
10632
10908
  setSyncBusy(false);
@@ -10650,28 +10926,33 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10650
10926
  if (!scopeReviewRequested || !cardRef.current) return;
10651
10927
  queueMicrotask(() => cardRef.current?.scrollIntoView({
10652
10928
  block: "center",
10653
- behavior: "smooth"
10929
+ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth"
10654
10930
  }));
10655
10931
  }, [scopeReviewRequested]);
10656
10932
  async function rename() {
10657
10933
  if (!peerId) return;
10658
10934
  const nextName = renameValue.trim();
10659
10935
  if (!nextName) {
10660
- showGlobalNotice("Enter a friendly name for this device.", "warning");
10936
+ const warning = {
10937
+ message: "Enter a friendly name for this device.",
10938
+ tone: "warning"
10939
+ };
10940
+ setFeedback(warning);
10941
+ state.syncPeerFeedbackById.set(peerId, warning);
10661
10942
  document.querySelector(`[data-device-name-input="${CSS.escape(peerId)}"]`)?.focus();
10662
10943
  return;
10663
10944
  }
10664
10945
  setRenameBusy(true);
10665
10946
  setRenameLabel("Saving…");
10666
- let ok = false;
10667
10947
  try {
10668
- await onRename(peerId, nextName);
10669
- ok = true;
10948
+ const nextFeedback = await onRename(peerId, nextName);
10949
+ setFeedback(nextFeedback);
10950
+ state.syncPeerFeedbackById.set(peerId, nextFeedback);
10951
+ setRenameLabel("Save name");
10670
10952
  } catch {
10671
- setRenameLabel("Retry save");
10953
+ setRenameLabel("Retry");
10672
10954
  } finally {
10673
10955
  setRenameBusy(false);
10674
- if (ok) setRenameLabel("Save name");
10675
10956
  }
10676
10957
  }
10677
10958
  async function sync() {
@@ -10686,7 +10967,9 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10686
10967
  }
10687
10968
  setSyncBusy(true);
10688
10969
  try {
10689
- await onSync(peer, primaryAddress);
10970
+ const nextFeedback = await onSync(peer, primaryAddress);
10971
+ setFeedback(nextFeedback);
10972
+ if (nextFeedback) state.syncPeerFeedbackById.set(peerId, nextFeedback);
10690
10973
  } finally {
10691
10974
  setSyncBusy(false);
10692
10975
  }
@@ -10716,46 +10999,46 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10716
10999
  async function savePerson() {
10717
11000
  if (!peerId) return;
10718
11001
  setApplyActorBusy(true);
10719
- setApplyActorLabel("Applying…");
10720
- let ok = false;
11002
+ setApplyActorLabel("Saving…");
10721
11003
  try {
10722
- await onAssignActor(peerId, selectedActorId || null);
10723
- ok = true;
11004
+ const nextFeedback = await onAssignActor(peerId, selectedActorId || null);
11005
+ setFeedback(nextFeedback);
11006
+ state.syncPeerFeedbackById.set(peerId, nextFeedback);
11007
+ setApplyActorLabel("Save person");
10724
11008
  } catch {
10725
11009
  setApplyActorLabel("Retry");
10726
11010
  } finally {
10727
11011
  setApplyActorBusy(false);
10728
- if (ok) setApplyActorLabel("Save person");
10729
11012
  }
10730
11013
  }
10731
11014
  async function saveScope() {
10732
11015
  if (!peerId) return;
10733
11016
  setSaveScopeBusy(true);
10734
11017
  setSaveScopeLabel("Saving…");
10735
- let ok = false;
10736
11018
  try {
10737
- await onSaveScope(peerId, includeEditor.values(), excludeEditor.values());
10738
- ok = true;
11019
+ const nextFeedback = await onSaveScope(peerId, includeEditor.values(), excludeEditor.values());
11020
+ setFeedback(nextFeedback);
11021
+ state.syncPeerFeedbackById.set(peerId, nextFeedback);
11022
+ setSaveScopeLabel("Save scope");
10739
11023
  } catch {
10740
- setSaveScopeLabel("Retry save");
11024
+ setSaveScopeLabel("Retry");
10741
11025
  } finally {
10742
11026
  setSaveScopeBusy(false);
10743
- if (ok) setSaveScopeLabel("Save scope");
10744
11027
  }
10745
11028
  }
10746
11029
  async function resetScope() {
10747
11030
  if (!peerId) return;
10748
11031
  setResetScopeBusy(true);
10749
11032
  setResetScopeLabel("Resetting…");
10750
- let ok = false;
10751
11033
  try {
10752
- await onResetScope(peerId);
10753
- ok = true;
11034
+ const nextFeedback = await onResetScope(peerId);
11035
+ setFeedback(nextFeedback);
11036
+ state.syncPeerFeedbackById.set(peerId, nextFeedback);
11037
+ setResetScopeLabel("Reset to global scope");
10754
11038
  } catch {
10755
- setResetScopeLabel("Retry reset");
11039
+ setResetScopeLabel("Retry");
10756
11040
  } finally {
10757
11041
  setResetScopeBusy(false);
10758
- if (ok) setResetScopeLabel("Reset to global scope");
10759
11042
  }
10760
11043
  }
10761
11044
  return /* @__PURE__ */ u("div", {
@@ -10849,10 +11132,10 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10849
11132
  children: [
10850
11133
  scopeReviewRequested ? /* @__PURE__ */ u("div", {
10851
11134
  className: "peer-meta",
10852
- children: "Review this device's sync scope now. Global defaults apply until you save an override here."
11135
+ children: "Review this device's sharing scope now."
10853
11136
  }) : pendingScopeReview ? /* @__PURE__ */ u("div", {
10854
11137
  className: "peer-meta",
10855
- children: "Scope review still pending. Save an override here or reset to global scope when you are done reviewing. Manual syncs can proceed, but they will use the current effective scope until you change it."
11138
+ children: "Scope review still pending."
10856
11139
  }) : null,
10857
11140
  /* @__PURE__ */ u("div", {
10858
11141
  className: "peer-scope-summary",
@@ -10862,10 +11145,6 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10862
11145
  className: "peer-meta",
10863
11146
  children: peer.actor_display_name ? `Assigned to ${peer.claimed_local_actor ? "You" : String(peer.actor_display_name)}` : "Unassigned person"
10864
11147
  }),
10865
- /* @__PURE__ */ u("div", {
10866
- className: "peer-meta",
10867
- children: trustSummary.description
10868
- }),
10869
11148
  /* @__PURE__ */ u("div", {
10870
11149
  className: "peer-actor-row",
10871
11150
  children: [/* @__PURE__ */ u("div", {
@@ -10889,6 +11168,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10889
11168
  children: applyActorLabel
10890
11169
  })]
10891
11170
  }),
11171
+ /* @__PURE__ */ u(SyncInlineFeedback, { feedback }),
10892
11172
  /* @__PURE__ */ u("div", { ref: setScopeHost })
10893
11173
  ]
10894
11174
  })
@@ -10896,17 +11176,20 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10896
11176
  });
10897
11177
  }
10898
11178
  function SyncPeersList(props) {
10899
- if (!props.peers.length) return /* @__PURE__ */ u("div", {
11179
+ const sectionFeedback = state.syncPeersSectionFeedback;
11180
+ const syncStatus = state.lastSyncStatus;
11181
+ const syncDisabled = syncStatus?.daemon_state === "disabled" || syncStatus?.enabled === false;
11182
+ if (!props.peers.length) return /* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u(SyncInlineFeedback, { feedback: sectionFeedback }), /* @__PURE__ */ u("div", {
10900
11183
  className: "sync-empty-state",
10901
- children: "No devices connected on this machine yet. Use the pairing command in Diagnostics to connect another device."
10902
- });
10903
- return /* @__PURE__ */ u(k$2, { children: props.peers.map((peer) => {
11184
+ children: [/* @__PURE__ */ u("strong", { children: "No paired devices yet." }), /* @__PURE__ */ u("span", { children: syncDisabled ? "Turn on sync in Settings → Device Sync first, then use Show pairing in Advanced diagnostics to connect another device." : "Use the Show pairing control in Advanced diagnostics, run the command on the other device, then come back here to name and assign it." })]
11185
+ })] });
11186
+ return /* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u(SyncInlineFeedback, { feedback: sectionFeedback }), props.peers.map((peer) => {
10904
11187
  const peerId = String(peer.peer_device_id || peer.name || "unknown-peer");
10905
11188
  return /* @__PURE__ */ u(SyncPeerCard, {
10906
11189
  peer,
10907
11190
  ...props
10908
11191
  }, peerId);
10909
- }) });
11192
+ })] });
10910
11193
  }
10911
11194
  function renderSyncPeersList(mount, props) {
10912
11195
  renderIntoSyncMount(mount, /* @__PURE__ */ u(SyncPeersList, { ...props }));
@@ -10962,6 +11245,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10962
11245
  //#region src/tabs/sync/people.ts
10963
11246
  var _loadSyncData = async () => {};
10964
11247
  var legacyDeviceValue = "";
11248
+ function setPeopleCreateControlsDisabled(disabled) {
11249
+ const createButton = document.getElementById("syncActorCreateButton");
11250
+ const createInput = document.getElementById("syncActorCreateInput");
11251
+ if (createButton) createButton.disabled = disabled;
11252
+ if (createInput) createInput.disabled = disabled;
11253
+ }
10965
11254
  function setLoadSyncData(fn) {
10966
11255
  _loadSyncData = fn;
10967
11256
  }
@@ -10970,6 +11259,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10970
11259
  const actorMeta = document.getElementById("syncActorsMeta");
10971
11260
  if (!actorList) return;
10972
11261
  hideSkeleton("syncActorsSkeleton");
11262
+ setPeopleCreateControlsDisabled(false);
10973
11263
  const actorVisibility = deriveVisiblePeopleActors({
10974
11264
  actors: state.lastSyncActors,
10975
11265
  peers: state.lastSyncPeers,
@@ -10977,7 +11267,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10977
11267
  });
10978
11268
  const actors = actorVisibility.visibleActors;
10979
11269
  if (actorMeta) {
10980
- actorMeta.textContent = actors.length ? "Create, rename, and combine people here. Assign each device below. Non-local people receive memories from allowed projects unless you mark them Only me." : "No named people yet. Create one here, then assign devices below.";
11270
+ actorMeta.textContent = actors.length ? "Manage people here, then assign devices below." : "No named people yet. Create a person here, then assign devices below so sync ownership is easier to review.";
10981
11271
  if (actorVisibility.hiddenLocalDuplicateCount > 0) actorMeta.textContent += ` ${actorVisibility.hiddenLocalDuplicateCount} unresolved duplicate ${actorVisibility.hiddenLocalDuplicateCount === 1 ? "entry is" : "entries are"} hidden here until reviewed in Needs attention.`;
10982
11272
  }
10983
11273
  renderSyncActorsList(actorList, {
@@ -10999,6 +11289,16 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10999
11289
  }
11000
11290
  });
11001
11291
  }
11292
+ function renderSyncActorsUnavailable() {
11293
+ const actorList = document.getElementById("syncActorsList");
11294
+ const actorMeta = document.getElementById("syncActorsMeta");
11295
+ setPeopleCreateControlsDisabled(true);
11296
+ if (actorMeta) actorMeta.textContent = "People controls are temporarily unavailable. Refresh this page to retry, but device status and sync health are still available below.";
11297
+ if (actorList) renderSyncEmptyState(actorList, {
11298
+ title: "People unavailable right now.",
11299
+ detail: "Refresh this page to reload named people once the people endpoint is responding again."
11300
+ });
11301
+ }
11002
11302
  function renderSyncPeers() {
11003
11303
  const syncPeers = document.getElementById("syncPeers");
11004
11304
  if (!syncPeers) return;
@@ -11009,75 +11309,133 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11009
11309
  onRename: async (peerId, nextName) => {
11010
11310
  try {
11011
11311
  await renamePeer(peerId, nextName);
11012
- showGlobalNotice("Device name saved.");
11013
11312
  await _loadSyncData();
11313
+ return {
11314
+ message: "Device name saved.",
11315
+ tone: "success"
11316
+ };
11014
11317
  } catch (error) {
11015
- showGlobalNotice(friendlyError(error, "Failed to save device name."), "warning");
11016
- throw error;
11318
+ return {
11319
+ message: friendlyError(error, "Failed to save device name."),
11320
+ tone: "warning"
11321
+ };
11017
11322
  }
11018
11323
  },
11019
11324
  onSync: async (peer, address) => {
11020
11325
  try {
11021
11326
  const summary = summarizeSyncRunResult(await triggerSync(address));
11022
11327
  const peerId = String(peer?.peer_device_id || "");
11023
- if (!summary.ok) showGlobalNotice(summary.message, "warning");
11024
- else if (peerId && isPeerScopeReviewPending(peerId)) showGlobalNotice(`Triggered sync for ${peer?.name || (peerId ? peerId.slice(0, 8) : "unknown")} before scope review was finished. Review scope in this card if you want tighter sharing rules.`, "warning");
11025
- else showGlobalNotice(summary.message);
11328
+ let feedback;
11329
+ if (!summary.ok) feedback = {
11330
+ message: summary.message,
11331
+ tone: "warning"
11332
+ };
11333
+ else if (peerId && isPeerScopeReviewPending(peerId)) feedback = {
11334
+ message: `Triggered sync for ${peer?.name || (peerId ? peerId.slice(0, 8) : "unknown")}. Review scope in this card if you want tighter sharing rules.`,
11335
+ tone: "warning"
11336
+ };
11337
+ else feedback = {
11338
+ message: summary.message,
11339
+ tone: "success"
11340
+ };
11341
+ try {
11342
+ await _loadSyncData();
11343
+ } catch {
11344
+ feedback = {
11345
+ message: "Sync started, but this view has not refreshed yet. Refresh the page or use Sync now again before retrying.",
11346
+ tone: "warning"
11347
+ };
11348
+ }
11349
+ return feedback;
11026
11350
  } catch (error) {
11027
- showGlobalNotice(friendlyError(error, "Failed to trigger sync."), "warning");
11028
- throw error;
11029
- }
11030
- try {
11031
- await _loadSyncData();
11032
- } catch {
11033
- showGlobalNotice("Sync started, but the local status view did not refresh yet.", "warning");
11351
+ return {
11352
+ message: friendlyError(error, "Failed to trigger sync."),
11353
+ tone: "warning"
11354
+ };
11034
11355
  }
11035
- return null;
11036
11356
  },
11037
11357
  onRemove: async (peerId, label) => {
11038
11358
  try {
11039
11359
  await deletePeer(peerId);
11040
- showGlobalNotice(`Removed peer ${label}.`);
11360
+ const feedback = {
11361
+ message: `Removed peer ${label}.`,
11362
+ tone: "success"
11363
+ };
11364
+ state.syncPeerFeedbackById.delete(peerId);
11365
+ state.syncPeersSectionFeedback = feedback;
11041
11366
  await _loadSyncData();
11367
+ return feedback;
11042
11368
  } catch (error) {
11043
- showGlobalNotice(friendlyError(error, "Failed to remove peer."), "warning");
11044
- throw error;
11369
+ return {
11370
+ message: friendlyError(error, "Failed to remove peer. The local peer entry is still here."),
11371
+ tone: "warning"
11372
+ };
11045
11373
  }
11046
11374
  },
11047
11375
  onAssignActor: async (peerId, actorId) => {
11048
11376
  try {
11049
11377
  await assignPeerActor(peerId, actorId);
11050
- showGlobalNotice(actorId ? "Device person updated." : "Device person cleared.");
11051
11378
  await _loadSyncData();
11379
+ return {
11380
+ message: actorId ? "Device person updated." : "Device person cleared.",
11381
+ tone: "success"
11382
+ };
11052
11383
  } catch (error) {
11053
- showGlobalNotice(friendlyError(error, "Failed to update device person."), "warning");
11054
- throw error;
11384
+ return {
11385
+ message: friendlyError(error, "Failed to update device person. The current assignment is unchanged."),
11386
+ tone: "warning"
11387
+ };
11055
11388
  }
11056
11389
  },
11057
11390
  onSaveScope: async (peerId, include, exclude) => {
11058
11391
  try {
11059
11392
  await updatePeerScope(peerId, include, exclude);
11060
11393
  clearPeerScopeReview(peerId);
11061
- showGlobalNotice("Device sync scope saved.");
11062
11394
  await _loadSyncData();
11395
+ return {
11396
+ message: "Device sync scope saved.",
11397
+ tone: "success"
11398
+ };
11063
11399
  } catch (error) {
11064
- showGlobalNotice(friendlyError(error, "Failed to save device scope."), "warning");
11065
- throw error;
11400
+ return {
11401
+ message: friendlyError(error, "Failed to save device scope. The current sharing rules are still active."),
11402
+ tone: "warning"
11403
+ };
11066
11404
  }
11067
11405
  },
11068
11406
  onResetScope: async (peerId) => {
11069
11407
  try {
11070
11408
  await updatePeerScope(peerId, null, null, true);
11071
11409
  clearPeerScopeReview(peerId);
11072
- showGlobalNotice("Device sync scope reset to global defaults.");
11073
11410
  await _loadSyncData();
11411
+ return {
11412
+ message: "Device sync scope reset to global defaults.",
11413
+ tone: "success"
11414
+ };
11074
11415
  } catch (error) {
11075
- showGlobalNotice(friendlyError(error, "Failed to reset device scope."), "warning");
11076
- throw error;
11416
+ return {
11417
+ message: friendlyError(error, "Failed to reset device scope. The current sharing rules are still active."),
11418
+ tone: "warning"
11419
+ };
11077
11420
  }
11078
11421
  }
11079
11422
  });
11080
11423
  }
11424
+ function renderSyncPeopleUnavailable() {
11425
+ const actorList = document.getElementById("syncActorsList");
11426
+ const actorMeta = document.getElementById("syncActorsMeta");
11427
+ const syncPeers = document.getElementById("syncPeers");
11428
+ setPeopleCreateControlsDisabled(true);
11429
+ if (actorMeta) actorMeta.textContent = "People and device details are unavailable right now. Refresh this page to retry once local sync status is reachable again.";
11430
+ if (actorList) renderSyncEmptyState(actorList, {
11431
+ title: "People unavailable right now.",
11432
+ detail: "Refresh this page to reload named people once the local sync status endpoint is responding again."
11433
+ });
11434
+ if (syncPeers) renderSyncEmptyState(syncPeers, {
11435
+ title: "Devices unavailable right now.",
11436
+ detail: "Refresh this page to reload paired devices. When sync is reachable again, you can rename, assign, or pair devices here."
11437
+ });
11438
+ }
11081
11439
  function renderLegacyDeviceClaims() {
11082
11440
  const panel = document.getElementById("syncLegacyClaims");
11083
11441
  const mount = document.getElementById("syncLegacyDeviceSelectMount");
@@ -11184,6 +11542,18 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11184
11542
  join_requests: []
11185
11543
  };
11186
11544
  }
11545
+ function hideStaleSyncSecondarySections() {
11546
+ const sharingReview = document.getElementById("syncSharingReview");
11547
+ const sharingReviewList = document.getElementById("syncSharingReviewList");
11548
+ const sharingReviewMeta = document.getElementById("syncSharingReviewMeta");
11549
+ const legacyClaims = document.getElementById("syncLegacyClaims");
11550
+ const legacyClaimsMeta = document.getElementById("syncLegacyClaimsMeta");
11551
+ if (sharingReview) sharingReview.hidden = true;
11552
+ if (sharingReviewList) sharingReviewList.textContent = "";
11553
+ if (sharingReviewMeta) sharingReviewMeta.textContent = "";
11554
+ if (legacyClaims) legacyClaims.hidden = true;
11555
+ if (legacyClaimsMeta) legacyClaimsMeta.textContent = "";
11556
+ }
11187
11557
  async function loadSyncData() {
11188
11558
  const requestId = ++latestSyncLoadRequestId;
11189
11559
  try {
@@ -11222,7 +11592,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11222
11592
  const statusPayload = payload.status && typeof payload.status === "object" ? payload.status : null;
11223
11593
  if (statusPayload) state.lastSyncStatus = statusPayload;
11224
11594
  if (Array.isArray(actorsPayload?.items)) state.lastSyncActors = actorsPayload.items;
11225
- else if (!actorLoadError) state.lastSyncActors = [];
11595
+ else state.lastSyncActors = [];
11226
11596
  const payloadPeers = Array.isArray(payload.peers) ? payload.peers : [];
11227
11597
  const realPeerIds = new Set(payloadPeers.map((peer) => String(peer?.peer_device_id || "").trim()).filter(Boolean));
11228
11598
  const pendingPeers = Array.isArray(state.pendingAcceptedSyncPeers) ? state.pendingAcceptedSyncPeers.filter((peer) => {
@@ -11251,18 +11621,17 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11251
11621
  renderLegacyDeviceClaims();
11252
11622
  renderSyncAttempts();
11253
11623
  renderHealthOverview();
11254
- if (actorLoadError) {
11255
- const actorMeta = document.getElementById("syncActorsMeta");
11256
- if (actorMeta) actorMeta.textContent = "People controls are temporarily unavailable. Device status and sync health still loaded.";
11257
- }
11624
+ if (actorLoadError) renderSyncActorsUnavailable();
11258
11625
  } catch {
11259
11626
  if (requestId !== latestSyncLoadRequestId) return;
11627
+ lastSyncHash = "";
11260
11628
  hideSkeleton("syncTeamSkeleton");
11261
11629
  hideSkeleton("syncActorsSkeleton");
11262
11630
  hideSkeleton("syncPeersSkeleton");
11263
11631
  hideSkeleton("syncDiagSkeleton");
11264
- const syncMeta = document.getElementById("syncMeta");
11265
- if (syncMeta) syncMeta.textContent = "Sync unavailable";
11632
+ hideStaleSyncSecondarySections();
11633
+ renderSyncPeopleUnavailable();
11634
+ renderSyncDiagnosticsUnavailable();
11266
11635
  }
11267
11636
  }
11268
11637
  async function loadPairingData() {
@@ -11270,6 +11639,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11270
11639
  state.pairingPayloadRaw = await loadPairing() || null;
11271
11640
  renderPairing();
11272
11641
  } catch {
11642
+ state.pairingPayloadRaw = null;
11273
11643
  renderPairing();
11274
11644
  }
11275
11645
  }
@@ -12924,10 +13294,60 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12924
13294
  try {
12925
13295
  const runtime = await loadRuntimeInfo();
12926
13296
  if (!runtime?.version) return;
12927
- setRuntimeLabel(runtime.version, "4a76585");
13297
+ setRuntimeLabel(runtime.version, "692076f");
12928
13298
  } catch {}
12929
13299
  }
12930
13300
  var lastAnnouncedRefreshState = null;
13301
+ var RECONNECT_POLL_MS = 1500;
13302
+ var reconnectTimer = null;
13303
+ var reconnecting = false;
13304
+ function setReconnectOverlay(open, detail) {
13305
+ const overlay = $$2("viewerReconnectOverlay");
13306
+ const detailEl = $$2("viewerReconnectDetail");
13307
+ if (!overlay || !detailEl) return;
13308
+ overlay.hidden = !open;
13309
+ detailEl.textContent = detail || "Trying again automatically while the viewer comes back.";
13310
+ }
13311
+ async function isViewerReady() {
13312
+ try {
13313
+ await pingViewerReady();
13314
+ return true;
13315
+ } catch {
13316
+ return false;
13317
+ }
13318
+ }
13319
+ function stopReconnectLoop() {
13320
+ if (reconnectTimer) {
13321
+ clearTimeout(reconnectTimer);
13322
+ reconnectTimer = null;
13323
+ }
13324
+ reconnecting = false;
13325
+ setReconnectOverlay(false);
13326
+ }
13327
+ function canResumeRefresh() {
13328
+ return document.visibilityState !== "hidden" && !isSettingsOpen();
13329
+ }
13330
+ function scheduleReconnectLoop() {
13331
+ if (reconnecting) return;
13332
+ reconnecting = true;
13333
+ stopPolling();
13334
+ setRefreshStatus("error", "(reconnecting)");
13335
+ setReconnectOverlay(true, "The viewer server is restarting or temporarily unavailable. Trying again automatically…");
13336
+ const tick = async () => {
13337
+ if (await isViewerReady()) {
13338
+ stopReconnectLoop();
13339
+ if (canResumeRefresh()) {
13340
+ setRefreshStatus("refreshing");
13341
+ startPolling();
13342
+ doRefresh();
13343
+ } else setRefreshStatus("paused", document.visibilityState === "hidden" ? "(tab hidden)" : "(settings open)");
13344
+ return;
13345
+ }
13346
+ setReconnectOverlay(true, "Still reconnecting… the viewer will recover automatically as soon as the server responds.");
13347
+ reconnectTimer = setTimeout(tick, RECONNECT_POLL_MS);
13348
+ };
13349
+ reconnectTimer = setTimeout(tick, RECONNECT_POLL_MS);
13350
+ }
12931
13351
  function setRefreshStatus(rs, detail) {
12932
13352
  state.refreshState = rs;
12933
13353
  const el = $$2("refreshStatus");
@@ -12947,6 +13367,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12947
13367
  announce("Auto refresh paused.");
12948
13368
  return;
12949
13369
  }
13370
+ if (rs === "error" && detail === "(reconnecting)") {
13371
+ el.textContent = "reconnecting…";
13372
+ announce("Viewer reconnecting.");
13373
+ return;
13374
+ }
12950
13375
  if (rs === "error") {
12951
13376
  el.textContent = "refresh failed";
12952
13377
  announce("Refresh failed.");
@@ -12970,7 +13395,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12970
13395
  if (document.visibilityState === "hidden") {
12971
13396
  stopPolling();
12972
13397
  setRefreshStatus("paused", "(tab hidden)");
12973
- } else if (!isSettingsOpen()) {
13398
+ } else if (!isSettingsOpen() && !reconnecting) {
12974
13399
  startPolling();
12975
13400
  refresh();
12976
13401
  }
@@ -13026,10 +13451,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
13026
13451
  });
13027
13452
  var refreshDebounceTimer = null;
13028
13453
  async function refresh() {
13454
+ if (reconnecting) return;
13029
13455
  if (refreshDebounceTimer) clearTimeout(refreshDebounceTimer);
13030
13456
  refreshDebounceTimer = setTimeout(() => doRefresh(), 80);
13031
13457
  }
13032
13458
  async function doRefresh() {
13459
+ if (reconnecting) return;
13033
13460
  if (state.refreshInFlight) {
13034
13461
  state.refreshQueued = true;
13035
13462
  return;
@@ -13044,10 +13471,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
13044
13471
  await Promise.all(promises);
13045
13472
  setRefreshStatus("idle");
13046
13473
  } catch {
13047
- setRefreshStatus("error");
13474
+ if (!await isViewerReady()) scheduleReconnectLoop();
13475
+ else setRefreshStatus("error");
13048
13476
  } finally {
13049
13477
  state.refreshInFlight = false;
13050
- if (state.refreshQueued) {
13478
+ if (state.refreshQueued && !reconnecting) {
13051
13479
  state.refreshQueued = false;
13052
13480
  doRefresh();
13053
13481
  }
@@ -13061,6 +13489,16 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
13061
13489
  initSyncTab(() => refresh());
13062
13490
  initSettings(stopPolling, startPolling, () => refresh());
13063
13491
  loadProjects();
13492
+ $$2("viewerReconnectRetry")?.addEventListener("click", async () => {
13493
+ setReconnectOverlay(true, "Checking whether the viewer server is back…");
13494
+ if (!await isViewerReady()) {
13495
+ scheduleReconnectLoop();
13496
+ return;
13497
+ }
13498
+ stopReconnectLoop();
13499
+ startPolling();
13500
+ doRefresh();
13501
+ });
13064
13502
  loadRuntimeLabel();
13065
13503
  refresh();
13066
13504
  startPolling();