Mesh P5: finish transfer feature set — moved/arrived visibility, recovery, bulk hand-off
Completes Phase 5 of the multi-device mesh / session-transfer program (docs/MESH_TRANSFER.md) except the two-Mac memory-carry spike. All additive + capability-gated; SyncProtocol stays v1. - Moved-session visibility: additive SessionSummary.movedTo (MovedDestination), decode-defaulted. A moved session no longer silently vanishes — the source keeps a read-only "Moved to <Mac>" tombstone under Archived (name resolved live from paired Macs), surfaced on relaunch without rebuilding a runnable controller, and sent on the wire so phones see it too. - Relaunch recovery driven from launch (+ on every peer reconnect, single-flight): AppStore.recoverInterruptedTransfers clears abandoned pre-tombstone locks, discards orphaned inbound staging, and re-drives a tombstoned commit via SessionTransferCoordinator.recoverTombstoned (bounded, idempotent; a dest that lost staging leaves the lock, never revives the source). - Bulk "Hand off active sessions…": transferableSessions + moveSessionsToPeer (sequential, rollup error) behind a "Hand off…" button → HandoffSheet checklist in RemoteAccessView. - Arrived-from provenance (mirror of moved-to): GRDB v24 arrived_from_device_id/arrived_at; the importer stamps them at staging; additive SessionSummary.arrivedFrom (ArrivedFrom); a subtle "Arrived from <Mac>" marker on the sidebar (live name) + iOS row (host-baked name). - Stranded-arrival "Activate anyway": the importer persists the staged Session to the staging dir at .ready, so a destination that relaunches before commit can recoverableInboundTransfers() and activateRecoveredTransfer()/clearInboundStaging(). AppStore surfaces pendingArrivedTransfers with activate/discard, shown in a new "Interrupted arrivals" section. (A .ready lock with no manifest is now cleared as unrecoverable.) Tests: +6 core, +2 protocol across WireMessageTests, SessionTransferTests, AppStoreTests, AppStoreSyncBridgeTests. Full package builds; Swift suites green. iOS NucleicRemote edits reviewed but not compiled here (separate Xcode target). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -1210,6 +1210,7 @@ extension WireSessionSummary {
|
|||||||
auto: auto ?? self.auto, autoShip: autoShip ?? self.autoShip,
|
auto: auto ?? self.auto, autoShip: autoShip ?? self.autoShip,
|
||||||
shipBranch: shipBranch ?? self.shipBranch,
|
shipBranch: shipBranch ?? self.shipBranch,
|
||||||
contextInputTokens: contextInputTokens,
|
contextInputTokens: contextInputTokens,
|
||||||
updatedAt: updatedAt ?? Date())
|
updatedAt: updatedAt ?? Date(),
|
||||||
|
movedTo: movedTo, arrivedFrom: arrivedFrom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,18 +125,32 @@ struct SessionRow: View {
|
|||||||
}
|
}
|
||||||
Text(summary.title).font(.body.weight(.medium)).lineLimit(1)
|
Text(summary.title).font(.body.weight(.medium)).lineLimit(1)
|
||||||
// The same trailing markers the Mac sidebar row carries: auto-approval
|
// The same trailing markers the Mac sidebar row carries: auto-approval
|
||||||
// bolt, Orchestra note, autoship box.
|
// bolt, Orchestra note, autoship box. Suppressed on a moved-away tombstone.
|
||||||
if summary.auto {
|
if summary.movedTo == nil {
|
||||||
Image(systemName: "bolt.fill").font(.caption2).foregroundStyle(Palette.accent)
|
if summary.auto {
|
||||||
}
|
Image(systemName: "bolt.fill").font(.caption2).foregroundStyle(Palette.accent)
|
||||||
if orchestra {
|
}
|
||||||
Image(systemName: "music.note.list").font(.caption2).foregroundStyle(Palette.orchestra)
|
if orchestra {
|
||||||
}
|
Image(systemName: "music.note.list").font(.caption2).foregroundStyle(Palette.orchestra)
|
||||||
if summary.autoShip {
|
}
|
||||||
Image(systemName: "shippingbox.fill").font(.caption2).foregroundStyle(Palette.accent)
|
if summary.autoShip {
|
||||||
|
Image(systemName: "shippingbox.fill").font(.caption2).foregroundStyle(Palette.accent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if showProjectName {
|
if let moved = summary.movedTo {
|
||||||
|
// Moved to another Mac (mesh P5): the name is baked by the host, so the phone
|
||||||
|
// renders it directly without needing to know that Mac itself.
|
||||||
|
Label("Moved to \(moved.deviceName)", systemImage: "arrow.up.forward")
|
||||||
|
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
||||||
|
} else if let arrived = summary.arrivedFrom {
|
||||||
|
// Arrived from another Mac (mesh P5) — subtle provenance, with the project name
|
||||||
|
// folded in when it would otherwise show.
|
||||||
|
Label(showProjectName ? "\(summary.projectName) · from \(arrived.deviceName)"
|
||||||
|
: "from \(arrived.deviceName)",
|
||||||
|
systemImage: "arrow.down.forward")
|
||||||
|
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
||||||
|
} else if showProjectName {
|
||||||
Text(summary.projectName).font(.caption).foregroundStyle(.secondary)
|
Text(summary.projectName).font(.caption).foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user