From 741dda5be714cca7165eb47f6a4e094a78da350c Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 17 Aug 2026 13:35:57 -0400 Subject: [PATCH] Document buildings and levels where the old single-map model was still taught The 0.11.0 release changed what a map position means and six documents still described the model it replaced. Each of these could have caused a real mistake rather than being merely out of date: - IMPORT-API mapped legacy mapleft/maptop to mapx/mapy with no mention of the level, so a scripted import - including the classic-ASP one still to run against production - would have produced markers the map shows as "level unknown". It now maps levelid too and says how to resolve the default level. - API-REFERENCE enumerates the unauthenticated surface in full, because that is what a deploy reviewer reads, and the three public /api/maplevels reads were missing from it. Also records why the write split is asymmetric: repositioning needs assets.edit, creating a level needs admin, since a level's dimensions are the coordinate space every marker on it is expressed in. - CONFIG still presented the four map_* settings as live, telling the reader to re-upload a blueprint in a settings page that no longer drives the map. They are marked superseded and kept for downgrade. - UPGRADE gained a 0.11.0 section: nothing moves on screen, and replacing a blueprint with one of different dimensions moves every marker on that level, so recalibrate from landmarks rather than editing width and height. - PLUGIN-HOOKS now states that a map overlay keys on assetid and must not return coordinates or a level - a second copy of a position is one that can disagree. Adds FLOOR-MAP.md, the operator's page: loading a plan, placing markers, and what to do when the plan changes, with the reasoning left in ADR-017. START-HERE routes to it from the new-site path, and specifically as the page to read BEFORE a floor plan changes. --- docs/API-REFERENCE.md | 12 +++- docs/CONFIG.md | 17 ++++-- docs/FLOOR-MAP.md | 135 ++++++++++++++++++++++++++++++++++++++++++ docs/IMPORT-API.md | 20 ++++++- docs/PLUGIN-HOOKS.md | 6 ++ docs/START-HERE.md | 3 + docs/UPGRADE.md | 30 ++++++++++ 7 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 docs/FLOOR-MAP.md diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md index 231dd25..c31ec47 100644 --- a/docs/API-REFERENCE.md +++ b/docs/API-REFERENCE.md @@ -96,6 +96,13 @@ Two auth patterns dominate the reads: Every mutating endpoint (POST / PUT / PATCH / DELETE) requires a JWT and is gated by `require_role` or `require_permission`; none are public. +The map write surface splits along that line. Repositioning markers +(`/api/mappositions/*`) needs `assets.edit` - the same permission the old +per-asset `PUT /api/assets/` always needed, so bulk editing grants nobody +anything new. Creating buildings and levels or uploading a blueprint +(`POST|PUT|PATCH|DELETE /api/maplevels/*`) needs `role:admin`, because a level's +dimensions are the coordinate space every marker on it is expressed in. + Two GETs deliberately break the optional-auth convention and require a permission, because what they return is not asset metadata but a file that carries value or liability of its own: @@ -115,7 +122,10 @@ like any other mutation. | `POST /api/auth/login` | Obtain a JWT. | | `GET /api/setup/needs-admin` | First-run check: does the instance have zero users. | | `POST /api/setup/create-admin` | First-run only; creates the first admin, then 403s forever. | -| `GET /api/settings/map-blueprint/` | Serve the floor-map blueprint image. | +| `GET /api/settings/map-blueprint/` | Serve a floor-map blueprint image. Pre-0.11.0 single-map path, retained; new uploads are served per level below. | +| `GET /api/maplevels` | Buildings and their levels, each with blueprint paths, native pixel size and marker count. Public because the printer-installer map draws a floor plan before anyone logs in; it exposes level names and image paths, nothing about assets. | +| `GET /api/maplevels/` | One level's name, building, blueprints and size. | +| `GET /api/maplevels//blueprint/` | Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script. | | `GET /api/settings/branding/` | Serve site branding assets (logo, etc.). | | `GET /api/settings` and `GET /api/settings/` | Read-only, and only the public allowlist: the `branding` and `map` categories, a few named site keys, plus any key a plugin declares `public` in `get_settings_defaults` (e.g. `printedparts_label_prefix`, which the logged-out parts kiosk renders). Every other key answers 404 to an anonymous caller. | | `GET /api/models/image/` | Serve a model image. | diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 5c18c06..ac04e9d 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -196,12 +196,21 @@ The batch generator at `/print/asset-label-batch/` (reached from the ### map +**SUPERSEDED as of 0.11.0.** A blueprint and its pixel dimensions are properties +of a LEVEL now, not of the site (ADR-017), because a site can have more than one +building and a building more than one floor. Manage them in +**Settings > Buildings and levels**, or through `/api/maplevels`. + +These four keys are still present so that downgrading finds the blueprint it had, +and the levels migration copied their values onto the first level. Editing them +changes nothing on the map. + | Key | Default | Notes | |-----|---------|-------| -| `map_blueprint_light` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (light theme). Re-upload your own in Settings > Floor Map. | -| `map_blueprint_dark` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (dark theme). | -| `map_width` | `3300` | Blueprint native width in pixels. | -| `map_height` | `2550` | Blueprint native height in pixels. | +| `map_blueprint_light` | `/static/images/floorplan-placeholder.svg` | Superseded by `maplevels.blueprintlight`. Retained for downgrade only. | +| `map_blueprint_dark` | `/static/images/floorplan-placeholder.svg` | Superseded by `maplevels.blueprintdark`. | +| `map_width` | `3300` | Superseded by `maplevels.mapwidth`. | +| `map_height` | `2550` | Superseded by `maplevels.mapheight`. | ### integrations diff --git a/docs/FLOOR-MAP.md b/docs/FLOOR-MAP.md new file mode 100644 index 0000000..dce1aca --- /dev/null +++ b/docs/FLOOR-MAP.md @@ -0,0 +1,135 @@ +# The floor map: buildings, levels and marker positions + +The map shows where things physically are. This page is for whoever maintains it: +loading a floor plan, adding a second floor or a second building, and moving the +markers when the plan changes. + +The reasoning behind the model is in +[ADR-017](adr/ADR-017-buildings-and-levels.md). This page is the operator's view. + +## The model in three sentences + +A **building** holds one or more **levels**. A level owns a floor-plan image (one +per theme) and the pixel size of that image. A marker's position is an X and Y in +the pixels of ONE level, so every position names the level it belongs to. + +That last point is the whole design. Coordinates alone are meaningless: `1200,900` +is a spot on the ground floor and a different spot on the second floor. A marker +whose level is unknown shows as **level unknown** and is not drawn, because +drawing it on the default plan would look completely correct while pointing at +the wrong part of the building. + +## Loading a floor plan + +**Settings > Buildings and levels.** + +1. Add a building if you do not have one. A single-building site needs exactly + one, and it can be called anything. +2. Add a level. Give it a name people use out loud - "Ground floor", "Mezzanine", + "Second floor" - and a sort order that puts the list in the order someone + would walk it. Name and order are separate on purpose: levels are not reliably + numbered, and leaving gaps in the order lets you insert one later without + renumbering everything. +3. Upload the plan, per theme. A light-theme image is enough to start; if there + is no dark version, the light one is used in both. + +**The level takes its pixel size from the image you upload, while the level is +still empty.** Once markers are on it, an upload whose size does not match reports +the mismatch and changes nothing. That is deliberate - see below. + +Supported: PNG, JPEG, GIF, WEBP and SVG. An SVG is served with headers that stop +it executing as script, so a plan exported from a CAD tool is safe to use. + +## Placing markers + +Two ways, both fine: + +- **One at a time**, from the asset's own form: the map-position picker opens on + the level the marker is already on, and you can switch level inside the picker. + The saved position states which drawing it is on. +- **Many at once**, in the map editor. Filter to what you are working through - + unplaced, needs-review, or one level - search for a specific thing, place it, + and confirm. Placing a marker by hand counts as confirming it. + +A location can carry a position too, which acts as the default for assets in it. + +## When the floor plan changes + +This is the case that needs care, and the reason the tooling exists. + +**A new plan of the SAME dimensions**: upload it and you are done. Markers keep +their coordinates and still line up. + +**A new plan of DIFFERENT dimensions, or a real move**: the coordinates are pixels +of the old image, so every marker is now in the wrong place relative to the +drawing. Do NOT fix this by editing the level's width and height - that just +reinterprets every existing position against a new scale. + +Use **Recalibrate** in the map editor: + +1. Pick two or more **landmarks** - features you can find on both the old and the + new plan. Corners of the building, a doorway, a column. Two is the minimum; + three or four spread across the plan is better. +2. For each, give the old coordinates and the new ones. +3. Review the **dry run**. It lists every marker's old and new position and flags + anything that would land off the drawing. Nothing has changed yet. +4. Apply it. The previous positions are snapshotted first. + +The transform is worked out separately for the X and Y axes from your landmarks, +and never guessed from the image dimensions. That matters in the exact case this +was built for: a plan that gained a floor below it got taller without anything +being rescaled, and a size-derived scale would have stretched everything +vertically. + +After a recalibration every affected marker is marked **needs review**, because a +transform is a good starting guess and nothing in the coordinates can tell you +which machines actually moved. Work the needs-review filter, correct what is off, +and confirm as you go. + +## Undo + +Every bulk change snapshots the positions first, and a restore snapshots before +it restores, so an undo is itself undoable. The history is in the map editor: +what caused each snapshot, when, and whether it has been restored. A restore puts +back the level and the review state, not only the coordinates. + +Assets deleted since a snapshot are reported and skipped rather than failing the +whole restore. + +## Adding a second building or a second floor + +Add it, upload its plan, then move the markers that belong on it. In the map +editor, markers on other levels are badged, so you can see what still needs +moving. + +Do not stack two floors onto one tall image. It looks simpler and it makes the +level implicit in the Y coordinate - "above 2550 means upstairs" - which nothing +can validate and which breaks the moment either plan is redrawn. + +## Things that are not levels + +- **Locations** are where something is in words ("Cell 4", "Gage lab"). A level is + which drawing. A location can name a level, but they are different things. +- **Business units** are organisational, not physical. + +## Known limits + +- The printer-installer map (the standalone program that installs printers by + clicking a floor plan) has one plan compiled into it and does not know about + levels. It is accurate for the default level only. See + [PRINTER-INSTALLER](PRINTER-INSTALLER.md) section 6. +- The first-run setup wizard still asks for one site-wide blueprint; those + settings no longer drive the map. Use Settings > Buildings and levels. +- Map PDF export covers the level you are looking at. + +## For integrators + +`GET /api/maplevels` is unauthenticated - the printer installer draws a plan +before anyone logs in - and returns every building with its levels, each level's +blueprint paths, native size and marker count, plus which level is the default. +Writes require an admin. Repositioning is `/api/mappositions/*` and needs +`assets.edit`. Full parameters in [API-REFERENCE](API-REFERENCE.md) and the +generated spec at `/api/docs`. + +If you import positions from another system, send `levelid` with every +coordinate - see [IMPORT-API](IMPORT-API.md). diff --git a/docs/IMPORT-API.md b/docs/IMPORT-API.md index 9fed4d4..03a1786 100644 --- a/docs/IMPORT-API.md +++ b/docs/IMPORT-API.md @@ -190,12 +190,28 @@ Common `machines` columns -> core Asset fields (same for every target plugin): | `serialnumber` | `serialnumber` | | | `machinestatusid` | `statusid` | remap via `machinestatus` -> asset statuses | | `businessunitid` | `businessunitid` | remap via imported business units | -| `mapleft` | `mapx` | | -| `maptop` | `mapy` | | +| `mapleft` | `mapx` | pixels of a LEVEL; send `levelid` with it | +| `maptop` | `mapy` | as above | +| n/a | `levelid` | which floor plan the coordinates belong to (ADR-017) | | `machinenotes` | `notes` | | | `dateadded` | `createddate` | import mode only | | `lastupdated` | `modifieddate` | import mode only | +**A position needs its level.** `mapx`/`mapy` are absolute pixels in the native +coordinate space of ONE level (ADR-017), so an import that sends coordinates +without `levelid` produces markers the map renders as "level unknown" rather than +drawing them on the default level. A legacy source that predates levels has one +floor plan, so the right value is that site's default level: + +```bash +curl -s http://server/api/maplevels | python -c "import json,sys; print(json.load(sys.stdin)['data']['defaultlevelid'])" +``` + +Send that `levelid` on every row carrying coordinates. If an import already ran +without it, the levels migration's backfill is not retroactive - assign the rows +with `POST /api/mappositions/positions`, which requires `levelid` per row for +exactly this reason. + Per-plugin extension fields: - **computers** (`POST /api/computers`): `hostname` <- `machines.hostname`, diff --git a/docs/PLUGIN-HOOKS.md b/docs/PLUGIN-HOOKS.md index 8382116..e8850fe 100644 --- a/docs/PLUGIN-HOOKS.md +++ b/docs/PLUGIN-HOOKS.md @@ -369,6 +369,12 @@ class MeasuringToolsPlugin(BasePlugin): Consumed by `GET /api/pluginui/map-overlays` (disabled plugins skipped; broken plugin isolated in prod, re-raised in dev/test). +An overlay keys on `assetid` and never on coordinates, so it needs to know +nothing about buildings or levels (ADR-017): it decorates whichever marker that +asset already has, on whatever level that marker is on. Do not return `mapx` / +`mapy` / `levelid` from an overlay endpoint - a position belongs to the asset, +and a second copy of it in an overlay is one that can disagree. + ### `get_asset_presentation() -> List[Dict]` Returns asset-type presentation/routing definitions. Added in contract 0.7.0 diff --git a/docs/START-HERE.md b/docs/START-HERE.md index f54d9ae..c354e43 100644 --- a/docs/START-HERE.md +++ b/docs/START-HERE.md @@ -13,6 +13,9 @@ exists. 2. [OPERATE-WINDOWS](OPERATE-WINDOWS.md) - restart, logs, backups, upgrades. 3. [CSV-IMPORT](CSV-IMPORT.md) if the site's data is in spreadsheets, or [IMPORT-API](IMPORT-API.md) if there is a source database to script against. +4. [FLOOR-MAP](FLOOR-MAP.md) - load the site's floor plans and place markers. + Also the page to read BEFORE a plan changes: it covers moving every marker + onto a redrawn plan without doing it one at a time. **Do not** follow [INSTALL-WINDOWS-IIS](INSTALL-WINDOWS-IIS.md) for a new site. That is the manual procedure, kept for hand-built servers that predate the diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 6b8dd85..dff8a96 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -89,6 +89,36 @@ Confirm the app is healthy (login page renders, `/api/auth/login` returns a ## Version-specific notes +### Upgrading to v0.11.0: the floor map became buildings and levels + +Nothing to do, and nothing moves on screen. Run both migration steps as always: + +```bash +flask db upgrade +flask plugin upgrade-all +``` + +`7d33_buildings_and_levels` creates one building ("Main") and one default level +("Ground floor") carrying the blueprint paths and dimensions from your `map_*` +settings, then assigns every already-placed asset and location to that level. +Unplaced things stay unplaced. The old settings rows are deliberately left in +place, so a downgrade still finds the blueprint. + +Two things to know afterwards: + +- **A second floor or building is a Settings task**, not a migration: add it under + Settings > Buildings and levels, upload its blueprint, then move the markers + that belong on it. `POST /api/mappositions/transform` moves a whole level's + markers onto a redrawn plan from landmark pairs, dry run by default. +- **Replacing a blueprint with one of DIFFERENT dimensions moves every marker on + that level**, because the coordinates are pixels of the old size. Upload it, then + recalibrate with landmarks rather than editing the level's width and height by + hand. The API refuses to adopt a new size on a level that already has markers, + for this reason. + +If you wrote a plugin that stores map positions, see CONTRACT-STABILITY.md: the +contract is 0.20.0 and a position must now carry its `levelid`. + ### Upgrading to v0.5.0 or later: bundled the reference site floor plan removed Versions before 0.5 shipped the the reference site facility floor-plan PNGs as the