The repo already enforces that every served route appears in docs/api-inventory.json, and the guides endpoints did not - so the parity test was failing. They are now listed with auth, params and a working example each, beside knowledgebase because the two get read for the same reason. docs/GUIDES.md covers what the inventory cannot: the block schema and why it is blocks rather than HTML, the id rule that makes a tick rememberable, what the server refuses and why validation happens at the door, the working-copy / revision / published-pointer split that stops a live procedure changing under somebody following it, and a worked example of creating and publishing a guide from a script. It also states plainly what does not exist yet - no importer, orphaned images never pruned - and what converting existing documents will actually be like: structured HTML and Markdown map onto block types nearly one for one, while a plain-text runbook lands as paragraphs and needs marking up by hand afterwards. Generated ids have to stay stable across re-imports, or every re-import resets readers' progress. Guides moves to position 8 in the sidebar, beside Slides, rather than down among the reference lists in Information. A guide is something somebody goes and does something with, not a catalogue they look things up in.
8.9 KiB
Guides: writing procedures in ShopDB, and generating them from elsewhere
A guide is a step-by-step procedure stored in ShopDB: images, working checklists, version history, and printable output. This is the reference for the data model and the API, aimed at somebody generating guides programmatically - from a script, an importer, or a model.
Not to be confused with the knowledgebase plugin, which is a link registry: a description, a URL and keywords pointing at a document held somewhere else. That one references documents; this one holds them. They stay separate, and share only a tag vocabulary and the global search box.
1. The shape of a guide
A guide is an ordered list of typed blocks, stored as JSON. Not HTML, and that is load-bearing:
- a tickable step needs a stable id to be remembered between visits
- dark mode needs semantic markup, not pasted colours
- print needs to know where a step begins so it does not split one across a page
Free-form HTML gives none of those, and pasted styling fights the theme.
Block types
| type | Fields | Notes |
|---|---|---|
heading |
text |
Section heading |
paragraph |
text |
Body text |
step |
title, text |
Numbers itself by position among steps |
checklist |
items[] |
The tickable one. Items need their own ids |
bullets |
items[] |
Unordered list |
numbers |
items[] |
Ordered list |
callout |
text, title, variant |
variant: info, warning, success |
code |
text, language, caption |
Never autocorrected; wraps rather than clips |
image |
url, alt, caption |
url comes from the image upload endpoint |
table |
header[], rows[][] |
Simple grid |
Every block needs:
{ "id": "b1", "type": "paragraph", "text": "..." }
Ids must be unique and stable. They are what a reader's tick is stored against, so a save without them is refused, and changing one loses that item's progress. Checklist items carry their own ids for the same reason:
{ "id": "b4", "type": "checklist",
"items": [ { "id": "i1", "text": "Confirm the Verified date" } ] }
An unknown block type renders as nothing rather than an error, so a guide written against a newer version stays readable on an older one.
Inline text
Block text supports a deliberately small subset: **bold**, *italic*,
`code`, [links](https://example.com), and line breaks. Nothing else -
no fonts, sizes or colours. Emphasis is semantic and the theme decides how it
looks, which is what makes dark mode work and keeps a guide written next year
matching one written today.
Text is escaped before formatting is applied, so no markup in the source can
reach the DOM. javascript: and data: links are dropped.
2. Creating a guide from a script
Every endpoint is listed in docs/api-inventory.json; this is the workflow.
TOK=$(curl -s -X POST http://localhost:5001/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"...","password":"..."}' | jq -r .data.access_token)
# 1. Create - always starts as a draft
curl -X POST http://localhost:5001/api/guides \
-H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{
"title": "Replace a part marker controller",
"summary": "Restoring patterns and settings after a swap",
"tags": "partmarker, backups",
"blocks": [
{"id":"b1","type":"paragraph","text":"The new unit arrives **empty**."},
{"id":"b2","type":"callout","variant":"warning","title":"Before you start",
"text":"Check the marking head."},
{"id":"b3","type":"checklist","items":[
{"id":"i1","text":"Copy the snapshot path"},
{"id":"i2","text":"Confirm the pattern count"}]},
{"id":"b4","type":"code","language":"powershell",
"text":"Backup-PartMarker.ps1 -Force"}
]
}'
# 2. Publish - readers see nothing until this happens
curl -X POST http://localhost:5001/api/guides/12/publish \
-H "Authorization: Bearer $TOK"
A scoped API token works the same way as a user JWT.
What the server rejects
Validation happens at the door rather than at render time, because a malformed guide stored is a page that fails every time somebody opens it, long after the client bug that caused it:
- a block with no
id, or a duplicateid - an unknown block
type(the error lists the known ones) - a checklist item with no
id - more than 500 blocks, or more than 2 MB of blocks
- text longer than 20,000 characters in one block
Errors name the offending block by position: block 3: needs a stable id.
3. Drafts, versions and publishing
Three separate things, and the distinction is what stops a live procedure changing under somebody following it:
| What it is | |
|---|---|
| Working copy | The guides row. What the editor loads and saves |
| Revisions | Immutable snapshots in guiderevisions, written when content changes |
| Published pointer | Which revision READERS get |
PUT /api/guides/<id>saves the working copy and writes a revision only if the content hash changed. An autosave that changed nothing records nothing, so the history stays readable. The response carriessaved: false.- The hash covers title, summary, tags and blocks - renaming or retagging is a change a reader notices.
POST /api/guides/<id>/publishpromotes an existing revision. It does not snapshot, so a publish cannot ship unsaved edits nobody reviewed.POST /api/guides/<id>/restore/<revisionid>brings old content back as a new revision. History is never rewritten.- Drafts are invisible to readers, to
GET /api/guides/<slug>(404, not 403), and to global search.
4. Images
curl -X POST http://localhost:5001/api/guides/images \
-H "Authorization: Bearer $TOK" -F file=@screenshot.png -F guideid=12
Returns {"url": "/api/guides/images/5", ...} for use as an image block's
url. PNG, JPEG, GIF and WebP, up to 8 MB. guideid is optional because an
image is pasted before a new guide has an id.
Bytes live in the database, not on disk: a row travels with a database backup,
whereas a file under instance/ has been lost to a container rebuild on this
deployment before.
Known gap: images uploaded to a guide that is then abandoned are never pruned.
5. Proofing
The editor runs checks continuously and warns rather than blocking. They exist because none of these are spelling mistakes:
- a smart quote or long dash in a code block - autocorrect does this, the command then fails, and it still looks right to the author (one-click fix)
- an empty checklist item, which reads as a step somebody forgot to write
- a heading with nothing under it, left behind by a reorganise
- an image with no alt text
- a leftover
TODO,TBDorFIXME - no summary or tags, which makes a guide hard to find
Publishing with unresolved errors asks for confirmation; it does not refuse. A procedure that exists imperfectly beats one nobody wrote.
6. Printing
Print always renders light, whatever the screen theme, because a dark page lays a full sheet of toner down and knocks the text out in white.
Also on paper: no buttons or progress bar, backgrounds dropped, steps and code blocks kept whole across page breaks, code wrapped rather than clipped, and checkboxes printed empty even where ticked on screen - a printout is a worksheet somebody ticks by hand. The footer carries the version and the print date, so a stale copy on a noticeboard is spottable.
7. Permissions
| Permission | Allows |
|---|---|
guides.view |
Read guides |
guides.create |
Create, and edit your OWN guide while it is unpublished |
guides.edit |
Edit any guide, including published ones |
guides.publish |
Publish or withdraw |
guides.delete |
Delete |
The split between edit and publish is deliberate: a technician can write a
procedure up while somebody else decides it is fit for the floor.
8. Converting existing documents
There is no importer yet. When one is written, the practical notes are:
- Structured HTML converts cleanly. The GE Aerospace KB template's
alert alert-warningdivs map to warning callouts,code-blockto code blocks,stepdivs to steps. - Markdown converts well: headings, fenced code, lists and tables all have matching block types.
- Plain-text runbooks convert poorly. ASCII underlines and hand-indented lists carry no structure, so everything lands as paragraphs and a person has to mark up the steps afterwards. Worth converting anyway - the text is then searchable and versioned - but budget the cleanup.
- Generate ids yourself and keep them stable across re-imports, or every re-import resets readers' progress.
See also
docs/api-inventory.json- every endpoint with auth, params and an exampleplugins/guides/services/blocks.py- the validation rules, and why each existsplugins/guides/services/authoring.py- the single write path