2026-03-21 02:43:28 +00:00
< template >
<!-- Decentralized Web Node ( DWN ) -->
< div class = "glass-card p-6" >
< 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 = "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</ svg >
</ div >
< div >
< h2 class = "text-lg font-semibold text-white" >{{ t ( 'web5.decentralizedWebNode' ) }}</ h2 >
< p class = "text-xs text-white/60" >{{ t ( 'web5.dwnDescription' ) }}</ p >
</ div >
</ div >
< 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 >
<!-- 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 >
< div class = "flex items-center gap-2" >
< div class = "w-2 h-2 rounded-full" : class = "dwnStatus?.running ? 'bg-green-400' : 'bg-red-400'" ></ div >
< span class = "text-sm text-white font-medium" >{{ dwnStatus ? . running ? t ( 'common.running' ) : t ( 'common.stopped' ) }}</ span >
</ div >
</ div >
< div class = "bg-white/5 rounded-lg p-3" >
< div class = "text-xs text-white/50 mb-1" > Sync </ div >
< div class = "flex items-center gap-2" >
< div class = "w-2 h-2 rounded-full" : class = "{
'bg-green-400': dwnSyncStatus === 'synced',
'bg-yellow-400 animate-pulse': dwnSyncStatus === 'syncing',
'bg-red-400': dwnSyncStatus === 'error',
'bg-white/30': dwnSyncStatus === 'idle'
}" ></ div >
< span class = "text-sm text-white font-medium capitalize" >{{ dwnSyncStatus }}</ span >
</ div >
</ div >
< div class = "bg-white/5 rounded-lg p-3" >
< div class = "text-xs text-white/50 mb-1" > Storage </ div >
< span class = "text-sm text-white font-medium" >{{ formatDwnStorage }}</ span >
</ div >
< div class = "bg-white/5 rounded-lg p-3" >
< div class = "text-xs text-white/50 mb-1" > Messages </ div >
< span class = "text-sm text-white font-medium" >{{ dwnStatus ? . message_count ?? 0 }}</ span >
</ div >
</ div >
<!-- Protocols -->
< div class = "mb-4" >
< div class = "flex items-center justify-between mb-2" >
< div class = "text-xs text-white/50" > Registered Protocols ({{ dwnProtocols . length }})</ div >
< button @click ="showRegisterProtocol = !showRegisterProtocol" class = "text-xs text-blue-400 hover:text-blue-300 transition-colors" >
{{ showRegisterProtocol ? 'Cancel' : '+ Register' }}
</ button >
</ div >
< div v-if = "showRegisterProtocol" class="bg-white/5 rounded-lg p-3 mb-3" >
< div class = "flex gap-2 items-end" >
< div class = "flex-1" >
< label class = "text-xs text-white/50 block mb-1" > Protocol URI </ label >
< input v-model = "newProtocolUri" type="text" placeholder="https://example.com/protocol" class="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-1.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-blue-500/50" />
</ div >
< label class = "flex items-center gap-1.5 text-xs text-white/60 cursor-pointer whitespace-nowrap pb-1.5" >
< input v-model = "newProtocolPublished" type="checkbox" class="rounded bg-black/30 border-white/20" />
Published
</ label >
< button @click ="registerDwnProtocol" : disabled = "registeringProtocol || !newProtocolUri.trim()" class = "glass-button glass-button-sm px-3 rounded-lg text-xs font-medium disabled:opacity-50 whitespace-nowrap" >
{{ registeringProtocol ? 'Registering...' : 'Register' }}
</ button >
</ div >
</ div >
< div v-if = "dwnProtocols.length" class="flex flex-wrap gap-2" >
< div v-for = "proto in dwnProtocols" :key="proto.protocol" class="flex items-center gap-1.5 px-2 py-1 rounded-md bg-blue-500/15 border border-blue-500/20 text-xs text-blue-300 group" >
< span >{{ proto . protocol }}</ span >
< span v-if = "proto.published" class="text-green-400/60" title="Published" > &# x2022 ; </ span >
< button @click ="removeDwnProtocol(proto.protocol)" : disabled = "removingProtocol === proto.protocol" class = "opacity-0 group-hover:opacity-100 text-red-400/60 hover:text-red-400 transition-all ml-1" title = "Remove" >
& times ;
</ button >
</ div >
</ div >
< div v-else class = "text-xs text-white/30 italic" > No protocols registered </ div >
</ div >
<!-- Sync Targets -->
< div v-if = "dwnStatus?.peer_sync_targets?.length" class="mb-4" >
< div class = "text-xs text-white/50 mb-2" > Peer Sync Targets </ div >
< div class = "space-y-1" >
< div v-for = "target in dwnStatus.peer_sync_targets" :key="target" class="flex items-center gap-2 text-xs text-white/70 bg-white/5 rounded-lg px-3 py-2" >
< svg class = "w-3 h-3 text-green-400 flex-shrink-0" fill = "currentColor" viewBox = "0 0 24 24" >< circle cx = "12" cy = "12" r = "4" /></ svg >
< span class = "truncate font-mono" >{{ target }}</ span >
</ div >
</ div >
</ div >
<!-- Messages Browser -->
< div class = "mb-4" >
< div class = "flex items-center justify-between mb-2" >
< div class = "text-xs text-white/50" > Messages </ div >
< button @click ="toggleDwnMessages" class = "text-xs text-blue-400 hover:text-blue-300 transition-colors" >
{{ showDwnMessages ? 'Hide' : 'Browse' }}
</ button >
</ div >
< div v-if = "showDwnMessages" >
2026-06-11 00:24:40 -04:00
< div v-if = "loadingDwnMessages && dwnMessages.length === 0" class="text-xs text-white/40 py-4 text-center" > Loading messages... </ div >
2026-03-21 02:43:28 +00:00
< div v-else-if = "dwnMessages.length === 0" class="text-xs text-white/30 italic py-2" > No messages stored </ div >
< div v-else class = "space-y-2 max-h-64 overflow-y-auto" >
2026-06-11 00:24:40 -04:00
< div v-if = "loadingDwnMessages" class="p-2 text-center text-white/45 text-xs flex items-center justify-center gap-2" >
< svg class = "animate-spin h-3.5 w-3.5" 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 >
Refreshing messages ...
</ div >
2026-03-21 02:43:28 +00:00
< div v-for = "msg in dwnMessages" :key="msg.record_id" class="bg-white/5 rounded-lg p-3" >
< div class = "flex items-center justify-between mb-1" >
< span class = "text-xs font-mono text-white/50 truncate max-w-[200px]" :title = "msg.record_id" >{{ ( msg . record_id || '' ). slice ( 0 , 8 ) }}...</ span >
< span class = "text-xs text-white/40" >{{ new Date ( msg . date_created ). toLocaleString () }}</ span >
</ div >
< div class = "flex flex-wrap gap-2 text-xs" >
< span class = "text-white/70" >{{ msg . author }}</ span >
< span v-if = "msg.descriptor.protocol" class="text-blue-300/80" >{{ msg.descriptor.protocol }}</ span >
< span v-if = "msg.descriptor.schema" class="text-purple-300/80" >{{ msg.descriptor.schema }}</ span >
</ div >
< div v-if = "msg.data" class="mt-1 text-xs text-white/40 font-mono truncate" >{{ JSON.stringify ( msg.data ) .slice ( 0 , 120 ) }}</ div >
</ div >
</ div >
</ div >
</ div >
<!-- Last Sync & Actions -- >
< div class = "flex items-center justify-between pt-3 border-t border-white/10" >
< div class = "text-xs text-white/40" >
{{ dwnStatus ? . last_sync ? `Last sync: ${ new Date ( dwnStatus . last_sync ). toLocaleString () } ` : 'Never synced' }}
</ div >
< button @click ="syncDWNs" : disabled = "syncingDWNs || !dwnStatus?.running" class = "glass-button glass-button-sm px-3 rounded-lg text-sm font-medium flex items-center gap-2 disabled:opacity-50" >
< svg class = "w-4 h-4" : class = "{ 'animate-spin': syncingDWNs }" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</ svg >
{{ syncingDWNs ? t ( 'web5.syncing' ) : t ( 'web5.syncNow' ) }}
</ button >
</ div >
</ template >
</ div >
</ template >
< script setup lang = "ts" >
import { ref , computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { rpcClient } from '@/api/rpc-client'
import { useAppStore } from '@/stores/app'
import { PackageState } from '@/types/api'
import type { DwnStatusData , DwnProtocol , DwnMessageEntry } from './types'
const { t } = useI18n ()
const appStore = useAppStore ()
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 [] >([])
const showDwnMessages = ref ( false )
const loadingDwnMessages = ref ( false )
const showRegisterProtocol = ref ( false )
const newProtocolUri = ref ( '' )
const newProtocolPublished = ref ( false )
const registeringProtocol = ref ( false )
const removingProtocol = ref < string | null >( null )
const formatDwnStorage = computed (() => {
if ( ! dwnStatus . value ) return '0 B'
const bytes = dwnStatus . value . storage_bytes
if ( bytes < 1024 ) return ` ${ bytes } B`
if ( bytes < 1024 * 1024 ) return ` ${ ( bytes / 1024 ). toFixed ( 1 ) } KB`
if ( bytes < 1024 * 1024 * 1024 ) return ` ${ ( bytes / ( 1024 * 1024 )). toFixed ( 1 ) } MB`
return ` ${ ( bytes / ( 1024 * 1024 * 1024 )). toFixed ( 2 ) } GB`
})
async function loadDwnStatus () {
try {
const res = await rpcClient . call < DwnStatusData >({ method : 'dwn.status' })
dwnStatus . value = res
dwnSyncStatus . value = ( res . sync_status as 'synced' | 'syncing' | 'error' | 'idle' ) || 'idle'
} catch {
dwnStatus . value = null
dwnSyncStatus . value = 'idle'
}
}
async function syncDWNs () {
syncingDWNs . value = true
dwnSyncStatus . value = 'syncing'
try {
const res = await rpcClient . call < { sync_status : string ; last_sync : string ; messages_synced : number } > ({ method : 'dwn.sync' })
dwnSyncStatus . value = ( res . sync_status as 'synced' | 'syncing' | 'error' | 'idle' ) || 'synced'
await loadDwnStatus ()
} catch {
dwnSyncStatus . value = 'error'
} finally {
syncingDWNs . value = false
}
}
async function loadDwnProtocols () {
try {
const res = await rpcClient . call < { protocols : DwnProtocol [] } > ({ method : 'dwn.list-protocols' })
dwnProtocols . value = res . protocols || []
} catch {
dwnProtocols . value = []
}
}
async function registerDwnProtocol () {
if ( registeringProtocol . value || ! newProtocolUri . value . trim ()) return
registeringProtocol . value = true
try {
await rpcClient . call ({ method : 'dwn.register-protocol' , params : { protocol : newProtocolUri . value . trim (), published : newProtocolPublished . value } })
newProtocolUri . value = ''
newProtocolPublished . value = false
showRegisterProtocol . value = false
await loadDwnProtocols ()
await loadDwnStatus ()
} catch {
if ( import . meta . env . DEV ) console . error ( 'Failed to register protocol' )
} finally {
registeringProtocol . value = false
}
}
async function removeDwnProtocol ( protocol : string ) {
removingProtocol . value = protocol
try {
await rpcClient . call ({ method : 'dwn.remove-protocol' , params : { protocol } })
await loadDwnProtocols ()
await loadDwnStatus ()
} catch {
if ( import . meta . env . DEV ) console . error ( 'Failed to remove protocol' )
} finally {
removingProtocol . value = null
}
}
async function toggleDwnMessages () {
showDwnMessages . value = ! showDwnMessages . value
if ( showDwnMessages . value ) {
await loadDwnMessages ()
}
}
async function loadDwnMessages () {
2026-06-11 00:24:40 -04:00
const hadMessages = dwnMessages . value . length > 0
2026-03-21 02:43:28 +00:00
loadingDwnMessages . value = true
try {
const res = await rpcClient . call < { messages : DwnMessageEntry []; count : number } > ({ method : 'dwn.query-messages' , params : { limit : 50 } })
dwnMessages . value = res . messages || []
} catch {
2026-06-11 00:24:40 -04:00
if ( ! hadMessages ) dwnMessages . value = []
2026-03-21 02:43:28 +00:00
} finally {
loadingDwnMessages . value = false
}
}
2026-06-11 00:24:40 -04:00
defineExpose ({ loadDwnStatus , loadDwnProtocols , loadDwnMessages , dwnMessages , showDwnMessages })
2026-03-21 02:43:28 +00:00
</ script >