feat: Federation UI polish — modals, backgrounds, scroll, names, blocked

- Federation page uses bg-web5.jpg background
- Invite code in full-screen modal with type label (Link/Peer)
- Join modal upgraded to full-screen with backdrop blur
- "Untrusted" renamed to "Blocked" in trust selector
- Your Nodes / Peers containers: max-h-[60vh] with inner scroll
- Server name from Settings shown on DID card + network map
- DID sync between Web5 and Federation on rotation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-19 21:11:11 +00:00
parent 203b044646
commit 867e56cb84
2 changed files with 79 additions and 60 deletions

View File

@@ -435,6 +435,7 @@ const ROUTE_BACKGROUNDS: Record<string, string> = {
'/dashboard/mesh': 'bg-mesh.jpg',
'/dashboard/server': 'bg-network.jpg',
'/dashboard/web5': 'bg-web5.jpg',
'/dashboard/federation': 'bg-web5.jpg',
'/dashboard/settings': 'bg-settings.jpg',
'/dashboard/chat': 'bg-aiui.jpg',
}

View File

@@ -19,7 +19,7 @@
<div v-if="selfDid" class="hidden md:block shrink-0">
<div class="glass-card px-4 py-3 flex items-center gap-3">
<div class="min-w-0">
<p class="text-[10px] text-white/40 mb-0.5">Your Node</p>
<p class="text-[10px] text-white/40 mb-0.5">{{ appStore.serverName }}</p>
<p class="text-xs text-white/80 font-mono cursor-pointer" :title="selfDid" @click="copyDid">{{ didCopied ? 'Copied!' : shortDid(selfDid) }}</p>
</div>
<button @click="copyDid" class="glass-button px-2.5 py-1 rounded text-[10px]">{{ didCopied ? 'Copied!' : 'Copy' }}</button>
@@ -30,7 +30,7 @@
<!-- Mobile: DID below title -->
<div v-if="selfDid" class="md:hidden glass-card px-4 py-3 mt-3 flex items-center gap-3">
<div class="min-w-0 flex-1">
<p class="text-[10px] text-white/40 mb-0.5">Your Node</p>
<p class="text-[10px] text-white/40 mb-0.5">{{ appStore.serverName }}</p>
<p class="text-xs text-white/80 font-mono truncate cursor-pointer" :title="selfDid" @click="copyDid">{{ didCopied ? 'Copied!' : shortDid(selfDid) }}</p>
</div>
<button @click="copyDid" class="glass-button px-2.5 py-1 rounded text-[10px]">{{ didCopied ? 'Copied!' : 'Copy' }}</button>
@@ -163,21 +163,32 @@
</div>
</div>
<!-- Invite Code Display -->
<div v-if="inviteCode" class="glass-card p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Invite Code</h2>
<button @click="inviteCode = ''" class="text-white/40 hover:text-white/70 transition-colors text-sm">Dismiss</button>
</div>
<p class="text-xs text-white/60 mb-3">Share this code with the node you want to federate with. It can only be used once.</p>
<div class="bg-black/30 rounded-lg p-4 font-mono text-xs text-orange-300 break-all select-all">{{ inviteCode }}</div>
<button
@click="copyInviteCode"
class="mt-3 px-4 py-2 glass-button rounded text-sm text-white/90 hover:text-white transition-colors"
>
{{ copiedInvite ? 'Copied' : 'Copy to Clipboard' }}
</button>
</div>
<!-- Invite Code Modal -->
<Teleport to="body">
<Transition name="modal">
<div v-if="inviteCode" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click.self="inviteCode = ''">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="glass-card p-6 max-w-md w-full relative z-10">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">{{ inviteType === 'trusted' ? 'Link Your Nodes — Invite Code' : 'Peer Invite Code' }}</h2>
<button @click="inviteCode = ''" class="text-white/40 hover:text-white/70 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="text-xs text-white/60 mb-3">Share this code with the node you want to federate with. It can only be used once.</p>
<div class="bg-black/30 rounded-lg p-4 font-mono text-xs text-orange-300 break-all select-all">{{ inviteCode }}</div>
<button
@click="copyInviteCode"
class="mt-3 px-4 py-2 glass-button rounded text-sm text-white/90 hover:text-white transition-colors"
>
{{ copiedInvite ? 'Copied' : 'Copy to Clipboard' }}
</button>
</div>
</div>
</Transition>
</Teleport>
<!-- Sync Results -->
<div v-if="syncResults.length > 0" class="glass-card p-6 mb-6">
@@ -204,7 +215,7 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Your Nodes (Trusted) -->
<div class="glass-card p-6">
<div class="glass-card p-6 max-h-[60vh] flex flex-col">
<h2 class="text-lg font-semibold text-white mb-4">Your Nodes <span v-if="trustedNodes.length > 0" class="text-sm font-normal text-white/50">({{ trustedNodes.length }})</span></h2>
<div v-if="loading" class="flex items-center gap-3 py-8 justify-center">
@@ -220,7 +231,7 @@
<p class="text-white/30 text-xs">Generate an invite code or join an existing federation</p>
</div>
<div v-else class="space-y-3">
<div v-else class="space-y-3 overflow-y-auto">
<div
v-for="node in trustedNodes"
:key="node.did"
@@ -266,7 +277,7 @@
</div>
<!-- Peers (Observer level) -->
<div class="glass-card p-6">
<div class="glass-card p-6 max-h-[60vh] flex flex-col">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Peers <span v-if="peerNodes.length > 0" class="text-sm font-normal text-white/50">({{ peerNodes.length }})</span></h2>
<button
@@ -282,7 +293,7 @@
<p class="text-white/50 text-sm">No peers yet</p>
<p class="text-white/30 text-xs mt-1">Invite a peer to share public content</p>
</div>
<div v-else class="space-y-3">
<div v-else class="space-y-3 overflow-y-auto">
<div
v-for="node in peerNodes"
:key="node.did"
@@ -339,7 +350,7 @@
>
<option value="trusted">Trusted</option>
<option value="observer">Observer</option>
<option value="untrusted">Untrusted</option>
<option value="untrusted">Blocked</option>
</select>
</div>
</div>
@@ -436,44 +447,49 @@
</div>
<!-- Join Modal -->
<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>
<button @click="showJoinModal = false" class="text-white/40 hover:text-white/70 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<Teleport to="body">
<Transition name="modal">
<div v-if="showJoinModal" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click.self="showJoinModal = false">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="glass-card p-6 max-w-md w-full relative z-10">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-white">Join Federation</h2>
<button @click="showJoinModal = false" class="text-white/40 hover:text-white/70 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="text-sm text-white/60 mb-4">Paste the invite code from the node you want to federate with.</p>
<textarea
v-model="joinCode"
placeholder="fed1:..."
rows="3"
class="w-full bg-black/30 text-white text-sm rounded-lg p-3 border border-white/10 focus:border-orange-400/50 focus:outline-none font-mono resize-none"
></textarea>
<div v-if="joinError" class="mt-3 text-sm text-red-400">{{ joinError }}</div>
<div v-if="joinSuccess" class="mt-3 text-sm text-green-400">Successfully joined federation</div>
<div class="flex gap-3 mt-4">
<button
@click="showJoinModal = false"
class="flex-1 px-4 py-2 glass-button rounded text-sm text-white/70"
>Cancel</button>
<button
@click="joinFederation"
class="flex-1 px-4 py-2 glass-button rounded text-sm text-white font-medium disabled:opacity-50"
:disabled="joining || !joinCode.trim()"
>
{{ joining ? 'Joining...' : 'Join' }}
</button>
</div>
</div>
</div>
<p class="text-sm text-white/60 mb-4">Paste the invite code from the node you want to federate with.</p>
<textarea
v-model="joinCode"
placeholder="fed1:..."
rows="3"
class="w-full bg-black/30 text-white text-sm rounded-lg p-3 border border-white/10 focus:border-orange-400/50 focus:outline-none font-mono resize-none"
></textarea>
<div v-if="joinError" class="mt-3 text-sm text-red-400">{{ joinError }}</div>
<div v-if="joinSuccess" class="mt-3 text-sm text-green-400">Successfully joined federation</div>
<div class="flex gap-3 mt-4">
<button
@click="showJoinModal = false"
class="flex-1 px-4 py-2 glass-button rounded text-sm text-white/70"
>Cancel</button>
<button
@click="joinFederation"
class="flex-1 px-4 py-2 glass-button rounded text-sm text-white font-medium disabled:opacity-50"
:disabled="joining || !joinCode.trim()"
>
{{ joining ? 'Joining...' : 'Join' }}
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
@@ -483,10 +499,12 @@ import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
import { useTransportStore } from '@/stores/transport'
import { useAppStore } from '@/stores/app'
import NetworkMap from '@/components/federation/NetworkMap.vue'
const router = useRouter()
const transportStore = useTransportStore()
const appStore = useAppStore()
interface AppStatus {
id: string
@@ -568,7 +586,7 @@ const mapNodes = computed(() => {
if (selfDid.value) {
result.push({
did: selfDid.value,
label: 'This Node',
label: appStore.serverName,
trust_level: 'trusted' as const,
online: true,
app_count: 0,