Re-adds the TCP transport (`0.0.0.0:8443`) to the rendered fips.yaml alongside UDP. Upstream factory default enables both; we had inadvertently narrowed to UDP-only when the yaml rewriter was last touched, which left nodes unable to reach fips.v0l.io (the public anchor only answers on TCP right now) or talk across networks that block UDP. Backend startup now compares the installed yaml against the current rendered schema and restarts whichever fips unit is active when they differ — so OTA-upgrading nodes pick up the new transport without anyone having to click Reconnect. Dropped the earlier plan to auto-add federated peers as seed anchors: invites don't carry a FIPS-reachable IP:port, and once TCP reconnects the public mesh, federated peers become npub-routable without needing a seed entry. Seed Anchors modal cleanup: replaced malformed header icon with a three-arc broadcast glyph, and the close button now matches the What's New modal (embedded in the card header, same icon + hover style) instead of the earlier floating off-design placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
187 lines
7.8 KiB
Vue
187 lines
7.8 KiB
Vue
<template>
|
|
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col transition-all hover:-translate-y-1 relative">
|
|
<button
|
|
v-if="closable"
|
|
type="button"
|
|
class="absolute top-4 right-4 p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors z-10"
|
|
aria-label="Close"
|
|
@click="$emit('close')"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
<div class="flex items-start gap-4 mb-4 shrink-0" :class="{ 'pr-10': closable }">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<!-- Radio/broadcast icon — three concentric arcs radiating from a
|
|
dot. Reads as mesh, signal, anchor-reaching-peers. -->
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.288 15.038a5.25 5.25 0 017.424 0M5.106 11.856c3.807-3.808 9.98-3.808 13.788 0M1.924 8.674c5.565-5.565 14.587-5.565 20.152 0" />
|
|
<circle cx="12" cy="18" r="1.25" fill="currentColor" stroke="none" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-start justify-between gap-4 mb-2">
|
|
<h2 class="text-xl font-semibold text-white">FIPS Seed Anchors</h2>
|
|
<button
|
|
type="button"
|
|
class="text-xs px-3 py-1.5 rounded-md bg-white/5 hover:bg-white/10 text-white/70 hover:text-white transition-colors disabled:opacity-60"
|
|
:disabled="applying"
|
|
:title="applying ? 'Applying…' : 'Re-dial every anchor in the list'"
|
|
@click="applyAll"
|
|
>
|
|
{{ applying ? 'Applying…' : 'Apply now' }}
|
|
</button>
|
|
</div>
|
|
<p class="text-white/70 text-sm mb-4">
|
|
Peers this node dials to bootstrap the FIPS mesh. A cluster with its own anchors doesn't depend on the global public anchor — if one is down, the next seeds the DHT instead.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="statusMessage" class="mb-3 p-3 rounded-lg text-xs" :class="statusIsError ? 'bg-red-400/10 text-red-300' : 'bg-green-400/10 text-green-300'">{{ statusMessage }}</div>
|
|
|
|
<div v-if="anchors.length === 0" class="p-4 rounded-lg bg-white/5 text-sm text-white/60 mb-3">
|
|
<p>No seed anchors configured. The daemon will fall back to whatever the upstream FIPS build dials on its own — usually the single public anchor, which is fine until it isn't.</p>
|
|
<p class="mt-2 text-white/50">Add at least one known-reachable peer (e.g. your VPS or a home node with port-forwarded UDP 8668) to make this cluster self-anchoring.</p>
|
|
</div>
|
|
|
|
<ul v-else class="space-y-2 mb-3">
|
|
<li v-for="a in anchors" :key="a.npub" class="p-3 bg-white/5 rounded-lg flex items-start gap-3">
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-medium text-white truncate">{{ a.label || 'Unlabeled anchor' }}</p>
|
|
<p class="text-xs text-white/60 font-mono break-all">{{ a.npub.slice(0, 20) }}…{{ a.npub.slice(-8) }}</p>
|
|
<p class="text-xs text-white/50 mt-0.5">{{ a.address }} · {{ a.transport }}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="shrink-0 text-xs px-2 py-1 rounded-md text-red-300 hover:bg-red-400/10 transition-colors"
|
|
:title="`Remove ${a.label || a.npub.slice(0, 12)}`"
|
|
@click="removeAnchor(a.npub)"
|
|
>Remove</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<form class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-auto pt-3 border-t border-white/10 shrink-0" @submit.prevent="addAnchor">
|
|
<label class="flex flex-col gap-1 sm:col-span-2">
|
|
<span class="text-xs text-white/60">Anchor npub</span>
|
|
<input v-model="draft.npub" type="text" placeholder="npub1…" class="px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none" />
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs text-white/60">Address (host:port)</span>
|
|
<input v-model="draft.address" type="text" placeholder="192.168.1.116:8668" class="px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none" />
|
|
</label>
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-xs text-white/60">Label (optional)</span>
|
|
<input v-model="draft.label" type="text" placeholder="Home anchor" class="px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none" />
|
|
</label>
|
|
<button type="submit" class="sm:col-span-2 min-h-[44px] glass-button rounded-lg text-sm font-medium disabled:opacity-60" :disabled="adding || !draft.npub || !draft.address">{{ adding ? 'Adding…' : 'Add anchor' }}</button>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
|
|
defineProps<{ closable?: boolean }>()
|
|
defineEmits<{ (e: 'close'): void }>()
|
|
|
|
interface SeedAnchor {
|
|
npub: string
|
|
address: string
|
|
transport: string
|
|
label: string
|
|
}
|
|
|
|
interface ApplyResult {
|
|
npub: string
|
|
ok: boolean
|
|
message: string
|
|
}
|
|
|
|
const anchors = ref<SeedAnchor[]>([])
|
|
const adding = ref(false)
|
|
const applying = ref(false)
|
|
const statusMessage = ref('')
|
|
const statusIsError = ref(false)
|
|
|
|
const draft = reactive<Pick<SeedAnchor, 'npub' | 'address' | 'label'>>({
|
|
npub: '',
|
|
address: '',
|
|
label: '',
|
|
})
|
|
|
|
function flash(msg: string, isError = false) {
|
|
statusMessage.value = msg
|
|
statusIsError.value = isError
|
|
setTimeout(() => { statusMessage.value = '' }, 6000)
|
|
}
|
|
|
|
async function load() {
|
|
try {
|
|
const res = await rpcClient.call<{ seed_anchors: SeedAnchor[] }>({ method: 'fips.list-seed-anchors' })
|
|
anchors.value = res.seed_anchors
|
|
} catch (e: unknown) {
|
|
if (import.meta.env.DEV) console.warn('fips.list-seed-anchors failed', e)
|
|
}
|
|
}
|
|
|
|
async function addAnchor() {
|
|
if (!draft.npub.trim() || !draft.address.trim()) return
|
|
adding.value = true
|
|
try {
|
|
const res = await rpcClient.call<{ seed_anchors: SeedAnchor[]; apply: ApplyResult[] }>({
|
|
method: 'fips.add-seed-anchor',
|
|
params: {
|
|
npub: draft.npub.trim(),
|
|
address: draft.address.trim(),
|
|
transport: 'udp',
|
|
label: draft.label.trim(),
|
|
},
|
|
})
|
|
anchors.value = res.seed_anchors
|
|
draft.npub = ''
|
|
draft.address = ''
|
|
draft.label = ''
|
|
const applied = res.apply.find(r => r.ok)
|
|
flash(applied ? 'Anchor added and dialed.' : 'Anchor saved — dial failed, will retry on the next apply cycle.', !applied)
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
flash(`Add failed: ${msg}`, true)
|
|
} finally {
|
|
adding.value = false
|
|
}
|
|
}
|
|
|
|
async function removeAnchor(npub: string) {
|
|
try {
|
|
const res = await rpcClient.call<{ seed_anchors: SeedAnchor[] }>({
|
|
method: 'fips.remove-seed-anchor',
|
|
params: { npub },
|
|
})
|
|
anchors.value = res.seed_anchors
|
|
flash('Anchor removed.')
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
flash(`Remove failed: ${msg}`, true)
|
|
}
|
|
}
|
|
|
|
async function applyAll() {
|
|
applying.value = true
|
|
try {
|
|
const res = await rpcClient.call<{ applied: number; results: ApplyResult[] }>({ method: 'fips.apply-seed-anchors' })
|
|
const ok = res.results.filter(r => r.ok).length
|
|
flash(`${ok} of ${res.applied} anchor${res.applied === 1 ? '' : 's'} dialed.`, ok === 0 && res.applied > 0)
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
flash(`Apply failed: ${msg}`, true)
|
|
} finally {
|
|
applying.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|