Buildings and levels for the floor map, and make every identifier searchable
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.
This commit is contained in:
@@ -39,6 +39,7 @@ Recorded in the comment block in `shopdb/__init__.py`:
|
||||
| 0.17.0 | Added `DashboardDefault` to the `shopdb.api` surface, so a plugin can resolve a display without reaching into core | additive surface (minor) |
|
||||
| 0.18.0 | Added `DISPLAY_ROLES`, `DISPLAY_ROLE_PATHS` and `normalize_display_role`, and finally exported `DashboardDefault`, which 0.17.0 imported but left out of `__all__`. The role vocabulary became the kiosk's own - `Dashboard`, `Lobby`, `3DPrintRoom` - so a plugin holding its own copy of that map reads core's instead of drifting from it | additive surface (minor) |
|
||||
| 0.19.0 | **BREAKING.** `get_dashboard_widgets` returns DATA AND SHAPE, not a component name. The old shape (`name` + `component` + `size`) named a Vue component per widget, which cannot survive a lean build - a plugin's component may never be staged into the frontend bundle (ADR-013) - and five plugins were declaring widgets that pointed at components nobody had written. A card now declares `id` / `title` / `endpoint` / `render` / `severity` / `permission` / `empty` / `position`; see PLUGIN-HOOKS.md. Also added `send_upload` so a plugin serving user-supplied bytes gets the headers that keep an SVG from running as script | **contract change (minor, pre-1.0)** |
|
||||
| 0.20.0 | The `Asset` model gained `levelid` (which floor plan its `mapx`/`mapy` are pixels of) and `mapverifiedat`; `Location` gained `levelid` too, and `resolve_asset_position` now returns the `levelid` belonging to whichever source supplied the coordinates. A plugin that writes a map position MUST write the level with it - a position without one renders as "level unknown" rather than being drawn on the default drawing, because a marker on the wrong floor plan looks correct (ADR-017) | additive surface (minor) |
|
||||
|
||||
The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, 0.10.0,
|
||||
0.11.0, 0.16.0, 0.18.0 and 0.19.0; 0.12.0 through 0.15.0 and 0.17.0 are recorded
|
||||
|
||||
@@ -46,19 +46,30 @@ Fields per row:
|
||||
| `installpath` | Installer path for this printer (see install-batch). |
|
||||
| `iscsf` | CSF flag. |
|
||||
| `locationname` | Location name, if the asset has one. |
|
||||
| `mapx` / `mapy` | Floor-map hotspot position. |
|
||||
| `mapx` / `mapy` | Floor-map hotspot position, in the native pixels of `levelid`. |
|
||||
| `levelid` | Which level (drawing) those pixels belong to. Null when unplaced. |
|
||||
|
||||
`?format=text` returns a pipe-delimited line per printer, one printer per line,
|
||||
with a fixed field order so the Inno / Pascal installer does a `split()` instead
|
||||
of parsing JSON:
|
||||
|
||||
```
|
||||
printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy
|
||||
printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy|levelid
|
||||
```
|
||||
|
||||
Any pipe or newline inside a value is neutralized to a space so the field count
|
||||
stays fixed. The web map uses the default JSON.
|
||||
|
||||
**Fields 0-7 are frozen.** The shipped installer reads them positionally
|
||||
(`GetField(Line, 6)` is mapx), so a new field goes on the END and nowhere else -
|
||||
inserting one shifts every later field and the installer keeps running while
|
||||
reading a model number as a coordinate. `levelid` is field 8 for that reason, and
|
||||
installers built before levels existed ignore it.
|
||||
|
||||
An installer that ignores `levelid` draws every printer on whichever single
|
||||
blueprint it ships, which is correct only while a site has one level. See
|
||||
ADR-017 and section 6.
|
||||
|
||||
---
|
||||
|
||||
## 3. `GET /api/printers/pc-default?machine=NNNN`
|
||||
@@ -102,3 +113,27 @@ name / number.
|
||||
The `common` scope's `printer map` manifest entry (see `GE-ENFORCE-DISPLAY.md`)
|
||||
lays down the signed installer that consumes these endpoints. The web map page
|
||||
covers the same install flow for a human at a browser.
|
||||
|
||||
---
|
||||
|
||||
## 6. The shipped installer predates levels (known gap)
|
||||
|
||||
The Pascal installer in the `inno` repo (`PrinterInstallerMap`) still assumes one
|
||||
drawing per site, in two places that must change together:
|
||||
|
||||
- `MAP_SOURCE_W = 3300` / `MAP_SOURCE_H = 2550` are compiled-in constants, and
|
||||
every hotspot is scaled by them. They are the dimensions of ONE level.
|
||||
- A downsized copy of that level's blueprint is bundled into the installer
|
||||
(`880x680`), so the picture is fixed at build time.
|
||||
|
||||
Two consequences, neither of which the installer can detect:
|
||||
|
||||
1. It ignores field 8, so printers on any level are drawn on the bundled image.
|
||||
Coordinates from a different drawing land somewhere plausible and wrong.
|
||||
2. When a level's blueprint is replaced with one of different dimensions, the
|
||||
constants and the bundled image are both stale and every hotspot shifts.
|
||||
|
||||
The API side is ready: `GET /api/maplevels` is deliberately unauthenticated so
|
||||
the installer can fetch level dimensions and blueprint URLs at runtime rather
|
||||
than compiling them in, which is what fixes both. Until the installer is
|
||||
rebuilt against it, treat its map as accurate for the default level only.
|
||||
|
||||
@@ -12,7 +12,7 @@ never by editing this file.
|
||||
| series | value | governed by |
|
||||
|---|---|---|
|
||||
| product `__version__` | `0.10.0` | ADR-007 |
|
||||
| plugin contract `__contract_version__` | `0.19.0` | ADR-002 |
|
||||
| plugin contract `__contract_version__` | `0.20.0` | ADR-002 |
|
||||
|
||||
They move independently. A contract bump is not a release.
|
||||
|
||||
@@ -23,7 +23,7 @@ with `flask plugin upgrade-all`. Both are needed on a deploy.
|
||||
|
||||
| chain | head |
|
||||
|---|---|
|
||||
| core | `7d32_displayrole_kiosk_vocabulary` |
|
||||
| core | `7d33_buildings_and_levels` |
|
||||
| backups | `backups0003clearlastseen` |
|
||||
| computers | `computers0001anchor` |
|
||||
| employees | `employees0002photo` |
|
||||
@@ -44,16 +44,16 @@ with `flask plugin upgrade-all`. Both are needed on a deploy.
|
||||
| plugin | version | core_version | owns migrations |
|
||||
|---|---|---|---|
|
||||
| backups | 1.0.0 | >=0.16.0,<1.0.0 | yes |
|
||||
| computers | 1.0.0 | >=0.1.0,<1.0.0 | yes |
|
||||
| computers | 1.0.0 | >=0.20.0,<1.0.0 | yes |
|
||||
| employees | 1.0.0 | >=0.1.0,<1.0.0 | yes |
|
||||
| geenforce | 0.1.0 | >=0.18.0,<1.0.0 | yes |
|
||||
| knowledgebase | 1.0.0 | >=0.1.0,<1.0.0 | yes |
|
||||
| machines | 1.0.0 | >=0.1.0,<1.0.0 | yes |
|
||||
| measuringtools | 1.0.0 | >=0.6.0,<1.0.0 | yes |
|
||||
| network | 1.0.0 | >=0.1.0,<1.0.0 | yes |
|
||||
| machines | 1.0.0 | >=0.20.0,<1.0.0 | yes |
|
||||
| measuringtools | 1.0.0 | >=0.20.0,<1.0.0 | yes |
|
||||
| network | 1.0.0 | >=0.20.0,<1.0.0 | yes |
|
||||
| notifications | 1.0.0 | >=0.1.0,<1.0.0 | yes |
|
||||
| printedparts | 0.1.0 | >=0.16.0,<1.0.0 | yes |
|
||||
| printers | 1.0.0 | >=0.16.0,<1.0.0 | yes |
|
||||
| printers | 1.0.0 | >=0.20.0,<1.0.0 | yes |
|
||||
| slides | 2.0.0 | >=0.2.0,<1.0.0 | yes |
|
||||
| tools | 1.0.0 | >=0.16.0,<1.0.0 | no |
|
||||
| usb | 1.0.0 | >=0.1.0,<1.0.0 | yes |
|
||||
@@ -81,9 +81,10 @@ Manifest-less directories under `plugins/` are core frontend surface and always
|
||||
| ADR-014-schema-lean-per-site.md | ADR-014: Schema-lean per-site builds (retire cross-plugin FKs, lift plugin tables) | ACCEPTED |
|
||||
| ADR-015-site-specific-configuration.md | ADR-015: Where a site's own data is allowed to live | ACCEPTED |
|
||||
| ADR-016-credential-delivery.md | ADR-016: Credential delivery to the fleet | ACCEPTED (decided; NOT yet implemented - |
|
||||
| ADR-017-buildings-and-levels.md | ADR-017: Buildings and levels as the map model | ACCEPTED |
|
||||
|
||||
## Size
|
||||
|
||||
- test functions defined: **1050** (parametrised cases collect higher)
|
||||
- documented API paths: **265** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`)
|
||||
- test functions defined: **1084** (parametrised cases collect higher)
|
||||
- documented API paths: **276** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`)
|
||||
|
||||
|
||||
143
docs/adr/ADR-017-buildings-and-levels.md
Normal file
143
docs/adr/ADR-017-buildings-and-levels.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# ADR-017: Buildings and levels as the map model
|
||||
|
||||
- Status: ACCEPTED
|
||||
- Date: 2026-08-17
|
||||
- Deciders: ShopDB maintainers
|
||||
- Relates to: ADR-001 (asset as the platform contract), ADR-004 (per-site
|
||||
instances), ADR-010 (frontend plugin hooks), ADR-015 (site-specific
|
||||
configuration)
|
||||
- Supersedes: the site-wide `map_blueprint_light` / `map_blueprint_dark` /
|
||||
`map_width` / `map_height` settings
|
||||
|
||||
## Context
|
||||
|
||||
A site's floor map is one image. Four settings describe it - two blueprints
|
||||
(light and dark) and the native pixel width and height - and `assets.mapx` /
|
||||
`assets.mapy` are absolute pixel coordinates in that image's space.
|
||||
|
||||
That model has one floor in one building, which was true when it was written and
|
||||
is no longer. The reference site is adding a second level, and a second building
|
||||
is likely within a year or two. The immediate trigger is a new blueprint at
|
||||
3308x4000 where the old one was 3300x2550.
|
||||
|
||||
The obvious cheap answer is to stack levels on one canvas: draw the second level
|
||||
below the first and keep one image. It works, and it was seriously considered.
|
||||
It was rejected because it makes the level an inference rather than a fact:
|
||||
|
||||
- "Which level is this asset on" becomes `mapy > 2550`. Every per-level filter,
|
||||
count, report and export has to know that constant, and re-exporting the
|
||||
drawing at a different height silently changes the answer everywhere. Not
|
||||
broken - wrong, which is worse.
|
||||
- Distance between two markers becomes computable and meaningless, so anything
|
||||
doing nearest-asset or clustering quietly answers nonsense across levels.
|
||||
- One canvas forces one scale. A mezzanine drawn at a different scale than the
|
||||
floor below cannot be expressed at all.
|
||||
- 3308x4000 is portrait for a building whose floor is landscape. At a zoom where
|
||||
a marker is clickable, roughly half a level is visible, and "fit to level" is
|
||||
not expressible.
|
||||
- A third level makes each of these worse, and adds a second threshold.
|
||||
|
||||
Timing decided it. Because the level layout changed and machines moved, every
|
||||
marker is going to be repositioned anyway. Introducing levels now costs one pass
|
||||
over the positions; stacking now and splitting later costs two, and in between
|
||||
every position placed encodes the threshold into real data - which a later
|
||||
migration would then have to un-guess by comparing Y against it.
|
||||
|
||||
## Decision
|
||||
|
||||
Two tables, and assets reference the level.
|
||||
|
||||
```
|
||||
buildings buildingid, buildingname, sortorder, isactive
|
||||
maplevels levelid, buildingid, levelname, sortorder,
|
||||
blueprintlight, blueprintdark, mapwidth, mapheight,
|
||||
isdefault, isactive
|
||||
assets levelid (nullable; the default level for existing rows)
|
||||
```
|
||||
|
||||
**1. An asset references the level, never the building.** The building derives
|
||||
from the level, so the two cannot disagree. Storing both would be a fact
|
||||
recorded twice with no difference in granularity to justify it.
|
||||
|
||||
**2. Blueprints and native dimensions belong to the level.** This is what the
|
||||
site-wide settings could not express: two levels in one building may be drawn at
|
||||
different sizes and scales, and two buildings certainly are.
|
||||
|
||||
**3. Name and order are separate.** `levelname` is text and `sortorder` is an
|
||||
integer. Levels are not reliably numbered - basement, ground, mezzanine, roof,
|
||||
tunnel - and sort order gives adjacency and up/down navigation without
|
||||
pretending the names are ordinal. It also lets a mezzanine be inserted between
|
||||
two existing levels without renumbering anything.
|
||||
|
||||
**4. `mapx` / `mapy` keep their meaning, scoped to the level.** They stay
|
||||
absolute pixels in the native coordinate space of the level's blueprint. No
|
||||
normalisation to fractions: pixels are what the drawing tools produce, what an
|
||||
operator can read off an image, and what the existing data already holds.
|
||||
|
||||
**5. A level is required to render a position, and absence is not a default.**
|
||||
Where a payload carries a position without a level, the UI renders "level
|
||||
unknown" rather than falling back to the default level. Falling back draws one
|
||||
building's ground floor with a marker positioned for another building's
|
||||
mezzanine: it renders perfectly and points at the wrong place, and nothing about
|
||||
the result looks wrong. A visible gap is worth more than a confident wrong
|
||||
answer.
|
||||
|
||||
**6. The level list is readable without authentication.** The printer installer
|
||||
map runs optional-auth, before anyone logs in, and it needs a blueprint. This
|
||||
follows the precedent already set for printer install-list and the slide feed.
|
||||
Blueprint paths and level names are not secrets; the positions of assets on them
|
||||
already render on public kiosk pages.
|
||||
|
||||
**7. Levels are not Locations.** Assets carry `locationid` already, and reusing
|
||||
it is tempting. A Location answers "which operation owns this"; a level answers
|
||||
"which drawing renders it, at what native size". Overloading Location with
|
||||
blueprint images and pixel dimensions makes both concepts worse. They coexist:
|
||||
an asset on level 2 in operation 0613.
|
||||
|
||||
## Migration
|
||||
|
||||
The existing four settings become one building and one level, marked default,
|
||||
and every asset with a position points at it. Nothing renders differently on the
|
||||
day it lands. The settings keys are then retired rather than left as a second
|
||||
source of truth that can disagree with the rows.
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- A second building costs a row and a blueprint. So does a third level.
|
||||
- Per-level export, per-level zoom, and per-level calibration all become
|
||||
expressible. Calibrating one level against its own landmarks is more correct
|
||||
than transforming a whole site at once.
|
||||
- Level becomes queryable - counts and filters per level or per building are
|
||||
ordinary queries rather than coordinate arithmetic.
|
||||
|
||||
Negative:
|
||||
|
||||
- Every surface that draws a marker learns there is more than one drawing. That
|
||||
is two components, seven views, four asset position pickers, the printer
|
||||
installer map, the PDF export, and eight API payloads that must emit `levelid`
|
||||
beside `mapx`. A payload that forgets it produces the silent-wrong-map failure
|
||||
described above, which is why rule 5 exists and why a build gate checks that
|
||||
the two fields travel together.
|
||||
- `get_map_overlays` gains level context, so this is a plugin contract change
|
||||
(0.20.0) and plugins rendering overlays need to declare which level they are
|
||||
for.
|
||||
- One more admin surface: levels and buildings have to be managed somewhere, and
|
||||
the existing floor-map settings page becomes that.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Stack levels on one canvas.** Rejected above. Zero cost today, and it makes
|
||||
the level an inference over a magic number.
|
||||
|
||||
**Normalise coordinates to fractions of the image.** Would make a rescaled
|
||||
blueprint self-correcting, which is genuinely attractive. Rejected for now
|
||||
because it converts every existing integer position through a lossy division,
|
||||
and because it does not help the actual problem: the levels changed and machines
|
||||
moved, so the positions need human review regardless. Worth revisiting
|
||||
independently.
|
||||
|
||||
**A `level` string on the asset, with one blueprint per level in settings.**
|
||||
Rejected: settings keyed by level name is a table with extra steps, and it gives
|
||||
no place for per-level dimensions or ordering.
|
||||
@@ -346,7 +346,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/search",
|
||||
"purpose": "Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search_<type>_enabled settings",
|
||||
"purpose": "Global search across assets (including the gaugelabreference and maintenancereference identifiers), applications, KB, employees, USB devices, printed items, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search_<type>_enabled settings. Retired (isactive=0) rows are excluded everywhere, including KB articles whose topic application is retired",
|
||||
"auth": "jwt-optional",
|
||||
"params": "q (required, 2-200 chars)",
|
||||
"example": "curl 'http://localhost:5001/api/search?q=WKSTN0042'"
|
||||
@@ -3232,5 +3232,122 @@
|
||||
"example": "curl -I http://localhost:5001/api/docs/redoc.standalone.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"surface": "core-map",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/maplevels",
|
||||
"auth": "none",
|
||||
"params": "none",
|
||||
"purpose": "Every building with its levels in display order, each carrying blueprint paths, native pixel size and marker count, plus defaultlevelid. PUBLIC: the printer installer map draws a blueprint before anyone logs in",
|
||||
"example": "curl http://localhost:5001/api/maplevels"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/maplevels/<levelid>",
|
||||
"auth": "none",
|
||||
"params": "levelid in path",
|
||||
"purpose": "One level: name, building, blueprints and native size. That size is what mapx/mapy on this level are pixels of (ADR-017)",
|
||||
"example": "curl http://localhost:5001/api/maplevels/2"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/maplevels/<levelid>/blueprint/<filename>",
|
||||
"auth": "none",
|
||||
"params": "levelid and filename in path",
|
||||
"purpose": "Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script",
|
||||
"example": "curl -I http://localhost:5001/api/maplevels/2/blueprint/level-2-light.png"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/maplevels/buildings",
|
||||
"auth": "jwt + role:admin",
|
||||
"params": "body: buildingname (required), sortorder",
|
||||
"purpose": "Create a building. An asset references the level, never the building, so the two cannot disagree",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"buildingname\":\"Annex\"}' -X POST http://localhost:5001/api/maplevels/buildings"
|
||||
},
|
||||
{
|
||||
"method": "PUT|PATCH",
|
||||
"path": "/api/maplevels/buildings/<buildingid>",
|
||||
"auth": "jwt + role:admin",
|
||||
"params": "buildingid in path; body: buildingname, sortorder, isactive",
|
||||
"purpose": "Rename or reorder a building. Levels move with it and nothing repositions",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/buildings/1"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/maplevels",
|
||||
"auth": "jwt + role:admin",
|
||||
"params": "body: buildingid and levelname (required), sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault",
|
||||
"purpose": "Create a level. Name and sort order are separate because levels are not reliably numbered (basement, mezzanine, roof), and gaps let one be inserted later without renumbering",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -X POST http://localhost:5001/api/maplevels"
|
||||
},
|
||||
{
|
||||
"method": "PUT|PATCH",
|
||||
"path": "/api/maplevels/<levelid>",
|
||||
"auth": "jwt + role:admin",
|
||||
"params": "levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive",
|
||||
"purpose": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate space every marker is expressed in, so a resize moves them all relative to the drawing - use a landmark transform instead",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/1"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/maplevels/<levelid>",
|
||||
"auth": "jwt + role:admin",
|
||||
"params": "levelid in path",
|
||||
"purpose": "Deactivate a level. Refused with 409 while assets are placed on it, and refused for the default level: deleting a drawing out from under a marker leaves a position in a coordinate space that no longer exists",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -X DELETE http://localhost:5001/api/maplevels/3"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/maplevels/<levelid>/blueprint",
|
||||
"auth": "jwt + role:admin",
|
||||
"params": "levelid in path; multipart/form-data: file=<image>, theme=light|dark",
|
||||
"purpose": "Upload a level's blueprint. Reads the image's real pixel size from its header and adopts it when the level is EMPTY; with markers already placed it reports the mismatch and changes nothing, because adopting a new coordinate space silently moves every marker while looking like a successful upload",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -F file=@second-floor.png -F theme=light -X POST http://localhost:5001/api/maplevels/2/blueprint"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/mappositions/transform",
|
||||
"auth": "permission:assets.edit",
|
||||
"params": "body: levelid (required), landmarks [{fromx,fromy,tox,toy}] (two or more), tolevelid, assetids, dryrun (defaults TRUE)",
|
||||
"purpose": "Move every placed marker on a level by a transform derived per axis from landmark pairs. NEVER from image dimensions: a level added below another changes canvas height without rescaling anything, and a dimension-derived scale would stretch Y and be wrong everywhere. Dry run returns every old and new position plus which land outside the target. Applying snapshots first and clears mapverifiedat, because a transformed position is a guess awaiting review",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"levelid\":1,\"landmarks\":[{\"fromx\":0,\"fromy\":0,\"tox\":0,\"toy\":1450},{\"fromx\":1000,\"fromy\":1000,\"tox\":1000,\"toy\":2450}]}' -X POST http://localhost:5001/api/mappositions/transform"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/mappositions/positions",
|
||||
"auth": "permission:assets.edit",
|
||||
"params": "body: positions [{assetid, mapx, mapy, levelid}] - levelid required per row - and verified",
|
||||
"purpose": "Set many positions at once, snapshotting first. levelid is per position rather than per request because a bulk save can span levels and inferring it is the guess this model exists to remove. Placing by hand counts as review, so mapverifiedat is stamped",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"positions\":[{\"assetid\":42,\"mapx\":1200,\"mapy\":900,\"levelid\":2}]}' -X POST http://localhost:5001/api/mappositions/positions"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/mappositions/verify",
|
||||
"auth": "permission:assets.edit",
|
||||
"params": "body: assetids (required), unverify",
|
||||
"purpose": "Mark markers reviewed against the current drawing without moving them - the common case in a review pass. No snapshot, because no position changes",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"assetids\":[42,43]}' -X POST http://localhost:5001/api/mappositions/verify"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/mappositions/snapshots",
|
||||
"auth": "permission:assets.view",
|
||||
"params": "none; newest 50",
|
||||
"purpose": "Position snapshots, newest first, with what caused each and whether it has been restored. Metadata only - the positions are large",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/mappositions/snapshots"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/mappositions/snapshots/<snapshotid>/restore",
|
||||
"auth": "permission:assets.edit",
|
||||
"params": "snapshotid in path",
|
||||
"purpose": "Put a snapshot back, snapshotting first so an undo is itself undoable. Restores level and review state, not just coordinates, and reports assets that no longer exist rather than failing the whole restore",
|
||||
"example": "curl -H \"Authorization: Bearer $TOKEN\" -X POST http://localhost:5001/api/mappositions/snapshots/7/restore"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "ShopDB Flask API",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Asset-management API (core + plugins). Responses use a `success_response` envelope: `{status, data, meta}`. Auth: Bearer JWT (login or a managed PAT) for `jwt`/`admin`/`permission:*`; `X-API-Key` for collector/managed-token endpoints; public endpoints need neither."
|
||||
},
|
||||
"servers": [
|
||||
@@ -173,6 +173,9 @@
|
||||
},
|
||||
{
|
||||
"name": "core-docs"
|
||||
},
|
||||
{
|
||||
"name": "core-map"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
@@ -2061,8 +2064,8 @@
|
||||
"tags": [
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets...",
|
||||
"description": "Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search_<type>_enabled settings\n\n**Auth:** jwt-optional\n\n**Params:** q (required, 2-200 chars)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/search?q=WKSTN0042'\n```",
|
||||
"summary": "Global search across assets (including the gaugelabreference and maintenancereference identifiers), applications, KB...",
|
||||
"description": "Global search across assets (including the gaugelabreference and maintenancereference identifiers), applications, KB, employees, USB devices, printed items, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search_<type>_enabled settings. Retired (isactive=0) rows are excluded everywhere, including KB articles whose topic application is retired\n\n**Auth:** jwt-optional\n\n**Params:** q (required, 2-200 chars)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/search?q=WKSTN0042'\n```",
|
||||
"security": [
|
||||
{},
|
||||
{
|
||||
@@ -18539,6 +18542,831 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/maplevels": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Every building with its levels in display order, each carrying blueprint paths, native pixel size and marker count...",
|
||||
"description": "Every building with its levels in display order, each carrying blueprint paths, native pixel size and marker count, plus defaultlevelid. PUBLIC: the printer installer map draws a blueprint before anyone logs in\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/maplevels\n```",
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Create a level. Name and sort order are separate because levels are not reliably numbered (basement, mezzanine, roof)...",
|
||||
"description": "Create a level. Name and sort order are separate because levels are not reliably numbered (basement, mezzanine, roof), and gaps let one be inserted later without renumbering\n\n**Auth:** jwt + role:admin\n\n**Params:** body: buildingid and levelname (required), sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X POST http://localhost:5001/api/maplevels\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "body: buildingid and levelname (required), sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/maplevels/{levelid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "One level: name, building, blueprints and native size. That size is what mapx/mapy on this level are pixels of (ADR-017)",
|
||||
"description": "One level: name, building, blueprints and native size. That size is what mapx/mapy on this level are pixels of (ADR-017)\n\n**Auth:** none\n\n**Params:** levelid in path\n\n**Example:**\n```\ncurl http://localhost:5001/api/maplevels/2\n```",
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "levelid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate...",
|
||||
"description": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate space every marker is expressed in, so a resize moves them all relative to the drawing - use a landmark transform instead\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/1\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "levelid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate...",
|
||||
"description": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate space every marker is expressed in, so a resize moves them all relative to the drawing - use a landmark transform instead\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/1\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "levelid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Deactivate a level. Refused with 409 while assets are placed on it, and refused for the default level: deleting a...",
|
||||
"description": "Deactivate a level. Refused with 409 while assets are placed on it, and refused for the default level: deleting a drawing out from under a marker leaves a position in a coordinate space that no longer exists\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X DELETE http://localhost:5001/api/maplevels/3\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "levelid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/maplevels/{levelid}/blueprint/{filename}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script",
|
||||
"description": "Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script\n\n**Auth:** none\n\n**Params:** levelid and filename in path\n\n**Example:**\n```\ncurl -I http://localhost:5001/api/maplevels/2/blueprint/level-2-light.png\n```",
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "levelid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "filename",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/maplevels/buildings": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Create a building. An asset references the level, never the building, so the two cannot disagree",
|
||||
"description": "Create a building. An asset references the level, never the building, so the two cannot disagree\n\n**Auth:** jwt + role:admin\n\n**Params:** body: buildingname (required), sortorder\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"buildingname\":\"Annex\"}' -X POST http://localhost:5001/api/maplevels/buildings\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "body: buildingname (required), sortorder"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/maplevels/buildings/{buildingid}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Rename or reorder a building. Levels move with it and nothing repositions",
|
||||
"description": "Rename or reorder a building. Levels move with it and nothing repositions\n\n**Auth:** jwt + role:admin\n\n**Params:** buildingid in path; body: buildingname, sortorder, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/buildings/1\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "buildingid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "buildingid in path; body: buildingname, sortorder, isactive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Rename or reorder a building. Levels move with it and nothing repositions",
|
||||
"description": "Rename or reorder a building. Levels move with it and nothing repositions\n\n**Auth:** jwt + role:admin\n\n**Params:** buildingid in path; body: buildingname, sortorder, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/buildings/1\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "buildingid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "buildingid in path; body: buildingname, sortorder, isactive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/maplevels/{levelid}/blueprint": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Upload a level's blueprint. Reads the image's real pixel size from its header and adopts it when the level is EMPTY...",
|
||||
"description": "Upload a level's blueprint. Reads the image's real pixel size from its header and adopts it when the level is EMPTY; with markers already placed it reports the mismatch and changes nothing, because adopting a new coordinate space silently moves every marker while looking like a successful upload\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path; multipart/form-data: file=<image>, theme=light|dark\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -F file=@second-floor.png -F theme=light -X POST http://localhost:5001/api/maplevels/2/blueprint\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "levelid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "levelid in path; multipart/form-data: file=<image>, theme=light|dark"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/mappositions/transform": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Move every placed marker on a level by a transform derived per axis from landmark pairs. NEVER from image dimensions: a...",
|
||||
"description": "Move every placed marker on a level by a transform derived per axis from landmark pairs. NEVER from image dimensions: a level added below another changes canvas height without rescaling anything, and a dimension-derived scale would stretch Y and be wrong everywhere. Dry run returns every old and new position plus which land outside the target. Applying snapshots first and clears mapverifiedat, because a transformed position is a guess awaiting review\n\n**Auth:** permission:assets.edit\n\n**Params:** body: levelid (required), landmarks [{fromx,fromy,tox,toy}] (two or more), tolevelid, assetids, dryrun (defaults TRUE)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"levelid\":1,\"landmarks\":[{\"fromx\":0,\"fromy\":0,\"tox\":0,\"toy\":1450},{\"fromx\":1000,\"fromy\":1000,\"tox\":1000,\"toy\":2450}]}' -X POST http://localhost:5001/api/mappositions/transform\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "body: levelid (required), landmarks [{fromx,fromy,tox,toy}] (two or more), tolevelid, assetids, dryrun (defaults TRUE)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/mappositions/positions": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Set many positions at once, snapshotting first. levelid is per position rather than per request because a bulk save can...",
|
||||
"description": "Set many positions at once, snapshotting first. levelid is per position rather than per request because a bulk save can span levels and inferring it is the guess this model exists to remove. Placing by hand counts as review, so mapverifiedat is stamped\n\n**Auth:** permission:assets.edit\n\n**Params:** body: positions [{assetid, mapx, mapy, levelid}] - levelid required per row - and verified\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"positions\":[{\"assetid\":42,\"mapx\":1200,\"mapy\":900,\"levelid\":2}]}' -X POST http://localhost:5001/api/mappositions/positions\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "body: positions [{assetid, mapx, mapy, levelid}] - levelid required per row - and verified"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/mappositions/verify": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Mark markers reviewed against the current drawing without moving them - the common case in a review pass. No snapshot...",
|
||||
"description": "Mark markers reviewed against the current drawing without moving them - the common case in a review pass. No snapshot, because no position changes\n\n**Auth:** permission:assets.edit\n\n**Params:** body: assetids (required), unverify\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"assetids\":[42,43]}' -X POST http://localhost:5001/api/mappositions/verify\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "body: assetids (required), unverify"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/mappositions/snapshots": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Position snapshots, newest first, with what caused each and whether it has been restored. Metadata only - the positions...",
|
||||
"description": "Position snapshots, newest first, with what caused each and whether it has been restored. Metadata only - the positions are large\n\n**Auth:** permission:assets.view\n\n**Params:** none; newest 50\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/mappositions/snapshots\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/mappositions/snapshots/{snapshotid}/restore": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-map"
|
||||
],
|
||||
"summary": "Put a snapshot back, snapshotting first so an undo is itself undoable. Restores level and review state, not just...",
|
||||
"description": "Put a snapshot back, snapshotting first so an undo is itself undoable. Restores level and review state, not just coordinates, and reports assets that no longer exist rather than failing the whole restore\n\n**Auth:** permission:assets.edit\n\n**Params:** snapshotid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X POST http://localhost:5001/api/mappositions/snapshots/7/restore\n```",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success. Body is the success_response envelope: {status, data, meta}.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Error. Body is the error envelope; the code and message are nested under data.error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"$ref": "#/components/schemas/ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid credentials."
|
||||
},
|
||||
"403": {
|
||||
"description": "Authenticated, but not permitted."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such record."
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "snapshotid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "snapshotid in path"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user