Fix the levels viewer: markers were drawn on whichever plan was showing
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 3s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 8s

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.
This commit is contained in:
cproudlock
2026-08-17 14:59:44 -04:00
parent dd503be4ba
commit aa6db94179
7 changed files with 189 additions and 35 deletions

View File

@@ -23,6 +23,14 @@ logger = logging.getLogger(__name__)
search_bp = Blueprint('search', __name__)
# EVERY searcher below truncates with .limit(). An unordered LIMIT lets the
# database return a DIFFERENT subset of the matching rows between two identical
# requests - the same search run twice came back with different hits, which reads
# as the search being broken rather than as a missing ORDER BY. Each query
# therefore ends with a TOTAL order: a display key plus the primary key, so ties
# cannot reorder. Relevance is applied afterwards in Python, over a stable set.
def _word_match(query, *columns):
"""SQL clause matching rows that contain EVERY word of the query, each word
in any of the columns, in any order.
@@ -213,7 +221,7 @@ def _search_applications(query, search_term):
apps = Application.query.filter(
Application.isactive == True,
_word_match(query, Application.appname, Application.appdescription)
).limit(10).all()
).order_by(Application.appname, Application.appid).limit(10).all()
for app in apps:
relevance = 20
@@ -258,7 +266,8 @@ def _search_knowledgebase(query, search_term):
KnowledgeBase.appid.notin_(retired)),
_word_match(query, KnowledgeBase.shortdescription,
KnowledgeBase.keywords)
).limit(20).all()
).order_by(KnowledgeBase.clicks.desc(),
KnowledgeBase.linkid).limit(20).all()
for kb in kb_articles:
relevance = 10 + (kb.clicks or 0) * 0.1
@@ -378,7 +387,7 @@ def _search_assets(query, search_term):
_word_match(query, Asset.assetnumber, Asset.name,
Asset.serialnumber, Asset.notes,
Asset.gaugelabreference, Asset.maintenancereference)
).limit(15).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
for asset in assets:
relevance = 15
@@ -421,7 +430,7 @@ def _search_measuringtools(query, search_term):
Asset.isactive == True,
_word_match(query, Asset.assetnumber, Asset.name,
Asset.serialnumber, Asset.gaugelabreference)
).limit(15).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(15).all()
for asset in assets:
relevance = 15
@@ -461,7 +470,8 @@ def _search_usbdevices(query, search_term):
USBDevice.isactive == True,
_word_match(query, USBDevice.serialnumber, USBDevice.assetnumber,
USBDevice.label, USBDevice.productname)
).limit(10).all()
).order_by(USBDevice.serialnumber,
USBDevice.usbdeviceid).limit(10).all()
for device in devices:
relevance = 20
@@ -507,7 +517,8 @@ def _search_printeditems(query, search_term):
PrintedItem.isactive == True,
_word_match(query, PrintedItem.itemcode, PrintedItem.gagelabtag,
PrintedItem.itemname, PrintedItem.itemdescription)
).limit(10).all()
).order_by(PrintedItem.itemcode,
PrintedItem.printeditemid).limit(10).all()
for item in items:
relevance = 20
@@ -563,7 +574,7 @@ def _search_customfields(query, search_term):
CustomField.searchable == True,
CustomField.isactive == True,
_word_match(query, CustomFieldValue.value),
).limit(15).all()
).order_by(Asset.assetnumber, CustomField.fieldid).limit(15).all()
for asset, field in rows:
result = _get_asset_result(asset, query, relevance=40)
@@ -583,7 +594,8 @@ def _search_by_ip(query, search_term):
).options(
joinedload(Communication.asset).joinedload(Asset.assettype),
joinedload(Communication.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(Communication.ipaddress,
Communication.communicationid).limit(10).all()
seen_assets = set()
for comm in comms:
@@ -643,7 +655,7 @@ def _search_hostnames(query, search_term):
).options(
joinedload(Computer.asset).joinedload(Asset.assettype),
joinedload(Computer.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(Computer.hostname, Computer.computerid).limit(10).all()
for comp in computers:
if comp.asset and comp.asset.isactive:
@@ -666,7 +678,7 @@ def _search_hostnames(query, search_term):
).options(
joinedload(Printer.asset).joinedload(Asset.assettype),
joinedload(Printer.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(Printer.hostname, Printer.printerid).limit(10).all()
for printer in printers:
if printer.asset and printer.asset.isactive:
@@ -689,7 +701,8 @@ def _search_hostnames(query, search_term):
).options(
joinedload(NetworkDevice.asset).joinedload(Asset.assettype),
joinedload(NetworkDevice.asset).joinedload(Asset.location),
).limit(10).all()
).order_by(NetworkDevice.hostname,
NetworkDevice.networkdeviceid).limit(10).all()
for device in devices:
if device.asset and device.asset.isactive:
@@ -773,7 +786,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True,
_word_match(query, Vendor.vendor, Model.modelnumber,
MachineType.machinetype)
).limit(10).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in machine_assets:
results.append(_get_asset_result(asset, query, 30))
@@ -801,7 +814,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True,
_word_match(query, Vendor.vendor, Model.modelnumber,
PrinterType.printertype)
).limit(10).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in printer_assets:
results.append(_get_asset_result(asset, query, 30))
@@ -827,7 +840,7 @@ def _search_vendor_model_type(query, search_term):
Asset.isactive == True,
_word_match(query, Vendor.vendor,
NetworkDeviceType.networkdevicetype)
).limit(10).all()
).order_by(Asset.assetnumber, Asset.assetid).limit(10).all()
for asset in netdev_assets:
results.append(_get_asset_result(asset, query, 30))