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>
Make the app distributable to other GE Aerospace sites (one self-hosted
instance per site, ADR-004). GE values remain the shipped defaults; every
site-specific behavior is now a Setting an admin can change in the UI.
Settings-driven site config:
- Branding: site/QR/badge logos, favicon, primary color (upload endpoints
mirror the map-blueprint pattern; new Settings > Branding section).
- ServiceNow: search/incident/change URL templates ({ticket}), ticket
prefixes, enable toggle. Defaults point at the current
geaerospaceqa.service-now.com global search. Disabled = plain-text tickets.
- Employee-id regex (employeeid_pattern), printer hostname template,
QR label targets (qr_target_printer / qr_target_usb, blank = asset page,
else URL template with placeholders), usb_label_style (barcode|qr).
- West Jefferson floor-plan PNGs removed from the tree; generic placeholder
ships as the map default and sites upload their own blueprint.
Security closeout:
- dashboarddefaults writes now require admin.
- Collector: generic error messages (no str(exc) leak); API key accepted
via X-API-Key header only (BREAKING: querystring api_key removed).
- IP-based login rate limiting (AUTH_RATELIMIT_* knobs) atop account lockout.
- Setting.set() creation race fixed (IntegrityError retry).
Release engineering and docs:
- __version__ 0.5.0 (distinct from __contract_version__, ADR-007),
CHANGELOG.md, Gitea Actions CI config, frontend version aligned.
- One wizard-first install story across README/DEPLOY; new CONFIG.md,
UPGRADE.md, BACKUP-RESTORE.md; CLAUDE.md and ROADMAP de-staled.
- Dockerfile multi-stage build now bundles the frontend; compose binds
MySQL to 127.0.0.1; stale database/schema.sql and one-off SQL removed.
Debt and fixes:
- .query.get() -> db.session.get() sweep; datetime.utcnow() removed
(naive-UTC via timezone-aware now); users.py on authz decorators.
- Fixed 4 stale tests (slides feed shape, shopfloor splitperemployee,
plugin contract purity) and the USB label page field mapping (both usb
modes emit the cmmc shape: device_id/device_desc).
- Health endpoint reports the real version.
248 tests pass; naming/style check green; frontend builds; fresh-DB
flask db upgrade + seeds verified; QR targets verified by decoding
rendered codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answers the confusion of asking for a DB connection while also offering to
create the tables. Each self-host-capable plugin (employees, usb) now shows a
mode choice; the external connection fields appear only for "connect your own
database". Default is self-hosted (create tables here) - the external path is
the niche/our-site option.
- provisioning_note gains mode_setting; employee_directory_mode + usb_directory_
mode settings (both default 'selfhosted').
- Wizard renders the radio, shows the note for self-hosted and the config fields
for external, and saves the chosen mode.
Employees works fully in both modes. USB self-hosted ROUTING is still TODO - the
USB routes read the external cmmc_usb schema; wiring them to the app-owned
tables is the remaining work (tracked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both plugins provision extra tables, so they now install disabled and explain
themselves before a site opts in.
- Plugin contract gains get_provisioning_note() -> {tables, note, docs}.
Employees and USB implement it (what tables get created in shopdb, how they
are referenced, link to the schema README; USB references the captured
DLP/reminder plans).
- Manifest default_enabled=false for employees + usb; the plugins list API
returns provisioning_note + default_enabled; install now registers a plugin
disabled when default_enabled is false.
- Setup wizard Features step renders the provisioning note the moment a plugin
with one is enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Most sites have no external HR database, so add a self-hosted directory mode.
- New employee_directory_mode setting: 'external' (default; read a separate HR
DB, unchanged) or 'selfhosted' (app-owned table).
- DirectoryEmployee model + directoryemployees table (migration 7d16). to_dict
emits the same keys the external contract uses (SSO/First_Name/...), so both
modes share one response shape and the frontend is unchanged.
- Employee search / single / batch lookup branch on the mode.
- Self-hosted-only management endpoints: list, create, update, delete, and CSV
import (upsert by SSO). Guarded so they only work in self-hosted mode.
- EmployeeDirectory.vue management page (Settings > Locations & Organization):
table + search + pagination, add/edit/delete, CSV import (file or paste).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Flag the employee directory as the integration most likely to differ per
site; USB (cmmc_usb) is standardized and rarely needs adaptation.
- Add "Option B: stand up a self-hosted directory" with the canonical employees
table DDL, for sites with no HR database. In-app management (CRUD/CSV import)
noted as a possible future enhancement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every site's HR directory and USB check-in/out databases may use a different
schema, so document exactly what each plugin queries and how to adapt.
- plugins/employees/README.md: required employees table columns (SSO,
First_Name, Last_Name, Team, Role, Picture), the queries run, photo handling,
and a CREATE VIEW recipe to map a different site schema without code changes.
- plugins/usb/README.md: cmmc_usb devices / checkinoutlog / users columns,
read-write ops, the employee-directory dependency, and a view recipe.
- USB plugin gains get_config_schema() (cmmc_usb_db_host/name/user + password);
cmmc_usb_connection reads host/name/user settings-first (env fallback), the
password stays env-only - matching the employees plugin.
- Config-field help points at the READMEs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Plugin contract gains get_config_schema(); the plugins list API returns it.
Employees plugin declares its directory-DB fields (host/name/user + password).
- employee_connection reads host/name/user settings-first (env fallback); the
password stays env-only.
- Setup wizard Features step renders each enabled plugin's config: non-secret
fields save to settings; secrets are never stored - the wizard emits .env
lines to paste. Fixed the plugins-list data path (data.plugins).
- Settings PUT now upserts (creates the row on first write) so plugin-config
keys can be saved without pre-seeding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third core feature pulled into a plugin (blueprint-only, like slides). The
employee directory is a read-only lookup over a separate HR database.
- plugins/employees/: manifest (api_prefix /api/employees, no deps), api/ (moved
blueprint, contract-pure: success/error/ErrorCodes + employee_connection all
from shopdb.api), plugin.py (get_blueprint, get_models -> []).
- employee_connection STAYS core infrastructure in shopdb.api (config-driven
external DB connector, shared by search + the notifications shopfloor feed). So
no get_services needed and no contract change - the plugin owns the directory
FEATURE, core owns the shared connector.
- Fixed a latent bug in the move: error paths used ErrorCodes.DATABASE_ERROR
which does not exist -> ErrorCodes.INTERNAL_ERROR (so a directory outage now
returns a clean 500 envelope instead of an AttributeError crash).
- De-cored: deleted shopdb/core/api/employees.py, removed from
CORE_BLUEPRINT_NAMES + core/api/__init__ import/__all__. Registered in
instance/plugins.json.
Pinned first: validation (400) + graceful-degrade (500) characterization tests;
the degrade test caught the DATABASE_ERROR bug and goes green with the fix.
184 tests pass, naming green, app boots 9 bundled plugins, endpoint verified live.
Plugin extractions complete: knowledgebase, slides, employees.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>