f34b9ca710912e67d1779940abb36b08c1edb500
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f34b9ca710 |
Carry the level everywhere a position is drawn, and gate it per occurrence
The hover mini-map said "This asset has a position (2835, 1410) but no level" for every asset in the product. When 0.11.0 gave LocationMapTooltip a levelid prop, NONE of its seven call sites were taught to pass one - printer, machine and PC detail pages, the toner report, enforcement reports, the warranty chip and the dashboard cards - so the component correctly reported a missing level and the preview never drew. Two payloads behind those views also emitted mapx/mapy with no level: the toner report and the enforcement report. The map PDF export had the ORIGINAL bug still in it: it plotted every filtered asset onto the sheet, so exporting the ground floor printed second-floor markers on it. Worse than on screen, because nobody can correct a sheet once it has been printed and carried onto the floor. It now exports only the level being viewed. The legacy import loader sent mapleft/maptop with no level at three call sites. That loader is the one still to run against production, and every marker it created would have been undrawable. It now resolves the site's default level - the legacy schema predates levels and has one floor plan, so that is what its coordinates mean. THE GATE MISSED ALL OF THIS because it 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/ as well as shopdb/ and plugins/, and fails any Vue file that binds tooltip coordinates without :levelid. Both new rules were confirmed to fail the build against planted violations before being relied on. Printer QR labels: the asset number is no longer printed. A label now reads name (8201-HPLaserJetPro), QR, FQDN, then IP. The name falls back to the assetnumber because that is where sites actually keep it - every printer here has an empty name field, so preferring the Windows queue name alone would have printed a blank line on every label. |
||
|
|
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. |
||
|
|
e7b8933588 |
Stop the publication scrub failing on the rules that enforce it
The export gate now carries the site patterns, and three kinds of file tripped it - two legitimately, one by construction. Two test files held real internal subnets as fixtures. They are documentation ranges now, which test the same logic and disclose nothing. The project-map generator and a changelog entry named a file that is excluded from publication, so a public reader was pointed at something they cannot see. Both now describe what happened without naming it. And the naming script has to CONTAIN the site patterns in order to grep for them, so written literally the rule's own definition fails the gate that enforces it. The patterns are assembled from fragments, the same trick the docs publishability test already uses for the same reason. Verified the hard way: a planted literal is still caught, so the fragmentation did not quietly turn the rule into one that matches nothing - which is the obvious way for this fix to have gone wrong. |
||
|
|
c648bdf560 |
labels: one module knows how to draw a code, seven views stop guessing
Three core pages and four plugin pages each imported qrcode and jsbarcode directly, and each carried its own answer to the same questions: what margin, what width, which error correction, how big a module must be before a scanner can read it. The answers had already drifted - margin 0 in one place and 2 in another, width 150 against 160 - and on a label that is the difference between a sticker that scans and one that does not. frontend/src/utils/codes.js owns it now: the label-stock presets, the quiet-zone and margin defaults, CODE128 with no printed value, and the printer-resolution arithmetic that only the Tech Tools generator had. A view passes what is specific to its own label and nothing else - MachineBadge still asks for CODE39, because the badge readers predate the shop-floor scanners and decode nothing else, and that is exactly the kind of thing a call site should say out loud. views/print/qrLogo.js is folded in rather than left as a second half-shared helper that only some of the pages reached into. The check script now fails a build that imports either library outside that module. Without it this re-forks within a month: the next label page starts by copying the nearest existing one, which is how it happened the first time. Tests cover the part no amount of looking at a screen verifies - a QR that looks fine at 96 dpi on a monitor can be unreadable at 203 dpi on half-inch stock. |
||
|
|
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. |
||
|
|
13e6e039fe |
ADR-015: where a site's own data is allowed to live
ShopDB is a product several sites adopt, developed at one site against that site's live fleet, so every feature arrives carrying West Jefferson's hostnames, networks and share paths. A scan of plugins/ and shopdb/ returns 19 site literals, of which about 8 are a shipped default or a hardcode rather than a comment: a kiosk URL baked into a PowerShell payload a sister site cannot override, a WJ UNC path as the backup share default, and device.geaerospace.net shipped as a SETTING DEFAULT - inherited silently by anyone who adopts. The mechanisms already existed; the rule did not, so each value landed wherever was convenient. The rule: a setting with a NEUTRAL default first, a site-namespaced directory second, seed data third. Blank must be a working state. Fleet-wide vocabulary such as the gea-shopfloor-* imaging types is not site data and is out of scope - it already has a per-site override. Enforced by an addition to check-naming-and-style.sh, which CI already runs, in REPORT-ONLY mode. Making a 19-item backlog a hard gate the day it lands blocks every unrelated commit until someone clears it; SITE_LITERALS_ENFORCE=1 turns it into a gate once the listed hardcodes are done. The ADR carries that backlog, including that the display FQDN domain is defined three times in three files. |
||
|
|
2c415a1712 |
fix(installer): correct a false security claim, and clear the should-fix list
CLIENT IP / SPOOFABILITY. docs/geenforce-api-cutover.md claimed that removing the IIS rewrite rule made the allowlist fail closed and that it does NOT become spoofable. The opposite is true. IIS never sets X-Forwarded-For on its own; the rule is the only thing that does. Remove it and IIS still forwards whatever X-Forwarded-For the CALLER sent, waitress trusts it because it arrives from 127.0.0.1, and remote_addr becomes attacker-controlled - so a token-less caller can fetch manifests from anywhere on the network. The document and the _trusted_client_ip docstring now say so, waitress runs with --trusted-proxy-count=1, and stage 5 checks the rule is actually live rather than assuming it. The wizard question is rephrased to something an operator can verify with their network team instead of guessing at. NON-ASCII. The style gate only ever checked .py/.vue/.js/.ts, so documentation accumulated em-dashes, arrows and box-drawing characters against this repo's own convention - including in files added this week. Cleaned, and the gate now uses INCLUDES_ALL so Markdown, JSON and YAML are covered. PLUGIN DEFAULTS. The wizard pre-ticked measuringtools and printedparts, both of which ship default_enabled=false, so every site taking the defaults installed and enabled them against their manifests. Inno has no JSON parser so the list must be hardcoded, but tests/test_installer_defaults.py now fails when it drifts. UPGRADES. The payload copy merges, so a plugin dropped from a site's profile kept its code forever - which defeats a lean build and leaves core's optional-import guards succeeding for a plugin the site no longer has. Stale plugin directories are now deregistered and removed before the copy. add-plugin used 'plugin install', which for the five default_enabled=false plugins left them installed but DISABLED - and printed a green success line anyway. It now goes through apply-profile, and the success line is gated on the exit code. Invoke-Flask records its own exit status, because $LASTEXITCODE keeps a stale value when flask.exe is missing and no native command runs. CHARSET. The utf8mb4 compiler hook lived inline in migrations/env.py, so it covered the CORE chain only: plugin baselines inherited the server default, which on a latin1 server means two charsets in one database. It is now shopdb/utils/mysql_charset.py, imported by both, and preflight reports the database's default charset. BACKUP HONESTY. The dump was described as 'all of your asset data'. Uploaded branding and floor-map images live in instance\ on disk, not in the database, so a restore from the .sql alone comes back with no map. backup now archives instance\ alongside it and says both are needed. VERSIONING. AppVersion was hardcoded at 0.9.0 while the product, the frontend and the newest tag said 0.7.0 - and 0.9.0 collides with a retired contract version. Both builders now generate version.iss from shopdb/__init__.py. Smaller: rollback overwrites .env before deleting it, as uninstall already did; appcmd unlocks are scoped to this site's location rather than server-wide, with the wide unlock as a fallback; DEVELOPMENT-SETUP says Python 3.14; the README plugin list gains printedparts; prune-schema --force is documented as first-provisioning-only; HTTPS is documented as not-the-default with the steps to add it; the DBA SQL is on the wizard's database page; the features page says unticking does not remove an installed feature; and the installer README states that bundle-lock cannot vouch for the exe itself - that needs signing or an out-of-band hash, neither of which is wired up. |
||
|
|
13d831eb90 |
feat(installer): require the wheelhouse to satisfy requirements.txt, and lock the real payload
The lock records what IS in the wheelhouse, not what the application NEEDS, so an incomplete wheelhouse was locked, blessed and shipped - and only failed on an air-gapped server. That is not hypothetical. Assembling the wheelhouse anywhere other than Windows silently omits colorama, a win32-only dependency of click, because pip evaluates environment markers against the machine doing the downloading rather than the machine being targeted. The bundle built here was short exactly that one wheel. Both verifiers now cross-check wheels/ against the staged requirements.txt, ignoring markers, since a requirement guarded by sys_platform == 'win32' is precisely the one that must be present. Names are normalised to PEP 427 wheel form, so mysql-connector-python matches mysql_connector_python. bundle-lock.json is the first real lock: 42 files, cp314/win_amd64 - 39 wheels, Python 3.14.6, HttpPlatformHandler 1.2 and URL Rewrite. MySQL is absent and optional; a site choosing the bundled-database option adds it and re-locks. The naming gate now skips the installer's build output. It contains a staged copy of the application plus a second SPA build under dist-subpath, which --exclude-dir=dist does not match, so a staged bundle failed the gate on vendored minified JS nobody in this repository wrote. |
||
|
|
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). |
||
|
|
c6a1e07a6c |
ADR-013 Phase 4: lint plugin-frontend imports (self-contained rule)
The naming/style check now fails a plugin frontend (plugins/<name>/frontend/) that imports with an escaping ../../ or another plugin's path. Plugin frontends must reach core only through the @/ alias and otherwise import only their own tree, so a per-site build can drop a plugin cleanly. All 14 plugin frontends pass. |
||
|
|
ce7259c17a |
Phase 0: lock platform contract, naming convention, and style enforcement
Establishes the framework's foundation as a multi-site adoptable platform. ADRs (migrations/adr/): - ADR-001 (ACCEPTED): Asset is the platform contract; Machine retires. Three relationship types (partof, controls, connectedto) with free-text label, position-resolution chain (asset > related > location), hierarchical locations, sibling-bay propagation. - ADR-002 (ACCEPTED): Plugin contract semver via __contract_version__. - ADR-003 (ACCEPTED): Hybrid plugin distribution (in-tree bundled + filesystem-based external). - ADR-004 (ACCEPTED): Per-site instances, not multi-tenant. - ADR-005 (ACCEPTED): Equipment plugin (manufacturing) split from measuringtools plugin (metrology). Subtype-table pattern for protocol data (FOCAS, CLM, MTConnect). - ADR-006 (ACCEPTED): Plugin collector contract via get_collector_schema hook with API-key auth and identity-based upsert. Naming convention v1 (CONTRIBUTING.md): - DB tables/columns: lowercase concatenated, no underscores or dashes - DB-mirrored Python/JS variables match column names exactly; pure code follows host-language convention (PEP 8 / camelCase) - Closed acronym allowlist (universal + shop-floor domain), banned shorthand list with suffix exception (printers_bp etc allowed) - Plain ASCII everywhere: chat, docs, comments, string literals Style enforcement (scripts/check-naming-and-style.sh): - Pre-commit-runnable check script: non-ASCII, banned shorthand, snake_case DB names, snake_case API params in frontend - Fixes 14 violations across 11 files (Unicode arrows, snake_case params, ctx -> canvasContext, res -> response, req -> request_obj) Project state (CLAUDE.md, README.md, frontend/CLAUDE.md): - De-staled CLAUDE.md to reflect actual current state - README unifies DB story (MySQL canonical, SQLite test-only) - frontend/CLAUDE.md points at root convention Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |