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
|
|
|
|
|
}
|
2026-03-28 02:51:02 +00:00
|
|
|
// Check if this is a fresh install that needs onboarding
|
|
|
|
|
try {
|
2026-04-02 18:20:52 +01:00
|
|
|
const { isOnboardingComplete, getSavedOnboardingStep } = await import('@/composables/useOnboarding')
|
2026-03-28 02:51:02 +00:00
|
|
|
const setupDone = await isOnboardingComplete()
|
|
|
|
|
if (!setupDone) {
|
2026-04-02 18:20:52 +01:00
|
|
|
const step = getSavedOnboardingStep()
|
|
|
|
|
next(`/onboarding/${step}`)
|
2026-03-28 02:51:02 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// If we can't check, assume fresh install and show onboarding
|
|
|
|
|
next('/onboarding/intro')
|
|
|
|
|
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
|
|
|
|
|
|