Five defects, four of them mine from 0.11.0, found by using the feature.
THE SERIOUS ONE: ShopFloorMap never checked a marker's level. It skipped null
coordinates and drew everything else on whatever blueprint was displayed, so
level 1 markers appeared on level 2 - the exact failure ADR-017 exists to
prevent, in the one component that draws the map. The build gate did not catch it
because that rule checks files EMITTING mapx, not the component consuming it.
Markers are now filtered to the drawn level, and a position with no level is
omitted rather than approximated.
The map page had no way to choose a level at all. It read currentlevelid only to
title the PDF, so a second floor was unreachable from the viewer that most people
use. Adds a level selector (hidden when a site has one level), passes it to the
map, and switches the drawing, the bounds, the coordinate space and the markers
together - swapping the image without the bounds would place every marker against
the wrong scale.
Searching the map now follows results across levels: a search whose matches are
all on another floor showed an empty map while the filter counted them.
Floor map settings had NO height input - only width - so a level's native size
could not be set even while empty, which is the one time it is editable. Both
fields are there now, and size is editable on a level that has markers, because
refusing it blocked the case the feature was built for: a new blueprint of new
dimensions on a floor already full of markers. It confirms first and points at
landmark recalibration.
Search results differed between the sidebar box and the results-page box:
- 14 of 16 searchers truncated with .limit() and no ORDER BY, so the database
could return a DIFFERENT subset of matching rows for the same query. Every
searcher now ends in a total order (display key plus primary key).
- Searching a term already in the URL was a duplicate navigation the router
aborts, so the route watcher never fired and the button did nothing. The
sidebar never hit this, because it always navigates from another page - which
is why the two boxes appeared to disagree.
Also removes a scrollbar from both map pages. They subtracted 2rem and 40px from
100vh for the page chrome, which is really 90px of padding on .main-content, so
each overflowed by the difference. The padding is now a CSS variable both the
layout and the pages read. Measured in the browser before and after: 1058 vs a
1000px viewport, now 1000.
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.
Global search did a single ilike('%CSF Roles%'), so any query with more than one
word required the exact contiguous phrase and usually returned nothing. Add
_word_match: split the query into words and AND them (OR across the searched
columns per word), so 'CSF Roles' matches a record with both words in any field,
any order. Applied across every domain (assets, applications, KB, employees
[selfhosted + external HR], notifications, hostnames, IP, custom fields,
vendor/model/type). External HR path uses a parameterized per-word LIKE.
Global-search rows built the plugin detail URL from a hardcoded url_map of
plugin routes in core. Now core prefers a plugin's declared
get_asset_presentation route (ADR-010), substituting the core assetid via the
plugin's by-asset resolver; types that have not declared fall back to the legacy
id-keyed map, so nothing breaks. Measuring tools (which declare the route) link
through it now; machines/PCs/printers/network migrate off the hardcode as they
add a by-asset route + declaration. Presentation map is collected once per
search (cached on flask.g). 2 consumer tests; 26 search tests green.
_search_employees always queried the external HR database (employee_connection),
so a site running the selfhosted employee directory (the app-owned
directoryemployees table) got zero employee results - searching an SSO or name
found nothing, and there was no way to reach the employee profile. Made it
mode-aware via the employee_directory_mode setting: selfhosted -> query the
DirectoryEmployee table (lazy, plugin-guarded); external -> the HR DB as before.
Verified: SSO 210009518 -> Jeff Pierce -> /employees/210009518; name "Pierce"
-> Pierce Cox, Andy Pierce, Jeff Pierce.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collector: the computers collector schema gains defaultprinter and
printers; apply_collector_payload resolves each reported identifier to
a printer asset (windowsname/hostname/sharename/assetnumber/IP,
first-hit case-insensitive) and idempotently syncs relationships -
defaultprinter (directional) for the default, connectedto for the
rest. Collector-created rows are tagged so a re-report archives dropped
links while manual relationships are never touched; unresolved
identifiers warn instead of failing. Both PC and printer detail pages
show the links via the shared relationships card (no frontend change).
GE-Enforce Win32_Printer collection snippet documented.
Searchable custom fields: a per-field searchable flag (migration 7d24);
global search matches custom-field values on flagged active fields and
routes each hit to the asset detail page, reusing the existing
(type,id) dedupe and search_<type>_enabled domain filter. Searchable
toggle on the Custom Fields settings page.
822 tests pass; both verified live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The measuringtools plugin was missing from two cross-cutting surfaces:
the asset-identifier matrix (no measuring_tool column or per-type
keys - gauge lab reference is their primary identifier) and global
search (results fell to a generic URL and gaugelabreference was never
searched). Measuring tools now have identifier toggles, gated
gauge-lab and maintenance-reference fields on their form and detail,
a search domain toggle, gage-tag search, and proper labels, routes,
and filter chips in search results.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The equipment plugin is now the machines plugin, ending the UI-vs-code
vocabulary split while the contract is pre-1.0 and nothing external
depends on the old names.
- plugins/equipment -> plugins/machines: manifest, class, /api/machines,
machines.* permissions, registry key (with an auto-migrating load shim
for existing installs).
- Tables: equipment -> machines (equipmentid -> machineid) and
equipmenttypes -> machinetypes, renamed in the plugin's own migration
chain (machines0002rename), idempotent for both upgrading and fresh
installs.
- The legacy core machinetypes lookup actually types the vendor MODELS
catalog, so it is renamed losslessly to modeltypes
(models.modeltypeid, /api/modeltypes, Model Types settings page)
rather than collapsed, freeing the machinetypes name. Core migration
7d17_machines_rename also flips data in place: assettypes row
equipment -> machine, auditlog entitytype, identifier_/search_
settings keys, permission rows, and renames alembic_version_equipment.
- Frontend: machinesApi/modeltypesApi, item.machine response shape,
assettype value compares 'equipment' -> 'machine' (map, search,
custom fields, relationships), routes machines.js with plugin gating
retagged, /print/machine-badge, Machine Types (subtypes) and Model
Types (catalog) settings pages, machines-by-type report id.
- Docs swept; ADRs left as history per the authoring rule.
Upgrade: flask db upgrade then flask plugin upgrade-all.
Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models
retyped, zero equipment tables remain); fresh scratch-MySQL install
produces the new names; 341 tests green; naming/style green; frontend
builds; live E2E on machines list/detail, PC relationships, map,
reports, and both settings pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make the app distributable to other GE Aerospace sites (one self-hosted
instance per site, ADR-004). GE values remain the shipped defaults; every
site-specific behavior is now a Setting an admin can change in the UI.
Settings-driven site config:
- Branding: site/QR/badge logos, favicon, primary color (upload endpoints
mirror the map-blueprint pattern; new Settings > Branding section).
- ServiceNow: search/incident/change URL templates ({ticket}), ticket
prefixes, enable toggle. Defaults point at the current
geaerospaceqa.service-now.com global search. Disabled = plain-text tickets.
- Employee-id regex (employeeid_pattern), printer hostname template,
QR label targets (qr_target_printer / qr_target_usb, blank = asset page,
else URL template with placeholders), usb_label_style (barcode|qr).
- West Jefferson floor-plan PNGs removed from the tree; generic placeholder
ships as the map default and sites upload their own blueprint.
Security closeout:
- dashboarddefaults writes now require admin.
- Collector: generic error messages (no str(exc) leak); API key accepted
via X-API-Key header only (BREAKING: querystring api_key removed).
- IP-based login rate limiting (AUTH_RATELIMIT_* knobs) atop account lockout.
- Setting.set() creation race fixed (IntegrityError retry).
Release engineering and docs:
- __version__ 0.5.0 (distinct from __contract_version__, ADR-007),
CHANGELOG.md, Gitea Actions CI config, frontend version aligned.
- One wizard-first install story across README/DEPLOY; new CONFIG.md,
UPGRADE.md, BACKUP-RESTORE.md; CLAUDE.md and ROADMAP de-staled.
- Dockerfile multi-stage build now bundles the frontend; compose binds
MySQL to 127.0.0.1; stale database/schema.sql and one-off SQL removed.
Debt and fixes:
- .query.get() -> db.session.get() sweep; datetime.utcnow() removed
(naive-UTC via timezone-aware now); users.py on authz decorators.
- Fixed 4 stale tests (slides feed shape, shopfloor splitperemployee,
plugin contract purity) and the USB label page field mapping (both usb
modes emit the cmmc shape: device_id/device_desc).
- Health endpoint reports the real version.
248 tests pass; naming/style check green; frontend builds; fresh-DB
flask db upgrade + seeds verified; QR targets verified by decoding
rendered codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lets an admin choose which content types appear in global search results,
independent of whether the owning plugin is enabled (the existing
_require_enabled gating was all-or-nothing per plugin).
- settings.py: SEARCH_DOMAINS const (9 result types: application, knowledgebase,
employee, equipment, computer, printer, network_device, notification, subnet)
+ seed keys search_<type>_enabled (boolean, default true) in
build_default_settings (covers API seed + CLI).
- search.py: global_search loads disabled types in one query (category 'search')
and filters the deduped results by type before counts/truncation. Missing key
= enabled.
- SystemSettings.vue: "Global Search" section, one toggle per domain (mirrors the
identifier pattern; create-on-404 fallback so an un-reseeded deploy still works).
- Tests: domain included by default, disabled domain excluded, seed creates the
9 keys.
166 tests pass, naming green, build green. Verified live: toggling
search_knowledgebase_enabled off drops knowledgebase from search counts and back
on restores it. Dev DB seeded (9 keys).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First feature extracted from core into a plugin per "plugin is the product",
mirroring the notifications plugin. KB is a NON-asset plugin: it contributes a
model + blueprint + nav item but registers no AssetType.
- plugins/knowledgebase/: manifest.json (api_prefix /api/knowledgebase, no deps),
models/ (KnowledgeBase, contract-pure imports via shopdb.api), api/ (the
blueprint, same routes/prefix so the frontend is unchanged), plugin.py
(get_blueprint + get_models + get_navigation_items).
- De-cored: removed shopdb/core/models/knowledgebase.py + api/knowledgebase.py,
their __init__ exports, and 'knowledgebase' from CORE_BLUEPRINT_NAMES; dropped
the hardcoded KB nav item from dashboard.py (now via the plugin nav hook).
- search.py and reports.py lazy-import KnowledgeBase from the plugin and degrade
gracefully (search skips via _require_enabled when disabled; kb-popularity
report returns 503 if the plugin is absent).
- Registered in instance/plugins.json (enabled).
The knowledgebase table stays in the core Alembic chain (bundled-plugin schema
folded into core, ADR-004); the model just maps it. KB was never in the
shopdb.api contract surface, so no __contract_version__ bump.
Pinned with characterization tests first (test_knowledgebase.py); they pass
unchanged against the plugin blueprint. 163 tests pass, naming green, app boots
7 bundled plugins, KB endpoint/nav/search verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verification audit (re-run of the 6 skill lenses) confirmed the prior fixes hold
and surfaced a few misses:
Security (HIGH):
- search.py _check_smart_redirect still opened a raw pymysql connection with
root/rootpassword (reachable on any 9-digit SSO query). Now uses the shared
env-backed employee_connection helper.
- Deleted dead shopdb/core/services/employee_service.py (zero importers; carried
another root/rootpassword literal). No hardcoded credentials remain in app
logic; config.py dev defaults stay gated by ProductionConfig.validate.
Dead hook:
- get_services was implemented by the printers plugin but had no consumer (docs
claimed otherwise). Added PluginManager.get_service(name) that resolves a
service from enabled plugins; updated PLUGIN-HOOKS.md.
Tests:
- search disabled-plugin exclusion (the high-value gap): enabled plugin's
hostname appears, disabled plugin's hostname drops out (searched by a hostname
distinct from assetnumber so only the gated domain can match).
- get_service consumer test (unknown name -> None).
Simplify:
- Extract the triplicated GE_LOGO_SVG + loadLogo + drawLogoOverlay into shared
frontend/src/views/print/qrLogo.js (renderQrDataUrl); both QR views use it.
- applications.py: lift the misplaced pagination import to the top; drop unused
Computer unpacking in the 3 endpoints that only touch ComputerInstalledApp.
154 tests pass, naming/style green, app boots, QR render verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Architectural pass from the skill review ("plugin is the product" boundary).
Core no longer imports plugin models at module load (was a hard import-time
dependency that broke core if the computers plugin was absent/disabled):
- collector.py, applications.py, reports.py: lazy + guarded imports of the
computers plugin models. Endpoints that need install-tracking now return 503
when the plugin is absent instead of failing at import.
Search honors runtime enable/disable:
- search.py: _require_enabled(name) raises ImportError for a disabled plugin,
so each plugin-scoped block skips it (a disabled plugin's rows leave search).
- Replace hardcoded root/rootpassword employee-DB connection in _search_employees
with the shared env-backed employee_connection helper.
Plugin hooks (integrating-plugin-hooks: every hook needs a consumer):
- get_dashboard_widgets: add the consumer GET /api/dashboard/widgets (5 plugins
already implemented the hook; it had none). Skips disabled, isolates in prod.
- get_searchable_fields: REMOVED. Zero plugins implemented it and there was no
consumer; global search is a core concern over the asset model. Contract
reduction, __contract_version__ 0.3.0 -> 0.4.0.
Docs/contract: PLUGIN-HOOKS.md (widgets consumer note, searchable-fields removal,
0.4.0), PLUGIN-QUICKSTART.md, ADR-001 hook list. Tests: widgets endpoint
aggregate + disabled-skip; contract tests for the removed/added hooks.
151 tests pass, naming/style green, app boots all 6 plugins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Search: refactor into modular helpers, add IP/hostname/notification/
vendor/model/type search, smart auto-redirects for exact matches,
ServiceNOW prefix detection, filter buttons with counts, share links
with highlight support, and dark mode badge colors.
Map: fix N+1 queries with eager loading (248->28 queries), switch to
canvas-rendered circleMarkers for better performance with 500+ assets.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add equipmentApi and computersApi to replace legacy machinesApi
- Add controller vendor/model fields to Equipment model and forms
- Fix map marker navigation to use plugin-specific IDs (equipmentid,
computerid, printerid, networkdeviceid) instead of assetid
- Fix search to use unified Asset table with correct plugin IDs
- Remove legacy printer search that used non-existent field names
- Enable optional JWT auth for detail endpoints (public read access)
- Clean up USB plugin models (remove unused checkout model)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New Plugins:
- USB plugin: Device checkout/checkin with employee lookup, checkout history
- Notifications plugin: Announcements with types, scheduling, shopfloor display
- Network plugin: Network device management with subnets and VLANs
- Equipment and Computers plugins: Asset type separation
Frontend:
- EmployeeSearch component: Reusable employee lookup with autocomplete
- USB views: List, detail, checkout/checkin modals
- Notifications views: List, form with recognition mode
- Network views: Device list, detail, form
- Calendar view with FullCalendar integration
- Shopfloor and TV dashboard views
- Reports index page
- Map editor for asset positioning
- Light/dark mode fixes for map tooltips
Backend:
- Employee search API with external lookup service
- Collector API for PowerShell data collection
- Reports API endpoints
- Slides API for TV dashboard
- Fixed AppVersion model (removed BaseModel inheritance)
- Added checkout_name column to usbcheckouts table
Styling:
- Unified detail page styles
- Improved pagination (page numbers instead of prev/next)
- Dark/light mode theme improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Flask backend with Vue 3 frontend for shop floor machine management.
Includes database schema export for MySQL shopdb_flask database.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>