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.
PrintedItem (catalog: code, name, image, cached quantityonhand,
per-item threshold, bin) and PrintedItemTransaction (the ledger:
signed quantity change attributed to a badge-resolved employee).
Both registered in PLUGIN_TABLE_OWNERS; 0001 is a post-cutover real
baseline. The migration-guard test learns the new expected head.
Routes are a placeholder ping until the next stage - the scaffold's
list route imported the deleted scaffold model, which surfaces as an
empty 'Migration error' because the alembic env imports the models
package.
Import-surface docs and docstrings describe the automation as a
migration script; status-doc references in CHANGELOG/ADR-009/ROADMAP
point at repo files. Screenshot/verify tools write to /tmp/shopdb-shots
(created on import) instead of a machine-specific directory.
Classic ShopDB listed an application's related KB articles on its page; the flask
app detail didn't, even though KB articles carry an appid FK. get_application now
returns a knowledgebase list (KB rows linked by appid; lazy + plugin-guarded so
core stays decoupled), and ApplicationDetail.vue renders a Knowledge Base section
linking each article.
Verified: an app returns its linked KB (e.g. 77 articles) and the section renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_search_employees always queried the external HR database (employee_connection),
so a site running the selfhosted employee directory (the app-owned
directoryemployees table) got zero employee results - searching an SSO or name
found nothing, and there was no way to reach the employee profile. Made it
mode-aware via the employee_directory_mode setting: selfhosted -> query the
DirectoryEmployee table (lazy, plugin-guarded); external -> the HR DB as before.
Verified: SSO 210009518 -> Jeff Pierce -> /employees/210009518; name "Pierce"
-> Pierce Cox, Andy Pierce, Jeff Pierce.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/applications/{id}/versions now calls apply_import_timestamps so an
imported version's original dateadded/releasedate survives (was skipped, unlike
the app + install endpoints). No-op outside import mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The legacy-import surface (docs/IMPORT-API.md) is the schema-agnostic contract
every adopting site targets; these close two gaps found while mapping the WJ
classic import.
Network device IP: POST/PUT /api/network now accept an `ipaddress` and
materialize a primary Communication (mirroring the printer route), and GET
(list + detail + create/update result) surface it. Previously a network
device's IP - which lives in the communications table, not on the extension -
had no HTTP import path at all.
communicationtypes seed: `flask seed reference-data` now seeds the eight
canonical communication types (IP/Serial/Network_Interface/USB/Parallel/VNC/
FTP/DNC), which IMPORT-API.md already documents as a prerequisite. The IP type
must exist before any asset import so printer/network routes can attach an IP.
There is no CRUD endpoint for these, so seeding is the only path.
Tests: network create/update IP upsert + GET surfacing + seed creates IP type.
204 targeted tests pass; naming + pyflakes green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the database review (verdict: sound-with-minor-issues). Applies the
actionable findings.
Redundant indexes: five non-unique secondary indexes duplicated a named idx_*
or a unique index on the same column - ix_communications_assetid,
ix_computers_hostname, ix_networkdevices_hostname, ix_printers_hostname (each
shadowing an idx_*), and idx_usb_serial (shadowing the serialnumber unique
index). Removed the redundant index source from the models (column index=True /
the extra db.Index) and added core migration 7d25 dropping the live duplicates.
The unique ix_*_assetid indexes are kept (they enforce assetid uniqueness).
Dead column: usbcheckouts.machineid was a NOT NULL soft-ref to the retired
machines table storing sentinel 0 (ADR-001). Dropped from the model + the
machineid=0 literal in selfhosted checkout; usb plugin migration 0002 drops it
live (downgrade restores it default 0).
Index: notifications.businessunitid (filtered by the shopfloor feed) was
unindexed; added index=True + notifications migration 0002.
CI: new migrations-mysql job proves the real multi-site deploy path - fresh
`flask db upgrade` + per-plugin install on utf8mb4 MySQL from empty, asserting
table count + charset and a clean second-run no-op. The pytest suite only
exercises SQLite create_all(), so a regression in the Alembic chain on MySQL
would otherwise ship undetected.
Verified: fresh core upgrade on a scratch utf8mb4 MySQL builds clean + no-op on
rerun (redundant indexes absent, unique assetid kept); plugin migrations applied
+ verified on the dev DB (machineid gone, bu index present). 953 backend tests
pass; naming + pyflakes green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DB review found four DateTime columns defaulting to db.func.now() (MySQL
session-timezone wall clock) while the rest of the schema stores naive UTC, so
one schema mixed two clocks and to_dict() labelled the local values UTC with a
'Z' suffix. Switch application.dateadded, computers.installeddate,
knowledgebase.lastupdated (default + onupdate), and slides.uploadeddate to the
module-level naive-UTC _utcnow callable already used elsewhere (apitoken.py).
ORM-side default only - no column-type change, no data migration; affects
new/updated rows.
Targeted tests pass (154); naming + pyflakes green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings exposure (review medium): GET /api/settings and /api/settings/<key>
now return the full table only to an authenticated principal. Unauthenticated
callers (kiosk dashboards, print pages, login screen, setup router) get just a
public allowlist - categories branding + map plus a named set (site_base_url,
facility_name, printer_hostname_template, contact_email_domain,
servicenow_enabled, setup_complete). A non-public single-key GET returns 404 so
existence is not confirmed. Secrets stay masked in both cases. Closes the
unauthenticated enumeration of smtp_host / employee_db_host / zabbix_url /
servicenow URLs. Allowlist mirrors the keys siteSettings.js + mapConfig.js +
setupState.js read before login.
audit.view (review low): the three audit-read routes (list, entity-history,
stats) were jwt_required only despite a defined-but-unwired audit.view
permission; now gated by it (seeded to admin), so a role-less member or unscoped
PAT can no longer read the cross-user audit trail.
flask seed test-user (review low): refuses outside DEBUG/TESTING - it creates
the well-known admin/admin123; production sites use `flask seed admin`.
Tests: unauthenticated allowlist + authed-full-masked + private-key-404, and
member-403 / admin-200 on audit routes. 336 authz tests pass; naming green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plugins list now carries a displayname (manifest display_name, else the
machine name title-cased). Adds display_name to the four whose title-case
was wrong: GE-Enforce, USB, Measuring Tools, Knowledge Base. The setup
wizard Features step and Settings > Plugins render it, so "Geenforce"/"Usb"
are gone.
Finish step shows a pointer when GE-Enforce is enabled: it still needs a
scoped service token (Settings > API Tokens) and a share export root
(GE-Enforce page) before the fleet uses it - operational config the wizard
does not collect.
frontend build green; naming green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PCs now report enforcement results back to shopdb, closing the desired-vs-observed
loop.
- POST /api/geenforce/report (geenforce.report service token): each cycle a PC
posts the published version it applied, install/skip/fail/filtered counts, and
per-entry outcomes.
- Two tables: manifestenforcementreports (latest-per-host + history: applied
version, enforcer version, counts, derived status ok/selfhealed/failed) and
manifestenforcementresults (per entry: action installed/skipped/failed,
selfhealed flag, exit code, warning/error message).
- RECEIVED: reports carry the applied version; the admin view derives
receivedlatest by comparing it to the scope's current published version, so
the fleet view shows which PCs picked up an update.
- SELF-HEAL: per-entry action captures drift correction (installed when it
should already be present) vs skipped (already good) vs failed, with messages.
- Admin reads: GET /reports (fleet compliance rollup) and GET /reports/<id>
(per-entry detail). New geenforce.report permission.
- Tables added to the (undeployed) 0001 baseline; geenforce.post_report is a
service-token endpoint so it is exempt from the JWT authz sweep, like the
collector blueprint. 8 reporting tests; full suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Collector: the computers collector schema gains defaultprinter and
printers; apply_collector_payload resolves each reported identifier to
a printer asset (windowsname/hostname/sharename/assetnumber/IP,
first-hit case-insensitive) and idempotently syncs relationships -
defaultprinter (directional) for the default, connectedto for the
rest. Collector-created rows are tagged so a re-report archives dropped
links while manual relationships are never touched; unresolved
identifiers warn instead of failing. Both PC and printer detail pages
show the links via the shared relationships card (no frontend change).
GE-Enforce Win32_Printer collection snippet documented.
Searchable custom fields: a per-field searchable flag (migration 7d24);
global search matches custom-field values on flagged active fields and
routes each hit to the asset detail page, reusing the existing
(type,id) dedupe and search_<type>_enabled domain filter. Searchable
toggle on the Custom Fields settings page.
822 tests pass; both verified live.
Co-Authored-By: Claude Fable 5 <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>
Email: a stdlib SMTP mailer (settings-first config, graceful no-op when
unconfigured), a test-email endpoint wired to the Email settings page,
forced first-login password change (users.mustchangepassword, migration
7d23, /change-password flow), new-user welcome mail, and on-demand
report/alert delivery (POST /api/reports/email + Email Report buttons)
with an external-cron-with-a-scoped-PAT path documented for automation.
All tests patch smtplib - no network.
Labels: a shared /print/asset-label/<type>/<id> view any asset detail
page opens - card or plain style, QR or barcode, configurable encoding.
Per-type qr_target_* templates plus label_default_style/codetype/encodes
settings on the Printing page. Measuring-tool labels default to encoding
their inspection-operation code (derived from the location name, e.g.
0615), so every tool in an area shares the area code - verified by
decoding the rendered QR. Machine labels default to the machine number;
blank-serial handled gracefully.
808 tests pass; both features verified live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
A token scoped to the new collector.ingest permission is a collector
service token: the collector endpoints accept it via X-API-Key or
Bearer alongside the env fleet keys (which remain the fallback), giving
the fleet credential rotation, revocation, and last-used visibility
from the API Tokens page. Containment holds both ways: a collector
token authorizes nothing else, and no other credential gains collector
access. Shared token validation refactored out of the auth shim; a
Collector service token quick-preset in the create modal; integration
guide documents minting, rotation via site-config.json, and the
service-identity pattern.
765 tests pass; live acceptance matrix verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A token may carry a scopes list: it then grants only those permissions,
intersected with what the owner holds at use time, with the admin role
bypass suspended and role-gated routes denied - a scoped token from an
admin account is genuinely limited. Scope ceiling enforced at
create/update too (only permissions the owner holds; 400 lists
violations) and the picker only offers what you hold. Token management
itself now requires the new apitokens.create permission (admin by
default, grantable via roles). Unscoped tokens keep the exact prior
act-as-owner behavior; imports need an unscoped admin token.
Migration 7d22.
756 tests pass; live-verified scoped 201/403 matrix.
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>
The measuringtools plugin was missing from two cross-cutting surfaces:
the asset-identifier matrix (no measuring_tool column or per-type
keys - gauge lab reference is their primary identifier) and global
search (results fell to a generic URL and gaugelabreference was never
searched). Measuring tools now have identifier toggles, gated
gauge-lab and maintenance-reference fields on their form and detail,
a search domain toggle, gage-tag search, and proper labels, routes,
and filter chips in search results.
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>
Symmetric relationship types (isdirectional flag, migration 7d19) show
one entry per peer on the relationships card - a Dualpath pair no
longer lists its partner twice - and directional types read naturally
instead of Outgoing/Incoming. Deleting a collapsed entry removes every
underlying direction row.
Propagation is now real (migration 7d20): relationship types declare
propagation-through pairs in relationshiptypepropagations (M:N,
replacing the never-consumed single column); creating a controls link
on either bay of a Dualpath pair auto-creates it on the partner,
mirrored across both endpoints because live data stores controls as
bay -> PC. flask relationships propagate backfills existing data (29
rows fanned out on the WJ dataset, idempotent).
This also completes the tree that commit 1d21bf0 accidentally split
(core/models/__init__ imported RelationshipTypePropagation ahead of the
file that defines it), returning CI to green.
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>
Display label only, matching the Machines relabel; the endpoint id and
equipmenttype entity are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- flask plugin new now scaffolds the frontend too: List/Detail/Form
views on the global styles, a gated route module (ADR-009), and an
api-client snippet emitted into the plugin dir. Views are written
before the route file so a partially generated plugin cannot 500 the
dev server.
- docs/PLUGIN-EXTERNAL-REPO.md + scripts/test-external-plugin.sh: how a
sister site develops a plugin in its own repo and runs the framework
contract tests in CI against a pinned framework ref (script verified
to fail on a broken core_version pin).
- docs/CONTRACT-STABILITY.md: settled vs churning contract surface and
the provisional 1.0 criteria.
- CLAUDE.md active-state refresh (contract 0.6.0, 11 plugins, 340
tests, measuringtools done).
Known limitation documented: Path.rglob does not descend symlinks, so
the import-surface contract test skips symlinked external plugins.
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>
Validated a clean install on MySQL 5.6 end to end and fixed the blockers.
- migrations/env.py: force alembic_version.version_num to VARCHAR(128) in its
own committed connection before running migrations. It was VARCHAR(32); the
revision id 7d02_widen_notification_employee_cols (37 chars) truncated, so the
next migration's version bump matched 0 rows and `flask db upgrade` died at
7d03 on a fresh DB. Now upgrades run clean to head.
- flask db-utils preflight: checks Python, required env, DB connectivity, and
the MySQL 5.6 utf8mb4 index flags (innodb_large_prefix/Barracuda) - the 767
prerequisite - and prints exact fixes. Exits non-zero on blockers.
- flask seed admin --username --email [--password]: real first-admin command
(generates + prints a password once). The docs referenced it but only the
dev-only test-user existed.
- setup endpoints for a UI-driven first run: /setup/needs-admin,
/setup/create-admin (guarded to zero-users), /setup/seed-reference.
- Wizard: drop the PC-access-domain question (always .device.geaerospace.net);
the setting keeps its default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Floor Map settings only had text fields for the blueprint image path with
no way to upload one. Add a real upload.
- POST /settings/map-blueprint (multipart: file + theme) saves the image to the
instance maps dir and points map_blueprint_<theme> at the served URL;
GET /settings/map-blueprint/<file> serves it (public - kiosks read the map).
- Settings > Floor Map: a file picker + thumbnail next to each theme's field
(still accepts a manual path/URL too).
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
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>
- 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>
- DellProvider: real Dell TechDirect lookup. OAuth2 via HTTP Basic auth,
asset-entitlements under /PROD/sbil/eapi/v5 (the device.warranty path 404s
for this account), map latest dated entitlement to service level + dates.
Cache the token process-wide; Dell rate-limits the token endpoint and a
fresh request per refresh trips a 401 cooldown. Verified against live Dell.
- Lenovo/HP stay config-shaped stubs.
- Settings: warranty_dell_* keys (category integrations); Dell Warranty Lookup
block in System Settings > Integrations (enable + client id/secret masked +
optional token/API URL overrides).
- WarrantyPanel takes optional pre-fetched items; PCDetail fetches once and
feeds both the panel and a new hero warranty-status/end-date badge.
- tools/mock_dell.py for offline testing of the provider flow.
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>
Extends the computers collector so it can replace the classic api.asp
updateCompleteAsset path that the shopfloor PC fleet uses to auto-update data.
Collector schema (project naming) now accepts the GE-Enforce/enrollment shape:
machinenumber, pctype, pcsubtype, serialnumber, loggedinuser, lastboottime,
lastcheckin, ipaddress, vendorname, modelnumber, osname, installedsoftware.
- machinenumber -> Asset.assetnumber (skips the 9999 imaging placeholder, falls
back to hostname), on create and update.
- pctype -> ComputerType via a configurable mapping (see below).
- vendor/model created if missing (free vocab); OS looked up (controlled, warns
if unknown); pcsubtype accepted but not yet stored (warning).
- Dropped per scope: VNC/WinRM flags, warranty, DNC config, multi-NIC.
Configurable pc-type mapping (the gea-shopfloor-* imaging taxonomy ->
ComputerType): defaults + resolution live in plugins/computers/pctypemap.py
(plugin domain, contract-pure - reads Setting via shopdb.api); overrides stored
as pctypemap_<pxetype> settings, seeded on plugin install, edited in Settings >
System > "Collector PC Type Mapping" (new UI section).
Migration doc: docs/COLLECTOR-INTEGRATION.md maps classic api.asp fields +
GE-Enforce status fields to the collector schema, documents machine-number
sourcing (registry MachineNo first, then C:\Enrollment\machine-number.txt) and
that the transport is interim.
Tests: complete-asset payload maps machinenumber/pctype/vendor/model/os; 9999
placeholder falls back to hostname. 186 tests pass, naming green, app boots,
mapping UI verified.
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>
Lets an admin choose which content types appear in global search results,
independent of whether the owning plugin is enabled (the existing
_require_enabled gating was all-or-nothing per plugin).
- settings.py: SEARCH_DOMAINS const (9 result types: application, knowledgebase,
employee, equipment, computer, printer, network_device, notification, subnet)
+ seed keys search_<type>_enabled (boolean, default true) in
build_default_settings (covers API seed + CLI).
- search.py: global_search loads disabled types in one query (category 'search')
and filters the deduped results by type before counts/truncation. Missing key
= enabled.
- SystemSettings.vue: "Global Search" section, one toggle per domain (mirrors the
identifier pattern; create-on-404 fallback so an un-reseeded deploy still works).
- Tests: domain included by default, disabled domain excluded, seed creates the
9 keys.
166 tests pass, naming green, build green. Verified live: toggling
search_knowledgebase_enabled off drops knowledgebase from search counts and back
on restores it. Dev DB seeded (9 keys).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>