Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd503be4ba | ||
|
|
5f5ed7b1d4 | ||
|
|
62f4a42210 | ||
|
|
9c1c6c5729 | ||
|
|
741dda5be7 |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -10,6 +10,42 @@ ADR-007 and ADR-002.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.11.1] - 2026-08-17
|
||||
|
||||
A patch release: the pagination-cap fixes, plus the documentation that missed the
|
||||
0.11.0 installer. No application behaviour changed on the server and the plugin
|
||||
contract stays at 0.20.0.
|
||||
|
||||
`docs/UPGRADE.md` is one of the documents the Windows installer puts on the box,
|
||||
and its buildings-and-levels section landed after 0.11.0 was tagged - so 0.11.0
|
||||
shipped the page an operator reads WHILE upgrading without the guidance about
|
||||
that upgrade. An air-gapped server has no other way to reach it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pickers and batch label sheets no longer stop at 100 rows. `perpage` is
|
||||
clamped to `MAX_PAGE_SIZE` (100) server-side and the response says nothing
|
||||
about it, so a control asking for 500 or 1000 received the first 100 rows and
|
||||
a success. On a site with 126 applications the 26 sorting last could not be
|
||||
chosen as a knowledge-base topic; batch label printing rendered a sheet for
|
||||
the first 100 of 262 machines that looked complete. New `fetchAllPages()`
|
||||
helper plus `listAll()` on the applications, models, machines, computers,
|
||||
printers, network, measuring-tools, USB and printed-parts APIs, used by the
|
||||
KB article form and topic filter, the notification form, the report filter
|
||||
builder, `AssetLabelBatch`, `PrinterQRBatch`, `USBLabelBatch`,
|
||||
`PrintedPartsLabels`, `MachineForm` and `NetworkDeviceForm`. Lists that render
|
||||
a page at a time are unchanged - they page deliberately.
|
||||
|
||||
### Changed
|
||||
|
||||
- The installer now also ships `FLOOR-MAP.md`, the operator's page for loading
|
||||
floor plans, placing markers and moving them when a plan changes. Same reason
|
||||
the Windows runbooks ship: the box cannot reach the wiki.
|
||||
- The bundled `UPGRADE.md` now carries the 0.11.0 section: an existing map is
|
||||
preserved and 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 the level's width and height.
|
||||
|
||||
## [0.11.0] - 2026-08-17
|
||||
|
||||
The floor map became a set of drawings instead of one picture. A site can hold
|
||||
|
||||
@@ -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/<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
|
||||
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/<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` 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. |
|
||||
|
||||
@@ -196,12 +196,21 @@ The batch generator at `/print/asset-label-batch/<assettype>` (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
|
||||
|
||||
|
||||
135
docs/FLOOR-MAP.md
Normal file
135
docs/FLOOR-MAP.md
Normal 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).
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ never by editing this file.
|
||||
|
||||
| series | value | governed by |
|
||||
|---|---|---|
|
||||
| product `__version__` | `0.11.0` | ADR-007 |
|
||||
| product `__version__` | `0.11.1` | ADR-007 |
|
||||
| plugin contract `__contract_version__` | `0.20.0` | ADR-002 |
|
||||
|
||||
They move independently. A contract bump is not a release.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shopdb-frontend",
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -50,6 +50,30 @@ api.interceptors.response.use(
|
||||
|
||||
export default api
|
||||
|
||||
// The backend clamps perpage to MAX_PAGE_SIZE (100) and says nothing about it,
|
||||
// so asking for `perpage: 1000` silently returns the first 100 rows and drops
|
||||
// the rest. A picker built that way looks complete and is not: with 126
|
||||
// applications on a live site, the 26 sorting last were simply unselectable.
|
||||
//
|
||||
// Use this wherever a control needs the WHOLE list (dropdowns, pickers, label
|
||||
// batches) rather than a page of it. Returns the full array directly, not an
|
||||
// axios response. Anything that renders a paged table should keep calling
|
||||
// list() with a real page number instead.
|
||||
export async function fetchAllPages(path, params = {}) {
|
||||
const first = await api.get(path, { params: { ...params, perpage: 100, page: 1 } })
|
||||
let items = first.data.data || []
|
||||
const totalpages = first.data.meta?.pagination?.totalpages || 1
|
||||
if (totalpages > 1) {
|
||||
const rest = await Promise.all(
|
||||
Array.from({ length: totalpages - 1 }, (_, i) =>
|
||||
api.get(path, { params: { ...params, perpage: 100, page: i + 2 } })
|
||||
)
|
||||
)
|
||||
rest.forEach(response => { items = items.concat(response.data.data || []) })
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Auth API
|
||||
export const authApi = {
|
||||
login(username, password) {
|
||||
@@ -77,6 +101,13 @@ export const machinesApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/machines', { params })
|
||||
},
|
||||
// Every machine, paged past the backend's 100-row cap. Batch label printing
|
||||
// and "pick any record" dropdowns must use this: list() with a large
|
||||
// perpage is clamped to 100 and still returns a success response, so
|
||||
// the tail simply goes missing.
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/machines', params)
|
||||
},
|
||||
get(id) {
|
||||
return api.get(`/machines/${id}`)
|
||||
},
|
||||
@@ -120,6 +151,13 @@ export const computersApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/computers', { params })
|
||||
},
|
||||
// Every PC, paged past the backend's 100-row cap. Batch label printing
|
||||
// and "pick any record" dropdowns must use this: list() with a large
|
||||
// perpage is clamped to 100 and still returns a success response, so
|
||||
// the tail simply goes missing.
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/computers', params)
|
||||
},
|
||||
displayKiosks() {
|
||||
return api.get('/computers/display-kiosks')
|
||||
},
|
||||
@@ -268,6 +306,13 @@ export const printersApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/printers', { params })
|
||||
},
|
||||
// Every printer, paged past the backend's 100-row cap. Batch label printing
|
||||
// and "pick any record" dropdowns must use this: list() with a large
|
||||
// perpage is clamped to 100 and still returns a success response, so
|
||||
// the tail simply goes missing.
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/printers', params)
|
||||
},
|
||||
get(id) {
|
||||
return api.get(`/printers/${id}`)
|
||||
},
|
||||
@@ -388,19 +433,8 @@ export const modelsApi = {
|
||||
// Backend caps perpage at 100, so page through every model. Returns the
|
||||
// full array directly (not an axios response). Use in forms whose model
|
||||
// dropdown must include the editing record's model regardless of page.
|
||||
async listAll() {
|
||||
const first = await api.get('/models', { params: { perpage: 100, page: 1 } })
|
||||
let items = first.data.data || []
|
||||
const totalpages = first.data.meta?.pagination?.totalpages || 1
|
||||
if (totalpages > 1) {
|
||||
const rest = await Promise.all(
|
||||
Array.from({ length: totalpages - 1 }, (_, i) =>
|
||||
api.get('/models', { params: { perpage: 100, page: i + 2 } })
|
||||
)
|
||||
)
|
||||
rest.forEach(r => { items = items.concat(r.data.data || []) })
|
||||
}
|
||||
return items
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/models', params)
|
||||
},
|
||||
get(id) {
|
||||
return api.get(`/models/${id}`)
|
||||
@@ -468,6 +502,12 @@ export const applicationsApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/applications', { params })
|
||||
},
|
||||
// Every application, paged past the backend's 100-row cap. The catalogue is
|
||||
// already over 100 entries on a live site, so any picker offering "all
|
||||
// applications" must use this and not list({ perpage: <big number> }).
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/applications', params)
|
||||
},
|
||||
get(id) {
|
||||
return api.get(`/applications/${id}`)
|
||||
},
|
||||
@@ -713,6 +753,13 @@ export const usbApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/usb', { params })
|
||||
},
|
||||
// Every USB device, paged past the backend's 100-row cap. Batch label printing
|
||||
// and "pick any record" dropdowns must use this: list() with a large
|
||||
// perpage is clamped to 100 and still returns a success response, so
|
||||
// the tail simply goes missing.
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/usb', params)
|
||||
},
|
||||
get(id) {
|
||||
return api.get(`/usb/${id}`)
|
||||
},
|
||||
@@ -1046,6 +1093,13 @@ export const networkApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/network', { params })
|
||||
},
|
||||
// Every network device, paged past the backend's 100-row cap. Batch label
|
||||
// printing and "pick any record" dropdowns must use this: list() with a
|
||||
// large perpage is clamped to 100 and still returns a success response, so
|
||||
// the tail simply goes missing.
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/network', params)
|
||||
},
|
||||
get(id) {
|
||||
return api.get(`/network/${id}`)
|
||||
},
|
||||
@@ -1192,6 +1246,13 @@ export const measuringtoolsApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/measuringtools', { params })
|
||||
},
|
||||
// Every measuring tool, paged past the backend's 100-row cap. Batch label printing
|
||||
// and "pick any record" dropdowns must use this: list() with a large
|
||||
// perpage is clamped to 100 and still returns a success response, so
|
||||
// the tail simply goes missing.
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/measuringtools', params)
|
||||
},
|
||||
get(id) {
|
||||
return api.get(`/measuringtools/${id}`)
|
||||
},
|
||||
@@ -1235,6 +1296,11 @@ export const printedpartsApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/printedparts/items', { params })
|
||||
},
|
||||
// Every printed item, paged past the backend's 100-row cap. The label sheet
|
||||
// must print the whole selection, not the first page of it.
|
||||
listAll(params = {}) {
|
||||
return fetchAllPages('/printedparts/items', params)
|
||||
},
|
||||
get(printeditemid) {
|
||||
return api.get(`/printedparts/items/${printeditemid}`)
|
||||
},
|
||||
|
||||
@@ -295,8 +295,10 @@ onMounted(async () => {
|
||||
codetype.value = (await getSetting('label_default_codetype', 'qr')) === 'barcode' ? 'barcode' : 'qr'
|
||||
encodes.value = await resolveDefaultEncodes(assettype)
|
||||
try {
|
||||
const response = await config.api.list({ perpage: 500 })
|
||||
assets.value = response.data.data || []
|
||||
// listAll, not list: perpage is clamped to 100 server-side, so a batch
|
||||
// sheet built from list() silently omitted every asset past the first
|
||||
// 100 and printed a short run that looked complete.
|
||||
assets.value = await config.api.listAll()
|
||||
} catch (err) {
|
||||
console.error('Error loading assets:', err)
|
||||
} finally {
|
||||
|
||||
@@ -270,8 +270,10 @@ async function loadFilterOptions(fields) {
|
||||
filterOptions.value.locations = response.data.data || []
|
||||
}
|
||||
if (fields.includes('application') && !filterOptions.value.applications.length) {
|
||||
const response = await applicationsApi.list({ perpage: 100 })
|
||||
filterOptions.value.applications = response.data.data || []
|
||||
// listAll: 100 is the server-side cap, not a generous limit, and the
|
||||
// catalogue is past it - a report filtered by a late-alphabet
|
||||
// application could not be built.
|
||||
filterOptions.value.applications = await applicationsApi.listAll()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading filter options:', error)
|
||||
|
||||
@@ -106,10 +106,13 @@ const applications = ref([])
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load applications for topic dropdown
|
||||
const appsRes = await applicationsApi.list({ perpage: 1000, showhidden: true }) // isactive is the only filter that applies to a topic:
|
||||
// ishidden governs whether an application shows on the tiles page, which
|
||||
// says nothing about whether it can be the subject of an article.
|
||||
applications.value = appsRes.data.data || []
|
||||
// listAll, not list: the backend clamps perpage to 100 without saying so,
|
||||
// and the catalogue is past that, so a topic sorting late in the alphabet
|
||||
// was silently missing from this dropdown.
|
||||
// isactive is the only filter that applies to a topic: ishidden governs
|
||||
// whether an application shows on the tiles page, which says nothing about
|
||||
// whether it can be the subject of an article.
|
||||
applications.value = await applicationsApi.listAll({ showhidden: true })
|
||||
|
||||
// Load article if editing
|
||||
if (isEdit.value) {
|
||||
|
||||
@@ -171,10 +171,13 @@ async function loadArticles() {
|
||||
|
||||
async function loadTopics() {
|
||||
try {
|
||||
const response = await applicationsApi.list({ perpage: 1000, showhidden: true }) // isactive is the only filter that applies to a topic:
|
||||
// ishidden governs whether an application shows on the tiles page, which
|
||||
// says nothing about whether it can be the subject of an article.
|
||||
topics.value = response.data.data || []
|
||||
// listAll, not list: the backend clamps perpage to 100 without saying so,
|
||||
// and the catalogue is past that, so topics sorting late in the alphabet
|
||||
// were silently missing from this filter.
|
||||
// isactive is the only filter that applies to a topic: ishidden governs
|
||||
// whether an application shows on the tiles page, which says nothing about
|
||||
// whether it can be the subject of an article.
|
||||
topics.value = await applicationsApi.listAll({ showhidden: true })
|
||||
} catch (error) {
|
||||
console.error('Error loading topics:', error)
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ onMounted(async () => {
|
||||
locationsApi.list({ perpage: 500 }),
|
||||
modelsApi.listAll(), // backend caps perpage at 100; page through all
|
||||
businessunitsApi.list({ perpage: 500 }),
|
||||
computersApi.list({ perpage: 500 }),
|
||||
computersApi.listAll(), // backend caps perpage at 100; page through all
|
||||
assetsApi.types.list() // Used for relationship types, will fix below
|
||||
])
|
||||
|
||||
|
||||
@@ -428,8 +428,9 @@ async function loadVendors() {
|
||||
try {
|
||||
const response = await vendorsApi.list({ perpage: 100 })
|
||||
vendors.value = response.data.data || []
|
||||
const modelResponse = await modelsApi.list({ perpage: 500 })
|
||||
models.value = modelResponse.data.data || []
|
||||
// listAll: the models catalogue is already past the backend's 100-row
|
||||
// cap, so list() left the tail of it unselectable.
|
||||
models.value = await modelsApi.listAll()
|
||||
} catch (err) {
|
||||
console.error('Error loading vendors:', err)
|
||||
}
|
||||
|
||||
@@ -318,7 +318,10 @@ onMounted(async () => {
|
||||
const [typesRes, buRes, appsRes, tzRes] = await Promise.all([
|
||||
notificationsApi.types.list(),
|
||||
businessUnitsApi.list().catch(() => ({ data: { data: [] } })),
|
||||
applicationsApi.list({ perpage: 500 }).catch(() => ({ data: { data: [] } })),
|
||||
// listAll: perpage is clamped to 100 server-side, and the catalogue is
|
||||
// already past that, so this dropdown was missing its tail.
|
||||
applicationsApi.listAll().then(items => ({ data: { data: items } }))
|
||||
.catch(() => ({ data: { data: [] } })),
|
||||
settingsApi.get('site_timezone').catch(() => null)
|
||||
])
|
||||
|
||||
|
||||
@@ -78,8 +78,9 @@ function labelText(item) {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await printedpartsApi.list({ perpage: 500 })
|
||||
items.value = response.data.data || []
|
||||
// listAll: perpage is clamped to 100, and a label sheet must cover every
|
||||
// item, not the first page of them.
|
||||
items.value = await printedpartsApi.listAll()
|
||||
// ?item=<id> preselects one part (the Detail-page print button)
|
||||
const preselect = new URLSearchParams(window.location.search).get('item')
|
||||
if (preselect) {
|
||||
|
||||
@@ -91,8 +91,9 @@ const pages = computed(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await printersApi.list({ perpage: 500 })
|
||||
printers.value = response.data.data || []
|
||||
// listAll: perpage is clamped to 100, and a batch sheet must cover every
|
||||
// printer, not the first page of them.
|
||||
printers.value = await printersApi.listAll()
|
||||
} catch (error) {
|
||||
console.error('Error loading printers:', error)
|
||||
} finally {
|
||||
|
||||
@@ -145,8 +145,9 @@ const sheetPages = computed(() => {
|
||||
onMounted(async () => {
|
||||
labelStyle.value = (await getSetting('usb_label_style', 'barcode')) === 'qr' ? 'qr' : 'barcode'
|
||||
try {
|
||||
const response = await usbApi.list({ perpage: 500 })
|
||||
devices.value = response.data.data || []
|
||||
// listAll: perpage is clamped to 100, and a batch sheet must cover every
|
||||
// device, not the first page of them.
|
||||
devices.value = await usbApi.listAll()
|
||||
} catch (error) {
|
||||
console.error('Error loading USB devices:', error)
|
||||
} finally {
|
||||
|
||||
@@ -95,7 +95,8 @@ python3 "$REPO/scripts/generate_sbom.py" "$REPO" -o "$OUT/sbom.cdx.json"
|
||||
echo "==> Staging docs ..."
|
||||
mkdir -p "$OUT/docs"
|
||||
for doc in openapi.json llms.txt api-inventory.json \
|
||||
INSTALL-WINDOWS.md OPERATE-WINDOWS.md BACKUP-RESTORE.md UPGRADE.md; do
|
||||
INSTALL-WINDOWS.md OPERATE-WINDOWS.md BACKUP-RESTORE.md UPGRADE.md \
|
||||
FLOOR-MAP.md; do
|
||||
[ -f "$REPO/docs/$doc" ] && cp -a "$REPO/docs/$doc" "$OUT/docs/"
|
||||
done
|
||||
# Never let a missing optional doc fail the build - the loop's last test governs
|
||||
|
||||
@@ -63,7 +63,7 @@ __contract_version__ = '0.20.0'
|
||||
# plugin-contract version above are distinct series with independent
|
||||
# bump rules. Not part of the shopdb.api contract surface, so it is
|
||||
# not re-exported there.
|
||||
__version__ = '0.11.0'
|
||||
__version__ = '0.11.1'
|
||||
|
||||
|
||||
def create_app(config_name: str = None) -> Flask:
|
||||
|
||||
Reference in New Issue
Block a user