chore: snapshot release workspace
This commit is contained in:
@@ -105,7 +105,6 @@ import Web5NodeVisibility from './Web5NodeVisibility.vue'
|
||||
import Web5ConnectedNodes from './Web5ConnectedNodes.vue'
|
||||
// import Web5SharedContent from './Web5SharedContent.vue' // hidden for now
|
||||
import Web5Identities from './Web5Identities.vue'
|
||||
// import Web5DWN from './Web5DWN.vue' // hidden for now
|
||||
// import Web5CredentialsSummary from './Web5CredentialsSummary.vue' // hidden for now
|
||||
import Web5Monitoring from './Web5Monitoring.vue'
|
||||
import Web5Federation from './Web5Federation.vue'
|
||||
@@ -122,7 +121,6 @@ const nostrRelaysRef = ref<InstanceType<typeof Web5NostrRelays> | null>(null)
|
||||
const nodeVisibilityRef = ref<InstanceType<typeof Web5NodeVisibility> | null>(null)
|
||||
const connectedNodesRef = ref<InstanceType<typeof Web5ConnectedNodes> | null>(null)
|
||||
const identitiesRef = ref<InstanceType<typeof Web5Identities> | null>(null)
|
||||
// const dwnRef = ref(null) // hidden for now
|
||||
// const credentialsRef = ref(null) // hidden for now
|
||||
// const sharedContentRef = ref(null) // hidden for now
|
||||
// const sendReceiveRef = ref(null) // wallet hidden
|
||||
@@ -393,8 +391,6 @@ onMounted(() => {
|
||||
nodeVisibilityRef.value?.loadVisibility()
|
||||
// domainsRef.value?.loadDomainNames() // hidden for now
|
||||
nostrRelaysRef.value?.loadNostrRelays()
|
||||
// dwnRef.value?.loadDwnStatus() // hidden for now
|
||||
// dwnRef.value?.loadDwnProtocols() // hidden for now
|
||||
// credentialsRef.value?.loadCredentials() // hidden for now
|
||||
// sharedContentRef.value?.loadContentItems() // hidden for now
|
||||
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
<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">•</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">
|
||||
×
|
||||
</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">
|
||||
<div v-if="loadingDwnMessages && dwnMessages.length === 0" class="text-xs text-white/40 py-4 text-center">Loading messages...</div>
|
||||
<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">
|
||||
<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>
|
||||
<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() {
|
||||
const hadMessages = dwnMessages.value.length > 0
|
||||
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 {
|
||||
if (!hadMessages) dwnMessages.value = []
|
||||
} finally {
|
||||
loadingDwnMessages.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ loadDwnStatus, loadDwnProtocols, loadDwnMessages, dwnMessages, showDwnMessages })
|
||||
</script>
|
||||
@@ -1,94 +0,0 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Web5DWN from '../Web5DWN.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useSyncStore } from '@/stores/sync'
|
||||
import { PackageState } from '@/types/api'
|
||||
import type { DwnMessageEntry } from '../types'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: {
|
||||
call: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
function makeMessage(recordId: string): DwnMessageEntry {
|
||||
return {
|
||||
record_id: recordId,
|
||||
author: 'did:key:alice',
|
||||
date_created: '2026-06-10T10:00:00Z',
|
||||
descriptor: {
|
||||
interface: 'Records',
|
||||
method: 'Write',
|
||||
protocol: 'https://example.com/protocol',
|
||||
},
|
||||
data: { title: recordId },
|
||||
}
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res
|
||||
reject = rej
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
describe('Web5DWN', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
useSyncStore().data = {
|
||||
'package-data': {
|
||||
dwn: {
|
||||
state: PackageState.Running,
|
||||
manifest: { id: 'dwn', title: 'DWN' },
|
||||
'static-files': {},
|
||||
},
|
||||
},
|
||||
} as never
|
||||
})
|
||||
|
||||
it('keeps stored messages visible while refresh is pending or fails', async () => {
|
||||
vi.mocked(rpcClient.call).mockResolvedValueOnce({
|
||||
messages: [makeMessage('record-one')],
|
||||
count: 1,
|
||||
})
|
||||
|
||||
const wrapper = mount(Web5DWN, {
|
||||
global: {
|
||||
stubs: { RouterLink: true },
|
||||
},
|
||||
})
|
||||
|
||||
;(wrapper.vm as unknown as { showDwnMessages: boolean }).showDwnMessages = true
|
||||
await (wrapper.vm as unknown as { loadDwnMessages: () => Promise<void> }).loadDwnMessages()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('record-o')
|
||||
|
||||
const pending = deferred<{ messages: DwnMessageEntry[]; count: number }>()
|
||||
vi.mocked(rpcClient.call).mockReturnValueOnce(pending.promise)
|
||||
|
||||
const refresh = (wrapper.vm as unknown as { loadDwnMessages: () => Promise<void> }).loadDwnMessages()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('record-o')
|
||||
expect(wrapper.text()).toContain('Refreshing messages...')
|
||||
expect(wrapper.text()).not.toContain('Loading messages...')
|
||||
|
||||
pending.reject(new Error('offline'))
|
||||
await refresh
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('record-o')
|
||||
expect(wrapper.text()).not.toContain('Refreshing messages...')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user