72b3904f717c2a7559c24a833abff7f01c03938f
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0dc0ac13c8 |
Assign printers to a machine, and let the PC that drives it inherit them
Printers belong to the bay, not to the box currently driving it. The assignment
goes on the MACHINE asset and reaches whichever PC controls it, so a reimaged or
swapped PC comes back with the right printers and nothing had to be saved off the
old one. The asset register is the backup.
New relationship type usesprinter ("this printer is installed here"), beside the
existing defaultprinter ("which of them is the default"), both seeded and both
given a propagation rail through controls. The rails are consumed at READ time
only: the create-time fan-out skips directional through-types, and controls is
directional, so assigning a printer to a machine does not copy rows onto its PC.
That is what keeps own-beats-inherited possible.
Resolution for a PC is its OWN rows if it has any, otherwise one hop out along
controls to the machines it drives. Whole set at a time, not merged: a PC with
its own assignment is overriding the bay deliberately, and the UI has to say so
or a tech "fixing" a bay by editing the PC will shadow the machine's record and
wonder why they keep disagreeing.
GET /api/printers/for-host/<hostname> is what the convergence client asks every
cycle. Resolved by hostname because the collector upserts PCs by hostname and an
office PC has no machine number. An unknown host, a site without the computers
plugin, and nothing assigned all return an empty set - that is the client's
designed no-op and it must stay indistinguishable from "assigned nothing".
PUT /api/printers/assignments/for-asset/<id> reconciles the whole set in one
call. The endpoint was specified, documented and asserted by three tests, and
never written - the verification pass caught that, with four failures. It
validates the default BEFORE any write, so a rejected request changes nothing;
soft-deletes rows that went away; and REACTIVATES soft-deleted rows rather than
inserting, because the unique constraint spans inactive rows and a blind insert
after an unassign raises IntegrityError on MySQL while passing on SQLite.
One default per asset, enforced here because the schema cannot: the constraint is
(source, target, type), which accepts two different defaults quite happily. Two
active defaults are still reachable through the generic relationships endpoint,
where the oldest silently wins - recorded in the proposal as the next thing to
close.
printerdrivers gains drivername: the exact string the INF declares, which
Add-PrinterDriver matches on and nothing else. Deriving it by parsing INFs on
hundreds of bays is fragile; a human confirming it once is not.
|
||
|
|
2083029ff2 |
Generate the collector script per site, and bring EventSaver into the repo
A site adopting ShopDB had to be handed two files and told what to edit in them. Both are now the product's, and one of them the server writes for you. GET /api/computers/client-script (admin) returns Report-AssetToShopDB.ps1 with this site's values already in it: site_base_url becomes the -ApiUrl default and the new computers_routableranges setting becomes -AllowedRanges. Only the PARAMETER DEFAULTS are substituted - the copy in plugins/computers/client/ stays runnable, so there is no second version to drift from the first - and everything stamped stays overridable by argument or registry, because a bay may need to differ from its site. Settings > Computers > Asset reporter edits the ranges, downloads the script and shows its SHA-256. The collector key is deliberately not stamped in, and a test fails if it ever is. That file lands on every shop-floor PC, and a token spread across hundreds of bays cannot be rotated quietly; it stays in the registry, provisioned per ADOPTING-AT-ANOTHER-SITE.md. The routable ranges are the last thing that was hardcoded in that script. They are now a setting, so West Jefferson's two CIDRs move out of source code and into that site's own configuration - which is what ADR-015 asks for - and a site that sets nothing still works, because the script falls back to the NIC carrying the default route. EventSaver joins it in plugins/slides/client/, source only: EventSaver.cs and EventSaver.ini, no compiled .scr - a binary is a release asset, like the installer exe. The share path that was compiled into Config.Folder is gone. It used to be the fallback when the ini was missing, which silently pointed a new site at the reference site's file server; it is now empty, and failing visibly beats displaying another site's slides. Verified by compiling the edited source in the Windows VM with the in-box csc.exe: 15,872 bytes, exit 0. Also: the DSC example in the adoption guide gains a CollectorRanges resource and stops passing -ApiUrl to a script that already reads BaseUrl from the registry the same example writes, and the guide points at the generated download instead of hand-editing a URL. The contract test caught the endpoint importing shopdb directly for the version string, which ADR-002 forbids a plugin from doing. The product and contract versions are in app.config now, which a plugin reads through current_app. Adds docs/proposals/printer-assignment.md: assign printers to a PC in ShopDB and let the bay install them, with what the fleet data says about drivers - HP and Xerox cover 41 of 44 printers with universal drivers, there are no Brother printers at all despite 208 files of Brother inkjet drivers in the installer, and printerdrivers holds one row pointing at a per-model folder instead of a universal driver. |
||
|
|
aede5ac621 |
Match the optional identifiers in every list's search box
gaugelabreference and maintenancereference are collected on the form and shown on the detail page for all five asset types, and no list's ?search= matched either. Someone holding a physical tag could read it off the machine, type it into the list they were already looking at, and get nothing back - while global search, fixed in the previous commit, found it. The clause is copy-pasted once per plugin, which is how all five came to omit fields their own forms collect, so the test is one parametrized pass over all five lists rather than five per-plugin tests that would drift the same way. It also pins that widening the clause did not turn the box into a pass-through. The api-inventory entries enumerate the fields each search matches, so all five were stale the moment the queries changed; updated with the OpenAPI spec. Not touched: the collector does not send either identifier, correctly - a lab-assigned tag is not something an agent on a PC can discover. The legacy import loader does not map them either, and the classic schema has no gauge-named column to map from. |
||
|
|
3324dbd91e |
Buildings and levels for the floor map, and make every identifier searchable
The map was one picture of one floor. A second floor was added, the blueprint changed size, and machines moved, so a position now records WHICH DRAWING its coordinates belong to. Buildings and levels (ADR-017). Each level owns its blueprint per theme and its own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the site. A position whose level is unknown renders "level unknown" and is never drawn on the default level, because a marker on the wrong floor plan looks entirely correct while pointing at the wrong place. Repositioning in bulk: filter by unplaced, needs-review or level, search, place, confirm. Landmark recalibration solves the transform PER AXIS from landmark pairs and never from image dimensions - the canvas grew taller without rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong everywhere. It defaults to a dry run, reports what would land off the drawing, snapshots before applying, and clears mapverifiedat because a transform is a guess awaiting review. Snapshots restore, including the level and the review state, and a restore snapshots first so an undo is undoable. Search: gaugelabreference was matched only for measuring tools and maintenancereference was matched nowhere at all, for any asset type, while Settings happily offers both identifiers on machines and PCs. A tag an operator is told to record has to be findable or it is a write-only field. USB devices and printed items were unreachable from search entirely - neither is an asset, so the generic asset search could not see them and no searcher existed; they now match on serial, asset tag, label, bin code and gage-lab tag, honouring isactive, with Settings toggles and result labels to match. The retired-application rule was half a rule: GET /api/knowledgebase hid articles whose topic application is retired while global search still returned them and printed the retired application as the subject. A filter is only real if every path that reaches the row applies it. Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location gained levelid, and resolve_asset_position returns the levelid belonging to whichever source supplied the coordinates. The five plugins that write a map position are re-pinned. The install-list text format gained levelid as a NINTH field, appended, because the shipped Pascal installer reads fields 0-7 by index. That installer still compiles in one drawing's dimensions and bundles one blueprint, so its map is accurate for the default level only; /api/maplevels is deliberately unauthenticated so it can read both at runtime once rebuilt. Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps. Migration 7d33 converts an existing single-map site into one building and one default level carrying the old map_* settings, then assigns every placed asset and location to it. Nothing moves on screen. Old settings rows are kept so a rollback still finds them. Verified end to end on MySQL 5.6 from a production-shaped database. |
||
|
|
ad335cfc9e |
api: document the twenty-two routes that were serving traffic in silence
The inventory is hand-maintained, and should stay that way - its value is the prose, and what an endpoint is FOR cannot be derived from the code. An audit of all 372 documented operations found zero phantom routes and zero wrong parameter names, so the maintenance is in good order. What hand-maintenance cannot do is notice a route somebody added. Twenty-two were undocumented: the entire backups plugin surface, every one of the dashboard card endpoints added with contract 0.19.0, the GE-Enforce publish preflight, the employee SSO resolver, the protocol update verbs, and the four /api/docs routes - so the spec did not describe how to fetch the spec. Coverage is now a test. It walks the live url_map and fails when a served route has no entry, which means adding an endpoint includes describing it, in the same commit, while the author still knows what it is for. The reverse direction is checked too: a documented route that no longer exists sends a reader to a 404. Writing that test found one more thing. The inventory writes multi-verb routes as "PUT|PATCH", and neither the parity check nor the generator split on the pipe - so those operations were absent from the published spec entirely, with nothing reporting it. The spec now carries all 394 operations the code serves, which is the first time the two numbers have matched. The generator's own docstring claimed the inventory could be regenerated. It cannot; nothing generates it. That sentence is why nobody noticed it was falling behind. |
||
|
|
36b0265668 |
openapi: emit a spec a machine can actually use
The generated spec carried no `parameters` and no `requestBody` on any of its 372 operations. That is invalid OpenAPI 3.1, and the practical cost was worse than the formal one: the MCP server builds its tools from this file, so every tool had an empty input schema and silently dropped whatever the caller passed. A request for one asset returned the list, and nothing anywhere reported an error. All 118 templated paths now declare their path parameters, typed from the Flask converter that named them, and write verbs declare a JSON body. The body is an open object carrying the prose description rather than an invented schema. The inventory describes bodies in sentences, and a field list this generator guessed at would be worse than none - but "an object, described here" is the difference between a client that can send a body and one that cannot send anything. Security was wrong on 123 operations. `jwt-optional` means "works logged out, returns more logged in", which OpenAPI expresses as the empty requirement alongside the scheme; publishing them as bearer-required told every reader that a public endpoint needs a token. Responses were one hardcoded 200, so a generated client had no idea a call could fail. Every operation now documents the error envelope - and the envelope itself is a defined schema, because its error nests under `data.error` rather than at the top level, which is the single thing people get wrong when writing against this API. 95 summaries were cut at 120 characters mid-word, which is what a tool picker shows a user as the whole description of a call. They now end on a word. Tests pin the shape rather than the prose. One of them contradicted an older test that REQUIRED the contract version as a literal in PLUGIN-HOOKS.md - the same copying that left nine documents stale - so that test now asserts the doc points at the generated map instead. |
||
|
|
05be4c4489 |
docs: fix the examples that fail if you paste them
Six procedures that could not be followed as written.
Eighty-nine curl examples single-quoted `Authorization: Bearer $TOKEN`, so the
shell never expanded it and the server answered 422 "Not enough segments". Nine
more did the same with X-API-Key. The other 129 examples in the same file
already used double quotes, so this was drift rather than a convention, and the
spec regenerated from it carried the fault onward.
The GE-Enforce report example put a `//` comment inside a JSON body. The server
parses with silent=True, so it saw `{}` and answered "hostname is required"
about a body that plainly has one - the worst kind of error message, one that
sends the reader to the wrong field entirely.
The IIS install ran `flask db upgrade` and a per-plugin install loop but never
`flask plugin upgrade-all`, leaving every plugin's own chain unapplied. That is
precisely the 1054 "Unknown column" a deploy then hits somewhere else, days
later, on the page that uses the new column.
The pilot runbook looped `flask plugin enable` over plugins that were not yet
installed; enable refuses those, so on a fresh database it exited 1 on every
iteration and enabled nothing. ADR-013 had already recorded that defect.
`apply-profile` installs and enables in dependency order, which is what the step
was reaching for.
DEPLOY-WINDOWS-IIS named a profile file that does not exist; the shipped ones do.
And PLUGIN-EXTERNAL-REPO never mentioned PLUGIN_TABLE_OWNERS, while the
migration engine raises for any plugin missing from it - so the guide's own
step 5 fails for any external plugin that owns a table. That the registry lives
in the framework repo is deliberate, so the guide now says so, and says what it
costs: a table-owning external plugin is a two-repository change, and a plugin
that owns no tables avoids it entirely.
|
||
|
|
4995456136 |
docs: take one site's name, hosts and paths off the public wiki
The publishability gate caught internal tooling names and developer paths but nothing site-specific, so roughly sixty leaks reached the wiki: the site name in ten documents, real fleet hostnames in the collector and GE-Enforce examples, an internal database name through the whole import guide, imaging-share paths, and a maintainer's username as the Deciders line of every ADR and inside a generated curl example. None of it is a security matter on an air-gapped fleet. It matters because these pages are read by engineers at other plants, and a document that names one site throughout reads as that site's notes rather than a product's documentation - which is exactly what it then gets treated as. Examples now use neutral hostnames, the site is "the reference site" where the distinction carries meaning, and ADRs are decided by "ShopDB maintainers". The gate carries all of these patterns, so the next one fails a build. Two documents leave docs/ because they were never written for an outside reader. PROJECT-REVIEW.md is an internal health memo pinned to a commit from July, whose headline finding (an untracked playbook) has since been fixed - it is history, and git holds it. PILOT-DEPLOY.md is one site's own cutover runbook, complete with a "re-measure before publishing" placeholder; it moves next to the loader it belongs to, in scripts/site_imports/wjf/. ADR-015 is AMENDED rather than rewritten. Its enforcement section still said report-only and its backlog still listed hardcodes that are now cleared, which left the record contradicting itself. The amendment says what changed and why the report-only period ended; the original text stays, because what the decision looked like when it was taken is the part worth keeping. Also corrects llms.txt's response envelope, which had errors at the top level and pagination at meta.total. Both are nested one deeper, so anything written against that description read undefined on every error it tried to handle. |
||
|
|
035419fa51 |
ADR-015: stop shipping one site's values, and make the rule a gate
The scanner has been reporting the same count for weeks, which is what a rule that only prints becomes. It now FAILS the build, and it looks where the leaks actually were: PowerShell, the installer, the seeds, generated JSON, the frontend - case-insensitively, across plugins, shopdb, scripts, deploy, tools. A line that is deliberate declares itself with an ADR-015-OK marker and a reason, so the claim is visible in review instead of tolerated in silence. What it found, fixed here: - The shadow client wrote one site's ShopDB URL into HKLM whenever the registry disagreed. At the site it was written for that reads as healing drift; anywhere else it overwrites the site's own address on every enforce cycle, and the site cannot win because the cycle repeats. The bay's value now wins, an explicit -BaseUrl seeds it, and with neither there is nothing honest to write, so it says so and skips. - The kiosk dispatcher fell back to one plant's host when HKLM was unset, so a kiosk elsewhere quietly opened a server it has no business reaching. The fallback is now this site's site_base_url, baked in at seed time, and the dispatcher refuses rather than guessing when neither is set. Its legacy shortcut matcher derives the host from that URL instead of naming one. - The OpenAPI generator hardcoded a production hostname into every spec it generated, which then published to a public wiki. The relative mount is the only server it can honestly name; a site passes its own by environment. - Placeholders and examples in the UI and the client help offered real internal subnets and a real production URL. They now use documentation ranges. Both publication gates - the export scrub and the docs publishability test - carry the site patterns, which neither did. One plant's hostname, FQDN and internal networks are out of the documentation and the generated specs. Comments naming the reference site are reworded rather than deleted: the reasoning is worth keeping, the plant name is not what makes it true. |
||
|
|
3d83806135 |
Make the toner forecast an order, not a table
The report answers a purchasing question, and it was answering it in seven columns, two tables and a rowspan. What someone actually needs from it is a short list of what to buy. So it opens with that list, grouped by part number with a quantity. Two cartridges of the same part in different printers is a quantity of two, which is the number an order needs and the one a per-printer table made the reader count by hand. It covers what is empty plus what goes within a fortnight - ordering only what is already empty means running empty. There is a copy button, because it ends up pasted into a mail. Below it the cartridges sit in urgency bands rather than in one long list sorted by a number. The question is which pile a thing is in, and a pile that is empty is worth seeing as empty. Everything past "empty" starts collapsed; the order list above already covers the same ground in a tenth of the height. The row is a cartridge now, not a printer, so it can carry its own part number, its own level bar and its own countdown. Nesting supplies under a printer meant opening a printer to find out whether anything on it needed doing. Cartridges with no part mapped are counted on a single line rather than given one each. They cannot be dropped, since that would quietly shorten the order, and they cannot be ordered from here either - the job they represent is mapping them, which is one job however many there are. Bands and the order horizon are decided server-side, next to the arithmetic that produces them, so a heading cannot disagree with what got added to the list. Checked against a fleet of 43 dev printers with real part mappings, driven by a stub Zabbix - live Zabbix is not reachable from the dev box. |
||
|
|
a64796f060 |
Document the upload, proof and forecast endpoints
Ten routes shipped over the last few commits without reaching the inventory, which is the file everything else is generated from - openapi.json, the /api/docs browser, and the per-site build. An endpoint absent from it is invisible to anyone integrating against the API even though it answers requests. The auth notes matter more than the paths. API-REFERENCE tells a deployment reviewer that essentially every GET is optional-auth; two of these are not, and an exception buried in a table of 372 operations is one nobody reads. The installer download and the warranty proof are called out with why: one is licensed vendor software, the other carries pricing and a service tag. The application image is listed with the fully public endpoints, since tiles render before login. |
||
|
|
959db2922b |
Release 0.8.1
The installer on the share was stamped 0.8.0 and contained sixteen commits that were not in the v0.8.0 tag. A build that misreports its own version is exactly what the version check in docs/RELEASING-WINDOWS.md exists to prevent, and it would have left two sites unable to say what they were running. Everything in 0.8.1 landed after v0.8.0 was tagged this morning, driven by two sites entering real data for the first time: the blank-code 500, the two-slide display that never rotated, modals discarding a part-filled form, filters returning an empty page, model photos that could not be saved, and the shared equipment catalog that lets a new site start with vendors, models and printer supply part numbers already present. CHANGELOG gains a 0.8.1 section, and the OpenAPI document follows __version__ rather than being restated. |
||
|
|
89e880afc3 |
Release 0.8.0
The Windows installer has never shipped under a version: v0.7.0 was tagged before any of it existed, so every build handed out so far stamped a server with 0.7.0. Two servers running different builds were indistinguishable, and the installer logged each upgrade as "same version already installed" rather than recording what changed. This cuts the release that fixes that. 0.8.0 rather than a patch: the air-gapped installer is a new capability, and pre-1.0 semantic versioning puts that in the minor slot (ADR-007). CHANGELOG gains a 0.8.0 section covering the twelve defects a real Windows Server 2019 install surfaced, the move from inferring "is this a re-run of my install?" to recording it, and the operator documentation. deploy/site-profile-universal.json is now in the repository. Released builds were being produced from a profile in a temporary directory, so the next release could not have been reproduced once that file was cleaned up. docs/RELEASING-WINDOWS.md points at the committed profile and says why. scripts/gen_openapi.py reads __version__ out of shopdb/__init__.py instead of restating it. Its hardcoded copy had already drifted a release behind, which is the same mistake that once shipped an installer stamped with the wrong version. |
||
|
|
b507884ad6 |
api: serve interactive OpenAPI docs at /api/docs (offline) + llms.txt
Generate docs/openapi.json (3.1, 362 operations) from the API inventory via scripts/gen_openapi.py, and serve it with a self-hosted Redoc bundle at /api/docs - no CDN, works on the air-gapped box. Also serve docs/llms.txt (a concise LLM entrypoint) at /api/docs/llms.txt. New core 'docs' blueprint; staticdocs/ excluded from the naming check (vendored minified JS). |