Document buildings and levels where the old single-map model was still taught
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s

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.
This commit is contained in:
cproudlock
2026-08-17 13:35:57 -04:00
parent 24647aa8e8
commit 741dda5be7
7 changed files with 216 additions and 7 deletions

View File

@@ -96,6 +96,13 @@ Two auth patterns dominate the reads:
Every mutating endpoint (POST / PUT / PATCH / DELETE) requires a JWT and is Every mutating endpoint (POST / PUT / PATCH / DELETE) requires a JWT and is
gated by `require_role` or `require_permission`; none are public. 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/<id>` 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 Two GETs deliberately break the optional-auth convention and require a
permission, because what they return is not asset metadata but a file that permission, because what they return is not asset metadata but a file that
carries value or liability of its own: carries value or liability of its own:
@@ -115,7 +122,10 @@ like any other mutation.
| `POST /api/auth/login` | Obtain a JWT. | | `POST /api/auth/login` | Obtain a JWT. |
| `GET /api/setup/needs-admin` | First-run check: does the instance have zero users. | | `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. | | `POST /api/setup/create-admin` | First-run only; creates the first admin, then 403s forever. |
| `GET /api/settings/map-blueprint/<filename>` | Serve the floor-map blueprint image. | | `GET /api/settings/map-blueprint/<filename>` | 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/<levelid>` | One level's name, building, blueprints and size. |
| `GET /api/maplevels/<levelid>/blueprint/<filename>` | Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script. |
| `GET /api/settings/branding/<filename>` | Serve site branding assets (logo, etc.). | | `GET /api/settings/branding/<filename>` | Serve site branding assets (logo, etc.). |
| `GET /api/settings` and `GET /api/settings/<key>` | 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/settings` and `GET /api/settings/<key>` | 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/<filename>` | Serve a model image. | | `GET /api/models/image/<filename>` | Serve a model image. |

View File

@@ -196,12 +196,21 @@ The batch generator at `/print/asset-label-batch/<assettype>` (reached from the
### map ### 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 | | 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_light` | `/static/images/floorplan-placeholder.svg` | Superseded by `maplevels.blueprintlight`. Retained for downgrade only. |
| `map_blueprint_dark` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (dark theme). | | `map_blueprint_dark` | `/static/images/floorplan-placeholder.svg` | Superseded by `maplevels.blueprintdark`. |
| `map_width` | `3300` | Blueprint native width in pixels. | | `map_width` | `3300` | Superseded by `maplevels.mapwidth`. |
| `map_height` | `2550` | Blueprint native height in pixels. | | `map_height` | `2550` | Superseded by `maplevels.mapheight`. |
### integrations ### integrations

135
docs/FLOOR-MAP.md Normal file
View File

@@ -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).

View File

@@ -190,12 +190,28 @@ Common `machines` columns -> core Asset fields (same for every target plugin):
| `serialnumber` | `serialnumber` | | | `serialnumber` | `serialnumber` | |
| `machinestatusid` | `statusid` | remap via `machinestatus` -> asset statuses | | `machinestatusid` | `statusid` | remap via `machinestatus` -> asset statuses |
| `businessunitid` | `businessunitid` | remap via imported business units | | `businessunitid` | `businessunitid` | remap via imported business units |
| `mapleft` | `mapx` | | | `mapleft` | `mapx` | pixels of a LEVEL; send `levelid` with it |
| `maptop` | `mapy` | | | `maptop` | `mapy` | as above |
| n/a | `levelid` | which floor plan the coordinates belong to (ADR-017) |
| `machinenotes` | `notes` | | | `machinenotes` | `notes` | |
| `dateadded` | `createddate` | import mode only | | `dateadded` | `createddate` | import mode only |
| `lastupdated` | `modifieddate` | 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: Per-plugin extension fields:
- **computers** (`POST /api/computers`): `hostname` <- `machines.hostname`, - **computers** (`POST /api/computers`): `hostname` <- `machines.hostname`,

View File

@@ -369,6 +369,12 @@ class MeasuringToolsPlugin(BasePlugin):
Consumed by `GET /api/pluginui/map-overlays` (disabled plugins skipped; broken Consumed by `GET /api/pluginui/map-overlays` (disabled plugins skipped; broken
plugin isolated in prod, re-raised in dev/test). 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]` ### `get_asset_presentation() -> List[Dict]`
Returns asset-type presentation/routing definitions. Added in contract 0.7.0 Returns asset-type presentation/routing definitions. Added in contract 0.7.0

View File

@@ -13,6 +13,9 @@ exists.
2. [OPERATE-WINDOWS](OPERATE-WINDOWS.md) - restart, logs, backups, upgrades. 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 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. [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. **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 That is the manual procedure, kept for hand-built servers that predate the

View File

@@ -89,6 +89,36 @@ Confirm the app is healthy (login page renders, `/api/auth/login` returns a
## Version-specific notes ## 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 ### 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 Versions before 0.5 shipped the the reference site facility floor-plan PNGs as the