Show durable connection requests and timestamped node availability

This commit is contained in:
archipelago
2026-10-05 23:10:34 -04:00
parent 10d31ae13c
commit 0c25449566
20 changed files with 473 additions and 142 deletions
@@ -1,12 +1,12 @@
<template>
<div v-if="visibleRequests.length > 0" class="glass-card p-6 mb-6">
<div id="connection-requests" v-if="visibleRequests.length > 0" class="glass-card p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-base font-semibold text-white">Pending Peer Requests</h2>
<h2 class="text-base font-semibold text-white">Nostr Connection Requests</h2>
<p class="text-xs text-white/60">
Inbound requests await your approval. Outbound requests show what you've sent.
Approved peers are added as <span class="text-orange-300">Observer</span> — promote
to Trusted manually if you want them to receive state syncs.
Connections become <span class="text-orange-300">Observer</span> peers.
Approval delivery retries automatically while the other node connects.
</p>
</div>
<button
@@ -80,6 +80,7 @@ const props = defineProps<{
requests: PendingPeerRequest[]
polling: boolean
busyId: string | null
peerDids?: string[]
}>()
defineEmits<{
@@ -96,6 +97,7 @@ const visibleRequests = computed(() => {
const cutoff = Date.now() - 24 * 60 * 60 * 1000
return props.requests.filter((r) => {
if (r.state === 'pending' || r.state === 'sent') return true
if (r.state === 'approved' && !props.peerDids?.includes(r.from_did)) return true
const ts = new Date(r.received_at).getTime()
return Number.isFinite(ts) && ts >= cutoff
})
@@ -104,14 +106,20 @@ const visibleRequests = computed(() => {
function badgeClass(r: PendingPeerRequest): string {
if (r.state === 'pending') return 'bg-yellow-500/20 text-yellow-300'
if (r.state === 'sent') return 'bg-blue-500/20 text-blue-300'
if (r.state === 'approved') return 'bg-green-500/20 text-green-300'
if (r.state === 'approved') return props.peerDids?.includes(r.from_did)
? 'bg-green-500/20 text-green-300' : 'bg-yellow-500/20 text-yellow-300'
if (r.state === 'rejected') return 'bg-red-500/20 text-red-300'
return 'bg-white/10 text-white/50'
}
function badgeLabel(r: PendingPeerRequest): string {
if (r.outbound && r.state === 'sent') return 'Sent'
if (r.outbound && r.state === 'approved') return 'They approved'
if (r.state === 'approved') {
if (props.peerDids?.includes(r.from_did)) return 'Peer added'
const age = Date.now() - new Date(r.received_at).getTime()
if (!Number.isFinite(age) || age > 30 * 24 * 60 * 60 * 1000) return 'Approval expired — reconnect'
return r.outbound ? 'They approved — connecting' : 'Approved — connecting'
}
if (r.outbound && r.state === 'rejected') return 'They rejected'
if (r.outbound) return r.state
if (r.state === 'pending') return 'Inbound'
@@ -126,7 +134,7 @@ function shortNpub(npub: string): string {
function relative(iso: string): string {
const ts = new Date(iso).getTime()
if (!Number.isFinite(ts)) return iso
const diff = Date.now() - ts
const diff = Math.max(0, Date.now() - ts)
const sec = Math.floor(diff / 1000)
if (sec < 60) return `${sec}s ago`
const min = Math.floor(sec / 60)
@@ -0,0 +1,25 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import PendingRequestsPanel from '../PendingRequestsPanel.vue'
import type { PendingPeerRequest } from '@/api/rpc-client'
const request: PendingPeerRequest = {
id: 'request', from_did: 'did:key:peer', from_nostr_pubkey: 'key', from_nostr_npub: 'npub',
from_name: 'Other node', message: null, outbound: false, state: 'approved',
received_at: new Date(Date.now()-2*86400000).toISOString(),
}
describe('Nostr connection progress', () => {
it('keeps unresolved approvals visible and distinguishes local membership', async () => {
const wrapper=mount(PendingRequestsPanel,{props:{requests:[request],polling:false,busyId:null,peerDids:[]}})
expect(wrapper.text()).toContain('Approved — connecting')
expect(wrapper.text()).not.toContain('Peer added')
await wrapper.setProps({requests:[{...request,received_at:new Date().toISOString()}],peerDids:[request.from_did]})
expect(wrapper.text()).toContain('Peer added')
expect(wrapper.text()).not.toContain('Approved — connecting')
wrapper.unmount()
})
it('explains expired approval instead of claiming a connection is still progressing', () => {
const wrapper=mount(PendingRequestsPanel,{props:{requests:[{...request,received_at:new Date(Date.now()-31*86400000).toISOString()}],polling:false,busyId:null}})
expect(wrapper.text()).toContain('Approval expired — reconnect')
wrapper.unmount()
})
})