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:
@@ -1,7 +1,8 @@
|
||||
"""Shared Alembic env.py logic for bundled plugins.
|
||||
|
||||
Each bundled plugin (computers, equipment, network, notifications, printers,
|
||||
usb) has a `migrations/env.py` that does the minimum:
|
||||
Every bundled plugin that owns tables (computers, employees, equipment,
|
||||
knowledgebase, network, notifications, printers, slides, usb, warranty) has a
|
||||
`migrations/env.py` that does the minimum:
|
||||
|
||||
import os
|
||||
os.environ['PLUGIN_NAME'] = 'computers'
|
||||
@@ -12,6 +13,13 @@ This module wires the plugin's models into a MetaData object filtered to
|
||||
only the tables that belong to that plugin, then runs Alembic in either
|
||||
offline or online mode against the Flask app's configured engine.
|
||||
|
||||
Ownership cutover (see ADR-008): the core Alembic chain created every table
|
||||
that exists through its head (`7d16_directoryemployees`), including the plugin
|
||||
tables. Each plugin's `0001` migration is therefore a stamp-only no-op that
|
||||
just records the anchor revision in `alembic_version_<plugin>`. NEW plugin
|
||||
schema changes land as `plugins/<name>/migrations/000N` from here on, never in
|
||||
the core chain.
|
||||
|
||||
Plugin tables must be importable via `plugins.<name>.models`. Plugins
|
||||
register their `__tablename__` set in PLUGIN_TABLE_OWNERS below so the
|
||||
filter is explicit (avoids depending on import-side-effect global state).
|
||||
@@ -31,12 +39,18 @@ logger = logging.getLogger('alembic.env.plugin')
|
||||
# Explicit table-ownership map. Adding tables to a plugin requires updating
|
||||
# this dict so the per-plugin migration knows which tables to include.
|
||||
PLUGIN_TABLE_OWNERS: dict[str, Iterable[str]] = {
|
||||
'computers': ('computertypes', 'computers', 'computerinstalledapps'),
|
||||
'computers': ('computertypes', 'computers', 'computerinstalledapps',
|
||||
'accessprotocols', 'computeraccess'),
|
||||
'employees': ('directoryemployees',),
|
||||
'equipment': ('equipmenttypes', 'equipment'),
|
||||
'knowledgebase': ('knowledgebase',),
|
||||
'measuringtools': ('measuringtooltypes', 'measuringtools'),
|
||||
'network': ('networkdevicetypes', 'networkdevices', 'vlans', 'subnets'),
|
||||
'notifications': ('notificationtypes', 'notifications'),
|
||||
'printers': ('printertypes', 'printers', 'modelsupplies'),
|
||||
'printers': ('printertypes', 'printers', 'modelsupplies', 'printerdrivers'),
|
||||
'slides': ('tvslides',),
|
||||
'usb': ('usbdevicetypes', 'usbdevices', 'usbcheckouts'),
|
||||
'warranty': ('warranties', 'warrantyassets'),
|
||||
}
|
||||
|
||||
|
||||
@@ -129,12 +143,12 @@ def run_migrations():
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
else:
|
||||
from sqlalchemy import engine_from_config
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
# Build the engine straight from the resolved URL. The plugin manager
|
||||
# drives this via a programmatic alembic Config (no ini file), so
|
||||
# config.get_section returns an empty dict and engine_from_config would
|
||||
# find no sqlalchemy.url. db_url is already resolved above.
|
||||
from sqlalchemy import create_engine
|
||||
connectable = create_engine(db_url, poolclass=pool.NullPool)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
|
||||
Reference in New Issue
Block a user