chore: snapshot release workspace

This commit is contained in:
archipelago
2026-06-12 03:00:15 -04:00
parent 6a30ff11bd
commit d6f108d818
76 changed files with 792 additions and 3613 deletions
-4
View File
@@ -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
-284
View File
@@ -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">&#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">
<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...')
})
})