An SVG is an XML document that may carry a script, and it is an accepted image
type because floor-plan maps and branding genuinely want vector. Loaded through
an img tag that script never runs, so the tiles and maps were never the risk.
Opening the file's own URL is - and the application image route is public, so
that URL needs no session.
Every route that serves an upload now goes through one helper that sends
Content-Security-Policy: default-src 'none'; sandbox, and nosniff. Seven routes
across core and five plugins, so a new one added later starts from the same
place rather than repeating the reasoning. Banning the format instead would
have cost the maps their only sensible one.
The app also sent no security headers at all. It now sets nosniff,
frame-ancestors self (as X-Frame-Options too, for the display bays' browsers)
and a referrer policy. Deliberately NOT a page-wide CSP: this serves an SPA with
inline styles, so a real script-src policy is a change worth making with the
frontend in front of you, and a permissive header claiming one would be worse
than having none.
Contract 0.19.0. send_upload is on the shopdb.api surface, because a plugin
serving user-supplied bytes should not have to remember these headers. The same
bump records that get_dashboard_widgets has taken data and shape rather than a
component name since the dashboard was rebuilt - that shipped without a bump,
while BasePlugin and PLUGIN-HOOKS.md both still documented the shape nothing
renders, which is how five plugins came to declare widgets pointing at
components nobody had written.
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.
Wires the ADR-010 get_asset_panels hook to a generic frontend renderer so a
plugin adds detail-page UI as JSON, no Vue. This is the Path A foundation that
lets simple plugins ship UI without a frontend build.
- components/PluginAssetPanels.vue + pluginAssetPanels.js: fetches
/api/pluginui/asset-panels for an asset, then each panel's data endpoint, and
renders by mode: list (title + status badge + meta lines via a field map),
keyvalue, table (declared or inferred columns), badge. Pure mapping logic is
in the .js module and unit tested (9 specs), same pattern as entryForm.js.
- New 'list' render mode with a declarative field map (title/badge/meta),
documented on the hook in base.py.
- Warranty migrated to it: get_asset_panels now declares a 'list' panel + map
that reproduces WarrantyPanel's output (vendor title, status badge with color
+ label map, servicelevel/ends/tag meta, manage link) with zero
warranty-specific frontend code.
- MachineDetail swapped from <WarrantyPanel> to <PluginAssetPanels> (pilot); the
hero warranty badge is unchanged. Verified end to end: the API serves the list
panel + map and the warranty rows; the page renders without error.
Rollout of the other 4 detail pages (PCDetail, PrinterDetail, NetworkDeviceDetail,
MeasuringToolDetail) and the map-overlays / asset-presentation renderers are
follow-up Phase 3 commits. 58 vitest, build clean, 1067 backend pass, naming green.
Support teams: contact management moved from a row expander to a modal
(Contacts (N) button per team); application detail Support card and the
modal show Email (mailto) and Teams chat buttons for contacts with an
SSO, derived as sso@ + a new contact_email_domain site setting
(default geaerospace.com, blank hides the buttons).
Audit log: hovering a user SSO shows the full name, resolved
best-effort from the employee directory in either mode.
Docs/hygiene from a standards review: CLAUDE.md active-state,
CONTRACT-STABILITY.md and README brought to contract 0.10.0 / 11
plugins / migration head 7d22; get_asset_panels endpoint path fixed in
the hook docstring; leftover debug console.logs removed.
781 tests pass; contacts modal, action-button hrefs, and the audit
tooltip verified live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plugins declare their own RBAC permissions instead of core accumulating
them: 36 permissions moved out of the core catalog into the 9 owning
plugins (core keeps the 19 its own blueprints enforce). The catalog is
resolved dynamically (core + enabled plugins) and feeds the roles grid,
the token scope picker and ceiling, and flask seed permissions;
installing or enabling a plugin seeds its permissions automatically. A
disabled plugin drops out of the assignable catalog while existing role
links keep working. New plugins - bundled or external - now bring their
permissions with zero core edits.
781 tests pass; live-verified with a machines.edit-scoped token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four data-only hooks on BasePlugin (get_settings_cards,
get_asset_panels, get_map_overlays, get_asset_presentation) with a
GET-only /api/pluginui consumer surface copying the dashboard-widgets
semantics. Pilots: warranty declares its asset panel; measuringtools
supplies its settings card, presentation, and calibration overlay -
the last hardcoded settings-nav entry is now hook-sourced. Generic
renderers for panels/overlays/presentation deferred per the ADR's
incremental adoption plan (documented in CONTRACT-STABILITY.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
- 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>
Architectural pass from the skill review ("plugin is the product" boundary).
Core no longer imports plugin models at module load (was a hard import-time
dependency that broke core if the computers plugin was absent/disabled):
- collector.py, applications.py, reports.py: lazy + guarded imports of the
computers plugin models. Endpoints that need install-tracking now return 503
when the plugin is absent instead of failing at import.
Search honors runtime enable/disable:
- search.py: _require_enabled(name) raises ImportError for a disabled plugin,
so each plugin-scoped block skips it (a disabled plugin's rows leave search).
- Replace hardcoded root/rootpassword employee-DB connection in _search_employees
with the shared env-backed employee_connection helper.
Plugin hooks (integrating-plugin-hooks: every hook needs a consumer):
- get_dashboard_widgets: add the consumer GET /api/dashboard/widgets (5 plugins
already implemented the hook; it had none). Skips disabled, isolates in prod.
- get_searchable_fields: REMOVED. Zero plugins implemented it and there was no
consumer; global search is a core concern over the asset model. Contract
reduction, __contract_version__ 0.3.0 -> 0.4.0.
Docs/contract: PLUGIN-HOOKS.md (widgets consumer note, searchable-fields removal,
0.4.0), PLUGIN-QUICKSTART.md, ADR-001 hook list. Tests: widgets endpoint
aggregate + disabled-skip; contract tests for the removed/added hooks.
151 tests pass, naming/style green, app boots all 6 plugins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the plugin collector contract from ADR-006 so any plugin can
accept idempotent inventory ingest, not just PCs.
- base.py: add apply_collector_payload hook (companion to get_collector_schema),
raises NotImplementedError by default for plugins that declare a schema but
do not implement the upsert.
- collector.py: generic POST /api/collector/<plugin> dispatch with per-plugin
API key (COLLECTOR_API_KEY_<PLUGINNAME> with COLLECTOR_API_KEY fallback),
schema-driven identity validation, idempotent upsert, ADR-006 response
contract (status, action, assetid, identityvalue, warnings), audit log.
JWT-protected GET /api/collector/_schemas lists registered schemas. Legacy
/pc, /apps, /heartbeat, /bulk kept for back-compat.
- computers plugin: implements get_collector_schema (identityfield hostname)
and apply_collector_payload (create-or-update Asset+Computer, serialnumber,
loggedinuser, lastboottime, primary IP communication, installed apps).
- tests: 7 collector-contract tests (auth, 404, validation, create/idempotent
update, per-plugin key precedence, JWT schema listing).
A single dynamic dispatch route is used instead of per-plugin blueprint
registration, avoiding Flask's register-blueprint-after-first-request error.
144 tests pass, naming/style check green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PluginMeta.core_version defaulted to >=1.0.0, which would reject the current
0.2.0 framework for any plugin relying on the default. Set to >=0.2.0,<1.0.0.
- Add GET /api/plugins introspection: lists loaded plugins (name, version,
core_version, api_prefix, dependencies) + the framework contract version.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hardens the plugin framework so sister-site adoption is safe.
Loader rewrite (shopdb/plugins/loader.py):
- Reads manifest.json directly. Dependency sort and version checks
no longer instantiate plugin classes (avoids __init__ side effects).
- Fail-loud policy: in dev/test (DEBUG or TESTING true), plugin
errors re-raise. In production, errors log with full context and
the plugin is excluded from registration. Framework keeps booting.
- Contract-version range check via packaging.SpecifierSet. Plugin's
manifest.core_version must include the framework's
__contract_version__ or load fails per the policy above.
- Manifest validation: required fields (name, version, description),
name matches directory, JSON parseable.
Exceptions (shopdb/exceptions.py):
- PluginNotFoundError, PluginContractError, PluginVersionError,
PluginDependencyError. Specific types replace generic Exception
swallowing.
Auto-register core blueprints (shopdb/__init__.py):
- CORE_BLUEPRINT_NAMES tuple drives registration. Adding a core
resource is one entry, not three lines (import + register call).
- Replaces 27 hand-coded register_blueprint calls.
- Asserts each blueprint is exported by shopdb.core.api at boot.
Public API namespace (shopdb/api/__init__.py):
- audit_log: thin wrapper over AuditLog.log() with stable signature.
- resolve_asset_position: implements ADR-001 position resolution
(asset > related > location). Asset.mapx/mapy and
AssetRelationship.inheritsposition columns are part of the locked
contract surface but not yet in models; helper degrades gracefully
to location-only fallback until the migration lands.
BasePlugin helpers (shopdb/plugins/base.py):
- get_setting(key, default), set_setting(key, value, ...). Settings
namespaced as plugin.<pluginname>.<key> so two plugins can use the
same key without colliding.
Manifest version compatibility (plugins/*/manifest.json):
- Bumped core_version from ">=1.0.0" to ">=0.1.0,<1.0.0" so all
bundled plugins satisfy the new range check.
Contract version bump (shopdb/__init__.py):
- 0.1.0 -> 0.2.0. Additive surface change (Setting helpers,
shopdb.api namespace) per ADR-002 minor-bump rules.
Tests (tests/test_plugin_loader.py, tests/test_api_namespace.py):
- 13 loader tests: manifest validation failures, version range
checks, plugin.py import errors, strict-vs-isolate behavior under
TESTING vs production-like config, manifest-first dependency sort.
- 8 api-namespace tests: audit_log roundtrip, resolve position
fallback chain, plugin.get_setting/set_setting roundtrip with
per-plugin namespacing.
Test count: 66 -> 87 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks the public surface plugin authors at sister sites depend on.
Contract version (shopdb/__init__.py):
- __contract_version__ = '0.1.0'. Per ADR-002, plugins declare a
compatible range in manifest.json `core_version`. Pre-1.0 signals
the contract is still settling; sister sites should pin tightly.
BasePlugin hook changes (shopdb/plugins/base.py):
- Add get_collector_schema() per ADR-006. Returns JSON Schema (with
identityfield + fields) describing the payload of an external
collector pushing to /api/collector/<pluginname>. Defaults to None
(no auto-registered endpoint).
- Remove get_event_handlers(). Event bus deferred indefinitely per
ADR-001 (no real use case yet; add via new ADR if it appears).
Hook reference (docs/PLUGIN-HOOKS.md):
- Canonical reference for the contract: required hooks (meta,
get_blueprint, get_models), optional hooks (init_app,
get_cli_commands, get_services, get_dashboard_widgets,
get_navigation_items, get_searchable_fields, get_collector_schema),
lifecycle hooks (on_install, on_uninstall, on_enable, on_disable),
helpers exposed in shopdb.api (audit_log, Setting,
resolve_asset_position).
- Versioning rules + change-classification guidance.
Compliance tests (tests/test_plugin_contract.py):
- 8 distinct contract assertions parametrized over 6 bundled plugins
(computers, equipment, network, notifications, printers, usb).
- Asserts: subclasses BasePlugin; manifest has required fields; meta
returns valid PluginMeta; get_blueprint returns Blueprint or None;
get_models returns model classes; get_collector_schema returns
None or {identityfield, fields}; get_navigation_items and
get_searchable_fields return list.
- Plus 3 framework-level: __contract_version__ is valid semver,
get_event_handlers absent, get_collector_schema present.
Test count: 15 -> 66 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Plugins:
- USB plugin: Device checkout/checkin with employee lookup, checkout history
- Notifications plugin: Announcements with types, scheduling, shopfloor display
- Network plugin: Network device management with subnets and VLANs
- Equipment and Computers plugins: Asset type separation
Frontend:
- EmployeeSearch component: Reusable employee lookup with autocomplete
- USB views: List, detail, checkout/checkin modals
- Notifications views: List, form with recognition mode
- Network views: Device list, detail, form
- Calendar view with FullCalendar integration
- Shopfloor and TV dashboard views
- Reports index page
- Map editor for asset positioning
- Light/dark mode fixes for map tooltips
Backend:
- Employee search API with external lookup service
- Collector API for PowerShell data collection
- Reports API endpoints
- Slides API for TV dashboard
- Fixed AppVersion model (removed BaseModel inheritance)
- Added checkout_name column to usbcheckouts table
Styling:
- Unified detail page styles
- Improved pagination (page numbers instead of prev/next)
- Dark/light mode theme improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Flask backend with Vue 3 frontend for shop floor machine management.
Includes database schema export for MySQL shopdb_flask database.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>