# Changelog

All notable changes to shopdb-flask are recorded here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The product version (`__version__`) and the plugin-contract version
(`__contract_version__`) are distinct series with independent bump rules; see
ADR-007 and ADR-002.

## [Unreleased]

### Added

- Dualpath "single machine" site toggle (`dualpath_single_machine`, default
  on). A Dualpath relationship pair is one physical dual-bay machine (single
  controller, bay-selector switch); when on, the machines list, dashboard and
  machines-by-type counts, and the floor map collapse each pair to one entry
  (the lower natural-sort assetnumber is PRIMARY; the SECONDARY bay is hidden)
  and show a combined `2007 / 2008` label. The data model is unchanged (both
  bay records always exist); detail pages stay per-bay and always show a
  sibling-bay banner regardless of the toggle. Contract surface (plugin
  contract bumped 0.8.0 -> 0.9.0, additive): new `shopdb.api` helpers
  `resolve_dualpath_pairs` and `dualpath_single_machine_enabled`, consumed by
  the machines plugin to collapse pairs contract-purely.
- Relationship propagation, wired and data-driven: relationship types
  declare propagation-through pairs (relationshiptypepropagations M:N,
  replacing the never-consumed single column); creating a controls link on
  one Dualpath bay auto-creates it on the partner bay, and
  `flask relationships propagate` backfills existing data.
- Employee photos, mode-aware: self-hosted directory employees support
  upload/replace/delete (admin), served publicly for kiosk cards; external
  directory mode passes the HR-supplied picture URL through read-only. One
  resolver feeds the shopfloor recognition/recert cards and the employee
  detail hero in either mode.
- Vendor-model photo management. New admin-gated core endpoints
  `POST /api/models/<modelid>/image` (multipart `file`, png/jpg/jpeg/gif/webp/svg,
  one image per model, replace semantics) and
  `DELETE /api/models/<modelid>/image`, plus the public
  `GET /api/models/image/<filename>` serve route. Uploads land in
  `instance/modelimages/` (survives upgrades, backed up with the rest of
  `instance/`) and set `models.imageurl` to the served URL; the manual Image URL
  field still accepts external URLs and the shipped `/images/models/*` assets
  (upload is additive). Delete only removes files we own under the instance dir.
  The Models settings page grows a thumbnail, Upload/Replace, and Remove
  controls in the edit modal. Asset hero images (e.g. the machine badge) read
  `imageurl` unchanged, so uploaded photos render with no consumer changes.

- Application support teams with contacts, replacing the legacy
  supportteams/appowners pair. New core `supportteamcontacts` table (multiple
  named contacts per team, ordered by `sortorder`); `supportteams` keeps
  `teamname` (now unique) and `teamurl` (a ServiceNow group deep link) and
  sheds the single-owner `appownerid` FK. New core blueprint at
  `/api/supportteams` (team + nested contact CRUD, admin-gated; `?teamname`
  exact-match lookup for import; delete a team 409s while any application still
  references it). Migration `7d18_supportteamcontacts` migrates each legacy
  team's app owner into one contact. Application payloads now flatten
  `supportteamname`, `teamurl`, and the team's active `contacts`; a Support
  card on the application detail page and a new `settings/supportteams`
  management page render them.

- Import mode: a complete, idempotent HTTP migration surface so a script or LLM
  can import the classic ASP shopdb through the API alone (no direct DB writes).
  - Contract surface (plugin contract bumped 0.7.0 -> 0.8.0, additive): new
    `shopdb.api` helpers `apply_import_timestamps`, `import_mode_active`,
    `parse_import_datetime` in `shopdb/utils/import_mode.py`. When the caller is
    an admin AND sends header `X-Import-Mode: true`, create/update endpoints
    accept optional `createddate` / `modifieddate` (ISO or legacy
    `YYYY-MM-DD HH:MM:SS`, naive-UTC) and preserve them instead of stamping now.
    Non-admin or missing header: the fields are ignored exactly as before.
    Wired into every timestamped import target: assets (all five type plugins),
    vendors, models, modeltypes, businessunits, locations, operating systems,
    applications, knowledge base, USB devices, and asset relationships.
  - Natural-key exact-match lookup filters for the documented
    lookup-then-upsert idempotency recipe: `assetnumber` on all five asset
    plugin list endpoints; `vendor`, `modelnumber`, `modeltype`,
    `businessunit`, `locationname`, `osname`/`osversion`, `appname`,
    knowledge base `linkurl`/`shortdescription`, warranty `servicetag`/`vendor`,
    and notification `ticketnumber`.
  - Backdated event history: in import mode the selfhosted USB checkout/checkin
    endpoints accept optional `checkouttime` / `checkintime` overrides so
    migrated `usbcheckouts` rows keep their real event times.
  - New operator manual `docs/IMPORT-API.md` grounded in the real `prodscratch`
    legacy schema: order of operations, a full table-by-table mapping, honest
    no-target list with dispositions, a worked idempotent Python importer, and
    row-count parity checks.

### Fixed

- System Settings tabs follow the URL: clicking a settings-rail link that
  only changes the ?tab= query (Branding, Floor Map) now switches the right
  panel, tab clicks update the URL, and browser back/forward restore tabs.
- Following a relationship link between two assets of the same type now loads
  the destination page instead of stale content (router-view keyed on path;
  query-only URL changes still avoid a remount).
- Asset relationships card no longer lists a symmetric peer twice. Relationship
  types gain `relationshiptypes.isdirectional` (migration
  `7d19_relationshiptype_directional`; seeded false for the connection-like
  types Dualpath, connectedto, Cluster Member, Serial Cable, Direct Ethernet,
  USB, WiFi, true for controls/Controlled By/Backup For/Master-Slave/partof/
  defaultprinter). The card now collapses every stored direction row of a
  symmetric type into one direction-blind "Connected" entry per peer (deleting
  it removes all collapsed rows), while directional types drop the
  Outgoing/Incoming headers for inline `Type -> peer` / `<- Type from peer`
  phrasing. The type CRUD and the per-asset relationships endpoint carry
  `isdirectional`; the Relationship Types settings page gains a Directional
  toggle.

## [0.6.0] - 2026-07-11

### Added

- measuringtools plugin (ADR-005): gage-lab instruments as Asset extensions
  with type lookup (color-coded), calibration tracking (derived
  overdue/due-soon/current status), calibration report, and full frontend.
  Built as the framework exemplar; docs/PLUGIN-GUIDE.md walks through its
  construction step by step as the plugin-system tutorial.
- CSV export on the Warranty and Toner report pages; per-report filter
  controls (business unit, asset type, location, application, limit) on the
  inline core reports; report open-state is URL-backed and deep-linkable.
- Per-plugin Alembic migration chains (ADR-008): every bundled plugin now
  carries its own chain with a stamp-only anchor; new plugin schema changes
  land in `plugins/<name>/migrations/`, never the core chain. Deploys run
  `flask plugin upgrade-all` after `flask db upgrade`.
- Frontend plugin route gating (ADR-009): a disabled backend plugin's pages
  redirect to the dashboard; new public `GET /api/plugins/enabled`.
- `get_reports()` plugin hook (plugin contract 0.5.0 -> 0.6.0): plugins
  contribute their own report cards; warranty and toner cards moved off the
  hardcoded frontend list.
- Reports hub grouped by category with a search filter.
- Configurable QR label targets: `qr_target_printer` / `qr_target_usb`
  settings (blank = the asset's own page, else a URL template with
  placeholders) and a `usb_label_style` barcode/QR toggle for USB
  mini-labels. New Settings > Printing & Labels section.
- Site palette theming: optional `brand_primary_dark_color`,
  `brand_accent_color`, `brand_sidebar_color` settings applied at bootstrap.
- Collector integration guide rewrite: header-only auth reference and a
  paste-ready GE-Enforce PowerShell reporting function.

### Changed

- Equipment -> machines rename (backend). The equipment plugin is now the
  machines plugin: `/api/equipment` -> `/api/machines`, tables
  `equipment`/`equipmenttypes` -> `machines`/`machinetypes` (columns
  `equipmentid` -> `machineid`, `equipmenttypeid` -> `machinetypeid`,
  `equipmenttype` -> `machinetype`), permissions `equipment.*` ->
  `machines.*`, assettype value `equipment` -> `machine`. The legacy core
  `machinetypes` lookup (it types the vendor MODELS catalog, not machine
  instances) is renamed to `modeltypes` (`/api/machinetypes` ->
  `/api/modeltypes`, `models.machinetypeid` -> `models.modeltypeid`) to
  free the name. Data flips migrate assettypes, auditlog entitytype,
  settings keys (`identifier_*_equipment_enabled` ->
  `identifier_*_machine_enabled`, `search_equipment_enabled` ->
  `search_machine_enabled`), and permission rows in place; plugins.json
  registry entries carry over automatically. Upgrade: run
  `flask db upgrade` then `flask plugin upgrade-all`.

- Inter (variable) replaces Roboto, bundled locally - no Google Fonts
  fetch, so air-gapped installs render correctly. Tables use tabular
  numerals.
- ServiceNow defaults point at the current geaerospaceqa.service-now.com
  global search (search, incident, and change links).

### Fixed

- USB frontend remapped to the actual API shape (`device_id` /
  `device_desc`): device list, detail, form, label batch, and the employee
  profile's checked-out/history panels were all reading dead legacy fields.
- External-mode `GET /api/usb/checkouts/active` now honors the `badge`
  filter.
- Warranties list page no longer demands login (matches every other list
  page; reads were already public).
- Removed the dead legacy Warranty Status report (always-zero buckets from
  a retired column); the warranty plugin's report is the real one.
- Pruned dead usbApi client methods that had no backend routes.

## [0.5.0] - 2026-07-10

First release cut with a version, tag, changelog, and CI. Focused on
letting other GE Aerospace sites stand up their own self-hosted instance
(single-tenant per ADR-004).

### Added

- First-run setup wizard (`/setup`): creates the initial superadmin
  in-app, configures each plugin (create tables here vs connect your own
  database), uploads light/dark floor-map blueprints, and seeds starter
  reference data.
- Self-hosted employee directory and USB plugins: in-app management plus
  CSV import, no external database required. Both ship default-disabled
  with an enable-time provisioning note.
- Dell warranty plugin: real Dell provider, bulk warranty sync,
  add-warranty from asset pages, PC hero warranty badge, disk-cached Dell
  API token.
- Custom fields, and a two-pane settings shell with tabbed, searchable
  System Settings and Settings index pages.
- Dashboard defaults (visitor-IP to business-unit mapping) for kiosk
  displays; printer installer endpoint (data plus floor-map positions).
- Global toast notifications replacing `alert()` calls.
- Multi-stage Docker build that compiles the Vue frontend and ships
  `frontend/dist`, which Flask serves.
- Documentation overhaul: new CONFIG, UPGRADE, and BACKUP-RESTORE guides;
  reconciled README, DEPLOY, CLAUDE, and ROADMAP.
- ADR-007 (product versioning and releases), CHANGELOG, and best-effort
  Gitea Actions CI (backend tests, naming/style gate, frontend build).

### Changed

- Plugin contract (`__contract_version__`) settled at 0.5.0: full plugin
  import surface exposed via `shopdb.api`, dead search hook removed, and
  the dashboard-widgets hook wired to a real consumer.
- Role-based access control now enforced on write routes, including
  admin-only guards on dashboard-defaults writes.
- Branding, ServiceNow integration, employee-ID pattern, printer
  hostname template, and floor-plan blueprints are settings-driven and
  per-site configurable, with GE defaults preserved as shipped fallbacks
  (branding and floor-plan configurability landed in this release; some
  consumer wiring continues under Unreleased).

### Security

- Dashboard-defaults writes now require admin authorization instead of
  any authenticated user.
- Collector error responses no longer leak exception detail; failures are
  logged server-side with generic client-facing messages.
- Login rate limiting added (IP-based fixed window) on top of the
  existing account lockout.

### BREAKING

- Collector API key must now be sent in the `X-API-Key` header. The
  api-key-in-querystring fallback has been removed. Update any collector
  integration that passed the key as a query parameter. See
  `docs/COLLECTOR-INTEGRATION.md`.

[Unreleased]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.5.0...HEAD
[0.5.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/releases/tag/v0.5.0
