fix(fips): fall back to upstream daemon npub on legacy/dev nodes
Nodes without a seed-derived FIPS key (legacy deploys, fresh pre-onboarding installs) were reporting "Awaiting seed" in the dashboard even when the upstream fips.service was running — status.npub was None unless /data/identity/fips_key.pub existed. - fips/service.rs: new read_upstream_npub() reads /etc/fips/fips.pub (bech32 text or raw 32 bytes) from the debian package. - fips/mod.rs: FipsStatus::current() prefers the seed-derived npub, falls back to the upstream key. service_active is now TRUE if either archipelago-fips.service OR upstream fips.service is active; adds upstream_service_state to the status payload. - fips/update.rs: resolve the upstream default branch from the GitHub repo API (jmcorgan/fips is on `master`, not `main`) instead of hardcoding — future repo rename just works. - network/router.rs + api/rpc/router.rs: diagnostics gain wifi_ssid from `nmcli -t device` so the Network card can show the connected SSID. - UI: Home.vue adds a FIPS row to the Local Network card; Server.vue mounts the new FipsNetworkCard and shows SSID + FIPS Mesh rows; HomeNetworkCard.vue removed (superseded by the inline rows). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
30a7f73ead
commit
6b42bfd503
@@ -151,6 +151,10 @@
|
||||
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="systemStats.bitcoinAvailable ? 'bg-orange-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">Bitcoin</span></div>
|
||||
<span class="text-sm font-medium" :class="systemStats.bitcoinAvailable ? 'text-orange-400' : 'text-white/40'">{{ bitcoinSyncDisplay }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="fipsDotClass"></div><span class="text-sm text-white/80">FIPS</span></div>
|
||||
<span class="text-sm font-medium" :class="fipsTextClass">{{ fipsStatusLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<RouterLink to="/dashboard/server" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">{{ t('home.manageNetwork') }}</RouterLink>
|
||||
@@ -313,6 +317,27 @@ const torConnected = computed(() => {
|
||||
})
|
||||
const vpnStatus = ref({ connected: false, provider: '' })
|
||||
const vpnConnected = computed(() => vpnStatus.value.connected || (!!packages.value['tailscale'] && packages.value['tailscale'].state === PackageState.Running))
|
||||
const fipsStatus = ref<{ installed: boolean; service_active: boolean; key_present: boolean } | null>(null)
|
||||
const fipsDotClass = computed(() => {
|
||||
const s = fipsStatus.value
|
||||
if (!s || !s.installed) return 'bg-white/40'
|
||||
if (s.service_active) return 'bg-green-400'
|
||||
return 'bg-white/40'
|
||||
})
|
||||
const fipsTextClass = computed(() => {
|
||||
const s = fipsStatus.value
|
||||
if (!s || !s.installed) return 'text-white/40'
|
||||
if (s.service_active) return 'text-green-400'
|
||||
return 'text-white/40'
|
||||
})
|
||||
const fipsStatusLabel = computed(() => {
|
||||
const s = fipsStatus.value
|
||||
if (!s) return '…'
|
||||
if (!s.installed) return 'Not installed'
|
||||
if (s.service_active) return 'Active'
|
||||
if (!s.key_present) return 'Awaiting seed'
|
||||
return 'Inactive'
|
||||
})
|
||||
const bitcoinSyncDisplay = computed(() => {
|
||||
if (!systemStats.bitcoinAvailable) return 'Not running'
|
||||
if (systemStats.bitcoinSyncPercent >= 99.9) return 'Synced'
|
||||
@@ -349,6 +374,7 @@ onMounted(async () => {
|
||||
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
|
||||
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 30000); checkUpdateStatus(); loadWeb5Status()
|
||||
rpcClient.vpnStatus().then(s => { vpnStatus.value = { connected: s.connected, provider: s.provider ?? '' } }).catch(() => {})
|
||||
rpcClient.call<{ installed: boolean; service_active: boolean; key_present: boolean }>({ method: 'fips.status' }).then(s => { fipsStatus.value = s }).catch(() => {})
|
||||
})
|
||||
|
||||
// Wallet modals
|
||||
|
||||
@@ -89,10 +89,10 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||
<span class="text-white/80 text-sm">WiFi Networks</span>
|
||||
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" /></svg>
|
||||
<span class="text-white/80 text-sm">WiFi</span>
|
||||
</div>
|
||||
<span class="text-white/60 text-sm">{{ networkData.wifiCount }}</span>
|
||||
<span class="text-sm" :class="networkData.wifiSsid ? 'text-green-400' : 'text-white/40'">{{ networkData.wifiSsid || 'Not connected' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -126,6 +126,13 @@
|
||||
{{ dnsDisplayLabel }}
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
<span class="text-white/80 text-sm">FIPS Mesh</span>
|
||||
</div>
|
||||
<span class="text-sm" :class="fipsRowTextClass">{{ fipsRowLabel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -160,6 +167,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FIPS Mesh (full card) -->
|
||||
<div class="mb-8">
|
||||
<FipsNetworkCard />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
|
||||
<!-- VPN Card -->
|
||||
<div class="glass-card p-6 transition-all hover:-translate-y-1">
|
||||
@@ -377,6 +389,7 @@ import { useAppStore } from '@/stores/app'
|
||||
import QuickActionsCard from './server/QuickActionsCard.vue'
|
||||
import TorServicesCard from './server/TorServicesCard.vue'
|
||||
import ServerModals from './server/ServerModals.vue'
|
||||
import FipsNetworkCard from './server/FipsNetworkCard.vue'
|
||||
import type { TorServiceInfo } from './server/TorServicesCard.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
@@ -401,11 +414,34 @@ const logCount = ref(0)
|
||||
// Network data
|
||||
const networkLoading = ref(true)
|
||||
const networkData = ref({
|
||||
wifiCount: 'N/A', torConnected: false, forwardCount: 'N/A',
|
||||
wifiCount: 'N/A', wifiSsid: null as string | null, torConnected: false, forwardCount: 'N/A',
|
||||
vpnConnected: false, vpnProvider: '', vpnIp: '', wgIp: '', wgPubkey: '', vpnHostname: '', vpnPeers: 0,
|
||||
dnsProvider: 'system', dnsServers: [] as string[], dnsDoH: false,
|
||||
})
|
||||
|
||||
// FIPS status row for the Local Network card. Full FIPS card lives below.
|
||||
const fipsSummary = ref<{ installed: boolean; service_active: boolean; key_present: boolean } | null>(null)
|
||||
const fipsRowLabel = computed(() => {
|
||||
const s = fipsSummary.value
|
||||
if (!s) return '…'
|
||||
if (!s.installed) return 'Not installed'
|
||||
// Service-active wins even on legacy nodes with no seed-derived key.
|
||||
if (s.service_active) return 'Active'
|
||||
if (!s.key_present) return 'Awaiting seed'
|
||||
return 'Inactive'
|
||||
})
|
||||
const fipsRowTextClass = computed(() => {
|
||||
const s = fipsSummary.value
|
||||
if (!s || !s.installed) return 'text-white/40'
|
||||
if (s.service_active) return 'text-green-400'
|
||||
return 'text-white/60'
|
||||
})
|
||||
async function loadFipsSummary() {
|
||||
try {
|
||||
fipsSummary.value = await rpcClient.call<{ installed: boolean; service_active: boolean; key_present: boolean }>({ method: 'fips.status' })
|
||||
} catch { /* backend too old */ }
|
||||
}
|
||||
|
||||
async function loadNetworkData() {
|
||||
networkLoading.value = true
|
||||
try {
|
||||
@@ -415,7 +451,7 @@ async function loadNetworkData() {
|
||||
rpcClient.vpnStatus(),
|
||||
rpcClient.dnsStatus(),
|
||||
])
|
||||
if (diagRes.status === 'fulfilled') { networkData.value.torConnected = diagRes.value.tor_connected; networkData.value.wifiCount = diagRes.value.wifi_count !== undefined ? `${diagRes.value.wifi_count} configured` : 'N/A' }
|
||||
if (diagRes.status === 'fulfilled') { networkData.value.torConnected = diagRes.value.tor_connected; networkData.value.wifiCount = diagRes.value.wifi_count !== undefined ? `${diagRes.value.wifi_count} configured` : 'N/A'; networkData.value.wifiSsid = (diagRes.value as { wifi_ssid?: string | null }).wifi_ssid ?? null }
|
||||
if (fwdRes.status === 'fulfilled') { const c = fwdRes.value.forwards?.length ?? 0; networkData.value.forwardCount = `${c} rule${c !== 1 ? 's' : ''}` }
|
||||
if (vpnRes.status === 'fulfilled') { networkData.value.vpnConnected = vpnRes.value.connected; networkData.value.vpnProvider = vpnRes.value.provider ?? ''; networkData.value.vpnIp = (vpnRes.value.ip_address ?? '').replace(/\/\d+$/, ''); networkData.value.wgIp = vpnRes.value.wg_ip ?? ''; networkData.value.wgPubkey = (vpnRes.value as Record<string, unknown>).wg_pubkey as string ?? '' }
|
||||
if (dnsRes.status === 'fulfilled') { networkData.value.dnsProvider = dnsRes.value.provider; networkData.value.dnsServers = dnsRes.value.resolv_conf_servers ?? []; networkData.value.dnsDoH = dnsRes.value.doh_enabled }
|
||||
@@ -672,7 +708,7 @@ async function createService(name: string, port: number | null) {
|
||||
catch (e) { addServiceError.value = e instanceof Error ? e.message : 'Failed to create service' } finally { addingService.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { checkTorStatus(); loadNetworkData(); loadInterfaces(); loadDiskStatus(); loadTorServices(); loadVpnPeers() })
|
||||
onMounted(() => { checkTorStatus(); loadNetworkData(); loadInterfaces(); loadDiskStatus(); loadTorServices(); loadVpnPeers(); loadFipsSummary() })
|
||||
|
||||
// Poll VPN status every 15s so IP updates after pairing
|
||||
const vpnPollInterval = setInterval(async () => {
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="home-card controller-focusable"
|
||||
:class="{ 'home-card-animate': animate }"
|
||||
style="--card-stagger: 5"
|
||||
>
|
||||
<div class="home-card-shell">
|
||||
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
||||
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
||||
<div class="home-card-text">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Network</h2>
|
||||
<p class="text-sm text-white/70">FIPS mesh — preferred over Tor for peer traffic</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2" :title="statusLabel">
|
||||
<span class="w-2 h-2 rounded-full" :class="statusDotColor"></span>
|
||||
<span class="text-sm font-medium" :class="statusTextColor">{{ statusLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-card-stats grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Daemon version</p>
|
||||
<p class="text-sm font-medium text-white break-all">{{ status.version || '—' }}</p>
|
||||
<p v-if="!status.installed" class="text-xs text-white/40 mt-1">Package not installed</p>
|
||||
</div>
|
||||
<div class="p-4 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">FIPS npub</p>
|
||||
<p class="text-sm font-mono text-white break-all">{{ npubDisplay }}</p>
|
||||
<p v-if="!status.key_present" class="text-xs text-white/40 mt-1">Unlock your seed to derive the FIPS key</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="updateInfo" class="mb-3 p-3 bg-white/5 rounded-lg border-l-2 border-orange-400">
|
||||
<p class="text-xs text-orange-400 font-medium mb-1">{{ updateInfo.update_available ? 'Update available' : 'Up to date' }}</p>
|
||||
<p class="text-xs text-white/70 break-all">{{ updateInfo.notes }}</p>
|
||||
</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 class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
||||
<button
|
||||
class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors"
|
||||
:disabled="checking"
|
||||
@click="checkForUpdate"
|
||||
>{{ checking ? 'Checking…' : 'Check for update' }}</button>
|
||||
<button
|
||||
v-if="status.key_present && !status.service_active"
|
||||
class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors"
|
||||
:disabled="installing"
|
||||
@click="installAndActivate"
|
||||
>{{ installing ? 'Installing…' : 'Activate' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
defineProps<{ animate: boolean }>()
|
||||
|
||||
interface FipsStatus {
|
||||
installed: boolean
|
||||
version: string | null
|
||||
service_state: string
|
||||
service_active: boolean
|
||||
key_present: boolean
|
||||
npub: string | null
|
||||
}
|
||||
|
||||
interface UpdateCheck {
|
||||
current: string | null
|
||||
latest_commit: string
|
||||
update_available: boolean
|
||||
notes: string
|
||||
}
|
||||
|
||||
const status = ref<FipsStatus>({
|
||||
installed: false,
|
||||
version: null,
|
||||
service_state: 'unknown',
|
||||
service_active: false,
|
||||
key_present: false,
|
||||
npub: null,
|
||||
})
|
||||
const updateInfo = ref<UpdateCheck | null>(null)
|
||||
const checking = ref(false)
|
||||
const installing = ref(false)
|
||||
const statusMessage = ref('')
|
||||
const statusIsError = ref(false)
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (!status.value.installed) return 'not installed'
|
||||
if (!status.value.key_present) return 'awaiting seed'
|
||||
if (status.value.service_active) return 'active'
|
||||
return status.value.service_state
|
||||
})
|
||||
|
||||
const statusDotColor = computed(() => {
|
||||
if (status.value.service_active) return 'bg-green-400'
|
||||
if (!status.value.installed || !status.value.key_present) return 'bg-white/30'
|
||||
return 'bg-orange-400'
|
||||
})
|
||||
|
||||
const statusTextColor = computed(() => {
|
||||
if (status.value.service_active) return 'text-green-400'
|
||||
if (!status.value.installed || !status.value.key_present) return 'text-white/50'
|
||||
return 'text-orange-400'
|
||||
})
|
||||
|
||||
const npubDisplay = computed(() => {
|
||||
const n = status.value.npub
|
||||
if (!n) return '—'
|
||||
return n.length > 20 ? `${n.slice(0, 12)}…${n.slice(-6)}` : n
|
||||
})
|
||||
|
||||
function flash(msg: string, isError = false) {
|
||||
statusMessage.value = msg
|
||||
statusIsError.value = isError
|
||||
setTimeout(() => { statusMessage.value = '' }, 6000)
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.status' })
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('fips.status failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdate() {
|
||||
checking.value = true
|
||||
try {
|
||||
updateInfo.value = await rpcClient.call<UpdateCheck>({ method: 'fips.check-update' })
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
flash(`Update check failed: ${msg}`, true)
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function installAndActivate() {
|
||||
installing.value = true
|
||||
try {
|
||||
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.install' })
|
||||
flash('FIPS installed and activated')
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
flash(`Install failed: ${msg}`, true)
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadStatus)
|
||||
</script>
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col transition-all hover:-translate-y-1">
|
||||
<div class="flex items-start gap-4 mb-4 shrink-0">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<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="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</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 Mesh</h2>
|
||||
<div class="flex items-center gap-2" :title="statusLabel">
|
||||
<span class="w-2 h-2 rounded-full" :class="statusDotColor"></span>
|
||||
<span class="text-sm font-medium" :class="statusTextColor">{{ statusLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-white/70 text-sm mb-4">Fast Nostr-keyed mesh routing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3 flex-1 min-h-0">
|
||||
<div class="p-3 bg-white/5 rounded-lg">
|
||||
<p class="text-xs text-white/60 mb-1">Daemon version</p>
|
||||
<p class="text-sm font-medium text-white break-all">{{ status.version || '—' }}</p>
|
||||
<p v-if="!status.installed" class="text-xs text-white/40 mt-1">Package not installed</p>
|
||||
</div>
|
||||
<div class="p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center justify-between gap-2 mb-1">
|
||||
<p class="text-xs text-white/60">FIPS npub</p>
|
||||
<button
|
||||
v-if="status.npub"
|
||||
type="button"
|
||||
class="text-xs text-white/60 hover:text-white transition-colors flex items-center gap-1"
|
||||
:title="copied ? 'Copied!' : 'Copy full npub to clipboard'"
|
||||
@click="copyNpub"
|
||||
>
|
||||
<svg v-if="!copied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
||||
<svg v-else class="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>
|
||||
<span :class="{ 'text-green-400': copied }">{{ copied ? 'Copied' : 'Copy' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm font-mono text-white break-all select-all">{{ npubDisplay }}</p>
|
||||
<p v-if="!status.key_present && status.npub" class="text-xs text-white/40 mt-1">Upstream key (not seed-derived)</p>
|
||||
<p v-else-if="!status.key_present" class="text-xs text-white/40 mt-1">Unlock seed to derive archipelago-managed key</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="status.key_present && !status.service_active" class="flex gap-2 mt-auto pt-3 shrink-0">
|
||||
<button class="flex-1 min-h-[44px] px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" :disabled="installing" @click="installAndActivate">{{ installing ? 'Installing…' : 'Activate' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { safeClipboardWrite } from '@/views/web5/utils'
|
||||
|
||||
interface FipsStatus {
|
||||
installed: boolean
|
||||
version: string | null
|
||||
service_state: string
|
||||
upstream_service_state: string
|
||||
service_active: boolean
|
||||
key_present: boolean
|
||||
npub: string | null
|
||||
}
|
||||
|
||||
const status = ref<FipsStatus>({
|
||||
installed: false,
|
||||
version: null,
|
||||
service_state: 'unknown',
|
||||
upstream_service_state: 'unknown',
|
||||
service_active: false,
|
||||
key_present: false,
|
||||
npub: null,
|
||||
})
|
||||
const installing = ref(false)
|
||||
const statusMessage = ref('')
|
||||
const statusIsError = ref(false)
|
||||
const copied = ref(false)
|
||||
|
||||
async function copyNpub() {
|
||||
if (!status.value.npub) return
|
||||
try {
|
||||
await safeClipboardWrite(status.value.npub)
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
flash(`Copy failed: ${msg}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (!status.value.installed) return 'not installed'
|
||||
// Active takes precedence: the daemon may be running from its own upstream
|
||||
// key on a legacy/dev node that doesn't have a seed-derived archipelago key.
|
||||
if (status.value.service_active) return 'active'
|
||||
if (!status.value.key_present) return 'awaiting seed'
|
||||
return status.value.service_state
|
||||
})
|
||||
|
||||
const statusDotColor = computed(() => {
|
||||
if (status.value.service_active) return 'bg-green-400'
|
||||
if (!status.value.installed || !status.value.key_present) return 'bg-white/30'
|
||||
return 'bg-orange-400'
|
||||
})
|
||||
|
||||
const statusTextColor = computed(() => {
|
||||
if (status.value.service_active) return 'text-green-400'
|
||||
if (!status.value.installed || !status.value.key_present) return 'text-white/50'
|
||||
return 'text-orange-400'
|
||||
})
|
||||
|
||||
const npubDisplay = computed(() => {
|
||||
const n = status.value.npub
|
||||
if (!n) return '—'
|
||||
return n.length > 20 ? `${n.slice(0, 12)}…${n.slice(-6)}` : n
|
||||
})
|
||||
|
||||
function flash(msg: string, isError = false) {
|
||||
statusMessage.value = msg
|
||||
statusIsError.value = isError
|
||||
setTimeout(() => { statusMessage.value = '' }, 6000)
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.status' })
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('fips.status failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function installAndActivate() {
|
||||
installing.value = true
|
||||
try {
|
||||
status.value = await rpcClient.call<FipsStatus>({ method: 'fips.install' })
|
||||
flash('FIPS installed and activated')
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
flash(`Install failed: ${msg}`, true)
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadStatus)
|
||||
</script>
|
||||
Reference in New Issue
Block a user