Three defects, all found on printedparts_label_prefix, all one root cause: nothing in the framework knew that setting existed. The parts kiosk runs logged out. An unauthenticated read of a setting is limited to an allowlist, the key was not on it, so the kiosk got a 404 and fell back to no prefix. An admin previewing the same page while logged in saw the prefix, which is why it looked like it worked. The same setting also looked like it would not save. The row did not exist on a site that installed the plugin before the setting was added, so the first save created it - under the placeholder category the settings API uses for keys it does not recognise, where the plugin's settings page, which lists by category, could no longer see it. The value was in the database the whole time. And the row was missing in the first place because seeding ran from on_install / on_enable, which fire only on a state transition. Neither runs again on an upgrade, so a setting added in a later plugin version never reached a site that installed an earlier one. The comment claiming enable ran every upgrade cycle was simply wrong. A plugin now declares the settings it owns in get_settings_defaults(): key, default, type, category, description, and whether a logged-out page may read it. The framework seeds declared keys at install, at enable, and on every flask plugin upgrade-all; files a first-time write under the declared category; re-homes any row left in the placeholder category, value untouched; and answers an anonymous read for keys marked public. Core carries no list of any plugin's keys. Contract 0.16.0 (additive optional hook). printedparts and printers move to the hook and floor their core_version at 0.16.0. The dev database had two rows in the misfiled state (printedparts_alert_email, employee_db_host); the first repairs itself on the next upgrade pass.
9.0 KiB
9.0 KiB
ShopDB Flask Project
Modern rewrite of the classic-ASP shopdb. Built as a framework so sister GE Aerospace sites can adopt it. Plugin system is the product.
Database
- Active database:
shopdb_flask(MySQL, asset-based schema) - Legacy database:
shopdb(Classic ASP schema, used only for one-time data import viascripts/import_from_mysql.py) - Connection:
.envfile. See.env.example.
Architecture decisions live in docs/adr/. Read those before making schema or contract changes.
- ADR-001: Asset model is the platform contract (Machine retires) - ACCEPTED
- ADR-002: Plugin contract versioning (semver) - ACCEPTED
- ADR-003: Plugin distribution model (in-tree bundled + filesystem-based external) - ACCEPTED
- ADR-004: Deployment topology (per-site instances, not multi-tenant) - ACCEPTED
- ADR-005: Equipment vs measuringtools plugin scope - ACCEPTED
- ADR-006: Plugin collector contract pattern - ACCEPTED
- ADR-007: Product versioning and releases - ACCEPTED
- ADR-008: Plugin migration ownership (per-plugin chains) - ACCEPTED
- ADR-009: Frontend plugin route gating - ACCEPTED
- ADR-010: Frontend plugin hook contract - ACCEPTED
- ADR-011: Machines rename + modeltypes retyping - ACCEPTED
- ADR-012: GE-Enforce manifest ownership in shopdb - ACCEPTED
- ADR-013: Plugin catalog, curated shelf, and lean per-site builds - PROPOSED
- ADR-014: Schema-lean per-site builds (retire cross-plugin FKs, prune not-installed plugin tables) - ACCEPTED
Coding convention
CONTRIBUTING.md defines naming rules (DB tables, columns, Python, JS, Vue, API). Pre-commit hook at scripts/check-naming-and-style.sh enforces them. Read CONTRIBUTING.md before naming any new identifier.
Current state (as of 2026-07-13)
Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) largely complete; the last big milestone is the legacy-data import + a production pilot.
Phases done
- Phase 0: 6 ADRs accepted, naming convention v1, pre-commit style hook
- Phase 1: 8 smoke tests + 7 production-config tests, Flask-SQLAlchemy 3 fixtures, uv lockfile, hardened ProductionConfig
- Phase 2: contract surface defined (
__contract_version__,BasePluginhooks,docs/PLUGIN-HOOKS.md), 51 contract tests - Phase 3: manifest-first loader, fail-loud/isolate policy, contract-version range checking, auto-register core blueprints,
shopdb.apinamespace,BasePlugin.get_setting/set_settinghelpers - Phase 4:
flask plugin new <name>CLI, scaffold templates, 14 canary tests,docs/PLUGIN-QUICKSTART.md - Phase 5: ADRs moved to
docs/adr/, Alembic baseline migration, per-site deploy artifacts (Dockerfile,docker-compose.yml,docs/DEPLOY.md)
Active state
- 1159 tests, naming/style check green, Gitea Actions CI (backend + naming + frontend build + a lean-build job + a migrations-mysql job that runs the real fresh upgrade on utf8mb4 MySQL 8)
- GE-Enforce HTTPS cutover: the displays/kiosks cohort now fetches manifest + inline payloads entirely over HTTPS (share-less); the
gea-shopfloor-displayscope is authored in code (plugins/geenforce/seed_display_scope.py) and published viaseed_display_scope(publish=True). Other fleet PC types still enforce from the SMB share and only report. Seedocs/geenforce-api-cutover.md. __contract_version__at 0.16.0 (0.12.0 mailer, 0.13.0 User/Role, 0.14.0 send_webhook, 0.15.0 authorized_service_token, 0.16.0 get_settings_defaults) (product__version__0.7.0, tags v0.5.0/v0.6.0/v0.7.0 - distinct series, ADR-007)- 13 bundled plugins all satisfy contract: computers, employees, geenforce, knowledgebase, machines, measuringtools, network, notifications, printedparts, printers, slides, usb, warranty
- Core Alembic chain: baseline
68b3947ae14f-> head7d26_settings_description_text(33 core migrations). Each plugin owns its own chain (ADR-008); deploy runsflask db upgradethenflask plugin upgrade-all. Reproducible + idempotent from empty (env.py relaxes session sql_mode so the chain runs on strict MySQL 8). - Lean per-site builds (ADR-013 + ADR-014):
scripts/build-site.sh(backend) +SITE_PLUGINSviascripts/stage-frontend.mjs(frontend) ship only chosen plugins;flask plugin prune-schemadrops non-installed plugins' tables at provisioning. Sidebar nav / settings / Displays all gate on staged routes. Manifest-lessplugins/<name>/frontend/dirs (e.g.applications) are core and always ship. - Windows sites install from a single air-gapped installer
.exebuilt per site from its plugin profile (deploy/windows/installer/, built bybuild-installer.shorbuild-installer.ps1). Operator docs:docs/INSTALL-WINDOWS.md+docs/OPERATE-WINDOWS.md- these are canonical for a NEW site.docs/INSTALL-WINDOWS-IIS.mdanddocs/DEPLOY-WINDOWS-IIS.mdare the MANUAL procedure, kept for hand-built servers only. The installer verifies its third-party payload againstbundle-lock.jsonand installs wheels withpip --require-hashes; every build stages a CycloneDX SBOM (sbom.cdx.json) onto the server. - Legacy import:
docs/IMPORT-API.mdis the schema-agnostic import contract;docs/IMPORT-ADOPTION.md+docs/PILOT-DEPLOY.mdcover adopting a site;scripts/site_imports/wjf/is the West Jefferson reference loader (all 16 stages, validated end-to-end including on a Windows + MySQL 8 VM). - API is migration-complete: an admin PAT + docs/IMPORT-API.md let a script import the whole legacy DB (X-Import-Mode preserves timestamps).
- Pre-1.0 framework; sister sites should pin tight
core_versionranges until contract reaches 1.0
Deferred
- Equipment data migration (one-shot script for legacy ASP shopdb -> assets). Per ADR-001, only
category='Equipment' AND machinenumber IS NOT NULLmigrates. Skillmigrating-asset-schemadocuments the pattern; the actual one-shot script lives inscripts/migration/when run. - Printers retirement: legacy
PrinterDatamodel + frontend changes. Coordinated with the equipment data migration. - (DONE 2026-07-11)
measuringtoolsplugin (ADR-005) is built and bundled; docs/PLUGIN-GUIDE.md narrates its construction as the plugin tutorial. - (DONE) Frontend plugin hook contract (ADR-010): get_settings_cards / get_asset_panels / get_map_overlays / get_asset_presentation shipped; generic renderers for panels/overlays land incrementally.
- (DONE) Per-plugin Alembic chains (ADR-008): every bundled plugin carries its own chain; no plugin uses db.create_all().
- Legacy ASP data import against the renamed schema (unblocked; run via docs/IMPORT-API.md) + a production pilot deployment.
Quick start
# Start dev environment
~/start-dev-env.sh
# Activate venv and install deps
cd /home/camp/projects/shopdb-flask
source venv/bin/activate
pip install -r requirements.txt
# Configure environment
cp .env.example .env
# Edit .env with DB credentials, JWT secrets
# Create / update database tables via the Alembic chain
flask db upgrade
# Seed RBAC, default settings, and reference data (all idempotent)
flask seed permissions
flask seed settings
flask seed reference-data
# Restart services
pm2 restart shopdb-flask-api shopdb-flask-ui
Service URLs
- Flask API: http://localhost:5001
- Flask UI: http://localhost:5173
- Legacy ASP (data source for one-time import): http://192.168.122.151:8080
Plugin structure
plugins/
<plugin_name>/
__init__.py
plugin.py # BasePlugin implementation
manifest.json # Plugin metadata (name, version, dependencies, api_prefix)
models/
__init__.py # Export all models
<model>.py # SQLAlchemy models
api/
__init__.py
routes.py # Flask Blueprint
services/ # Optional, business logic
schemas/ # Optional, marshmallow schemas
migrations/ # Optional, plugin-specific Alembic migrations
Each plugin must have:
models/__init__.pyexports all modelsplugin.pyextendsBasePluginmanifest.jsonwith metadata (single source of truth per ADR-002)- No direct imports from core code (use the contract surface defined in ADR-001)
Key files
shopdb/__init__.py- app factory, blueprint registrationshopdb/plugins/base.py- BasePlugin ABC, PluginMeta dataclassshopdb/plugins/loader.py- filesystem discovery, dependency-aware loadingshopdb/core/api/assets.py- example of optional plugin importsfrontend/src/router/index.js- frontend routingfrontend/src/components/AppSidebar.vue- navigation menudocs/adr/- architecture decision records
Migration notes
migrations/DATA_MIGRATION_GUIDE.md- one-time import from legacy ASP shopdbmigrations/MIGRATE_USB_DEVICES_FROM_EQUIPMENT.md- USB device migration from equipment tablemigrations/FIX_LOCATIONONLY_EQUIPMENT_TYPES.md- LocationOnly equipment type fixmigrations/PRODUCTION_MIGRATION_GUIDE.md- production import methodsmigrations/rename_underscore_columns.sql- one-time rename of snake_case columns to lowercase concatenated (per CONTRIBUTING.md)migrations/versions/- the core Alembic chain (baseline68b3947ae14f-> head7d26_settings_description_text). Runflask db upgradeto apply.