Files
shopdb-flask/docs/GUIDES.md
cproudlock bd0e9695e5 Document the guides API, and move Guides beside Slides
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.
2026-08-26 12:39:42 -04:00

238 lines
8.9 KiB
Markdown

# 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:
```json
{ "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:
```json
{ "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.
```bash
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 duplicate `id`
- 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 carries `saved: false`.
- The hash covers title, summary, tags **and** blocks - renaming or retagging is
a change a reader notices.
- `POST /api/guides/<id>/publish` **promotes 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
```bash
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`, `TBD` or `FIXME`
- 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-warning` divs map to warning callouts, `code-block` to code
blocks, `step` divs 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 example
- `plugins/guides/services/blocks.py` - the validation rules, and why each exists
- `plugins/guides/services/authoring.py` - the single write path