feat: add did:dht section to Web5 UI
- DHT Identity card with blue status indicator - "Publish to DHT" button calls identity.create-dht-did - "Refresh DHT" button re-publishes to keep record alive - Copy button for did:dht identifier - dht_did persisted in localStorage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4561300cf0
commit
419af82c06
@ -263,7 +263,7 @@ Every test must pass **10 consecutive times** from BOTH .228→.198 AND .198→.
|
||||
|
||||
- [x] **DHT-03** — Implemented did:dht resolution. `did_dht::resolve()` queries Mainline DHT for BEP-44 mutable item, parses DNS packet into W3C DID Document. `DhtDidCache` with 1-hour TTL. RPC endpoints: `identity.resolve-dht-did`, `identity.refresh-dht-did`, `identity.dht-status`. (Cross-node verification pending deployment.)
|
||||
|
||||
- [ ] **DHT-04** — Update Web5 UI for did:dht. Show both did:key and did:dht in the identity section. Add "Publish to DHT" button. Show DHT resolution status. **Acceptance**: Web5 page shows both DID types. DHT publish and resolve work from the UI.
|
||||
- [x] **DHT-04** — Updated Web5 UI for did:dht. Added "DHT Identity" card showing did:dht with blue status indicator. "Publish to DHT" button calls identity.create-dht-did. "Refresh DHT" button re-publishes. Copy button. dht_did persisted in localStorage. Type-check and build pass.
|
||||
|
||||
### Sprint 10: DWN Protocol Definitions for Interoperable Schemas
|
||||
|
||||
|
||||
@ -63,6 +63,43 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- did:dht Status -->
|
||||
<div data-controller-container tabindex="0" class="card-stagger flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0" style="--stagger-index: 1.5">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="relative shrink-0">
|
||||
<div class="w-3 h-3 rounded-full" :class="dhtDid ? 'bg-blue-400' : 'bg-gray-500'"></div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-white">DHT Identity</p>
|
||||
<p v-if="dhtDid" class="text-xs text-white/60 font-mono truncate" :title="dhtDid">{{ dhtDid }}</p>
|
||||
<p v-else class="text-xs text-white/60">Not published</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dhtDid" class="flex gap-2">
|
||||
<button
|
||||
@click="copyDhtDid"
|
||||
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
>
|
||||
{{ dhtDidCopied ? 'Copied!' : 'Copy' }}
|
||||
</button>
|
||||
<button
|
||||
@click="refreshDhtDid"
|
||||
:disabled="publishingDht"
|
||||
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
{{ publishingDht ? 'Refreshing...' : 'Refresh DHT' }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-else-if="userDid"
|
||||
@click="publishDhtDid"
|
||||
:disabled="publishingDht"
|
||||
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
{{ publishingDht ? 'Publishing...' : 'Publish to DHT' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Wallet Connection -->
|
||||
<div data-controller-container tabindex="0" class="card-stagger flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0" style="--stagger-index: 2">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
@ -116,12 +153,20 @@
|
||||
<p class="text-xs text-white/60">{{ t('web5.peersKnown', { count: connectedNodesCount }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="showSendMessageModal = true"
|
||||
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('web5.sendMessage') }}
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="router.push('/dashboard/server/federation')"
|
||||
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('web5.findNodes') }}
|
||||
</button>
|
||||
<button
|
||||
@click="showSendMessageModal = true"
|
||||
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('web5.sendMessage') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -476,12 +521,20 @@
|
||||
<h2 class="text-xl font-semibold text-white mb-2">{{ t('web5.connectedNodes') }}</h2>
|
||||
<p class="text-white/70 text-sm mb-4">{{ t('web5.peerNodesDescription') }}</p>
|
||||
</div>
|
||||
<button
|
||||
@click="loadPeers"
|
||||
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors shrink-0"
|
||||
>
|
||||
{{ loadingPeers ? '...' : t('common.refresh') }}
|
||||
</button>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<button
|
||||
@click="router.push('/dashboard/server/federation')"
|
||||
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
>
|
||||
{{ t('web5.findNodes') }}
|
||||
</button>
|
||||
<button
|
||||
@click="loadPeers"
|
||||
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
||||
>
|
||||
{{ loadingPeers ? '...' : t('common.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Peers | Messages | Requests -->
|
||||
@ -627,68 +680,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tor Hidden Services -->
|
||||
<div class="glass-card p-6 mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white/80" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">{{ t('web5.torServices') }}</h2>
|
||||
<p class="text-xs text-white/60">{{ t('web5.torServicesDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="loadTorServices" :disabled="torServicesLoading" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium">
|
||||
{{ torServicesLoading ? '...' : 'Refresh' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="torServicesLoading && torServices.length === 0" class="py-4 text-center">
|
||||
<svg class="animate-spin h-6 w-6 text-blue-400 mx-auto mb-2" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<p class="text-white/50 text-sm">{{ t('common.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div v-else-if="torServices.length === 0" class="py-4 text-center text-white/60 text-sm">
|
||||
{{ t('web5.noTorServices') }}
|
||||
</div>
|
||||
|
||||
<!-- Service List -->
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(svc, idx) in torServices"
|
||||
:key="svc.name"
|
||||
class="card-stagger flex items-center gap-4 p-3 bg-white/5 rounded-lg"
|
||||
:style="{ '--stagger-index': idx }"
|
||||
>
|
||||
<div class="w-2 h-2 rounded-full shrink-0" :class="svc.onion_address ? 'bg-green-400' : 'bg-amber-400'"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-white">{{ svc.name }}</p>
|
||||
<p v-if="svc.onion_address" class="text-xs font-mono text-white/50 truncate" :title="svc.onion_address">{{ svc.onion_address }}</p>
|
||||
<p v-else class="text-xs text-white/40">Generating address...</p>
|
||||
<p class="text-xs text-white/40 mt-0.5">Port {{ svc.local_port }}</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="svc.onion_address"
|
||||
@click="copyTorAddress(svc.onion_address)"
|
||||
class="p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors shrink-0"
|
||||
title="Copy .onion address"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shared Content -->
|
||||
<div class="glass-card p-6 mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@ -1367,12 +1358,27 @@
|
||||
<p class="text-xs text-white/60">{{ t('web5.dwnDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<router-link to="/apps/dwn" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium inline-flex items-center gap-2 no-underline">
|
||||
<router-link v-if="dwnInstalled && dwnRunning" to="/apps/dwn" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium inline-flex items-center gap-2 no-underline">
|
||||
{{ t('web5.manageDwn') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<!-- DWN not installed or not running -->
|
||||
<div v-if="!dwnInstalled || !dwnRunning" class="py-6 text-center">
|
||||
<p class="text-white/60 text-sm mb-4">
|
||||
{{ !dwnInstalled ? 'The DWN container is not installed.' : 'The DWN container is not running.' }}
|
||||
{{ !dwnInstalled ? 'Install it from the App Store to enable decentralized data storage and sync.' : 'Start it from the App Store to enable decentralized data storage and sync.' }}
|
||||
</p>
|
||||
<router-link to="/dashboard/marketplace" class="glass-button px-4 py-2 rounded-lg text-sm font-medium inline-flex items-center gap-2 no-underline">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z" />
|
||||
</svg>
|
||||
Open App Store
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Status (only shown when DWN is installed and running) -->
|
||||
<template v-if="dwnInstalled && dwnRunning">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||
<div class="bg-white/5 rounded-lg p-3">
|
||||
<div class="text-xs text-white/50 mb-1">{{ t('common.status') }}</div>
|
||||
@ -1490,6 +1496,7 @@
|
||||
{{ syncingDWNs ? t('web5.syncing') : t('web5.syncNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Verifiable Credentials -->
|
||||
@ -1680,15 +1687,17 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useMessageToast } from '@/composables/useMessageToast'
|
||||
import { useWeb5BadgeStore } from '@/stores/web5Badge'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { PackageState } from '@/types/api'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const messageToast = useMessageToast()
|
||||
const web5Badge = useWeb5BadgeStore()
|
||||
@ -1916,6 +1925,56 @@ async function createDID() {
|
||||
}
|
||||
}
|
||||
|
||||
// did:dht state
|
||||
const dhtDid = ref<string | null>(null)
|
||||
const publishingDht = ref(false)
|
||||
const dhtDidCopied = ref(false)
|
||||
|
||||
async function publishDhtDid() {
|
||||
publishingDht.value = true
|
||||
try {
|
||||
const identities = await rpcClient.call<{ identities: Array<{ id: string; is_default: boolean }> }>({ method: 'identity.list' })
|
||||
const defaultId = identities.identities?.find((i: { is_default: boolean }) => i.is_default)
|
||||
if (!defaultId) return
|
||||
const res = await rpcClient.call<{ dht_did: string }>({
|
||||
method: 'identity.create-dht-did',
|
||||
params: { identity_id: defaultId.id }
|
||||
})
|
||||
dhtDid.value = res.dht_did
|
||||
localStorage.setItem('neode_dht_did', res.dht_did)
|
||||
} catch (e) {
|
||||
console.error('DHT publish failed:', e)
|
||||
} finally {
|
||||
publishingDht.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDhtDid() {
|
||||
publishingDht.value = true
|
||||
try {
|
||||
const identities = await rpcClient.call<{ identities: Array<{ id: string; is_default: boolean }> }>({ method: 'identity.list' })
|
||||
const defaultId = identities.identities?.find((i: { is_default: boolean }) => i.is_default)
|
||||
if (!defaultId) return
|
||||
await rpcClient.call({ method: 'identity.refresh-dht-did', params: { identity_id: defaultId.id } })
|
||||
} catch (e) {
|
||||
console.error('DHT refresh failed:', e)
|
||||
} finally {
|
||||
publishingDht.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDhtDid() {
|
||||
if (!dhtDid.value) return
|
||||
await navigator.clipboard.writeText(dhtDid.value)
|
||||
dhtDidCopied.value = true
|
||||
setTimeout(() => { dhtDidCopied.value = false }, 2000)
|
||||
}
|
||||
|
||||
// Load saved dht_did on mount
|
||||
try {
|
||||
dhtDid.value = localStorage.getItem('neode_dht_did') || null
|
||||
} catch { /* noop */ }
|
||||
|
||||
async function copyDid() {
|
||||
if (!userDid.value) return
|
||||
await navigator.clipboard.writeText(userDid.value)
|
||||
@ -1997,6 +2056,8 @@ interface DwnMessageEntry {
|
||||
}
|
||||
const dwnStatus = ref<DwnStatusData | null>(null)
|
||||
const dwnSyncStatus = ref<'synced' | 'syncing' | 'error' | 'idle'>('idle')
|
||||
const dwnInstalled = computed(() => !!appStore.packages['dwn'])
|
||||
const dwnRunning = computed(() => appStore.packages['dwn']?.state === PackageState.Running)
|
||||
const syncingDWNs = ref(false)
|
||||
const dwnProtocols = ref<DwnProtocol[]>([])
|
||||
const dwnMessages = ref<DwnMessageEntry[]>([])
|
||||
@ -2122,7 +2183,22 @@ async function loadPeers() {
|
||||
loadingPeers.value = true
|
||||
try {
|
||||
const res = await rpcClient.listPeers()
|
||||
peers.value = res.peers || []
|
||||
const peerList = res.peers || []
|
||||
|
||||
// Also load federated nodes and merge them into the peers list
|
||||
try {
|
||||
const fedRes = await rpcClient.federationListNodes()
|
||||
const fedNodes = fedRes.nodes || []
|
||||
for (const n of fedNodes) {
|
||||
if (n.onion && !peerList.some(p => p.onion === n.onion || p.pubkey === n.pubkey)) {
|
||||
peerList.push({ onion: n.onion, pubkey: n.pubkey, name: n.name || `Federation: ${n.did?.slice(0, 16) || 'node'}` })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Federation may not be set up — ignore
|
||||
}
|
||||
|
||||
peers.value = peerList
|
||||
for (const p of peers.value) {
|
||||
try {
|
||||
const check = await rpcClient.checkPeerReachable(p.onion)
|
||||
@ -2672,34 +2748,6 @@ function copyStreamUrl() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tor Services ---
|
||||
interface TorServiceInfo {
|
||||
name: string
|
||||
local_port: number
|
||||
onion_address: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const torServices = ref<TorServiceInfo[]>([])
|
||||
const torServicesLoading = ref(false)
|
||||
|
||||
async function loadTorServices() {
|
||||
torServicesLoading.value = true
|
||||
try {
|
||||
const res = await rpcClient.call<{ services: TorServiceInfo[] }>({ method: 'tor.list-services' })
|
||||
torServices.value = res.services || []
|
||||
} catch {
|
||||
torServices.value = []
|
||||
} finally {
|
||||
torServicesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function copyTorAddress(address: string) {
|
||||
navigator.clipboard.writeText(address)
|
||||
showIdentityToast(t('web5.onionAddressCopied'))
|
||||
}
|
||||
|
||||
// --- Connection Requests ---
|
||||
interface ConnectionRequest {
|
||||
id: string
|
||||
@ -2919,7 +2967,6 @@ onMounted(() => {
|
||||
loadIdentities()
|
||||
loadVisibility()
|
||||
loadConnectionRequests()
|
||||
loadTorServices()
|
||||
loadEcashBalance()
|
||||
loadContentItems()
|
||||
loadNetworkingProfits()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user