Files
shopdb-flask/CHANGELOG.md
cproudlock 7dfbe7bf8a
All checks were successful
CI / backend (push) Successful in 1m20s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
Add the get_permissions plugin hook (contract 0.10.0)
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>
2026-07-12 09:29:55 -04:00

354 lines
21 KiB
Markdown

# 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
- Plugin `get_permissions` hook (contract 0.10.0) so a plugin declares the RBAC
permissions its own routes enforce, instead of core accumulating every
plugin's permissions in `Permission.PERMISSIONS` (plugin-is-the-product). The
core catalog (`Permission.CORE_PERMISSIONS`) now holds only genuinely core
sets (assets, applications, reports, settings, users, audit, apitokens,
collector); the 36 permissions for machines, computers, printers, network,
knowledgebase, notifications, usb, warranty, and measuringtools moved into
each owning plugin's hook. New core helper `full_permission_catalog()` merges
core plus every ENABLED plugin's permissions and backs all three consumers:
`flask seed permissions`, the role grid (`GET /api/users/permissions`), and
API-token scope validation (`ApiToken.unknown_scope_names`). Plugin install
and enable seed the plugin's own permissions idempotently. A disabled plugin
drops out of the catalog (no new scope grants or role assignments), but its
existing `Permission` rows and role links persist so current roles keep
working. Docs: `docs/PLUGIN-HOOKS.md` new section, `docs/PLUGIN-GUIDE.md`
permissions walkthrough rewritten to the hook, `docs/PLUGIN-QUICKSTART.md`
hooks table row.
- Personal API tokens (PATs) so scripts and integrations authenticate without
the hourly-expiring login JWT (immediate consumer: long legacy-import runs
that die when the JWT expires mid-run). New core `apitokens` table + migration
`7d21_apitokens` (stores only the sha256 hash of each secret; the full secret
`shopdb_pat_<40 hex>` is shown ONCE at creation). New core blueprint
`/api/apitokens` (list own / admin `?all=true`; create; rename or deactivate;
revoke). A `Bearer shopdb_pat_...` header is recognized before any JWT decode
by a before_request shim that mints a request-scoped JWT for the token's
owner, so the entire existing auth+authz stack (jwt_required,
require_permission, require_role, import mode, current_user) authenticates the
PAT as its owner with zero decorator changes; an invalid, revoked, or expired
PAT gets a clean 401. `lastusedat` is stamped on use (throttled to at most one
write per 60s). Any authenticated user manages their own tokens; admins may
list or revoke anyone's. New Settings > API Tokens page (`ApiTokensList.vue`)
with a create modal that reveals the secret once (copy button) and an admin
All Tokens section. Docs: `docs/IMPORT-API.md` and `docs/CONFIG.md` updated to
recommend a PAT for imports. Core feature; no plugin-contract change.
- Optional permission scopes on personal API tokens. A token MAY carry a scopes
list (permission names, migration `7d22_apitokens_scopes` adds the nullable
`apitokens.scopes` JSON column); NULL keeps the original behavior (acts as its
owner). A scoped token grants ONLY the listed permissions, intersected with
what the owner actually holds at use time, and SUSPENDS the admin-role bypass,
so a scoped token minted by an admin is genuinely limited: it is denied on
role-gated (`require_role`) endpoints and gets no import mode. The shim mints
the request JWT with a `patscopes` claim that `require_permission`,
`require_role`, and `import_mode_active` read; normal login JWTs carry no such
claim and are unaffected (zero regression). Scopes are validated at write time
against the token OWNER's permissions (the scope ceiling - a token can never
grant more than its owner holds; when an admin edits another user's token the
ceiling is that owner's permissions), rejecting unknown or unheld names 400.
Minting/managing tokens now requires the new `apitokens.create` permission
(category `apitokens`; admins hold it by default, grantable via the roles UI)
rather than being open to any authenticated user. The Settings > API Tokens
create/edit modals gain a "Restrict permissions" section (a category-grouped
checkbox grid limited to the permissions the creator holds) and the token
lists show a full-access / N-permissions access chip.
- Managed collector service tokens: the collector ingest API
(`/api/collector/<plugin>` + the legacy `/pc` `/apps` `/heartbeat` `/bulk`
`/status` endpoints) now ALSO accepts a managed API token scoped to the new
`collector.ingest` permission (category `collector`), on top of the existing
`COLLECTOR_API_KEY[_<PLUGIN>]` env keys (which stay supported as a
bootstrap/legacy fallback - nothing breaks). The token may be presented in
`X-API-Key` (as GE-Enforce sends today) OR as an `Authorization: Bearer`
token; both transports validate the PAT the same way the login shim does
(hash lookup, active, unexpired, active owner) via a shared
`resolve_api_token` helper refactored out of `apitoken_auth.py`, require
`collector.ingest` in the token's scope list AND that the owner holds it, and
stamp `lastusedat` (same 60s throttle). A token scoped to ONLY
`collector.ingest` is a collector service token: it authorizes the collector
API and NOTHING else - the existing scoped-token machinery denies it on every
permission- and role-gated route and on import mode, so a leaked collector
token cannot touch the regular API. Recommended flow (documented): an admin
mints the scoped token (the scope suspends the admin bypass, containing it);
rotate by minting a new one, deploying via `site-config.json`, watching
`lastusedat`, then revoking the old. The Settings > API Tokens create modal
gains a "Collector service token" quick-preset (pre-selects only
`collector.ingest`). Docs: `docs/COLLECTOR-INTEGRATION.md` (new "Managed
collector tokens" section) and `docs/CONFIG.md`. Core feature; no
plugin-contract change.
- Vendor-model photos on asset detail heroes: computers and printers now
surface the linked model's `imageurl` in their extension payloads (the
field machines already exposed), and the machine, PC, printer, network
device, and measuring tool detail pages render the photo in the hero card
when present (hidden cleanly when absent). Network devices and measuring
tools have no model link yet, so their heroes stay photo-less until one
is 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.
### Changed
- System Settings is no longer one tabbed page. The inner tab bar is gone and
each section is its own routed settings page reached through the settings
rail: ServiceNow, Zabbix Supplies, Dell Warranty, and Collector PC Types
(the former Integrations dumping ground, now split three-plus ways), plus
Branding, Floor Map, Printing & Labels, Email / SMTP, Authentication,
Audit & Logging, Asset Identifiers, and Global Search. The rail regroups
these under Site & Facility, Integrations, Communication, Search & Identity,
and Access & Security. Shared load/save/upload plumbing moved into a
`useSystemSettings` composable so the pages stay thin. Old bookmarks keep
working: `/settings/system` and every `/settings/system?tab=<key>` redirect
to the matching new page.
### Fixed
- Measuring tools wired into the remaining cross-cutting surfaces an audit
found them missing from: generic asset serialization (typedata + pluginid,
which also fixes relationship-card links to tools), map subtype
filtering/coloring and the MapEditor filter, dashboard totals, warranty
asset links (via a new by-asset detail route), and the two ADR-010 hook
declarations (presentation route token corrected; the calibration
map-overlay endpoint now actually exists). The login avatar also resolves
through the employee-photo helper, so self-hosted uploads show.
- Measuring tools are now wired into the asset-identifier matrix and global
search. The Settings identifier matrix gains a Measuring Tool column and the
gauge-lab and maintenance reference inputs/rows on the measuring-tool form
and detail pages honor those per-type toggles (a maintenance-reference field
was added, matching the other asset types). Global search gains a Measuring
Tools domain toggle and filter chip, routes measuring-tool hits to
`/measuringtools/<id>` (previously the generic `/assets/<id>` fallback), and
matches on `gaugelabreference` so a gage-tag lookup resolves the tool.
- Site & Facility settings page renders booleans as toggles and the
directory-mode settings as dropdowns, with labels and help text for every
field (no more raw keys or type-true/false boxes).
- 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