The map was one picture of one floor. A second floor was added, the blueprint changed size, and machines moved, so a position now records WHICH DRAWING its coordinates belong to. Buildings and levels (ADR-017). Each level owns its blueprint per theme and its own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the site. A position whose level is unknown renders "level unknown" and is never drawn on the default level, because a marker on the wrong floor plan looks entirely correct while pointing at the wrong place. Repositioning in bulk: filter by unplaced, needs-review or level, search, place, confirm. Landmark recalibration solves the transform PER AXIS from landmark pairs and never from image dimensions - the canvas grew taller without rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong everywhere. It defaults to a dry run, reports what would land off the drawing, snapshots before applying, and clears mapverifiedat because a transform is a guess awaiting review. Snapshots restore, including the level and the review state, and a restore snapshots first so an undo is undoable. Search: gaugelabreference was matched only for measuring tools and maintenancereference was matched nowhere at all, for any asset type, while Settings happily offers both identifiers on machines and PCs. A tag an operator is told to record has to be findable or it is a write-only field. USB devices and printed items were unreachable from search entirely - neither is an asset, so the generic asset search could not see them and no searcher existed; they now match on serial, asset tag, label, bin code and gage-lab tag, honouring isactive, with Settings toggles and result labels to match. The retired-application rule was half a rule: GET /api/knowledgebase hid articles whose topic application is retired while global search still returned them and printed the retired application as the subject. A filter is only real if every path that reaches the row applies it. Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location gained levelid, and resolve_asset_position returns the levelid belonging to whichever source supplied the coordinates. The five plugins that write a map position are re-pinned. The install-list text format gained levelid as a NINTH field, appended, because the shipped Pascal installer reads fields 0-7 by index. That installer still compiles in one drawing's dimensions and bundles one blueprint, so its map is accurate for the default level only; /api/maplevels is deliberately unauthenticated so it can read both at runtime once rebuilt. Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps. Migration 7d33 converts an existing single-map site into one building and one default level carrying the old map_* settings, then assigns every placed asset and location to it. Nothing moves on screen. Old settings rows are kept so a rollback still finds them. Verified end to end on MySQL 5.6 from a production-shaped database.
1087 lines
67 KiB
Markdown
1087 lines
67 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]
|
|
|
|
The floor map became a set of drawings instead of one picture. A site can hold
|
|
more than one building, a building more than one level, and every map position
|
|
now records which drawing its coordinates belong to. Driven by a real move: a
|
|
second floor was added, a new blueprint changed size, and machines relocated.
|
|
|
|
### Added
|
|
|
|
- Buildings and levels (ADR-017). Each level owns its own blueprint per theme
|
|
and its own native pixel size; `assets.mapx`/`mapy` are pixels of the level
|
|
named by the new `assets.levelid`, not of the site.
|
|
- A bulk repositioning tool on the map editor: filter by unplaced, by
|
|
needs-review, or by level, search, then place markers and confirm them. New
|
|
`POST /api/mappositions/positions` and `/verify`.
|
|
- Landmark recalibration (`POST /api/mappositions/transform`). Name two or more
|
|
points that appear on both the old and new drawing and every marker on the
|
|
level moves onto the new one. Defaults to a dry run that reports each old and
|
|
new position and anything that would land off the drawing. The transform is
|
|
solved PER AXIS from the landmarks, never derived from image dimensions: a
|
|
taller drawing that gained a level below did not rescale, and a
|
|
dimension-derived scale would stretch Y by 1.57 and be wrong everywhere.
|
|
- Position snapshots with restore, taken before any bulk change including a
|
|
restore, so an undo is itself undoable.
|
|
- A buildings and levels admin under Settings: name and order levels, see each
|
|
level's id and marker count, upload a blueprint per theme, choose the default.
|
|
- `assets.mapverifiedat`, the record of when a position was last confirmed
|
|
against the current drawing. A bulk transform clears it, because a transform
|
|
is a starting guess and nothing in the coordinates says which markers moved.
|
|
|
|
### Changed
|
|
|
|
- Plugin contract to 0.20.0 (additive; see CONTRACT-STABILITY.md). A plugin that
|
|
writes a map position must now write its level.
|
|
- A position whose level is unknown renders as "level unknown" and is NOT drawn
|
|
on the default level. Drawing it there would look entirely correct while
|
|
pointing at the wrong part of the building.
|
|
- `GET /api/printers/install-list?format=text` gained `levelid` as a NINTH
|
|
field, appended. Fields 0-7 are unchanged because the shipped Pascal installer
|
|
reads them by index.
|
|
- Knowledge base: an article whose topic is a retired (`isactive = 0`)
|
|
application no longer appears in listings, searches or the counts, and the
|
|
topic picker offers every active application rather than only installable
|
|
ones.
|
|
|
|
### Migration
|
|
|
|
- `7d33_buildings_and_levels` converts an existing single-map site: it creates
|
|
one building and one default level carrying the blueprint paths and dimensions
|
|
from the old `map_*` settings, then assigns every already-placed asset and
|
|
location to it. Nothing moves on screen. The old settings rows are left in
|
|
place so a rollback still finds them. Verified end to end on MySQL 5.6 from a
|
|
production-shaped database.
|
|
|
|
### Known gaps
|
|
|
|
- The Pascal printer-installer map still compiles in one drawing's dimensions and
|
|
bundles one blueprint, so it is accurate for the default level only. See
|
|
section 6 of PRINTER-INSTALLER.md.
|
|
- The setup wizard still asks for a single site-wide blueprint and writes
|
|
settings that no longer drive the map.
|
|
- Map PDF export covers the current level only.
|
|
|
|
## [0.10.0] - 2026-08-17
|
|
|
|
A security release. The Windows installer left the directories it creates
|
|
writable by every logged-on user, which made two of the files it puts there a
|
|
local privilege escalation; that is fixed and verified on Windows. Alongside it,
|
|
the work that makes this product genuinely adoptable by a second site: one
|
|
site's hostnames, share paths and networks are out of the product code and out
|
|
of the published documentation, and the rule that keeps them out now fails the
|
|
build rather than printing a warning nobody read.
|
|
|
|
The plugin contract moved to 0.19.0, which is a BREAKING change - see the
|
|
Changed section and CONTRACT-STABILITY.md before upgrading a plugin you wrote.
|
|
|
|
### Added
|
|
|
|
- Applications accept an uploaded image and an uploaded installer, rather than
|
|
only a path typed into a text box. The path field kept working only while
|
|
someone maintained a share by hand, and a link to a file nobody could still
|
|
find was indistinguishable from a link to one that worked. The installer
|
|
download is gated on `applications.view` because it is licensed vendor
|
|
software; the image is public, since tiles render before login.
|
|
- Warranty records accept an uploaded proof document - the invoice or
|
|
certificate that actually settles a claim. The documentation path stays as it
|
|
was: it points at a vendor portal or a share, which an upload cannot replace.
|
|
Downloads require `warranty.view`, since a proof carries pricing and a service
|
|
tag, and it comes back under the vendor's own filename so it is recognisable
|
|
in a support ticket.
|
|
- Toner forecast report: days until each cartridge runs out, soonest first,
|
|
from Zabbix level history. Ordering by days left rather than by level is the
|
|
point - a cartridge at 60% draining fast needs ordering before one sitting at
|
|
8% that has not moved in a month. It also counts how many cartridges each
|
|
printer has been through in the window, which is the number a purchasing
|
|
conversation asks for. Printers with no honest estimate are listed
|
|
separately with the reason (too little history, gauge has not moved, replaced
|
|
recently) instead of being sorted in as zero or as safe, and an unreachable
|
|
Zabbix says so rather than rendering an empty table that reads as "nothing is
|
|
due".
|
|
- Fiscal week under the site name in the sidebar and on the shop-floor board,
|
|
as the classic ASP site showed it. People quote a week number across both
|
|
sites, so the arithmetic is a port of the old one (ISO 8601: week 1 contains
|
|
4 January, the week's Thursday decides the year) rather than a fresh
|
|
interpretation.
|
|
|
|
- Backup history distinguishes CAPTURED from LAST VERIFIED. Captured is when a
|
|
configuration was written; verified is when a PC last posted it and the hash
|
|
still matched. They differ by design, because an unchanged config writes no
|
|
new revision - so a machine checked daily for a year shows one revision from
|
|
last spring, and previously read as abandoned. Shown on the history page and
|
|
on the asset panel; only the current revision carries it, since an older one
|
|
is not what the PC holds now.
|
|
- A backup now records that it was CHECKED, not only that it changed. Dedup
|
|
means an unchanged configuration writes no revision, so the stored timestamps
|
|
moved only on a change: a machine whose settings had been stable for six
|
|
months was indistinguishable from a machine whose backup died six months ago.
|
|
The only evidence a backup still ran was a line in a log file on the PC.
|
|
`lastseenat` is touched on every matching post, including the no-op, and is
|
|
backfilled from the timestamps that already exist.
|
|
- The low-supplies report lists only the cartridges that need replacing. A
|
|
printer with one empty black and three full colour cartridges was listing all
|
|
four, so the reader had to hunt for the problem inside the row. The report
|
|
exists to answer "what needs replacing".
|
|
- Dashboard cards for enforcement failures, machine numbers claimed by two PCs,
|
|
PCs not reporting, backups that have stopped, printer supplies, expiring
|
|
warranties, machines out of service, and the notifications currently showing
|
|
on the floor. The dashboard now renders cards plugins declare, which it never
|
|
did before - five plugins had been declaring widgets into a void, pointing at
|
|
components nobody wrote. Cards are ordered by severity, hide themselves when
|
|
there is nothing to report, gate on a permission, and each fetches
|
|
independently so one broken endpoint cannot blank the board.
|
|
- Tech Tools: a section for the small conversions and code generation that
|
|
otherwise happen in a browser tab on somebody's phone. Starts with barcode and
|
|
QR codes laid out for the label stock actually in the printers.
|
|
- Kiosks report what they are. The dispatcher already read
|
|
`C:\Enrollment\display-type.txt` to pick its target; now it tells ShopDB, so
|
|
the Asset column on an enforcement report shows a reported value instead of
|
|
one inferred from a mapping table nobody filled in.
|
|
- Enforcement reports carry what the PC is, where it is, and whether it is
|
|
backed up: the host links to its own page, the Asset column names the machine
|
|
or instrument the PC controls, a map pin shows the location on hover, and the
|
|
backup column judges the chain rather than printing its last date.
|
|
- `flask relationships audit`, read-only, listing links that cannot both be
|
|
true, plus an inverse guard that refuses to create one.
|
|
- An employees endpoint that says WHY a name did not resolve, rather than
|
|
returning nothing and leaving the caller to guess.
|
|
- The display client module updates itself through the manifest, so a client
|
|
change no longer means hands on every kiosk.
|
|
- `docs/START-HERE.md` routes a reader by what they came to do, and is now the
|
|
wiki's landing page - which previously recommended the one install document
|
|
the docs themselves forbid for a new site. `docs/FLEET-ARCHITECTURE.md` is the
|
|
page nothing else could assume: how the server, GE-Enforce, the asset
|
|
reporter, the backup collectors and EventSaver relate, where credentials live
|
|
at both ends, and which of the five to open for a given symptom.
|
|
- `docs/ADOPTING-AT-ANOTHER-SITE.md`: how another site repoints the asset
|
|
reporter and the EventSaver screensaver at its own ShopDB, with worked
|
|
examples for Intune (including Machine Configuration/DSC), a GE-Enforce
|
|
manifest entry, and manual installation. Both tools were already built to be
|
|
repointed - the URL, the key and the targeting are inputs - but nothing said
|
|
so, and the two things that catch people out are written down: EventSaver's
|
|
compiled-in fallback path is not neutral, and a config enforced by hash
|
|
reverts a hand edit by design.
|
|
- `docs/PROJECT-MAP.md`, generated by `scripts/gen_project_map.py`: versions,
|
|
the plugin inventory, every Alembic chain head, the ADR index and the size of
|
|
the codebase, derived from the code. The hand-written equivalents had drifted
|
|
by two contract releases and 400 tests.
|
|
|
|
### Changed
|
|
|
|
- Plugin contract **0.19.0**: `get_dashboard_widgets` returns data and shape,
|
|
not a component name. The old shape named a Vue component per widget, which
|
|
cannot survive a lean build (ADR-013) and had five plugins pointing at
|
|
components nobody wrote. The change itself shipped earlier without a bump and
|
|
with `BasePlugin` and PLUGIN-HOOKS.md still teaching the dead shape; this
|
|
records it and corrects both.
|
|
- Plugin contract **0.17.0**: `DashboardDefault` is part of the `shopdb.api`
|
|
surface, so a plugin can resolve a display without reaching into core.
|
|
- Plugin contract **0.18.0**: one name per display role, and it is the kiosk's
|
|
own (`Dashboard`, `Lobby`, `3DPrintRoom`). Server and dispatcher had three
|
|
vocabularies for the same three kiosks, and a display could report a role core
|
|
could not store. `normalize_display_role` resolves case and the retired
|
|
spelling; an unknown value is kept verbatim so a typo stays visible.
|
|
Contract versions are their own series (ADR-002) and belong in this file;
|
|
0.17.0 and 0.18.0 shipped without an entry, which is what this note corrects.
|
|
- Publishing a GE-Enforce manifest is refused when PCs reporting for that scope
|
|
run an older client library than the manifest needs, naming the hosts. A minor
|
|
version bump that NARROWS behaviour is not backward compatible: an old library
|
|
reads a gated entry as ungated and installs every version it cannot detect.
|
|
- Shadow mode runs inside the enforce cycle instead of on its own schedule. The
|
|
share is mounted only for the length of a cycle, so a task on its own clock
|
|
woke to a missing drive and reported 0 installed / 0 skipped / 0 failed, which
|
|
is indistinguishable from a healthy no-op.
|
|
- A GE-Enforce report is judged stale on both clocks, the server's and the
|
|
client's asserted check-in, so a PC cannot mask real silence with a future
|
|
timestamp. A PC that has gone quiet stops reading as healthy.
|
|
|
|
### Fixed
|
|
|
|
- Documentation a reader could not follow. 98 curl examples single-quoted
|
|
`$TOKEN`, so the shell never expanded it and the server answered 422; a JSON
|
|
body carried a `//` comment, which made the server report a missing hostname
|
|
about a body that has one; the IIS install omitted `flask plugin upgrade-all`,
|
|
which is the 1054 error a deploy hits days later; a pilot loop exited 1 on
|
|
every iteration; and the external-plugin guide pinned a contract version six
|
|
releases stale, so a plugin built by following it is refused at startup. Nine
|
|
documents now point at the generated project map rather than typing a version,
|
|
and a test enforces that. The API inventory gained the 22 routes that were
|
|
serving traffic undocumented, with a parity test against the live URL map so
|
|
the next one cannot slip in silently.
|
|
|
|
- **Every plugin migration chain re-ran its head on each MySQL deploy.** Core's
|
|
Alembic env got a `connection.commit()`; the per-plugin template did not, so
|
|
the last migration of every run lost its version stamp while its DDL survived.
|
|
Invisible for as long as every head happened to be idempotent - and two were
|
|
not. The backups head cleared a column for every row, and the geenforce head
|
|
added a column unconditionally and failed on the second attempt. Both are now
|
|
bounded and guarded, and the template commits.
|
|
- A part-marker PC reported 200 on the cycle that created its marker and 500 on
|
|
every cycle after. Both of the links it makes were looked up by label, so a row
|
|
made by hand or by the legacy import was invisible and the insert violated the
|
|
uniqueness of (source, target, type); and the marker's asset number is derived
|
|
from the PC, so it could already be taken.
|
|
- A CMM reported the same way: the instrument IS the reported bay, so adoption
|
|
was relabelling the row the machine sync owns and finding it by that label.
|
|
- A reported identifier is matched exactly rather than with `ilike`, so `_` and
|
|
`%` in a file on a shopfloor PC are no longer wildcards. `MT-600_` had been
|
|
adopting MT-6001, and a bare `%` adopted whatever active asset came first.
|
|
- Measuring tools are adopted before they are minted. Minting derived the asset
|
|
number from the HOSTNAME, so a permanent instrument took the identity of
|
|
whichever PC drove it that week, and the collector could not see a tool it had
|
|
not created - which left legacy instruments shadowed by minted twins.
|
|
- An unanchored rsync exclude in the export was matching at any depth, so a
|
|
whole plugin was silently dropped from the published payload. Root excludes
|
|
are anchored and the export now aborts if a plugin manifest fails to survive.
|
|
- Backup revisions are ordered in Python, not with `ORDER BY ... NULLS LAST`,
|
|
which SQLite accepts and MySQL rejects.
|
|
|
|
### Security
|
|
|
|
- The Windows installer breaks ACL inheritance on the roots it creates. A
|
|
directory made directly under `C:\` inherits an inherit-only Modify grant for
|
|
Authenticated Users, so `C:\shopdb-flask`, `C:\Python314` and `C:\MySQL84`
|
|
were writable by any logged-on user. Two files inside them make that a local
|
|
privilege escalation: `shopdb-admin.ps1`, which self-elevates, and
|
|
`instance/config.py`, which the app executes unconditionally. The MySQL data
|
|
directory is locked down as well, since it holds the password hashes. A Python
|
|
installation this installer did not create is reported rather than seized.
|
|
- Site-specific values no longer ship in product code (ADR-015). The backups
|
|
share root defaults to blank instead of one site's file server, the shadow
|
|
client no longer overwrites a site's own ShopDB URL in the registry on every
|
|
cycle, the kiosk dispatcher takes its URL from the `site_base_url` setting and
|
|
refuses rather than guessing, and one site's hostname, FQDN and internal
|
|
subnets are out of the documentation, the generated API specs and the UI
|
|
placeholders. The ADR-015 scanner now covers PowerShell, the installer, the
|
|
seeds and generated JSON, case-insensitively, and FAILS the build; deliberate
|
|
organisation-wide defaults are declared with an `ADR-015-OK` marker. Both
|
|
publication gates carry the site patterns.
|
|
|
|
## [0.9.0] - 2026-08-11
|
|
|
|
Driven by a fleet that had been failing quietly. A bay had been returning 500
|
|
to every collector report since it was imaged, backups were accumulating
|
|
duplicate revisions on any machine number more than one PC reported against,
|
|
and a setting that looked configurable had never been readable by the script
|
|
that consumed it. The theme of the release is making those failures visible:
|
|
each fix ships with the check, report or document that would have surfaced it.
|
|
|
|
|
|
### Changed
|
|
|
|
- The toner forecast is read as an order, not as a table. It opens with what to
|
|
buy - grouped by part number, with a quantity - because two cartridges of the
|
|
same part in different printers is a quantity of two, and that was a number
|
|
the reader had to work out by hand. Below it, cartridges sit in urgency bands
|
|
(empty / two weeks / thirty days / later) rather than in one long sortable
|
|
list, since the question being asked is which pile a thing is in. A row is
|
|
now a cartridge rather than a printer, carries its own part number and a
|
|
level bar, and everything past "empty" starts collapsed. Cartridges with no
|
|
part mapped are counted on one line instead of a line each: they still have
|
|
to be ordered, but not from this page - the job is to map them.
|
|
|
|
### Fixed
|
|
|
|
- The toner forecast was reading the wrong end of the window. Zabbix applies a
|
|
query's `limit` to the whole answer rather than to each item, and combined
|
|
with an ascending sort that kept the OLDEST rows: a four-cartridge printer
|
|
polled every five minutes writes over 100k readings in 90 days, so a "90 day"
|
|
forecast was fitted to the first few days of the window and nothing since.
|
|
That is where "4 days left" beside a cartridge at 20% came from - the rate
|
|
was real, it just described a cartridge thrown away in May. Long windows now
|
|
read hourly trends, which is the table meant for this and is a tenth of the
|
|
rows; short ones read raw history, newest first, with a budget scaled per
|
|
item.
|
|
- The forecast now counts down from the level it displays. It showed the live
|
|
reading but computed days-left from the last stored one, so the two could
|
|
disagree by a whole cartridge. A live level far above the stored run is
|
|
treated as a swap that happened since the last reading rather than as a
|
|
collapse in the burn rate.
|
|
- A cartridge at or below 5% reads as empty rather than as a slow drain. At 1%
|
|
losing a tenth of a point a day the arithmetic said ten days; the printer is
|
|
out of toner, and it is the first thing that should be ordered.
|
|
- Days-left in the forecast is per cartridge again. It was rendered in a cell
|
|
spanning the printer's rows, which put the printer's soonest figure beside
|
|
every supply it had - a cartridge at 20% displaying "4 days" that belonged to
|
|
the black beside it, and a cartridge at 1% displaying weeks that belonged to
|
|
nothing on that row at all.
|
|
- Supplies typed as floats were dropped from the forecast on any printer that
|
|
also had an integer-typed one. The two live in different Zabbix history
|
|
tables and the fetch stopped at whichever answered first, so half a printer's
|
|
cartridges silently had no history at all.
|
|
- 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.10.0...HEAD
|
|
[0.10.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.9.0...v0.10.0
|
|
[0.9.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.8.1...v0.9.0
|
|
[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
|