archy/neode-ui/src/App.vue

271 lines
10 KiB
Vue
Raw Normal View History

2026-01-24 22:59:20 +00:00
<template>
<div id="app">
<!-- Splash Screen (only on first visit) -->
<SplashScreen v-if="showSplash" @complete="handleSplashComplete" />
<!-- Main App Content - only show after splash and routing is complete -->
<div v-if="!showSplash && !isReady" class="min-h-screen bg-black" />
<RouterView v-else-if="!showSplash && isReady" />
2026-01-24 22:59:20 +00:00
<!-- Spotlight command palette (Cmd+K / Ctrl+K) -->
<SpotlightSearch />
<!-- CLI popup (F key) -->
<CLIPopup />
<!-- App launcher overlay (iframe popup) -->
<AppLauncherOverlay />
<!-- Global toast notifications -->
<ToastStack />
<!-- Screensaver -->
<Screensaver />
<!-- Help guide modal (from spotlight) -->
<HelpGuideModal
:show="spotlightStore.helpModal.show"
:title="spotlightStore.helpModal.title"
:content="spotlightStore.helpModal.content"
:related-path="spotlightStore.helpModal.relatedPath"
@close="spotlightStore.closeHelpModal()"
/>
2026-01-24 22:59:20 +00:00
<!-- PWA Update Prompt -->
<PWAUpdatePrompt />
<!-- PWA Install Prompt (Install app, not just Add to Home Screen) -->
<PWAInstallPrompt />
<!-- Toast notifications - top right, glass style, any page -->
<Teleport to="body">
<Transition name="toast">
<div
v-if="toastMessage.show"
@click="messageToast.dismissToastAndOpenMessages"
class="fixed top-20 right-4 left-4 z-[100] w-auto max-w-md cursor-pointer rounded-xl p-4 transition-all hover:border-white/30 hover:shadow-2xl md:top-6 md:right-6 md:left-auto md:max-w-md toast-glass"
>
<div class="flex items-start gap-3">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-orange-500/20">
<svg class="h-5 w-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-white">New message</p>
<p class="mt-0.5 text-sm text-white/70 line-clamp-2">{{ toastMessage.text }}</p>
<p class="mt-1 text-xs text-orange-400">Click to view</p>
</div>
</div>
</div>
</Transition>
</Teleport>
2026-01-24 22:59:20 +00:00
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
2026-01-24 22:59:20 +00:00
import { useRouter, useRoute } from 'vue-router'
import SplashScreen from './components/SplashScreen.vue'
import PWAUpdatePrompt from './components/PWAUpdatePrompt.vue'
import PWAInstallPrompt from './components/PWAInstallPrompt.vue'
import SpotlightSearch from './components/SpotlightSearch.vue'
import CLIPopup from './components/CLIPopup.vue'
import AppLauncherOverlay from './components/AppLauncherOverlay.vue'
import ToastStack from './components/ToastStack.vue'
import Screensaver from './components/Screensaver.vue'
import HelpGuideModal from './components/HelpGuideModal.vue'
import { useControllerNav } from '@/composables/useControllerNav'
import { playKeyboardTypingSound } from '@/composables/useLoginSounds'
import { useSpotlightStore } from '@/stores/spotlight'
import { useCLIStore } from '@/stores/cli'
import { useMessageToast } from '@/composables/useMessageToast'
import { useAppStore } from '@/stores/app'
import { useScreensaverStore } from '@/stores/screensaver'
import { useUIModeStore } from '@/stores/uiMode'
import { startRemoteRelay, stopRemoteRelay } from '@/api/remote-relay'
2026-01-24 22:59:20 +00:00
const router = useRouter()
const screensaverStore = useScreensaverStore()
const spotlightStore = useSpotlightStore()
const cliStore = useCLIStore()
const appStore = useAppStore()
const uiModeStore = useUIModeStore()
const messageToast = useMessageToast()
const toastMessage = messageToast.toastMessage
useControllerNav()
// Start/stop message polling and remote relay when auth state changes
watch(() => appStore.isAuthenticated, (authenticated) => {
if (authenticated) {
messageToast.startPolling()
screensaverStore.resetInactivityTimer()
// Don't start relay on kiosk — kiosk gets input via xdotool (system-level),
// relay would duplicate every keystroke/click as DOM events
const isKiosk = window.location.pathname.startsWith('/kiosk')
if (!isKiosk) {
startRemoteRelay()
}
} else {
messageToast.stopPolling()
toastMessage.value = { show: false, text: '' }
screensaverStore.clearInactivityTimer()
screensaverStore.deactivate()
stopRemoteRelay()
}
}, { immediate: true })
// Reset screensaver inactivity on user activity (when authenticated)
function onUserActivity() {
if (appStore.isAuthenticated && !screensaverStore.isActive) {
screensaverStore.resetInactivityTimer()
}
}
function onKeyDown(e: KeyboardEvent) {
const isMac = navigator.platform.toUpperCase().includes('MAC')
const mod = isMac ? e.metaKey : e.ctrlKey
// Cmd+K / Ctrl+K only (modifier required - avoids accidental trigger when typing)
const target = e.target as HTMLElement
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
if (mod && e.key === 'k') {
e.preventDefault()
spotlightStore.toggle()
return
}
// F key - CLI popup (skip when in input or modifier held)
if ((e.key === 'f' || e.key === 'F') && !isInput && !mod && !e.altKey) {
e.preventDefault()
cliStore.toggle()
return
}
// Cmd+1/2/3 - switch UI mode (skip when in input)
if (mod && !isInput && appStore.isAuthenticated) {
if (e.key === '1') { e.preventDefault(); uiModeStore.setMode('easy'); router.push('/dashboard'); return }
if (e.key === '2') { e.preventDefault(); uiModeStore.setMode('gamer'); router.push('/dashboard'); return }
if (e.key === '3') { e.preventDefault(); router.push('/dashboard/chat'); return }
}
// Cmd+M / Ctrl+M - cycle UI mode (skip when in input)
if (mod && (e.key === 'm' || e.key === 'M') && !isInput && appStore.isAuthenticated) {
e.preventDefault()
uiModeStore.cycleMode()
router.push('/dashboard')
return
}
// 's' key activates screensaver when authenticated (skip if typing in input)
if (e.key === 's' || e.key === 'S') {
if (!isInput && appStore.isAuthenticated && !screensaverStore.isActive) {
e.preventDefault()
screensaverStore.activate()
}
}
// Keyboard typing sound - plays on any character typed in inputs (global)
if (isInput && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
playKeyboardTypingSound()
}
}
2026-01-24 22:59:20 +00:00
const route = useRoute()
// Start with splash hidden — onMounted decides whether to show it
const showSplash = ref(false)
2026-01-24 22:59:20 +00:00
const isReady = ref(false)
/**
* Determine if splash screen should be shown
* Splash is skipped if:
* - User has already seen the intro
* - User is on a direct route (refresh/bookmark)
*/
// Fix Chromium backdrop-filter rendering bug: when tab loses/regains focus,
// the compositor fails to repaint backdrop-filter layers over animated
// fixed-position overlays (body::before/after with mix-blend-mode).
// On return: strip backdrop-filter via class, wait a frame, then restore.
function onVisibilityChange() {
if (document.hidden) {
document.documentElement.classList.add('tab-hidden')
} else {
fix: production onboarding, CI tests, container security, keyboard nav Install & Onboarding: - Remove DEV_MODE=true from production ISO service file (auto-created users, skipped password setup) - Auto-install no longer overwrites rootfs service file with bad template - Login.vue always checks auth.isSetup — shows password creation form on fresh install without requiring dev build flag - Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes - First-boot-containers sources image-versions.sh, runs podman as archipelago user (rootless), enables linger + podman.socket - Correct volume ownership (100000:100000 for rootless UID mapping) Container Security: - FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access - FileBrowser: add --read-only, /data volume for database, proper cmd args - First-boot script matches backend config (security hardening + health check) CI Pipeline: - Add vue-tsc type check + vitest run to build-iso.yml (runs every push) - Add post-install-tests.yml workflow (workflow_dispatch, SSH to target) - Build report: set +eo pipefail, fix rootfs path, add || true guards - Bundle run-post-install-tests.sh into ISO E2E Test Suite (scripts/run-post-install-tests.sh): - Phase 1: Install verification (files, services, podman, linger, DEV_MODE check) - Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete) - Phase 3: Container lifecycle (install 3 apps via package.install RPC, verify running, stop, verify stopped, restart, verify running, health) - Phase 4: Log verification (first-boot log, diagnostics, journal errors) - Correct package.install params: {"id", "dockerImage"} Frontend: - Fix backdrop-filter tab-switch bug (keep animations paused during rebuild) - Dashboard glitch animations paused during tab-hidden - Gamepad nav: auto-focus first container on route change - Tab roving: Left/Right on role="tab" cycles and activates sibling tabs - ContainerApps: data-controller-launch on running app cards - 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
// Step 1: strip backdrop-filter while animations stay paused (tab-hidden)
document.documentElement.classList.add('no-backdrop')
fix: production onboarding, CI tests, container security, keyboard nav Install & Onboarding: - Remove DEV_MODE=true from production ISO service file (auto-created users, skipped password setup) - Auto-install no longer overwrites rootfs service file with bad template - Login.vue always checks auth.isSetup — shows password creation form on fresh install without requiring dev build flag - Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes - First-boot-containers sources image-versions.sh, runs podman as archipelago user (rootless), enables linger + podman.socket - Correct volume ownership (100000:100000 for rootless UID mapping) Container Security: - FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access - FileBrowser: add --read-only, /data volume for database, proper cmd args - First-boot script matches backend config (security hardening + health check) CI Pipeline: - Add vue-tsc type check + vitest run to build-iso.yml (runs every push) - Add post-install-tests.yml workflow (workflow_dispatch, SSH to target) - Build report: set +eo pipefail, fix rootfs path, add || true guards - Bundle run-post-install-tests.sh into ISO E2E Test Suite (scripts/run-post-install-tests.sh): - Phase 1: Install verification (files, services, podman, linger, DEV_MODE check) - Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete) - Phase 3: Container lifecycle (install 3 apps via package.install RPC, verify running, stop, verify stopped, restart, verify running, health) - Phase 4: Log verification (first-boot log, diagnostics, journal errors) - Correct package.install params: {"id", "dockerImage"} Frontend: - Fix backdrop-filter tab-switch bug (keep animations paused during rebuild) - Dashboard glitch animations paused during tab-hidden - Gamepad nav: auto-focus first container on route change - Tab roving: Left/Right on role="tab" cycles and activates sibling tabs - ContainerApps: data-controller-launch on running app cards - 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
// Step 2: restore backdrop-filter over static content (clean compositor rebuild)
// Use setTimeout — Chromium batches rAFs on tab return
setTimeout(() => {
document.documentElement.classList.remove('no-backdrop')
// Step 3: resume animations after backdrop-filter layers are established
requestAnimationFrame(() => {
fix: production onboarding, CI tests, container security, keyboard nav Install & Onboarding: - Remove DEV_MODE=true from production ISO service file (auto-created users, skipped password setup) - Auto-install no longer overwrites rootfs service file with bad template - Login.vue always checks auth.isSetup — shows password creation form on fresh install without requiring dev build flag - Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes - First-boot-containers sources image-versions.sh, runs podman as archipelago user (rootless), enables linger + podman.socket - Correct volume ownership (100000:100000 for rootless UID mapping) Container Security: - FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access - FileBrowser: add --read-only, /data volume for database, proper cmd args - First-boot script matches backend config (security hardening + health check) CI Pipeline: - Add vue-tsc type check + vitest run to build-iso.yml (runs every push) - Add post-install-tests.yml workflow (workflow_dispatch, SSH to target) - Build report: set +eo pipefail, fix rootfs path, add || true guards - Bundle run-post-install-tests.sh into ISO E2E Test Suite (scripts/run-post-install-tests.sh): - Phase 1: Install verification (files, services, podman, linger, DEV_MODE check) - Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete) - Phase 3: Container lifecycle (install 3 apps via package.install RPC, verify running, stop, verify stopped, restart, verify running, health) - Phase 4: Log verification (first-boot log, diagnostics, journal errors) - Correct package.install params: {"id", "dockerImage"} Frontend: - Fix backdrop-filter tab-switch bug (keep animations paused during rebuild) - Dashboard glitch animations paused during tab-hidden - Gamepad nav: auto-focus first container on route change - Tab roving: Left/Right on role="tab" cycles and activates sibling tabs - ContainerApps: data-controller-launch on running app cards - 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
document.documentElement.classList.remove('tab-hidden')
})
fix: production onboarding, CI tests, container security, keyboard nav Install & Onboarding: - Remove DEV_MODE=true from production ISO service file (auto-created users, skipped password setup) - Auto-install no longer overwrites rootfs service file with bad template - Login.vue always checks auth.isSetup — shows password creation form on fresh install without requiring dev build flag - Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes - First-boot-containers sources image-versions.sh, runs podman as archipelago user (rootless), enables linger + podman.socket - Correct volume ownership (100000:100000 for rootless UID mapping) Container Security: - FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access - FileBrowser: add --read-only, /data volume for database, proper cmd args - First-boot script matches backend config (security hardening + health check) CI Pipeline: - Add vue-tsc type check + vitest run to build-iso.yml (runs every push) - Add post-install-tests.yml workflow (workflow_dispatch, SSH to target) - Build report: set +eo pipefail, fix rootfs path, add || true guards - Bundle run-post-install-tests.sh into ISO E2E Test Suite (scripts/run-post-install-tests.sh): - Phase 1: Install verification (files, services, podman, linger, DEV_MODE check) - Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete) - Phase 3: Container lifecycle (install 3 apps via package.install RPC, verify running, stop, verify stopped, restart, verify running, health) - Phase 4: Log verification (first-boot log, diagnostics, journal errors) - Correct package.install params: {"id", "dockerImage"} Frontend: - Fix backdrop-filter tab-switch bug (keep animations paused during rebuild) - Dashboard glitch animations paused during tab-hidden - Gamepad nav: auto-focus first container on route change - Tab roving: Left/Right on role="tab" cycles and activates sibling tabs - ContainerApps: data-controller-launch on running app cards - 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
}, 50)
}
}
onMounted(async () => {
document.addEventListener('visibilitychange', onVisibilityChange)
window.addEventListener('keydown', onKeyDown, true)
window.addEventListener('mousemove', onUserActivity)
window.addEventListener('mousedown', onUserActivity)
window.addEventListener('keydown', onUserActivity)
window.addEventListener('touchstart', onUserActivity)
2026-01-24 22:59:20 +00:00
const seenIntro = localStorage.getItem('neode_intro_seen') === '1'
const isDirectRoute = route.path !== '/'
feat: rootless podman, session hardening, boot stability, sidebar fix Rootless podman migration (TASK-11): - Remove sudo from all podman calls in PodmanClient + 8 backend files - Remove sudo from all podman/docker calls in deploy script - Restore full systemd security hardening: NoNewPrivileges, RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime, RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict - Enable loginctl linger for rootless container persistence - Remove Ollama from auto-deploy (marketplace-only) Session & auth hardening: - Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms) - Debounced 401 redirect in rpc-client.ts (prevents redirect storms) Boot stability: - optimize-debian.sh: adds chrony, swap, removes policy-rc.d - deploy script: pre-restart chrony + swap setup - ISO build: chrony package, swap file creation - BootScreen: no longer clears localStorage (prevents splash replay) - RootRedirect: sole owner of localStorage clearing on server ready UI fixes: - Sidebar opacity default changed from 0→visible (fixes missing sidebar after page-persistence login without entrance animation) - Console.log/error wrapped in import.meta.env.DEV guards - Remove unused route import from RootRedirect Beta tracking: - CLAUDE.md: beta freeze protocol added - MASTER_PLAN.md: TASK-11, TASK-17, phase structure - BETA-PROGRESS.md: initial tracking doc - Tagged v1.2.0-alpha.1 as pre-rootless baseline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
const fromBoot = sessionStorage.getItem('archipelago_from_boot') === '1'
if (fromBoot) sessionStorage.removeItem('archipelago_from_boot')
if (import.meta.env.DEV) console.log('[App] onMounted — seenIntro:', seenIntro, 'fromBoot:', fromBoot)
if (fromBoot && !seenIntro) {
feat: rootless podman, session hardening, boot stability, sidebar fix Rootless podman migration (TASK-11): - Remove sudo from all podman calls in PodmanClient + 8 backend files - Remove sudo from all podman/docker calls in deploy script - Restore full systemd security hardening: NoNewPrivileges, RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime, RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict - Enable loginctl linger for rootless container persistence - Remove Ollama from auto-deploy (marketplace-only) Session & auth hardening: - Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms) - Debounced 401 redirect in rpc-client.ts (prevents redirect storms) Boot stability: - optimize-debian.sh: adds chrony, swap, removes policy-rc.d - deploy script: pre-restart chrony + swap setup - ISO build: chrony package, swap file creation - BootScreen: no longer clears localStorage (prevents splash replay) - RootRedirect: sole owner of localStorage clearing on server ready UI fixes: - Sidebar opacity default changed from 0→visible (fixes missing sidebar after page-persistence login without entrance animation) - Console.log/error wrapped in import.meta.env.DEV guards - Remove unused route import from RootRedirect Beta tracking: - CLAUDE.md: beta freeze protocol added - MASTER_PLAN.md: TASK-11, TASK-17, phase structure - BETA-PROGRESS.md: initial tracking doc - Tagged v1.2.0-alpha.1 as pre-rootless baseline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
// Coming from boot screen — show the full splash intro (Enter to Exit → typing → logo)
showSplash.value = true
} else if (!seenIntro && !isDirectRoute && import.meta.env.VITE_DEV_MODE !== 'boot') {
// Normal first visit (not boot mode) — show splash intro
showSplash.value = true
} else {
// Already seen intro, direct route, or boot mode (boot screen handles intro)
// Set isReady BEFORE hiding splash to prevent flash of partial content
await router.isReady()
2026-01-24 22:59:20 +00:00
isReady.value = true
showSplash.value = false
document.body.classList.add('splash-complete')
2026-01-24 22:59:20 +00:00
}
})
onBeforeUnmount(() => {
document.removeEventListener('visibilitychange', onVisibilityChange)
window.removeEventListener('keydown', onKeyDown, true)
window.removeEventListener('mousemove', onUserActivity)
window.removeEventListener('mousedown', onUserActivity)
window.removeEventListener('keydown', onUserActivity)
window.removeEventListener('touchstart', onUserActivity)
})
2026-01-24 22:59:20 +00:00
/**
* Handle splash screen completion
* Routes user directly to appropriate screen based on onboarding status (from backend)
2026-01-24 22:59:20 +00:00
*/
async function handleSplashComplete() {
2026-01-24 22:59:20 +00:00
showSplash.value = false
document.body.classList.add('splash-complete')
isReady.value = true
sessionStorage.setItem('archipelago_from_splash', '1')
const devMode = import.meta.env.VITE_DEV_MODE
if (devMode === 'setup' || devMode === 'existing') {
router.push('/login').catch(() => {})
return
}
try {
const { isOnboardingComplete } = await import('@/composables/useOnboarding')
const seenOnboarding = await isOnboardingComplete()
const destination = seenOnboarding ? '/login' : '/onboarding/intro'
router.push(destination).catch(() => {})
} catch {
router.push('/onboarding/intro').catch(() => {})
}
2026-01-24 22:59:20 +00:00
}
</script>
<style>
/* Global styles are in style.css */
</style>