Product version to 0.9.0, frontend in lock-step, Unreleased notes moved into a
dated section per ADR-007. The plugin contract stays at 0.16.0: it moved after
v0.8.1 and is already recorded in this release's notes, and nothing since
touched the contract surface.
A minor rather than a patch: collector behaviour changed in ways an integrator
must know about. A reported machine number no longer becomes the PC's asset
number, it builds a controls link instead; a second PC claiming a machine is
treated as a claim rather than a handover; and a backup revision chain is now
per source PC rather than per asset.
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.
The installer on the share was stamped 0.8.0 and contained sixteen commits that
were not in the v0.8.0 tag. A build that misreports its own version is exactly
what the version check in docs/RELEASING-WINDOWS.md exists to prevent, and it
would have left two sites unable to say what they were running.
Everything in 0.8.1 landed after v0.8.0 was tagged this morning, driven by two
sites entering real data for the first time: the blank-code 500, the two-slide
display that never rotated, modals discarding a part-filled form, filters
returning an empty page, model photos that could not be saved, and the shared
equipment catalog that lets a new site start with vendors, models and printer
supply part numbers already present.
CHANGELOG gains a 0.8.1 section, and the OpenAPI document follows __version__
rather than being restated.
A site reported "internal server error" adding a second business unit. It was
reproducible: create one with a blank code, create another with a blank code,
500.
A column that is unique and nullable accepts any number of NULLs - that is what
makes "optional but unique" work - and exactly ONE empty string. The form sent
'', so the first blank code saved and every one after it collided with it. The
field showed no asterisk because it genuinely is optional; the database just
behaved as though it were not.
This is not specific to business units. A dozen columns across core and the
plugins are unique and nullable - asset numbers, hostnames, item codes, subnet
names, gage-lab tags - and each was one blank form away from the same 500.
Fixing them an endpoint at a time would have left the next to be found by a
user, so a before_flush listener normalises blank to NULL on any unique nullable
text column. Listening on Session rather than on individual mappers covers
plugin models imported later, and avoids mapper-event semantics that differ
between SQLAlchemy versions.
A genuine duplicate is now a 409 with a readable message rather than a bare 500
with a traceback in the log: reusing a code that is taken is the caller's
mistake, not a server fault.
Verified against the development database: three business units with blank codes
all save, the blank stores as NULL, and a real duplicate code returns 409.
The Windows installer has never shipped under a version: v0.7.0 was tagged
before any of it existed, so every build handed out so far stamped a server with
0.7.0. Two servers running different builds were indistinguishable, and the
installer logged each upgrade as "same version already installed" rather than
recording what changed. This cuts the release that fixes that.
0.8.0 rather than a patch: the air-gapped installer is a new capability, and
pre-1.0 semantic versioning puts that in the minor slot (ADR-007).
CHANGELOG gains a 0.8.0 section covering the twelve defects a real Windows
Server 2019 install surfaced, the move from inferring "is this a re-run of my
install?" to recording it, and the operator documentation.
deploy/site-profile-universal.json is now in the repository. Released builds
were being produced from a profile in a temporary directory, so the next release
could not have been reproduced once that file was cleaned up.
docs/RELEASING-WINDOWS.md points at the committed profile and says why.
scripts/gen_openapi.py reads __version__ out of shopdb/__init__.py instead of
restating it. Its hardcoded copy had already drifted a release behind, which is
the same mistake that once shipped an installer stamped with the wrong version.
Adopting a site means getting its asset register in. The HTTP import API suits a
site with a source system and someone to script against it; a sister site with a
spreadsheet and no developer needs something else, and that is the common case.
FOREIGN KEYS TAKE NAMES. This is the whole design. A CSV row has to say where an
asset is, and the database stores locationid, an integer. Requiring the number
means importing locations, reading back the generated ids and pasting them into
the asset sheet - a workflow nobody finishes. Every foreign key here accepts
either a numeric id or the referenced row's name:
assetnumber,assettypeid,statusid,locationid
CMM-01,Measuring Tool,Active,Gage Lab
The column keeps its database name, per CONTRIBUTING.md; the value is whatever
the operator actually knows. Names resolve across files in one run, so
assets.csv can reference a location that only exists because locations.csv was
read moments earlier. A name that does not resolve is reported with its line,
column and value, not as a foreign key violation from three layers down.
Dry run is the default, and writes go into the transaction either way - the
rollback is what makes it a dry run. Skipping the writes instead made every
cross-file reference fail, which is the one thing a folder-wide check exists to
verify. Validation covers every row before anything is written, so a typo on
line 400 cannot leave 399 rows imported. Files are matched on a natural key, so
correcting a spreadsheet and re-running updates rather than duplicates.
TEMPLATES ARE GENERATED, NOT MAINTAINED. "flask csv templates" builds them from
the live schema, annotated with required/optional and which file each foreign
key refers to. The prompt for this was a hand-written template set that had
invented columns on seven of eleven tables and named a table that does not
exist, while looking entirely plausible - and described an import mechanism
(a Data Import page, a flask import-csv command) that had never existed. A test
fails the build if a generated template ever offers a column the schema lacks.
User accounts are deliberately not importable: passwords do not belong in a
spreadsheet in either direction.
Verified end to end against MySQL 5.6 - a folder dry run catching one bad
reference, the fix, the commit, and a re-run reporting updates rather than
inserts. 16 tests.
Two independent ways a restart leaves the SPA unable to navigate, both of
which look identical to a user - a click that does nothing.
1. The router awaits loadEnabledPlugins() to gate plugin routes. An app-pool
restart leaves that request hanging (IIS queues it while the worker starts)
and axios sets no timeout, so the navigation never resolves. Worse, the
promise is cached, so every later navigation awaited the same dead request
and stayed frozen long after the backend recovered. Bound the wait and fail
open on expiry, and drop the cached promise when an attempt times out or
fails so the next navigation retries. The setup-state probe in the guard
gets the same bound (it already fails open, defaulting to "complete").
2. A deploy replaces the content-hashed chunk files, so a tab open across it
asks for chunks that no longer exist and the dynamic import rejects with
nothing handling it. Reload once on a chunk-load error, via router.onError
and Vite's preloadError, guarded by a sessionStorage flag against a reload
loop and cleared on the next successful navigation.
Also stop index.html being cached: it names the hashed chunks, so a stale copy
points at files the deploy already deleted. It now revalidates while the
hashed assets under assets/ cache for a year.
Generate docs/openapi.json (3.1, 362 operations) from the API inventory via
scripts/gen_openapi.py, and serve it with a self-hosted Redoc bundle at
/api/docs - no CDN, works on the air-gapped box. Also serve docs/llms.txt (a
concise LLM entrypoint) at /api/docs/llms.txt. New core 'docs' blueprint;
staticdocs/ excluded from the naming check (vendored minified JS).
A geenforce.fetch token can now be pinned to specific manifest scopes so a
fleet-wide key (a display's, delivered by DSC or baked into the image) is not a
skeleton key for the whole content store. NULL binding = unrestricted, so every
existing service token keeps working.
Core:
- ApiToken.resourcescopes column + resourcescopelist property (migration
7d30_apitoken_resourcescopes; NULL = unrestricted).
- apitokens API create/update accept + persist an optional resourcescopes list
(a resource-name allowlist; not permission-catalog names).
- New contract helper authorized_service_token(scope): same check as
service_token_authorized but returns the ApiToken so a plugin can read its
binding. Contract 0.14.0 -> 0.15.0; also export SupportTeam.
GE-Enforce enforcement:
- get_manifest: a bound token requesting a scope outside its allowlist -> 403.
- get_payload: a bound token may only pull a blob its own scope(s) reference
(service.blob_referenced_by_scopes); anything else -> 404 (no hash probing).
- Decorator stashes the authorized token on g for the route to read.
Also fixes a pre-existing contract-surface violation: the printers/printedparts
alert helpers imported shopdb.core.models / shopdb.extensions directly; now
via shopdb.api (SupportTeam newly exported). Docs: GE-ENFORCE-DISPLAY.md
provisioning note, PLUGIN-HOOKS.md, CLAUDE.md.
9 new resource-binding tests; full suite 1131 passing.
send_webhook(title,text) posts alerts to an optional webhook (Teams Incoming
Webhook / Workflow, or generic JSON) via alert_webhook_url + alert_webhook_format
settings; send_alert fans out to it alongside email; exposed on shopdb.api
(0.13.0->0.14.0, PLUGIN-HOOKS synced); low-stock posts on its custom-recipient
path too. Also: recent-transactions table shows the consumed print-file revision.
Contract 0.13.0 puts the User model on the plugin surface. The
settings page gains a checkbox picker over the user list; selected
users receive low-stock alerts at their account email, merged and
deduped with the free-text address list, inactive accounts skipped,
site alert_recipients still the fallback when both are empty.
Contract 0.12.0: send_email/send_alert join the plugin surface (the
mailer was core-only), PLUGIN-HOOKS and status docs updated, manifest
pins the new floor. The alert fires inside _ledger_write only when a
decrement CROSSES the item's threshold - one alert per depletion,
rearmed by restocking above - and is best-effort after the commit so
mail trouble can never fail a take. Recipients come from
printedparts_alert_email, falling back to the site alert_recipients.
on_enable re-seeds settings idempotently so existing installs pick up
new keys. Crossing/rearm semantics proven by test.
First execution phases of docs/proposals/ge-enforce-plugin.md. The GE-Enforce
manifest becomes shopdb data.
P0 scaffold: new geenforce plugin (api_prefix /api/geenforce, default_enabled
false, core_version >=0.7.0). Registered in PLUGIN_TABLE_OWNERS (ADR-008); its
0001 baseline really creates the tables.
P1a model: one wide manifestentries table + entrytype discriminator (not STI,
not JSON blob), manifestscopes (UNIQUE scopename+phase), the three multi-value
filter child tables, inusechecks + processes, immutable manifestpublishedversions
(frozen rendered JSON), manifestpayloads (inline, capped), pctypealiases
(mirror of the engine lib's alias graph). regvalue stored as its raw JSON
literal so DWord typing survives.
P1c importer + exporter: parse common + gea-shopfloor-* + preinstall.json into
draft rows and rebuild the JSON verbatim from rows in sortorder.
P1d parity harness (GATE A): filters.py mirrors the engine's four filter
functions + alias graph; parity.py proves import+export is behaviorally lossless
(field-identical + same-entries-fire across 18 machine-profile fixtures) WITHOUT
byte-diffing. Verified PASS against all 11 real reference manifests (64 entries)
and a synthetic site-neutral fixture covering every type/filter (the CI gate).
First slice (gea-shopfloor-cmm shape): service layer (import/publish/rollback/
export-to-share), CLI (parity, import-share, publish, export-share), and the
client endpoint GET /api/geenforce/manifest serving the current published
snapshot (never the draft) with ETag/304. Split permissions
geenforce.manage/publish/fetch. Tests prove import->publish->serve, draft edits
never change served bytes, publish+rollback, and auth (401 unauth/wrong-scope).
Contract 0.11.0: added service_token_authorized(scope) to shopdb.api so plugin
service endpoints authorize a scoped managed token without importing core token
internals. Documented in PLUGIN-HOOKS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cuts the large post-0.6.0 pile as a pinnable release: the machines
rename (ADR-011), the API import surface, personal/scoped/collector
API tokens and the get_permissions plugin hook, the ADR-010 frontend
hook contract, per-plugin migrations, model/employee photos, the
dualpath single-machine toggle and relationship propagation, support
teams, email sending, and the shared asset label generator. Plugin
contract moved 0.6.0 -> 0.10.0 over this range (distinct series per
ADR-007).
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>
API tokens: any user mints named, optionally-expiring tokens
(shopdb_pat_..., sha256-stored, secret shown once) at Settings > API
Tokens; a before-request shim swaps a valid PAT for a request-scoped
JWT of its owner, so the entire existing auth/authz/import-mode stack
works unchanged and revoked/expired tokens 401 cleanly. Built for
long-running scripts - the legacy import no longer dies when a login
JWT expires. Migration 7d21_apitokens; create/revoke audit-logged.
Audited integration gaps fixed: Asset.to_dict serializes measuring
tools (typedata + pluginid - relationship links to tools resolve); map
subtype filter/colors and MapEditor include them; dashboard totals
count them; warranty links use a new by-asset route; the measuringtools
ADR-010 hooks are real (corrected presentation token, implemented
map-overlay endpoint); the login avatar resolves through the
employee-photo helper.
737 tests pass; naming green; frontend builds; both features verified
live end-to-end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Most facilities consider a Dualpath pair one physical dual-bay machine.
New site setting dualpath_single_machine (default on): the machines
list, dashboard counts, machines-by-type report, and floor map collapse
each pair to its primary bay (lower assetnumber), with combined
2007 / 2008 labels; pagination totals stay honest. Detail pages remain
per-bay and always show a dual-bay sibling banner linking the partner.
Pair resolution lives in core services and joins the plugin contract
surface (0.8.0 -> 0.9.0).
On the WJ dataset: 31 pairs collapse, machine counts 262 -> 231, map
470 assets. Toggle verified live in both states, left on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Model photos: upload/replace/delete on /api/models/<id>/image (admin),
stored under instance/modelimages/ with a public serve route; thumbnail
plus Upload/Replace/Remove controls in the Models settings modal; the
URL field remains as a manual alternative.
Employee photos, mode-aware: self-hosted directory employees get
upload/replace/delete (photo-<sso> under instance/employeephotos/,
employees plugin migration 0002); external directory mode passes the
HR-supplied picture URL through read-only (writes 409). One resolver
feeds both consumers - the shopfloor recognition/recert kiosk cards and
the employee detail hero - in either mode.
Navigation fix: router-view is keyed on route path, so following a
relationship link between two assets of the same type (machine ->
dualpath machine) reloads the page instead of showing stale content;
query-only URL changes still avoid a remount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the legacy supportteams/appowners pair: supportteams
(teamname unique, teamurl ServiceNow link) + supportteamcontacts
(multiple named contacts with SSO per team, the people you reach out
to), applications.supportteamid intact. Migration 7d18 migrates each
legacy team owner into a contact, drops appowners, and has a validated
downgrade. New /api/supportteams CRUD (admin writes, import-mode
timestamps, teamname lookup), Support card on application detail,
contacts column on the list, and a settings management page.
IMPORT-API.md mapping updated to the concrete endpoints.
658 tests pass; live dev migration applied (24 teams / 24 contacts);
fresh-install and downgrade round-trips verified on scratch DBs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Goal: an LLM or script can migrate an entire legacy database using only
the HTTP API - original history preserved, safely re-runnable.
- X-Import-Mode header (admin only): create/update endpoints across 15
timestamped entity types accept original createddate/modifieddate;
helper exposed via shopdb.api (contract 0.7.0 -> 0.8.0).
- Exact-match natural-key lookup filters on 13 list endpoints for the
lookup-then-upsert recipe.
- Selfhosted USB checkout/checkin accept backdated event times in
import mode.
- docs/IMPORT-API.md: operator manual grounded in the real legacy
schema - order of operations, full table-by-table mapping including
the machines fan-out, idempotent Python importer with dry-run, parity
checks, and decided dispositions for unmigrated tables (DNC config
stays live-fed via the collector; supportteams/appowners map to the
upcoming supportteams model).
635 tests pass; naming green; frontend untouched.
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>
Bump __version__ per ADR-007; roll Unreleased into the 0.6.0
changelog section. Covers the machines rename (ADR-011), the
measuringtools plugin, per-plugin migrations (ADR-008), route
gating (ADR-009), the get_reports hook (contract 0.6.0), and
the sister-site adoption docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The equipment plugin is now the machines plugin, ending the UI-vs-code
vocabulary split while the contract is pre-1.0 and nothing external
depends on the old names.
- plugins/equipment -> plugins/machines: manifest, class, /api/machines,
machines.* permissions, registry key (with an auto-migrating load shim
for existing installs).
- Tables: equipment -> machines (equipmentid -> machineid) and
equipmenttypes -> machinetypes, renamed in the plugin's own migration
chain (machines0002rename), idempotent for both upgrading and fresh
installs.
- The legacy core machinetypes lookup actually types the vendor MODELS
catalog, so it is renamed losslessly to modeltypes
(models.modeltypeid, /api/modeltypes, Model Types settings page)
rather than collapsed, freeing the machinetypes name. Core migration
7d17_machines_rename also flips data in place: assettypes row
equipment -> machine, auditlog entitytype, identifier_/search_
settings keys, permission rows, and renames alembic_version_equipment.
- Frontend: machinesApi/modeltypesApi, item.machine response shape,
assettype value compares 'equipment' -> 'machine' (map, search,
custom fields, relationships), routes machines.js with plugin gating
retagged, /print/machine-badge, Machine Types (subtypes) and Model
Types (catalog) settings pages, machines-by-type report id.
- Docs swept; ADRs left as history per the authoring rule.
Upgrade: flask db upgrade then flask plugin upgrade-all.
Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models
retyped, zero equipment tables remain); fresh scratch-MySQL install
produces the new names; 341 tests green; naming/style green; frontend
builds; live E2E on machines list/detail, PC relationships, map,
reports, and both settings pages.
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>
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>
- setup_complete site setting; a fresh admin is steered to /setup until it is
finished (skippable for the session).
- SetupWizard.vue: Site (facility/base-url/access-domain), Features (plugin
enable/disable), Floor Map dimensions, Starter Data (seed common vendors),
Finish. Reuses the settings + plugins APIs.
- setup blueprint: POST /setup/seed-starter (idempotent common vendors) and
POST /setup/complete. setupState composable drives the router redirect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feature work from the 2026-07 session:
Settings IA
- Replace the flat 27-card settings hub with a persistent two-pane shell
(SettingsLayout.vue): grouped, searchable left rail + content pane.
- Nest all settings/* routes under the shell via router post-processing;
shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers,
Equipment, Network) so per-type settings stop scattering.
Custom fields (core)
- customfields + customfieldvalues tables (migration 7d14), CRUD API at
/api/customfields, per-asset value get/save.
- Settings management page + reusable CustomFieldsSection (detail) and
CustomFieldsInputs (form) wired into all four asset types.
Warranty (new plugin)
- plugins/warranty: warranties + warrantyassets (migration 7d15), derived
coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs).
- API CRUD + per-asset panel + report buckets; WarrantyPanel on all four
detail pages; Warranties management page; Warranty report + Reports card.
- Seed warranty.* permissions.
Printer drivers
- printerdrivers table (migration 7d13) linked to printer models; drivers now
surface on the matching printer's detail page.
Other
- PCDetail rebalanced (Network + Status + Warranty + custom fields on the right).
- Rename PCs list "Features" column to "Remote Access"; fix badge hover underline.
- Drop equipment islocationonly field.
- Centralize asset-type label/route maps into utils/assetTypes.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Classic feature gap: a shopfloor/lobby kiosk auto-selects which business unit to
show based on the display PC's IP (classic dashboarddefaults table +
apivisitorlocation.asp). For the main admin dashboard this does nothing - it is
kiosk/visitor-display infra.
- Model: DashboardDefault (dashboarddefaults: ipaddress unique, businessunitid
FK, description). Migration 7d01_dashboarddefaults (head).
- API (core, /api/dashboarddefaults): CRUD + GET /visitor-location that resolves
the calling display's business unit from its IP (X-Forwarded-For/remote_addr,
or explicit ?ipaddress=); unmapped IP returns a null businessunitid, not an
error. Unauthenticated resolve (kiosks); writes are admin.
- Frontend: ShopfloorDashboard auto-selects its business unit via visitor-location
on load when none is chosen; Settings > Dashboard Defaults CRUD page +
dashboardDefaultsApi client.
Tests: create + resolve by IP -> BU; unmapped IP -> null; duplicate IP 409.
191 tests pass, naming green, app boots, endpoint + admin page verified live.
Co-Authored-By: Claude Opus 4.8 <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>
Second core feature pulled into a plugin. Slides is the minimal plugin shape:
a blueprint only - no model, no AssetType, no nav (it is purely an API surface
consumed by the TV dashboard).
- plugins/slides/: manifest (api_prefix /api/slides, no deps), api/ (moved
blueprint, contract-pure imports via shopdb.api, same /api/slides route so the
TV dashboard is unchanged), plugin.py (get_blueprint, get_models -> []).
- Fixed a latent bug in the move: error path used ErrorCodes.SERVER_ERROR which
does not exist -> ErrorCodes.INTERNAL_ERROR.
- De-cored: deleted shopdb/core/api/slides.py, removed from CORE_BLUEPRINT_NAMES
+ core/api/__init__ import/__all__. Registered in instance/plugins.json.
Pinned with a characterization test first; passes unchanged against the plugin
blueprint. 174 tests pass, naming green, app boots 8 bundled plugins, /api/slides
verified live.
Note: the shopfloor dashboard is NOT extractable - it is a frontend-only Vue view
aggregating the businessunits + notifications APIs, with no backend to move and no
frontend plugin system.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First feature extracted from core into a plugin per "plugin is the product",
mirroring the notifications plugin. KB is a NON-asset plugin: it contributes a
model + blueprint + nav item but registers no AssetType.
- plugins/knowledgebase/: manifest.json (api_prefix /api/knowledgebase, no deps),
models/ (KnowledgeBase, contract-pure imports via shopdb.api), api/ (the
blueprint, same routes/prefix so the frontend is unchanged), plugin.py
(get_blueprint + get_models + get_navigation_items).
- De-cored: removed shopdb/core/models/knowledgebase.py + api/knowledgebase.py,
their __init__ exports, and 'knowledgebase' from CORE_BLUEPRINT_NAMES; dropped
the hardcoded KB nav item from dashboard.py (now via the plugin nav hook).
- search.py and reports.py lazy-import KnowledgeBase from the plugin and degrade
gracefully (search skips via _require_enabled when disabled; kb-popularity
report returns 503 if the plugin is absent).
- Registered in instance/plugins.json (enabled).
The knowledgebase table stays in the core Alembic chain (bundled-plugin schema
folded into core, ADR-004); the model just maps it. KB was never in the
shopdb.api contract surface, so no __contract_version__ bump.
Pinned with characterization tests first (test_knowledgebase.py); they pass
unchanged against the plugin blueprint. 163 tests pass, naming green, app boots
7 bundled plugins, KB endpoint/nav/search verified live.
Co-Authored-By: Claude Opus 4.8 <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>
Addresses findings from a 6-lens review against the project skills
(defining-asset-contract, enforcing-plugin-contract, hardening-flask-config,
integrating-plugin-hooks, pinning-flask-behavior, simplifying-python).
Security (hardening-flask-config):
- Load per-plugin COLLECTOR_API_KEY_<PLUGIN> from env in create_app. from_object
only copies class attributes, so per-plugin keys (ADR-006) were dead in real
deploys and silently fell back to the shared key.
- EMPLOYEE_DB_USER/PASSWORD no longer default to root/rootpassword (no safe
default for a secret; unset fails loud). Documented in .env.example + DEPLOY.md.
- COLLECTOR_API_KEY + per-plugin + EMPLOYEE_DB_* added to .env.example/DEPLOY.md.
Hook isolation (integrating-plugin-hooks):
- collector _collector_plugins and dashboard get_navigation now re-raise in
dev/test and log+isolate in prod, instead of silently swallowing a broken
plugin hook.
Plugin loader (enforcing-plugin-contract):
- enable_plugin/install_plugin read dependencies+version from the manifest
instead of instantiating the plugin class.
- _register_plugin_components rejects a second plugin claiming an already-used
api_prefix (reset per app in init_app).
Tests (pinning-flask-behavior):
- test_identifiers.py: gauge/maintenance round-trip on computer/printer/network
create+update; per-type seed yields the 12 identifier keys.
- contract tests for apply_collector_payload presence + schema-declarers-implement.
- security tests for per-plugin key env loading + no employee-db password default.
Docs/contract sync (defining-asset-contract):
- PLUGIN-HOOKS.md documents apply_collector_payload; stale 0.2.0 -> 0.3.0.
- ADR-006 documents apply_collector_payload + single-dispatch rationale.
- ADR-001 enumerates the expanded shopdb.api import surface.
Simplify (simplifying-python):
- De-duplicate the 21-entry settings defaults: shared build_default_settings()
used by both the /settings/seed route and the CLI (were drifting copies).
- Remove dead AssetStatus import + redundant AssetType local import in computers
plugin; comment the statusid=1 collector default.
153 tests pass (was 145), naming/style green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plugins were reaching into internal core paths (shopdb.core.models.*,
shopdb.extensions, shopdb.utils.*), coupling them to core's file layout and
violating the ADR-001 contract. Consolidate onto one versioned surface.
- shopdb.api: expand from 2 helpers to the full plugin import surface -
db, cache; BaseModel, AuditMixin; core models (Asset, AssetType,
AssetStatus, Vendor, Model, Communication, CommunicationType, Location,
Setting, AuditLog, Application, AppVersion, OperatingSystem); response +
pagination helpers; employee_connection. Documented in PLUGIN-HOOKS.md.
- Migrate all 22 plugin source files to import only from shopdb.api (plus
shopdb.plugins.base for the ABC).
- Drop the printers plugin's legacy MachineType dependency: remove
_ensure_legacy_machine_types and the seed_supplies machinetypeid lookup
(Model.machinetypeid is nullable; printers carry type via PrinterType).
- Guard test test_plugins_only_import_contract_surface scans plugin source
and fails on any core import outside shopdb.api / shopdb.plugins.base.
- Scaffold templates updated so generated plugins are contract-pure.
- Bump __contract_version__ 0.2.0 -> 0.3.0 (additive surface expansion;
manifests pin <1.0.0 so they still satisfy).
145 tests pass, naming/style green, app factory boots all 6 plugins.
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>
The asset/computer model is now the single source of truth. Remove the Machine
instance layer end to end:
- Delete models Machine, MachineStatus, PCType, MachineRelationship,
InstalledApp, PrinterData; keep MachineType (models.machinetypeid still
references it).
- Delete the /api/machines, /api/statuses, /api/pctypes blueprints and the
legacy /api/printers/legacy (PrinterData) blueprint.
- Drop the deprecated communications.machineid column and its FK.
- Migration 7c01 drops tables machines, machinestatuses, pctypes,
machinerelationships, installedapps, printerdata (idempotent).
- Fix remaining readers (applications install counts) to ComputerInstalledApp.
- Frontend: remove dead machinesApi/statusesApi/pctypesApi wrappers; repoint
the PC Types settings page at computer types.
143 tests pass; all asset/computer/dashboard/report/collector endpoints 200.
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>
Establishes the safety net required before any structural refactor.
Tests (tests/):
- conftest.py rewritten for Flask-SQLAlchemy 3.x (drop-recreate per
test, StaticPool-shared in-memory SQLite, admin_user + auth_headers
fixtures). Removes deprecated db.create_scoped_session pattern.
- test_smoke.py: 8 baseline tests (app boot, JWT login valid+invalid,
protected routes, paginated response shape, plugin auto-discovery).
- test_security_config.py: 7 tests pinning ProductionConfig.validate
failure modes (missing/dev SECRET_KEY, missing JWT_SECRET_KEY,
missing DATABASE_URL, wildcard CORS, empty CORS) and one happy-path.
Production hardening (shopdb/config.py, shopdb/__init__.py):
- ProductionConfig.validate() raises ConfigError on missing or
insecure SECRET_KEY, JWT_SECRET_KEY, DATABASE_URL, CORS_ORIGINS.
No silent fallback to dev defaults in production.
- create_app invokes validate() when config_name == 'production'.
- CORS_ORIGINS default no longer wildcard; defaults to localhost
Vite dev origin.
- Drop os.path.exists probe in serve_frontend (path-traversal risk
surface). send_from_directory handles safe-join + 404 itself.
- Replace User.query.get with db.session.get (SQLAlchemy 2.0 API).
TestingConfig (shopdb/config.py):
- Add StaticPool + check_same_thread connect_args so SQLite in-memory
is shared across the test session.
Index dedup (plugins/printers/models/printer_extension.py):
- Rename idx_printer_windowsname -> idx_printerdata_windowsname.
Two model classes (Printer, PrinterData) declared the same index
name; SQLite enforces global index uniqueness even across tables.
Per CONTRIBUTING.md naming convention, indexes follow
idx_<table>_<column>.
Dependency pinning (requirements.in, requirements.txt):
- requirements.in holds the loose source pins (the human-edited file).
- requirements.txt is now a uv-compiled lockfile (every transitive
dep pinned to an exact version). Reproducible builds. Run
`uv pip compile requirements.in -o requirements.txt` to refresh.
Test count: 0 -> 15 passing. All naming/style checks still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
System Settings:
- Add SystemSettings.vue with Zabbix integration, SMTP/email config, SAML SSO settings
- Add Setting model with key-value storage and typed values
- Add settings API with caching
Audit Logging:
- Add AuditLog model tracking user, IP, action, entity changes
- Add comprehensive audit logging to all CRUD operations:
- Machines, Computers, Equipment, Network devices, VLANs, Subnets
- Printers, USB devices (including checkout/checkin)
- Applications, Settings, Users/Roles
- Track old/new values for all field changes
- Mask sensitive values (passwords, tokens) in logs
User Management:
- Add UsersList.vue with full user CRUD
- Add Role management with granular permissions
- Add 41 predefined permissions across 10 categories
- Add users API with roles and permissions endpoints
Reports:
- Add TonerReport.vue for printer supply monitoring
Dark Mode Fixes:
- Fix map position section in PCForm, PrinterForm
- Fix alert-warning in KnowledgeBaseDetail
- All components now use CSS variables for theming
CLI Commands:
- Add flask seed permissions
- Add flask seed settings
Co-Authored-By: Claude Opus 4.5 <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>