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

@@ -65,6 +65,35 @@ export async function getBrandPrimaryColor() {
return getSetting('brand_primary_color', '')
}
// Primary hover/active color (--primary-dark). Empty = derive from primary.
export async function getBrandPrimaryDarkColor() {
return getSetting('brand_primary_dark_color', '')
}
// Accent color (--secondary). Empty = built-in palette from style.css.
export async function getBrandAccentColor() {
return getSetting('brand_accent_color', '')
}
// Sidebar background color (--sidebar-bg). Empty = built-in palette.
export async function getBrandSidebarColor() {
return getSetting('brand_sidebar_color', '')
}
// Darken a #rrggbb hex color by the given fraction (0.15 = 15% darker).
// Returns null for anything that is not a 6-digit hex so callers can skip it.
function darkenHexColor(hex, fraction) {
const match = /^#([0-9a-fA-F]{6})$/.exec(hex)
if (!match) return null
const num = parseInt(match[1], 16)
const scale = 1 - fraction
const red = Math.round(((num >> 16) & 0xff) * scale)
const green = Math.round(((num >> 8) & 0xff) * scale)
const blue = Math.round((num & 0xff) * scale)
const toHex = (channel) => channel.toString(16).padStart(2, '0')
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`
}
// ServiceNow ticket-link config. Returns the enabled flag, incident/change URL
// templates, and the global-search URL template (all with a {ticket}
// placeholder). Defaults mirror the previously-hardcoded GE ServiceNow URLs.
@@ -104,8 +133,25 @@ export async function applyBranding() {
}
link.href = favicon
}
const rootStyle = document.documentElement.style
const primaryColor = await getBrandPrimaryColor()
if (primaryColor) {
document.documentElement.style.setProperty('--primary', primaryColor)
rootStyle.setProperty('--primary', primaryColor)
}
// Hover color: use the explicit value, else darken primary ~15%.
const primaryDarkColor = await getBrandPrimaryDarkColor()
if (primaryDarkColor) {
rootStyle.setProperty('--primary-dark', primaryDarkColor)
} else if (primaryColor) {
const derived = darkenHexColor(primaryColor, 0.15)
if (derived) rootStyle.setProperty('--primary-dark', derived)
}
const accentColor = await getBrandAccentColor()
if (accentColor) {
rootStyle.setProperty('--secondary', accentColor)
}
const sidebarColor = await getBrandSidebarColor()
if (sidebarColor) {
rootStyle.setProperty('--sidebar-bg', sidebarColor)
}
}