feat(federation): cancel button for outbound pending peer requests

Previously the Pending Peer Requests panel only had Approve/Reject for
inbound rows; outbound rows in the 'sent' state had no action and
would sit there until the target explicitly approved or rejected. Now
you can Cancel an outbound request — the local row is dropped and a
PeerCancel nostr DM is sent so the target's inbound row also
disappears.

Backend:
- HandshakeMessage::PeerCancel {reason: Option<String>} variant.
- nostr_handshake::send_peer_cancel() mirrors send_peer_reject.
- handshake.poll handler dispatches inbound PeerCancel: finds the
  matching inbound pending row (same from_nostr_pubkey, state=Pending)
  and deletes it. Reply shape gains `cancelled_inbound: [id]`.
- federation::pending::delete() — hard-remove (set_state only
  transitions; we don't want 'Cancelled' ghosts in the audit trail).
- federation.cancel-request RPC: outbound+Sent only, default
  notify=true (cancelling silently is a footgun), best-effort DM
  (relay failure doesn't block local deletion). Wired in dispatcher.

Frontend:
- PendingRequestsPanel.vue: Cancel button appears only on
  outbound+sent rows. Emits 'cancel' event with request id.
- Federation.vue: cancelPending(id) handler calls
  rpcClient.federationCancelRequest and reloads the list.
- rpcClient.federationCancelRequest(id, reason?, notify=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-19 02:28:16 -04:00
co-authored by Claude Opus 4.7
parent 2f327183eb
commit 2e8417e39b
8 changed files with 179 additions and 0 deletions
+19
View File
@@ -742,6 +742,25 @@ class RPCClient {
})
}
/// Withdraw an outbound peer request we sent. Default notify=true so
/// the recipient's inbound row disappears from their UI (the main
/// UX reason to cancel — avoid leaving a stale handshake dangling).
async federationCancelRequest(
id: string,
reason?: string,
notify = true,
): Promise<{ cancelled: boolean; id: string; notified: boolean }> {
return this.call({
method: 'federation.cancel-request',
params: {
id,
...(reason ? { reason } : {}),
notify,
},
timeout: 30000,
})
}
async federationSyncState(): Promise<{
synced: number
failed: number