@codemem/server 0.21.2 → 0.22.0-alpha.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/dist/index.js +171 -45
- package/dist/index.js.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +223 -5
- package/static/index.html +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;AAyBhE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,KAAK,YAAY,GAAG,MAAM,WAAW,CAAC;AACtC,KAAK,iBAAiB,GAAG;IACxB,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;IACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AA4fF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,8EAwMxD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACzB,QAAQ,EAAE,YAAY,EACtB,oBAAoB,CAAC,EAAE,MAAM,iBAAiB,GAAG,IAAI,8EAkiBrD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemem/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0-alpha.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@hono/node-server": "^1.14.3",
|
|
19
19
|
"drizzle-orm": "^0.45.1",
|
|
20
20
|
"hono": "^4.7.10",
|
|
21
|
-
"@codemem/core": "^0.
|
|
21
|
+
"@codemem/core": "^0.22.0-alpha.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/better-sqlite3": "^7.6.13",
|
package/static/app.js
CHANGED
|
@@ -366,6 +366,32 @@
|
|
|
366
366
|
if (!resp.ok) throw new Error(payload?.error || text || "request failed");
|
|
367
367
|
return payload;
|
|
368
368
|
}
|
|
369
|
+
async function deletePeer(peerDeviceId) {
|
|
370
|
+
const resp = await fetch(`/api/sync/peers/${encodeURIComponent(peerDeviceId)}`, { method: "DELETE" });
|
|
371
|
+
const text = await resp.text();
|
|
372
|
+
const payload = text ? JSON.parse(text) : {};
|
|
373
|
+
if (!resp.ok) throw new Error(payload?.error || text || "request failed");
|
|
374
|
+
return payload;
|
|
375
|
+
}
|
|
376
|
+
async function acceptDiscoveredPeer(peerDeviceId, fingerprint) {
|
|
377
|
+
const resp = await fetch("/api/sync/peers/accept-discovered", {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { "Content-Type": "application/json" },
|
|
380
|
+
body: JSON.stringify({
|
|
381
|
+
peer_device_id: peerDeviceId,
|
|
382
|
+
fingerprint
|
|
383
|
+
})
|
|
384
|
+
});
|
|
385
|
+
const text = await resp.text();
|
|
386
|
+
let payload = {};
|
|
387
|
+
try {
|
|
388
|
+
payload = text ? JSON.parse(text) : {};
|
|
389
|
+
} catch {
|
|
390
|
+
payload = {};
|
|
391
|
+
}
|
|
392
|
+
if (!resp.ok) throw new Error(payload?.detail || payload?.error || text || "request failed");
|
|
393
|
+
return payload;
|
|
394
|
+
}
|
|
369
395
|
async function createActor(displayName) {
|
|
370
396
|
const resp = await fetch("/api/sync/actors", {
|
|
371
397
|
method: "POST",
|
|
@@ -421,11 +447,19 @@
|
|
|
421
447
|
}
|
|
422
448
|
async function triggerSync(address) {
|
|
423
449
|
const payload = address ? { address } : {};
|
|
424
|
-
await fetch("/api/sync/run", {
|
|
450
|
+
const resp = await fetch("/api/sync/run", {
|
|
425
451
|
method: "POST",
|
|
426
452
|
headers: { "Content-Type": "application/json" },
|
|
427
453
|
body: JSON.stringify(payload)
|
|
428
454
|
});
|
|
455
|
+
const text = await resp.text();
|
|
456
|
+
let body = {};
|
|
457
|
+
try {
|
|
458
|
+
body = text ? JSON.parse(text) : {};
|
|
459
|
+
} catch {
|
|
460
|
+
body = {};
|
|
461
|
+
}
|
|
462
|
+
if (!resp.ok) throw new Error(body?.error || text || "request failed");
|
|
429
463
|
}
|
|
430
464
|
//#endregion
|
|
431
465
|
//#region src/lib/format.ts
|
|
@@ -1774,6 +1808,30 @@
|
|
|
1774
1808
|
teamInvitePanelOpen = v;
|
|
1775
1809
|
}
|
|
1776
1810
|
var openPeerScopeEditors = /* @__PURE__ */ new Set();
|
|
1811
|
+
var pendingPeerScopeReviewIds = /* @__PURE__ */ new Set();
|
|
1812
|
+
var freshPeerScopeReviewIds = /* @__PURE__ */ new Set();
|
|
1813
|
+
function requestPeerScopeReview(peerDeviceId) {
|
|
1814
|
+
const value = String(peerDeviceId || "").trim();
|
|
1815
|
+
if (!value) return;
|
|
1816
|
+
pendingPeerScopeReviewIds.add(value);
|
|
1817
|
+
freshPeerScopeReviewIds.add(value);
|
|
1818
|
+
openPeerScopeEditors.add(value);
|
|
1819
|
+
}
|
|
1820
|
+
function isPeerScopeReviewPending(peerDeviceId) {
|
|
1821
|
+
const value = String(peerDeviceId || "").trim();
|
|
1822
|
+
return Boolean(value) && pendingPeerScopeReviewIds.has(value);
|
|
1823
|
+
}
|
|
1824
|
+
function clearPeerScopeReview(peerDeviceId) {
|
|
1825
|
+
const value = String(peerDeviceId || "").trim();
|
|
1826
|
+
if (!value) return;
|
|
1827
|
+
pendingPeerScopeReviewIds.delete(value);
|
|
1828
|
+
}
|
|
1829
|
+
function consumePeerScopeReviewRequest(peerDeviceId) {
|
|
1830
|
+
const value = String(peerDeviceId || "").trim();
|
|
1831
|
+
if (!value || !freshPeerScopeReviewIds.has(value)) return false;
|
|
1832
|
+
freshPeerScopeReviewIds.delete(value);
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1777
1835
|
/** Redact the last two octets of IPv4 addresses. */
|
|
1778
1836
|
function redactIpOctets(text) {
|
|
1779
1837
|
return text.replace(/\b(\d{1,3}\.\d{1,3})\.\d{1,3}\.\d{1,3}\b/g, "$1.#.#");
|
|
@@ -2254,6 +2312,9 @@
|
|
|
2254
2312
|
const toggleAdmin = document.getElementById("syncToggleAdmin");
|
|
2255
2313
|
const joinPanel = document.getElementById("syncJoinPanel");
|
|
2256
2314
|
const joinRequests = document.getElementById("syncJoinRequests");
|
|
2315
|
+
const discoveredPanel = document.getElementById("syncCoordinatorDiscovered");
|
|
2316
|
+
const discoveredMeta = document.getElementById("syncCoordinatorDiscoveredMeta");
|
|
2317
|
+
const discoveredList = document.getElementById("syncCoordinatorDiscoveredList");
|
|
2257
2318
|
if (!meta || !setupPanel || !list || !actions) return;
|
|
2258
2319
|
hideSkeleton("syncTeamSkeleton");
|
|
2259
2320
|
ensureInvitePanelInAdminSection();
|
|
@@ -2261,6 +2322,7 @@
|
|
|
2261
2322
|
list.textContent = "";
|
|
2262
2323
|
actions.textContent = "";
|
|
2263
2324
|
if (joinRequests) joinRequests.textContent = "";
|
|
2325
|
+
if (discoveredList) discoveredList.textContent = "";
|
|
2264
2326
|
setInviteOutputVisibility();
|
|
2265
2327
|
const coordinator = state.lastSyncCoordinator;
|
|
2266
2328
|
const configured = Boolean(coordinator && coordinator.configured);
|
|
@@ -2270,6 +2332,7 @@
|
|
|
2270
2332
|
list.hidden = true;
|
|
2271
2333
|
actions.hidden = true;
|
|
2272
2334
|
if (joinRequests) joinRequests.hidden = true;
|
|
2335
|
+
if (discoveredPanel) discoveredPanel.hidden = true;
|
|
2273
2336
|
if (invitePanel) invitePanel.hidden = !adminSetupExpanded;
|
|
2274
2337
|
if (toggleAdmin) toggleAdmin.textContent = adminSetupExpanded ? "Hide team setup" : "Set up a new team instead…";
|
|
2275
2338
|
return;
|
|
@@ -2278,16 +2341,121 @@
|
|
|
2278
2341
|
list.hidden = false;
|
|
2279
2342
|
actions.hidden = false;
|
|
2280
2343
|
if (joinRequests) joinRequests.hidden = false;
|
|
2344
|
+
if (discoveredPanel) discoveredPanel.hidden = true;
|
|
2281
2345
|
const presenceLabel = coordinator.presence_status === "posted" ? "Connected" : coordinator.presence_status === "not_enrolled" ? "Not connected — import an invite or ask your admin to enroll this device" : "Connection error";
|
|
2282
2346
|
const statusRow = el("div", "sync-team-summary");
|
|
2283
2347
|
const statusLine = el("div", "sync-team-status-row");
|
|
2284
2348
|
const statusLabel = el("span", "sync-team-status-label", "Status");
|
|
2285
2349
|
const statusBadge = el("span", `pill ${coordinator.presence_status === "posted" ? "pill-success" : coordinator.presence_status === "not_enrolled" ? "pill-warning" : "pill-error"}`, presenceLabel);
|
|
2286
|
-
const metricParts = [`Paired devices: ${Number(coordinator.paired_peer_count || 0)}`, `
|
|
2350
|
+
const metricParts = [`Paired devices: ${Number(coordinator.paired_peer_count || 0)}`, `Fresh discovered: ${Number(coordinator.fresh_peer_count || 0)}`];
|
|
2287
2351
|
if (Number(coordinator.stale_peer_count || 0) > 0) metricParts.push(`Inactive: ${Number(coordinator.stale_peer_count || 0)}`);
|
|
2288
2352
|
statusLine.append(statusLabel, statusBadge);
|
|
2289
2353
|
statusRow.append(statusLine, el("div", "sync-team-metrics", metricParts.join(" · ")));
|
|
2290
2354
|
list.appendChild(statusRow);
|
|
2355
|
+
const localPeers = Array.isArray(state.lastSyncPeers) ? state.lastSyncPeers : [];
|
|
2356
|
+
const unhealthyPeers = localPeers.filter((peer) => {
|
|
2357
|
+
const status = peer?.status || {};
|
|
2358
|
+
return Boolean(peer?.has_error) || [
|
|
2359
|
+
"offline",
|
|
2360
|
+
"stale",
|
|
2361
|
+
"degraded"
|
|
2362
|
+
].includes(String(status.peer_state || ""));
|
|
2363
|
+
});
|
|
2364
|
+
if (unhealthyPeers.length) {
|
|
2365
|
+
const row = el("div", "sync-action");
|
|
2366
|
+
const textWrap = el("div", "sync-action-text");
|
|
2367
|
+
textWrap.textContent = `${unhealthyPeers.length} paired device${unhealthyPeers.length === 1 ? "" : "s"} look stale or unhealthy.`;
|
|
2368
|
+
textWrap.appendChild(el("span", "sync-action-command", "Use Remove peer in People or codemem sync peers remove <peer> before re-pairing."));
|
|
2369
|
+
row.appendChild(textWrap);
|
|
2370
|
+
actions.appendChild(row);
|
|
2371
|
+
}
|
|
2372
|
+
const discoveredDevices = Array.isArray(coordinator.discovered_devices) ? coordinator.discovered_devices : [];
|
|
2373
|
+
if (discoveredPanel && discoveredMeta && discoveredList && discoveredDevices.length) {
|
|
2374
|
+
discoveredPanel.hidden = false;
|
|
2375
|
+
discoveredMeta.textContent = "These devices are visible through coordinator discovery only. They are not active sync peers unless you pair them.";
|
|
2376
|
+
discoveredDevices.forEach((device) => {
|
|
2377
|
+
const row = el("div", "actor-row");
|
|
2378
|
+
const details = el("div", "actor-details");
|
|
2379
|
+
const title = el("div", "actor-title");
|
|
2380
|
+
const rowActions = el("div", "actor-actions");
|
|
2381
|
+
const deviceId = String(device.device_id || "").trim();
|
|
2382
|
+
const displayName = String(device.display_name || "").trim() || deviceId || "Discovered device";
|
|
2383
|
+
const fingerprint = String(device.fingerprint || "").trim();
|
|
2384
|
+
const pairedPeer = localPeers.find((peer) => String(peer?.peer_device_id || "") === deviceId);
|
|
2385
|
+
const pairedFingerprint = String(pairedPeer?.fingerprint || "").trim();
|
|
2386
|
+
const hasConflict = Boolean(pairedPeer) && Boolean(fingerprint) && Boolean(pairedFingerprint) && pairedFingerprint !== fingerprint;
|
|
2387
|
+
const canAccept = Boolean(deviceId) && Boolean(fingerprint) && !pairedPeer && !device.stale;
|
|
2388
|
+
title.append(el("strong", null, displayName));
|
|
2389
|
+
title.appendChild(el("span", `badge actor-badge${device.stale ? "" : " local"}`, device.stale ? "Inactive" : "Fresh"));
|
|
2390
|
+
title.appendChild(el("span", "badge actor-badge", hasConflict ? "Conflicts with local peer" : pairedPeer ? "Paired locally" : "Not paired locally"));
|
|
2391
|
+
const addresses = Array.isArray(device.addresses) ? device.addresses : [];
|
|
2392
|
+
const noteParts = [deviceId, addresses.length ? addresses.map((address) => isSyncRedactionEnabled() ? redactAddress(String(address || "")) : String(address || "")).filter(Boolean).join(" · ") : "No fresh addresses"];
|
|
2393
|
+
if (hasConflict) noteParts.push("repair the local peer before accepting this discovered device");
|
|
2394
|
+
else if (pairedPeer && isPeerScopeReviewPending(deviceId)) noteParts.push("scope review pending in People");
|
|
2395
|
+
else if (pairedPeer?.last_error) noteParts.push(`paired error: ${String(pairedPeer.last_error)}`);
|
|
2396
|
+
else if (pairedPeer?.status?.peer_state) noteParts.push(`paired status: ${String(pairedPeer.status.peer_state)}`);
|
|
2397
|
+
details.append(title, el("div", "peer-meta", noteParts.join(" · ")));
|
|
2398
|
+
if (canAccept) {
|
|
2399
|
+
const acceptBtn = el("button", "settings-button", "Accept peer");
|
|
2400
|
+
acceptBtn.addEventListener("click", async () => {
|
|
2401
|
+
acceptBtn.disabled = true;
|
|
2402
|
+
acceptBtn.textContent = "Accepting…";
|
|
2403
|
+
try {
|
|
2404
|
+
await acceptDiscoveredPeer(deviceId, fingerprint);
|
|
2405
|
+
requestPeerScopeReview(deviceId);
|
|
2406
|
+
showGlobalNotice(`Accepted ${displayName}. Its scope editor is now open in People so you can review sync scope next.`);
|
|
2407
|
+
await _loadSyncData$1();
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
showGlobalNotice(friendlyError(error, "Failed to accept discovered peer."), "warning");
|
|
2410
|
+
acceptBtn.textContent = "Retry accept";
|
|
2411
|
+
} finally {
|
|
2412
|
+
acceptBtn.disabled = false;
|
|
2413
|
+
if (acceptBtn.textContent === "Accepting…") acceptBtn.textContent = "Accept peer";
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
rowActions.appendChild(acceptBtn);
|
|
2417
|
+
} else if (!pairedPeer && device.stale) rowActions.appendChild(el("div", "peer-meta", "Wait for a fresh coordinator presence update."));
|
|
2418
|
+
else if (hasConflict) {
|
|
2419
|
+
const inspectBtn = el("button", "settings-button", "Jump to local peer");
|
|
2420
|
+
inspectBtn.addEventListener("click", () => {
|
|
2421
|
+
const peerCard = document.querySelector(`[data-peer-device-id="${CSS.escape(deviceId)}"]`);
|
|
2422
|
+
if (peerCard instanceof HTMLElement) {
|
|
2423
|
+
peerCard.scrollIntoView({
|
|
2424
|
+
block: "center",
|
|
2425
|
+
behavior: "smooth"
|
|
2426
|
+
});
|
|
2427
|
+
showGlobalNotice(`Jumped to the conflicting local peer for ${displayName}.`, "warning");
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
showGlobalNotice("Conflicting local peer is not visible yet. Scroll to the People area and try again.", "warning");
|
|
2431
|
+
});
|
|
2432
|
+
const removeConflictBtn = el("button", "settings-button", "Remove conflicting peer");
|
|
2433
|
+
removeConflictBtn.addEventListener("click", async () => {
|
|
2434
|
+
if (!pairedPeer) return;
|
|
2435
|
+
if (!window.confirm(`Remove the conflicting local peer for ${displayName}? You can accept the discovered device again after this refreshes.`)) return;
|
|
2436
|
+
removeConflictBtn.disabled = true;
|
|
2437
|
+
inspectBtn.disabled = true;
|
|
2438
|
+
removeConflictBtn.textContent = "Removing…";
|
|
2439
|
+
try {
|
|
2440
|
+
await deletePeer(deviceId);
|
|
2441
|
+
showGlobalNotice(`Removed the conflicting local peer for ${displayName}. If it is still fresh, you can now accept it from Team sync.`);
|
|
2442
|
+
await _loadSyncData$1();
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
showGlobalNotice(friendlyError(error, "Failed to remove the conflicting local peer."), "warning");
|
|
2445
|
+
removeConflictBtn.textContent = "Retry remove";
|
|
2446
|
+
} finally {
|
|
2447
|
+
removeConflictBtn.disabled = false;
|
|
2448
|
+
inspectBtn.disabled = false;
|
|
2449
|
+
if (removeConflictBtn.textContent === "Removing…") removeConflictBtn.textContent = "Remove conflicting peer";
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
rowActions.append(inspectBtn, removeConflictBtn);
|
|
2453
|
+
} else if (pairedPeer && isPeerScopeReviewPending(deviceId)) rowActions.appendChild(el("div", "peer-meta", "Review this peer's scope in People next."));
|
|
2454
|
+
else if (pairedPeer) rowActions.appendChild(el("div", "peer-meta", "Manage this peer in People."));
|
|
2455
|
+
row.append(details, rowActions);
|
|
2456
|
+
discoveredList.appendChild(row);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2291
2459
|
const inviteToggleRow = el("div", "sync-action");
|
|
2292
2460
|
const inviteToggleText = el("div", "sync-action-text");
|
|
2293
2461
|
inviteToggleText.textContent = "Generate an invite to add another teammate to this team.";
|
|
@@ -2421,6 +2589,7 @@
|
|
|
2421
2589
|
syncInviteOutput.focus();
|
|
2422
2590
|
syncInviteOutput.select();
|
|
2423
2591
|
showGlobalNotice("Invite created. Copy the text above and share it with your teammate.");
|
|
2592
|
+
(Array.isArray(result.warnings) ? result.warnings : []).forEach((warning) => showGlobalNotice(String(warning), "warning"));
|
|
2424
2593
|
} catch (error) {
|
|
2425
2594
|
showGlobalNotice(friendlyError(error, "Failed to create invite."), "warning");
|
|
2426
2595
|
} finally {
|
|
@@ -2587,27 +2756,66 @@
|
|
|
2587
2756
|
const card = el("div", "peer-card");
|
|
2588
2757
|
const titleRow = el("div", "peer-title");
|
|
2589
2758
|
const peerId = peer.peer_device_id ? String(peer.peer_device_id) : "";
|
|
2759
|
+
if (peerId) card.dataset.peerDeviceId = peerId;
|
|
2590
2760
|
const displayName = peer.name || (peerId ? peerId.slice(0, 8) : "unknown");
|
|
2761
|
+
const destructiveLabel = peer.name || peerId || displayName;
|
|
2762
|
+
const pendingScopeReview = isPeerScopeReviewPending(peerId);
|
|
2591
2763
|
const name = el("strong", null, displayName);
|
|
2592
2764
|
if (peerId) name.title = peerId;
|
|
2593
2765
|
const peerStatus = peer.status || {};
|
|
2594
2766
|
const online = peerStatus.sync_status === "ok" || peerStatus.ping_status === "ok";
|
|
2595
2767
|
const badge = el("span", `badge ${online ? "badge-online" : "badge-offline"}`, online ? "Online" : "Offline");
|
|
2596
2768
|
name.append(" ", badge);
|
|
2769
|
+
if (pendingScopeReview) name.append(" ", el("span", "badge actor-badge", "Needs scope review"));
|
|
2597
2770
|
const actions = el("div", "peer-actions");
|
|
2598
2771
|
const primaryAddress = pickPrimaryAddress(peer.addresses);
|
|
2599
2772
|
const syncBtn = el("button", null, "Sync now");
|
|
2600
2773
|
syncBtn.disabled = !primaryAddress;
|
|
2601
2774
|
syncBtn.addEventListener("click", async () => {
|
|
2775
|
+
if (pendingScopeReview) {
|
|
2776
|
+
if (!window.confirm(`Sync scope review is still pending for ${displayName}. Continue with a manual sync anyway?`)) return;
|
|
2777
|
+
}
|
|
2602
2778
|
syncBtn.disabled = true;
|
|
2603
2779
|
syncBtn.textContent = "Syncing…";
|
|
2604
2780
|
try {
|
|
2605
2781
|
await triggerSync(primaryAddress);
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2782
|
+
if (pendingScopeReview) showGlobalNotice(`Triggered sync for ${displayName} before scope review was finished. Review scope in this card if you want tighter sharing rules.`, "warning");
|
|
2783
|
+
else showGlobalNotice(`Started sync with ${displayName}.`);
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
showGlobalNotice(friendlyError(error, "Failed to trigger sync."), "warning");
|
|
2786
|
+
syncBtn.disabled = false;
|
|
2787
|
+
syncBtn.textContent = "Sync now";
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
try {
|
|
2791
|
+
await _loadSyncData();
|
|
2792
|
+
} catch {
|
|
2793
|
+
showGlobalNotice("Sync started, but the local status view did not refresh yet.", "warning");
|
|
2794
|
+
} finally {
|
|
2795
|
+
syncBtn.disabled = false;
|
|
2796
|
+
syncBtn.textContent = "Sync now";
|
|
2797
|
+
}
|
|
2609
2798
|
});
|
|
2610
2799
|
actions.appendChild(syncBtn);
|
|
2800
|
+
const removeBtn = el("button", null, "Remove peer");
|
|
2801
|
+
removeBtn.addEventListener("click", async () => {
|
|
2802
|
+
if (!peerId) return;
|
|
2803
|
+
if (!window.confirm(`Remove peer ${destructiveLabel}? This deletes the local sync peer entry.`)) return;
|
|
2804
|
+
removeBtn.disabled = true;
|
|
2805
|
+
removeBtn.textContent = "Removing…";
|
|
2806
|
+
try {
|
|
2807
|
+
await deletePeer(peerId);
|
|
2808
|
+
showGlobalNotice(`Removed peer ${destructiveLabel}.`);
|
|
2809
|
+
await _loadSyncData();
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
showGlobalNotice(friendlyError(error, "Failed to remove peer."), "warning");
|
|
2812
|
+
removeBtn.textContent = "Retry remove";
|
|
2813
|
+
} finally {
|
|
2814
|
+
removeBtn.disabled = false;
|
|
2815
|
+
if (removeBtn.textContent === "Removing…") removeBtn.textContent = "Remove peer";
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
actions.appendChild(removeBtn);
|
|
2611
2819
|
const toggleScopeBtn = el("button", null, "Edit scope");
|
|
2612
2820
|
actions.appendChild(toggleScopeBtn);
|
|
2613
2821
|
const peerAddresses = Array.isArray(peer.addresses) ? Array.from(new Set(peer.addresses.filter(Boolean))) : [];
|
|
@@ -2658,6 +2866,7 @@
|
|
|
2658
2866
|
const includeEditor = createChipEditor(includeList, "Add included project", "All projects");
|
|
2659
2867
|
const excludeEditor = createChipEditor(excludeList, "Add excluded project", "No exclusions");
|
|
2660
2868
|
const scopeEditorOpen = openPeerScopeEditors.has(peerId);
|
|
2869
|
+
const scopeReviewRequested = consumePeerScopeReviewRequest(peerId);
|
|
2661
2870
|
const editorWrap = el("div", `peer-scope-editor-wrap${scopeEditorOpen ? "" : " collapsed"}`);
|
|
2662
2871
|
if (!scopeEditorOpen) editorWrap.inert = true;
|
|
2663
2872
|
const inputRow = el("div", "peer-scope-row");
|
|
@@ -2670,6 +2879,7 @@
|
|
|
2670
2879
|
saveScopeBtn.textContent = "Saving…";
|
|
2671
2880
|
try {
|
|
2672
2881
|
await updatePeerScope(peerId, includeEditor.values(), excludeEditor.values());
|
|
2882
|
+
clearPeerScopeReview(peerId);
|
|
2673
2883
|
showGlobalNotice("Device sync scope saved.");
|
|
2674
2884
|
await _loadSyncData();
|
|
2675
2885
|
} catch (error) {
|
|
@@ -2685,6 +2895,7 @@
|
|
|
2685
2895
|
inheritBtn.textContent = "Resetting…";
|
|
2686
2896
|
try {
|
|
2687
2897
|
await updatePeerScope(peerId, null, null, true);
|
|
2898
|
+
clearPeerScopeReview(peerId);
|
|
2688
2899
|
showGlobalNotice("Device sync scope reset to global defaults.");
|
|
2689
2900
|
await _loadSyncData();
|
|
2690
2901
|
} catch (error) {
|
|
@@ -2708,6 +2919,13 @@
|
|
|
2708
2919
|
toggleScopeBtn.setAttribute("aria-expanded", String(isCollapsed));
|
|
2709
2920
|
toggleScopeBtn.textContent = isCollapsed ? "Hide scope editor" : "Edit scope";
|
|
2710
2921
|
});
|
|
2922
|
+
if (scopeReviewRequested) {
|
|
2923
|
+
scopePanel.prepend(el("div", "peer-meta", "Review this device's sync scope now. Global defaults apply until you save an override here."));
|
|
2924
|
+
queueMicrotask(() => card.scrollIntoView({
|
|
2925
|
+
block: "center",
|
|
2926
|
+
behavior: "smooth"
|
|
2927
|
+
}));
|
|
2928
|
+
} else if (pendingScopeReview) scopePanel.prepend(el("div", "peer-meta", "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."));
|
|
2711
2929
|
scopePanel.append(identityRow, identityMeta, actorRow, actorHint, scopeSummary, effectiveSummary, editorWrap);
|
|
2712
2930
|
titleRow.append(name, actions);
|
|
2713
2931
|
card.append(titleRow, addressLabel, meta, scopePanel);
|
package/static/index.html
CHANGED
|
@@ -1774,6 +1774,11 @@
|
|
|
1774
1774
|
</div>
|
|
1775
1775
|
<div class="actor-list" id="syncTeamStatus" aria-live="polite"></div>
|
|
1776
1776
|
<div class="actor-list" id="syncJoinRequests"></div>
|
|
1777
|
+
<div id="syncCoordinatorDiscovered" hidden>
|
|
1778
|
+
<h3 class="settings-group-title" style="margin-top: 20px">Coordinator-discovered devices</h3>
|
|
1779
|
+
<div class="section-meta" id="syncCoordinatorDiscoveredMeta"></div>
|
|
1780
|
+
<div class="actor-list" id="syncCoordinatorDiscoveredList"></div>
|
|
1781
|
+
</div>
|
|
1777
1782
|
<div class="sync-actions" id="syncTeamActions" hidden></div>
|
|
1778
1783
|
</div>
|
|
1779
1784
|
|