From 68b02359dc09a6a865e8ca8958a913f1e24347d1 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 11 Apr 2026 13:35:52 +0100 Subject: [PATCH] ui updates --- neode-ui/mock-backend.js | 147 ++++++++++++++++++ neode-ui/src/components/Screensaver.vue | 29 +++- neode-ui/src/stores/screensaver.ts | 9 +- neode-ui/src/style.css | 11 +- neode-ui/src/views/Dashboard.vue | 52 ++++++- neode-ui/src/views/Fleet.vue | 25 ++- neode-ui/src/views/Monitoring.vue | 24 ++- neode-ui/src/views/RootRedirect.vue | 4 +- .../views/dashboard/useRouteTransitions.ts | 25 ++- .../src/views/federation/FederationHeader.vue | 17 +- neode-ui/src/views/home/HomeWalletCard.vue | 2 +- neode-ui/src/views/settings/BackupSection.vue | 10 +- .../src/views/settings/SystemDangerZone.vue | 42 ++--- neode-ui/src/views/web5/Web5.vue | 101 +++++------- .../src/views/web5/Web5ConnectedNodes.vue | 2 - neode-ui/src/views/web5/Web5Federation.vue | 88 +++++++++++ neode-ui/src/views/web5/Web5Monitoring.vue | 133 ++++++++++++++++ .../src/views/web5/Web5NodeVisibility.vue | 2 +- neode-ui/src/views/web5/Web5NostrRelays.vue | 2 +- neode-ui/src/views/web5/Web5Wallet.vue | 2 +- 20 files changed, 614 insertions(+), 113 deletions(-) create mode 100644 neode-ui/src/views/web5/Web5Federation.vue create mode 100644 neode-ui/src/views/web5/Web5Monitoring.vue diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 2ffd8de6..c33f17e5 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -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({ diff --git a/neode-ui/src/components/Screensaver.vue b/neode-ui/src/components/Screensaver.vue index ff463d3c..06193c9f 100644 --- a/neode-ui/src/components/Screensaver.vue +++ b/neode-ui/src/components/Screensaver.vue @@ -7,8 +7,12 @@ @click="store.deactivate()" @keydown.escape="store.deactivate()" > - -
+ +
+ +
+ +
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); + } +} diff --git a/neode-ui/src/stores/screensaver.ts b/neode-ui/src/stores/screensaver.ts index 1393ad0d..fab8c1d8 100644 --- a/neode-ui/src/stores/screensaver.ts +++ b/neode-ui/src/stores/screensaver.ts @@ -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 | 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, diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index 3c50a0d6..b096bc25 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -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; } } diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue index d7b31141..9ed860b9 100644 --- a/neode-ui/src/views/Dashboard.vue +++ b/neode-ui/src/views/Dashboard.vue @@ -125,7 +125,7 @@ diff --git a/neode-ui/src/views/Monitoring.vue b/neode-ui/src/views/Monitoring.vue index 66c9c722..62d97861 100644 --- a/neode-ui/src/views/Monitoring.vue +++ b/neode-ui/src/views/Monitoring.vue @@ -1,5 +1,25 @@