bug fixes from sxsw

This commit is contained in:
Dorian
2026-03-14 17:12:41 +00:00
parent dfffa8606d
commit ee15fbc457
50 changed files with 1635 additions and 543 deletions
+4 -4
View File
@@ -417,7 +417,7 @@
class="fixed inset-0 z-50 flex items-center justify-center p-4"
@click="closeUninstallModal()"
>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="absolute inset-0 bg-black/10 backdrop-blur-md"></div>
<div
ref="uninstallModalRef"
@click.stop
@@ -486,7 +486,6 @@ const appId = computed(() => route.params.id as string)
// Web-only app detection (no container — external websites)
const WEB_ONLY_APP_URLS: Record<string, string> = {
'indeedhub': 'https://archipelago.indeehub.studio',
'botfights': 'https://botfights.net',
'nwnn': 'https://nwnn.l484.com',
'484-kitchen': 'https://484.kitchen',
@@ -528,6 +527,7 @@ const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
portainer: 'portainer',
'uptime-kuma': 'uptime-kuma',
tailscale: 'tailscale',
indeedhub: 'indeedhub',
}
/** Backend may register under variant container names */
@@ -740,8 +740,8 @@ function launchApp() {
prod: 'http://localhost:8103' // Self-hosted splash screen
},
'indeedhub': {
dev: 'https://archipelago.indeehub.studio',
prod: 'https://archipelago.indeehub.studio'
dev: 'http://localhost:8190',
prod: 'http://localhost:8190'
},
// Dummy apps - replace with real URLs when packaged
'bitcoin': {
+15 -17
View File
@@ -1,24 +1,28 @@
<template>
<div class="pb-6">
<div class="hidden md:flex items-start justify-between mb-8 gap-4">
<div>
<h1 class="text-3xl font-bold text-white mb-2">{{ t('apps.title') }}</h1>
<p class="text-white/70">{{ t('apps.subtitle') }}</p>
</div>
<!-- Desktop: tabs + search in one row -->
<div class="hidden md:flex items-center gap-4 mb-4">
<div class="mode-switcher flex-shrink-0">
<RouterLink to="/dashboard/apps" class="mode-switcher-btn mode-switcher-btn-active">My Apps</RouterLink>
<RouterLink to="/dashboard/marketplace" class="mode-switcher-btn">App Store</RouterLink>
</div>
</div>
<!-- Search Bar -->
<div class="mb-4">
<input
v-model="searchQuery"
type="text"
:placeholder="t('apps.searchPlaceholder')"
:aria-label="t('apps.searchLabel')"
class="w-full px-4 py-3 md:py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
class="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
/>
</div>
<!-- Mobile: search only (tabs are in Dashboard tab bar) -->
<div class="md:hidden mb-4">
<input
v-model="searchQuery"
type="text"
:placeholder="t('apps.searchPlaceholder')"
:aria-label="t('apps.searchLabel')"
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
/>
</div>
@@ -175,7 +179,7 @@
class="fixed inset-0 z-50 flex items-center justify-center p-4"
@click="closeUninstallModal()"
>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="absolute inset-0 bg-black/10 backdrop-blur-md"></div>
<div
ref="uninstallModalRef"
@click.stop
@@ -260,7 +264,6 @@ function showActionError(msg: string) {
// Web-only app IDs and their URLs
const WEB_ONLY_APP_URLS: Record<string, string> = {
'indeedhub': 'https://archipelago.indeehub.studio',
'botfights': 'https://botfights.net',
'nwnn': 'https://nwnn.l484.com',
'484-kitchen': 'https://484.kitchen',
@@ -276,11 +279,6 @@ function isWebOnlyApp(id: string): boolean {
// Web-only apps (no container) — always show as installed bookmarks
const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
'indeedhub': {
state: 'running' as PackageState,
manifest: { id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: { short: 'Bitcoin documentary streaming platform', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/indeehub.ico' },
},
'botfights': {
state: 'running' as PackageState,
manifest: { id: 'botfights', title: 'BotFights', version: '1.0.0', description: { short: 'AI bot arena — build, train, and battle autonomous agents', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
+2 -2
View File
@@ -69,9 +69,9 @@ const aiuiAvailable = ref<boolean | null>(null) // null = checking, true/false =
const aiuiUrl = computed(() => {
const envUrl = import.meta.env.VITE_AIUI_URL
if (envUrl) return `${envUrl}?embedded=true`
if (envUrl) return `${envUrl}?embedded=true&hideClose=true`
// In production, only return the URL if we've confirmed AIUI files exist
if (import.meta.env.PROD && aiuiAvailable.value === true) return `/aiui/?embedded=true&v=${Date.now()}`
if (import.meta.env.PROD && aiuiAvailable.value === true) return `/aiui/?embedded=true&hideClose=true&v=${Date.now()}`
return ''
})
+67 -31
View File
@@ -1,13 +1,5 @@
<template>
<div>
<div class="hidden md:block mb-8">
<div class="flex items-center justify-between mb-2">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Cloud</h1>
<p class="text-white/70">Your files, photos, and media</p>
</div>
</div>
</div>
<div class="pb-6">
<!-- Content Type Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -56,12 +48,48 @@
<span v-else-if="sectionCounts[section.id] !== undefined" class="text-white/30">{{ sectionCounts[section.id] }} items</span>
</div>
</div>
<!-- Peer Files Card -->
<!-- Individual Peer Cards -->
<div
v-for="peer in peerNodes"
:key="peer.did"
data-controller-container
tabindex="0"
class="glass-card p-6 cursor-pointer transition-all hover:-translate-y-1 hover:bg-white/10"
@click="router.push({ name: 'peer-files' })"
@click="router.push({ name: 'peer-files', params: { peerId: peer.onion } })"
>
<div class="flex items-center gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/15">
<svg class="w-7 h-7 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-white mb-0.5 truncate">{{ peer.name || truncateDid(peer.did) }}</h3>
<p class="text-xs text-white/50 truncate">{{ peer.onion }}</p>
</div>
<svg class="w-5 h-5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
<div class="flex items-center gap-2 text-xs">
<span
class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full"
:class="peer.trust_level === 'trusted' ? 'bg-green-500/15 text-green-400' : 'bg-purple-500/15 text-purple-400'"
>
<span class="w-1.5 h-1.5 rounded-full" :class="peer.trust_level === 'trusted' ? 'bg-green-400' : 'bg-purple-400'"></span>
{{ peer.trust_level }}
</span>
<span class="text-white/30">Peer Node</span>
</div>
</div>
<!-- No Peers placeholder (only if no peers found) -->
<div
v-if="!peersLoading && peerNodes.length === 0"
data-controller-container
tabindex="0"
class="glass-card p-6 cursor-pointer transition-all hover:-translate-y-1 hover:bg-white/10"
@click="router.push('/dashboard/server/federation')"
>
<div class="flex items-center gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/15">
@@ -71,26 +99,17 @@
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-white mb-0.5 truncate">Peer Files</h3>
<p class="text-xs text-white/50">Browse files shared by federated nodes</p>
<p class="text-xs text-white/50">Set up federation to share files with peers</p>
</div>
<svg class="w-5 h-5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
<div class="flex items-center gap-2 text-xs">
<template v-if="hasFederatedPeers">
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-purple-500/15 text-purple-400">
<span class="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
{{ peerCount }} peers
</span>
</template>
<template v-else>
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-white/5 text-white/40">
<span class="w-1.5 h-1.5 rounded-full bg-white/30"></span>
No peers yet
</span>
<span class="text-white/30">Set up federation to share files</span>
</template>
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-white/5 text-white/40">
<span class="w-1.5 h-1.5 rounded-full bg-white/30"></span>
No peers yet
</span>
</div>
</div>
</div>
@@ -116,8 +135,17 @@ const router = useRouter()
const store = useAppStore()
const sectionCounts = ref<Record<string, number>>({})
const countsLoading = ref(false)
const peerCount = ref(0)
const hasFederatedPeers = computed(() => peerCount.value > 0)
interface PeerNode {
did: string
pubkey: string
onion: string
name?: string
trust_level: string
}
const peerNodes = ref<PeerNode[]>([])
const peersLoading = ref(true)
const APP_ALIASES: Record<string, string[]> = {
immich: ['immich_server', 'immich-server'],
@@ -224,18 +252,26 @@ async function loadCounts() {
onMounted(() => {
loadCounts()
loadPeerCount()
loadPeers()
})
async function loadPeerCount() {
async function loadPeers() {
peersLoading.value = true
try {
const result = await rpcClient.federationListNodes()
peerCount.value = result?.nodes?.length ?? 0
peerNodes.value = result?.nodes ?? []
} catch {
peerCount.value = 0
peerNodes.value = []
} finally {
peersLoading.value = false
}
}
function truncateDid(did: string): string {
if (did.length <= 24) return did
return did.slice(0, 16) + '...' + did.slice(-8)
}
function openSection(section: ContentSection) {
router.push({ name: 'cloud-folder', params: { folderId: section.id } })
}
+1 -1
View File
@@ -37,7 +37,7 @@
/>
</svg>
</div>
<div>
<div class="hidden md:block">
<h1 class="text-2xl font-bold text-white">{{ section?.name || 'Folder' }}</h1>
<p class="text-sm text-white/50">{{ section?.description }}</p>
</div>
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<div>
<div class="pb-6">
<div class="mb-8">
<div class="flex items-center gap-3 mb-2">
<button @click="$router.push('/dashboard/web5')" class="glass-button glass-button-sm px-3 py-1.5 text-sm">
@@ -115,7 +115,7 @@
<!-- Credential Detail Modal -->
<Teleport to="body">
<div v-if="selectedCredential" class="fixed inset-0 z-50 flex items-center justify-center p-4" @click.self="selectedCredential = null">
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="fixed inset-0 bg-black/10 backdrop-blur-md"></div>
<div class="relative glass-card p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-white">Credential Details</h3>
+15 -11
View File
@@ -52,10 +52,9 @@
aria-hidden="true"
/>
<!-- Background overlay - 0.3 opacity default, 0.8 opacity for Web5, Network, and Settings -->
<div
class="fixed inset-0 transition-opacity duration-500 pointer-events-none"
:class="showDarkOverlay ? 'bg-black/80' : 'bg-black/30'"
<!-- Background overlay - uniform 0.2 opacity -->
<div
class="fixed inset-0 pointer-events-none bg-black/20"
style="z-index: -5;"
/>
@@ -149,7 +148,7 @@
<main
id="main-content"
data-controller-zone="main"
class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece z-10"
class="flex-1 overflow-hidden relative pb-0 glass-piece z-10"
:class="{ 'glass-throw-main': showZoomIn }"
>
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
@@ -277,14 +276,16 @@
<div
v-else
:class="[
'px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto h-full',
'absolute inset-0 px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto',
needsMobileBackButtonSpace
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-24'
: 'pb-[calc(var(--mobile-tab-bar-height,_72px)+48px)] md:pb-24'
? 'mobile-scroll-pad-back'
: 'mobile-scroll-pad'
]"
:style="mobileTabPaddingTop ? { paddingTop: (mobileTabPaddingTop + 16) + 'px' } : undefined"
>
<component :is="Component" class="view-container" />
<component :is="Component" class="view-container flex-none" />
<!-- Bottom spacer scroll clearance on all pages -->
<div class="shrink-0 h-6 md:h-12" aria-hidden="true"></div>
</div>
</div>
</Transition>
@@ -846,6 +847,10 @@ function getTransitionName(currentRoute: RouteLocationNormalizedLoaded) {
} else if (wasAppDetails && isMarketplaceList) {
transitionName = 'depth-back'
}
// Desktop: no transition between Apps ↔ Marketplace (same-page tab feel)
else if ((wasAppsList && isMarketplaceList) || (wasMarketplaceList && isAppsList)) {
transitionName = 'fade'
}
// Vertical transition: between main tabs (desktop)
else {
const currentIndex = tabOrder.indexOf(currentPath)
@@ -1278,8 +1283,7 @@ aside:not(.sidebar-animate) .sidebar-logout-btn {
}
.view-container {
height: 100%;
min-height: 100%;
/* No forced height — content sizes naturally, spacer below provides clearance */
}
/* Forward transition: 2advanced fluid depth */
+3 -3
View File
@@ -1,5 +1,5 @@
<template>
<div>
<div class="pb-6">
<div class="mb-8">
<button
@click="router.push('/dashboard/web5')"
@@ -195,7 +195,7 @@
</template>
<!-- Node Detail Modal -->
<div v-if="selectedNode" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="selectedNode = null; confirmRemove = false">
<div v-if="selectedNode" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="selectedNode = null; confirmRemove = false">
<div class="glass-card p-6 w-full max-w-lg max-h-[80vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-white">Node Details</h2>
@@ -322,7 +322,7 @@
</div>
<!-- Join Modal -->
<div v-if="showJoinModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showJoinModal = false">
<div v-if="showJoinModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="showJoinModal = false">
<div class="glass-card p-6 w-full max-w-md">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-white">Join Federation</h2>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div>
<div class="pb-6">
<div class="mb-4 md:mb-8 flex items-start justify-between gap-4">
<div class="min-h-[4.5rem]">
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">
+1 -1
View File
@@ -109,7 +109,7 @@ const launchableApps = computed<KioskApp[]>(() => {
'fedimint-gateway': '/app/fedimint-gateway/',
'dwn': '/app/dwn/',
'nostr-rs-relay': '/app/nostr-rs-relay/',
'indeedhub': 'https://archipelago.indeehub.studio',
'indeedhub': 'http://localhost:8190',
'botfights': 'https://botfights.net',
'nwnn': 'https://nwnn.l484.com',
'484-kitchen': 'https://484.kitchen',
+7 -14
View File
@@ -74,20 +74,13 @@
</div>
</div>
<div class="hidden md:flex mb-8 items-start justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-white mb-2">{{ t('marketplace.title') }}</h1>
<p class="text-white/70">{{ t('marketplace.subtitle') }}</p>
</div>
<!-- Desktop: tabs + categories + search in one row -->
<div class="hidden md:flex mb-4 items-center gap-4">
<div class="mode-switcher flex-shrink-0">
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
<RouterLink to="/dashboard/marketplace" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
</div>
</div>
<!-- Category Tabs + Search (Desktop only) -->
<div class="hidden md:flex mb-6 items-center justify-between gap-4">
<div class="mode-switcher">
<div class="mode-switcher flex-shrink-0">
<button
v-for="category in categoriesWithApps"
:key="category.id"
@@ -104,7 +97,7 @@
type="text"
:placeholder="t('marketplace.searchPlaceholder')"
:aria-label="t('marketplace.searchApps')"
class="flex-shrink-0 w-64 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
class="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
/>
</div>
@@ -259,7 +252,7 @@
<Transition name="modal">
<div
v-if="showFilterModal"
class="fixed inset-0 z-50 flex items-end justify-center md:hidden bg-black/60 backdrop-blur-sm"
class="fixed inset-0 z-50 flex items-end justify-center md:hidden bg-black/10 backdrop-blur-md"
@click.self="closeFilterModal()"
>
<div ref="filterModalRef" class="glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto">
@@ -648,7 +641,6 @@ function isInstalled(appId: string): boolean {
/** Web-only apps — external URLs with no container */
const WEB_ONLY_APP_URLS: Record<string, string> = {
'indeedhub': 'https://archipelago.indeehub.studio',
'botfights': 'https://botfights.net',
'nwnn': 'https://nwnn.l484.com',
'484-kitchen': 'https://484.kitchen',
@@ -685,6 +677,7 @@ const APP_LAUNCH_URLS: Record<string, string> = {
'fedimint': 'http://localhost:8175',
'nostr-rs-relay': 'http://localhost:18081',
'dwn': 'http://localhost:3100',
'indeedhub': 'http://localhost:8190',
}
function launchInstalledApp(app: MarketplaceApp) {
@@ -995,7 +988,7 @@ function getCuratedAppList() {
id: 'indeedhub',
title: 'Indeehub',
version: '0.1.0',
description: 'Bitcoin documentary streaming platform. Stream God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology.',
description: 'Bitcoin documentary streaming platform with Nostr identity sign-in. Stream God Bless Bitcoin and other educational content about sovereignty and decentralized technology.',
icon: '/assets/img/app-icons/indeehub.ico',
author: 'Indeehub Team',
dockerImage: 'localhost/indeedhub:latest',
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div>
<div class="pb-6">
<div class="hidden md:block mb-8">
<div class="flex items-center justify-between">
<div>
+128 -126
View File
@@ -1,118 +1,92 @@
<template>
<div>
<div class="flex items-center gap-3 mb-6">
<button class="glass-button p-2 rounded-lg" @click="router.push({ name: 'cloud' })">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="pb-6">
<!-- Header with back button -->
<div class="shrink-0 mb-4">
<button @click="goBack" class="hidden md:flex mb-4 items-center gap-2 text-white/70 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Back to Cloud
</button>
<div>
<h1 class="text-2xl font-bold text-white">Peer Files</h1>
<p class="text-sm text-white/50">Browse files shared by federated nodes</p>
</div>
</div>
<!-- Peer list -->
<div v-if="!selectedPeer" class="space-y-3">
<div v-if="loading" class="glass-card p-8 text-center">
<p class="text-white/50 animate-pulse">Loading federation peers...</p>
</div>
<div v-else-if="peers.length === 0" class="glass-card p-8 text-center">
<p class="text-white/50">No federated peers found. Join a federation from Settings to share files.</p>
</div>
<div
v-for="peer in peers"
:key="peer.did"
data-controller-container
tabindex="0"
class="glass-card p-5 cursor-pointer transition-all hover:-translate-y-0.5 hover:bg-white/10"
@click="browsePeer(peer)"
>
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-purple-500/20 flex items-center justify-center">
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-white truncate">{{ peer.name || truncateDid(peer.did) }}</h3>
<p class="text-xs text-white/40 truncate">{{ peer.onion }}</p>
</div>
<div class="flex items-center gap-2">
<span
class="text-xs px-2 py-0.5 rounded-full"
:class="peer.trust_level === 'trusted' ? 'bg-green-500/15 text-green-400' : 'bg-yellow-500/15 text-yellow-400'"
>
{{ peer.trust_level }}
</span>
<svg class="w-4 h-4 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</div>
<!-- Peer content catalog -->
<div v-else>
<div class="flex items-center gap-3 mb-4">
<button class="glass-button p-2 rounded-lg" @click="selectedPeer = null">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<!-- Mobile Back Button -->
<Teleport to="body">
<button
@click="goBack"
class="md:hidden mobile-back-btn glass-button px-6 py-3 rounded-lg font-medium shadow-2xl flex items-center justify-center gap-2"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<span>Back to Cloud</span>
</button>
<div>
<h2 class="text-lg font-semibold text-white">{{ selectedPeer.name || truncateDid(selectedPeer.did) }}</h2>
<p class="text-xs text-white/40">{{ selectedPeer.onion }}</p>
</Teleport>
<!-- Peer Header -->
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/15">
<svg class="w-7 h-7 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
</div>
<div class="hidden md:block">
<h1 class="text-2xl font-bold text-white">{{ peerDisplayName }}</h1>
<p class="text-sm text-white/50">{{ currentPeer?.onion || 'Peer files' }}</p>
</div>
</div>
</div>
<div v-if="catalogLoading" class="glass-card p-8 text-center">
<p class="text-white/50 animate-pulse">Connecting via Tor... This may take a few seconds.</p>
</div>
<!-- Loading -->
<div v-if="loading" class="glass-card p-8 text-center">
<svg class="animate-spin h-6 w-6 text-purple-400 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" 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">Connecting via Tor... This may take a few seconds.</p>
</div>
<div v-else-if="catalogError" class="glass-card p-6">
<p class="text-red-400 text-sm">{{ catalogError }}</p>
<button class="glass-button mt-3 px-4 py-2 rounded-lg text-sm" @click="browsePeer(selectedPeer!)">Retry</button>
</div>
<!-- Error -->
<div v-else-if="catalogError" class="glass-card p-6">
<p class="text-red-400 text-sm mb-3">{{ catalogError }}</p>
<button class="glass-button px-4 py-2 rounded-lg text-sm" @click="loadCatalog">Retry</button>
</div>
<div v-else-if="catalogItems.length === 0" class="glass-card p-8 text-center">
<p class="text-white/50">This peer has no shared files.</p>
</div>
<!-- Empty -->
<div v-else-if="catalogItems.length === 0 && !loading" class="glass-card p-8 text-center">
<p class="text-white/50">This peer has no shared files.</p>
</div>
<div v-else class="space-y-2">
<div
v-for="item in catalogItems"
:key="item.id"
class="glass-card p-4 flex items-center gap-4"
>
<div class="flex-shrink-0 w-9 h-9 rounded-lg flex items-center justify-center" :class="fileIconBg(item.mime_type)">
<svg class="w-5 h-5" :class="fileIconColor(item.mime_type)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fileIconPath(item.mime_type)" />
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white truncate">{{ item.filename }}</p>
<p class="text-xs text-white/40">{{ formatSize(item.size_bytes) }} &middot; {{ item.mime_type }}</p>
</div>
<div class="flex items-center gap-2">
<span
class="text-xs px-2 py-0.5 rounded-full"
:class="accessBadgeClass(item.access)"
>
{{ accessLabel(item.access) }}
</span>
<button
v-if="canDownload(item.access)"
class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium"
:disabled="downloading === item.id"
@click="downloadFile(item)"
>
{{ downloading === item.id ? 'Downloading...' : 'Download' }}
</button>
</div>
<!-- File Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="item in catalogItems"
:key="item.id"
class="glass-card p-4 flex items-center gap-4"
>
<div class="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center" :class="fileIconBg(item.mime_type)">
<svg class="w-5 h-5" :class="fileIconColor(item.mime_type)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fileIconPath(item.mime_type)" />
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-white truncate">{{ item.filename }}</p>
<p class="text-xs text-white/40">{{ formatSize(item.size_bytes) }}</p>
</div>
<div class="flex items-center gap-2">
<span
class="text-xs px-2 py-0.5 rounded-full"
:class="accessBadgeClass(item.access)"
>
{{ accessLabel(item.access) }}
</span>
<button
v-if="canDownload(item.access)"
class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium"
:disabled="downloading === item.id"
@click="downloadFile(item)"
>
{{ downloading === item.id ? '...' : 'Download' }}
</button>
</div>
</div>
</div>
@@ -120,10 +94,14 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted, Teleport } from 'vue'
import { useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
const props = defineProps<{
peerId?: string
}>()
const router = useRouter()
interface PeerNode {
@@ -144,42 +122,65 @@ interface CatalogItem {
}
const loading = ref(true)
const peers = ref<PeerNode[]>([])
const selectedPeer = ref<PeerNode | null>(null)
const catalogLoading = ref(false)
const currentPeer = ref<PeerNode | null>(null)
const catalogError = ref('')
const catalogItems = ref<CatalogItem[]>([])
const downloading = ref<string | null>(null)
const peerDisplayName = computed(() => {
if (currentPeer.value?.name) return currentPeer.value.name
if (currentPeer.value?.did) return truncateDid(currentPeer.value.did)
return props.peerId ? truncateOnion(props.peerId) : 'Peer Files'
})
function goBack() {
router.push({ name: 'cloud' })
}
onMounted(async () => {
try {
const result = await rpcClient.federationListNodes()
peers.value = result?.nodes ?? []
} catch {
peers.value = []
} finally {
if (props.peerId) {
// Find the peer by onion address
try {
const result = await rpcClient.federationListNodes()
const peers = result?.nodes ?? []
currentPeer.value = peers.find((p: PeerNode) => p.onion === props.peerId) || null
} catch {
// Continue with just the onion address
}
await loadCatalog()
} else {
loading.value = false
}
})
async function loadCatalog() {
const onion = props.peerId || currentPeer.value?.onion
if (!onion) return
loading.value = true
catalogError.value = ''
catalogItems.value = []
try {
const result = await rpcClient.call<{ items?: CatalogItem[] }>({
method: 'content.browse-peer',
params: { onion },
timeout: 30000,
})
catalogItems.value = result?.items ?? []
} catch (e: unknown) {
catalogError.value = e instanceof Error ? e.message : 'Failed to connect to peer'
} finally {
loading.value = false
}
}
function truncateDid(did: string): string {
if (did.length <= 24) return did
return did.slice(0, 16) + '...' + did.slice(-8)
}
async function browsePeer(peer: PeerNode) {
selectedPeer.value = peer
catalogLoading.value = true
catalogError.value = ''
catalogItems.value = []
try {
const result = await rpcClient.call<{ items?: CatalogItem[] }>({ method: 'content.browse-peer', params: { onion: peer.onion }, timeout: 30000 })
catalogItems.value = result?.items ?? []
} catch (e: unknown) {
catalogError.value = e instanceof Error ? e.message : 'Failed to connect to peer'
} finally {
catalogLoading.value = false
}
function truncateOnion(onion: string): string {
if (onion.length <= 20) return onion
return onion.slice(0, 12) + '...'
}
function formatSize(bytes: number): string {
@@ -231,12 +232,13 @@ function canDownload(access: CatalogItem['access']): boolean {
}
async function downloadFile(item: CatalogItem) {
if (!selectedPeer.value) return
const onion = props.peerId || currentPeer.value?.onion
if (!onion) return
downloading.value = item.id
try {
const result = await rpcClient.call<{ data?: string }>({
method: 'content.download-peer',
params: { onion: selectedPeer.value.onion, content_id: item.id },
params: { onion, content_id: item.id },
timeout: 120000,
})
if (result?.data) {
+31 -34
View File
@@ -1,10 +1,5 @@
<template>
<div>
<div class="hidden md:block mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Network</h1>
<p class="text-white/70">Manage your network infrastructure and Web3 services</p>
<p class="text-sm text-white/60 mt-2">{{ connectedNodes }} connected nodes</p>
</div>
<div class="pb-6">
<!-- Disk Space Warning Banner -->
<div
@@ -53,7 +48,7 @@
</div>
<button
@click="restartServices"
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"
class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50 flex items-center justify-center"
:disabled="restarting"
>
{{ restarting ? 'Restarting...' : 'Restart' }}
@@ -74,7 +69,7 @@
</div>
<button
@click="checkConnectivity"
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"
class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50 flex items-center justify-center"
:disabled="checkingConnectivity"
>
{{ checkingConnectivity ? 'Checking...' : 'Check' }}
@@ -83,25 +78,27 @@
<!-- Auto-Sync Toggle -->
<div data-controller-container tabindex="0" class="flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0">
<div class="flex items-center gap-3 min-w-0">
<svg class="w-5 h-5 text-white/60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
<div class="min-w-0">
<p class="text-sm font-medium text-white">Auto-Sync</p>
<p class="text-xs text-white/60">{{ autoSyncEnabled ? 'Enabled' : 'Disabled' }}</p>
<div class="flex items-center justify-between min-w-0">
<div class="flex items-center gap-3 min-w-0">
<svg class="w-5 h-5 text-white/60 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
<div class="min-w-0">
<p class="text-sm font-medium text-white">Auto-Sync</p>
<p class="text-xs text-white/60">{{ autoSyncEnabled ? 'Enabled' : 'Disabled' }}</p>
</div>
</div>
<button
@click="toggleAutoSync"
class="relative inline-flex h-8 w-14 items-center rounded-full transition-colors shrink-0"
:class="autoSyncEnabled ? 'bg-green-500' : 'bg-white/20'"
>
<span
class="inline-block h-6 w-6 transform rounded-full bg-white transition-transform shadow"
:class="autoSyncEnabled ? 'translate-x-7' : 'translate-x-1'"
/>
</button>
</div>
<button
@click="toggleAutoSync"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors self-start"
:class="autoSyncEnabled ? 'bg-green-500' : 'bg-white/20'"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="autoSyncEnabled ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<!-- Logs & Diagnostics -->
@@ -117,7 +114,7 @@
</div>
<button
@click="viewLogs"
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"
class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center"
>
View
</button>
@@ -128,7 +125,7 @@
<!-- Overview Cards -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Local Network Card -->
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col h-full min-h-0">
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col">
<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">
@@ -220,13 +217,13 @@
</template>
</div>
<button disabled title="Coming Soon" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed shrink-0">
<button disabled title="Coming Soon" class="mt-4 w-full min-h-[44px] glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed flex items-center justify-center">
Manage Local Network
</button>
</div>
<!-- Web3 Card -->
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col h-full min-h-0">
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col">
<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">
@@ -281,7 +278,7 @@
</div>
</div>
<button disabled title="Coming Soon" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed shrink-0">
<button disabled title="Coming Soon" class="mt-4 w-full min-h-[44px] glass-button rounded-lg text-sm font-medium opacity-50 cursor-not-allowed flex items-center justify-center">
Manage Web3 Services
</button>
</div>
@@ -379,7 +376,7 @@
</div>
<!-- WiFi Scan Modal -->
<div v-if="showWifiModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="showWifiModal = false">
<div v-if="showWifiModal" class="fixed inset-0 bg-black/10 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="showWifiModal = false">
<div class="glass-card p-6 w-full max-w-md">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-white">WiFi Networks</h3>
@@ -439,7 +436,7 @@
</div>
<!-- DNS Configuration Modal -->
<div v-if="showDnsModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="showDnsModal = false">
<div v-if="showDnsModal" class="fixed inset-0 bg-black/10 backdrop-blur-md z-50 flex items-center justify-center p-4" @click.self="showDnsModal = false">
<div class="glass-card p-6 w-full max-w-md">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-white">DNS Configuration</h3>
@@ -822,7 +819,7 @@ function copyTorAddress(address: string) {
async function toggleTorApp(appId: string, enabled: boolean) {
try {
await rpcClient.call({ method: 'tor.toggle-service', params: { name: appId, enabled } })
await rpcClient.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled } })
await loadTorServices()
} catch (e) {
if (import.meta.env.DEV) console.warn('Failed to toggle Tor app:', e)
@@ -832,7 +829,7 @@ async function toggleTorApp(appId: string, enabled: boolean) {
async function rotateNodeAddress() {
torRotating.value = true
try {
await rpcClient.call({ method: 'tor.rotate-address', params: { name: 'archipelago' } })
await rpcClient.call({ method: 'tor.rotate-service', params: { name: 'archipelago' } })
await loadTorServices()
} catch (e) {
if (import.meta.env.DEV) console.warn('Failed to rotate Tor address:', e)
+31 -41
View File
@@ -1,14 +1,8 @@
<template>
<div>
<div class="mb-8 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">{{ t('settings.title') }}</h1>
<p class="text-white/80">{{ t('settings.subtitle') }}</p>
</div>
<!-- Controller indicator - Mobile only (desktop shows in sidebar) -->
<div class="md:hidden">
<ControllerIndicator />
</div>
<div class="pb-6">
<!-- Controller indicator - Mobile only (desktop shows in sidebar) -->
<div class="md:hidden mb-4">
<ControllerIndicator />
</div>
<!-- Account Section -->
@@ -102,26 +96,24 @@
<!-- Tor / Onion Address (below DID, with copy button) -->
<div v-if="serverTorAddress" :class="userDid ? 'pt-4 border-t border-white/10' : ''">
<div class="flex items-center justify-between gap-2 mb-2">
<div class="flex items-center gap-3 min-w-0">
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.onionAddress') }}</p>
</div>
<button
@click="copyOnionAddress"
class="shrink-0 px-3 py-1.5 rounded-lg glass-button glass-button-sm text-xs font-medium text-white/90 hover:text-white transition-colors flex items-center gap-1.5"
>
<svg v-if="!copiedOnion" 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 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>
<span v-else class="text-green-400 text-xs">{{ t('common.copied') }}</span>
<span v-if="!copiedOnion">{{ t('common.copy') }}</span>
</button>
<div class="flex items-center gap-3 mb-2">
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.onionAddress') }}</p>
</div>
<p class="text-sm font-mono text-amber-400/90 break-all" :title="serverTorAddress">{{ serverTorAddress }}</p>
<p class="text-xs text-white/50 mt-1">{{ t('settings.onionHelper') }}</p>
<p class="text-sm font-mono text-amber-400/90 break-all mb-1" :title="serverTorAddress">{{ serverTorAddress }}</p>
<p class="text-xs text-white/50 mb-3">{{ t('settings.onionHelper') }}</p>
<button
@click="copyOnionAddress"
class="w-full min-h-[44px] rounded-lg glass-button text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<svg v-if="!copiedOnion" 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 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>
<span v-if="!copiedOnion">{{ t('common.copy') }}</span>
<span v-else class="text-green-400">{{ t('common.copied') }}</span>
</button>
</div>
</div>
</div>
@@ -143,7 +135,7 @@
<Teleport to="body">
<div
v-if="showChangePasswordModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
@click.self="closeChangePasswordModal()"
>
<div ref="changePasswordModalRef" class="glass-card p-6 max-w-md w-full">
@@ -255,7 +247,7 @@
<Teleport to="body">
<div
v-if="showTotpSetupModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
@click.self="closeTotpSetup"
@keydown.escape="closeTotpSetup"
>
@@ -361,7 +353,7 @@
<Teleport to="body">
<div
v-if="showTotpDisableModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
@click.self="closeTotpDisable"
@keydown.escape="closeTotpDisable"
>
@@ -507,7 +499,7 @@
<Teleport to="body">
<div
v-if="showClaudeLoginModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md"
@click.self="showClaudeLoginModal = false"
>
<div class="glass-card p-0 max-w-lg w-full overflow-hidden" style="height: 480px">
@@ -723,12 +715,10 @@
<!-- Backup & Restore Section -->
<div class="glass-card px-6 py-6 mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-xl font-semibold text-white/96">{{ t('settings.backup') }}</h2>
<p class="text-sm text-white/60 mt-1">{{ t('settings.backupRestoreDesc') }}</p>
</div>
<button @click="showCreateBackupModal = true" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<div class="mb-4">
<h2 class="text-xl font-semibold text-white/96 mb-1">{{ t('settings.backup') }}</h2>
<p class="text-sm text-white/60 mb-3">{{ t('settings.backupRestoreDesc') }}</p>
<button @click="showCreateBackupModal = true" class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium flex items-center justify-center gap-2">
<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="M12 4v16m8-8H4" />
</svg>
@@ -770,7 +760,7 @@
<!-- Create Backup Modal -->
<Teleport to="body">
<div v-if="showCreateBackupModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showCreateBackupModal = false">
<div v-if="showCreateBackupModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="showCreateBackupModal = false">
<div class="glass-card p-6 w-full max-w-md" role="dialog" aria-modal="true" aria-labelledby="create-backup-title">
<h3 id="create-backup-title" class="text-lg font-semibold text-white mb-4">{{ t('settings.createEncryptedBackup') }}</h3>
<div class="space-y-3">
@@ -795,7 +785,7 @@
<!-- Restore Backup Modal -->
<Teleport to="body">
<div v-if="showRestoreModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showRestoreModal = false">
<div v-if="showRestoreModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="showRestoreModal = false">
<div class="glass-card p-6 w-full max-w-md" role="dialog" aria-modal="true" aria-labelledby="restore-backup-title">
<h3 id="restore-backup-title" class="text-lg font-semibold text-white mb-2">{{ t('settings.restoreBackupTitle') }}</h3>
<p class="text-sm text-red-400/80 mb-4">{{ t('settings.restoreWarning') }}</p>
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<div>
<div class="pb-6">
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">{{ t('systemUpdate.title') }}</h1>
<p class="text-white/70">{{ t('systemUpdate.subtitle') }}</p>
@@ -166,7 +166,7 @@
<!-- Confirmation modal -->
<Transition name="fade">
<div v-if="confirmAction" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="cancelConfirm">
<div v-if="confirmAction" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="cancelConfirm">
<div class="glass-card p-6 max-w-sm w-full mx-4">
<h3 class="text-lg font-semibold text-white mb-3">
{{ confirmAction === 'apply' ? t('systemUpdate.applyTitle') : t('systemUpdate.rollbackTitle') }}
+177 -70
View File
@@ -1,14 +1,9 @@
<template>
<div>
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">{{ t('web5.title') }}</h1>
<p class="text-white/70">{{ t('web5.subtitle') }}</p>
<p class="text-sm text-white/60 mt-2">{{ t('web5.profitsHelper') }}</p>
</div>
<div class="pb-6">
<!-- Quick Actions Container -->
<div class="glass-card p-6 mb-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 stagger-grid">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-6 gap-4 stagger-grid">
<!-- Networking Profits -->
<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: 0">
<div class="flex items-center gap-3 min-w-0">
@@ -188,8 +183,8 @@
<!-- DID Document Modal -->
<Teleport to="body">
<div v-if="showDidDocModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showDidDocModal = false" @keydown.escape="showDidDocModal = false">
<div class="glass-card p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="did-doc-title">
<div v-if="showDidDocModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="showDidDocModal = false" @keydown.escape="showDidDocModal = false">
<div class="glass-card p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="did-doc-title">
<div class="flex items-center justify-between mb-4">
<h3 id="did-doc-title" class="text-lg font-semibold text-white">{{ t('web5.didDocument') }}</h3>
<div class="flex items-center gap-2">
@@ -222,7 +217,7 @@
<!-- Send Message Modal -->
<Teleport to="body">
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="closeSendMessageModal()">
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="closeSendMessageModal()">
<div ref="sendMessageModalRef" class="glass-card p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<h3 class="text-lg font-semibold text-white mb-4">{{ t('web5.sendMessageTitle') }}</h3>
<p class="text-white/70 text-sm mb-4">Messages are sent over the Tor network to the selected peer.</p>
@@ -235,7 +230,7 @@
>
<option value="">{{ t('web5.selectPeer') }}</option>
<option v-for="p in peers" :key="p.pubkey" :value="p.onion">
{{ p.name || p.onion || p.pubkey.slice(0, 12) + '...' }}
{{ p.name || p.onion || (p.pubkey || '').slice(0, 12) + '...' }}
</option>
</select>
</div>
@@ -270,10 +265,10 @@
</div>
</Teleport>
<!-- Core Services Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Core Services Overview Cards Row 1 -->
<div class="flex flex-col md:flex-row gap-6 mb-6">
<!-- Bitcoin Domain Name Portfolio -->
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 0">
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 0">
<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">
@@ -320,13 +315,13 @@
</div>
</div>
<button @click="showDomainsModal = true" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
<button @click="showDomainsModal = true" class="mt-6 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
{{ t('web5.manageDomains') }}
</button>
</div>
<!-- Wallet -->
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 1">
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 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">
@@ -389,9 +384,12 @@
</button>
</div>
</div>
</div>
<!-- Core Services Overview Cards Row 2 -->
<div class="flex flex-col md:flex-row gap-6 mb-8">
<!-- Nostr Relays -->
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 2">
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 2">
<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">
@@ -438,13 +436,13 @@
</div>
</div>
<button @click="showRelaysModal = true" class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
<button @click="showRelaysModal = true" class="mt-6 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
{{ t('web5.relays') }}
</button>
</div>
<!-- Node Visibility -->
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col h-full min-h-0" style="--stagger-index: 3">
<div data-controller-container tabindex="0" class="glass-card card-stagger p-6 flex flex-col md:w-1/2" style="--stagger-index: 3">
<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">
@@ -508,10 +506,12 @@
{{ t('web5.discoverableWarning') }}
</p>
</div>
</div>
<!-- Connected Nodes (P2P over Tor) -->
<div ref="nodesContainerRef" data-controller-container tabindex="0" class="glass-card p-6 lg:col-span-4 scroll-mt-24">
<div class="flex items-start gap-4 mb-4">
<!-- Connected Nodes (P2P over Tor) -->
<div ref="nodesContainerRef" data-controller-container tabindex="0" class="glass-card p-6 mb-8 scroll-mt-24">
<!-- Desktop: side-by-side layout -->
<div class="hidden md:flex items-start gap-4 mb-4">
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
@@ -536,6 +536,32 @@
</button>
</div>
</div>
<!-- Mobile: stacked layout -->
<div class="md:hidden mb-4">
<div class="flex items-center gap-4 mb-2">
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<h2 class="text-xl font-semibold text-white">{{ t('web5.connectedNodes') }}</h2>
</div>
<p class="text-white/70 text-sm mb-3">{{ t('web5.peerNodesDescription') }}</p>
<div class="grid grid-cols-2 gap-2">
<button
@click="router.push('/dashboard/server/federation')"
class="min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center"
>
{{ t('web5.findNodes') }}
</button>
<button
@click="loadPeers"
class="min-h-[44px] glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center"
>
{{ loadingPeers ? '...' : t('common.refresh') }}
</button>
</div>
</div>
<!-- Tabs: Peers | Messages | Requests -->
<div class="flex gap-1 mb-4 border-b border-white/10">
@@ -580,7 +606,7 @@
<div class="flex items-center gap-3 min-w-0">
<div class="w-2 h-2 rounded-full shrink-0" :class="peerReachable[p.onion] ? 'bg-green-400' : 'bg-amber-400'"></div>
<div class="min-w-0">
<p class="text-sm font-mono text-white/90 truncate">{{ p.name || p.onion || p.pubkey.slice(0, 16) + '...' }}</p>
<p class="text-sm font-mono text-white/90 truncate">{{ p.name || p.onion || (p.pubkey || '').slice(0, 16) + '...' }}</p>
<p class="text-xs text-white/50 truncate">{{ p.onion }}</p>
</div>
</div>
@@ -607,7 +633,7 @@
class="p-3 bg-white/5 rounded-lg border-l-2 border-orange-500/50"
>
<div class="flex items-center justify-between gap-2 mb-1">
<p class="text-xs font-mono text-white/60 truncate" :title="m.from_pubkey">{{ m.from_pubkey.slice(0, 16) }}...</p>
<p class="text-xs font-mono text-white/60 truncate" :title="m.from_pubkey">{{ (m.from_pubkey || '').slice(0, 16) }}...</p>
<span class="text-xs text-white/40 shrink-0">{{ formatMessageTime(m.timestamp) }}</span>
</div>
<p class="text-sm text-white/90 break-words">{{ m.message }}</p>
@@ -678,11 +704,11 @@
{{ loadingRequests ? t('common.loading') : t('web5.refreshRequests') }}
</button>
</div>
</div>
<!-- Shared Content -->
<div class="glass-card p-6 mb-8">
<div class="flex items-center justify-between mb-4">
<!-- Desktop: side-by-side -->
<div class="hidden md: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">
@@ -694,11 +720,34 @@
<p class="text-xs text-white/60">{{ t('web5.contentDesc') }}</p>
</div>
</div>
<div class="flex items-center gap-2">
<button v-if="contentTab === 'mine'" @click="loadContentItems" :disabled="contentLoading" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium">
<div v-if="contentTab === 'mine'" class="flex items-center gap-2">
<button @click="loadContentItems" :disabled="contentLoading" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium">
{{ contentLoading ? '...' : 'Refresh' }}
</button>
<button v-if="contentTab === 'mine'" @click="showAddContentModal = true" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium flex items-center gap-2">
<button @click="showAddContentModal = true" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium flex items-center gap-2">
<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="M12 4v16m8-8H4" />
</svg>
Add
</button>
</div>
</div>
<!-- Mobile: stacked -->
<div class="md:hidden mb-4">
<div class="flex items-center gap-3 mb-2">
<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="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z" />
</svg>
</div>
<h2 class="text-lg font-semibold text-white">{{ t('web5.content') }}</h2>
</div>
<p class="text-xs text-white/60 mb-3">{{ t('web5.contentDesc') }}</p>
<div v-if="contentTab === 'mine'" class="grid grid-cols-2 gap-2">
<button @click="loadContentItems" :disabled="contentLoading" class="glass-button min-h-[44px] rounded-lg text-sm font-medium flex items-center justify-center">
{{ contentLoading ? '...' : 'Refresh' }}
</button>
<button @click="showAddContentModal = true" class="glass-button min-h-[44px] rounded-lg text-sm font-medium flex items-center justify-center gap-2">
<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="M12 4v16m8-8H4" />
</svg>
@@ -822,7 +871,7 @@
>
<option value="">{{ t('web5.selectPeer') }}</option>
<option v-for="p in peers" :key="p.pubkey" :value="p.onion">
{{ p.name || p.onion || p.pubkey.slice(0, 12) + '...' }}
{{ p.name || p.onion || (p.pubkey || '').slice(0, 12) + '...' }}
</option>
</select>
<button
@@ -912,7 +961,7 @@
<!-- Content Streaming Player -->
<Teleport to="body">
<div v-if="streamingItem" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" @click.self="closePlayer" @keydown.escape="closePlayer">
<div v-if="streamingItem" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="closePlayer" @keydown.escape="closePlayer">
<div class="glass-card p-0 w-full max-w-2xl overflow-hidden" role="dialog" aria-modal="true">
<!-- Player Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-white/10">
@@ -984,7 +1033,7 @@
<!-- Add Content Modal -->
<Teleport to="body">
<div v-if="showAddContentModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showAddContentModal = false" @keydown.escape="showAddContentModal = false">
<div v-if="showAddContentModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showAddContentModal = false" @keydown.escape="showAddContentModal = false">
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="add-content-title">
<h2 id="add-content-title" class="text-lg font-bold text-white mb-4">{{ t('web5.addContentTitle') }}</h2>
<div class="space-y-4">
@@ -1035,7 +1084,8 @@
<!-- Identity Management -->
<div class="glass-card p-6 mb-8">
<div class="flex items-center justify-between mb-4">
<!-- Desktop: side-by-side -->
<div class="hidden md: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">
@@ -1054,6 +1104,24 @@
Create
</button>
</div>
<!-- Mobile: stacked -->
<div class="md:hidden mb-4">
<div class="flex items-center gap-3 mb-2">
<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="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
</svg>
</div>
<h2 class="text-lg font-semibold text-white">{{ t('web5.identities') }}</h2>
</div>
<p class="text-xs text-white/60 mb-3">{{ t('web5.identitiesDesc') }}</p>
<button @click="showCreateIdentityModal = true" class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium flex items-center justify-center gap-2">
<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="M12 4v16m8-8H4" />
</svg>
Create
</button>
</div>
<!-- Loading -->
<div v-if="identitiesLoading" class="py-6 text-center">
@@ -1112,7 +1180,7 @@
<!-- Actions -->
<div class="flex items-center gap-1 shrink-0">
<button @click="copyIdentityDid(identity.did)" class="p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="Copy DID">
<button @click="copyIdentityDid(identity.did)" class="p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="Copy">
<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>
@@ -1133,7 +1201,7 @@
</div>
<!-- Create Identity Modal -->
<div v-if="showCreateIdentityModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showCreateIdentityModal = false" @keydown.escape="showCreateIdentityModal = false">
<div v-if="showCreateIdentityModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showCreateIdentityModal = false" @keydown.escape="showCreateIdentityModal = false">
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="create-identity-title">
<h2 id="create-identity-title" class="text-lg font-bold text-white mb-4">{{ t('web5.createIdentityTitle') }}</h2>
<div class="space-y-4">
@@ -1167,7 +1235,7 @@
</div>
<!-- Delete Confirmation Modal -->
<div v-if="deleteIdentityTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="deleteIdentityTarget = null" @keydown.escape="deleteIdentityTarget = null">
<div v-if="deleteIdentityTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="deleteIdentityTarget = null" @keydown.escape="deleteIdentityTarget = null">
<div class="glass-card p-6 w-full max-w-sm mx-4" role="dialog" aria-modal="true" aria-labelledby="delete-identity-title">
<h2 id="delete-identity-title" class="text-lg font-bold text-white mb-2">{{ t('web5.deleteIdentityTitle') }}</h2>
<p class="text-white/60 text-sm mb-4">{{ t('web5.deleteIdentityConfirm') }}</p>
@@ -1180,7 +1248,7 @@
</div>
</div>
<!-- Unified Send Modal -->
<div v-if="showUnifiedSendModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedSendModal" @keydown.escape="closeUnifiedSendModal">
<div v-if="showUnifiedSendModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="closeUnifiedSendModal" @keydown.escape="closeUnifiedSendModal">
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="send-bitcoin-title">
<h2 id="send-bitcoin-title" class="text-lg font-bold text-white mb-4">{{ t('web5.sendBitcoinTitle') }}</h2>
@@ -1280,7 +1348,7 @@
</div>
<!-- Unified Receive Modal -->
<div v-if="showUnifiedReceiveModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeUnifiedReceiveModal" @keydown.escape="closeUnifiedReceiveModal">
<div v-if="showUnifiedReceiveModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="closeUnifiedReceiveModal" @keydown.escape="closeUnifiedReceiveModal">
<div class="glass-card p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true" aria-labelledby="receive-bitcoin-title">
<h2 id="receive-bitcoin-title" class="text-lg font-bold text-white mb-4">{{ t('web5.receiveBitcoinTitle') }}</h2>
@@ -1470,7 +1538,7 @@
<div v-else class="space-y-2 max-h-64 overflow-y-auto">
<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 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">
@@ -1501,7 +1569,8 @@
<!-- Verifiable Credentials -->
<div class="glass-card p-6 mb-8">
<div class="flex items-center justify-between mb-4">
<!-- Desktop: side-by-side -->
<div class="hidden md: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">
@@ -1517,6 +1586,21 @@
Manage
</router-link>
</div>
<!-- Mobile: stacked -->
<div class="md:hidden mb-4">
<div class="flex items-center gap-3 mb-2">
<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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h2 class="text-lg font-semibold text-white">{{ t('web5.verifiableCredentials') }}</h2>
</div>
<p class="text-xs text-white/60 mb-3">{{ t('web5.verifiableCredentialsDesc') }}</p>
<router-link to="/dashboard/web5/credentials" class="w-full min-h-[44px] glass-button rounded-lg text-sm font-medium flex items-center justify-center gap-2">
Manage
</router-link>
</div>
<!-- Stats -->
<div class="grid grid-cols-3 gap-3 mb-4">
@@ -1539,7 +1623,7 @@
<div v-for="vc in vcCredentials.slice(0, 3)" :key="vc.id" class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="min-w-0 flex-1">
<div class="text-sm text-white font-medium">{{ vc.type }}</div>
<div class="text-xs text-white/50 truncate">To: {{ vc.subject.slice(0, 30) }}...</div>
<div class="text-xs text-white/50 truncate">To: {{ (vc.subject || '').slice(0, 30) }}...</div>
</div>
<span :class="{
'text-green-400': vc.status === 'active',
@@ -1557,7 +1641,7 @@
</div>
<!-- Domains Management Modal -->
<div v-if="showDomainsModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showDomainsModal = false" @keydown.escape="showDomainsModal = false">
<div v-if="showDomainsModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showDomainsModal = false" @keydown.escape="showDomainsModal = false">
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="domains-title">
<div class="flex items-center justify-between mb-4">
<h2 id="domains-title" class="text-lg font-bold text-white">{{ t('web5.domainsTitle') }}</h2>
@@ -1604,7 +1688,7 @@
<label class="text-white/60 text-xs block mb-1">Link to Identity</label>
<select v-model="newDomainIdentityId" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-white/30">
<option value="" disabled>Select identity...</option>
<option v-for="id in managedIdentities" :key="id.id" :value="id.id">{{ id.name }} ({{ id.did.slice(0, 24) }}...)</option>
<option v-for="id in managedIdentities" :key="id.id" :value="id.id">{{ id.name }} ({{ (id.did || '').slice(0, 24) }}...)</option>
</select>
</div>
<div v-if="domainError" class="text-xs text-red-400 mb-2">{{ domainError }}</div>
@@ -1634,7 +1718,7 @@
</div>
<!-- Relay Management Modal -->
<div v-if="showRelaysModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showRelaysModal = false" @keydown.escape="showRelaysModal = false">
<div v-if="showRelaysModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="showRelaysModal = false" @keydown.escape="showRelaysModal = false">
<div class="glass-card p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="relays-title">
<div class="flex items-center justify-between mb-4">
<h2 id="relays-title" class="text-lg font-bold text-white">{{ t('web5.nostrRelays') }}</h2>
@@ -1908,18 +1992,27 @@ async function createDID() {
localStorage.setItem('neode_did', res.did)
} catch {
// Fallback: generate a did:key locally using Web Crypto
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign', 'verify']
)
const exported = await crypto.subtle.exportKey('raw', keyPair.publicKey)
const bytes = new Uint8Array(exported)
// Multicodec prefix for P-256 public key (0x1200) + base58btc
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
const did = `did:key:z${hex}`
storedDid.value = did
localStorage.setItem('neode_did', did)
if (!crypto.subtle) {
// crypto.subtle requires HTTPS generate random fallback
const randomBytes = new Uint8Array(32)
crypto.getRandomValues(randomBytes)
const hex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('')
const did = `did:key:z${hex}`
storedDid.value = did
localStorage.setItem('neode_did', did)
} else {
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign', 'verify']
)
const exported = await crypto.subtle.exportKey('raw', keyPair.publicKey)
const bytes = new Uint8Array(exported)
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
const did = `did:key:z${hex}`
storedDid.value = did
localStorage.setItem('neode_did', did)
}
} finally {
creatingDid.value = false
}
@@ -1965,7 +2058,7 @@ async function refreshDhtDid() {
async function copyDhtDid() {
if (!dhtDid.value) return
await navigator.clipboard.writeText(dhtDid.value)
await safeClipboardWrite(dhtDid.value)
dhtDidCopied.value = true
setTimeout(() => { dhtDidCopied.value = false }, 2000)
}
@@ -1977,7 +2070,7 @@ try {
async function copyDid() {
if (!userDid.value) return
await navigator.clipboard.writeText(userDid.value)
await safeClipboardWrite(userDid.value)
didCopied.value = true
setTimeout(() => { didCopied.value = false }, 2000)
}
@@ -2016,7 +2109,7 @@ async function showDidDocument() {
async function copyDidDocument() {
if (!didDocumentFormatted.value) return
await navigator.clipboard.writeText(didDocumentFormatted.value)
await safeClipboardWrite(didDocumentFormatted.value)
didDocCopied.value = true
setTimeout(() => { didDocCopied.value = false }, 2000)
}
@@ -2411,7 +2504,7 @@ async function finalizePsbt() {
function copyPsbt() {
if (!psbtData.value) return
window.navigator.clipboard.writeText(psbtData.value)
window.safeClipboardWrite(psbtData.value)
unifiedSendError.value = t('web5.psbtCopied')
}
@@ -2478,12 +2571,27 @@ async function unifiedReceive() {
}
function copyEcashToken(token: string) {
navigator.clipboard.writeText(token)
safeClipboardWrite(token)
showIdentityToast(t('web5.ecashTokenCopied'))
}
async function safeClipboardWrite(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
await safeClipboardWrite(text)
} else {
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}
}
function copyToClipboard(text: string, msg: string) {
navigator.clipboard.writeText(text)
safeClipboardWrite(text)
showIdentityToast(msg)
}
@@ -2711,7 +2819,7 @@ function downloadPeerContent(item: PeerContentItem) {
if (!browsePeerOnion.value) return
const url = `http://${browsePeerOnion.value}/content/${item.id}`
showIdentityToast(t('web5.downloadUrlCopied'))
navigator.clipboard.writeText(url)
safeClipboardWrite(url)
}
function closePlayer() {
@@ -2743,7 +2851,7 @@ function onPlayerError() {
function copyStreamUrl() {
if (streamUrl.value) {
navigator.clipboard.writeText(streamUrl.value)
safeClipboardWrite(streamUrl.value)
showIdentityToast(t('web5.streamUrlCopied'))
}
}
@@ -2832,8 +2940,8 @@ const settingVisibility = ref(false)
const visibilityOptions = [
{ value: 'hidden' as VisibilityLevel, label: 'Hidden', description: 'Your node is not discoverable by others' },
{ value: 'discoverable' as VisibilityLevel, label: 'Discoverable', description: 'Other Archipelago nodes can find you via Nostr' },
{ value: 'public' as VisibilityLevel, label: 'Public', description: 'Visible to everyone with your onion address published' },
{ value: 'discoverable' as VisibilityLevel, label: 'Discoverable', description: 'Federated peers can find and connect to your node' },
{ value: 'public' as VisibilityLevel, label: 'Public', description: 'Accepting connections from any Archipelago node' },
]
async function loadVisibility() {
@@ -2869,7 +2977,7 @@ async function setVisibility(level: VisibilityLevel) {
function copyOnionAddress() {
if (!nodeOnionAddress.value) return
navigator.clipboard.writeText(nodeOnionAddress.value)
safeClipboardWrite(nodeOnionAddress.value)
showIdentityToast(t('web5.onionAddressCopied'))
}
@@ -2928,7 +3036,7 @@ async function createIdentity() {
}
function copyIdentityDid(did: string) {
navigator.clipboard.writeText(did)
safeClipboardWrite(did)
showIdentityToast(t('web5.didCopied'))
}
@@ -3098,8 +3206,7 @@ async function connectWallet() {
}
function manageRelays() {
// TODO: Navigate to relay management or open modal
if (import.meta.env.DEV) console.log('Managing Nostr relays...')
showRelaysModal.value = true
}
</script>
@@ -119,7 +119,7 @@
</Transition>
<!-- Open Channel Modal -->
<div v-if="showOpenModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showOpenModal = false">
<div v-if="showOpenModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="showOpenModal = false">
<div class="glass-card p-6 w-full max-w-md mx-4">
<h2 class="text-lg font-bold text-white mb-4">Open Channel</h2>
@@ -165,7 +165,7 @@
</div>
<!-- Close Confirmation Modal -->
<div v-if="closeTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="closeTarget = null">
<div v-if="closeTarget" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="closeTarget = null">
<div class="glass-card p-6 w-full max-w-sm mx-4">
<h2 class="text-lg font-bold text-white mb-2">Close Channel?</h2>
<p class="text-white/60 text-sm mb-4">This will cooperatively close the channel with peer {{ closeTarget.remote_pubkey.slice(0, 16) }}...</p>