Dedup compared a posted config against the latest revision for the ASSET, which is only correct when a machine number means one PC. Several PCs share one here: the part markers on 0613, 0615 and WJPRT are separate devices, differing by COM port, filed under one machine number. Each marker's post therefore differed from whichever marker had posted last, nothing ever deduped, and the table grew by one row per PC per collection cycle. A chain is now (asset, kind, source hostname). An unchanged config is a no-op again, and each PC keeps its own history against the machine. NULL sources - rows written before the column was populated, and hand-loaded ones - form their own chain via IS NULL; `column == None` never matches in SQL, so without that those rows would have re-posted forever. Two consumers assumed the old key and are fixed with it. Retention pruned per asset, so a busy marker's revisions could evict a quiet marker's only backup; it now prunes each chain separately, protecting the newest and oldest of each. The revision diff compared against the previous revision on the machine, which across two markers reported one device's COM port as a change on the other; it now compares within the source's own chain. scripts/collapse_duplicate_backup_revisions.py cleans up what the old rule wrote. It removes only a revision whose hash repeats the one before it in the same chain - rows the fixed code would never have written - and keeps every genuine change, every chain's newest and oldest, and every source. Dry run by default. Its --report mode explains what grew each chain, which separates a legitimately shared machine number from two PCs wrongly carrying the same one, and from a value inside the config that changes on its own.
744 lines
45 KiB
Markdown
744 lines
45 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]
|
|
|
|
### Fixed
|
|
|
|
- A shop-floor PC that reported the machine number of a machine ShopDB already
|
|
knew got a 500 from the collector, on every report, forever. The reported
|
|
machine number was being written to the PC's own `assets.assetnumber`, which
|
|
is uniquely indexed and already held by the machine, so the insert failed with
|
|
a duplicate-key error and the whole report was lost: operating system, boot
|
|
time, applications, printers and access protocols never landed. The machine
|
|
number now identifies the machine only. A new PC takes its hostname as its
|
|
asset number, which is what the existing data already does, and an existing
|
|
PC's asset number is never overwritten.
|
|
- Configuration backups piled up duplicate revisions on any machine number
|
|
reported by more than one PC. A posted config was compared against the latest
|
|
revision for the machine, so where several PCs share a number - the part
|
|
markers do, and their configs differ by COM port - each PC's post differed
|
|
from whichever PC posted last, nothing deduped, and the table grew by a row
|
|
per PC per collection cycle. A backup chain is now per source PC, so an
|
|
unchanged config is a no-op again and each PC keeps its own history against
|
|
the machine it belongs to. Retention prunes each chain separately, so a busy
|
|
PC's revisions can no longer push out a quiet PC's only backup, and the
|
|
revision diff compares against the same PC's previous revision rather than
|
|
another PC's.
|
|
- A backup export containing a value with an empty right-hand side (`Name=`)
|
|
failed to parse, and with it the whole file, so that machine could never be
|
|
backed up. The form is not strictly legal but occurs in real exports. It is
|
|
now read as an empty string, preserving the value name.
|
|
- The 3D parts kiosk label prefix never appeared on the kiosk. The kiosk runs
|
|
logged out, and an unauthenticated read of a setting is limited to an
|
|
allowlist the key was not on, so the kiosk got a 404 and fell back to no
|
|
prefix. An admin previewing the same page while logged in saw it, which is
|
|
why it looked like it worked. A plugin now declares which of its settings a
|
|
logged-out page may read.
|
|
- 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 a placeholder category, where the settings page, which
|
|
lists by category, could no longer see it. The value was in the database the
|
|
whole time. A first-time save now files the key under the category its owner
|
|
declares. Any row already misfiled is repaired in place on upgrade, value
|
|
untouched. The 3D parts alert email and the employee directory host were in
|
|
that state.
|
|
- Settings added in a later plugin version never reached a site that installed
|
|
an earlier one. The seeding ran from `on_install` / `on_enable`, which fire
|
|
only on a state transition, so neither ran again on an upgrade - and the
|
|
comment claiming enable ran every upgrade cycle was wrong.
|
|
`flask plugin upgrade-all` now seeds missing declared settings, without
|
|
touching values a site has configured.
|
|
|
|
### Added
|
|
|
|
- A PC reporting a machine number is now linked to that machine. Until now the
|
|
number was collected and then discarded, so a replaced PC never took over its
|
|
bay: the retired PC kept the link and everything that walks PC to machine, the
|
|
warranty machine column and the DNC info card among them, pointed at hardware
|
|
that had been pulled out. The link is a `controls` relationship carrying a
|
|
`collector:machine` origin label, so a link made by hand is never touched.
|
|
Superseding a PC archives the old link rather than deleting it, keeping the
|
|
history of which PC ran a machine and when. The retired PC's status is
|
|
deliberately left alone: the collector cannot tell whether it was shelved,
|
|
sent for repair or re-imaged for another bay, so a person decides.
|
|
|
|
A second PC reporting a machine another PC already holds is treated as a
|
|
claim, not a handover. A PC imaged for a machine carries that number before
|
|
it ever reaches the floor, so the PC actually running the machine keeps the
|
|
link while it is still reporting and still In Use, and the challenger is
|
|
recorded as a dormant link. The handover completes on its own once the old PC
|
|
has been quiet for a day, or immediately when someone moves it off In Use.
|
|
Without this the two PCs traded the link back and forth on every collector
|
|
cycle.
|
|
|
|
Alerts for both cases go out by email and webhook, gated on a new
|
|
`computers_machinelink_alerts` setting that ships OFF: a site may legitimately
|
|
run several PCs on one machine number, and there the alerts fire on correct
|
|
data. The collector response warns either way.
|
|
- Plugin contract 0.16.0: `get_settings_defaults()` lets a plugin declare the
|
|
settings it owns (key, default, type, category, description, and whether an
|
|
unauthenticated caller may read it). See `docs/PLUGIN-HOOKS.md`.
|
|
|
|
## [0.8.1] - 2026-08-05
|
|
|
|
Everything here shipped after v0.8.0 was tagged the same morning, driven by two
|
|
sites putting real data in for the first time. Most of it is defects that only
|
|
appear when somebody who did not build the software tries to use it.
|
|
|
|
### Fixed
|
|
|
|
- A blank optional code could be saved once and never again. A unique nullable
|
|
column accepts any number of NULLs but exactly one empty string, so the second
|
|
business unit with no code collided with the first and returned 500 from a
|
|
field the form correctly showed as optional. Blank now stores as NULL, which
|
|
covers every unique nullable column - asset numbers, hostnames, item codes,
|
|
subnet names, gage-lab tags - not just the one that was reported. A genuine
|
|
duplicate answers 409 with a readable message instead of a bare 500.
|
|
- A lobby display with exactly two slides never changed between them. The
|
|
slideshow was started twice, and two timer chains advancing one slide each
|
|
landed back on the first every cycle. With three or more slides it advanced by
|
|
two and merely skipped one, which is why it went unnoticed. The per-slide
|
|
duration the feed has always sent was also ignored in favour of a hardcoded
|
|
ten seconds.
|
|
- Clicking slightly outside a modal discarded a part-filled form, in 35 modals
|
|
across 30 files. Confirmation dialogs still dismiss that way, since they hold
|
|
nothing to lose.
|
|
- Selecting a filter while past page one returned an empty list, because the
|
|
filter asked for page five of a result set that now had one page.
|
|
- Model photos could not be saved: an uploaded photo sets the field to an
|
|
application path, and the input was type="url", which demands an absolute
|
|
address. The upload button was also hidden when creating a model.
|
|
- An empty bordered box appeared after Notes on the network device form.
|
|
|
|
### Added
|
|
|
|
- The equipment catalog travels. `flask seed catalog` loads 53 vendors, 128
|
|
models with photos, 146 printer supply part numbers and every type vocabulary,
|
|
so a new site can start adding printers immediately instead of retyping a
|
|
catalog another site spent a year building. Idempotent and additive; catalog
|
|
only, with nothing site-specific. Offered by the installer as a tick-box and
|
|
by the operator console.
|
|
- `shopdb-admin.ps1 repair` completes an interrupted provisioning, and `check`
|
|
now says when a server is not fully provisioned instead of leaving it to be
|
|
inferred from 500s on unrelated pages.
|
|
- Network devices can be linked to a catalog model, and their map position is
|
|
picked on the floor plan rather than typed as coordinates.
|
|
- An asset's vendor and type can be derived from its catalog model, by exact
|
|
name match only, with a reviewable backfill script.
|
|
|
|
### Changed
|
|
|
|
- Labels say whose type they mean: Machine Type, PC Type, Printer Type, Device
|
|
Type, next to the catalog's Model type.
|
|
- The 3D parts kiosk label prefix is a setting rather than one site's initials
|
|
hardcoded in the source.
|
|
- The operator console menu is grouped by what each action touches.
|
|
|
|
## [0.8.0] - 2026-08-05
|
|
|
|
First release to carry the Windows installer. Everything below shipped after
|
|
v0.7.0 was tagged, and a complete install was exercised end to end on Windows
|
|
Server 2019 before this release was cut.
|
|
|
|
### Fixed - installer, from a real Server 2019 install
|
|
|
|
- `packaging` was imported by the plugin loader but declared nowhere. It reached
|
|
development and CI only as a dependency of pytest, so the whole suite passed
|
|
while a virtual environment built from `requirements.txt` alone - which is
|
|
exactly what the installer builds - could not import the application at all.
|
|
`tests/test_runtime_dependencies.py` now fails on any runtime import that is
|
|
not a declared dependency.
|
|
- IIS returned 500.52 before the application was ever launched. `web.config`
|
|
declared `<allowedServerVariables>` for the X-Forwarded-For rule, and that
|
|
section ships `overrideModeDefault="Deny"`, so the whole file was rejected.
|
|
The installer now permits the single variable at server level instead of
|
|
unlocking the section for every site on the machine, and repairs a
|
|
`web.config` an earlier build had made unusable.
|
|
- The config unlock ran before the application it unlocks existed, so the scoped
|
|
form could never succeed on a first install and every install silently fell
|
|
back to granting handler delegation server-wide.
|
|
- The stage 5 smoke test discarded the status code and error page it had already
|
|
received, reporting "site did not return 200" for a fault IIS had named. It
|
|
now records both, plus the tail of the application log.
|
|
- Database dumps were readable by every authenticated user: a directory created
|
|
under ProgramData inherits `Users:RX`, and the owner-only ACL was applied only
|
|
when the installer itself created it.
|
|
- The uninstaller ran the 32-bit PowerShell, which cannot see IIS, so the site,
|
|
pool and application survived a "successful" uninstall pointing at a deleted
|
|
directory.
|
|
- Uninstall matched applications by alias alone and would remove an unrelated
|
|
application of the same name under another site.
|
|
- Wizard input reached a command line unchecked: unvalidated ports, a drive-root
|
|
path that escaped its own quote, and a password written as ANSI but read back
|
|
as UTF-8, which reported a correct non-ASCII password as wrong.
|
|
- Plugin deregistration could never succeed - it omitted `--yes` against a
|
|
command that prompts - while the plugin directory was deleted regardless.
|
|
- Preflight rows were drawn past the bottom of the panel and silently vanished;
|
|
the failures loop had no cap at all.
|
|
|
|
### Changed - installer behaviour
|
|
|
|
- "Is this a re-run of my install?" is answered from a durable install record
|
|
rather than inferred from the state of the machine. Nothing on a server says
|
|
who created its database tables, so a retry after a failed first install was
|
|
taken for an upgrade of a working system: it demanded a mandatory backup of a
|
|
database its own failed attempt had written, then refused to prune tables it
|
|
had created minutes earlier. During unfinished first provisioning the backup
|
|
is advisory and prune may force; on an established install both are unchanged.
|
|
- The installer offers every bundled plugin, so one build serves any site
|
|
instead of one build per plugin profile.
|
|
|
|
### Added - operator documentation and diagnostics
|
|
|
|
- `docs/UPDATES-WINDOWS.md` - what operators should expect from future updates,
|
|
bug fixes and security releases, including downtime, what is preserved, and
|
|
the effect on other sites sharing the same IIS server.
|
|
- `docs/RELEASING-WINDOWS.md` - how to build and release, and the two known gaps.
|
|
- `deploy/windows/shopdb-diagnose.py` - collects what IIS answers, the config
|
|
lock state, the application logs and the ACLs in one pass, scrubbing secrets
|
|
before writing anything.
|
|
|
|
### Added - the installer itself
|
|
|
|
- Air-gapped Windows installer (`deploy/windows/installer/`). One `.exe` per
|
|
site, built from that site's plugin profile, containing Python, the wheels,
|
|
the SPA, the IIS modules and optionally MySQL. Operator docs:
|
|
`docs/INSTALL-WINDOWS.md` and `docs/OPERATE-WINDOWS.md`, both shipped onto the
|
|
server. `docs/INSTALL-WINDOWS-IIS.md` and `docs/DEPLOY-WINDOWS-IIS.md` are now
|
|
reference-only, for hand-built servers.
|
|
- `bundle-lock.json`: an exact sha256 + size record of the installer's
|
|
third-party payload (wheels, Python installer, IIS MSIs). Verified as set
|
|
equality by both builders and again on the server before anything runs; there
|
|
is no install-time override. Regenerate with `refresh-bundle-lock.ps1`.
|
|
- CycloneDX SBOM (`sbom.cdx.json`) generated on every build from
|
|
`requirements.txt` and `package-lock.json`, covering both ecosystems, staged
|
|
into the application tree so an air-gapped server can answer "do we carry this
|
|
component" locally: `shopdb-admin.ps1 verify -Path <name>`.
|
|
- `shopdb-admin.ps1`, the operator console: status, start/stop/restart, logs,
|
|
health check, backup, plugins, verify. `check -Json` emits secret-free
|
|
structured state for pasting into a support ticket or an AI assistant.
|
|
- `build-installer.ps1`, the whole build natively on Windows, so a work PC needs
|
|
no Bash. Shares `scripts/resolve_plugin_closure.py` with `build-site.sh`.
|
|
- URL Rewrite is bundled and the wizard asks where client IPs come from
|
|
(`-ClientIpSource direct|proxy`). Without the rule IIS sends no
|
|
`X-Forwarded-For` at all and every client reads as 127.0.0.1.
|
|
|
|
### Changed
|
|
|
|
- `requirements.txt` and `requirements-dev.txt` are compiled `--universal
|
|
--generate-hashes`. Installs run under `pip --require-hashes`, so a wheel whose
|
|
sha256 is not listed is refused. The dev lockfile is constrained to the
|
|
production pins; the two had drifted.
|
|
- Python 3.14 across the Dockerfile, CI, `web.config` and the docs, which
|
|
previously declared four different versions.
|
|
- The naming/style gate covers Markdown, JSON and YAML, not just code.
|
|
|
|
### Fixed
|
|
|
|
- `plugins/employees` imported `shopdb.core.models` directly, failing the
|
|
contract-surface test on `main` since the dashboard employee-name resolver
|
|
landed.
|
|
- `scripts/build-site.sh` copied all of `deploy/` into its own output directory,
|
|
which recursed when the output was staged inside it - the documented Windows
|
|
build could not complete.
|
|
- Plugin baseline migrations inherited the MySQL server's default charset: the
|
|
utf8mb4 compiler hook lived in `migrations/env.py` and so covered the core
|
|
chain only. It is now `shopdb/utils/mysql_charset.py`, imported by both.
|
|
`flask db-utils preflight` reports the database's default charset.
|
|
|
|
### Added
|
|
|
|
- Configurable site timezone: a `site_timezone` site setting (default
|
|
`America/New_York`, public-readable), editable in Settings > Site >
|
|
Localization. Notification start/end times are entered and displayed in this
|
|
zone, and daily-reset notification expiry is computed in it. A shared
|
|
`frontend/src/utils/datetime.js` (Intl-based, DST-safe) does the conversion.
|
|
|
|
### Changed
|
|
|
|
- Asset detail pages (machines, PCs, printers, network devices, measuring
|
|
tools) now share one canonical card skeleton: Identity -> type-specific ->
|
|
status -> Location & Organization -> domain -> Custom Fields -> Warranty ->
|
|
Relationships -> Notes -> audit footer. The location card reads "Location &
|
|
Organization" on every page. The network device page was rebuilt into the
|
|
family (its "Asset Information" folded into Identity, "Record Info" converted
|
|
to the standard audit footer). Printer Notes moved out of mid-page to
|
|
second-to-last and the printer gained an audit footer. Template reordering
|
|
only; no data or API changes.
|
|
|
|
### Fixed
|
|
|
|
- Notification start/end times were off by the timezone offset (a 2:34 PM entry
|
|
displayed as 6:34 PM). Times are now stored UTC and shown/entered in the site
|
|
timezone; the calendar keys all-day events off the site-local day.
|
|
- Kiosk displays showed a white screen on login: a legacy 32-bit kiosk
|
|
installer's autostart kept relaunching Edge at a now-dead URL. The install's
|
|
HKLM Run value was WOW64-redirected into `SOFTWARE\Wow6432Node\...\Run` and
|
|
survived earlier cleanup. The `gea-shopfloor-display` dispatcher now purges
|
|
the legacy autostarts every enforce cycle across both registry views, all
|
|
user hives, Run/RunOnce/policy-Run, and every Startup folder.
|
|
- List pages keep the current page (and search term) in the URL query, so
|
|
paging to page 9, opening an item, and hitting browser Back returns to page 9
|
|
instead of resetting to page 1. Applies to all 18 list views via a shared
|
|
useListQuery composable; page 1 with no search stays a bare path.
|
|
- PC detail Installed Applications no longer 500s and silently disappears on
|
|
real PCs (ComputerInstalledApp had no to_dict); the section renders app
|
|
name, version, and description again.
|
|
- Employee detail skips its USB panels when the usb plugin is disabled (no
|
|
more 404 console noise).
|
|
- Shopfloor kiosk header text is readable (light on the dark navy header).
|
|
|
|
|
|
### Added (continued)
|
|
|
|
- Collector-driven PC -> printer relationships. The computers collector schema
|
|
gained optional `defaultprinter` (string) and `printers` (array of strings)
|
|
fields carrying Win32_Printer identifiers. On ingest each identifier resolves
|
|
to a printer asset (by windows name / share / hostname / asset number-name or
|
|
a communications IP) and the PC is linked to it: the default via a
|
|
`defaultprinter` relationship, the rest via `connectedto`. The links render in
|
|
the shared Relationships card on both the PC and printer detail pages. The
|
|
sync is idempotent and archives collector-created links to printers no longer
|
|
reported (tagged `assetrelationships.label = 'collector:printers'`, so
|
|
manually-created links are never touched); unresolved identifiers become
|
|
response warnings, never failures. The collector response carries
|
|
`printerlinkcount` and a `printerlinks` list. See docs/COLLECTOR-INTEGRATION.md.
|
|
- Searchable custom fields. Each custom-field definition gained a `searchable`
|
|
flag (Settings > Custom Fields). When on, that field's stored values are
|
|
matched by global search and a hit routes to the owning asset's detail page.
|
|
The asset's `search_<type>_enabled` domain toggle still applies, and matches
|
|
dedupe against built-in-field asset hits so an asset appears once. Inactive or
|
|
non-searchable fields are never matched.
|
|
- Single-label sheet-position printing. The single asset-label page
|
|
(`/print/asset-label/:assettype/:id`) gained an Output control that toggles
|
|
between the standalone label (unchanged default) and placing that one label at
|
|
a chosen cell (1-6, via a 2x3 grid picker) of a ULINE 6-up sheet, leaving the
|
|
other five cells blank. This prints a single label onto the correct physical
|
|
spot of a partially-used sheet instead of wasting a fresh sheet, mirroring the
|
|
legacy shopdb behavior and complementing the batch page's start-cell offset.
|
|
The ULINE 6-up cell layout and dimensions are replicated from
|
|
`AssetLabelBatch.vue` (left untouched); encode resolution stays shared via
|
|
`assetLabel.js`.
|
|
|
|
## [0.7.0] - 2026-07-12
|
|
|
|
### Added
|
|
|
|
- Email sending. A stdlib-only mail service (`shopdb/utils/mailer.py`;
|
|
`smtplib`/`ssl`/`email`) reads the existing `email` SMTP settings
|
|
settings-first with an `SMTP_*` env fallback, sends multipart HTML+text, and
|
|
is a graceful no-op (logs a warning, returns False) when email is disabled or
|
|
the host is unset. The SMTP password is never logged. Three flows use it:
|
|
(1) New-user welcome + forced first-login password change. Admin-created users
|
|
(POST `/api/users`) are flagged `mustchangepassword` (new `users` column,
|
|
migration `7d23_user_mustchangepassword`, default false) and sent a
|
|
best-effort welcome email with the facility name, username, temporary
|
|
password, and sign-in link; the account is created even if mail fails
|
|
(response carries a `warning`). Login returns `mustchangepassword`; the
|
|
frontend forces the user through a new `/change-password` view (POST
|
|
`/api/auth/change-password`, jwt-guarded) before the app, and changing the
|
|
password clears the flag and resets lockout counters. A self-service "Change
|
|
password" entry is also available from the user menu.
|
|
(2) Test email. POST `/api/settings/test-email` (settings.edit) sends a probe
|
|
and surfaces any SMTP error with the password scrubbed; wires up the Email /
|
|
SMTP settings page "Send Test Email" button.
|
|
(3) On-demand alert/report delivery. POST `/api/reports/email`
|
|
(reports.export) mails `{subject, columns, rows}` as an HTML table to a
|
|
supplied recipient or the site `alert_recipients`; an "Email report" button on
|
|
the Warranty and Toner report pages posts the loaded rows. There is no
|
|
scheduler: automation is an external cron hitting the endpoint with a scoped
|
|
API token (PAT). Documented in `docs/CONFIG.md`.
|
|
- Shared asset label/code generator: a single `/print/asset-label/<assettype>/<id>`
|
|
page (public, like the other `/print/*` routes) that any asset detail page
|
|
opens via a "Print Label" button (machines, computers, printers, network
|
|
devices, measuring tools). A no-print controls panel toggles the layout
|
|
(`card` badge vs `plain` code-only), the code type (QR vs CODE128 barcode),
|
|
and what the code encodes: the asset page link, asset number, serial number,
|
|
a per-type custom target template, or - for measuring tools by default - the
|
|
tool's inspection location code so every tool at one operation shares one
|
|
code (e.g. `0615`). QR codes reuse the shared logo-overlay renderer. New
|
|
`printing` settings seed and surface on the Printing & Labels settings page:
|
|
`qr_target_machine`, `qr_target_computer`, `qr_target_network_device`,
|
|
`qr_target_measuring_tool`; `label_default_style` (default `card`) and
|
|
`label_default_codetype` (default `qr`); and a per-asset-type default for what
|
|
the code encodes, `label_default_encodes_<type>` (machines default to their
|
|
machine number, measuring tools to their inspection location code, the rest to
|
|
a page link), all overridable on the label page itself. When the chosen field
|
|
has no value (e.g. serial number on an asset with none), the label states so
|
|
instead of rendering an empty code. Asset payloads now carry a derived
|
|
`locationcode` (leading token of the resolved own/inherited location name).
|
|
- Batch label sheets: a "Print Labels" button on each asset list page opens
|
|
`/print/asset-label-batch/<assettype>`, a multi-select sheet that lays the
|
|
chosen assets onto ULINE label pages (6-up 3 in x 3 in, or a dense 72-up
|
|
mini-label format), with a start-cell offset to reuse partial sheets. Shares
|
|
the same code-type and encode settings/defaults as the single label, so a
|
|
batch of measuring tools encodes each tool's inspection location code just
|
|
like the single label does. Restores the ULINE batch printing the legacy
|
|
shopdb had, generalized across all asset types.
|
|
- Support-team contact UX: the settings Support Teams page now manages each
|
|
team's contacts in a per-team "Contacts (N)" modal (name, SSO, active, plus
|
|
Add/Edit/Delete) instead of an inline row expander, and the application
|
|
detail Support card renders Email (`mailto:`) and Microsoft Teams chat
|
|
(`teams.microsoft.com/l/chat`) action buttons for every contact that has an
|
|
SSO. Both link targets derive as `sso@<domain>` from a new `site` setting
|
|
`contact_email_domain` (default `geaerospace.com`; blank hides the buttons),
|
|
surfaced in Site & Facility settings under Naming & Patterns and read on the
|
|
frontend via `getContactEmailDomain()`.
|
|
- 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 migration script
|
|
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
|
|
|
|
- Audit log: hovering a user's SSO now shows their full name (best-effort,
|
|
resolved from the employee directory in either mode).
|
|
- Refreshed the internal status docs to match the code (project active
|
|
state, CONTRACT-STABILITY.md and README plugin list at contract 0.10.0),
|
|
corrected the get_asset_panels endpoint path in the hook docstring, and
|
|
removed leftover debug console.log lines.
|
|
- 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, status docs, 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.8.1...HEAD
|
|
[0.8.1]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.8.0...v0.8.1
|
|
[0.8.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.7.0...v0.8.0
|
|
[0.7.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.6.0...v0.7.0
|
|
[0.6.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.5.0...v0.6.0
|
|
[0.5.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/releases/tag/v0.5.0
|