ui updates
This commit is contained in:
parent
0a493593b8
commit
ed4e95a914
@ -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: {
|
default: {
|
||||||
console.log(`[RPC] Unknown method: ${method}`)
|
console.log(`[RPC] Unknown method: ${method}`)
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|||||||
@ -7,8 +7,12 @@
|
|||||||
@click="store.deactivate()"
|
@click="store.deactivate()"
|
||||||
@keydown.escape="store.deactivate()"
|
@keydown.escape="store.deactivate()"
|
||||||
>
|
>
|
||||||
<!-- Logo with audio viz ring - explicitly centered in viewport -->
|
<!-- ASCII variant (every 3rd activation) -->
|
||||||
<div class="screensaver-content">
|
<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 -->
|
<!-- Radial audio visualization - bars around the logo -->
|
||||||
<div class="screensaver-viz-ring">
|
<div class="screensaver-viz-ring">
|
||||||
<div
|
<div
|
||||||
@ -31,6 +35,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onBeforeUnmount } from 'vue'
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
import ScreensaverLogo from '@/components/ScreensaverLogo.vue'
|
import ScreensaverLogo from '@/components/ScreensaverLogo.vue'
|
||||||
|
import BitcoinFaceAscii from '@/views/discover/BitcoinFaceAscii.vue'
|
||||||
import { useScreensaverStore } from '@/stores/screensaver'
|
import { useScreensaverStore } from '@/stores/screensaver'
|
||||||
|
|
||||||
const store = useScreensaverStore()
|
const store = useScreensaverStore()
|
||||||
@ -180,4 +185,24 @@ onBeforeUnmount(() => {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
filter: drop-shadow(0 0 40px rgba(255, 255, 255, 0.15));
|
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>
|
</style>
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const INACTIVITY_MS = 3 * 60 * 1000 // 3 minutes
|
const INACTIVITY_MS = 3 * 60 * 1000 // 3 minutes
|
||||||
|
|
||||||
export const useScreensaverStore = defineStore('screensaver', () => {
|
export const useScreensaverStore = defineStore('screensaver', () => {
|
||||||
const isActive = ref(false)
|
const isActive = ref(false)
|
||||||
|
const activationCount = ref(0)
|
||||||
let inactivityTimer: ReturnType<typeof setTimeout> | null = null
|
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() {
|
function activate() {
|
||||||
|
activationCount.value++
|
||||||
isActive.value = true
|
isActive.value = true
|
||||||
clearInactivityTimer()
|
clearInactivityTimer()
|
||||||
}
|
}
|
||||||
@ -34,6 +39,8 @@ export const useScreensaverStore = defineStore('screensaver', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isActive,
|
isActive,
|
||||||
|
isAsciiMode,
|
||||||
|
activationCount,
|
||||||
activate,
|
activate,
|
||||||
deactivate,
|
deactivate,
|
||||||
resetInactivityTimer,
|
resetInactivityTimer,
|
||||||
|
|||||||
@ -330,14 +330,13 @@ input[type="radio"]:active + * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* On mobile, shrink iframe height so AIUI ends above the Archipelago tab bar.
|
/* 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,
|
Subtract both the bottom tab bar AND the top safe-area offset (status bar on
|
||||||
100% resolves through the parent chain to the large viewport (100vh) which
|
Android WebView / companion app) so the AIUI's internal tabs (chat/content/
|
||||||
is taller than the visible area when the browser chrome is showing. dvh
|
context) stay above the tab bar instead of sliding underneath it. */
|
||||||
tracks the actual visible viewport. */
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.chat-iframe-mobile {
|
.chat-iframe-mobile {
|
||||||
height: calc(100vh - 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)) !important;
|
height: calc(100dvh - var(--mobile-tab-bar-height, 72px) - var(--safe-area-top, env(safe-area-inset-top, 0px)) - 16px) !important;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,7 +125,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { RouterView, useRouter, useRoute } from 'vue-router'
|
||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||||
@ -161,8 +161,10 @@ const isHomeRoute = computed(() => route.path === '/dashboard' || route.path ===
|
|||||||
const isGlitching = ref(false)
|
const isGlitching = ref(false)
|
||||||
|
|
||||||
const backgroundImage = computed(() => {
|
const backgroundImage = computed(() => {
|
||||||
|
const mapped = ROUTE_BACKGROUNDS[route.path]
|
||||||
|
if (mapped) return mapped
|
||||||
if (isDetailRoute(route.path)) return 'bg-intro.jpg'
|
if (isDetailRoute(route.path)) return 'bg-intro.jpg'
|
||||||
return ROUTE_BACKGROUNDS[route.path] || 'bg-home.jpg'
|
return 'bg-home.jpg'
|
||||||
})
|
})
|
||||||
|
|
||||||
const isDarkRoute = computed(() => {
|
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)
|
const mobileNavRef = ref<InstanceType<typeof DashboardMobileNav> | null>(null)
|
||||||
|
|
||||||
@ -196,9 +201,49 @@ const mobileTabPaddingTop = computed(() => {
|
|||||||
return mobileNavRef.value?.mobileTabPaddingTop ?? 0
|
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) => {
|
watch(() => route.path, (newPath) => {
|
||||||
const isAppDetails = isDetailRoute(newPath)
|
const isAppDetails = isDetailRoute(newPath)
|
||||||
const wasAppDetails = showAltBackground.value
|
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
|
showAltBackground.value = isAppDetails
|
||||||
|
|
||||||
@ -211,6 +256,7 @@ watch(() => route.path, (newPath) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
previousRoutePath = route.path
|
||||||
document.body.classList.add('dashboard-active')
|
document.body.classList.add('dashboard-active')
|
||||||
if (loginTransition.justLoggedIn) {
|
if (loginTransition.justLoggedIn) {
|
||||||
playDashboardLoadOomph()
|
playDashboardLoadOomph()
|
||||||
|
|||||||
@ -1,5 +1,25 @@
|
|||||||
<template>
|
<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 -->
|
<!-- Header -->
|
||||||
<div class="hidden md:block mb-8">
|
<div class="hidden md:block mb-8">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@ -98,6 +118,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import FleetOverviewCards from './fleet/FleetOverviewCards.vue'
|
import FleetOverviewCards from './fleet/FleetOverviewCards.vue'
|
||||||
import FleetNodeGrid from './fleet/FleetNodeGrid.vue'
|
import FleetNodeGrid from './fleet/FleetNodeGrid.vue'
|
||||||
import FleetAlerts from './fleet/FleetAlerts.vue'
|
import FleetAlerts from './fleet/FleetAlerts.vue'
|
||||||
@ -105,5 +126,7 @@ import FleetNodeDetail from './fleet/FleetNodeDetail.vue'
|
|||||||
import FleetContainerMatrix from './fleet/FleetContainerMatrix.vue'
|
import FleetContainerMatrix from './fleet/FleetContainerMatrix.vue'
|
||||||
import { useFleetData, timeAgo } from './fleet/useFleetData'
|
import { useFleetData, timeAgo } from './fleet/useFleetData'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const fleet = useFleetData()
|
const fleet = useFleetData()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,5 +1,25 @@
|
|||||||
<template>
|
<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="hidden md:block mb-8">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@ -213,6 +233,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
import LineChart from '@/components/LineChart.vue'
|
import LineChart from '@/components/LineChart.vue'
|
||||||
@ -274,6 +295,7 @@ interface FiredAlert {
|
|||||||
acknowledged: boolean
|
acknowledged: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const current = ref<MetricSnapshot | null>(null)
|
const current = ref<MetricSnapshot | null>(null)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-full">
|
<div class="min-h-screen">
|
||||||
<BootScreen :visible="showBootScreen" @ready="onServerReady" />
|
<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">
|
<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">
|
<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>
|
<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/mesh': 'bg-mesh.jpg',
|
||||||
'/dashboard/server': 'bg-network.jpg',
|
'/dashboard/server': 'bg-network.jpg',
|
||||||
'/dashboard/web5': 'bg-web5.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/settings': 'bg-settings.jpg',
|
||||||
'/dashboard/chat': 'bg-aiui.jpg',
|
'/dashboard/chat': 'bg-aiui.jpg',
|
||||||
}
|
}
|
||||||
@ -81,6 +83,15 @@ export function useRouteTransitions() {
|
|||||||
const wasCloudFolder = previousPath.includes('/cloud/') && !previousPath.endsWith('/cloud')
|
const wasCloudFolder = previousPath.includes('/cloud/') && !previousPath.endsWith('/cloud')
|
||||||
const wasCloudList = previousPath === '/dashboard/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'
|
let transitionName = 'fade'
|
||||||
|
|
||||||
// Mobile: Horizontal slide transitions between sub-tabs
|
// Mobile: Horizontal slide transitions between sub-tabs
|
||||||
@ -123,6 +134,18 @@ export function useRouteTransitions() {
|
|||||||
transitionName = 'depth-forward'
|
transitionName = 'depth-forward'
|
||||||
} else if (wasCloudFolder && isCloudList) {
|
} else if (wasCloudFolder && isCloudList) {
|
||||||
transitionName = 'depth-back'
|
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) {
|
} else if (wasMarketplaceList && isAppDetails) {
|
||||||
transitionName = 'depth-forward'
|
transitionName = 'depth-forward'
|
||||||
} else if (wasAppDetails && isMarketplaceList) {
|
} else if (wasAppDetails && isMarketplaceList) {
|
||||||
|
|||||||
@ -2,13 +2,24 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<button
|
<button
|
||||||
@click="router.push('/dashboard/web5')"
|
@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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
Back to Web5
|
Web5
|
||||||
</button>
|
</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 class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-white mb-2">Federation & Peers</h1>
|
<h1 class="text-3xl font-bold text-white mb-2">Federation & Peers</h1>
|
||||||
|
|||||||
@ -129,7 +129,7 @@
|
|||||||
{{ t('common.send') }}
|
{{ t('common.send') }}
|
||||||
</button>
|
</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">
|
<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>
|
||||||
<button @click="$emit('showTransactions')" class="home-card-btn px-3 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
<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
|
Transactions
|
||||||
|
|||||||
@ -280,10 +280,12 @@ loadBackups()
|
|||||||
|
|
||||||
<!-- Lightning Channel Backup -->
|
<!-- Lightning Channel Backup -->
|
||||||
<div class="glass-card px-6 py-6 mb-6">
|
<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>
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||||
<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>
|
||||||
<div class="flex gap-3">
|
<h2 class="text-xl font-semibold text-white/96 mb-1">Lightning Channel Backup</h2>
|
||||||
<button @click="exportChannelBackup" :disabled="exportingChannelBackup" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
|
|||||||
@ -49,12 +49,12 @@ async function performFactoryReset() {
|
|||||||
<template>
|
<template>
|
||||||
<!-- Network Diagnostics Link -->
|
<!-- Network Diagnostics Link -->
|
||||||
<div class="glass-card px-6 py-6 mb-6">
|
<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>
|
<div>
|
||||||
<h2 class="text-xl font-semibold text-white/96">{{ t('common.network') }}</h2>
|
<h2 class="text-xl font-semibold text-white/96 mb-1">{{ t('common.network') }}</h2>
|
||||||
<p class="text-sm text-white/60 mt-1">{{ t('settings.networkDesc') }}</p>
|
<p class="text-sm text-white/60">{{ t('settings.networkDesc') }}</p>
|
||||||
</div>
|
</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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -64,14 +64,14 @@ async function performFactoryReset() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reboot Section -->
|
<!-- Reboot Section -->
|
||||||
<div class="path-option-card px-6 py-6 mt-6">
|
<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>
|
<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>
|
<p class="text-sm text-white/60">Restart the machine. All containers will restart automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
:disabled="rebooting"
|
||||||
@click="showRebootConfirm = true"
|
@click="showRebootConfirm = true"
|
||||||
>
|
>
|
||||||
@ -109,17 +109,21 @@ async function performFactoryReset() {
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<!-- Factory Reset Section -->
|
<!-- Factory Reset Section -->
|
||||||
<div class="path-option-card px-6 py-6 mt-6 border-red-500/30">
|
<div class="glass-card px-6 py-6 mb-6 border border-red-500/30">
|
||||||
<h2 class="text-xl font-semibold text-red-400/90 mb-3">Factory Reset</h2>
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||||
<p class="text-sm text-white/60 mb-4">
|
<div>
|
||||||
Wipe all user data, identities, and credentials. Container images are preserved. The node will restart and show the onboarding screen.
|
<h2 class="text-xl font-semibold text-red-400/90 mb-1">Factory Reset</h2>
|
||||||
</p>
|
<p class="text-sm text-white/60">
|
||||||
<button
|
Wipe all user data, identities, and credentials. Container images are preserved. The node will restart and show the onboarding screen.
|
||||||
class="glass-button glass-button-danger"
|
</p>
|
||||||
@click="showFactoryResetConfirm = true"
|
</div>
|
||||||
>
|
<button
|
||||||
Factory Reset
|
class="glass-button glass-button-danger w-full md:w-auto shrink-0"
|
||||||
</button>
|
@click="showFactoryResetConfirm = true"
|
||||||
|
>
|
||||||
|
Factory Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Factory Reset Confirmation Modal -->
|
<!-- Factory Reset Confirmation Modal -->
|
||||||
|
|||||||
@ -55,50 +55,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<!-- Core Services Overview Cards -- Row 1 -->
|
<!-- Connected Nodes + Node Visibility -->
|
||||||
<div class="flex flex-col md:flex-row gap-6 mb-6">
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
|
||||||
<Web5Domains ref="domainsRef" :showStagger="showStagger" :managedIdentities="identitiesRef?.managedIdentities ?? []" />
|
<Web5ConnectedNodes ref="connectedNodesRef" @toast="showToast" />
|
||||||
<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" />
|
|
||||||
<Web5NodeVisibility :showStagger="showStagger" ref="nodeVisibilityRef" @toast="showToast" />
|
<Web5NodeVisibility :showStagger="showStagger" ref="nodeVisibilityRef" @toast="showToast" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connected Nodes + Shared Content grid -->
|
<!-- Identities + Nostr Relays -->
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-8">
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
|
||||||
<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">
|
|
||||||
<Web5Identities ref="identitiesRef" :showStagger="showStagger" @toast="showToast" />
|
<Web5Identities ref="identitiesRef" :showStagger="showStagger" @toast="showToast" />
|
||||||
<Web5DWN ref="dwnRef" />
|
<Web5NostrRelays ref="nostrRelaysRef" :showStagger="showStagger" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Verifiable Credentials -->
|
<!-- Monitoring + Federation -->
|
||||||
<Web5CredentialsSummary ref="credentialsRef" :identityCount="identitiesRef?.managedIdentities?.length ?? 0" />
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-8">
|
||||||
|
<Web5Monitoring />
|
||||||
|
<Web5Federation />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Send/Receive Modals -->
|
<!-- Send/Receive Modals hidden — wallet card removed -->
|
||||||
<Web5SendReceiveModals ref="sendReceiveRef" @toast="showToast" @balancesChanged="reloadBalances" />
|
<!-- <Web5SendReceiveModals ref="sendReceiveRef" @toast="showToast" @balancesChanged="reloadBalances" /> -->
|
||||||
|
|
||||||
<!-- Identity Toast -->
|
<!-- Identity Toast -->
|
||||||
<Transition name="content-fade">
|
<Transition name="content-fade">
|
||||||
@ -122,16 +98,18 @@ import { safeClipboardWrite } from './utils'
|
|||||||
import type { ProfitsData, WalletTransaction, HwWalletDevice } from './types'
|
import type { ProfitsData, WalletTransaction, HwWalletDevice } from './types'
|
||||||
|
|
||||||
import Web5QuickActions from './Web5QuickActions.vue'
|
import Web5QuickActions from './Web5QuickActions.vue'
|
||||||
import Web5Wallet from './Web5Wallet.vue'
|
// import Web5Wallet from './Web5Wallet.vue' // hidden for now
|
||||||
import Web5Domains from './Web5Domains.vue'
|
// import Web5Domains from './Web5Domains.vue' // hidden for now
|
||||||
import Web5NostrRelays from './Web5NostrRelays.vue'
|
import Web5NostrRelays from './Web5NostrRelays.vue'
|
||||||
import Web5NodeVisibility from './Web5NodeVisibility.vue'
|
import Web5NodeVisibility from './Web5NodeVisibility.vue'
|
||||||
import Web5ConnectedNodes from './Web5ConnectedNodes.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 Web5Identities from './Web5Identities.vue'
|
||||||
import Web5DWN from './Web5DWN.vue'
|
// import Web5DWN from './Web5DWN.vue' // hidden for now
|
||||||
import Web5CredentialsSummary from './Web5CredentialsSummary.vue'
|
// import Web5CredentialsSummary from './Web5CredentialsSummary.vue' // hidden for now
|
||||||
import Web5SendReceiveModals from './Web5SendReceiveModals.vue'
|
import Web5Monitoring from './Web5Monitoring.vue'
|
||||||
|
import Web5Federation from './Web5Federation.vue'
|
||||||
|
// import Web5SendReceiveModals from './Web5SendReceiveModals.vue' // wallet hidden
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -139,15 +117,15 @@ const { t } = useI18n()
|
|||||||
const showStagger = !web5AnimationDone
|
const showStagger = !web5AnimationDone
|
||||||
|
|
||||||
// Child refs
|
// 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 nostrRelaysRef = ref<InstanceType<typeof Web5NostrRelays> | null>(null)
|
||||||
const nodeVisibilityRef = ref<InstanceType<typeof Web5NodeVisibility> | null>(null)
|
const nodeVisibilityRef = ref<InstanceType<typeof Web5NodeVisibility> | null>(null)
|
||||||
const connectedNodesRef = ref<InstanceType<typeof Web5ConnectedNodes> | null>(null)
|
const connectedNodesRef = ref<InstanceType<typeof Web5ConnectedNodes> | null>(null)
|
||||||
const identitiesRef = ref<InstanceType<typeof Web5Identities> | null>(null)
|
const identitiesRef = ref<InstanceType<typeof Web5Identities> | null>(null)
|
||||||
const dwnRef = ref<InstanceType<typeof Web5DWN> | null>(null)
|
// const dwnRef = ref(null) // hidden for now
|
||||||
const credentialsRef = ref<InstanceType<typeof Web5CredentialsSummary> | null>(null)
|
// const credentialsRef = ref(null) // hidden for now
|
||||||
const sharedContentRef = ref<InstanceType<typeof Web5SharedContent> | null>(null)
|
// const sharedContentRef = ref(null) // hidden for now
|
||||||
const sendReceiveRef = ref<InstanceType<typeof Web5SendReceiveModals> | null>(null)
|
// const sendReceiveRef = ref(null) // wallet hidden
|
||||||
|
|
||||||
// --- Toast ---
|
// --- Toast ---
|
||||||
const identityToastText = ref('')
|
const identityToastText = ref('')
|
||||||
@ -323,13 +301,8 @@ const lndChannelBalance = ref(0)
|
|||||||
const walletError = ref('')
|
const walletError = ref('')
|
||||||
const ecashBalance = ref(0)
|
const ecashBalance = ref(0)
|
||||||
|
|
||||||
// Transactions
|
// Transactions — wallet card hidden, but loadTransactions still called for QuickActions walletConnected state
|
||||||
const walletTransactions = ref<WalletTransaction[]>([])
|
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
|
// Hardware wallets
|
||||||
const detectedHwWallets = ref<HwWalletDevice[]>([])
|
const detectedHwWallets = ref<HwWalletDevice[]>([])
|
||||||
@ -392,11 +365,11 @@ async function detectHardwareWallets() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadBalances() {
|
// function reloadBalances() { // wallet hidden
|
||||||
loadLndBalances()
|
// loadLndBalances()
|
||||||
loadEcashBalance()
|
// loadEcashBalance()
|
||||||
loadTransactions()
|
// loadTransactions()
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Auto-refresh wallet data every 30s
|
// Auto-refresh wallet data every 30s
|
||||||
let walletRefreshInterval: ReturnType<typeof setInterval> | null = null
|
let walletRefreshInterval: ReturnType<typeof setInterval> | null = null
|
||||||
@ -418,12 +391,12 @@ onMounted(() => {
|
|||||||
connectedNodesRef.value?.loadConnectionRequests()
|
connectedNodesRef.value?.loadConnectionRequests()
|
||||||
identitiesRef.value?.loadIdentities()
|
identitiesRef.value?.loadIdentities()
|
||||||
nodeVisibilityRef.value?.loadVisibility()
|
nodeVisibilityRef.value?.loadVisibility()
|
||||||
domainsRef.value?.loadDomainNames()
|
// domainsRef.value?.loadDomainNames() // hidden for now
|
||||||
nostrRelaysRef.value?.loadNostrRelays()
|
nostrRelaysRef.value?.loadNostrRelays()
|
||||||
dwnRef.value?.loadDwnStatus()
|
// dwnRef.value?.loadDwnStatus() // hidden for now
|
||||||
dwnRef.value?.loadDwnProtocols()
|
// dwnRef.value?.loadDwnProtocols() // hidden for now
|
||||||
credentialsRef.value?.loadCredentials()
|
// credentialsRef.value?.loadCredentials() // hidden for now
|
||||||
sharedContentRef.value?.loadContentItems()
|
// sharedContentRef.value?.loadContentItems() // hidden for now
|
||||||
|
|
||||||
// Load local state data
|
// Load local state data
|
||||||
loadEcashBalance()
|
loadEcashBalance()
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h2 class="text-xl font-semibold text-white mb-2">{{ t('web5.connectedNodes') }}</h2>
|
<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>
|
||||||
<div class="flex gap-2 shrink-0">
|
<div class="flex gap-2 shrink-0">
|
||||||
<button
|
<button
|
||||||
@ -37,7 +36,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-semibold text-white">{{ t('web5.connectedNodes') }}</h2>
|
<h2 class="text-xl font-semibold text-white">{{ t('web5.connectedNodes') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-white/70 text-sm mb-3">{{ t('web5.peerNodesDescription') }}</p>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@click="router.push('/dashboard/server/federation')"
|
@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>
|
<template>
|
||||||
<!-- Node Visibility -->
|
<!-- 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 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">
|
<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">
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Nostr Relays -->
|
<!-- 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 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">
|
<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">
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Wallet -->
|
<!-- 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 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">
|
<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">
|
<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