From aa6db941794eeb98106b238a492c19360fdc6396 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 17 Aug 2026 14:59:44 -0400 Subject: [PATCH] Fix the levels viewer: markers were drawn on whichever plan was showing 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. --- frontend/src/assets/style.css | 6 +- frontend/src/components/ShopFloorMap.vue | 35 ++++++++++ frontend/src/views/MapEditor.vue | 2 +- frontend/src/views/MapView.vue | 64 +++++++++++++++++-- frontend/src/views/SearchResults.vue | 13 +++- .../src/views/settings/FloorMapSettings.vue | 63 +++++++++++++++--- shopdb/core/api/search.py | 41 ++++++++---- 7 files changed, 189 insertions(+), 35 deletions(-) diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css index 364b270..36e9d98 100644 --- a/frontend/src/assets/style.css +++ b/frontend/src/assets/style.css @@ -33,6 +33,10 @@ --sidebar-bg: #00003d; --sidebar-text: #ffffff; --sidebar-width: 250px; + /* Vertical chrome around .main-content. A full-height page subtracts these; + hardcoding a different number is what put a scrollbar on the map. */ + --main-pad-top: 20px; + --main-pad-bottom: 70px; /* Hover variants */ --secondary-dark: #82503f; @@ -243,7 +247,7 @@ h1, h2, h3, h4, h5, h6 { .main-content { flex: 1; margin-left: var(--sidebar-width); - padding: 20px 10px 70px 10px; + padding: var(--main-pad-top) 10px var(--main-pad-bottom) 10px; overflow-x: hidden; } diff --git a/frontend/src/components/ShopFloorMap.vue b/frontend/src/components/ShopFloorMap.vue index b7de1c0..cf6e9cf 100644 --- a/frontend/src/components/ShopFloorMap.vue +++ b/frontend/src/components/ShopFloorMap.vue @@ -437,8 +437,18 @@ function renderMarkers() { overlayLayers.forEach(layer => layer.remove()) overlayLayers = [] + // Markers belong to ONE level (ADR-017). This is the drawing being shown, and + // anything positioned against a different one is not drawn on it. + const drawnLevel = drawnLevelId() ?? null + props.machines.forEach(item => { if (item.mapx == null || item.mapy == null) return + // A marker from another level, placed on THIS blueprint, looks entirely + // correct and points at the wrong part of the building - so it is omitted + // rather than approximated. A position with no level at all is omitted for + // the same reason: the map editor lists both, badged, so they can be fixed + // rather than silently misplaced here. + if ((item.levelid ?? null) !== drawnLevel) return // Transform coordinates (database Y is top-down, Leaflet is bottom-up) const leafletY = MAP_HEIGHT - item.mapy @@ -599,6 +609,31 @@ watch(() => props.theme, (newTheme) => { } }) +// Switching level changes the drawing, the coordinate space AND which markers +// belong on it. All three move together: the size is what marker coordinates +// mean, so swapping the image without the bounds would place every marker +// against the wrong scale, and keeping the markers would show the previous +// floor's assets on this floor's plan. +watch(() => drawnLevelId(), (levelid) => { + if (!map || !imageOverlay) return + + MAP_WIDTH = dimensionsFor(levelid).width + MAP_HEIGHT = dimensionsFor(levelid).height + const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]] + + const url = blueprintUrlFor(props.theme, levelid) + // A level with no blueprint is left blank rather than showing the previous + // one, which would be a floor plan labelled as somewhere it is not. + imageOverlay.setUrl(url || '') + imageOverlay.setBounds(bounds) + + map.setMaxBounds(bounds) + map.setView([MAP_HEIGHT / 2, MAP_WIDTH / 2], map.getZoom()) + + renderMarkers() + loadOverlays() +}) + onMounted(async () => { // Load this facility's blueprint + dimensions before building the map so // bounds and coordinate math use the right size. Falls back to defaults. diff --git a/frontend/src/views/MapEditor.vue b/frontend/src/views/MapEditor.vue index 5d978e4..48e0735 100644 --- a/frontend/src/views/MapEditor.vue +++ b/frontend/src/views/MapEditor.vue @@ -492,7 +492,7 @@ function cancelEdit() { .map-editor { display: flex; flex-direction: column; - height: calc(100vh - 40px); + height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom)); } .header-actions { diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue index f6b001d..724204a 100644 --- a/frontend/src/views/MapView.vue +++ b/frontend/src/views/MapView.vue @@ -12,6 +12,17 @@