ui updates

This commit is contained in:
Dorian 2026-04-11 13:35:52 +01:00
parent 0a493593b8
commit ed4e95a914
20 changed files with 614 additions and 113 deletions

View File

@ -2778,6 +2778,153 @@ app.post('/rpc/v1', (req, res) => {
})
}
// ── Monitoring ──────────────────────────────────────────────
case 'monitoring.current': {
return res.json({
result: {
system: {
cpu_percent: +(12 + Math.random() * 18).toFixed(1),
load_avg_1: +(0.5 + Math.random() * 1.5).toFixed(2),
load_avg_5: +(0.8 + Math.random()).toFixed(2),
load_avg_15: +(0.6 + Math.random()).toFixed(2),
mem_used_bytes: 6_200_000_000 + Math.floor(Math.random() * 500_000_000),
mem_total_bytes: 16_000_000_000,
disk_used_bytes: 620_000_000_000,
disk_total_bytes: 1_800_000_000_000,
net_rx_bytes: 12_400_000_000 + Math.floor(Math.random() * 100_000_000),
net_tx_bytes: 8_900_000_000 + Math.floor(Math.random() * 50_000_000),
uptime_secs: Math.floor(process.uptime()) + 604800,
},
rpc: { avg_latency_ms: +(2 + Math.random() * 8).toFixed(1), requests_per_minute: Math.floor(30 + Math.random() * 40) },
},
})
}
case 'monitoring.history': {
const points = 60
const history = []
for (let i = 0; i < points; i++) {
history.push({
timestamp: new Date(Date.now() - (points - i) * 60000).toISOString(),
system: {
cpu_percent: +(10 + Math.random() * 25).toFixed(1),
mem_used_bytes: 5_800_000_000 + Math.floor(Math.random() * 1_000_000_000),
mem_total_bytes: 16_000_000_000,
net_rx_bytes: Math.floor(Math.random() * 5_000_000),
net_tx_bytes: Math.floor(Math.random() * 3_000_000),
},
rpc: { avg_latency_ms: +(1 + Math.random() * 10).toFixed(1) },
})
}
return res.json({ result: { history } })
}
case 'monitoring.alerts': {
return res.json({
result: {
alerts: [
{ id: 'a1', kind: 'cpu_high', message: 'CPU usage exceeded 80% for 5 minutes', timestamp: new Date(Date.now() - 7200000).toISOString(), acknowledged: false },
{ id: 'a2', kind: 'disk_high', message: 'Disk usage at 85%', timestamp: new Date(Date.now() - 86400000).toISOString(), acknowledged: true },
],
},
})
}
case 'monitoring.alert-rules': {
return res.json({
result: {
rules: [
{ kind: 'cpu_high', enabled: true, threshold: 80, description: 'Alert when CPU usage exceeds threshold' },
{ kind: 'mem_high', enabled: true, threshold: 85, description: 'Alert when memory usage exceeds threshold' },
{ kind: 'disk_high', enabled: true, threshold: 90, description: 'Alert when disk usage exceeds threshold' },
{ kind: 'backend_error_spike', enabled: false, threshold: 500, description: 'Alert when RPC latency exceeds threshold' },
],
},
})
}
case 'monitoring.configure-alert':
case 'monitoring.acknowledge-alert': {
return res.json({ result: { success: true } })
}
case 'monitoring.export': {
return res.json({ result: { data: 'timestamp,cpu,mem\n2026-04-10T12:00:00Z,15.2,38.5\n' } })
}
case 'monitoring.container-metrics': {
return res.json({
result: {
containers: [
{ name: 'bitcoin-knots', cpu_percent: 8.2, mem_used_bytes: 1_200_000_000, net_rx_bytes: 5_000_000, net_tx_bytes: 3_200_000 },
{ name: 'lnd', cpu_percent: 3.1, mem_used_bytes: 480_000_000, net_rx_bytes: 2_100_000, net_tx_bytes: 1_800_000 },
{ name: 'electrs', cpu_percent: 12.4, mem_used_bytes: 890_000_000, net_rx_bytes: 800_000, net_tx_bytes: 600_000 },
{ name: 'mempool', cpu_percent: 5.6, mem_used_bytes: 320_000_000, net_rx_bytes: 1_500_000, net_tx_bytes: 900_000 },
{ name: 'filebrowser', cpu_percent: 0.8, mem_used_bytes: 45_000_000, net_rx_bytes: 200_000, net_tx_bytes: 150_000 },
],
},
})
}
// ── Fleet / Telemetry ─────────────────────────────────────
case 'telemetry.fleet-status': {
return res.json({
result: {
nodes: [
{
id: 'node-1', name: 'archy-main', did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9',
status: 'online', last_seen: new Date().toISOString(),
version: '1.3.0', uptime_secs: 604800,
system: { cpu_percent: 15.2, mem_used_bytes: 6_200_000_000, mem_total_bytes: 16_000_000_000, disk_used_bytes: 620_000_000_000, disk_total_bytes: 1_800_000_000_000 },
apps: ['bitcoin-knots', 'lnd', 'electrs', 'mempool', 'filebrowser'],
tor_connected: true,
},
{
id: 'node-2', name: 'archy-198', did: 'did:key:z6Mkp2z3PJbJHbQ95fGk3CqYVNEPE3VNnFGA7yUYkQjXoZTL',
status: 'online', last_seen: new Date(Date.now() - 120000).toISOString(),
version: '1.2.1', uptime_secs: 259200,
system: { cpu_percent: 8.7, mem_used_bytes: 3_100_000_000, mem_total_bytes: 8_000_000_000, disk_used_bytes: 180_000_000_000, disk_total_bytes: 500_000_000_000 },
apps: ['bitcoin-knots', 'lnd', 'mempool'],
tor_connected: true,
},
{
id: 'node-3', name: 'archy-vps', did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi5ER7eLYwBqPR4NkDhCUD7Li',
status: 'offline', last_seen: new Date(Date.now() - 3600000).toISOString(),
version: '1.3.0', uptime_secs: 0,
system: { cpu_percent: 0, mem_used_bytes: 0, mem_total_bytes: 4_000_000_000, disk_used_bytes: 45_000_000_000, disk_total_bytes: 80_000_000_000 },
apps: ['lnd'],
tor_connected: false,
},
],
},
})
}
case 'telemetry.fleet-alerts': {
return res.json({
result: {
alerts: [
{ id: 'fa1', node_id: 'node-3', node_name: 'archy-vps', kind: 'node_offline', message: 'Node went offline', timestamp: new Date(Date.now() - 3600000).toISOString(), acknowledged: false },
{ id: 'fa2', node_id: 'node-2', node_name: 'archy-198', kind: 'disk_high', message: 'Disk usage at 36%', timestamp: new Date(Date.now() - 86400000).toISOString(), acknowledged: true },
],
},
})
}
case 'telemetry.fleet-node-history': {
const nodeId = params?.node_id || 'node-1'
const points = 60
const history = []
for (let i = 0; i < points; i++) {
history.push({
timestamp: new Date(Date.now() - (points - i) * 60000).toISOString(),
cpu_percent: +(8 + Math.random() * 20).toFixed(1),
mem_used_bytes: 5_000_000_000 + Math.floor(Math.random() * 2_000_000_000),
})
}
return res.json({ result: { node_id: nodeId, history } })
}
default: {
console.log(`[RPC] Unknown method: ${method}`)
return res.json({

View File

@ -7,8 +7,12 @@
@click="store.deactivate()"
@keydown.escape="store.deactivate()"
>
<!-- Logo with audio viz ring - explicitly centered in viewport -->
<div class="screensaver-content">
<!-- ASCII variant (every 3rd activation) -->
<div v-if="store.isAsciiMode" class="screensaver-ascii-content">
<BitcoinFaceAscii />
</div>
<!-- Normal logo with audio viz ring -->
<div v-else class="screensaver-content">
<!-- Radial audio visualization - bars around the logo -->
<div class="screensaver-viz-ring">
<div
@ -31,6 +35,7 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import ScreensaverLogo from '@/components/ScreensaverLogo.vue'
import BitcoinFaceAscii from '@/views/discover/BitcoinFaceAscii.vue'
import { useScreensaverStore } from '@/stores/screensaver'
const store = useScreensaverStore()
@ -180,4 +185,24 @@ onBeforeUnmount(() => {
z-index: 10;
filter: drop-shadow(0 0 40px rgba(255, 255, 255, 0.15));
}
/* ASCII variant — centered Bitcoin face animation */
.screensaver-ascii-content {
display: flex;
align-items: center;
justify-content: center;
transform: scale(2);
}
@media (min-width: 640px) {
.screensaver-ascii-content {
transform: scale(2.5);
}
}
@media (min-width: 768px) {
.screensaver-ascii-content {
transform: scale(3);
}
}
</style>

View File

@ -1,13 +1,18 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, computed } from 'vue'
const INACTIVITY_MS = 3 * 60 * 1000 // 3 minutes
export const useScreensaverStore = defineStore('screensaver', () => {
const isActive = ref(false)
const activationCount = ref(0)
let inactivityTimer: ReturnType<typeof setTimeout> | null = null
/** True when the current activation is the ASCII variant (every 3rd time) */
const isAsciiMode = computed(() => activationCount.value > 0 && activationCount.value % 3 === 0)
function activate() {
activationCount.value++
isActive.value = true
clearInactivityTimer()
}
@ -34,6 +39,8 @@ export const useScreensaverStore = defineStore('screensaver', () => {
return {
isActive,
isAsciiMode,
activationCount,
activate,
deactivate,
resetInactivityTimer,

View File

@ -330,14 +330,13 @@ input[type="radio"]:active + * {
}
/* On mobile, shrink iframe height so AIUI ends above the Archipelago tab bar.
Use dvh (dynamic viewport height) instead of 100% on a normal mobile browser,
100% resolves through the parent chain to the large viewport (100vh) which
is taller than the visible area when the browser chrome is showing. dvh
tracks the actual visible viewport. */
Subtract both the bottom tab bar AND the top safe-area offset (status bar on
Android WebView / companion app) so the AIUI's internal tabs (chat/content/
context) stay above the tab bar instead of sliding underneath it. */
@media (max-width: 767px) {
.chat-iframe-mobile {
height: calc(100vh - var(--mobile-tab-bar-height, 72px)) !important;
height: calc(100dvh - var(--mobile-tab-bar-height, 72px)) !important;
height: calc(100vh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - 16px) !important;
height: calc(100dvh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - 16px) !important;
flex: none;
}
}

View File

@ -125,7 +125,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { RouterView, useRouter, useRoute } from 'vue-router'
import { useAppStore } from '../stores/app'
import { useAppLauncherStore } from '../stores/appLauncher'
@ -161,8 +161,10 @@ const isHomeRoute = computed(() => route.path === '/dashboard' || route.path ===
const isGlitching = ref(false)
const backgroundImage = computed(() => {
const mapped = ROUTE_BACKGROUNDS[route.path]
if (mapped) return mapped
if (isDetailRoute(route.path)) return 'bg-intro.jpg'
return ROUTE_BACKGROUNDS[route.path] || 'bg-home.jpg'
return 'bg-home.jpg'
})
const isDarkRoute = computed(() => {
@ -188,7 +190,10 @@ watch(isDarkRoute, (dark) => {
}
})
const needsMobileBackButtonSpace = computed(() => isDetailRoute(route.path))
const WEB5_DETAIL_ROUTES = ['/dashboard/server/federation', '/dashboard/monitoring', '/dashboard/fleet']
const needsMobileBackButtonSpace = computed(() =>
isDetailRoute(route.path) || WEB5_DETAIL_ROUTES.includes(route.path)
)
const mobileNavRef = ref<InstanceType<typeof DashboardMobileNav> | null>(null)
@ -196,9 +201,49 @@ const mobileTabPaddingTop = computed(() => {
return mobileNavRef.value?.mobileTabPaddingTop ?? 0
})
// Scroll position save/restore only restore when coming BACK from a detail page
const savedScrollPositions = new Map<string, number>()
let previousRoutePath = ''
function isAnyDetailRoute(path: string): boolean {
return isDetailRoute(path) || WEB5_DETAIL_ROUTES.includes(path)
}
function saveCurrentScroll() {
const el = document.querySelector<HTMLElement>('.perspective-container .view-wrapper > div[class*="overflow-y-auto"]')
if (el && previousRoutePath) {
savedScrollPositions.set(previousRoutePath, el.scrollTop)
}
}
function restoreScroll(path: string) {
const saved = savedScrollPositions.get(path)
if (saved == null) return
nextTick(() => {
// Wait for transition to settle
setTimeout(() => {
const el = document.querySelector<HTMLElement>('.perspective-container .view-wrapper > div[class*="overflow-y-auto"]')
if (el) el.scrollTop = saved
}, 50)
})
}
watch(() => route.path, (newPath) => {
const isAppDetails = isDetailRoute(newPath)
const wasAppDetails = showAltBackground.value
const oldPath = previousRoutePath
const wasDetail = isAnyDetailRoute(oldPath)
const isDetail = isAnyDetailRoute(newPath)
// Save scroll position of the page we're leaving
saveCurrentScroll()
// Restore scroll only when returning from a detail page to the parent list
if (wasDetail && !isDetail) {
restoreScroll(newPath)
}
previousRoutePath = newPath
showAltBackground.value = isAppDetails
@ -211,6 +256,7 @@ watch(() => route.path, (newPath) => {
})
onMounted(() => {
previousRoutePath = route.path
document.body.classList.add('dashboard-active')
if (loginTransition.justLoggedIn) {
playDashboardLoadOomph()

View File

@ -1,5 +1,25 @@
<template>
<div class="pb-6 mobile-scroll-pad">
<div class="pb-16 md:pb-6 mobile-scroll-pad">
<!-- Desktop Back Button -->
<button @click="router.push('/dashboard/web5')" class="hidden md:flex mb-6 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>
Web5
</button>
<!-- Mobile Back Button -->
<Teleport to="body">
<button
@click="router.push('/dashboard/web5')"
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>Web5</span>
</button>
</Teleport>
<!-- Header -->
<div class="hidden md:block mb-8">
<div class="flex items-center justify-between">
@ -98,6 +118,7 @@
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import FleetOverviewCards from './fleet/FleetOverviewCards.vue'
import FleetNodeGrid from './fleet/FleetNodeGrid.vue'
import FleetAlerts from './fleet/FleetAlerts.vue'
@ -105,5 +126,7 @@ import FleetNodeDetail from './fleet/FleetNodeDetail.vue'
import FleetContainerMatrix from './fleet/FleetContainerMatrix.vue'
import { useFleetData, timeAgo } from './fleet/useFleetData'
const router = useRouter()
const fleet = useFleetData()
</script>

View File

@ -1,5 +1,25 @@
<template>
<div class="pb-6">
<div class="pb-16 md:pb-6">
<!-- Desktop Back Button -->
<button @click="router.push('/dashboard/web5')" class="hidden md:flex mb-6 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>
Web5
</button>
<!-- Mobile Back Button -->
<Teleport to="body">
<button
@click="router.push('/dashboard/web5')"
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>Web5</span>
</button>
</Teleport>
<div class="hidden md:block mb-8">
<div class="flex items-center justify-between">
<div>
@ -213,6 +233,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { rpcClient } from '@/api/rpc-client'
import LineChart from '@/components/LineChart.vue'
@ -274,6 +295,7 @@ interface FiredAlert {
acknowledged: boolean
}
const router = useRouter()
const { t } = useI18n()
const current = ref<MetricSnapshot | null>(null)

View File

@ -1,7 +1,7 @@
<template>
<div class="min-h-full">
<div class="min-h-screen">
<BootScreen :visible="showBootScreen" @ready="onServerReady" />
<div v-if="!showBootScreen" class="min-h-full flex items-center justify-center">
<div v-if="!showBootScreen" class="min-h-screen flex items-center justify-center">
<div class="flex flex-col items-center gap-4 opacity-0 root-redirect-fade">
<svg class="animate-spin h-8 w-8 text-white/60" 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>

View File

@ -28,7 +28,9 @@ export 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/server/federation': 'bg-web5.jpg',
'/dashboard/monitoring': 'bg-web5.jpg',
'/dashboard/fleet': 'bg-web5.jpg',
'/dashboard/settings': 'bg-settings.jpg',
'/dashboard/chat': 'bg-aiui.jpg',
}
@ -81,6 +83,15 @@ export function useRouteTransitions() {
const wasCloudFolder = previousPath.includes('/cloud/') && !previousPath.endsWith('/cloud')
const wasCloudList = previousPath === '/dashboard/cloud'
const isFederation = currentPath === '/dashboard/server/federation'
const wasFederation = previousPath === '/dashboard/server/federation'
const isMonitoring = currentPath === '/dashboard/monitoring'
const wasMonitoring = previousPath === '/dashboard/monitoring'
const isFleet = currentPath === '/dashboard/fleet'
const wasFleet = previousPath === '/dashboard/fleet'
const isWeb5 = currentPath === '/dashboard/web5'
const wasWeb5 = previousPath === '/dashboard/web5'
let transitionName = 'fade'
// Mobile: Horizontal slide transitions between sub-tabs
@ -123,6 +134,18 @@ export function useRouteTransitions() {
transitionName = 'depth-forward'
} else if (wasCloudFolder && isCloudList) {
transitionName = 'depth-back'
} else if (wasWeb5 && isFederation) {
transitionName = 'depth-forward'
} else if (wasFederation && isWeb5) {
transitionName = 'depth-back'
} else if (wasWeb5 && isMonitoring) {
transitionName = 'depth-forward'
} else if (wasMonitoring && isWeb5) {
transitionName = 'depth-back'
} else if (wasWeb5 && isFleet) {
transitionName = 'depth-forward'
} else if (wasFleet && isWeb5) {
transitionName = 'depth-back'
} else if (wasMarketplaceList && isAppDetails) {
transitionName = 'depth-forward'
} else if (wasAppDetails && isMarketplaceList) {

View File

@ -2,13 +2,24 @@
<div class="mb-6">
<button
@click="router.push('/dashboard/web5')"
class="flex items-center gap-2 text-white/50 hover:text-white/80 transition-colors text-sm mb-4"
class="hidden md:flex items-center gap-2 text-white/70 hover:text-white transition-colors mb-4"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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 Web5
Web5
</button>
<Teleport to="body">
<button
@click="router.push('/dashboard/web5')"
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>Web5</span>
</button>
</Teleport>
<div class="flex items-start justify-between gap-4">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Federation & Peers</h1>

View File

@ -129,7 +129,7 @@
{{ t('common.send') }}
</button>
<button @click="$emit('showReceive')" class="home-card-btn px-3 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
{{ t('web5.receiveBitcoin') }}
Receive
</button>
<button @click="$emit('showTransactions')" class="home-card-btn px-3 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
Transactions

View File

@ -280,10 +280,12 @@ loadBackups()
<!-- Lightning Channel Backup -->
<div class="glass-card px-6 py-6 mb-6">
<h2 class="text-xl font-semibold text-white/96 mb-1">Lightning Channel Backup</h2>
<p class="text-sm text-white/60 mb-3">Export your channel state so you can restore channels on a new node. Does not include on-chain wallet seed.</p>
<div class="flex gap-3">
<button @click="exportChannelBackup" :disabled="exportingChannelBackup" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-white/96 mb-1">Lightning Channel Backup</h2>
<p class="text-sm text-white/60">Export your channel state so you can restore channels on a new node. Does not include on-chain wallet seed.</p>
</div>
<button @click="exportChannelBackup" :disabled="exportingChannelBackup" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center justify-center gap-2 w-full md:w-auto shrink-0">
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>

View File

@ -49,12 +49,12 @@ async function performFactoryReset() {
<template>
<!-- Network Diagnostics Link -->
<div class="glass-card px-6 py-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-white/96">{{ t('common.network') }}</h2>
<p class="text-sm text-white/60 mt-1">{{ t('settings.networkDesc') }}</p>
<h2 class="text-xl font-semibold text-white/96 mb-1">{{ t('common.network') }}</h2>
<p class="text-sm text-white/60">{{ t('settings.networkDesc') }}</p>
</div>
<button @click="router.push('/dashboard/server')" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<button @click="router.push('/dashboard/server')" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center justify-center gap-2 w-full md:w-auto shrink-0">
<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="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
@ -64,14 +64,14 @@ async function performFactoryReset() {
</div>
<!-- Reboot Section -->
<div class="path-option-card px-6 py-6 mt-6">
<div class="flex items-center justify-between">
<div class="glass-card px-6 py-6 mb-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-white/90 mb-1">Reboot</h2>
<h2 class="text-xl font-semibold text-white/96 mb-1">Reboot</h2>
<p class="text-sm text-white/60">Restart the machine. All containers will restart automatically.</p>
</div>
<button
class="glass-button px-6 py-2 text-sm"
class="glass-button px-6 py-2 rounded-lg text-sm w-full md:w-auto shrink-0"
:disabled="rebooting"
@click="showRebootConfirm = true"
>
@ -109,17 +109,21 @@ async function performFactoryReset() {
</Teleport>
<!-- Factory Reset Section -->
<div class="path-option-card px-6 py-6 mt-6 border-red-500/30">
<h2 class="text-xl font-semibold text-red-400/90 mb-3">Factory Reset</h2>
<p class="text-sm text-white/60 mb-4">
Wipe all user data, identities, and credentials. Container images are preserved. The node will restart and show the onboarding screen.
</p>
<button
class="glass-button glass-button-danger"
@click="showFactoryResetConfirm = true"
>
Factory Reset
</button>
<div class="glass-card px-6 py-6 mb-6 border border-red-500/30">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-red-400/90 mb-1">Factory Reset</h2>
<p class="text-sm text-white/60">
Wipe all user data, identities, and credentials. Container images are preserved. The node will restart and show the onboarding screen.
</p>
</div>
<button
class="glass-button glass-button-danger w-full md:w-auto shrink-0"
@click="showFactoryResetConfirm = true"
>
Factory Reset
</button>
</div>
</div>
<!-- Factory Reset Confirmation Modal -->

View File

@ -55,50 +55,26 @@
</div>
</Teleport>
<!-- Core Services Overview Cards -- Row 1 -->
<div class="flex flex-col md:flex-row gap-6 mb-6">
<Web5Domains ref="domainsRef" :showStagger="showStagger" :managedIdentities="identitiesRef?.managedIdentities ?? []" />
<Web5Wallet
:showStagger="showStagger"
:walletConnected="walletConnected"
:walletError="walletError"
:lndOnchainBalance="lndOnchainBalance"
:lndChannelBalance="lndChannelBalance"
:ecashBalance="ecashBalance"
:incomingTransactions="incomingTransactions"
:incomingTxCount="incomingTxCount"
:txActivityCount="txActivityCount"
:meshRelayActive="sendReceiveRef?.meshRelayActive ?? false"
:meshRelayStatus="sendReceiveRef?.meshRelayStatus ?? ''"
:sendResultTxid="sendReceiveRef?.sendResultTxid ?? ''"
@openSend="sendReceiveRef?.openSend()"
@openReceive="sendReceiveRef?.openReceive()"
/>
</div>
<!-- Core Services Overview Cards -- Row 2 -->
<div class="flex flex-col md:flex-row gap-6 mb-8">
<Web5NostrRelays ref="nostrRelaysRef" :showStagger="showStagger" />
<!-- Connected Nodes + Node Visibility -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
<Web5ConnectedNodes ref="connectedNodesRef" @toast="showToast" />
<Web5NodeVisibility :showStagger="showStagger" ref="nodeVisibilityRef" @toast="showToast" />
</div>
<!-- Connected Nodes + Shared Content grid -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-8">
<Web5ConnectedNodes ref="connectedNodesRef" @toast="showToast" />
<Web5SharedContent ref="sharedContentRef" :showStagger="showStagger" :peers="connectedNodesRef?.peers ?? []" @toast="showToast" />
</div>
<!-- Identities + DWN grid -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-8">
<!-- Identities + Nostr Relays -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
<Web5Identities ref="identitiesRef" :showStagger="showStagger" @toast="showToast" />
<Web5DWN ref="dwnRef" />
<Web5NostrRelays ref="nostrRelaysRef" :showStagger="showStagger" />
</div>
<!-- Verifiable Credentials -->
<Web5CredentialsSummary ref="credentialsRef" :identityCount="identitiesRef?.managedIdentities?.length ?? 0" />
<!-- Monitoring + Federation -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-8">
<Web5Monitoring />
<Web5Federation />
</div>
<!-- Send/Receive Modals -->
<Web5SendReceiveModals ref="sendReceiveRef" @toast="showToast" @balancesChanged="reloadBalances" />
<!-- Send/Receive Modals hidden wallet card removed -->
<!-- <Web5SendReceiveModals ref="sendReceiveRef" @toast="showToast" @balancesChanged="reloadBalances" /> -->
<!-- Identity Toast -->
<Transition name="content-fade">
@ -122,16 +98,18 @@ import { safeClipboardWrite } from './utils'
import type { ProfitsData, WalletTransaction, HwWalletDevice } from './types'
import Web5QuickActions from './Web5QuickActions.vue'
import Web5Wallet from './Web5Wallet.vue'
import Web5Domains from './Web5Domains.vue'
// import Web5Wallet from './Web5Wallet.vue' // hidden for now
// import Web5Domains from './Web5Domains.vue' // hidden for now
import Web5NostrRelays from './Web5NostrRelays.vue'
import Web5NodeVisibility from './Web5NodeVisibility.vue'
import Web5ConnectedNodes from './Web5ConnectedNodes.vue'
import Web5SharedContent from './Web5SharedContent.vue'
// import Web5SharedContent from './Web5SharedContent.vue' // hidden for now
import Web5Identities from './Web5Identities.vue'
import Web5DWN from './Web5DWN.vue'
import Web5CredentialsSummary from './Web5CredentialsSummary.vue'
import Web5SendReceiveModals from './Web5SendReceiveModals.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'
// import Web5SendReceiveModals from './Web5SendReceiveModals.vue' // wallet hidden
const route = useRoute()
const { t } = useI18n()
@ -139,15 +117,15 @@ const { t } = useI18n()
const showStagger = !web5AnimationDone
// Child refs
const domainsRef = ref<InstanceType<typeof Web5Domains> | null>(null)
// const domainsRef = ref(null) // hidden for now
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<InstanceType<typeof Web5DWN> | null>(null)
const credentialsRef = ref<InstanceType<typeof Web5CredentialsSummary> | null>(null)
const sharedContentRef = ref<InstanceType<typeof Web5SharedContent> | null>(null)
const sendReceiveRef = ref<InstanceType<typeof Web5SendReceiveModals> | 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
// --- Toast ---
const identityToastText = ref('')
@ -323,13 +301,8 @@ const lndChannelBalance = ref(0)
const walletError = ref('')
const ecashBalance = ref(0)
// Transactions
// Transactions wallet card hidden, but loadTransactions still called for QuickActions walletConnected state
const walletTransactions = ref<WalletTransaction[]>([])
const incomingTransactions = computed(() =>
walletTransactions.value.filter(tx => tx.direction === 'incoming' && tx.num_confirmations < 3)
)
const incomingTxCount = computed(() => incomingTransactions.value.length)
const txActivityCount = computed(() => incomingTxCount.value + (sendReceiveRef.value?.meshRelayActive ? 1 : 0))
// Hardware wallets
const detectedHwWallets = ref<HwWalletDevice[]>([])
@ -392,11 +365,11 @@ async function detectHardwareWallets() {
}
}
function reloadBalances() {
loadLndBalances()
loadEcashBalance()
loadTransactions()
}
// function reloadBalances() { // wallet hidden
// loadLndBalances()
// loadEcashBalance()
// loadTransactions()
// }
// Auto-refresh wallet data every 30s
let walletRefreshInterval: ReturnType<typeof setInterval> | null = null
@ -418,12 +391,12 @@ onMounted(() => {
connectedNodesRef.value?.loadConnectionRequests()
identitiesRef.value?.loadIdentities()
nodeVisibilityRef.value?.loadVisibility()
domainsRef.value?.loadDomainNames()
// domainsRef.value?.loadDomainNames() // hidden for now
nostrRelaysRef.value?.loadNostrRelays()
dwnRef.value?.loadDwnStatus()
dwnRef.value?.loadDwnProtocols()
credentialsRef.value?.loadCredentials()
sharedContentRef.value?.loadContentItems()
// dwnRef.value?.loadDwnStatus() // hidden for now
// dwnRef.value?.loadDwnProtocols() // hidden for now
// credentialsRef.value?.loadCredentials() // hidden for now
// sharedContentRef.value?.loadContentItems() // hidden for now
// Load local state data
loadEcashBalance()

View File

@ -10,7 +10,6 @@
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">{{ t('web5.connectedNodes') }}</h2>
<p class="text-white/70 text-sm mb-4">{{ t('web5.peerNodesDescription') }}</p>
</div>
<div class="flex gap-2 shrink-0">
<button
@ -37,7 +36,6 @@
</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')"

View File

@ -0,0 +1,88 @@
<template>
<!-- Federation Summary -->
<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="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-white">Federation</h2>
<p class="text-xs text-white/60">Federated nodes &amp; peers</p>
</div>
</div>
<RouterLink to="/dashboard/fleet" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium inline-flex items-center gap-2 no-underline">
Details
<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="M9 5l7 7-7 7" />
</svg>
</RouterLink>
</div>
<div class="space-y-3">
<!-- Node Count -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full" :class="nodeCount > 0 ? 'bg-green-400' : 'bg-white/30'" />
<span class="text-sm text-white/80">Known Nodes</span>
</div>
<span class="text-sm font-medium text-white">{{ nodeCount }}</span>
</div>
<!-- Online -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full" :class="onlineCount > 0 ? 'bg-green-400' : 'bg-white/30'" />
<span class="text-sm text-white/80">Online</span>
</div>
<span class="text-sm font-medium" :class="onlineCount > 0 ? 'text-green-400' : 'text-white/40'">{{ onlineCount }}</span>
</div>
<!-- Pending Requests -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full" :class="pendingCount > 0 ? 'bg-orange-400' : 'bg-white/30'" />
<span class="text-sm text-white/80">Pending Requests</span>
</div>
<span class="text-sm font-medium" :class="pendingCount > 0 ? 'text-orange-400' : 'text-white/40'">{{ pendingCount }}</span>
</div>
<!-- Self DID -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-sm text-white/80">Node DID</span>
<span class="text-sm font-medium text-white/40 truncate max-w-[140px]">{{ selfDid || 'Not set' }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
const nodeCount = ref(0)
const onlineCount = ref(0)
const pendingCount = ref(0)
const selfDid = ref('')
onMounted(async () => {
try {
const res = await rpcClient.call<{
nodes: Array<{ status: string }>
pending_requests: Array<unknown>
}>({ method: 'federation.list', timeout: 5000 })
const nodes = res.nodes || []
nodeCount.value = nodes.length
onlineCount.value = nodes.filter(n => n.status === 'online' || n.status === 'connected').length
pendingCount.value = (res.pending_requests || []).length
} catch { /* unavailable */ }
try {
const res = await rpcClient.getNodeDid()
selfDid.value = res.did || ''
} catch { /* unavailable */ }
})
</script>

View File

@ -0,0 +1,133 @@
<template>
<!-- System Monitoring Summary -->
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-white">Monitoring</h2>
<p class="text-xs text-white/60">System resources &amp; health</p>
</div>
</div>
<RouterLink to="/dashboard/monitoring" class="glass-button glass-button-sm px-3 rounded-lg text-sm font-medium inline-flex items-center gap-2 no-underline">
Details
<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="M9 5l7 7-7 7" />
</svg>
</RouterLink>
</div>
<div class="space-y-3">
<!-- CPU -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<span class="text-sm text-white/80">CPU</span>
</div>
<div class="flex items-center gap-3">
<div class="w-24 h-1.5 bg-white/10 rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500" :class="barColor(cpuPercent)" :style="{ width: cpuPercent + '%' }" />
</div>
<span class="text-sm font-medium text-white min-w-[3rem] text-right">{{ cpuPercent.toFixed(0) }}%</span>
</div>
</div>
<!-- Memory -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<span class="text-sm text-white/80">Memory</span>
</div>
<div class="flex items-center gap-3">
<div class="w-24 h-1.5 bg-white/10 rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500" :class="barColor(memPercent)" :style="{ width: memPercent + '%' }" />
</div>
<span class="text-sm font-medium text-white min-w-[3rem] text-right">{{ memPercent.toFixed(0) }}%</span>
</div>
</div>
<!-- Disk -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<span class="text-sm text-white/80">Disk</span>
</div>
<div class="flex items-center gap-3">
<div class="w-24 h-1.5 bg-white/10 rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500" :class="barColor(diskPercent)" :style="{ width: diskPercent + '%' }" />
</div>
<span class="text-sm font-medium text-white min-w-[3rem] text-right">{{ diskPercent.toFixed(0) }}%</span>
</div>
</div>
<!-- Uptime -->
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-sm text-white/80">Uptime</span>
<span class="text-sm font-medium text-white/60">{{ uptimeDisplay }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { RouterLink } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
const cpuPercent = ref(0)
const memUsed = ref(0)
const memTotal = ref(0)
const diskUsed = ref(0)
const diskTotal = ref(0)
const uptimeSecs = ref(0)
const memPercent = computed(() => memTotal.value > 0 ? (memUsed.value / memTotal.value) * 100 : 0)
const diskPercent = computed(() => diskTotal.value > 0 ? (diskUsed.value / diskTotal.value) * 100 : 0)
const uptimeDisplay = computed(() => {
const s = uptimeSecs.value
if (s === 0) return '--'
const days = Math.floor(s / 86400)
const hours = Math.floor((s % 86400) / 3600)
const mins = Math.floor((s % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
return `${hours}h ${mins}m`
})
function barColor(pct: number): string {
if (pct > 85) return 'bg-red-400'
if (pct > 60) return 'bg-orange-400'
return 'bg-green-400'
}
async function loadStats() {
try {
const res = await rpcClient.call<{
cpu_usage_percent: number
mem_used_bytes: number
mem_total_bytes: number
disk_used_bytes: number
disk_total_bytes: number
uptime_secs: number
}>({ method: 'system.stats' })
cpuPercent.value = res.cpu_usage_percent
memUsed.value = res.mem_used_bytes
memTotal.value = res.mem_total_bytes
diskUsed.value = res.disk_used_bytes
diskTotal.value = res.disk_total_bytes
uptimeSecs.value = res.uptime_secs
} catch { /* unavailable */ }
}
let refreshInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
loadStats()
refreshInterval = setInterval(loadStats, 30000)
})
onBeforeUnmount(() => {
if (refreshInterval) clearInterval(refreshInterval)
})
</script>

View File

@ -1,6 +1,6 @@
<template>
<!-- Node Visibility -->
<div data-controller-container tabindex="0" :class="{ 'card-stagger': showStagger }" class="glass-card p-6 flex flex-col md:w-1/2" style="--stagger-index: 3">
<div data-controller-container tabindex="0" :class="{ 'card-stagger': showStagger }" class="glass-card p-6 flex flex-col" 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">

View File

@ -1,6 +1,6 @@
<template>
<!-- Nostr Relays -->
<div data-controller-container tabindex="0" :class="{ 'card-stagger': showStagger }" class="glass-card p-6 flex flex-col md:w-1/2" style="--stagger-index: 2">
<div data-controller-container tabindex="0" :class="{ 'card-stagger': showStagger }" class="glass-card p-6 flex flex-col" 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">

View File

@ -1,6 +1,6 @@
<template>
<!-- Wallet -->
<div data-controller-container tabindex="0" :class="{ 'card-stagger': showStagger }" class="glass-card p-6 flex flex-col md:w-1/2" style="--stagger-index: 1">
<div data-controller-container tabindex="0" :class="{ 'card-stagger': showStagger }" class="glass-card p-6 flex flex-col" 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">