1954 lines
121 KiB
Markdown
1954 lines
121 KiB
Markdown
# Changelog
|
|
|
|
All notable changes to shopdb-flask are recorded here.
|
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
The product version (`__version__`) and the plugin-contract version
|
|
(`__contract_version__`) are distinct series with independent bump rules; see
|
|
ADR-007 and ADR-002.
|
|
|
|
## [Unreleased]
|
|
|
|
### Added
|
|
|
|
- **A link that opens the code generator with the work already in it.** Copy
|
|
share link hands somebody a URL that reopens the page with the content, the
|
|
label, the copy count and every layout value the sender changed - so "here is
|
|
the label, print it" is a link rather than a screenshot and a list of
|
|
settings to retype. Everything rides in the fragment, which browsers never
|
|
transmit: it does not reach Flask, the IIS log or the proxy in between. Only
|
|
the values that differ from a first-time visitor's defaults travel, so the
|
|
link stays short enough to paste into a chat box, and an unknown key is
|
|
ignored rather than refused, so a link made by a newer version still opens.
|
|
The picture never travels - a data URL runs to megabytes and no chat client
|
|
would carry it. This is convenience, not secrecy: anyone holding the link can
|
|
read the content, which is what Secure Send is for.
|
|
|
|
- **Reading anything now needs a permission.** A sweep of the URL map found
|
|
**165 GET routes that answered anybody with no token at all**: the whole
|
|
asset inventory, every machine, PC, printer and network device, the USB
|
|
checkout history and who holds which drive, the staff directory, the core
|
|
reports. Guest access had been argued about one feature-flag at a time while
|
|
the front door stood open. Every one of them is now behind the read
|
|
permission it belongs to - `assets.view`, `machines.view`, `printers.view`
|
|
and so on - which also means the guest role can open exactly the ones a site
|
|
chooses and nothing else.
|
|
|
|
Thirty-three endpoints stay public, each named with its reason in
|
|
`tests/test_core/test_public_read_surface.py`: the app shell and its
|
|
bootstrap, the settings allowlist, images fetched by `<img src>` (which
|
|
carries no Authorization header), what a display kiosk renders before anybody
|
|
signs in, the parts and USB kiosks, the client scripts that run as SYSTEM
|
|
before a user session exists, and a secure-send link whose token is itself
|
|
the authorisation. That file is a standing gate: a new public read fails CI
|
|
until somebody adds it to the list, in front of a reviewer. Four more reads
|
|
need a login but no permission because the handler decides from who you are -
|
|
your own user record, your own API tokens, the role catalogue - and the same
|
|
test proves each still refuses an anonymous caller.
|
|
|
|
The employees plugin gained `employees.view`: the directory is names, SSOs,
|
|
photographs and reporting lines, and it had no permission at all.
|
|
|
|
- **A guide's sections can be ticked off while you read it.** Only checklist
|
|
items were tickable, which answered "what have I done" but not the question a
|
|
long procedure actually raises - where was I. Somebody looks away to do the
|
|
thing the guide asked for and looks back at a wall of text. Every step and
|
|
every heading now carries a checkbox in the reader; ticking one strikes it
|
|
through and fades it rather than hiding it, because scanning back up the page
|
|
has to still show what was done. Headings count as well as steps: a guide
|
|
converted from an HTML article often has fifty headings and no steps at all.
|
|
Progress is per browser and per version, as it already was for checklists -
|
|
shopfloor PCs share a login, so one technician's ticks must not greet the
|
|
next person on that bay.
|
|
|
|
- **A guest is a role now, not a switch per feature.** Whether somebody with no
|
|
account may see something was answered one feature at a time: a
|
|
`securesend_allow_guests` setting, an enable flag per kiosk, and very nearly a
|
|
third for guides - each with its own settings page and its own hand-written
|
|
branch inside a route. It is a permissions question, and this app already
|
|
administers permissions per role, so anyone not signed in now holds the
|
|
seeded `guest` role. It starts with nothing, so an upgrade opens no door; a
|
|
site opens one by ticking a box in the same role grid it uses for everybody
|
|
else, and closes it the same way - taking effect on the next request, not the
|
|
next restart. `require_permission` consults the role when there is no token,
|
|
which is the whole mechanism.
|
|
|
|
What a guest may be granted is bounded: any `.view` or `.read` permission,
|
|
plus writes a plugin has explicitly declared safe to perform without an
|
|
account (`get_guest_grantable_permissions`, contract 0.21.0). Secure Send
|
|
declares its create permission, because sending a password to a vendor is the
|
|
one write the product means to offer somebody who never signs in. Everything
|
|
else is refused by the API and greyed out in the grid rather than warned
|
|
about. A guest write records no creator - the audit trail can say when and
|
|
from where, never who - and that is the reason for the bar.
|
|
|
|
GE-Enforce and Slides gained read permissions (`geenforce.view`,
|
|
`slides.view`) so they can be opened to guests without handing out the manage
|
|
right; their read routes accept either, so nothing an existing manager could
|
|
reach has moved. The navigation endpoint now returns only what the caller can
|
|
actually reach, so a signed-out visitor sees the pages that were opened to
|
|
them rather than a sidebar of links that bounce off a login page.
|
|
|
|
- **USB check-in / check-out audit report.** CMMC removable-media tracking is an
|
|
audit obligation and there was no report for it - answering "who had this
|
|
drive, and when" meant paging through the check-in/out list by hand. The
|
|
window is named rather than a day count: today, last month, six months, a
|
|
year, or all time. Events rather than totals, because that is the question an
|
|
audit actually asks, with a CSV export for the people who want it in a
|
|
spreadsheet. Works against the external `cmmc_usb` log or the self-hosted
|
|
tables; in self-hosted mode one stored loan expands into a take and a return,
|
|
so a returned drive is distinguishable from one still out.
|
|
|
|
- **`flask guides export` / `flask guides import` - a guide can reach another
|
|
site.** Sites are separate databases and the ones that matter are air-gapped
|
|
from wherever a guide gets written, so a procedure written once was stranded
|
|
where it was typed. Export writes one `.guide.json` per guide with its images
|
|
carried inside it - one file, because a bundle that needs its images copied
|
|
alongside it is a bundle that will eventually arrive without them - and
|
|
import reads them back. The slug travels, since that is the address somebody
|
|
pastes into a work order and the same guide should answer to the same name on
|
|
every site; ids, revision history and publication state do not, because those
|
|
belong to the database that issued them. Imports arrive as drafts;
|
|
`--replace` updates a guide already carrying that slug rather than creating a
|
|
second one, which is how a corrected guide is re-shipped.
|
|
|
|
- **`flask guides import-html` - an existing HTML article becomes a guide.**
|
|
A site adopting this app arrives holding procedures already written: styled
|
|
HTML pages that get mailed around and opened off a share. Retyping one into
|
|
the editor is how a good procedure stays on a share forever. The importer
|
|
reads the page and produces blocks - steps, callouts, code, checklists,
|
|
tables, lists - and lifts embedded images into `guideimages` so a 1.6 MB
|
|
document stops carrying its screenshots as base64. Imports land as DRAFTS,
|
|
and `--dry-run` (the default) reports what it would produce without writing.
|
|
Conversion is deliberately lossy: colour, fonts and layout go, because that
|
|
is the whole reason a guide is stored as blocks rather than markup. The
|
|
blocks go through the same validator the editor uses, so an import cannot
|
|
store something the editor could not have produced.
|
|
|
|
### Fixed
|
|
|
|
- **The published site's API documentation page had no spec to show.**
|
|
`shopdb/core/api/docs.py` reads `docs/openapi.json` at runtime to serve
|
|
`/api/docs`, and publication excluded the whole of `docs/` because the prose
|
|
becomes the wiki. So every site installed from the published repo shipped the
|
|
ReDoc page, loaded it, and answered `{"error":"openapi.json not generated"}`.
|
|
The spec now publishes: it is a product file that happens to live under
|
|
`docs/`, not documentation. The prose still goes to the wiki and nowhere else.
|
|
|
|
- **Three gates were reporting green by not running.**
|
|
`test_api_inventory_parity`, `test_openapi_spec` and `test_docs_publishable`
|
|
all skip themselves when the file they read is absent - correct in a
|
|
published checkout, and silently wrong on GitHub, which runs against exactly
|
|
that checkout. An outside branch added nine routes documented nowhere and
|
|
passed CI. `scripts/check-gates-ran.sh` now asserts each gate actually ran,
|
|
splitting the ones that must run everywhere from the ones that need files
|
|
publication deliberately leaves behind, and a new `gates` job runs it in CI.
|
|
Verified in both directions: it passes here, and it fails when the file a
|
|
gate reads is taken away.
|
|
|
|
- **Secure Send now hands you a message, not a bare URL.** A link on its own
|
|
invites being pasted onto text already sitting in the chat box, and a chat
|
|
client turns the selection into the link's caption - so the secret travels as
|
|
the visible message and the link beside it protects nothing. That happened in
|
|
production: six LAPS passwords sent as the caption of a link whose whole job
|
|
was to keep them out of the chat. Copy message puts a sentence on the
|
|
clipboard instead, which has nowhere to attach itself, and the success card
|
|
now says plainly to delete the password from the chat box before pasting.
|
|
Nothing about the link itself was wrong - the URL carries only the token, the
|
|
key rides in the fragment, and neither ever reached the message.
|
|
|
|
- **Ticking a section off crossed out its title and nothing else.** The strike
|
|
now covers everything under that heading, including its sub-sections, which
|
|
is what "this section is done" means to somebody who ticked the thing that
|
|
contained them. It needed the heading RANK, which the HTML importer was
|
|
discarding: every heading arrived as a sibling, so a parent had no children
|
|
to cross out. Headings now carry their level, render at their rank, and a
|
|
guide written before this reads as flat, exactly as it did. Code blocks,
|
|
tables and images inside a finished section are faded but not struck - a line
|
|
through a command makes it unreadable, and re-reading the command is usually
|
|
why somebody looks back at a step they have already done.
|
|
|
|
- **Tech Tools was invisible to anyone without an account**, even though every
|
|
tool on the page is deliberately usable without one and Secure Send is the
|
|
single write this product offers a guest. The nav item named no permission,
|
|
because the plugin had none to name, so the sidebar filter dropped it. The
|
|
plugin gained `tools.view`, and the item now names either that or
|
|
`tools.secrets` - somebody granted only Secure Send still needs the entry
|
|
that leads to it.
|
|
|
|
- **The sidebar offered pages the reader could not open, and hid ones they
|
|
could.** A nav item is hidden from anybody who does not hold its permission,
|
|
but only four items ever declared one - so the filter did nothing for the
|
|
rest, and a signed-out visitor granted `machines.view` still saw an empty
|
|
sidebar. Every item now names the read permission its page needs, in the
|
|
plugins, in the core navigation endpoint and in the frontend's fallback list.
|
|
A signed-in member with a sparse role will notice the same correction: a link
|
|
that could only answer 403 no longer appears.
|
|
|
|
- **Every report offers to email itself, not just two of them.** `POST
|
|
/api/reports/email` has always taken any `{columns, rows}`, but the button was
|
|
on the toner and warranty pages only - because those two happen to be their
|
|
own routed pages, and the reports page had no button at all. It is now beside
|
|
Export CSV for every report the page can build a table from, which is all of
|
|
them: the plugin reports through their declared columns, the core reports
|
|
through the flat list they return, and the asset inventory summary by
|
|
flattening its three tables into one with the grouping named in a column.
|
|
Presentation-only fields - a status colour, a link id - are left out, since a
|
|
column of hex codes is worse than no column. The button is hidden without the
|
|
`reports.export` permission the endpoint enforces, on all three pages: offering
|
|
somebody a button that can only answer 403 is worse than not offering it.
|
|
|
|
- **A long drive name pushed Take and Return out of the card.** On the USB kiosk
|
|
the name is a flex item, which defaults to `min-width: auto`, so it kept its
|
|
full intrinsic width and shoved the button through the side of the card -
|
|
leaving a drive nobody could take. Names now wrap, including the serial-style
|
|
ones that carry no spaces at all, and a description is clamped to two lines so
|
|
one long name cannot make every card in its row tall. The locker line is never
|
|
clipped: it is how somebody finds the thing.
|
|
|
|
- **Muted text on the USB kiosk and in guides was not muted.** Both named a
|
|
`--textmuted` variable this app has never defined, so the browser dropped the
|
|
declaration and the hints, subtitles and locker lines rendered at full
|
|
strength against the text they were meant to sit behind. The variable is
|
|
`--text-light`.
|
|
|
|
- **Plugin reports rendered a raw JSON dump.** The reports page ran a hardcoded
|
|
switch over five core report ids and fell through to
|
|
`console.error('Unknown report')`, and its results panel was a matching chain
|
|
of per-id templates ending in a `JSON.stringify`. So a plugin could register a
|
|
report, watch it appear in the catalogue, and get a wall of JSON when it was
|
|
opened - which is where the three printedparts reports have been since they
|
|
were added. Any report that declares an endpoint is now fetched from it, and
|
|
any report returning `{columns, rows}` renders as a table. A plugin needs no
|
|
core code to have a working report. CSV export follows the same endpoint
|
|
rather than assuming `/api/reports/<id>`, which does not exist for a plugin
|
|
report, and a report that fails now says so instead of showing an empty panel.
|
|
Export CSV is fetched through the api client rather than `window.open`: a new
|
|
window carries no Authorization header, so every report behind a real
|
|
`jwt_required` answered the export button with a 401. The core reports are
|
|
jwt-optional, which is the only reason the old way ever appeared to work.
|
|
|
|
- **Reports had no way to choose a time period.** Endpoints accepted a window;
|
|
the page offered no control for it, so `printedparts-consumption` was
|
|
permanently stuck on its 30-day default.
|
|
|
|
- **USB kiosk: check a drive out and back in without a login.** The people who
|
|
actually take USB drives are standing at the cabinet, and most of them have no
|
|
ShopDB account - so check-out and check-in were gated behind a login that was
|
|
not doing much anyway, since both modes already attribute the action to a
|
|
scanned BADGE rather than to the signed-in user. `/usb-kiosk` is a full-screen
|
|
touch screen modelled on the parts kiosk, and works against the self-hosted
|
|
tables or an existing `cmmc_usb` database.
|
|
|
|
It meets the same bar as the parts kiosk: it can move a device between
|
|
checked-out and checked-in and nothing else - no create, edit or retire - and
|
|
it does not exist at all until a site sets `usb_kiosk_enabled`. An
|
|
unauthenticated write is a decision about the door.
|
|
|
|
The drives carry no barcode, so unlike the parts kiosk there is nothing to
|
|
present and the screen has to offer a LIST. That widens what a logged-out
|
|
screen exposes, so the list is not anonymous: an SSO comes first and has to
|
|
resolve to a real person before any drive is named - the same bar taking one
|
|
already had to clear. The main screen shows the caller's own drives, what is
|
|
free, and a COUNT of everything else; the holders of other people's drives
|
|
appear only behind a deliberate second tap, so a passer-by cannot read the
|
|
whole cabinet off a wall panel.
|
|
|
|
A badge that resolves to no employee is REFUSED, unlike the signed-in route
|
|
which creates the user row: a checkout nobody can trace to a person defeats
|
|
the point, and at a kiosk there is no operator standing there to vouch for it.
|
|
|
|
Returning somebody else's drive asks first, then records the badge that was
|
|
scanned - so the log says who actually handed it back, which is the honest
|
|
record and needs no change to a schema this app does not own. Refusing a
|
|
mismatch outright is available as a setting, but is not the default: a drive
|
|
left behind by somebody on leave would then be unreturnable at the kiosk,
|
|
which just moves the problem to a person with a login.
|
|
|
|
The list of free drives is filtered from a search box, and lays itself out in
|
|
as many columns as the panel is wide - up to four on a wall display. A cabinet
|
|
of thirty drives was one long column somebody had to read through. A drive is
|
|
found by its id, by what is written on it, or by where it lives, in whatever
|
|
word order comes naturally: "locker engineer" finds the engineering locker,
|
|
because that is what people say.
|
|
|
|
- **Audit log retention.** The audit log is append-only and nothing ever removed
|
|
a row, so it grew for as long as a site ran - here to the point where the
|
|
audit page was tens of thousands of pages deep and therefore useless, because
|
|
nobody pages through that to find anything. Site & Facility gains
|
|
`audit_retentiondays` plus a card showing the log's size and exactly how many
|
|
entries a purge would remove, and there is `flask audit stats` /
|
|
`flask audit purge --apply` for the server. **Opt-in: the default is 0, which
|
|
keeps everything.** An upgrade that silently started deleting a site's audit
|
|
history would be indefensible. Deletion runs in committed batches, because one
|
|
`DELETE` over a million rows holds table locks long enough to block the
|
|
collectors posting inventory; a run that stops early reports how much is left
|
|
rather than claiming it finished. Purging needs its own `audit.purge`
|
|
permission - reading the log and destroying it are different acts.
|
|
|
|
- **Guides: images, and a reader who cannot see drafts.** Guide images are
|
|
fetched through the authenticated API client instead of being pointed at by
|
|
`<img src>`. An image tag sends no Authorization header and this app keeps its
|
|
JWT in one, so every guide image was a broken icon for every user; the same
|
|
change makes stored image links survive a subpath mount. A failed image now
|
|
says so in words rather than showing a broken icon. Revision and image routes
|
|
are gated on their parent guide - revision ids are sequential, so reading one
|
|
guide used to be enough to walk the ids and read every unpublished draft on
|
|
the site. Search, the guide list and the tag chips now match the PUBLISHED
|
|
text, not the working copy, so a reader can no longer get a hit on wording
|
|
that is not in the page when they open it.
|
|
|
|
### Fixed
|
|
|
|
- **A guide's URL no longer moves when its title is edited.** The slug was
|
|
regenerated on every save that carried a title, and the editor autosaves every
|
|
three seconds - so fixing a typo in a heading silently moved the guide and
|
|
broke exactly the pasted links the stored slug exists to protect. The slug is
|
|
now set once, at create.
|
|
|
|
- **Publishing a guide after a failed save reported success.** `publish()` ran
|
|
regardless of whether the pre-publish save reached the server, so the author
|
|
saw "Published version N" while readers got the previous revision. A failed
|
|
save now stops the publish and says why.
|
|
|
|
- **The ADR-015 site-literal check was enforcing nothing.** Its patterns are
|
|
assembled from fragments so the published script does not contain the strings
|
|
it greps for, but they used the apostrophe-escape form (`'a'\''b'`) instead of
|
|
concatenation (`'a''b'`), embedding a literal apostrophe in every pattern.
|
|
Nothing could ever match and the check passed everything from the day it was
|
|
written. Fixed, and it immediately found 25 real violations - among them a QA
|
|
ServiceNow tenant URL sitting in the shipped defaults in seven places.
|
|
|
|
- **ServiceNow ships off and blank.** The defaults pointed at one organisation's
|
|
QA ServiceNow tenant with the integration enabled, so a site that had never
|
|
configured ServiceNow still rendered ticket links - to a tenant its
|
|
technicians cannot reach, and for an adopting site, into somebody else's
|
|
instance. There is no cross-site default for "which ServiceNow is yours".
|
|
|
|
- **Credentials are no longer stored in backup revisions.** The NTLARS/DNC
|
|
registry carries an FTP password in cleartext, and it was stored verbatim -
|
|
rendered by the revision panel and handed back by the download route as a
|
|
ready-to-import `.reg`. Credential values are now redacted at parse, so they
|
|
never reach the database, and the rendered file NAMES each withheld value
|
|
instead of carrying a placebo one: importing a fake password would produce a
|
|
machine that looks restored and cannot connect. `scripts/redact_backup_secrets.py`
|
|
scrubs revisions recorded before this (dry run by default). Rotate any
|
|
password that was stored.
|
|
|
|
- **A search domain that fails is no longer indistinguishable from one that
|
|
found nothing.** Each domain catches broadly and returns no rows so one broken
|
|
table cannot take the whole search box down - but the technician was then told
|
|
they might have misspelled a query that was right, and concluded the record
|
|
did not exist. Failed domains are named in the response, shown as a banner,
|
|
and suppress the did-you-mean.
|
|
|
|
- **An empty `sourcehostname` defeated backup dedup permanently.** The dedup
|
|
query maps `''` to `IS NULL` while the insert stored the raw `''`, so those
|
|
rows could never match their own chain: every post inserted, the table grew
|
|
without bound and the history filled with identical entries. Normalized once
|
|
at ingest, whitespace included.
|
|
|
|
- **`printedparts` migration 0003 was unguarded.** Unlike its twenty siblings it
|
|
ran a bare `ALTER`, so `flask plugin upgrade-all` failed on any database built
|
|
by `create_all` and took 0004 down with it. The migration test that should
|
|
have caught this only covered single-migration chains; it now covers every
|
|
post-cutover chain.
|
|
|
|
- **Guide links with several parameters opened the wrong page.** Text is escaped
|
|
before formatting, so a captured URL arrives carrying entities and was then
|
|
escaped a second time - `&amp;` in every multi-parameter ServiceNow and
|
|
SharePoint link. Emphasis could also reach inside an `href`, and
|
|
`//host` passed as an in-app link when the browser reads it as off-site.
|
|
|
|
- **Retire an asset from the interface.** Machines, PCs, printers and measuring
|
|
tools had no way to take a record out of ShopDB without calling the API, so a
|
|
scrapped tool stayed in every list and report. The endpoints already existed
|
|
and were already soft deletes: the record leaves the lists while its
|
|
calibration history, backups and relationships stay put, and it can be
|
|
restored by setting `isactive` back. Called Retire rather than Delete for
|
|
that reason, and the confirm says what is kept. USB is not included - it
|
|
retires through a status column in the external CMMC database and needs its
|
|
own decision.
|
|
|
|
- **`flask catalog dedupe`, `merge` and `rename`: catalog hygiene.** The legacy
|
|
data carried space-stripped twins - `DellInc.` beside `Dell Inc.`,
|
|
`Windows10` beside `Windows 10` - which rode into the shipped seed catalog
|
|
and out to every site that ran `seed catalog`. `dedupe` merges only
|
|
whitespace-and-punctuation twins, because those are one name typed two ways;
|
|
`Dell` and `Dell Inc.` normalise apart and are deliberately left alone, since
|
|
that is a naming choice only a person can make. The surviving row is the most
|
|
referenced, tie-broken toward the spaced spelling. `merge` makes that
|
|
judgment call explicitly and refuses to invent a target that does not exist.
|
|
`rename` fixes a stripped name with no twin, which neither of the others can
|
|
reach. All three report and change nothing unless `--apply` is passed, and
|
|
`dedupe` is idempotent. References are discovered from the live schema, not
|
|
listed in code: there are 14 foreign keys into these three tables, and
|
|
`machines.controllervendorid` is not one anybody would enumerate by hand.
|
|
|
|
- **Secure Send: hand somebody a password without leaving it in a chat
|
|
window.** A link in Tech Tools that carries one credential, dies when it is
|
|
opened, and expires on its own if nobody opens it. The secret is encrypted in
|
|
the sender's browser and the key rides in the URL fragment, which browsers
|
|
never transmit, so the server stores ciphertext it cannot read and a
|
|
compromised database yields nothing. Nobody can recover a lost secret either,
|
|
including an administrator, which is the same property seen from the other
|
|
side. Redeeming is a POST because chat and mail clients prefetch links to
|
|
build previews, and a one-view secret consumed by a preview window is a secret
|
|
the recipient never sees; a GET reports availability only. Expiry defaults to
|
|
24 hours (maximum a week) and views to one, whichever comes first, and a spent
|
|
row is deleted. Creating needs a login and `tools.secrets`; reading needs
|
|
neither, because the recipient usually has no account and the 128-bit token is
|
|
the authorisation. The 4096-character ciphertext limit is deliberately the
|
|
whole server-side enforcement: a zero-knowledge server cannot tell a password
|
|
from a document, so size is the only control it has, and raising it turns this
|
|
into the file transfer it was scoped not to be. Documented in
|
|
`docs/SECURE-SEND.md`.
|
|
- **A site can let people without an account create a Secure Send link.**
|
|
New `securesend_allow_guests` setting (Settings > Tools > Secure Send), off by
|
|
default because a guest secret has no creator and the audit trail can then say
|
|
when and from where but never who. Guest creation is rate limited per address
|
|
and the setting fails closed if it cannot be read, so an error can never be
|
|
what opens an unauthenticated write endpoint.
|
|
- **The code generator can print a back side.** A badge card is two-sided and
|
|
the generator only ever emitted one page per card. The Back side section
|
|
turns on a second page per card carrying either the same code again, a fixed
|
|
block of text, its own per-row content (CSV `back` / `backlabel`), or
|
|
nothing. Page order is a choice, because the two kinds of printer want
|
|
opposite things: **interleaved** front-back-front-back for a duplex card
|
|
printer, **grouped** all-fronts-then-all-backs for a single-sided printer
|
|
that gets its stack flipped and reloaded. Getting that backwards prints
|
|
perfectly readable cards with the wrong back on every one, which is why the
|
|
order is pinned by tests rather than by looking at the preview.
|
|
- **The gap between a code and its caption is its own control.** It was the
|
|
quiet zone doing double duty: closing up a caption meant shaving the clear
|
|
space a scanner needs, and the only way to get a code to scan was to push the
|
|
caption away from it. Quiet zone now means only the code's own margin; gap
|
|
means only the distance to the caption, and adds to the quiet zone rather
|
|
than eating into it. Every stock preset carries a gap that suits it.
|
|
- **Contents can be positioned inside the label without moving the label.**
|
|
Align and Vertical place the code and caption block anywhere on the stock,
|
|
and Content X/Y nudge it from there. The existing X/Y nudge kept its
|
|
behavior and is now labeled Media X/Y: it moves the whole printed area, for
|
|
a printer whose origin is off, which is a different problem from composing a
|
|
label.
|
|
|
|
- **A picture can go on the label.** Pick an image once and it prints on every
|
|
label - above the code, beside it, or behind everything as a watermark - on
|
|
the front, the back, or both. The file never leaves the browser, so an
|
|
air-gapped site can use it, and it is deliberately not saved between visits:
|
|
a data URL runs to megabytes and would take every sizing value with it when
|
|
the storage quota gave out.
|
|
- **The back can carry its own code, in its own symbology.** "Back carries" now
|
|
defaults to a code rather than text, takes per-row content from the CSV
|
|
`back` column, and has its own QR/CODE128 choice - a QR on the front that
|
|
opens the record, a CODE128 on the back that the bench scanner already reads.
|
|
A back set to carry a code can no longer be laid out as text-only: that
|
|
combination printed a blank card that looked like the code had failed, and it
|
|
is corrected on load for anyone who already saved it.
|
|
|
|
### Fixed
|
|
|
|
- **Every detail page had the same browser tab title.** A machine, a PC and a
|
|
printer all read "West Jefferson ShopDB - Machine" or the equivalent, so two
|
|
tabs on two different records were indistinguishable, a bookmark said nothing
|
|
about what it pointed at, and a link shared into Teams previewed as the
|
|
section rather than the thing. Detail pages now name the record: machines,
|
|
PCs, printers, network devices, subnets, measuring tools, USB devices,
|
|
printed parts, applications, employees, knowledge base articles and guides.
|
|
A record with no usable name, or one that failed to load, keeps the section
|
|
name, and a long name is cut at a word boundary. Report pages gained proper
|
|
titles too, which also fixes "Pc Relationships".
|
|
|
|
- **Guides would have been dropped at provisioning.** The plugin was missing
|
|
from `deploy/site-profile-universal.json`, the profile the released Windows
|
|
installer is built from, so the wizard would not have offered it and
|
|
`flask plugin prune-schema` drops the tables of any plugin absent from the
|
|
profile (ADR-014). Its model also imported core directly rather than through
|
|
`shopdb.api`, which the contract test catches.
|
|
|
|
- **The shipped catalog offered two ways to spell the same vendor, and one row
|
|
that should never have left a test.** `HP Inc.` and `Zebra Technologies` were
|
|
empty rows duplicating `HP` and `Zebra`, which carry every model between
|
|
them; neither exists in this site's database, so they would have arrived as
|
|
duplicates at a new site and nowhere else. `UnscopedLiveVendor` was a test
|
|
artifact: `export_catalog.py` reads the live development database, so a row
|
|
left behind by a lab run was published as reference data. The canonical name
|
|
is whichever spelling holds the entries, which is why it is `Dell Inc.` but
|
|
`HP` and `Zebra`. Vendors 51 to 48.
|
|
|
|
- **The shipped seed catalog contained duplicate vendors, models and operating
|
|
systems.** Four space-stripped twins, each with a correctly spaced original
|
|
already present, so a new site inherited the same company listed twice.
|
|
`OptiPlex5000` had no twin and was renamed to `OptiPlex 5000` rather than
|
|
dropped. Vendors 53 to 51, models 128 to 127, operating systems 14 to 12.
|
|
Not a code fault: the collector strips only leading and trailing whitespace
|
|
and treats the OS list as lookup-only, so it neither created these nor is
|
|
creating more. They came in with the classic-ASP import.
|
|
|
|
- **A maintenance script could not find the database the site actually uses.**
|
|
`python scripts/export_catalog.py` on a deployed site died with 1045 "Access
|
|
denied for user 'root'@'localhost'", naming a user that site does not have.
|
|
Nothing loads `.env` for a plain script run: `wsgi.py` does it for the served
|
|
app and the `flask` CLI does it for itself, so both reach the real database,
|
|
while a bare `python scripts/...` gets neither and falls back to a hardcoded
|
|
`root@localhost` URL. The read-only failure is the harmless version - six of
|
|
the seven scripts write, and on a box where those fallback credentials happen
|
|
to resolve they would have silently rewritten the wrong database. All seven
|
|
now load `.env` from the repo root, resolved from the script's own location
|
|
rather than the working directory.
|
|
|
|
- **A search result opened the list it sits in, not the record it found.** A
|
|
NOTICE linked to `/notifications`, so clicking the row search had already
|
|
identified sent the reader to the list page to find it again by hand. The id
|
|
was in the payload the whole time and only the URL ignored it. The subnet
|
|
searcher had the same defect: an IP search resolves which subnet an address
|
|
falls inside, then linked to the `/network` hub rather than that subnet.
|
|
Both now link to the record. Every other result type already carried its id.
|
|
|
|
- **`flask plugin new` scaffolded a frontend that no lean build could prune.**
|
|
It wrote views to `frontend/src/views/<name>/` and a route file to
|
|
`frontend/src/router/routes/<name>.js`, while all 16 bundled plugins keep
|
|
their frontend in `plugins/<name>/frontend/`, staged by
|
|
`stage-frontend.mjs`. Nothing looked broken, which is why it lasted: the
|
|
router still globs `./routes/*.js` during the Phase 4 dual-location
|
|
transition, so the pages loaded. What a new plugin author silently lost was
|
|
ADR-013 lean builds, since pruning works off the staged tree, plus stranded
|
|
views on deleting the plugin. The templates for the right layout already
|
|
existed; only the output paths were wrong. Their core imports moved to the
|
|
`@` alias, which resolves the same before and after staging, where the
|
|
previous `../../api` was correct only in the old location.
|
|
|
|
- **A zero bar height printed a barcode 176 wide and 0 high.** Nothing rejected
|
|
it: the value went straight into a CSS length, `height: 0in` is perfectly
|
|
valid, and an image at `height: 100%` of a zero-height box is zero pixels
|
|
tall. A cleared field was its own version of the same bug - `'' + 'in'` is not
|
|
a length, the browser dropped the declaration, and the box fell back to auto
|
|
height. Every measurement now goes through `labelVars()`, which will not emit
|
|
NaN, an empty length, or a zero where a zero means the box stops existing;
|
|
padding, quiet zone and gap still accept zero, because there it is a real
|
|
answer. The stylesheet carries its own fallbacks too, though those only cover
|
|
a property that is absent - a property set to garbage is dropped at computed
|
|
value time and takes the fallback with it, which is why the sanitiser is the
|
|
fix and the CSS is only a net.
|
|
- **A caption could eat the barcode.** The code box and the caption both grew,
|
|
so any caption took half the label and kept going: "BAY 12 PRESS LINE 1" on
|
|
1.00 x 0.50in stock left a 36px stub, which is a code that is present,
|
|
unscannable, and reads as one that failed to render. The caption now takes
|
|
only the room it needs and a CODE128 keeps at least 40% of the label, all of
|
|
it when stacked. A code that genuinely fails to render says so in the preview
|
|
now, in red, rather than leaving white space that looks like a label with no
|
|
code on it.
|
|
- **The generator prints from inside the app shell.** It was a full-screen
|
|
route with no sidebar and a narrow column of controls on the left. It is now
|
|
an ordinary page - navigation, full width - and `style.css` hides the shell at
|
|
print time, so the stock still gets nothing but labels. Verified against a
|
|
real print PDF rather than a preview.
|
|
- A test print paired each page with a code by loop position while the codes
|
|
are keyed to position in the whole run, so a two-sided test card could carry
|
|
the wrong code. The page index now travels with the page.
|
|
- The back label's dotted preview outline outspecified the print reset and drew
|
|
a dashed rule down the edge of every second card.
|
|
- **The warranty list could not be searched by machine number.** The number was
|
|
in the payload and rendered in the table as a chip, but the search box looked
|
|
only at vendor, service level, service tag and asset number. That made the
|
|
identifier the feature was built around - a shopfloor PC is warranted as a PC
|
|
but found by the machine it drives - the one thing the box could not find.
|
|
Machine number and machine name are now searched too, and the placeholder
|
|
names them.
|
|
- **`flask plugin upgrade-all` failed a plugin whose tables already existed**
|
|
("table securesendsecrets already exists" on the tools plugin). Every
|
|
post-cutover baseline emitted a plain `op.create_table`, which is correct only
|
|
while a table's existence and its plugin's Alembic stamp cannot disagree.
|
|
They can: anything that builds tables from the models stamps nothing, and a
|
|
run that creates a table then fails before its stamp commits leaves the same
|
|
split, where re-running is the obvious response. Creates and indexes across
|
|
all nine table-creating plugin migrations now skip what is already there.
|
|
Indexes are matched on their COLUMNS rather than their name, because
|
|
`db.create_all()` names them by SQLAlchemy's convention and a migration by
|
|
this repo's, so a name-keyed check would silently add a duplicate index
|
|
instead of skipping. Note the remaining limit: this makes a baseline
|
|
idempotent, not a whole chain. A longer chain can still meet an `ALTER` whose
|
|
column is already present, which no guard on `create_table` addresses.
|
|
- **Saving a machine that has a controlling PC answered "this relationship
|
|
already exists" and refused the save.** Reported against a map position
|
|
change, but the map was incidental: the form re-POSTed the controlling-PC
|
|
link on every save, and the row was already there. It used "do I know the
|
|
existing link's id" as a stand-in for "is there an existing link", and read
|
|
that id from a key the API does not emit, so the two disagreed on any machine
|
|
that had a PC. The same conflation meant CLEARING a machine's controlling PC
|
|
silently did nothing, which was the more dangerous half and had not been
|
|
reported. The loaded PC is now what decides, and re-selecting the same PC is
|
|
a no-op rather than a delete-and-recreate.
|
|
|
|
### Changed
|
|
|
|
- **The computers collector no longer accepts `defaultprinter` or `printers`,
|
|
and can no longer write a printer assignment.** Those keys used to become
|
|
`defaultprinter`/`connectedto` relationship rows straight from a report, which
|
|
contradicts the rule the whole printer feature rests on: a collector report is
|
|
OBSERVED state and never ASSIGNED state, or a bay that installed the wrong
|
|
printer makes itself right simply by reporting it. It was not a dormant code
|
|
path - the collector schema advertised both keys and the example client in
|
|
`docs/COLLECTOR-INTEGRATION.md` set `defaultprinter`, while another page of
|
|
the same document promised nothing on that path writes an assignment row.
|
|
**A client still sending them keeps working**: the keys are ignored, not
|
|
rejected, and the response carries a warning naming them and pointing at
|
|
`POST /api/collector/printers` for reporting queues and
|
|
`seed-from-observed` for adopting them. Silently dropping them would have left
|
|
an integrator believing assignments were still being written.
|
|
- **Tech Tools no longer asks people to sign in for tools that need no
|
|
account.** `/tools`, `/tools/codes` and `/tools/secure-send` were all behind
|
|
`requiresAuth`. The label generator runs entirely in the browser - no API, no
|
|
data, nothing to protect - so gating it only ever cost a guest the use of it,
|
|
and gating the Secure Send page contradicted the guest setting that lets
|
|
somebody with no account create a link. The server still decides who may
|
|
create a secret: making a page reachable is not the same as allowing the
|
|
action, and the two were conflated.
|
|
- **Settings on the left, labels on the right.** The controls were a narrow
|
|
column with the preview trailing underneath, so the labels being adjusted were
|
|
off the bottom of the screen. The preview is now a column of its own that
|
|
stays in view and scrolls by itself, and it folds back to one column on a
|
|
narrow window. Note for anyone changing it: that scrolling column is exactly
|
|
the kind of ancestor that clips a multi-page print to whatever was on screen,
|
|
so print resets its overflow - verified by printing a six-page two-sided run,
|
|
not by looking at the preview.
|
|
- CSV parsing and page building moved out of the generator component into
|
|
`plugins/tools/frontend/labelPages.js`, where the column contract and the
|
|
two-sided page order are covered by tests.
|
|
|
|
## [0.12.0] - 2026-08-20
|
|
|
|
Printers become a property of the bay rather than of whoever last walked up to a
|
|
PC, and the container stops throwing away the site's own files on every update.
|
|
|
|
**Air-gapped sites need a fresh offline bundle for this one.** The stack moves to
|
|
MySQL 8.4 LTS, and an existing tarball only carries the 8.0 image, so
|
|
`docker compose up` would ask for an image the site cannot pull.
|
|
|
|
### Added
|
|
|
|
- **Printer assignment has a form.** `PrinterAssignmentPicker` is the ONE picker
|
|
for both ends: MachineForm gained it (the machine is where the assignment
|
|
belongs and there was no way to set it except the relationships card), and
|
|
PCForm now uses the same component instead of its own copy, so the two sides
|
|
of an override cannot drift. PCForm also stopped reconciling row at a time
|
|
through the generic relationship endpoints, which left a PC half-assigned on
|
|
an HTTP failure; it calls
|
|
`PUT /api/printers/assignments/for-asset/<id>` instead.
|
|
- **A relationship type can declare itself singular**
|
|
(`relationshiptypes.issingular`, migration 7d34), and `defaultprinter` does.
|
|
Setting a second default REPLACES the first. The unique constraint is
|
|
(source, target, type), so two different targets were two valid rows and the
|
|
resolver took the OLDEST: a new default silently lost.
|
|
- **Observed printer queues.** `POST /api/collector/printers` (the ADR-006 hook,
|
|
no new transport and no new credential) records what a bay ACTUALLY has in a
|
|
plugin-owned table, kept strictly apart from what it is assigned. Adoption is
|
|
explicit via `POST /api/printers/assignments/seed-from-observed`, which routes
|
|
through the same reconcile path as the editor and REFUSES a queue matching no
|
|
known printer. New client script `Report-PrintersToShopDB.ps1`.
|
|
- **Wave-gated driver rollout.** `Install-ShopdbPrinterDrivers.ps1` takes
|
|
`-WaveStart`, `-Waves`, `-WaveUnit` and `-IgnoreWave`. GE-Enforce offsets each
|
|
PC by SHA256(hostname) % 5 MINUTES, which was sized for a JSON check, not a
|
|
100 MB driver set: ungated, ~300 bays pull ~30 GB inside one five-minute
|
|
window on the share the whole floor depends on. Each bay derives its wave from
|
|
its own hostname, the gate runs BEFORE the manifest is read (the manifest is
|
|
on that share too), and it fails closed on an unparseable date.
|
|
|
|
### Changed
|
|
|
|
- **MySQL 8.4 LTS** in both compose files, and as the default in
|
|
`build-offline-bundle.ps1`. 8.0 reached end of life in April 2026, which
|
|
`INSTALL-WINDOWS-IIS.md` already said while the stack still pinned it; the two
|
|
halves of the product disagreed about which server a site should run.
|
|
Verified against a real 8.4.11 server rather than by changing a tag: the core
|
|
chain and five plugin chains applied clean, 66 tables at a single
|
|
`utf8mb4_unicode_ci` collation, and the image's PyMySQL authenticates against
|
|
8.4's `caching_sha2_password` (which is why `cryptography` is pinned).
|
|
Sites upgrading an EXISTING server should note that 8.4 removes
|
|
`mysql_native_password`: an account created on 5.6 or 5.7 must be moved to
|
|
`caching_sha2_password` before the upgrade or it cannot log in afterwards.
|
|
Air-gapped sites need a fresh offline bundle, since the tarball carries the
|
|
MySQL image.
|
|
- **The database healthcheck probes over TCP** rather than the unix socket. The
|
|
entrypoint's init pass answers on the socket while running the server with
|
|
`--skip-networking`, so a socket ping reported healthy DURING init and
|
|
`depends_on: service_healthy` released `api` and `migrate` against a server
|
|
that was about to restart. Found by having it happen: a probe passed at 8s and
|
|
the next query failed because the server was mid-restart.
|
|
|
|
### Fixed
|
|
|
|
- **The image did not build.** `npm run build` fires a `prebuild` hook that runs
|
|
`node ../scripts/stage-frontend.mjs`, which stages each plugin's frontend into
|
|
the Vite tree and codegens `routes.gen.js`. The Dockerfile's frontend stage
|
|
copied `frontend/` alone and flattened it to the stage root, so that path
|
|
resolved to `/scripts` and every build since the staging script landed died on
|
|
`Cannot find module`. The stage now keeps the repo-relative layout and copies
|
|
`scripts/stage-frontend.mjs` and `plugins/` in beside it. Verified end to end:
|
|
16 plugin frontends staged, `frontend/dist` in the final image.
|
|
- **The compose stack did not persist `instance/`.** `db_data` was a volume and
|
|
the instance directory was not, so `docker compose build api && up -d api`
|
|
recreated the container and discarded `plugins.json` along with every upload:
|
|
floor plans, branding, model and application images, employee photos,
|
|
warranty proofs, slides, printed-part files and the Dell OAuth token. The
|
|
visible symptom was a site coming back with its plugins disabled. Both compose
|
|
files now mount an `instance_data` volume (the air-gap `migrate` service too,
|
|
since `plugin upgrade-all` writes `plugins.json`), and DEPLOY.md carries the
|
|
one-time rescue for a stack that predates it.
|
|
- **A drifted print queue is corrected, not just a missing one.** Queues were
|
|
matched by NAME alone, so a bay whose printer had moved or whose queue was
|
|
built on a replaced driver looked converged and printed to the wrong device.
|
|
`Set-ShopdbPrinters.ps1` now repoints a wrong port and swaps a wrong driver
|
|
IN PLACE with `Set-Printer`, so the queue keeps its name, sharing, permissions
|
|
and whoever holds it as their default. The driver is only swapped when the
|
|
wanted one is staged, and there is still no removal path in the script.
|
|
- **MachineForm's dropdowns all came up empty.** It read `.data.data` off
|
|
`computersApi.listAll()`, which already resolves to the array, so the whole
|
|
parallel load threw into the catch and the machine's own values never loaded.
|
|
- **The legacy import loader dropped `machines.printerid`**, the classic
|
|
system's record of each machine's default printer, so the production import
|
|
would have lost every one.
|
|
|
|
## [0.11.3] - 2026-08-19
|
|
|
|
Fixes the last of the buildings-and-levels bugs, and brings the shop-floor
|
|
clients into the product: the collector reporter, EventSaver and a printer
|
|
assignment feature that lets a bay install its own printers.
|
|
|
|
**Anyone on 0.11.0-0.11.2 should take this.** The hover preview reported "this
|
|
asset has a position but no level" for every asset, and the map PDF printed
|
|
markers from every floor onto one sheet.
|
|
|
|
### Fixed
|
|
|
|
- **Every hover mini-map said "no level".** When 0.11.0 gave LocationMapTooltip a
|
|
levelid, none of its seven call sites were taught to pass one - printer,
|
|
machine and PC detail, the toner report, enforcement reports, the warranty chip
|
|
and the dashboard cards. Two payloads behind them also emitted coordinates with
|
|
no level.
|
|
- **Map PDF export printed other floors' markers.** It plotted every filtered
|
|
asset onto one sheet, which is worse than the on-screen version was: nobody can
|
|
correct a sheet once it is printed and carried onto the floor.
|
|
- **The legacy import loader created undrawable markers**, sending mapleft/maptop
|
|
with no level at three call sites. That loader is the one still to run against
|
|
production.
|
|
- The build gate that should have caught all of this asked whether a FILE
|
|
mentions `levelid`, not whether each position does - one module emitted `mapx`
|
|
six times and `levelid` once and passed. It now checks per occurrence, covers
|
|
`scripts/`, and fails any Vue file that binds tooltip coordinates without a
|
|
level.
|
|
|
|
### Added
|
|
|
|
- **Printer assignment.** Printers belong to the MACHINE and reach whichever PC
|
|
controls it, so a reimaged or swapped PC comes back with the bay's printers and
|
|
nothing had to be saved off the old one. New `usesprinter` relationship type,
|
|
propagating through `controls` at read time; `GET /api/printers/for-host/<host>`
|
|
for the client; `PUT /api/printers/assignments/for-asset/<id>` to reconcile a
|
|
whole set in one call.
|
|
- **`printerdrivers.vendorid`**, so one row serves a make. HP and Xerox universal
|
|
drivers cover 41 of the reference site's 44 printers; binding a driver to a
|
|
single model meant 21 near-duplicate rows. Resolution is model, then vendor,
|
|
then the pre-vendorid name convention.
|
|
- **Client scripts** in `plugins/printers/client/`: stage a site's driver set
|
|
(`Install-ShopdbPrinterDrivers.ps1`, with `-TestOnly` for DSC), converge a
|
|
bay's queues (`Set-ShopdbPrinters.ps1`), and apply the per-user default
|
|
(`Apply-ShopdbDefaultPrinter.ps1`).
|
|
- **The collector reporter is in the repository** at
|
|
`plugins/computers/client/`, with no site in it, and the server generates a
|
|
copy carrying this site's URL and ranges: Settings > Computers > Asset
|
|
reporter, or `GET /api/computers/client-script`. The collector key is
|
|
deliberately never stamped into it.
|
|
- **EventSaver source** at `plugins/slides/client/`. The site UNC that was
|
|
compiled into it is gone; a missing ini now fails visibly rather than pointing
|
|
a new site at the reference site's file server.
|
|
- Settings: `computers_routableranges` (replaces two hardcoded VLANs) and the
|
|
buildings-and-levels admin gains editable width AND height.
|
|
|
|
### Changed
|
|
|
|
- The map editor accepts a click. Its handler was bound only if the map was
|
|
already a picker when it mounted, and the editor opens with nothing selected,
|
|
so placing a marker by hand was impossible for the life of the page.
|
|
- Global search orders every query before truncating, so the same search cannot
|
|
return a different subset twice.
|
|
|
|
## [0.11.2] - 2026-08-17
|
|
|
|
Bug fixes for the buildings-and-levels work in 0.11.0, all found by using it.
|
|
Anyone on 0.11.0 or 0.11.1 with more than one level should take this: until now
|
|
the map drew markers from every level on whichever floor plan was showing.
|
|
|
|
### Fixed
|
|
|
|
- **Markers from other levels were drawn on the displayed level.** The map
|
|
component filtered out positions with no coordinates and drew everything else,
|
|
never checking which drawing a position belonged to - the exact failure ADR-017
|
|
exists to prevent, in the one component that draws the map. Markers are now
|
|
filtered to the level being shown, and a position with no level is omitted
|
|
rather than approximated onto the default.
|
|
- **The map page had no way to choose a level.** It read the current level only
|
|
to title a PDF export, so a second floor was unreachable from the viewer most
|
|
people use. A level selector appears when a site has more than one, and
|
|
switching moves the drawing, the bounds, the coordinate space and the markers
|
|
together.
|
|
- **The map editor never accepted a click.** Its click handler was bound only if
|
|
the map was already in picker mode when it mounted, and the editor opens with
|
|
nothing selected, so selecting an asset and clicking did nothing at all for the
|
|
life of the page. The asset forms were unaffected, because their picker is
|
|
mounted inside a dialog that is already in picker mode.
|
|
- Existing markers are now drawn while placing one. Positioning an asset relative
|
|
to the machines already on the floor is the whole task.
|
|
- Searching the map follows results across levels: a search whose matches are all
|
|
on another floor showed an empty map while the filter counted them.
|
|
- **A level's native size could not be set.** The settings page had a width field
|
|
and no height field at all, and locked both once any marker was placed - which
|
|
blocked the case the feature was built for, a new blueprint of new dimensions
|
|
on a floor that already has markers. Both fields are editable, with a
|
|
confirmation and a pointer to landmark recalibration.
|
|
- **The same search could return different results.** Fourteen of the sixteen
|
|
searchers behind global search truncated with `LIMIT` and no `ORDER BY`, so the
|
|
database was free to return a different subset of matching rows each time.
|
|
Every searcher now ends in a total order.
|
|
- Re-searching a term already in the address bar did nothing: the router treats
|
|
it as a duplicate navigation and aborts, so the results page never re-queried.
|
|
This is why the sidebar box and the results-page box appeared to disagree.
|
|
- The empty legend no longer draws its bar and border, which read as a stray
|
|
input box under the map toolbar.
|
|
- The map editor's panel header no longer crams a heading and three controls onto
|
|
one row, which had squeezed the search box until its placeholder read
|
|
"Search na".
|
|
- Both map pages had a scrollbar. They subtracted the wrong amount from the
|
|
viewport height for the page chrome, so each overflowed by the difference; the
|
|
padding is now a value the pages and the layout share.
|
|
|
|
## [0.11.1] - 2026-08-17
|
|
|
|
A patch release: the pagination-cap fixes, plus the documentation that missed the
|
|
0.11.0 installer. No application behaviour changed on the server and the plugin
|
|
contract stays at 0.20.0.
|
|
|
|
`docs/UPGRADE.md` is one of the documents the Windows installer puts on the box,
|
|
and its buildings-and-levels section landed after 0.11.0 was tagged - so 0.11.0
|
|
shipped the page an operator reads WHILE upgrading without the guidance about
|
|
that upgrade. An air-gapped server has no other way to reach it.
|
|
|
|
### Fixed
|
|
|
|
- Pickers and batch label sheets no longer stop at 100 rows. `perpage` is
|
|
clamped to `MAX_PAGE_SIZE` (100) server-side and the response says nothing
|
|
about it, so a control asking for 500 or 1000 received the first 100 rows and
|
|
a success. On a site with 126 applications the 26 sorting last could not be
|
|
chosen as a knowledge-base topic; batch label printing rendered a sheet for
|
|
the first 100 of 262 machines that looked complete. New `fetchAllPages()`
|
|
helper plus `listAll()` on the applications, models, machines, computers,
|
|
printers, network, measuring-tools, USB and printed-parts APIs, used by the
|
|
KB article form and topic filter, the notification form, the report filter
|
|
builder, `AssetLabelBatch`, `PrinterQRBatch`, `USBLabelBatch`,
|
|
`PrintedPartsLabels`, `MachineForm` and `NetworkDeviceForm`. Lists that render
|
|
a page at a time are unchanged - they page deliberately.
|
|
|
|
### Changed
|
|
|
|
- The installer now also ships `FLOOR-MAP.md`, the operator's page for loading
|
|
floor plans, placing markers and moving them when a plan changes. Same reason
|
|
the Windows runbooks ship: the box cannot reach the wiki.
|
|
- The bundled `UPGRADE.md` now carries the 0.11.0 section: an existing map is
|
|
preserved and nothing moves on screen, and replacing a blueprint with one of
|
|
different dimensions moves every marker on that level, so recalibrate from
|
|
landmarks rather than editing the level's width and height.
|
|
|
|
## [0.11.0] - 2026-08-17
|
|
|
|
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.
|
|
|
|
Alongside it, a sweep of what search can actually reach. Two identifiers the UI
|
|
collects were findable in almost no way, and two plugins' records were findable
|
|
in no way at all.
|
|
|
|
### 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.
|
|
|
|
### Fixed
|
|
|
|
- `gaugelabreference` was matched only on measuring tools and
|
|
`maintenancereference` was matched nowhere at all, for any asset type, while
|
|
Settings offers both identifiers on machines, PCs, printers and network
|
|
devices. Both are now matched by global search AND by every asset list's
|
|
`?search=`. An identifier that can be entered has to be findable, or it is a
|
|
write-only field, and the tag is read off the physical machine by someone
|
|
looking at the list page.
|
|
- USB devices and printed items were unreachable from global 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 and label, and on bin
|
|
code (`itemcode`), gage-lab tag, name and description respectively - both
|
|
honouring `isactive`, both with Settings toggles, result labels and filter
|
|
chips.
|
|
- Global search returned knowledge-base articles whose topic application is
|
|
retired, while `GET /api/knowledgebase` hid them. A filter that only some paths
|
|
apply is not a filter: the article stayed two keystrokes away and the result
|
|
printed the retired application as its subject.
|
|
|
|
### 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
|
|
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`.
|
|
|