2026-01-24 22:59:20 +00:00
|
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
2026-02-17 19:19:54 +00:00
|
|
|
import { nextTick } from 'vue'
|
2026-01-24 22:59:20 +00:00
|
|
|
import { useAppStore } from '../stores/app'
|
2026-03-12 00:19:30 +00:00
|
|
|
import { stopAllAudio } from '../composables/useLoginSounds'
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
history: createWebHistory(),
|
|
|
|
|
routes: [
|
|
|
|
|
{
|
|
|
|
|
path: '/',
|
|
|
|
|
component: () => import('../views/OnboardingWrapper.vue'),
|
|
|
|
|
meta: { public: true },
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
path: '',
|
2026-02-17 15:03:34 +00:00
|
|
|
component: () => import('../views/RootRedirect.vue'),
|
2026-01-24 22:59:20 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'login',
|
|
|
|
|
name: 'login',
|
|
|
|
|
component: () => import('../views/Login.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/intro',
|
|
|
|
|
name: 'onboarding-intro',
|
|
|
|
|
component: () => import('../views/OnboardingIntro.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/options',
|
|
|
|
|
name: 'onboarding-options',
|
|
|
|
|
component: () => import('../views/OnboardingOptions.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/path',
|
|
|
|
|
name: 'onboarding-path',
|
|
|
|
|
component: () => import('../views/OnboardingPath.vue'),
|
|
|
|
|
},
|
2026-03-31 01:41:24 +01:00
|
|
|
{
|
|
|
|
|
path: 'onboarding/seed',
|
|
|
|
|
name: 'onboarding-seed',
|
|
|
|
|
component: () => import('../views/OnboardingSeedGenerate.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/seed-verify',
|
|
|
|
|
name: 'onboarding-seed-verify',
|
|
|
|
|
component: () => import('../views/OnboardingSeedVerify.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/seed-restore',
|
|
|
|
|
name: 'onboarding-seed-restore',
|
|
|
|
|
component: () => import('../views/OnboardingSeedRestore.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'onboarding/did',
|
|
|
|
|
name: 'onboarding-did',
|
|
|
|
|
component: () => import('../views/OnboardingDid.vue'),
|
|
|
|
|
},
|
2026-03-09 07:43:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'onboarding/identity',
|
|
|
|
|
name: 'onboarding-identity',
|
|
|
|
|
component: () => import('../views/OnboardingIdentity.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'onboarding/backup',
|
|
|
|
|
name: 'onboarding-backup',
|
|
|
|
|
component: () => import('../views/OnboardingBackup.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/verify',
|
|
|
|
|
name: 'onboarding-verify',
|
|
|
|
|
component: () => import('../views/OnboardingVerify.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/done',
|
|
|
|
|
name: 'onboarding-done',
|
|
|
|
|
component: () => import('../views/OnboardingDone.vue'),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: '/recovery',
|
|
|
|
|
name: 'recovery',
|
|
|
|
|
component: () => import('../views/KioskRecovery.vue'),
|
|
|
|
|
meta: { public: true },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/kiosk',
|
|
|
|
|
name: 'kiosk',
|
2026-04-07 16:04:58 +01:00
|
|
|
redirect: '/',
|
2026-04-11 13:01:10 -04:00
|
|
|
beforeEnter: () => {
|
|
|
|
|
// Persist kiosk mode before redirect so App.vue can skip the remote relay
|
|
|
|
|
// (relay duplicates xdotool input on the kiosk display)
|
|
|
|
|
localStorage.setItem('kiosk', 'true')
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: '/dashboard',
|
|
|
|
|
component: () => import('../views/Dashboard.vue'),
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
path: '',
|
|
|
|
|
name: 'home',
|
|
|
|
|
component: () => import('../views/Home.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'apps',
|
|
|
|
|
name: 'apps',
|
|
|
|
|
component: () => import('../views/Apps.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'apps/:id',
|
|
|
|
|
name: 'app-details',
|
|
|
|
|
component: () => import('../views/AppDetails.vue'),
|
|
|
|
|
},
|
2026-03-09 07:43:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'apps/lnd/channels',
|
|
|
|
|
name: 'lightning-channels',
|
|
|
|
|
component: () => import('../views/apps/LightningChannels.vue'),
|
|
|
|
|
},
|
feat: add Discover page — cypherpunk app store with sovereignty messaging
- New Discover.vue with hero banner, featured sovereignty stack apps,
principle cards, manifesto footer, and full app grid
- Featured apps (Bitcoin Knots, LND, BTCPay, Vaultwarden) with
expanded privacy/sovereignty descriptions
- Discover is first tab in categories bar on App Store pages
- Smart back navigation: detail pages return to Discover when navigated from there
- Category clicks from Discover navigate to Marketplace with category pre-selected
- Cypherpunk aesthetic: terminal tags, scanline overlays, gradient accents,
animated Bitcoin orange headings
- Global CSS classes: discover-hero, discover-terminal-tag, discover-featured-card,
discover-principle-card, discover-manifesto
- Route added: /dashboard/discover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:14:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'discover',
|
|
|
|
|
name: 'discover',
|
|
|
|
|
component: () => import('../views/Discover.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'marketplace',
|
|
|
|
|
name: 'marketplace',
|
|
|
|
|
component: () => import('../views/Marketplace.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'marketplace/:id',
|
|
|
|
|
name: 'marketplace-app-detail',
|
|
|
|
|
component: () => import('../views/MarketplaceAppDetails.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'cloud',
|
|
|
|
|
name: 'cloud',
|
|
|
|
|
component: () => import('../views/Cloud.vue'),
|
|
|
|
|
},
|
2026-03-13 02:37:59 +00:00
|
|
|
{
|
2026-03-14 17:12:41 +00:00
|
|
|
path: 'cloud/peers/:peerId?',
|
2026-03-13 02:37:59 +00:00
|
|
|
name: 'peer-files',
|
|
|
|
|
component: () => import('../views/PeerFiles.vue'),
|
2026-03-14 17:12:41 +00:00
|
|
|
props: true,
|
2026-03-13 02:37:59 +00:00
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'cloud/:folderId',
|
|
|
|
|
name: 'cloud-folder',
|
|
|
|
|
component: () => import('../views/CloudFolder.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'server',
|
|
|
|
|
name: 'server',
|
|
|
|
|
component: () => import('../views/Server.vue'),
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'monitoring',
|
|
|
|
|
name: 'monitoring',
|
|
|
|
|
component: () => import('../views/Monitoring.vue'),
|
|
|
|
|
},
|
feat: add Discover page — cypherpunk app store with sovereignty messaging
- New Discover.vue with hero banner, featured sovereignty stack apps,
principle cards, manifesto footer, and full app grid
- Featured apps (Bitcoin Knots, LND, BTCPay, Vaultwarden) with
expanded privacy/sovereignty descriptions
- Discover is first tab in categories bar on App Store pages
- Smart back navigation: detail pages return to Discover when navigated from there
- Category clicks from Discover navigate to Marketplace with category pre-selected
- Cypherpunk aesthetic: terminal tags, scanline overlays, gradient accents,
animated Bitcoin orange headings
- Global CSS classes: discover-hero, discover-terminal-tag, discover-featured-card,
discover-principle-card, discover-manifesto
- Route added: /dashboard/discover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:14:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'fleet',
|
|
|
|
|
name: 'fleet',
|
|
|
|
|
component: () => import('../views/Fleet.vue'),
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'server/federation',
|
|
|
|
|
name: 'federation',
|
|
|
|
|
component: () => import('../views/Federation.vue'),
|
|
|
|
|
},
|
2026-03-17 00:03:08 +00:00
|
|
|
{
|
|
|
|
|
path: 'mesh',
|
|
|
|
|
name: 'mesh',
|
|
|
|
|
component: () => import('../views/Mesh.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'web5',
|
|
|
|
|
name: 'web5',
|
2026-03-21 02:43:28 +00:00
|
|
|
component: () => import('../views/web5/Web5.vue'),
|
2026-01-24 22:59:20 +00:00
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'web5/credentials',
|
|
|
|
|
name: 'credentials',
|
|
|
|
|
component: () => import('../views/Credentials.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'settings',
|
|
|
|
|
name: 'settings',
|
|
|
|
|
component: () => import('../views/Settings.vue'),
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'settings/update',
|
|
|
|
|
name: 'system-update',
|
|
|
|
|
component: () => import('../views/SystemUpdate.vue'),
|
|
|
|
|
},
|
release(v1.7.29-alpha): VPS as default app registry + settings UI
- New Settings → App registries page (/dashboard/settings/registries)
that mirrors the update-mirrors experience: list of configured
registries, test reachability, set primary, add/remove. New
registry.set-primary RPC; existing registry.{list,add,remove,test}
reused.
- Default RegistryConfig flipped: VPS (23.182.128.160:3000/lfg2025) is
now Server 1 (primary), tx1138 is Server 2 (fallback).
- Install pipeline now rewrites the first pull to the primary registry
URL before attempting it. Before this, installs always hit whichever
registry the image was hardcoded to, so changing the primary didn't
actually affect where images came from. On failure, the existing
fallback walk skips the primary (already tried) and walks the rest.
- App catalog proxy UPSTREAMS order flipped so the catalog follows the
same VPS-first rule.
- Reboot overlay: animated "a" logo now sits in the center of the ring
(matches the screensaver composition). Extracted the logo-wrapper
pattern inline.
7/7 registry tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:54:07 -04:00
|
|
|
{
|
|
|
|
|
path: 'settings/registries',
|
|
|
|
|
name: 'app-registries',
|
|
|
|
|
component: () => import('../views/AppRegistries.vue'),
|
|
|
|
|
},
|
2026-03-04 07:09:31 +00:00
|
|
|
{
|
|
|
|
|
path: 'goals/:goalId',
|
|
|
|
|
name: 'goal-detail',
|
|
|
|
|
component: () => import('../views/GoalDetail.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'chat',
|
|
|
|
|
name: 'chat',
|
|
|
|
|
component: () => import('../views/Chat.vue'),
|
|
|
|
|
},
|
2026-03-15 00:40:55 +00:00
|
|
|
{
|
|
|
|
|
path: 'app-session/:appId',
|
|
|
|
|
name: 'app-session',
|
|
|
|
|
component: () => import('../views/AppSession.vue'),
|
|
|
|
|
},
|
2026-02-14 16:44:20 +00:00
|
|
|
// Containers removed: My Apps serves the same purpose. Redirect old links.
|
|
|
|
|
{
|
|
|
|
|
path: 'containers',
|
|
|
|
|
redirect: () => ({ path: '/dashboard/apps' }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'containers/:id',
|
|
|
|
|
redirect: (to) => ({ path: `/dashboard/apps/${to.params.id}` }),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
],
|
|
|
|
|
},
|
2026-03-15 04:50:24 +00:00
|
|
|
{
|
|
|
|
|
path: '/:pathMatch(.*)*',
|
|
|
|
|
name: 'not-found',
|
|
|
|
|
component: () => import('../views/NotFound.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-02 08:34:13 +00:00
|
|
|
// Session check with timeout - avoids endless spinner on mobile/slow networks
|
|
|
|
|
const SESSION_CHECK_TIMEOUT_MS = 8000
|
|
|
|
|
|
|
|
|
|
async function checkSessionWithTimeout(store: ReturnType<typeof useAppStore>): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
return await Promise.race([
|
|
|
|
|
store.checkSession(),
|
|
|
|
|
new Promise<boolean>((resolve) =>
|
|
|
|
|
setTimeout(() => resolve(false), SESSION_CHECK_TIMEOUT_MS)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
/**
|
|
|
|
|
* Navigation Guard
|
|
|
|
|
* Handles authentication and onboarding flow routing
|
|
|
|
|
*/
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
export function isLocalRedirect(path: unknown): path is string {
|
2026-03-18 00:55:00 +00:00
|
|
|
if (typeof path !== 'string') return false
|
|
|
|
|
try {
|
|
|
|
|
if (path.startsWith('//') || path.includes('://')) return false
|
|
|
|
|
const url = new URL(path, window.location.origin)
|
|
|
|
|
return url.origin === window.location.origin
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
router.beforeEach(async (to, _from, next) => {
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
const isPublic = to.meta.public
|
|
|
|
|
|
|
|
|
|
// Allow all public routes (login, onboarding) without auth check
|
|
|
|
|
if (isPublic) {
|
2026-03-02 08:34:13 +00:00
|
|
|
// If authenticated and visiting /login: show login immediately, validate in background.
|
|
|
|
|
// This prevents endless spinner on mobile when checkSession hangs (slow/unreachable network).
|
2026-01-24 22:59:20 +00:00
|
|
|
if (to.path === '/login' && store.isAuthenticated) {
|
2026-03-16 12:58:35 +00:00
|
|
|
// Redirect back to intended page (from ?redirect= query) or default to home
|
2026-03-18 00:55:00 +00:00
|
|
|
const rawRedirect = to.query.redirect
|
|
|
|
|
const redirectTo = isLocalRedirect(rawRedirect) ? rawRedirect : '/dashboard'
|
2026-03-01 17:53:18 +00:00
|
|
|
if (store.needsSessionValidation()) {
|
|
|
|
|
next()
|
2026-03-02 08:34:13 +00:00
|
|
|
checkSessionWithTimeout(store).then((valid) => {
|
|
|
|
|
if (valid) {
|
2026-03-16 12:58:35 +00:00
|
|
|
router.replace(redirectTo).catch(() => {})
|
2026-03-02 08:34:13 +00:00
|
|
|
}
|
|
|
|
|
})
|
2026-03-01 17:53:18 +00:00
|
|
|
return
|
|
|
|
|
}
|
2026-03-16 12:58:35 +00:00
|
|
|
next(redirectTo)
|
2026-01-24 22:59:20 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
next()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 07:22:20 +00:00
|
|
|
// Protected routes: validate session if stale auth from localStorage
|
2026-03-01 17:53:18 +00:00
|
|
|
if (store.needsSessionValidation()) {
|
2026-03-05 07:22:20 +00:00
|
|
|
// localStorage says we're authed — proceed immediately, revalidate in background.
|
|
|
|
|
// No timeout wrapper here: a slow server shouldn't bounce the user to login.
|
2026-03-01 17:53:18 +00:00
|
|
|
next()
|
2026-03-05 07:22:20 +00:00
|
|
|
store.checkSession().then((valid) => {
|
|
|
|
|
if (!valid) {
|
2026-03-16 12:58:35 +00:00
|
|
|
router.replace({ path: '/login', query: { redirect: to.fullPath } }).catch(() => {})
|
2026-03-05 07:22:20 +00:00
|
|
|
}
|
|
|
|
|
})
|
2026-03-01 17:53:18 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 08:34:13 +00:00
|
|
|
// Not authenticated at all (with timeout to avoid endless spinner on mobile)
|
2026-01-24 22:59:20 +00:00
|
|
|
if (!store.isAuthenticated) {
|
2026-03-02 08:34:13 +00:00
|
|
|
const hasSession = await checkSessionWithTimeout(store)
|
2026-01-24 22:59:20 +00:00
|
|
|
if (hasSession) {
|
|
|
|
|
next()
|
|
|
|
|
return
|
|
|
|
|
}
|
release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- auth.rs now infers onboarding-complete from setup_complete + password_hash so
nodes stop bouncing users through the intro wizard after browser clear / update
/ reboot; the flag self-heals to disk on next check
- frontend: "backend uncertain" no longer defaults to /onboarding/intro —
useOnboarding returns null + callers poll / retry instead of flashing the wizard
- login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by
isFirstInstallPhase(); typing sounds unaffected
- removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog,
frontend config, Rust AppMetadata + install dispatch + install_penpot_stack;
docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted;
15 image versions deleted from tx1138, .168, gitea-local registries (.160
Gitea was 502 at release time — follow-up)
- AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target
falls back to demo/aiui/ when the AIUI sibling checkout is missing
- prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the
two copies can no longer drift (was the source of the "apps still visible"
bug — public/ had stale data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:02:24 -04:00
|
|
|
// Check if this is a fresh install that needs onboarding.
|
|
|
|
|
// Prefer checkOnboardingStatus() (tri-state) so we can distinguish
|
|
|
|
|
// "confirmed fresh install" from "backend unreachable". On the
|
|
|
|
|
// latter, send the user to RootRedirect (/) rather than the intro
|
|
|
|
|
// wizard — RootRedirect polls the backend and will route to
|
|
|
|
|
// /login once it answers, instead of forcing a re-onboarding.
|
2026-03-28 02:51:02 +00:00
|
|
|
try {
|
release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- auth.rs now infers onboarding-complete from setup_complete + password_hash so
nodes stop bouncing users through the intro wizard after browser clear / update
/ reboot; the flag self-heals to disk on next check
- frontend: "backend uncertain" no longer defaults to /onboarding/intro —
useOnboarding returns null + callers poll / retry instead of flashing the wizard
- login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by
isFirstInstallPhase(); typing sounds unaffected
- removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog,
frontend config, Rust AppMetadata + install dispatch + install_penpot_stack;
docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted;
15 image versions deleted from tx1138, .168, gitea-local registries (.160
Gitea was 502 at release time — follow-up)
- AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target
falls back to demo/aiui/ when the AIUI sibling checkout is missing
- prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the
two copies can no longer drift (was the source of the "apps still visible"
bug — public/ had stale data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:02:24 -04:00
|
|
|
const { checkOnboardingStatus, getSavedOnboardingStep } = await import('@/composables/useOnboarding')
|
|
|
|
|
const setupDone = await checkOnboardingStatus()
|
|
|
|
|
if (setupDone === false) {
|
2026-04-02 18:20:52 +01:00
|
|
|
const step = getSavedOnboardingStep()
|
|
|
|
|
next(`/onboarding/${step}`)
|
2026-03-28 02:51:02 +00:00
|
|
|
return
|
|
|
|
|
}
|
release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- auth.rs now infers onboarding-complete from setup_complete + password_hash so
nodes stop bouncing users through the intro wizard after browser clear / update
/ reboot; the flag self-heals to disk on next check
- frontend: "backend uncertain" no longer defaults to /onboarding/intro —
useOnboarding returns null + callers poll / retry instead of flashing the wizard
- login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by
isFirstInstallPhase(); typing sounds unaffected
- removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog,
frontend config, Rust AppMetadata + install dispatch + install_penpot_stack;
docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted;
15 image versions deleted from tx1138, .168, gitea-local registries (.160
Gitea was 502 at release time — follow-up)
- AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target
falls back to demo/aiui/ when the AIUI sibling checkout is missing
- prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the
two copies can no longer drift (was the source of the "apps still visible"
bug — public/ had stale data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:02:24 -04:00
|
|
|
if (setupDone === null) {
|
|
|
|
|
// Backend unreachable after retries — bounce through RootRedirect
|
|
|
|
|
// so it can keep polling and land the user on /login once the
|
|
|
|
|
// backend answers, instead of flashing the onboarding wizard.
|
|
|
|
|
const cached = localStorage.getItem('neode_onboarding_complete') === '1'
|
|
|
|
|
if (!cached) {
|
|
|
|
|
next('/')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// Cached as onboarded — continue to the /login path below.
|
|
|
|
|
}
|
2026-03-28 02:51:02 +00:00
|
|
|
} catch {
|
release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- auth.rs now infers onboarding-complete from setup_complete + password_hash so
nodes stop bouncing users through the intro wizard after browser clear / update
/ reboot; the flag self-heals to disk on next check
- frontend: "backend uncertain" no longer defaults to /onboarding/intro —
useOnboarding returns null + callers poll / retry instead of flashing the wizard
- login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by
isFirstInstallPhase(); typing sounds unaffected
- removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog,
frontend config, Rust AppMetadata + install dispatch + install_penpot_stack;
docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted;
15 image versions deleted from tx1138, .168, gitea-local registries (.160
Gitea was 502 at release time — follow-up)
- AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target
falls back to demo/aiui/ when the AIUI sibling checkout is missing
- prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the
two copies can no longer drift (was the source of the "apps still visible"
bug — public/ had stale data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:02:24 -04:00
|
|
|
// Unexpected error — do NOT default to onboarding. Hand off to
|
|
|
|
|
// RootRedirect which has retry + polling + boot-screen handling.
|
|
|
|
|
next('/')
|
2026-03-28 02:51:02 +00:00
|
|
|
return
|
|
|
|
|
}
|
2026-03-16 12:58:35 +00:00
|
|
|
next({ path: '/login', query: { redirect: to.fullPath } })
|
2026-01-24 22:59:20 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
// Validated and authenticated - ensure WebSocket is connected
|
2026-02-01 13:24:03 +00:00
|
|
|
if (!store.isConnected && !store.isReconnecting) {
|
|
|
|
|
store.connectWebSocket().catch((err) => {
|
2026-03-12 00:19:30 +00:00
|
|
|
if (import.meta.env.DEV) console.warn('[Router] WebSocket connection failed:', err)
|
2026-02-01 13:24:03 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
next()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-02 18:20:52 +01:00
|
|
|
// Persist onboarding step so page refresh resumes where user left off
|
|
|
|
|
router.afterEach((to) => {
|
|
|
|
|
const match = to.path.match(/^\/onboarding\/(.+)/)
|
|
|
|
|
if (match && match[1] !== 'intro') {
|
|
|
|
|
localStorage.setItem('neode_onboarding_step', match[1]!)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
// Stop all login/splash audio when entering the dashboard
|
|
|
|
|
router.afterEach((to, from) => {
|
|
|
|
|
if (to.path.startsWith('/dashboard') && !from.path.startsWith('/dashboard')) {
|
|
|
|
|
stopAllAudio()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-17 19:19:54 +00:00
|
|
|
// Focus Home nav item for gamepad when landing on dashboard home (e.g. after login)
|
|
|
|
|
router.afterEach((to) => {
|
|
|
|
|
if (to.path === '/dashboard' || to.path === '/dashboard/') {
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const homeLink = document.querySelector<HTMLAnchorElement>(
|
|
|
|
|
'[data-controller-zone="sidebar"] a[href="/dashboard"], [data-controller-zone="sidebar"] a[href="/dashboard/"]'
|
|
|
|
|
)
|
|
|
|
|
if (homeLink) homeLink.focus()
|
|
|
|
|
}, 150)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
export default router
|
|
|
|
|
|