A PC that drives a device which is its own asset had been implemented twice. METROLOGY_TOOL_MAP covered CMM, Keyence, Genspect and wax-trace, minting a measuring_tool. A separate path keyed on one hardcoded pc-type minted a Part Marker machine and filed it under its operation. Both create a device, link the PC with controls, and archive that link when the PC is re-imaged: one mechanism with different nouns, written out twice because the second case arrived later. That is the same trap as the site literals in ADR-015 - a pattern implemented per instance rather than declared - and it has a known next occurrence. Part markers already share operation numbers, and any site with two marking lasers or two wax-trace units on one number needs identical treatment. One SUBORDINATE_DEVICE_MAP now declares asset type, type name, naming suffix, whether the device files partof the operation, and the relationship label. The labels are unchanged per case on purpose: those values are in the production database and only rows carrying them are archived by a collector push. A site overrides or adds an entry through subordinatedevice_<pctype> settings, per ADR-015, so the next case needs no code. A malformed override falls back to the default rather than failing the push, because a bad setting must not stop a bay reporting its inventory. metrology_tool_for stays as a shim over the same map: filters.py and the older tests read it, and unifying must not change what it returns. A test pins that. Also adds flask relationships check-shared-machines, which finds the next 0615 rather than waiting for someone to notice duplicate backups. Several devices legitimately sharing a number and two PCs mis-numbered at imaging look the same from outside; the difference is whether child assets exist, so that is what it reports. Read-only.
778 lines
47 KiB
Markdown
778 lines
47 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.
|
|
- The backup collection interval could not actually be changed. The collecting
|
|
script reads `backups_intervalhours` from the public settings endpoint before
|
|
it holds any credential, but the plugin did not declare the key as public, so
|
|
the endpoint never returned it and the script fell back to its built-in 24
|
|
hours - silently. The setting looked configurable in the UI and was not.
|
|
- 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.
|
|
- A PC that drives a subordinate device is now declared, not coded. Two cases
|
|
had arrived separately and been written twice: a metrology PC drives a CMM or
|
|
Keyence unit that becomes a measuring-tool asset, and a part-marker PC drives
|
|
a marker that becomes a machine asset filed under its operation. Both mint a
|
|
device, link the PC with `controls` and archive that link on a re-image - one
|
|
mechanism with different nouns. They are now one map, and a site can add or
|
|
retarget an entry through a `subordinatedevice_<pctype>` setting, so the next
|
|
case - two marking lasers or two wax-trace units on one machine number - is
|
|
configuration rather than a code change. A malformed override falls back to
|
|
the default instead of failing the collector push.
|
|
- `flask relationships check-shared-machines` reports machine numbers claimed
|
|
by more than one PC, and says which are modelled and which are faults. Several
|
|
devices genuinely sharing a number and two PCs mis-numbered at imaging look
|
|
identical from the outside; the difference is whether anything is filed under
|
|
the operation, which is what the check reports. Read-only.
|
|
- A part marker is its own asset. Several Telesis markers can serve one
|
|
operation number, so treating the operation as the marker collapsed separate
|
|
devices into one record: their configurations, which differ by COM port,
|
|
overwrote each other in the backup history, and no question about an
|
|
individual marker - how many there are, which port one is on, which one
|
|
failed - could be asked at all. There is one marker per PC, so the collector
|
|
now mints a Part Marker machine asset for a marker PC the same way it already
|
|
mints a CMM or a Keyence unit for a metrology PC: the PC `controls` the
|
|
marker, and the marker is `partof` the operation it serves. Because `controls`
|
|
propagates through `partof`, control of the operation still follows from
|
|
controlling its marker, and two markers on one operation no longer contest a
|
|
link only one of them can hold. Backups from a marker PC file against the
|
|
marker. Moving a marker to another operation archives the old membership
|
|
rather than deleting it.
|
|
- 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
|