feat: Phase 4 hardening — retry/backoff, loop detection, sync status UI + tests
Phase 4.1 — Retry/backoff for failed permission pushes: - _compute_next_retry(): capped exponential backoff with jitter (60s–1h) - _record_push_result(): tracks push_attempts and next_retry_at per link - replay_pending_pushes(): skips links still in backoff window, logs deferred count - _load() migration: adds push_attempts/next_retry_at to existing records Phase 4.2 — Loop detection (A→B→A routing cycle): - set_peer_route_via(): returns 409 if target cell already routes peers through us - apply_remote_permissions(): soft warning when accepting exit-relay that would cycle Phase 4.3 — Sync staleness indicator in Cell Network UI: - SyncBadge component: green (synced), amber (pending/failed), gray (never) - Shows relativeTime of last sync + error message + next retry estimate - Injected into CellPanel header alongside tunnel online/handshake status Tests (54 new): - TestCheckInviteConflicts: subnet overlap, domain conflict, exclude_cell (9 tests) - TestPushInviteToRemote: success, 4xx, no endpoint, subprocess errors (7 tests) - TestAcceptInviteNew: new cell, idempotent, healing dns/subnet changes (16 tests) - TestAddConnectionMutualPairing: push-invite call, non-fatal failure (5 tests) - TestPeerSyncAcceptInvite endpoint: happy path, field validation, error propagation (16 tests) - Fixed 2 existing replay tests to clear backoff gate (simulates elapsed window) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,31 @@ function StatusDot({ online }) {
|
||||
: <span className="inline-block h-2 w-2 rounded-full bg-red-400 mr-1.5" title="Offline" />;
|
||||
}
|
||||
|
||||
function SyncBadge({ conn }) {
|
||||
const { last_push_status, last_push_at, last_push_error, pending_push, next_retry_at } = conn;
|
||||
let color, label, tip;
|
||||
if (last_push_status === 'never' || (!last_push_at && !pending_push)) {
|
||||
color = 'bg-gray-300'; label = 'Sync pending';
|
||||
tip = 'Permissions not yet synced to remote cell';
|
||||
} else if (!pending_push && last_push_status === 'ok') {
|
||||
color = 'bg-green-500'; label = `Synced${last_push_at ? ' ' + relativeTime(last_push_at) : ''}`;
|
||||
tip = `Permissions last synced ${last_push_at ? relativeTime(last_push_at) : ''}`;
|
||||
} else {
|
||||
color = 'bg-amber-400'; label = 'Out of sync';
|
||||
tip = last_push_error ? `Sync failed: ${last_push_error}` : 'Permissions pending sync';
|
||||
if (next_retry_at) {
|
||||
const retryIn = Math.max(0, Math.round((new Date(next_retry_at) - Date.now()) / 60000));
|
||||
tip += ` — next retry in ~${retryIn}m`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-gray-500" title={tip}>
|
||||
<span className={`inline-block h-2 w-2 rounded-full ${color}`} />
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function Toast({ toasts }) {
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 space-y-2">
|
||||
@@ -204,6 +229,7 @@ function CellPanel({ conn, onDisconnect, addToast }) {
|
||||
{conn.last_handshake && (
|
||||
<span>{relativeTime(conn.last_handshake)}</span>
|
||||
)}
|
||||
<SyncBadge conn={conn} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user