ui updates
This commit is contained in:
parent
b7ff0b1d38
commit
68b02359dc
@ -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({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')"
|
||||
|
||||
88
neode-ui/src/views/web5/Web5Federation.vue
Normal file
88
neode-ui/src/views/web5/Web5Federation.vue
Normal 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 & 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>
|
||||
133
neode-ui/src/views/web5/Web5Monitoring.vue
Normal file
133
neode-ui/src/views/web5/Web5Monitoring.vue
Normal 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 & 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>
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user