import { createRouter, createWebHistory } from 'vue-router' import { useAuthStore } from '../stores/auth' import AppLayout from '../views/AppLayout.vue' import SettingsLayout from '../views/settings/SettingsLayout.vue' import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState } from '../composables/setupState' import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins' import { useToast } from '../composables/toast' // Auto-discover all route modules from routes/ directory const routeModules = import.meta.glob('./routes/*.js', { eager: true }) const rawChildren = Object.values(routeModules).flatMap(m => m.default) // Gather the settings pages (spread across plugin route files) and nest them // under a single two-pane shell so the grouped rail stays put while the right // pane swaps. Slides is a sidebar page that happens to live at /settings/slides; // keep it full-width (not inside the settings rail). const SETTINGS_STANDALONE = new Set(['settings/slides']) const settingsChildren = [] const otherChildren = [] for (const route of rawChildren) { const path = route.path if (path === 'settings') { // Old index becomes the shell's default child (keeps name 'settings'). settingsChildren.unshift({ ...route, path: '' }) } else if (typeof path === 'string' && path.startsWith('settings/') && !SETTINGS_STANDALONE.has(path)) { settingsChildren.push({ ...route, path: path.replace(/^settings\//, '') }) } else { otherChildren.push(route) } } const appChildren = [ ...otherChildren, { path: 'settings', component: SettingsLayout, meta: { requiresAuth: true, requiresAdmin: true }, children: settingsChildren, }, ] const routes = [ { path: '/login', name: 'login', component: () => import('../views/Login.vue'), meta: { guest: true } }, // First-run setup wizard (standalone, admin-only) { path: '/setup', name: 'setup', component: () => import('../views/SetupWizard.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, // Standalone full-screen dashboards (no sidebar, no auth required) { path: '/shopfloor', name: 'shopfloor', component: () => import('../views/ShopfloorDashboard.vue') }, { path: '/tv', name: 'tv', component: () => import('../views/TVDashboard.vue'), meta: { plugin: 'slides' } }, // Print pages (standalone, no sidebar/header) { path: '/print/machine-badge/:id', name: 'print-machine-badge', component: () => import('../views/print/MachineBadge.vue') }, { path: '/print/printer-qr', name: 'print-printer-qr-batch', component: () => import('../views/print/PrinterQRBatch.vue'), meta: { plugin: 'printers' } }, { path: '/print/printer-qr/:id', name: 'print-printer-qr-single', component: () => import('../views/print/PrinterQRSingle.vue'), meta: { plugin: 'printers' } }, { path: '/print/usb-labels', name: 'print-usb-labels', component: () => import('../views/print/USBLabelBatch.vue'), meta: { plugin: 'usb' } }, { path: '/', component: AppLayout, children: appChildren } ] const router = createRouter({ history: createWebHistory(), routes }) // Navigation guard router.beforeEach(async (to, from, next) => { const authStore = useAuthStore() if (to.meta.requiresAuth && !authStore.isAuthenticated) { return next('/login') } if (to.meta.requiresAdmin && !authStore.isAdmin) { return next('/') } if (to.meta.guest && authStore.isAuthenticated) { return next('/') } // Plugin gating: a disabled backend plugin's frontend routes are dead ends. // The enabled list is fetched once and cached; fail-open on error so a blip // cannot brick navigation. Works unauthenticated (endpoint is jwt-optional). if (to.meta.plugin) { await loadEnabledPlugins() if (!isPluginEnabled(to.meta.plugin)) { useToast().info(`The ${to.meta.plugin} feature is not enabled.`) return next('/') } } // First-run: steer a fresh admin into the setup wizard (once, until finished). if (authStore.isAuthenticated && authStore.isAdmin && to.path !== '/setup') { if (!isSetupLoaded()) { await refreshSetupState() } if (!setupComplete.value && !setupSkipped.value) { return next('/setup') } } next() }) export default router