@@ -134,39 +137,46 @@
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAppStore } from '../stores/app'
+import { useLoginTransitionStore } from '../stores/loginTransition'
import { rpcClient } from '../api/rpc-client'
+import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh } from '@/composables/useLoginSounds'
const router = useRouter()
const store = useAppStore()
+const loginTransition = useLoginTransitionStore()
const password = ref('')
const confirmPassword = ref('')
const loading = ref(false)
const error = ref
(null)
const isSetup = ref(false)
+const whooshAway = ref(false)
// Check if we're in setup mode (original StartOS node setup)
const isSetupMode = computed(() => {
return import.meta.env.VITE_DEV_MODE === 'setup'
})
-// Check if node is already set up
onMounted(async () => {
+ if (sessionStorage.getItem('archipelago_from_splash') !== '1') {
+ startSynthwave()
+ } else {
+ sessionStorage.removeItem('archipelago_from_splash')
+ }
if (isSetupMode.value) {
try {
const result = await rpcClient.call({ method: 'auth.isSetup', params: {} })
isSetup.value = Boolean(result)
} catch (err) {
console.error('Failed to check setup status:', err)
- // Assume not set up if check fails
isSetup.value = false
}
} else {
- // Not in setup mode, assume already set up
isSetup.value = true
}
})
+
async function handleSetup() {
if (!password.value || password.value.length < 8) {
error.value = 'Password must be at least 8 characters'
@@ -187,11 +197,17 @@ async function handleSetup() {
params: { password: password.value }
})
- // After setup, automatically log in
+ stopSynthwave()
+ whooshAway.value = true
+ playLoginSuccessWhoosh()
+ loginTransition.setJustLoggedIn(true)
await store.login(password.value)
- router.push('/dashboard')
+ await new Promise(r => setTimeout(r, 350))
+ router.replace({ name: 'home' })
} catch (err) {
+ whooshAway.value = false
error.value = err instanceof Error ? err.message : 'Setup failed. Please try again.'
+ startSynthwave()
} finally {
loading.value = false
}
@@ -205,9 +221,16 @@ async function handleLogin() {
try {
await store.login(password.value)
- router.push('/dashboard')
+ stopSynthwave()
+ whooshAway.value = true
+ playLoginSuccessWhoosh()
+ loginTransition.setJustLoggedIn(true)
+ await new Promise(r => setTimeout(r, 350))
+ router.replace({ name: 'home' })
} catch (err) {
+ whooshAway.value = false
error.value = err instanceof Error ? err.message : 'Login failed. Please check your password.'
+ startSynthwave()
} finally {
loading.value = false
}
@@ -221,3 +244,11 @@ function replayIntro() {
}
+
diff --git a/neode-ui/src/views/OnboardingWrapper.vue b/neode-ui/src/views/OnboardingWrapper.vue
index ba8db278..0a89efd2 100644
--- a/neode-ui/src/views/OnboardingWrapper.vue
+++ b/neode-ui/src/views/OnboardingWrapper.vue
@@ -20,7 +20,19 @@
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
@@ -52,6 +63,7 @@
@@ -338,16 +366,16 @@ onMounted(() => {
height: 100%;
}
-/* Forward transition: Current screen pulls forward, new screen emerges from back */
+/* 2advanced-style: fluid depth transitions */
.depth-forward-enter-active.view-wrapper,
.depth-forward-leave-active.view-wrapper {
- transition: all 0.7s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ transition: all 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.depth-forward-enter-from.view-wrapper {
opacity: 0;
- transform: translateZ(-1500px) scale(0.5);
- filter: blur(8px);
+ transform: translateZ(-1200px) scale(0.6);
+ filter: blur(10px);
}
.depth-forward-enter-to.view-wrapper {
@@ -364,13 +392,13 @@ onMounted(() => {
.depth-forward-leave-to.view-wrapper {
opacity: 0;
- transform: translateZ(600px) scale(1.4);
- filter: blur(12px);
+ transform: translateZ(500px) scale(1.25);
+ filter: blur(10px);
}
-/* Background zoom effect - makes you feel like you're going deeper */
+/* Background zoom - 2advanced fluid */
.bg-zoom {
- transition: transform 1.5s cubic-bezier(0.4, 0, 0.2, 1);
+ transition: transform 1.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
transform: scale(1);
}
@@ -378,14 +406,14 @@ onMounted(() => {
transform: scale(1.15);
}
-/* Enhanced effect with rotation for more console-like feel */
+/* Subtle 3D tilt - 2advanced layered depth */
@media (min-width: 768px) {
.depth-forward-enter-from.view-wrapper {
- transform: translateZ(-1500px) scale(0.5) rotateX(15deg);
+ transform: translateZ(-1200px) scale(0.6) rotateX(6deg);
}
.depth-forward-leave-to.view-wrapper {
- transform: translateZ(600px) scale(1.4) rotateX(-10deg);
+ transform: translateZ(500px) scale(1.25) rotateX(-4deg);
}
}
@@ -397,6 +425,16 @@ onMounted(() => {
perspective-origin: 50% 50%;
z-index: -10;
overflow: hidden;
+ min-width: 100vw;
+ width: 100vw;
+}
+
+/* Full width background on every screen */
+.bg-fullwidth {
+ min-width: 100vw;
+ width: 100vw;
+ background-size: cover;
+ background-position: center center;
}
.bg-layer {
@@ -425,6 +463,79 @@ video.bg-layer {
transform: translateZ(0) scale(1);
}
+/* Login: static background - just there, no zoom */
+.bg-login-static {
+ opacity: 1;
+ transform: none;
+}
+
+/* Archipelago-style glitch overlays for login - continuous every 5s */
+.login-glitch-layer {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 5;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ opacity: 0;
+}
+
+.login-glitch-1 {
+ mix-blend-mode: screen;
+ filter: hue-rotate(22deg) saturate(1.35);
+ animation: login-glitch-shift 5s steps(10, end) infinite;
+}
+
+.login-glitch-2 {
+ mix-blend-mode: screen;
+ filter: hue-rotate(-30deg) saturate(1.45);
+ animation: login-glitch-shift-2 5s steps(9, end) infinite;
+}
+
+.login-glitch-scan {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 6;
+ background:
+ linear-gradient(180deg, rgba(255,255,255,0.16), rgba(0,0,0,0) 60%),
+ repeating-linear-gradient(180deg, rgba(255,255,255,0.05) 0 2px, rgba(0,0,0,0) 2px 4px),
+ radial-gradient(ellipse at center, rgba(0,0,0,0) 40%, rgba(0,0,0,0.35) 100%);
+ opacity: 0;
+ animation: login-glitch-scan 5s ease-out infinite;
+}
+
+@keyframes login-glitch-shift {
+ 0%, 82% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
+ 82.1% { opacity: 0.22; }
+ 84% { transform: translate(6px,-2px); clip-path: inset(8% 0 70% 0); }
+ 86% { transform: translate(-5px,2px); clip-path: inset(42% 0 40% 0); }
+ 88% { transform: translate(3px,0); clip-path: inset(68% 0 10% 0); }
+ 91% { transform: translate(-4px,3px); clip-path: inset(18% 0 60% 0); }
+ 93% { transform: translate(5px,-3px); clip-path: inset(55% 0 20% 0); }
+ 95% { transform: translate(-3px,1px); clip-path: inset(10% 0 80% 0); }
+ 100% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
+}
+
+@keyframes login-glitch-shift-2 {
+ 0%, 82% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
+ 82.1% { opacity: 0.18; }
+ 84% { transform: translate(-6px,2px); clip-path: inset(12% 0 65% 0); }
+ 86% { transform: translate(5px,-1px) skewX(0.6deg); clip-path: inset(36% 0 42% 0); }
+ 89% { transform: translate(-3px,2px); clip-path: inset(72% 0 8% 0); }
+ 92% { transform: translate(4px,-3px); clip-path: inset(22% 0 58% 0); }
+ 95% { transform: translate(-4px,1px); clip-path: inset(50% 0 26% 0); }
+ 100% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
+}
+
+@keyframes login-glitch-scan {
+ 0%, 82% { opacity: 0; transform: translateY(-20%); }
+ 84% { opacity: 0.4; }
+ 90% { opacity: 0.28; }
+ 100% { opacity: 0; transform: translateY(115%); }
+}
+
/* Glitch overlay layer */
.bg-glitch-layer {
position: absolute;
diff --git a/scripts/setup-kiosk.sh b/scripts/setup-kiosk.sh
new file mode 100644
index 00000000..a03cc1a2
--- /dev/null
+++ b/scripts/setup-kiosk.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+#
+# Setup Archipelago kiosk mode on the server
+# Runs Chromium in kiosk mode so keyboard/touchpad control the web UI
+# Only starts when logging in at the physical console (tty1)
+#
+# Run on server: sudo ./setup-kiosk.sh
+#
+
+set -e
+
+KIOSK_USER="${1:-archipelago}"
+ARCHIPELAGO_URL="${ARCHIPELAGO_URL:-http://localhost}"
+
+echo "Setting up kiosk for user: $KIOSK_USER"
+echo "URL: $ARCHIPELAGO_URL"
+echo ""
+
+# Create .xinitrc for kiosk
+HOMEDIR=$(getent passwd "$KIOSK_USER" | cut -d: -f6)
+XINITRC="$HOMEDIR/.xinitrc"
+
+cat > "$XINITRC" << 'XINITRC_EOF'
+#!/bin/bash
+# Archipelago kiosk - Chromium fullscreen
+exec chromium --kiosk \
+ --app=http://localhost \
+ --noerrdialogs \
+ --disable-infobars \
+ --disable-translate \
+ --no-first-run \
+ --check-for-update-interval=31536000 \
+ --disable-features=TranslateUI \
+ --disable-session-crashed-bubble
+XINITRC_EOF
+
+# Replace localhost with actual URL if different
+if [ "$ARCHIPELAGO_URL" != "http://localhost" ]; then
+ sed -i "s|http://localhost|$ARCHIPELAGO_URL|g" "$XINITRC"
+fi
+
+chown "$KIOSK_USER:$KIOSK_USER" "$XINITRC"
+chmod +x "$XINITRC"
+
+# Add startx to .bash_profile only when on console (tty1)
+BASHPROFILE="$HOMEDIR/.bash_profile"
+if [ ! -f "$BASHPROFILE" ]; then
+ touch "$BASHPROFILE"
+ chown "$KIOSK_USER:$KIOSK_USER" "$BASHPROFILE"
+fi
+
+# Remove any existing kiosk block
+if grep -q "ARCHIPELAGO_KIOSK" "$BASHPROFILE" 2>/dev/null; then
+ sed -i '/# ARCHIPELAGO_KIOSK/,/^# END ARCHIPELAGO_KIOSK/d' "$BASHPROFILE"
+fi
+
+# Add kiosk startup (only runs on physical console tty1)
+cat >> "$BASHPROFILE" << 'BASHPROFILE_EOF'
+
+# ARCHIPELAGO_KIOSK - Start X/kiosk when logging in at physical console
+if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
+ exec startx
+fi
+# END ARCHIPELAGO_KIOSK
+BASHPROFILE_EOF
+
+chown "$KIOSK_USER:$KIOSK_USER" "$BASHPROFILE"
+
+echo "✅ Kiosk installed!"
+echo ""
+echo " When you log in at the physical console (monitor + keyboard):"
+echo " - X will start automatically"
+echo " - Chromium will open in kiosk mode"
+echo " - Your keyboard/touchpad will control the Archipelago UI"
+echo ""
+echo " To use: Connect a display, plug in keyboard, reboot (or log in at tty1)"
+echo ""