@codemem/server 0.22.0 → 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 {
@@ -252,6 +270,9 @@
252
270
  async function loadStats() {
253
271
  return fetchJson("/api/stats");
254
272
  }
273
+ async function loadRuntimeInfo() {
274
+ return fetchJson("/api/runtime");
275
+ }
255
276
  async function loadUsage(project) {
256
277
  return fetchJson(`/api/usage?project=${encodeURIComponent(project)}`);
257
278
  }
@@ -2366,6 +2387,10 @@
2366
2387
  if (peer?.status?.fresh) return "connected";
2367
2388
  return "waiting";
2368
2389
  }
2390
+ function isOfflineTeamDevice(device) {
2391
+ if (!device.discovered?.stale) return false;
2392
+ return device.peer ? derivePeerUiStatus(device.peer) !== "connected" : true;
2393
+ }
2369
2394
  function peerErrorText(peer) {
2370
2395
  return cleanText(peer?.last_error).toLowerCase();
2371
2396
  }
@@ -2504,18 +2529,18 @@
2504
2529
  priority: 10,
2505
2530
  title: `${device.name} needs repair`,
2506
2531
  summary: device.summary,
2507
- actionLabel: "Review device",
2532
+ actionLabel: "Open device",
2508
2533
  deviceId: device.id
2509
2534
  };
2510
2535
  }
2511
2536
  function createReviewItem(device) {
2512
2537
  return {
2513
- id: `review:${device.id}`,
2538
+ id: `review:${device.id}:${device.key || "default"}`,
2514
2539
  kind: "review-team-device",
2515
2540
  priority: 20,
2516
2541
  title: `${device.name} is available to review`,
2517
2542
  summary: device.summary,
2518
- actionLabel: "Review device",
2543
+ actionLabel: "Open device",
2519
2544
  deviceId: device.id
2520
2545
  };
2521
2546
  }
@@ -2526,7 +2551,7 @@
2526
2551
  priority: 30,
2527
2552
  title: `Name ${device.name}`,
2528
2553
  summary: device.summary,
2529
- actionLabel: "Name device",
2554
+ actionLabel: "Go to name field",
2530
2555
  deviceId: device.id
2531
2556
  };
2532
2557
  }
@@ -2574,7 +2599,7 @@
2574
2599
  priority: candidate.includesLocal ? 5 : 15,
2575
2600
  title: `Possible duplicate person: ${candidate.displayName}`,
2576
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.",
2577
- actionLabel: "Review people",
2602
+ actionLabel: "Go to people",
2578
2603
  actorIds: candidate.actorIds
2579
2604
  });
2580
2605
  });
@@ -2610,9 +2635,22 @@
2610
2635
  }));
2611
2636
  if (device.peer && trustSummary?.state === "trusted-by-you") attentionItems.push(createReviewItem({
2612
2637
  id: device.deviceId,
2638
+ key: "other-device-trust",
2613
2639
  name,
2614
2640
  summary: "You accepted this device. Finish onboarding on the other device so it trusts this one too."
2615
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
+ }));
2616
2654
  if (device.peer && deviceNeedsFriendlyName({
2617
2655
  localName: device.localName,
2618
2656
  coordinatorName: device.coordinatorName,
@@ -2627,7 +2665,7 @@
2627
2665
  summary: {
2628
2666
  connectedDeviceCount: peers.filter((peer) => derivePeerUiStatus(peer) === "connected").length,
2629
2667
  seenOnTeamCount: discoveredDevices.length,
2630
- offlineTeamDeviceCount: discoveredDevices.filter((device) => Boolean(device?.stale)).length
2668
+ offlineTeamDeviceCount: mergedDevices.filter((device) => isOfflineTeamDevice(device)).length
2631
2669
  },
2632
2670
  duplicatePeople,
2633
2671
  attentionItems: attentionItems.sort((a, b) => a.priority - b.priority || a.title.localeCompare(b.title))
@@ -3940,6 +3978,11 @@
3940
3978
  placeholder: "Invite will appear here",
3941
3979
  readOnly: true,
3942
3980
  hidden: true
3981
+ }),
3982
+ /* @__PURE__ */ u("div", {
3983
+ className: "peer-meta",
3984
+ id: "syncInviteWarnings",
3985
+ hidden: true
3943
3986
  })
3944
3987
  ]
3945
3988
  });
@@ -4022,6 +4065,12 @@
4022
4065
  function PairingText({ text }) {
4023
4066
  return /* @__PURE__ */ u(k$2, { children: text });
4024
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
+ }
4025
4074
  function renderDiagnosticsGrid(mount, items) {
4026
4075
  if (!items.length) {
4027
4076
  clearSyncMount(mount);
@@ -4036,6 +4085,9 @@
4036
4085
  }
4037
4086
  renderIntoSyncMount(mount, /* @__PURE__ */ u(AttemptsList, { attempts }));
4038
4087
  }
4088
+ function renderSyncEmptyState(mount, view) {
4089
+ renderIntoSyncMount(mount, /* @__PURE__ */ u(SyncEmptyState, { ...view }));
4090
+ }
4039
4091
  function renderPairingView(payloadMount, hintMount, view) {
4040
4092
  renderIntoSyncMount(payloadMount, /* @__PURE__ */ u(PairingText, { text: view.payloadText }));
4041
4093
  if (hintMount) renderIntoSyncMount(hintMount, /* @__PURE__ */ u(PairingText, { text: view.hintText }));
@@ -4145,11 +4197,6 @@
4145
4197
  function assignedActorCount(actorId) {
4146
4198
  return (Array.isArray(state.lastSyncPeers) ? state.lastSyncPeers : []).filter((peer) => String(peer?.actor_id || "") === actorId).length;
4147
4199
  }
4148
- function assignmentNote(actorId) {
4149
- if (!actorId) return "Unassigned devices keep legacy fallback attribution until you choose a person.";
4150
- if ((Array.isArray(state.lastSyncActors) ? state.lastSyncActors : []).find((item) => String(item?.actor_id || "") === actorId)?.is_local) return "Assigning this device to you keeps it in your identity across your devices, including private sync.";
4151
- return "This person receives memories from allowed projects by default. Use Only me on a memory when it should stay local.";
4152
- }
4153
4200
  function visibleSyncActors() {
4154
4201
  return deriveVisiblePeopleActors({
4155
4202
  actors: state.lastSyncActors,
@@ -4257,6 +4304,13 @@
4257
4304
  //#region src/tabs/sync/diagnostics.ts
4258
4305
  var SYNC_REDACT_MOUNT_ID = "syncRedactMount";
4259
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
+ }
4260
4314
  var _renderSyncPeers = () => {};
4261
4315
  function setRenderSyncPeers(fn) {
4262
4316
  _renderSyncPeers = fn;
@@ -4293,6 +4347,31 @@
4293
4347
  hintText: "Copy this command and run it on the other device. Use --include/--exclude to control which projects sync."
4294
4348
  };
4295
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
+ }
4296
4375
  function renderSyncStatus() {
4297
4376
  const syncStatusGrid = document.getElementById("syncStatusGrid");
4298
4377
  const syncMeta = document.getElementById("syncMeta");
@@ -4301,16 +4380,16 @@
4301
4380
  hideSkeleton("syncDiagSkeleton");
4302
4381
  const status = state.lastSyncStatus;
4303
4382
  if (!status) {
4304
- clearSyncMount(syncStatusGrid);
4383
+ renderSyncEmptyState(syncStatusGrid, diagnosticsLoadingState());
4305
4384
  renderActionList(syncActions, []);
4306
- if (syncMeta) syncMeta.textContent = "Loading sync status…";
4385
+ if (syncMeta) syncMeta.textContent = "Loading advanced sync diagnostics…";
4307
4386
  return;
4308
4387
  }
4309
4388
  const peers = status.peers || {};
4310
4389
  const pingPayload = status.ping || {};
4311
4390
  const syncPayload = status.sync || {};
4312
4391
  const lastSync = status.last_sync_at || status.last_sync_at_utc || null;
4313
- 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;
4314
4393
  const syncError = status.last_sync_error || "";
4315
4394
  const pingError = status.last_ping_error || "";
4316
4395
  const pending = Number(status.pending || 0);
@@ -4326,8 +4405,8 @@
4326
4405
  const peerCount = Object.keys(peers).length;
4327
4406
  const syncNoPeers = !syncDisabled && peerCount === 0;
4328
4407
  if (syncMeta) {
4329
- const parts = syncDisabled ? ["State: Disabled", "Sync is optional and currently off"] : syncNoPeers ? ["State: No peers", "Add peers to enable replication"] : [
4330
- `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}`,
4331
4410
  `Peers: ${peerCount}`,
4332
4411
  lastSync ? `Last sync: ${formatAgeShort(secondsSince(lastSync))} ago` : "Last sync: never"
4333
4412
  ];
@@ -4352,7 +4431,7 @@
4352
4431
  },
4353
4432
  {
4354
4433
  label: "Last sync",
4355
- value: "n/a"
4434
+ value: "Not running"
4356
4435
  }
4357
4436
  ] : syncNoPeers ? [
4358
4437
  {
@@ -4361,7 +4440,7 @@
4361
4440
  },
4362
4441
  {
4363
4442
  label: "Mode",
4364
- value: "Idle"
4443
+ value: "Ready to pair"
4365
4444
  },
4366
4445
  {
4367
4446
  label: "Pending events",
@@ -4369,7 +4448,7 @@
4369
4448
  },
4370
4449
  {
4371
4450
  label: "Last sync",
4372
- value: "n/a"
4451
+ value: "Waiting for first peer"
4373
4452
  }
4374
4453
  ] : [
4375
4454
  {
@@ -4385,7 +4464,7 @@
4385
4464
  value: lastSync ? `${formatAgeShort(secondsSince(lastSync))} ago` : "never"
4386
4465
  },
4387
4466
  {
4388
- label: "Last ping",
4467
+ label: "Last peer ping",
4389
4468
  value: lastPing ? `${formatAgeShort(secondsSince(lastPing))} ago` : "never"
4390
4469
  },
4391
4470
  {
@@ -4402,7 +4481,7 @@
4402
4481
  value: `${syncPayload.seconds_since_last}s`
4403
4482
  });
4404
4483
  if (!syncDisabled && !syncNoPeers && pingPayload.seconds_since_last) items.push({
4405
- label: "Since last ping",
4484
+ label: "Since last peer ping",
4406
4485
  value: `${pingPayload.seconds_since_last}s`
4407
4486
  });
4408
4487
  if (!syncDisabled && retentionEnabled && retentionLastError) items.push({
@@ -4417,7 +4496,7 @@
4417
4496
  command: "codemem sync start"
4418
4497
  });
4419
4498
  actions.push({
4420
- label: "Then run one immediate sync pass.",
4499
+ label: "Run one sync pass now.",
4421
4500
  command: "codemem sync once"
4422
4501
  });
4423
4502
  } else if (daemonState === "needs_attention") actions.push({
@@ -4434,7 +4513,7 @@
4434
4513
  command: "codemem sync restart && codemem sync once"
4435
4514
  });
4436
4515
  actions.push({
4437
- label: "Then run doctor for root cause.",
4516
+ label: "Run doctor for the root cause.",
4438
4517
  command: "codemem sync doctor"
4439
4518
  });
4440
4519
  } else if (!syncDisabled && !syncNoPeers && pending > 0) actions.push({
@@ -4448,7 +4527,7 @@
4448
4527
  if (!syncAttempts) return;
4449
4528
  const attempts = state.lastSyncAttempts;
4450
4529
  if (!Array.isArray(attempts) || !attempts.length) {
4451
- clearSyncMount(syncAttempts);
4530
+ renderSyncEmptyState(syncAttempts, noAttemptsState());
4452
4531
  return;
4453
4532
  }
4454
4533
  renderAttemptsList(syncAttempts, attempts.slice(0, 5).map((attempt) => {
@@ -4460,6 +4539,16 @@
4460
4539
  };
4461
4540
  }));
4462
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
+ }
4463
4552
  function renderPairingCollapsible() {
4464
4553
  const mount = document.getElementById("syncPairingDisclosureMount");
4465
4554
  const contentHost = document.getElementById("syncPairingPanelMount");
@@ -4573,7 +4662,10 @@
4573
4662
  className: "sync-action",
4574
4663
  children: [/* @__PURE__ */ u("div", {
4575
4664
  className: "sync-action-text",
4576
- 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
+ })]
4577
4669
  }), /* @__PURE__ */ u("button", {
4578
4670
  type: "button",
4579
4671
  className: "settings-button",
@@ -4591,9 +4683,9 @@
4591
4683
  className: "sync-action",
4592
4684
  children: [/* @__PURE__ */ u("div", {
4593
4685
  className: "sync-action-text",
4594
- children: ["No devices are paired yet.", /* @__PURE__ */ u("span", {
4686
+ children: ["Pair another device.", /* @__PURE__ */ u("span", {
4595
4687
  className: "sync-action-command",
4596
- children: "codemem sync pair --payload-only"
4688
+ children: "Use the pairing command in Advanced diagnostics when you are ready to connect one."
4597
4689
  })]
4598
4690
  }), /* @__PURE__ */ u("button", {
4599
4691
  type: "button",
@@ -4605,26 +4697,34 @@
4605
4697
  }
4606
4698
  function SyncInviteJoinPanels({ invitePanel, invitePanelOpen, inviteRestoreParent, joinPanel, joinRestoreParent, onToggleInvitePanel, pairedPeerCount, presenceStatus }) {
4607
4699
  return /* @__PURE__ */ u(k$2, { children: [
4608
- /* @__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, {
4609
4723
  invitePanel,
4610
4724
  invitePanelOpen,
4611
4725
  inviteRestoreParent,
4612
4726
  onToggle: onToggleInvitePanel
4613
- }),
4614
- presenceStatus === "not_enrolled" ? /* @__PURE__ */ u(k$2, { children: [joinPanel ? /* @__PURE__ */ u(ExistingElementSlot$1, {
4615
- element: joinPanel,
4616
- hidden: false,
4617
- restoreParent: joinRestoreParent
4618
- }) : null, /* @__PURE__ */ u("div", {
4619
- className: "sync-action",
4620
- children: /* @__PURE__ */ u("div", {
4621
- className: "sync-action-text",
4622
- children: ["This device is not connected to the team yet.", /* @__PURE__ */ u("span", {
4623
- className: "sync-action-command",
4624
- children: "Import a team invite or ask your admin to enroll this device"
4625
- })]
4626
- })
4627
- })] }) : null,
4727
+ }) : null,
4628
4728
  !pairedPeerCount && presenceStatus === "posted" ? /* @__PURE__ */ u(PairingCopyRow, {}) : null
4629
4729
  ] });
4630
4730
  }
@@ -4669,7 +4769,30 @@
4669
4769
  }, `${item.peerName}:${item.actorId}:${item.scopeLabel}`)) });
4670
4770
  }
4671
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
4672
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
+ }
4673
4796
  function AttentionRow({ item, onAction }) {
4674
4797
  const [busy, setBusy] = d(false);
4675
4798
  return /* @__PURE__ */ u("div", {
@@ -4698,225 +4821,256 @@
4698
4821
  }
4699
4822
  function PendingJoinRequestRow({ request, onApprove, onDeny }) {
4700
4823
  const [busyAction, setBusyAction] = d(null);
4824
+ const [feedback, setFeedback] = d(null);
4701
4825
  const [approveLabel, setApproveLabel] = d("Approve");
4702
4826
  const [denyLabel, setDenyLabel] = d("Deny");
4703
4827
  return /* @__PURE__ */ u("div", {
4704
4828
  className: "actor-row",
4705
- children: [/* @__PURE__ */ u("div", {
4706
- className: "actor-details",
4707
- children: [/* @__PURE__ */ u("div", {
4708
- className: "actor-title",
4709
- children: request.displayName
4710
- }), /* @__PURE__ */ u("div", {
4711
- className: "peer-meta",
4712
- children: ["request: ", request.requestId]
4713
- })]
4714
- }), /* @__PURE__ */ u("div", {
4715
- className: "actor-actions",
4716
- children: [/* @__PURE__ */ u("button", {
4717
- type: "button",
4718
- className: "settings-button",
4719
- disabled: busyAction !== null,
4720
- onClick: async () => {
4721
- setBusyAction("approve");
4722
- setApproveLabel("Approving…");
4723
- try {
4724
- await onApprove(request);
4725
- setApproveLabel("Approve");
4726
- } catch {
4727
- setApproveLabel("Retry");
4728
- } finally {
4729
- setBusyAction(null);
4730
- }
4731
- },
4732
- children: approveLabel
4733
- }), /* @__PURE__ */ u("button", {
4734
- type: "button",
4735
- className: "settings-button",
4736
- disabled: busyAction !== null,
4737
- onClick: async () => {
4738
- setBusyAction("deny");
4739
- setDenyLabel("Denying…");
4740
- try {
4741
- await onDeny(request);
4742
- setDenyLabel("Deny");
4743
- } catch {
4744
- setDenyLabel("Retry deny");
4745
- } finally {
4746
- setBusyAction(null);
4747
- }
4748
- },
4749
- children: denyLabel
4750
- })]
4751
- })]
4752
- });
4753
- }
4754
- function DiscoveredDeviceRow({ row, onInspectConflict, onRemoveConflict, onReview }) {
4755
- const [busy, setBusy] = d(null);
4756
- const [reviewLabel, setReviewLabel] = d(row.actionLabel || "Review device");
4757
- const [removeLabel, setRemoveLabel] = d("Remove broken device record");
4758
- return /* @__PURE__ */ u("div", {
4759
- className: "actor-row",
4760
- "data-discovered-device-id": row.deviceId,
4761
- children: [/* @__PURE__ */ u("div", {
4762
- className: "actor-details",
4763
- children: [/* @__PURE__ */ u("div", {
4764
- className: "actor-title",
4765
- children: [
4766
- /* @__PURE__ */ u("strong", { children: row.displayName }),
4767
- /* @__PURE__ */ u("span", {
4768
- className: `badge actor-badge${row.availabilityLabel === "Offline" ? "" : " local"}`,
4769
- children: row.availabilityLabel
4770
- }),
4771
- /* @__PURE__ */ u("span", {
4772
- className: "badge actor-badge",
4773
- children: row.connectionLabel
4774
- }),
4775
- row.approvalBadgeLabel ? /* @__PURE__ */ u("span", {
4776
- className: "badge actor-badge",
4777
- children: row.approvalBadgeLabel
4778
- }) : null
4779
- ]
4780
- }), /* @__PURE__ */ u("div", {
4781
- className: "peer-meta",
4782
- children: row.note
4783
- })]
4784
- }), /* @__PURE__ */ u("div", {
4785
- className: "actor-actions",
4786
- children: [
4787
- 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", {
4788
4841
  type: "button",
4789
4842
  className: "settings-button",
4790
- disabled: busy !== null,
4843
+ disabled: busyAction !== null,
4791
4844
  onClick: async () => {
4792
- setBusy("review");
4793
- setReviewLabel("Opening…");
4845
+ setBusyAction("approve");
4846
+ setApproveLabel("Approving…");
4794
4847
  try {
4795
- await onReview(row);
4796
- setReviewLabel(row.actionLabel || "Review device");
4848
+ setFeedback(await onApprove(request) || null);
4849
+ setApproveLabel("Approve");
4797
4850
  } catch {
4798
- setReviewLabel("Retry review");
4851
+ setApproveLabel("Retry");
4799
4852
  } finally {
4800
- setBusy(null);
4853
+ setBusyAction(null);
4801
4854
  }
4802
4855
  },
4803
- children: reviewLabel
4804
- }) : null,
4805
- row.mode === "stale" || row.mode === "ambiguous" || row.mode === "scope-pending" ? /* @__PURE__ */ u("div", {
4806
- className: "peer-meta",
4807
- children: row.actionMessage
4808
- }) : null,
4809
- row.mode === "paired" && row.pairedMessage ? /* @__PURE__ */ u("div", {
4810
- className: "peer-meta",
4811
- children: row.pairedMessage
4812
- }) : null,
4813
- row.mode === "conflict" ? /* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u("button", {
4814
- type: "button",
4815
- className: "settings-button",
4816
- disabled: busy !== null,
4817
- onClick: () => onInspectConflict(row),
4818
- children: "Open device details"
4856
+ children: approveLabel
4819
4857
  }), /* @__PURE__ */ u("button", {
4820
4858
  type: "button",
4821
4859
  className: "settings-button",
4822
- disabled: busy !== null,
4860
+ disabled: busyAction !== null,
4823
4861
  onClick: async () => {
4824
- setBusy("remove");
4825
- setRemoveLabel("Removing…");
4862
+ setBusyAction("deny");
4863
+ setDenyLabel("Denying…");
4826
4864
  try {
4827
- await onRemoveConflict(row);
4828
- setRemoveLabel("Remove broken device record");
4865
+ setFeedback(await onDeny(request) || null);
4866
+ setDenyLabel("Deny");
4829
4867
  } catch {
4830
- setRemoveLabel("Retry remove");
4868
+ setDenyLabel("Retry");
4831
4869
  } finally {
4832
- setBusy(null);
4870
+ setBusyAction(null);
4833
4871
  }
4834
4872
  },
4835
- children: removeLabel
4836
- })] }) : null
4837
- ]
4838
- })]
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
+ ]
4839
4973
  });
4840
4974
  }
4841
4975
  function ActionContent(props) {
4842
4976
  const hasAttentionItems = props.actionItems.length > 0;
4977
+ const hasOtherActionableWork = props.actionableCount > props.actionItems.length;
4843
4978
  return /* @__PURE__ */ u(k$2, { children: [
4844
- hasAttentionItems ? /* @__PURE__ */ u("div", {
4845
- className: "sync-action-text",
4846
- children: "Needs attention"
4847
- }) : null,
4848
- 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, {
4849
4981
  item,
4850
4982
  onAction: props.onAttentionAction
4851
4983
  }, item.id)) : null,
4852
- !hasAttentionItems && props.presenceStatus === "posted" ? /* @__PURE__ */ u("div", {
4984
+ !hasAttentionItems && !hasOtherActionableWork && props.presenceStatus === "posted" ? /* @__PURE__ */ u("div", {
4853
4985
  className: "sync-action",
4854
4986
  children: /* @__PURE__ */ u("div", {
4855
4987
  className: "sync-action-text",
4856
- children: ["No immediate issues", /* @__PURE__ */ u("span", {
4857
- className: "sync-action-command",
4858
- children: "Your devices and team records do not currently need review."
4859
- })]
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."
4860
4996
  })
4861
4997
  }) : null,
4862
4998
  !hasAttentionItems && props.presenceStatus === "not_enrolled" ? /* @__PURE__ */ u("div", {
4863
4999
  className: "sync-action",
4864
5000
  children: /* @__PURE__ */ u("div", {
4865
5001
  className: "sync-action-text",
4866
- children: ["This device still needs team enrollment", /* @__PURE__ */ u("span", {
5002
+ children: ["This device needs team enrollment", /* @__PURE__ */ u("span", {
4867
5003
  className: "sync-action-command",
4868
- 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."
4869
5005
  })]
4870
5006
  })
4871
- }) : null,
4872
- props.children
5007
+ }) : null
4873
5008
  ] });
4874
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
+ }
4875
5014
  function TeamStatusPortal({ mount, statusSummary }) {
4876
5015
  return $(/* @__PURE__ */ u("div", {
4877
5016
  className: "sync-team-summary",
4878
- children: [/* @__PURE__ */ u("div", {
4879
- className: "sync-team-status-row",
4880
- children: [/* @__PURE__ */ u("span", {
4881
- className: "sync-team-status-label",
4882
- children: "Status"
4883
- }), /* @__PURE__ */ u("span", {
4884
- className: statusSummary.badgeClassName,
4885
- children: statusSummary.presenceLabel
4886
- })]
4887
- }), /* @__PURE__ */ u("div", {
4888
- className: "sync-team-metrics",
4889
- children: statusSummary.metricsText
4890
- })]
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
+ ]
4891
5037
  }), mount);
4892
5038
  }
4893
5039
  function DiscoveredPortal({ mount, rows, onInspectConflict, onRemoveConflict, onReview }) {
4894
- if (!mount) return null;
4895
- return $(/* @__PURE__ */ u(k$2, { children: rows.map((row) => /* @__PURE__ */ u(DiscoveredDeviceRow, {
4896
- row,
4897
- onInspectConflict,
4898
- onRemoveConflict,
4899
- onReview
4900
- }, 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);
4901
5054
  }
4902
5055
  function PendingRequestsPortal({ mount, requests, onApprove, onDeny }) {
4903
- if (!mount || !requests.length) return null;
4904
- return $(/* @__PURE__ */ u(k$2, { children: [/* @__PURE__ */ u("div", {
4905
- className: "peer-meta",
4906
- children: [
4907
- requests.length,
4908
- " pending join request",
4909
- requests.length === 1 ? "" : "s"
4910
- ]
4911
- }), requests.map((request) => /* @__PURE__ */ u(PendingJoinRequestRow, {
4912
- request,
4913
- onApprove,
4914
- onDeny
4915
- }, 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);
4916
5069
  }
4917
5070
  function TeamSyncPanel(props) {
4918
5071
  return /* @__PURE__ */ u(k$2, { children: [
4919
5072
  /* @__PURE__ */ u(ActionContent, { ...props }),
5073
+ /* @__PURE__ */ u(TeamActionsContent, { children: props.children }),
4920
5074
  /* @__PURE__ */ u(TeamStatusPortal, {
4921
5075
  mount: props.listMount,
4922
5076
  statusSummary: props.statusSummary
@@ -9864,14 +10018,43 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9864
10018
  }
9865
10019
  function setInviteOutputVisibility() {
9866
10020
  const syncInviteOutput = document.getElementById("syncInviteOutput");
10021
+ const syncInviteWarnings = document.getElementById("syncInviteWarnings");
9867
10022
  if (!syncInviteOutput) return;
9868
10023
  const encoded = String(state.lastTeamInvite?.encoded || "").trim();
9869
10024
  syncInviteOutput.value = encoded;
9870
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"}` : ""}`;
9871
10041
  }
9872
10042
  function clearContent(node) {
9873
10043
  if (node) node.textContent = "";
9874
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
+ }
9875
10058
  function renderInvitePolicySelect() {
9876
10059
  const mount = document.getElementById("syncInvitePolicyMount");
9877
10060
  if (!mount) return;
@@ -9939,11 +10122,13 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9939
10122
  const meta = document.getElementById("syncTeamMeta");
9940
10123
  const setupPanel = document.getElementById("syncSetupPanel");
9941
10124
  const list = document.getElementById("syncTeamStatus");
10125
+ const listHeading = list?.previousElementSibling;
9942
10126
  const actions = document.getElementById("syncTeamActions");
9943
10127
  if (!meta || !setupPanel || !list || !actions) return;
9944
10128
  renderAdminSetupDisclosure();
9945
10129
  renderInvitePolicySelect();
9946
10130
  setInviteOutputVisibility();
10131
+ setJoinFeedbackVisibility();
9947
10132
  const invitePanel = document.getElementById("syncInvitePanel");
9948
10133
  const inviteRestoreParent = document.getElementById("syncAdminSection");
9949
10134
  const joinPanel = document.getElementById("syncJoinPanel");
@@ -9957,16 +10142,24 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9957
10142
  ensureJoinPanelInSetupSection();
9958
10143
  const coordinator = state.lastSyncCoordinator;
9959
10144
  const syncView = state.lastSyncViewModel || {
9960
- summary: {},
10145
+ summary: {
10146
+ connectedDeviceCount: 0,
10147
+ seenOnTeamCount: 0,
10148
+ offlineTeamDeviceCount: 0
10149
+ },
10150
+ duplicatePeople: [],
9961
10151
  attentionItems: []
9962
10152
  };
9963
10153
  const focusAttentionTarget = (item) => {
9964
10154
  if (item.kind === "possible-duplicate-person") {
9965
10155
  const actorList = document.getElementById("syncActorsList");
9966
- if (actorList instanceof HTMLElement) actorList.scrollIntoView({
9967
- block: "center",
9968
- behavior: "smooth"
9969
- });
10156
+ if (actorList instanceof HTMLElement) {
10157
+ actorList.scrollIntoView({
10158
+ block: "center",
10159
+ behavior: syncScrollBehavior()
10160
+ });
10161
+ pulseAttentionTarget(actorList);
10162
+ }
9970
10163
  return;
9971
10164
  }
9972
10165
  const deviceId = String(item.deviceId || "").trim();
@@ -9976,10 +10169,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9976
10169
  if (renameInput instanceof HTMLInputElement) {
9977
10170
  renameInput.scrollIntoView({
9978
10171
  block: "center",
9979
- behavior: "smooth"
10172
+ behavior: syncScrollBehavior()
9980
10173
  });
9981
10174
  renameInput.focus();
9982
10175
  renameInput.select();
10176
+ pulseAttentionTarget(renameInput);
9983
10177
  return;
9984
10178
  }
9985
10179
  }
@@ -9987,21 +10181,25 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
9987
10181
  if (peerCard instanceof HTMLElement) {
9988
10182
  peerCard.scrollIntoView({
9989
10183
  block: "center",
9990
- behavior: "smooth"
10184
+ behavior: syncScrollBehavior()
9991
10185
  });
10186
+ pulseAttentionTarget(peerCard);
9992
10187
  return;
9993
10188
  }
9994
10189
  const discoveredRow = document.querySelector(`[data-discovered-device-id="${CSS.escape(deviceId)}"]`);
9995
- if (discoveredRow instanceof HTMLElement) discoveredRow.scrollIntoView({
9996
- block: "center",
9997
- behavior: "smooth"
9998
- });
10190
+ if (discoveredRow instanceof HTMLElement) {
10191
+ discoveredRow.scrollIntoView({
10192
+ block: "center",
10193
+ behavior: syncScrollBehavior()
10194
+ });
10195
+ pulseAttentionTarget(discoveredRow);
10196
+ }
9999
10197
  };
10000
10198
  const reviewDuplicatePeople = async (item) => {
10001
10199
  const actorIds = Array.isArray(item.actorIds) ? item.actorIds.map((value) => String(value || "").trim()).filter(Boolean) : [];
10002
10200
  const actors = (Array.isArray(state.lastSyncActors) ? state.lastSyncActors : []).filter((actor) => actorIds.includes(String(actor?.actor_id || "").trim()));
10003
10201
  if (actors.length < 2) {
10004
- 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");
10005
10203
  return;
10006
10204
  }
10007
10205
  const result = await openDuplicatePersonDialog({
@@ -10023,7 +10221,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10023
10221
  const primary = actors.find((actor) => String(actor?.actor_id || "") === result.primaryActorId);
10024
10222
  const secondary = actors.find((actor) => String(actor?.actor_id || "") === result.secondaryActorId);
10025
10223
  if (!primary?.actor_id || !secondary?.actor_id) {
10026
- 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");
10027
10225
  return;
10028
10226
  }
10029
10227
  try {
@@ -10050,7 +10248,8 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10050
10248
  });
10051
10249
  };
10052
10250
  const configured = Boolean(coordinator && coordinator.configured);
10053
- 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() : "";
10054
10253
  if (!configured) {
10055
10254
  teardownTeamSyncRender(actions, [
10056
10255
  list,
@@ -10059,6 +10258,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10059
10258
  ]);
10060
10259
  setupPanel.hidden = false;
10061
10260
  list.hidden = true;
10261
+ if (listHeading) listHeading.hidden = true;
10062
10262
  actions.hidden = true;
10063
10263
  if (joinRequests) joinRequests.hidden = true;
10064
10264
  if (discoveredPanel) discoveredPanel.hidden = true;
@@ -10066,21 +10266,24 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10066
10266
  }
10067
10267
  setupPanel.hidden = true;
10068
10268
  list.hidden = false;
10269
+ if (listHeading) listHeading.hidden = false;
10069
10270
  actions.hidden = false;
10070
10271
  const presenceStatus = String(coordinator.presence_status || "");
10071
- const presenceLabel = presenceStatus === "posted" ? "Connected" : presenceStatus === "not_enrolled" ? "Not connected — import an invite or ask your admin to enroll this device" : "Connection error";
10072
- const metricParts = [`Connected devices: ${Number(syncView.summary?.connectedDeviceCount || 0)}`, `Seen on team: ${Number(syncView.summary?.seenOnTeamCount || 0)}`];
10073
- if (Number(syncView.summary?.offlineTeamDeviceCount || 0) > 0) metricParts.push(`Offline on team: ${Number(syncView.summary?.offlineTeamDeviceCount || 0)}`);
10074
- const statusSummary = {
10075
- badgeClassName: `pill ${presenceStatus === "posted" ? "pill-success" : presenceStatus === "not_enrolled" ? "pill-warning" : "pill-error"}`,
10076
- metricsText: metricParts.join(" · "),
10077
- presenceLabel
10078
- };
10272
+ const presenceLabel = presenceStatus === "posted" ? "Connected" : presenceStatus === "not_enrolled" ? "Needs enrollment" : "Connection error";
10079
10273
  const localPeers = Array.isArray(state.lastSyncPeers) ? state.lastSyncPeers : [];
10080
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}`);
10081
10280
  const discoveredRows = (Array.isArray(coordinator.discovered_devices) ? coordinator.discovered_devices : []).map((device) => {
10082
10281
  const deviceId = String(device.device_id || "").trim();
10083
- const displayName = String(device.display_name || "").trim() || deviceId || "Discovered device";
10282
+ const displayName = resolveFriendlyDeviceName({
10283
+ coordinatorName: String(device.display_name || "").trim(),
10284
+ deviceId
10285
+ }) || "Discovered device";
10286
+ const displayTitle = deviceId && displayName !== deviceId ? deviceId : null;
10084
10287
  const fingerprint = String(device.fingerprint || "").trim();
10085
10288
  const hasAmbiguousCoordinatorGroup = (Array.isArray(device.groups) ? device.groups.map((value) => String(value || "").trim()).filter(Boolean) : []).length > 1;
10086
10289
  const pairedPeer = localPeers.find((peer) => String(peer?.peer_device_id || "") === deviceId);
@@ -10092,33 +10295,29 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10092
10295
  const hasConflict = Boolean(pairedPeer) && Boolean(fingerprint) && Boolean(pairedFingerprint) && pairedFingerprint !== fingerprint;
10093
10296
  const canAccept = Boolean(deviceId) && Boolean(fingerprint) && !pairedPeer && !device.stale && !hasAmbiguousCoordinatorGroup;
10094
10297
  const addresses = Array.isArray(device.addresses) ? device.addresses : [];
10095
- const noteParts = [deviceId, addresses.length ? addresses.map((address) => isSyncRedactionEnabled() ? redactAddress(String(address || "")) : String(address || "")).filter(Boolean).join(" · ") : "No fresh addresses"];
10298
+ const noteParts = [addresses.length ? addresses.map((address) => isSyncRedactionEnabled() ? redactAddress(String(address || "")) : String(address || "")).filter(Boolean).join(" · ") : "No fresh addresses"];
10299
+ if (!addresses.length && displayTitle) noteParts.push(`device id: ${deviceId}`);
10096
10300
  let actionMessage = null;
10097
10301
  let mode = canAccept ? "accept" : "none";
10098
10302
  let pairedMessage = null;
10099
- if (hasConflict) {
10100
- noteParts.push("repair the local peer before accepting this discovered device");
10101
- mode = "conflict";
10102
- } else if (hasAmbiguousCoordinatorGroup) {
10103
- noteParts.push("this device appears in multiple coordinator groups; review team setup before approving it here");
10104
- 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.";
10105
10306
  mode = "ambiguous";
10106
10307
  } else if (pairedPeer && isPeerScopeReviewPending(deviceId)) {
10107
- noteParts.push("scope review pending in People");
10108
- 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.`;
10109
10309
  mode = "scope-pending";
10110
10310
  } else if (pairedPeer?.last_error) {
10111
- noteParts.push(`paired error: ${String(pairedPeer.last_error)}`);
10311
+ noteParts.push(`error: ${String(pairedPeer.last_error)}`);
10112
10312
  mode = "paired";
10113
10313
  } else if (pairedPeer?.status?.peer_state) {
10114
- noteParts.push(`paired status: ${String(pairedPeer.status.peer_state)}`);
10314
+ noteParts.push(`status: ${String(pairedPeer.status.peer_state)}`);
10115
10315
  mode = "paired";
10116
10316
  } else if (!pairedPeer && device.stale) {
10117
- actionMessage = "Wait for a fresh coordinator presence update.";
10317
+ actionMessage = "Wait for a fresh coordinator presence update, then review this device again here.";
10118
10318
  mode = "stale";
10119
10319
  } else if (pairedPeer) mode = "paired";
10120
- if (approvalSummary.description) noteParts.push(approvalSummary.description);
10121
- 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;
10122
10321
  return {
10123
10322
  actionMessage,
10124
10323
  actionLabel: approvalSummary.actionLabel || "Review device",
@@ -10127,6 +10326,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10127
10326
  connectionLabel: hasConflict ? SYNC_TERMINOLOGY.conflicts : pairedPeer ? SYNC_TERMINOLOGY.pairedLocally : "Not connected on this device",
10128
10327
  deviceId,
10129
10328
  displayName,
10329
+ displayTitle,
10130
10330
  fingerprint,
10131
10331
  mode,
10132
10332
  note: noteParts.join(" · "),
@@ -10137,9 +10337,19 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10137
10337
  displayName: String(request.display_name || request.device_id || "Pending device"),
10138
10338
  requestId: String(request.request_id || "")
10139
10339
  }));
10140
- if (discoveredPanel) discoveredPanel.hidden = discoveredRows.length === 0;
10141
- if (discoveredMeta) discoveredMeta.textContent = discoveredRows.length ? "These devices are visible through team discovery. Review them before connecting them on this machine." : "";
10142
- 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;
10143
10353
  teardownTeamSyncRender(actions, [
10144
10354
  list,
10145
10355
  joinRequests,
@@ -10150,6 +10360,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10150
10360
  actions.appendChild(actionMount);
10151
10361
  renderIntoSyncMount(actionMount, _$1(TeamSyncPanel, {
10152
10362
  actionItems: attentionItems,
10363
+ actionableCount,
10153
10364
  children: _$1(SyncInviteJoinPanels, {
10154
10365
  invitePanel,
10155
10366
  invitePanelOpen: teamInvitePanelOpen,
@@ -10171,11 +10382,18 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10171
10382
  onApproveJoinRequest: async (request) => {
10172
10383
  try {
10173
10384
  await reviewJoinRequest(request.requestId, "approve");
10174
- 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;
10175
10390
  await _loadSyncData$1();
10391
+ return feedback;
10176
10392
  } catch (error) {
10177
- showGlobalNotice(friendlyError(error, "Failed to approve join request."), "warning");
10178
- 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
+ };
10179
10397
  }
10180
10398
  },
10181
10399
  onAttentionAction: async (item) => {
@@ -10192,14 +10410,21 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10192
10410
  confirmLabel: "Deny request",
10193
10411
  cancelLabel: "Keep request pending",
10194
10412
  tone: "danger"
10195
- })) return;
10413
+ })) return null;
10196
10414
  try {
10197
10415
  await reviewJoinRequest(request.requestId, "deny");
10198
- 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;
10199
10421
  await _loadSyncData$1();
10422
+ return feedback;
10200
10423
  } catch (error) {
10201
- showGlobalNotice(friendlyError(error, "Failed to deny join request."), "warning");
10202
- 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
+ };
10203
10428
  }
10204
10429
  },
10205
10430
  onInspectConflict: (row) => {
@@ -10207,7 +10432,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10207
10432
  if (peerCard instanceof HTMLElement) {
10208
10433
  peerCard.scrollIntoView({
10209
10434
  block: "center",
10210
- behavior: "smooth"
10435
+ behavior: syncScrollBehavior()
10211
10436
  });
10212
10437
  showGlobalNotice(`Opened the conflicting local device record for ${row.displayName}.`, "warning");
10213
10438
  return;
@@ -10221,20 +10446,27 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10221
10446
  confirmLabel: "Remove device record",
10222
10447
  cancelLabel: "Keep device record",
10223
10448
  tone: "danger"
10224
- })) return;
10449
+ })) return null;
10225
10450
  try {
10226
10451
  await deletePeer(row.deviceId);
10227
- 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;
10228
10457
  await _loadSyncData$1();
10458
+ return feedback;
10229
10459
  } catch (error) {
10230
- showGlobalNotice(friendlyError(error, "Failed to remove the broken local device record."), "warning");
10231
- 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
+ };
10232
10464
  }
10233
10465
  },
10234
10466
  onReviewDiscoveredDevice: async (row) => {
10235
10467
  try {
10236
10468
  const reviewedName = await reviewDiscoveredDeviceName(row.displayName);
10237
- if (!reviewedName) return;
10469
+ if (!reviewedName) return null;
10238
10470
  const result = await acceptDiscoveredPeer(row.deviceId, row.fingerprint);
10239
10471
  const optimisticName = String(result?.name || row.displayName || "").trim() || row.displayName;
10240
10472
  state.pendingAcceptedSyncPeers = [...Array.isArray(state.pendingAcceptedSyncPeers) ? state.pendingAcceptedSyncPeers.filter((peer) => String(peer?.peer_device_id || "").trim() !== row.deviceId) : [], {
@@ -10247,23 +10479,44 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10247
10479
  last_error: "Waiting for the other device to approve this one."
10248
10480
  }];
10249
10481
  requestPeerScopeReview(row.deviceId);
10250
- 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
+ };
10251
10486
  try {
10252
10487
  if (reviewedName.trim() !== optimisticName.trim()) {
10253
10488
  await renamePeer(row.deviceId, reviewedName.trim());
10254
- 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
+ };
10255
10497
  }
10256
10498
  } catch (error) {
10257
- 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
+ };
10258
10503
  }
10504
+ state.syncDiscoveredFeedback = feedback;
10259
10505
  try {
10260
10506
  await _loadSyncData$1();
10261
10507
  } catch (error) {
10262
- 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;
10263
10513
  }
10514
+ return feedback;
10264
10515
  } catch (error) {
10265
- showGlobalNotice(friendlyError(error, "Failed to review this device."), "warning");
10266
- 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
+ };
10267
10520
  }
10268
10521
  },
10269
10522
  pendingJoinRequests,
@@ -10300,17 +10553,23 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10300
10553
  ttl_hours: ttlValue || 24
10301
10554
  });
10302
10555
  state.lastTeamInvite = result;
10556
+ setInviteOutputVisibility();
10303
10557
  syncInviteOutput.value = String(result.encoded || "");
10304
10558
  syncInviteOutput.hidden = false;
10305
10559
  syncInviteOutput.focus();
10306
10560
  syncInviteOutput.select();
10307
- showGlobalNotice("Invite created. Copy the text above and share it with your teammate.");
10308
- (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");
10309
10563
  } catch (error) {
10310
- showGlobalNotice(friendlyError(error, "Failed to create invite."), "warning");
10311
- } 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";
10312
10566
  syncCreateInviteButton.disabled = false;
10313
- syncCreateInviteButton.textContent = "Create invite";
10567
+ return;
10568
+ } finally {
10569
+ if (syncCreateInviteButton.disabled) {
10570
+ syncCreateInviteButton.disabled = false;
10571
+ syncCreateInviteButton.textContent = "Create invite";
10572
+ }
10314
10573
  }
10315
10574
  });
10316
10575
  syncJoinButton?.addEventListener("click", async () => {
@@ -10326,14 +10585,37 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10326
10585
  try {
10327
10586
  const result = await importCoordinatorInvite(inviteValue);
10328
10587
  state.lastTeamJoin = result;
10329
- 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();
10330
10594
  syncJoinInvite.value = "";
10331
- 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
+ }
10332
10605
  } catch (error) {
10333
- showGlobalNotice(friendlyError(error, "Failed to import invite."), "warning");
10334
- } 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";
10335
10612
  syncJoinButton.disabled = false;
10336
- syncJoinButton.textContent = "Join team";
10613
+ return;
10614
+ } finally {
10615
+ if (syncJoinButton.disabled) {
10616
+ syncJoinButton.disabled = false;
10617
+ syncJoinButton.textContent = "Join team";
10618
+ }
10337
10619
  }
10338
10620
  });
10339
10621
  syncNowButton?.addEventListener("click", async () => {
@@ -10344,7 +10626,10 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10344
10626
  const summary = summarizeSyncRunResult(await triggerSync());
10345
10627
  showGlobalNotice(summary.message, summary.warning ? "warning" : void 0);
10346
10628
  } catch (error) {
10347
- 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;
10348
10633
  }
10349
10634
  syncNowButton.disabled = false;
10350
10635
  syncNowButton.textContent = "Sync now";
@@ -10432,12 +10717,17 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10432
10717
  className: "actor-title",
10433
10718
  children: [/* @__PURE__ */ u("strong", { children: actorDisplayLabel(actor) }), /* @__PURE__ */ u("span", {
10434
10719
  className: `badge actor-badge${actor.is_local ? " local" : ""}`,
10720
+ title: actor.is_local ? localActorNote(hiddenLocalDuplicateCount) : void 0,
10435
10721
  children: actor.is_local ? "You" : `${count} device${count === 1 ? "" : "s"}`
10436
10722
  })]
10437
- }), /* @__PURE__ */ u("div", {
10723
+ }), !actor.is_local ? /* @__PURE__ */ u("div", {
10438
10724
  className: "peer-meta",
10439
- children: actor.is_local ? localActorNote(hiddenLocalDuplicateCount) : `${count} assigned device${count === 1 ? "" : "s"}`
10440
- })]
10725
+ children: [
10726
+ count,
10727
+ " assigned device",
10728
+ count === 1 ? "" : "s"
10729
+ ]
10730
+ }) : null]
10441
10731
  }), /* @__PURE__ */ u("div", {
10442
10732
  className: "actor-actions",
10443
10733
  children: actor.is_local ? /* @__PURE__ */ u("div", {
@@ -10493,7 +10783,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10493
10783
  function SyncActorsList({ actors, hiddenLocalDuplicateCount, onRename, onMerge }) {
10494
10784
  if (!actors.length) return /* @__PURE__ */ u("div", {
10495
10785
  className: "sync-empty-state",
10496
- 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." })]
10497
10787
  });
10498
10788
  return /* @__PURE__ */ u(k$2, { children: actors.map((actor) => {
10499
10789
  const actorId = String(actor.actor_id || actor.display_name || actor.is_local || "");
@@ -10544,9 +10834,6 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10544
10834
  function listText(value) {
10545
10835
  return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
10546
10836
  }
10547
- function summaryText(prefix, values, emptyLabel) {
10548
- return `${prefix}: ${values.join(", ") || emptyLabel}`;
10549
- }
10550
10837
  function ExistingElementSlot({ element }) {
10551
10838
  const hostRef = A$1(null);
10552
10839
  _(() => {
@@ -10569,9 +10856,9 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10569
10856
  const scope = peer.project_scope || {};
10570
10857
  const includeList = listText(scope.include);
10571
10858
  const excludeList = listText(scope.exclude);
10572
- const effectiveInclude = listText(scope.effective_include);
10573
- const effectiveExclude = listText(scope.effective_exclude);
10574
- const inheritsGlobal = Boolean(scope.inherits_global);
10859
+ listText(scope.effective_include);
10860
+ listText(scope.effective_exclude);
10861
+ Boolean(scope.inherits_global);
10575
10862
  const primaryAddress = pickPrimaryAddress(peer.addresses);
10576
10863
  const peerAddresses = Array.isArray(peer.addresses) ? Array.from(new Set(peer.addresses.filter(Boolean).map((value) => String(value)))) : [];
10577
10864
  const addressLine = peerAddresses.length ? peerAddresses.map((address) => isSyncRedactionEnabled() ? redactAddress(address) : address).join(" · ") : "No addresses";
@@ -10582,6 +10869,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10582
10869
  const cardRef = A$1(null);
10583
10870
  const [scopeHost, setScopeHost] = d(null);
10584
10871
  const [renameValue, setRenameValue] = d(displayName);
10872
+ const [feedback, setFeedback] = d(() => state.syncPeerFeedbackById.get(peerId) ?? null);
10585
10873
  const [renameBusy, setRenameBusy] = d(false);
10586
10874
  const [renameLabel, setRenameLabel] = d("Save name");
10587
10875
  const [syncBusy, setSyncBusy] = d(false);
@@ -10614,6 +10902,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10614
10902
  const excludeEditor = T$1(() => createChipEditor(excludeList, "Add excluded project", "No exclusions"), [peerId, excludeList.join("|")]);
10615
10903
  y(() => {
10616
10904
  setRenameValue(displayName);
10905
+ setFeedback(state.syncPeerFeedbackById.get(peerId) ?? null);
10617
10906
  setRenameBusy(false);
10618
10907
  setRenameLabel("Save name");
10619
10908
  setSyncBusy(false);
@@ -10637,28 +10926,33 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10637
10926
  if (!scopeReviewRequested || !cardRef.current) return;
10638
10927
  queueMicrotask(() => cardRef.current?.scrollIntoView({
10639
10928
  block: "center",
10640
- behavior: "smooth"
10929
+ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth"
10641
10930
  }));
10642
10931
  }, [scopeReviewRequested]);
10643
10932
  async function rename() {
10644
10933
  if (!peerId) return;
10645
10934
  const nextName = renameValue.trim();
10646
10935
  if (!nextName) {
10647
- 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);
10648
10942
  document.querySelector(`[data-device-name-input="${CSS.escape(peerId)}"]`)?.focus();
10649
10943
  return;
10650
10944
  }
10651
10945
  setRenameBusy(true);
10652
10946
  setRenameLabel("Saving…");
10653
- let ok = false;
10654
10947
  try {
10655
- await onRename(peerId, nextName);
10656
- ok = true;
10948
+ const nextFeedback = await onRename(peerId, nextName);
10949
+ setFeedback(nextFeedback);
10950
+ state.syncPeerFeedbackById.set(peerId, nextFeedback);
10951
+ setRenameLabel("Save name");
10657
10952
  } catch {
10658
- setRenameLabel("Retry save");
10953
+ setRenameLabel("Retry");
10659
10954
  } finally {
10660
10955
  setRenameBusy(false);
10661
- if (ok) setRenameLabel("Save name");
10662
10956
  }
10663
10957
  }
10664
10958
  async function sync() {
@@ -10673,7 +10967,9 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10673
10967
  }
10674
10968
  setSyncBusy(true);
10675
10969
  try {
10676
- await onSync(peer, primaryAddress);
10970
+ const nextFeedback = await onSync(peer, primaryAddress);
10971
+ setFeedback(nextFeedback);
10972
+ if (nextFeedback) state.syncPeerFeedbackById.set(peerId, nextFeedback);
10677
10973
  } finally {
10678
10974
  setSyncBusy(false);
10679
10975
  }
@@ -10703,46 +10999,46 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10703
10999
  async function savePerson() {
10704
11000
  if (!peerId) return;
10705
11001
  setApplyActorBusy(true);
10706
- setApplyActorLabel("Applying…");
10707
- let ok = false;
11002
+ setApplyActorLabel("Saving…");
10708
11003
  try {
10709
- await onAssignActor(peerId, selectedActorId || null);
10710
- ok = true;
11004
+ const nextFeedback = await onAssignActor(peerId, selectedActorId || null);
11005
+ setFeedback(nextFeedback);
11006
+ state.syncPeerFeedbackById.set(peerId, nextFeedback);
11007
+ setApplyActorLabel("Save person");
10711
11008
  } catch {
10712
11009
  setApplyActorLabel("Retry");
10713
11010
  } finally {
10714
11011
  setApplyActorBusy(false);
10715
- if (ok) setApplyActorLabel("Save person");
10716
11012
  }
10717
11013
  }
10718
11014
  async function saveScope() {
10719
11015
  if (!peerId) return;
10720
11016
  setSaveScopeBusy(true);
10721
11017
  setSaveScopeLabel("Saving…");
10722
- let ok = false;
10723
11018
  try {
10724
- await onSaveScope(peerId, includeEditor.values(), excludeEditor.values());
10725
- 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");
10726
11023
  } catch {
10727
- setSaveScopeLabel("Retry save");
11024
+ setSaveScopeLabel("Retry");
10728
11025
  } finally {
10729
11026
  setSaveScopeBusy(false);
10730
- if (ok) setSaveScopeLabel("Save scope");
10731
11027
  }
10732
11028
  }
10733
11029
  async function resetScope() {
10734
11030
  if (!peerId) return;
10735
11031
  setResetScopeBusy(true);
10736
11032
  setResetScopeLabel("Resetting…");
10737
- let ok = false;
10738
11033
  try {
10739
- await onResetScope(peerId);
10740
- ok = true;
11034
+ const nextFeedback = await onResetScope(peerId);
11035
+ setFeedback(nextFeedback);
11036
+ state.syncPeerFeedbackById.set(peerId, nextFeedback);
11037
+ setResetScopeLabel("Reset to global scope");
10741
11038
  } catch {
10742
- setResetScopeLabel("Retry reset");
11039
+ setResetScopeLabel("Retry");
10743
11040
  } finally {
10744
11041
  setResetScopeBusy(false);
10745
- if (ok) setResetScopeLabel("Reset to global scope");
10746
11042
  }
10747
11043
  }
10748
11044
  return /* @__PURE__ */ u("div", {
@@ -10836,10 +11132,10 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10836
11132
  children: [
10837
11133
  scopeReviewRequested ? /* @__PURE__ */ u("div", {
10838
11134
  className: "peer-meta",
10839
- 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."
10840
11136
  }) : pendingScopeReview ? /* @__PURE__ */ u("div", {
10841
11137
  className: "peer-meta",
10842
- 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."
10843
11139
  }) : null,
10844
11140
  /* @__PURE__ */ u("div", {
10845
11141
  className: "peer-scope-summary",
@@ -10849,10 +11145,6 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10849
11145
  className: "peer-meta",
10850
11146
  children: peer.actor_display_name ? `Assigned to ${peer.claimed_local_actor ? "You" : String(peer.actor_display_name)}` : "Unassigned person"
10851
11147
  }),
10852
- /* @__PURE__ */ u("div", {
10853
- className: "peer-meta",
10854
- children: trustSummary.description
10855
- }),
10856
11148
  /* @__PURE__ */ u("div", {
10857
11149
  className: "peer-actor-row",
10858
11150
  children: [/* @__PURE__ */ u("div", {
@@ -10876,18 +11168,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10876
11168
  children: applyActorLabel
10877
11169
  })]
10878
11170
  }),
10879
- /* @__PURE__ */ u("div", {
10880
- className: "peer-scope-effective",
10881
- children: assignmentNote(selectedActorId)
10882
- }),
10883
- /* @__PURE__ */ u("div", {
10884
- className: "peer-scope-summary",
10885
- children: inheritsGlobal ? "Using global sync scope" : `Device override · include: ${includeList.join(", ") || "all"} · exclude: ${excludeList.join(", ") || "none"}`
10886
- }),
10887
- /* @__PURE__ */ u("div", {
10888
- className: "peer-scope-effective",
10889
- children: `Effective scope · ${summaryText("include", effectiveInclude, "all")} · ${summaryText("exclude", effectiveExclude, "none")}`
10890
- }),
11171
+ /* @__PURE__ */ u(SyncInlineFeedback, { feedback }),
10891
11172
  /* @__PURE__ */ u("div", { ref: setScopeHost })
10892
11173
  ]
10893
11174
  })
@@ -10895,17 +11176,20 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10895
11176
  });
10896
11177
  }
10897
11178
  function SyncPeersList(props) {
10898
- 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", {
10899
11183
  className: "sync-empty-state",
10900
- children: "No devices connected on this machine yet. Use the pairing command in Diagnostics to connect another device."
10901
- });
10902
- 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) => {
10903
11187
  const peerId = String(peer.peer_device_id || peer.name || "unknown-peer");
10904
11188
  return /* @__PURE__ */ u(SyncPeerCard, {
10905
11189
  peer,
10906
11190
  ...props
10907
11191
  }, peerId);
10908
- }) });
11192
+ })] });
10909
11193
  }
10910
11194
  function renderSyncPeersList(mount, props) {
10911
11195
  renderIntoSyncMount(mount, /* @__PURE__ */ u(SyncPeersList, { ...props }));
@@ -10961,6 +11245,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10961
11245
  //#region src/tabs/sync/people.ts
10962
11246
  var _loadSyncData = async () => {};
10963
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
+ }
10964
11254
  function setLoadSyncData(fn) {
10965
11255
  _loadSyncData = fn;
10966
11256
  }
@@ -10969,6 +11259,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10969
11259
  const actorMeta = document.getElementById("syncActorsMeta");
10970
11260
  if (!actorList) return;
10971
11261
  hideSkeleton("syncActorsSkeleton");
11262
+ setPeopleCreateControlsDisabled(false);
10972
11263
  const actorVisibility = deriveVisiblePeopleActors({
10973
11264
  actors: state.lastSyncActors,
10974
11265
  peers: state.lastSyncPeers,
@@ -10976,7 +11267,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10976
11267
  });
10977
11268
  const actors = actorVisibility.visibleActors;
10978
11269
  if (actorMeta) {
10979
- 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.";
10980
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.`;
10981
11272
  }
10982
11273
  renderSyncActorsList(actorList, {
@@ -10998,6 +11289,16 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
10998
11289
  }
10999
11290
  });
11000
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
+ }
11001
11302
  function renderSyncPeers() {
11002
11303
  const syncPeers = document.getElementById("syncPeers");
11003
11304
  if (!syncPeers) return;
@@ -11008,75 +11309,133 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11008
11309
  onRename: async (peerId, nextName) => {
11009
11310
  try {
11010
11311
  await renamePeer(peerId, nextName);
11011
- showGlobalNotice("Device name saved.");
11012
11312
  await _loadSyncData();
11313
+ return {
11314
+ message: "Device name saved.",
11315
+ tone: "success"
11316
+ };
11013
11317
  } catch (error) {
11014
- showGlobalNotice(friendlyError(error, "Failed to save device name."), "warning");
11015
- throw error;
11318
+ return {
11319
+ message: friendlyError(error, "Failed to save device name."),
11320
+ tone: "warning"
11321
+ };
11016
11322
  }
11017
11323
  },
11018
11324
  onSync: async (peer, address) => {
11019
11325
  try {
11020
11326
  const summary = summarizeSyncRunResult(await triggerSync(address));
11021
11327
  const peerId = String(peer?.peer_device_id || "");
11022
- if (!summary.ok) showGlobalNotice(summary.message, "warning");
11023
- 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");
11024
- 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;
11025
11350
  } catch (error) {
11026
- showGlobalNotice(friendlyError(error, "Failed to trigger sync."), "warning");
11027
- throw error;
11028
- }
11029
- try {
11030
- await _loadSyncData();
11031
- } catch {
11032
- 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
+ };
11033
11355
  }
11034
- return null;
11035
11356
  },
11036
11357
  onRemove: async (peerId, label) => {
11037
11358
  try {
11038
11359
  await deletePeer(peerId);
11039
- 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;
11040
11366
  await _loadSyncData();
11367
+ return feedback;
11041
11368
  } catch (error) {
11042
- showGlobalNotice(friendlyError(error, "Failed to remove peer."), "warning");
11043
- throw error;
11369
+ return {
11370
+ message: friendlyError(error, "Failed to remove peer. The local peer entry is still here."),
11371
+ tone: "warning"
11372
+ };
11044
11373
  }
11045
11374
  },
11046
11375
  onAssignActor: async (peerId, actorId) => {
11047
11376
  try {
11048
11377
  await assignPeerActor(peerId, actorId);
11049
- showGlobalNotice(actorId ? "Device person updated." : "Device person cleared.");
11050
11378
  await _loadSyncData();
11379
+ return {
11380
+ message: actorId ? "Device person updated." : "Device person cleared.",
11381
+ tone: "success"
11382
+ };
11051
11383
  } catch (error) {
11052
- showGlobalNotice(friendlyError(error, "Failed to update device person."), "warning");
11053
- throw error;
11384
+ return {
11385
+ message: friendlyError(error, "Failed to update device person. The current assignment is unchanged."),
11386
+ tone: "warning"
11387
+ };
11054
11388
  }
11055
11389
  },
11056
11390
  onSaveScope: async (peerId, include, exclude) => {
11057
11391
  try {
11058
11392
  await updatePeerScope(peerId, include, exclude);
11059
11393
  clearPeerScopeReview(peerId);
11060
- showGlobalNotice("Device sync scope saved.");
11061
11394
  await _loadSyncData();
11395
+ return {
11396
+ message: "Device sync scope saved.",
11397
+ tone: "success"
11398
+ };
11062
11399
  } catch (error) {
11063
- showGlobalNotice(friendlyError(error, "Failed to save device scope."), "warning");
11064
- throw error;
11400
+ return {
11401
+ message: friendlyError(error, "Failed to save device scope. The current sharing rules are still active."),
11402
+ tone: "warning"
11403
+ };
11065
11404
  }
11066
11405
  },
11067
11406
  onResetScope: async (peerId) => {
11068
11407
  try {
11069
11408
  await updatePeerScope(peerId, null, null, true);
11070
11409
  clearPeerScopeReview(peerId);
11071
- showGlobalNotice("Device sync scope reset to global defaults.");
11072
11410
  await _loadSyncData();
11411
+ return {
11412
+ message: "Device sync scope reset to global defaults.",
11413
+ tone: "success"
11414
+ };
11073
11415
  } catch (error) {
11074
- showGlobalNotice(friendlyError(error, "Failed to reset device scope."), "warning");
11075
- throw error;
11416
+ return {
11417
+ message: friendlyError(error, "Failed to reset device scope. The current sharing rules are still active."),
11418
+ tone: "warning"
11419
+ };
11076
11420
  }
11077
11421
  }
11078
11422
  });
11079
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
+ }
11080
11439
  function renderLegacyDeviceClaims() {
11081
11440
  const panel = document.getElementById("syncLegacyClaims");
11082
11441
  const mount = document.getElementById("syncLegacyDeviceSelectMount");
@@ -11157,6 +11516,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11157
11516
  //#region src/tabs/sync/index.ts
11158
11517
  var lastSyncHash = "";
11159
11518
  var cachedSyncStatus = null;
11519
+ var latestSyncLoadRequestId = 0;
11160
11520
  var HEALTH_SYNC_STATUS_CACHE_TTL_MS = 15e3;
11161
11521
  function syncStatusCacheKey(project) {
11162
11522
  return `project:${project || ""}|includeJoinRequests:false`;
@@ -11182,21 +11542,35 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11182
11542
  join_requests: []
11183
11543
  };
11184
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
+ }
11185
11557
  async function loadSyncData() {
11558
+ const requestId = ++latestSyncLoadRequestId;
11186
11559
  try {
11187
11560
  const project = state.currentProject || "";
11188
11561
  const includeJoinRequests = state.activeTab === "sync";
11189
11562
  const useCache = state.activeTab === "health";
11563
+ let fetchedFreshSyncStatus = false;
11190
11564
  let payload;
11191
11565
  if (useCache) {
11192
11566
  payload = readCachedSyncStatus(project);
11193
11567
  if (!payload) {
11194
11568
  payload = await loadSyncStatus(true, project, { includeJoinRequests: false });
11195
- writeCachedSyncStatus(project, normalizeSyncStatusForCache(payload));
11569
+ fetchedFreshSyncStatus = true;
11196
11570
  }
11197
11571
  } else {
11198
11572
  payload = await loadSyncStatus(true, project, { includeJoinRequests });
11199
- writeCachedSyncStatus(project, normalizeSyncStatusForCache(payload));
11573
+ fetchedFreshSyncStatus = true;
11200
11574
  }
11201
11575
  let actorsPayload = null;
11202
11576
  let actorLoadError = false;
@@ -11206,6 +11580,8 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11206
11580
  } catch {
11207
11581
  actorLoadError = true;
11208
11582
  }
11583
+ if (requestId !== latestSyncLoadRequestId) return;
11584
+ if (fetchedFreshSyncStatus) writeCachedSyncStatus(project, normalizeSyncStatusForCache(payload));
11209
11585
  const hash = JSON.stringify([
11210
11586
  payload,
11211
11587
  actorsPayload,
@@ -11216,7 +11592,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11216
11592
  const statusPayload = payload.status && typeof payload.status === "object" ? payload.status : null;
11217
11593
  if (statusPayload) state.lastSyncStatus = statusPayload;
11218
11594
  if (Array.isArray(actorsPayload?.items)) state.lastSyncActors = actorsPayload.items;
11219
- else if (!actorLoadError) state.lastSyncActors = [];
11595
+ else state.lastSyncActors = [];
11220
11596
  const payloadPeers = Array.isArray(payload.peers) ? payload.peers : [];
11221
11597
  const realPeerIds = new Set(payloadPeers.map((peer) => String(peer?.peer_device_id || "").trim()).filter(Boolean));
11222
11598
  const pendingPeers = Array.isArray(state.pendingAcceptedSyncPeers) ? state.pendingAcceptedSyncPeers.filter((peer) => {
@@ -11245,17 +11621,17 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11245
11621
  renderLegacyDeviceClaims();
11246
11622
  renderSyncAttempts();
11247
11623
  renderHealthOverview();
11248
- if (actorLoadError) {
11249
- const actorMeta = document.getElementById("syncActorsMeta");
11250
- if (actorMeta) actorMeta.textContent = "People controls are temporarily unavailable. Device status and sync health still loaded.";
11251
- }
11624
+ if (actorLoadError) renderSyncActorsUnavailable();
11252
11625
  } catch {
11626
+ if (requestId !== latestSyncLoadRequestId) return;
11627
+ lastSyncHash = "";
11253
11628
  hideSkeleton("syncTeamSkeleton");
11254
11629
  hideSkeleton("syncActorsSkeleton");
11255
11630
  hideSkeleton("syncPeersSkeleton");
11256
11631
  hideSkeleton("syncDiagSkeleton");
11257
- const syncMeta = document.getElementById("syncMeta");
11258
- if (syncMeta) syncMeta.textContent = "Sync unavailable";
11632
+ hideStaleSyncSecondarySections();
11633
+ renderSyncPeopleUnavailable();
11634
+ renderSyncDiagnosticsUnavailable();
11259
11635
  }
11260
11636
  }
11261
11637
  async function loadPairingData() {
@@ -11263,6 +11639,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11263
11639
  state.pairingPayloadRaw = await loadPairing() || null;
11264
11640
  renderPairing();
11265
11641
  } catch {
11642
+ state.pairingPayloadRaw = null;
11266
11643
  renderPairing();
11267
11644
  }
11268
11645
  }
@@ -11285,6 +11662,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11285
11662
  var settingsEnvOverrides = {};
11286
11663
  var settingsTouchedKeys = /* @__PURE__ */ new Set();
11287
11664
  var settingsShellMounted = false;
11665
+ var settingsProtectedKeys = /* @__PURE__ */ new Set();
11288
11666
  var settingsStartPolling = null;
11289
11667
  var settingsRefresh = null;
11290
11668
  var SETTINGS_ADVANCED_KEY = "codemem-settings-advanced";
@@ -11317,6 +11695,14 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11317
11695
  syncCoordinatorTimeout: "sync_coordinator_timeout_s",
11318
11696
  syncCoordinatorPresenceTtl: "sync_coordinator_presence_ttl_s"
11319
11697
  };
11698
+ var PROTECTED_VIEWER_CONFIG_KEYS = new Set([
11699
+ "claude_command",
11700
+ "observer_base_url",
11701
+ "observer_auth_file",
11702
+ "observer_auth_command",
11703
+ "observer_headers",
11704
+ "sync_coordinator_url"
11705
+ ]);
11320
11706
  var settingsRenderState = {
11321
11707
  effectiveText: "",
11322
11708
  isSaving: false,
@@ -11802,6 +12188,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11802
12188
  type: hasWarning ? "warning" : "success"
11803
12189
  };
11804
12190
  }
12191
+ function isProtectedConfigKey(key) {
12192
+ return settingsProtectedKeys.has(key) || PROTECTED_VIEWER_CONFIG_KEYS.has(key);
12193
+ }
12194
+ function protectedConfigHelp(key) {
12195
+ return `${key} is read-only in the viewer for security. Edit the config file or environment instead.`;
12196
+ }
11805
12197
  function formStateFromPayload(payload) {
11806
12198
  const config = payload.config || {};
11807
12199
  const effective = payload.effective || {};
@@ -11842,8 +12234,10 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11842
12234
  const defaults = payload.defaults || {};
11843
12235
  const config = payload.config || {};
11844
12236
  const envOverrides = payload.env_overrides && typeof payload.env_overrides === "object" ? payload.env_overrides : {};
12237
+ const protectedKeys = Array.isArray(payload.protected_keys) ? payload.protected_keys.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
11845
12238
  const values = formStateFromPayload(payload);
11846
12239
  settingsEnvOverrides = envOverrides;
12240
+ settingsProtectedKeys = new Set(protectedKeys);
11847
12241
  state.configDefaults = defaults;
11848
12242
  state.configPath = payload.path || "";
11849
12243
  updateRenderState({
@@ -11996,6 +12390,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
11996
12390
  const current = collectSettingsPayload({ allowUntouchedParseErrors: true });
11997
12391
  const changed = {};
11998
12392
  Object.entries(current).forEach(([key, value]) => {
12393
+ if (isProtectedConfigKey(key)) return;
11999
12394
  if (hasOwn(settingsEnvOverrides, key) && !settingsTouchedKeys.has(key)) return;
12000
12395
  if (!isEqualValue(value, settingsBaseline[key])) changed[key] = value;
12001
12396
  });
@@ -12398,15 +12793,15 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12398
12793
  children: "Claude command (JSON argv)"
12399
12794
  }),
12400
12795
  /* @__PURE__ */ u("textarea", {
12796
+ disabled: true,
12401
12797
  id: "claudeCommand",
12402
- onInput: onTextInput("claudeCommand"),
12403
12798
  placeholder: "[\"claude\"]",
12404
12799
  rows: 2,
12405
12800
  value: values.claudeCommand
12406
12801
  }),
12407
12802
  /* @__PURE__ */ u("div", {
12408
12803
  className: "small",
12409
- children: "Used by `claude_sidecar` runtime. Example wrapper: `[\"wrapper\", \"claude\", \"--\"]`."
12804
+ children: protectedConfigHelp("claude_command")
12410
12805
  })
12411
12806
  ]
12412
12807
  }),
@@ -12496,14 +12891,14 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12496
12891
  children: "Token file path"
12497
12892
  }),
12498
12893
  /* @__PURE__ */ u("input", {
12894
+ disabled: true,
12499
12895
  id: "observerAuthFile",
12500
- onInput: onTextInput("observerAuthFile"),
12501
12896
  placeholder: "~/.codemem/work-token.txt",
12502
12897
  value: values.observerAuthFile
12503
12898
  }),
12504
12899
  /* @__PURE__ */ u("div", {
12505
12900
  className: "small",
12506
- children: "Reads token text from this file when method is `file`."
12901
+ children: protectedConfigHelp("observer_auth_file")
12507
12902
  })
12508
12903
  ]
12509
12904
  }),
@@ -12525,15 +12920,15 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12525
12920
  })]
12526
12921
  }),
12527
12922
  /* @__PURE__ */ u("textarea", {
12923
+ disabled: true,
12528
12924
  id: "observerAuthCommand",
12529
- onInput: onTextInput("observerAuthCommand"),
12530
12925
  placeholder: "[\"iap-auth\", \"--audience\", \"gateway\"]",
12531
12926
  rows: 3,
12532
12927
  value: values.observerAuthCommand
12533
12928
  }),
12534
12929
  /* @__PURE__ */ u("div", {
12535
12930
  className: "small",
12536
- children: "When method is `command`, command stdout is used as the token."
12931
+ children: protectedConfigHelp("observer_auth_command")
12537
12932
  })
12538
12933
  ]
12539
12934
  }),
@@ -12589,15 +12984,15 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12589
12984
  })]
12590
12985
  }),
12591
12986
  /* @__PURE__ */ u("textarea", {
12987
+ disabled: true,
12592
12988
  id: "observerHeaders",
12593
- onInput: onTextInput("observerHeaders"),
12594
12989
  placeholder: "{\"Authorization\":\"Bearer ${auth.token}\"}",
12595
12990
  rows: 4,
12596
12991
  value: values.observerHeaders
12597
12992
  }),
12598
12993
  /* @__PURE__ */ u("div", {
12599
12994
  className: "small",
12600
- children: ["Template variables: ", "`${auth.token}`, `${auth.type}`, `${auth.source}`."]
12995
+ children: protectedConfigHelp("observer_headers")
12601
12996
  })
12602
12997
  ]
12603
12998
  })
@@ -12781,14 +13176,14 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12781
13176
  children: "Coordinator URL"
12782
13177
  }),
12783
13178
  /* @__PURE__ */ u("input", {
13179
+ disabled: true,
12784
13180
  id: "syncCoordinatorUrl",
12785
- onInput: onTextInput("syncCoordinatorUrl"),
12786
13181
  placeholder: "https://coord.example.com",
12787
13182
  value: values.syncCoordinatorUrl
12788
13183
  }),
12789
13184
  /* @__PURE__ */ u("div", {
12790
13185
  className: "small",
12791
- children: "Optional self-hosted/operator-run discovery service. Direct peer-to-peer sync remains the data path."
13186
+ children: protectedConfigHelp("sync_coordinator_url")
12792
13187
  })
12793
13188
  ]
12794
13189
  }),
@@ -12888,7 +13283,71 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12888
13283
  }
12889
13284
  //#endregion
12890
13285
  //#region src/app.ts
13286
+ function setRuntimeLabel(version, commit) {
13287
+ const el = $$2("runtimeLabel");
13288
+ if (!el) return;
13289
+ el.textContent = commit ? `v${version} (${commit})` : `v${version}`;
13290
+ el.title = commit ? `codemem ${version} (${commit})` : `codemem ${version}`;
13291
+ el.hidden = false;
13292
+ }
13293
+ async function loadRuntimeLabel() {
13294
+ try {
13295
+ const runtime = await loadRuntimeInfo();
13296
+ if (!runtime?.version) return;
13297
+ setRuntimeLabel(runtime.version, "692076f");
13298
+ } catch {}
13299
+ }
12891
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
+ }
12892
13351
  function setRefreshStatus(rs, detail) {
12893
13352
  state.refreshState = rs;
12894
13353
  const el = $$2("refreshStatus");
@@ -12908,6 +13367,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12908
13367
  announce("Auto refresh paused.");
12909
13368
  return;
12910
13369
  }
13370
+ if (rs === "error" && detail === "(reconnecting)") {
13371
+ el.textContent = "reconnecting…";
13372
+ announce("Viewer reconnecting.");
13373
+ return;
13374
+ }
12911
13375
  if (rs === "error") {
12912
13376
  el.textContent = "refresh failed";
12913
13377
  announce("Refresh failed.");
@@ -12931,7 +13395,7 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12931
13395
  if (document.visibilityState === "hidden") {
12932
13396
  stopPolling();
12933
13397
  setRefreshStatus("paused", "(tab hidden)");
12934
- } else if (!isSettingsOpen()) {
13398
+ } else if (!isSettingsOpen() && !reconnecting) {
12935
13399
  startPolling();
12936
13400
  refresh();
12937
13401
  }
@@ -12987,10 +13451,12 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
12987
13451
  });
12988
13452
  var refreshDebounceTimer = null;
12989
13453
  async function refresh() {
13454
+ if (reconnecting) return;
12990
13455
  if (refreshDebounceTimer) clearTimeout(refreshDebounceTimer);
12991
13456
  refreshDebounceTimer = setTimeout(() => doRefresh(), 80);
12992
13457
  }
12993
13458
  async function doRefresh() {
13459
+ if (reconnecting) return;
12994
13460
  if (state.refreshInFlight) {
12995
13461
  state.refreshQueued = true;
12996
13462
  return;
@@ -13005,10 +13471,11 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
13005
13471
  await Promise.all(promises);
13006
13472
  setRefreshStatus("idle");
13007
13473
  } catch {
13008
- setRefreshStatus("error");
13474
+ if (!await isViewerReady()) scheduleReconnectLoop();
13475
+ else setRefreshStatus("error");
13009
13476
  } finally {
13010
13477
  state.refreshInFlight = false;
13011
- if (state.refreshQueued) {
13478
+ if (state.refreshQueued && !reconnecting) {
13012
13479
  state.refreshQueued = false;
13013
13480
  doRefresh();
13014
13481
  }
@@ -13022,6 +13489,17 @@ For more information, see https://radix-ui.com/primitives/docs/components/${titl
13022
13489
  initSyncTab(() => refresh());
13023
13490
  initSettings(stopPolling, startPolling, () => refresh());
13024
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
+ });
13502
+ loadRuntimeLabel();
13025
13503
  refresh();
13026
13504
  startPolling();
13027
13505
  //#endregion