Plugin framework maturation, reports overhaul, theming, and USB frontend repair

Framework:
- Per-plugin Alembic migration chains (ADR-008): every bundled plugin
  carries its own chain with a stamp-only anchor at the ownership cutover;
  new plugin schema lands in plugins/<name>/migrations/, never the core
  chain. Deploys add flask plugin upgrade-all. Fixed a latent bug in the
  shared alembic template (engine URL resolution) and taught the metadata
  filter to include FK-referenced core tables.
- Frontend plugin route gating (ADR-009): plugin routes carry meta.plugin;
  a disabled plugin's pages redirect to the dashboard via a cached,
  fail-open check against the new public GET /api/plugins/enabled.
- get_reports() plugin hook (contract 0.5.0 -> 0.6.0): plugins contribute
  report cards; warranty and toner cards moved off the hardcoded list.

Reports:
- Hub grouped by category with search; inline reports render at the top,
  are URL-backed (?report=id, back-button and deep links work), expose
  their server-side filter params as controls, and export CSV. Warranty
  and Toner pages gained CSV export.
- Deleted the dead legacy Warranty Status report (always-zero buckets
  from a retired column).

Theming and fonts:
- Inter (variable) bundled locally via @fontsource, replacing the Google
  Fonts Roboto import - air-gapped installs now render correctly; tables
  use tabular numerals.
- Optional brand_primary_dark_color, brand_accent_color,
  brand_sidebar_color settings applied to CSS vars at bootstrap.

USB frontend repair (views were reading a dead legacy shape):
- List/detail/form and the employee profile USB panels remapped to the
  real API shape (device_id/device_desc/checkinoutlog); employee panels
  now use /usb/checkouts endpoints; external-mode /usb/checkouts/active
  honors the badge filter; dead client methods pruned.

Also: warranties list page no longer requires login (matches app
convention); collector doc rewritten with a GE-Enforce integration guide
and paste-ready PowerShell reporter; ADR index and CHANGELOG updated.

Verified: 323 tests pass, naming/style green, frontend builds, plugin
migration dry-run green on scratch MySQL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-11 10:01:47 -04:00
parent b8c22244a1
commit 22e623c1f6
85 changed files with 3274 additions and 972 deletions

View File

@@ -1,46 +1,48 @@
/**
* Computers plugin routes
*/
export default [
{
path: 'pcs',
name: 'pcs',
component: () => import('../../views/pcs/PCsList.vue')
},
{
path: 'pcs/new',
name: 'pc-new',
component: () => import('../../views/pcs/PCForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'pcs/:id',
name: 'pc-detail',
component: () => import('../../views/pcs/PCDetail.vue')
},
{
path: 'pcs/:id/edit',
name: 'pc-edit',
component: () => import('../../views/pcs/PCForm.vue'),
meta: { requiresAuth: true }
},
// Computer-specific settings
{
path: 'settings/pctypes',
name: 'pctypes',
component: () => import('../../views/settings/PCTypesList.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
},
{
path: 'settings/operatingsystems',
name: 'operatingsystems',
component: () => import('../../views/settings/OperatingSystemsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
},
{
path: 'settings/accessprotocols',
name: 'access-protocols',
component: () => import('../../views/settings/AccessProtocolsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
}
]
/**
* Computers plugin routes
*/
export default [
{
path: 'pcs',
name: 'pcs',
component: () => import('../../views/pcs/PCsList.vue'),
meta: { plugin: 'computers' }
},
{
path: 'pcs/new',
name: 'pc-new',
component: () => import('../../views/pcs/PCForm.vue'),
meta: { requiresAuth: true, plugin: 'computers' }
},
{
path: 'pcs/:id',
name: 'pc-detail',
component: () => import('../../views/pcs/PCDetail.vue'),
meta: { plugin: 'computers' }
},
{
path: 'pcs/:id/edit',
name: 'pc-edit',
component: () => import('../../views/pcs/PCForm.vue'),
meta: { requiresAuth: true, plugin: 'computers' }
},
// Computer-specific settings
{
path: 'settings/pctypes',
name: 'pctypes',
component: () => import('../../views/settings/PCTypesList.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'computers' }
},
{
path: 'settings/operatingsystems',
name: 'operatingsystems',
component: () => import('../../views/settings/OperatingSystemsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'computers' }
},
{
path: 'settings/accessprotocols',
name: 'access-protocols',
component: () => import('../../views/settings/AccessProtocolsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'computers' }
}
]

View File

@@ -31,17 +31,20 @@ export default [
{
path: 'reports/pc-relationships',
name: 'report-pc-relationships',
component: () => import('../../views/reports/PCRelationshipsReport.vue')
component: () => import('../../views/reports/PCRelationshipsReport.vue'),
meta: { plugin: 'computers' }
},
{
path: 'reports/toner',
name: 'toner-report',
component: () => import('../../views/reports/TonerReport.vue')
component: () => import('../../views/reports/TonerReport.vue'),
meta: { plugin: 'printers' }
},
{
path: 'employees/:sso',
name: 'employee-detail',
component: () => import('../../views/employees/EmployeeDetail.vue')
component: () => import('../../views/employees/EmployeeDetail.vue'),
meta: { plugin: 'employees' }
},
// Settings
{
@@ -102,7 +105,7 @@ export default [
path: 'settings/slides',
name: 'slide-manager',
component: () => import('../../views/settings/SlideManager.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'slides' }
},
{
path: 'settings/equipmenttypes',

View File

@@ -1,27 +1,29 @@
/**
* Equipment plugin routes
*/
export default [
{
path: 'machines',
name: 'machines',
component: () => import('../../views/machines/MachinesList.vue')
},
{
path: 'machines/new',
name: 'machine-new',
component: () => import('../../views/machines/MachineForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'machines/:id',
name: 'machine-detail',
component: () => import('../../views/machines/MachineDetail.vue')
},
{
path: 'machines/:id/edit',
name: 'machine-edit',
component: () => import('../../views/machines/MachineForm.vue'),
meta: { requiresAuth: true }
}
]
/**
* Equipment plugin routes
*/
export default [
{
path: 'machines',
name: 'machines',
component: () => import('../../views/machines/MachinesList.vue'),
meta: { plugin: 'equipment' }
},
{
path: 'machines/new',
name: 'machine-new',
component: () => import('../../views/machines/MachineForm.vue'),
meta: { requiresAuth: true, plugin: 'equipment' }
},
{
path: 'machines/:id',
name: 'machine-detail',
component: () => import('../../views/machines/MachineDetail.vue'),
meta: { plugin: 'equipment' }
},
{
path: 'machines/:id/edit',
name: 'machine-edit',
component: () => import('../../views/machines/MachineForm.vue'),
meta: { requiresAuth: true, plugin: 'equipment' }
}
]

View File

@@ -1,27 +1,29 @@
/**
* Knowledge Base routes (core feature)
*/
export default [
{
path: 'knowledgebase',
name: 'knowledgebase',
component: () => import('../../views/knowledgebase/KnowledgeBaseList.vue')
},
{
path: 'knowledgebase/new',
name: 'knowledgebase-new',
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'knowledgebase/:id',
name: 'knowledgebase-detail',
component: () => import('../../views/knowledgebase/KnowledgeBaseDetail.vue')
},
{
path: 'knowledgebase/:id/edit',
name: 'knowledgebase-edit',
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'),
meta: { requiresAuth: true }
}
]
/**
* Knowledge Base plugin routes
*/
export default [
{
path: 'knowledgebase',
name: 'knowledgebase',
component: () => import('../../views/knowledgebase/KnowledgeBaseList.vue'),
meta: { plugin: 'knowledgebase' }
},
{
path: 'knowledgebase/new',
name: 'knowledgebase-new',
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'),
meta: { requiresAuth: true, plugin: 'knowledgebase' }
},
{
path: 'knowledgebase/:id',
name: 'knowledgebase-detail',
component: () => import('../../views/knowledgebase/KnowledgeBaseDetail.vue'),
meta: { plugin: 'knowledgebase' }
},
{
path: 'knowledgebase/:id/edit',
name: 'knowledgebase-edit',
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'),
meta: { requiresAuth: true, plugin: 'knowledgebase' }
}
]

View File

@@ -1,40 +1,42 @@
/**
* Network plugin routes
*/
export default [
{
path: 'network',
name: 'network',
component: () => import('../../views/network/NetworkDevicesList.vue')
},
{
path: 'network/new',
name: 'network-new',
component: () => import('../../views/network/NetworkDeviceForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'network/:id',
name: 'network-detail',
component: () => import('../../views/network/NetworkDeviceDetail.vue')
},
{
path: 'network/:id/edit',
name: 'network-edit',
component: () => import('../../views/network/NetworkDeviceForm.vue'),
meta: { requiresAuth: true }
},
// Network-specific settings
{
path: 'settings/vlans',
name: 'vlans',
component: () => import('../../views/settings/VLANsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
},
{
path: 'settings/subnets',
name: 'subnets',
component: () => import('../../views/settings/SubnetsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
}
]
/**
* Network plugin routes
*/
export default [
{
path: 'network',
name: 'network',
component: () => import('../../views/network/NetworkDevicesList.vue'),
meta: { plugin: 'network' }
},
{
path: 'network/new',
name: 'network-new',
component: () => import('../../views/network/NetworkDeviceForm.vue'),
meta: { requiresAuth: true, plugin: 'network' }
},
{
path: 'network/:id',
name: 'network-detail',
component: () => import('../../views/network/NetworkDeviceDetail.vue'),
meta: { plugin: 'network' }
},
{
path: 'network/:id/edit',
name: 'network-edit',
component: () => import('../../views/network/NetworkDeviceForm.vue'),
meta: { requiresAuth: true, plugin: 'network' }
},
// Network-specific settings
{
path: 'settings/vlans',
name: 'vlans',
component: () => import('../../views/settings/VLANsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'network' }
},
{
path: 'settings/subnets',
name: 'subnets',
component: () => import('../../views/settings/SubnetsList.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'network' }
}
]

View File

@@ -1,38 +1,41 @@
/**
* Notifications plugin routes
*/
export default [
{
path: 'notifications',
name: 'notifications',
component: () => import('../../views/notifications/NotificationsList.vue')
},
{
path: 'notifications/new',
name: 'notification-new',
component: () => import('../../views/notifications/NotificationForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'settings/notificationtypes',
name: 'notification-types',
component: () => import('../../views/notifications/NotificationTypesList.vue'),
meta: { requiresAuth: true }
},
{
path: 'notifications/:id',
name: 'notification-detail',
component: () => import('../../views/notifications/NotificationForm.vue')
},
{
path: 'notifications/:id/edit',
name: 'notification-edit',
component: () => import('../../views/notifications/NotificationForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'calendar',
name: 'calendar',
component: () => import('../../views/CalendarView.vue')
}
]
/**
* Notifications plugin routes
*/
export default [
{
path: 'notifications',
name: 'notifications',
component: () => import('../../views/notifications/NotificationsList.vue'),
meta: { plugin: 'notifications' }
},
{
path: 'notifications/new',
name: 'notification-new',
component: () => import('../../views/notifications/NotificationForm.vue'),
meta: { requiresAuth: true, plugin: 'notifications' }
},
{
path: 'settings/notificationtypes',
name: 'notification-types',
component: () => import('../../views/notifications/NotificationTypesList.vue'),
meta: { requiresAuth: true, plugin: 'notifications' }
},
{
path: 'notifications/:id',
name: 'notification-detail',
component: () => import('../../views/notifications/NotificationForm.vue'),
meta: { plugin: 'notifications' }
},
{
path: 'notifications/:id/edit',
name: 'notification-edit',
component: () => import('../../views/notifications/NotificationForm.vue'),
meta: { requiresAuth: true, plugin: 'notifications' }
},
{
path: 'calendar',
name: 'calendar',
component: () => import('../../views/CalendarView.vue'),
meta: { plugin: 'notifications' }
}
]

View File

@@ -1,40 +1,42 @@
/**
* Printers plugin routes
*/
export default [
{
path: 'printers',
name: 'printers',
component: () => import('../../views/printers/PrintersList.vue')
},
{
path: 'printers/new',
name: 'printer-new',
component: () => import('../../views/printers/PrinterForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'printers/:id',
name: 'printer-detail',
component: () => import('../../views/printers/PrinterDetail.vue')
},
{
path: 'printers/:id/edit',
name: 'printer-edit',
component: () => import('../../views/printers/PrinterForm.vue'),
meta: { requiresAuth: true }
},
// printer-specific settings
{
path: 'settings/modelsupplies',
name: 'model-supplies',
component: () => import('../../views/settings/ModelSuppliesList.vue'),
meta: { requiresAuth: true }
},
{
path: 'settings/printerdrivers',
name: 'printer-drivers',
component: () => import('../../views/settings/PrinterDriversList.vue'),
meta: { requiresAuth: true, requiresAdmin: true }
}
]
/**
* Printers plugin routes
*/
export default [
{
path: 'printers',
name: 'printers',
component: () => import('../../views/printers/PrintersList.vue'),
meta: { plugin: 'printers' }
},
{
path: 'printers/new',
name: 'printer-new',
component: () => import('../../views/printers/PrinterForm.vue'),
meta: { requiresAuth: true, plugin: 'printers' }
},
{
path: 'printers/:id',
name: 'printer-detail',
component: () => import('../../views/printers/PrinterDetail.vue'),
meta: { plugin: 'printers' }
},
{
path: 'printers/:id/edit',
name: 'printer-edit',
component: () => import('../../views/printers/PrinterForm.vue'),
meta: { requiresAuth: true, plugin: 'printers' }
},
// printer-specific settings
{
path: 'settings/modelsupplies',
name: 'model-supplies',
component: () => import('../../views/settings/ModelSuppliesList.vue'),
meta: { requiresAuth: true, plugin: 'printers' }
},
{
path: 'settings/printerdrivers',
name: 'printer-drivers',
component: () => import('../../views/settings/PrinterDriversList.vue'),
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printers' }
}
]

View File

@@ -1,27 +1,29 @@
/**
* USB plugin routes
*/
export default [
{
path: 'usb',
name: 'usb',
component: () => import('../../views/usb/USBList.vue')
},
{
path: 'usb/new',
name: 'usb-new',
component: () => import('../../views/usb/USBForm.vue'),
meta: { requiresAuth: true }
},
{
path: 'usb/:id',
name: 'usb-detail',
component: () => import('../../views/usb/USBDetail.vue')
},
{
path: 'usb/:id/edit',
name: 'usb-edit',
component: () => import('../../views/usb/USBForm.vue'),
meta: { requiresAuth: true }
}
]
/**
* USB plugin routes
*/
export default [
{
path: 'usb',
name: 'usb',
component: () => import('../../views/usb/USBList.vue'),
meta: { plugin: 'usb' }
},
{
path: 'usb/new',
name: 'usb-new',
component: () => import('../../views/usb/USBForm.vue'),
meta: { requiresAuth: true, plugin: 'usb' }
},
{
path: 'usb/:id',
name: 'usb-detail',
component: () => import('../../views/usb/USBDetail.vue'),
meta: { plugin: 'usb' }
},
{
path: 'usb/:id/edit',
name: 'usb-edit',
component: () => import('../../views/usb/USBForm.vue'),
meta: { requiresAuth: true, plugin: 'usb' }
}
]

View File

@@ -6,11 +6,12 @@ export default [
path: 'warranties',
name: 'warranties',
component: () => import('../../views/warranty/WarrantiesList.vue'),
meta: { requiresAuth: true }
meta: { plugin: 'warranty' }
},
{
path: 'reports/warranty',
name: 'warranty-report',
component: () => import('../../views/reports/WarrantyReport.vue')
component: () => import('../../views/reports/WarrantyReport.vue'),
meta: { plugin: 'warranty' }
}
]