The comment explaining the concatenation fix spelled the example hostname out
in full, which tripped the publication scrub - the same trap the surrounding
comment warns about, since the scrub greps this file for the very strings the
patterns are assembled from. Reworded with a neutral example.
Re-verified after the reword by planting a violation and watching the gate fail.
The patterns are assembled from fragments so the published script does not
contain the strings it greps for. They used the apostrophe-escape form
('a'\''b'), which EMBEDS a literal apostrophe, instead of the concatenation form
('a''b'), which joins them. Every pattern therefore carried a stray quote in the
middle and could never match a line of real code, so the check passed everything
from the day it was written.
Verified the only way that means anything: plant a violation, watch it fail,
revert. Fixed, it immediately found 25 real hits.
Among them, in seven places, the default ServiceNow URLs - one organisation's QA
tenant, shipped with the integration ENABLED. Any site that had not configured
ServiceNow still rendered ticket links, pointing its technicians at a tenant
they cannot reach; an adopting site would have been pointed into somebody else's
instance entirely. There is no sensible cross-site default for "which ServiceNow
is yours", so the URLs ship blank and the integration ships off, matching the
Zabbix block that got this right.
A named SaaS tenant is as site-specific as a hostname, so it joins the patterns.
Colocated .spec.js files are excluded the same way tests/ already is: sample data
naming a facility is not a shipped literal.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.