The equipment plugin is now the machines plugin, ending the UI-vs-code vocabulary split while the contract is pre-1.0 and nothing external depends on the old names. - plugins/equipment -> plugins/machines: manifest, class, /api/machines, machines.* permissions, registry key (with an auto-migrating load shim for existing installs). - Tables: equipment -> machines (equipmentid -> machineid) and equipmenttypes -> machinetypes, renamed in the plugin's own migration chain (machines0002rename), idempotent for both upgrading and fresh installs. - The legacy core machinetypes lookup actually types the vendor MODELS catalog, so it is renamed losslessly to modeltypes (models.modeltypeid, /api/modeltypes, Model Types settings page) rather than collapsed, freeing the machinetypes name. Core migration 7d17_machines_rename also flips data in place: assettypes row equipment -> machine, auditlog entitytype, identifier_/search_ settings keys, permission rows, and renames alembic_version_equipment. - Frontend: machinesApi/modeltypesApi, item.machine response shape, assettype value compares 'equipment' -> 'machine' (map, search, custom fields, relationships), routes machines.js with plugin gating retagged, /print/machine-badge, Machine Types (subtypes) and Model Types (catalog) settings pages, machines-by-type report id. - Docs swept; ADRs left as history per the authoring rule. Upgrade: flask db upgrade then flask plugin upgrade-all. Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models retyped, zero equipment tables remain); fresh scratch-MySQL install produces the new names; 341 tests green; naming/style green; frontend builds; live E2E on machines list/detail, PC relationships, map, reports, and both settings pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
142 lines
4.3 KiB
JavaScript
142 lines
4.3 KiB
JavaScript
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
|