archy/neode-ui/src/views/Dashboard.vue

358 lines
13 KiB
Vue
Raw Normal View History

2026-01-24 22:59:20 +00:00
<template>
<div class="min-h-screen flex relative dashboard-view" :class="{ 'glass-throw-active': showZoomIn }">
<!-- Skip to main content link for keyboard users -->
<!-- Skip-to-content handled by controller nav sidebarmain transition -->
<!-- Background container with 3D perspective - full width to avoid letterboxing -->
2026-01-24 22:59:20 +00:00
<div class="bg-perspective-container">
<!-- Background - primary layer (visible for all routes, transitions out only for detail pages) -->
<div
2026-01-24 22:59:20 +00:00
ref="bgDefault"
class="bg-layer bg-fullwidth"
:class="[
{ 'bg-transitioning-out': showAltBackground },
{ 'zoom-reveal-bg': showZoomIn }
]"
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
/>
<!-- Background - detail layer (only visible during app/marketplace detail 3D transition) -->
2026-01-24 22:59:20 +00:00
<div
ref="bgAlt"
class="bg-layer bg-fullwidth"
:class="{ 'bg-transitioning-in': showAltBackground }"
style="background-image: url(/assets/img/bg-intro-3.jpg)"
2026-01-24 22:59:20 +00:00
/>
<!-- Glitch overlays - trigger on background change -->
<div
2026-01-24 22:59:20 +00:00
class="bg-glitch-layer-1"
:class="{ 'glitch-active': isGlitching }"
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
2026-01-24 22:59:20 +00:00
/>
<div
2026-01-24 22:59:20 +00:00
class="bg-glitch-layer-2"
:class="{ 'glitch-active': isGlitching }"
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
2026-01-24 22:59:20 +00:00
/>
<div
2026-01-24 22:59:20 +00:00
class="bg-glitch-scan"
:class="{ 'glitch-active': isGlitching }"
/>
<!-- Glitch overlays removed only intro glitch plays (via isGlitching) -->
2026-01-24 22:59:20 +00:00
</div>
<!-- Oomph accent - brief impact flash when dashboard loads -->
<div
v-if="showZoomIn"
class="fixed inset-0 pointer-events-none z-[100] oomph-flash"
aria-hidden="true"
/>
<!-- Reveal flashes and glitch overlay - enthralling entrance -->
<div
v-if="showZoomIn"
class="fixed inset-0 pointer-events-none z-[99] reveal-flash-glitch"
aria-hidden="true"
/>
2026-03-14 17:12:41 +00:00
<!-- Background overlay - uniform 0.2 opacity -->
<div
class="fixed inset-0 pointer-events-none bg-black/20"
2026-01-24 22:59:20 +00:00
style="z-index: -5;"
/>
<!-- Sidebar - Desktop Only -->
<DashboardSidebar :show-zoom-in="showZoomIn" @logout="handleLogout" />
2026-01-24 22:59:20 +00:00
<!-- Main Content (Xbox: Right goes here from sidebar) -->
<main
id="main-content"
data-controller-zone="main"
2026-03-14 17:12:41 +00:00
class="flex-1 overflow-hidden relative pb-0 glass-piece z-10"
:class="{ 'glass-throw-main': showZoomIn }"
tabindex="-1"
@pointerenter="activateMainScroll"
@wheel.capture="activateMainScroll"
>
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
<!-- Controller zone entry point - no switcher -->
</div>
<!-- Connection Status Banners -->
<ConnectionBanner />
2026-01-24 22:59:20 +00:00
<div class="perspective-container-wrapper glass-piece" :class="{ 'glass-throw-content': showZoomIn && !isHomeRoute }">
2026-01-24 22:59:20 +00:00
<div class="perspective-container">
<RouterView v-slot="{ Component, route }">
<Transition :name="getTransitionName(route)">
<div :key="route.path" class="view-wrapper">
<div
v-if="route.path === '/dashboard/chat' || route.path === '/dashboard/mesh'"
:class="['h-full dashboard-scroll-panel mobile-scroll-pad', mobileTabPaddingTop ? 'overflow-y-auto' : '']"
fix: overhaul container lifecycle — recovery, health, uninstall, UI state Container recovery: - Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s - Dependency-aware restarts: won't restart services before their deps - Reset dependent counters when a dependency recovers - Handle "created" state containers (were invisible to health monitor) - Added IndeedHub, mempool-api, mysql to tier system - Crash recovery: podman start timeout 30s→120s with retry - Podman client: socket timeout 5s→30s, added restart policy UI state representation: - Exit code 0 shows "stopped" (gray), not "crashed" (red) - Exit code 137 shows "killed (OOM)" - Non-zero exit shows "crashed" (red) - Added exit_code field to PackageDataEntry Install/uninstall fixes: - Install returns error when container doesn't start (was silent success) - Post-install hooks awaited instead of fire-and-forget tokio::spawn - Uninstall: graceful rm before force, volume prune, network cleanup - Uninstall returns error on partial failure (was 200 OK) Config consistency: - DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded) - Bitcoin: added ZMQ ports 28332/28333 for LND block notifications - IndeedHub port 7777→8190 (was conflicting with strfry) - Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0 Performance: - Metrics collector interval 60s→300s (was duplicating health monitor) - Podman client: proper error propagation instead of unwrap_or_default Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
:style="{ paddingTop: mobileTabPaddingTop ? (mobileTabPaddingTop + 16) + 'px' : undefined }"
class="mobile-safe-top"
>
<component :is="Component" />
</div>
<div
v-else
2026-01-24 22:59:20 +00:00
:class="[
'absolute inset-0 px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto mobile-safe-top dashboard-scroll-panel',
needsMobileBackButtonSpace
2026-03-14 17:12:41 +00:00
? 'mobile-scroll-pad-back'
: 'mobile-scroll-pad'
2026-01-24 22:59:20 +00:00
]"
:style="mobileTabPaddingTop ? { paddingTop: (mobileTabPaddingTop + 16) + 'px' } : undefined"
2026-01-24 22:59:20 +00:00
>
2026-03-14 17:12:41 +00:00
<component :is="Component" class="view-container flex-none" />
<!-- Bottom spacer scroll clearance on all pages -->
<div class="shrink-0 h-6 md:h-12" aria-hidden="true"></div>
2026-01-24 22:59:20 +00:00
</div>
</div>
</Transition>
</RouterView>
</div>
</div>
<!-- Panel mode app session renders alongside current page content -->
<Transition name="panel-slide">
<div v-if="appLauncher.panelAppId" class="app-panel-container">
<AppSession :app-id-prop="appLauncher.panelAppId" @close="appLauncher.closePanel()" />
</div>
</Transition>
2026-01-24 22:59:20 +00:00
</main>
<!-- Persistent Mobile Tabs + Bottom Tab Bar outside <main> so position:fixed isn't broken by will-change:transform -->
<DashboardMobileNav ref="mobileNavRef" :show-zoom-in="showZoomIn" />
<!-- Health Notifications Toast -->
<HealthNotifications />
<!-- First-use companion intro overlay -->
<CompanionIntroOverlay />
2026-01-24 22:59:20 +00:00
</div>
</template>
<script setup lang="ts">
2026-04-11 13:35:52 +01:00
import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { RouterView, useRouter, useRoute } from 'vue-router'
2026-01-24 22:59:20 +00:00
import { useAppStore } from '../stores/app'
import { useAppLauncherStore } from '../stores/appLauncher'
import AppSession from '@/views/AppSession.vue'
import { useLoginTransitionStore } from '../stores/loginTransition'
import { playDashboardLoadOomph } from '@/composables/useLoginSounds'
import DashboardSidebar from '@/views/dashboard/DashboardSidebar.vue'
import DashboardMobileNav from '@/views/dashboard/DashboardMobileNav.vue'
import ConnectionBanner from '@/views/dashboard/ConnectionBanner.vue'
import HealthNotifications from '@/views/dashboard/HealthNotifications.vue'
import CompanionIntroOverlay from '@/components/CompanionIntroOverlay.vue'
import { useRouteTransitions, isDetailRoute, ROUTE_BACKGROUNDS } from '@/views/dashboard/useRouteTransitions'
import '@/views/dashboard/dashboard-styles.css'
2026-01-24 22:59:20 +00:00
const router = useRouter()
const route = useRoute()
const store = useAppStore()
const appLauncher = useAppLauncherStore()
const loginTransition = useLoginTransitionStore()
const { getTransitionName } = useRouteTransitions()
const showZoomIn = ref(false)
const pendingTimers: ReturnType<typeof setTimeout>[] = []
function scheduledTimeout(fn: () => void, ms: number) {
const id = setTimeout(fn, ms)
pendingTimers.push(id)
return id
}
2026-01-24 22:59:20 +00:00
const showAltBackground = ref(false)
const isHomeRoute = computed(() => route.path === '/dashboard' || route.path === '/dashboard/')
2026-01-24 22:59:20 +00:00
const isGlitching = ref(false)
const backgroundImage = computed(() => {
2026-04-11 13:35:52 +01:00
const mapped = ROUTE_BACKGROUNDS[route.path]
if (mapped) return mapped
// Cloud subpages (folders) use the same background as Cloud
if (route.path.startsWith('/dashboard/cloud/')) return 'bg-cloud.jpg'
if (isDetailRoute(route.path)) return 'bg-intro.jpg'
2026-04-11 13:35:52 +01:00
return 'bg-home.jpg'
2026-01-24 22:59:20 +00:00
})
const isDarkRoute = computed(() => {
const p = route.path
return p.includes('/dashboard/web5') ||
p.includes('/dashboard/server') ||
p.includes('/dashboard/settings') ||
(p.includes('/dashboard/apps') && !isDetailRoute(p)) ||
p.includes('/dashboard/marketplace') ||
p.includes('/dashboard/discover') ||
p.includes('/dashboard/cloud')
2026-01-24 22:59:20 +00:00
})
const showDarkOverlay = ref(isDarkRoute.value)
let overlayTimer: ReturnType<typeof setTimeout> | null = null
watch(isDarkRoute, (dark) => {
if (overlayTimer) { clearTimeout(overlayTimer); overlayTimer = null }
if (dark) {
showDarkOverlay.value = true
} else {
overlayTimer = scheduledTimeout(() => { showDarkOverlay.value = false }, 450)
}
2026-01-24 22:59:20 +00:00
})
2026-04-11 13:35:52 +01:00
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)
2026-01-24 22:59:20 +00:00
const mobileTabPaddingTop = computed(() => {
return mobileNavRef.value?.mobileTabPaddingTop ?? 0
})
2026-01-24 22:59:20 +00:00
2026-04-11 13:35:52 +01:00
// 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)
})
}
function activateMainScroll() {
const active = document.activeElement as HTMLElement | null
if (active?.closest?.('[data-controller-zone="sidebar"]')) {
active.blur()
document.getElementById('main-content')?.focus({ preventScroll: true })
}
}
2026-01-24 22:59:20 +00:00
watch(() => route.path, (newPath) => {
const isAppDetails = isDetailRoute(newPath)
const wasAppDetails = showAltBackground.value
2026-04-11 13:35:52 +01:00
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
2026-01-24 22:59:20 +00:00
showAltBackground.value = isAppDetails
2026-01-24 22:59:20 +00:00
if (isAppDetails && !wasAppDetails) {
scheduledTimeout(() => {
2026-01-24 22:59:20 +00:00
isGlitching.value = true
scheduledTimeout(() => { isGlitching.value = false }, 375)
}, 500)
2026-01-24 22:59:20 +00:00
}
})
onMounted(() => {
2026-04-11 13:35:52 +01:00
previousRoutePath = route.path
document.body.classList.add('dashboard-active')
release(v1.7.33-alpha): onboarding/login UX fixes + PWA cache bust - useOnboarding.ts: prefer the backend over localStorage when checking onboarding completion. The old order (localStorage first) meant any browser that had ever onboarded a node would treat every new fresh node as already-onboarded and skip the wizard, dumping the user straight at the inline set-password form. Backend is now authoritative; localStorage stays as the offline fallback. - OnboardingWrapper.vue: skip the intro video on `/login` once `neode_onboarding_complete` is set. Returning logged-out users now get the static lock-screen background + glitch overlay instead of replaying the full intro on every logout. - RootRedirect.vue: when the health check fails, only show the full BootScreen if the node was never onboarded. For already-onboarded nodes (i.e. an OTA-update blip), keep the spinner and poll the health endpoint every 2s for up to 60s before falling back to the boot screen. Fixes the "fake boot loader" / "server starting up" screens flashing on every successful update. - loginTransition store: new `justCompletedOnboarding` flag distinct from `justLoggedIn`. Set true only by the inline setup-password flow (handleSetup). Dashboard.vue branches on it: full glitch+zoom reveal for the post-onboarding entry, quick zoom + welcome typing on every other login (no triple glitch flashes, ~1.2s vs 8s). - vite.config.ts: bump assets cache from `assets-cache-v2` to `assets-cache-v3` so service workers running the previous bundle invalidate their cache and pick up the new UI cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 04:45:33 -04:00
if (loginTransition.justCompletedOnboarding) {
// Full glitchy reveal — only on the very first dashboard entry
// right after onboarding (one-time event, persists in feel).
playDashboardLoadOomph()
showZoomIn.value = true
loginTransition.setPendingWelcomeTyping(true)
release(v1.7.33-alpha): onboarding/login UX fixes + PWA cache bust - useOnboarding.ts: prefer the backend over localStorage when checking onboarding completion. The old order (localStorage first) meant any browser that had ever onboarded a node would treat every new fresh node as already-onboarded and skip the wizard, dumping the user straight at the inline set-password form. Backend is now authoritative; localStorage stays as the offline fallback. - OnboardingWrapper.vue: skip the intro video on `/login` once `neode_onboarding_complete` is set. Returning logged-out users now get the static lock-screen background + glitch overlay instead of replaying the full intro on every logout. - RootRedirect.vue: when the health check fails, only show the full BootScreen if the node was never onboarded. For already-onboarded nodes (i.e. an OTA-update blip), keep the spinner and poll the health endpoint every 2s for up to 60s before falling back to the boot screen. Fixes the "fake boot loader" / "server starting up" screens flashing on every successful update. - loginTransition store: new `justCompletedOnboarding` flag distinct from `justLoggedIn`. Set true only by the inline setup-password flow (handleSetup). Dashboard.vue branches on it: full glitch+zoom reveal for the post-onboarding entry, quick zoom + welcome typing on every other login (no triple glitch flashes, ~1.2s vs 8s). - vite.config.ts: bump assets cache from `assets-cache-v2` to `assets-cache-v3` so service workers running the previous bundle invalidate their cache and pick up the new UI cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 04:45:33 -04:00
loginTransition.setJustCompletedOnboarding(false)
loginTransition.setJustLoggedIn(false)
const triggerRevealGlitch = () => {
isGlitching.value = true
scheduledTimeout(() => { isGlitching.value = false }, 380)
}
scheduledTimeout(triggerRevealGlitch, 500)
scheduledTimeout(triggerRevealGlitch, 1200)
scheduledTimeout(triggerRevealGlitch, 2000)
scheduledTimeout(() => { showZoomIn.value = false }, 8000)
scheduledTimeout(() => {
loginTransition.setStartWelcomeTyping(true)
loginTransition.setPendingWelcomeTyping(false)
}, 4000)
release(v1.7.33-alpha): onboarding/login UX fixes + PWA cache bust - useOnboarding.ts: prefer the backend over localStorage when checking onboarding completion. The old order (localStorage first) meant any browser that had ever onboarded a node would treat every new fresh node as already-onboarded and skip the wizard, dumping the user straight at the inline set-password form. Backend is now authoritative; localStorage stays as the offline fallback. - OnboardingWrapper.vue: skip the intro video on `/login` once `neode_onboarding_complete` is set. Returning logged-out users now get the static lock-screen background + glitch overlay instead of replaying the full intro on every logout. - RootRedirect.vue: when the health check fails, only show the full BootScreen if the node was never onboarded. For already-onboarded nodes (i.e. an OTA-update blip), keep the spinner and poll the health endpoint every 2s for up to 60s before falling back to the boot screen. Fixes the "fake boot loader" / "server starting up" screens flashing on every successful update. - loginTransition store: new `justCompletedOnboarding` flag distinct from `justLoggedIn`. Set true only by the inline setup-password flow (handleSetup). Dashboard.vue branches on it: full glitch+zoom reveal for the post-onboarding entry, quick zoom + welcome typing on every other login (no triple glitch flashes, ~1.2s vs 8s). - vite.config.ts: bump assets cache from `assets-cache-v2` to `assets-cache-v3` so service workers running the previous bundle invalidate their cache and pick up the new UI cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 04:45:33 -04:00
} else if (loginTransition.justLoggedIn) {
// Regular re-login — no zoom, no glitch. Just land on the
// dashboard and kick off the welcome typing quickly.
release(v1.7.33-alpha): onboarding/login UX fixes + PWA cache bust - useOnboarding.ts: prefer the backend over localStorage when checking onboarding completion. The old order (localStorage first) meant any browser that had ever onboarded a node would treat every new fresh node as already-onboarded and skip the wizard, dumping the user straight at the inline set-password form. Backend is now authoritative; localStorage stays as the offline fallback. - OnboardingWrapper.vue: skip the intro video on `/login` once `neode_onboarding_complete` is set. Returning logged-out users now get the static lock-screen background + glitch overlay instead of replaying the full intro on every logout. - RootRedirect.vue: when the health check fails, only show the full BootScreen if the node was never onboarded. For already-onboarded nodes (i.e. an OTA-update blip), keep the spinner and poll the health endpoint every 2s for up to 60s before falling back to the boot screen. Fixes the "fake boot loader" / "server starting up" screens flashing on every successful update. - loginTransition store: new `justCompletedOnboarding` flag distinct from `justLoggedIn`. Set true only by the inline setup-password flow (handleSetup). Dashboard.vue branches on it: full glitch+zoom reveal for the post-onboarding entry, quick zoom + welcome typing on every other login (no triple glitch flashes, ~1.2s vs 8s). - vite.config.ts: bump assets cache from `assets-cache-v2` to `assets-cache-v3` so service workers running the previous bundle invalidate their cache and pick up the new UI cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 04:45:33 -04:00
playDashboardLoadOomph()
loginTransition.setPendingWelcomeTyping(true)
loginTransition.setJustLoggedIn(false)
scheduledTimeout(() => {
loginTransition.setStartWelcomeTyping(true)
loginTransition.setPendingWelcomeTyping(false)
}, 300)
}
window.addEventListener('keydown', handleKioskShortcuts)
2026-01-24 22:59:20 +00:00
})
onBeforeUnmount(() => {
document.body.classList.remove('dashboard-active')
window.removeEventListener('keydown', handleKioskShortcuts)
for (const id of pendingTimers) clearTimeout(id)
pendingTimers.length = 0
if (overlayTimer) { clearTimeout(overlayTimer); overlayTimer = null }
2026-01-24 22:59:20 +00:00
})
function isKioskMode(): boolean {
try {
return localStorage.getItem('kiosk') === 'true' || new URLSearchParams(window.location.search).has('kiosk')
} catch { return false }
}
function handleKioskShortcuts(e: KeyboardEvent) {
if (!isKioskMode()) return
if (e.ctrlKey && e.shiftKey) {
if (e.key === 'R' || e.key === 'r') {
e.preventDefault()
router.push('/recovery')
} else if (e.key === 'H' || e.key === 'h') {
e.preventDefault()
router.push('/dashboard')
} else if (e.key === 'Q' || e.key === 'q') {
e.preventDefault()
if (confirm('Reboot the server?')) {
fetch('/rpc/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'system.reboot' }) }).catch(() => {})
}
}
}
}
2026-01-24 22:59:20 +00:00
async function handleLogout() {
try {
await store.logout()
} catch {
/* proceed to login regardless */
}
router.push('/login').catch(() => {
window.location.href = '/login'
})
2026-01-24 22:59:20 +00:00
}
</script>
<!-- Styles extracted to dashboard/dashboard-styles.css -->