Docs: guides in the indexes, the stalest-kind rule, and the changelog
Guides was shipped without an entry in PLUGINS.md or START-HERE, so a site adopting the product had no way to find out it exists. BACKUP-KINDS.md described a host's badge as coming from the most recently confirmed kind. It has not worked that way since the fleet report was reworked: the freshest revision per (host, kind) is taken, and then the STALEST of those kinds decides, because reporting the freshest would let a healthy NTLARS backup mask a part-marker capture that stopped a month ago. Stalest among kinds that can be JUDGED - a kind never confirmed is unmeasurable, not stale, and treating it as the worst value made a host with a working backup show no badge at all. The backups row also still advertised UDC and CMM kinds that do not exist. api-inventory, openapi.json and PROJECT-MAP regenerated.
This commit is contained in:
88
CHANGELOG.md
88
CHANGELOG.md
@@ -12,6 +12,94 @@ ADR-007 and ADR-002.
|
||||
|
||||
### Added
|
||||
|
||||
- **Audit log retention.** The audit log is append-only and nothing ever removed
|
||||
a row, so it grew for as long as a site ran - here to the point where the
|
||||
audit page was tens of thousands of pages deep and therefore useless, because
|
||||
nobody pages through that to find anything. Site & Facility gains
|
||||
`audit_retentiondays` plus a card showing the log's size and exactly how many
|
||||
entries a purge would remove, and there is `flask audit stats` /
|
||||
`flask audit purge --apply` for the server. **Opt-in: the default is 0, which
|
||||
keeps everything.** An upgrade that silently started deleting a site's audit
|
||||
history would be indefensible. Deletion runs in committed batches, because one
|
||||
`DELETE` over a million rows holds table locks long enough to block the
|
||||
collectors posting inventory; a run that stops early reports how much is left
|
||||
rather than claiming it finished. Purging needs its own `audit.purge`
|
||||
permission - reading the log and destroying it are different acts.
|
||||
|
||||
- **Guides: images, and a reader who cannot see drafts.** Guide images are
|
||||
fetched through the authenticated API client instead of being pointed at by
|
||||
`<img src>`. An image tag sends no Authorization header and this app keeps its
|
||||
JWT in one, so every guide image was a broken icon for every user; the same
|
||||
change makes stored image links survive a subpath mount. A failed image now
|
||||
says so in words rather than showing a broken icon. Revision and image routes
|
||||
are gated on their parent guide - revision ids are sequential, so reading one
|
||||
guide used to be enough to walk the ids and read every unpublished draft on
|
||||
the site. Search, the guide list and the tag chips now match the PUBLISHED
|
||||
text, not the working copy, so a reader can no longer get a hit on wording
|
||||
that is not in the page when they open it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A guide's URL no longer moves when its title is edited.** The slug was
|
||||
regenerated on every save that carried a title, and the editor autosaves every
|
||||
three seconds - so fixing a typo in a heading silently moved the guide and
|
||||
broke exactly the pasted links the stored slug exists to protect. The slug is
|
||||
now set once, at create.
|
||||
|
||||
- **Publishing a guide after a failed save reported success.** `publish()` ran
|
||||
regardless of whether the pre-publish save reached the server, so the author
|
||||
saw "Published version N" while readers got the previous revision. A failed
|
||||
save now stops the publish and says why.
|
||||
|
||||
- **The ADR-015 site-literal check was enforcing nothing.** Its patterns are
|
||||
assembled from fragments so the published script does not contain the strings
|
||||
it greps for, but they used the apostrophe-escape form (`'a'\''b'`) instead of
|
||||
concatenation (`'a''b'`), embedding a literal apostrophe in every pattern.
|
||||
Nothing could ever match and the check passed everything from the day it was
|
||||
written. Fixed, and it immediately found 25 real violations - among them a QA
|
||||
ServiceNow tenant URL sitting in the shipped defaults in seven places.
|
||||
|
||||
- **ServiceNow ships off and blank.** The defaults pointed at one organisation's
|
||||
QA ServiceNow tenant with the integration enabled, so a site that had never
|
||||
configured ServiceNow still rendered ticket links - to a tenant its
|
||||
technicians cannot reach, and for an adopting site, into somebody else's
|
||||
instance. There is no cross-site default for "which ServiceNow is yours".
|
||||
|
||||
- **Credentials are no longer stored in backup revisions.** The NTLARS/DNC
|
||||
registry carries an FTP password in cleartext, and it was stored verbatim -
|
||||
rendered by the revision panel and handed back by the download route as a
|
||||
ready-to-import `.reg`. Credential values are now redacted at parse, so they
|
||||
never reach the database, and the rendered file NAMES each withheld value
|
||||
instead of carrying a placebo one: importing a fake password would produce a
|
||||
machine that looks restored and cannot connect. `scripts/redact_backup_secrets.py`
|
||||
scrubs revisions recorded before this (dry run by default). Rotate any
|
||||
password that was stored.
|
||||
|
||||
- **A search domain that fails is no longer indistinguishable from one that
|
||||
found nothing.** Each domain catches broadly and returns no rows so one broken
|
||||
table cannot take the whole search box down - but the technician was then told
|
||||
they might have misspelled a query that was right, and concluded the record
|
||||
did not exist. Failed domains are named in the response, shown as a banner,
|
||||
and suppress the did-you-mean.
|
||||
|
||||
- **An empty `sourcehostname` defeated backup dedup permanently.** The dedup
|
||||
query maps `''` to `IS NULL` while the insert stored the raw `''`, so those
|
||||
rows could never match their own chain: every post inserted, the table grew
|
||||
without bound and the history filled with identical entries. Normalized once
|
||||
at ingest, whitespace included.
|
||||
|
||||
- **`printedparts` migration 0003 was unguarded.** Unlike its twenty siblings it
|
||||
ran a bare `ALTER`, so `flask plugin upgrade-all` failed on any database built
|
||||
by `create_all` and took 0004 down with it. The migration test that should
|
||||
have caught this only covered single-migration chains; it now covers every
|
||||
post-cutover chain.
|
||||
|
||||
- **Guide links with several parameters opened the wrong page.** Text is escaped
|
||||
before formatting, so a captured URL arrives carrying entities and was then
|
||||
escaped a second time - `&amp;` in every multi-parameter ServiceNow and
|
||||
SharePoint link. Emphasis could also reach inside an `href`, and
|
||||
`//host` passed as an in-app link when the browser reads it as off-site.
|
||||
|
||||
- **Retire an asset from the interface.** Machines, PCs, printers and measuring
|
||||
tools had no way to take a record out of ShopDB without calling the API, so a
|
||||
scrapped tool stayed in every list and report. The endpoints already existed
|
||||
|
||||
@@ -74,7 +74,7 @@ they would have to go and look up.
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `backupkind` | Which kind was most recently confirmed. `null` = no backup at all. |
|
||||
| `backupkind` | The kind the verdict is about - the STALEST measurable one, not the freshest. `null` = no backup at all. |
|
||||
| `backuplastseen` | When it was last CONFIRMED still current (ISO). Tooltip only. |
|
||||
| `backupcollectedat` | When the config was last CAPTURED (ISO). Only moves on a real change. Tooltip only. |
|
||||
| `backupok` | `true` good, `false` stale, `null` nothing to judge. |
|
||||
@@ -86,6 +86,32 @@ backup taken 12 Aug 5:20 PM is still current."* - the first date proves the
|
||||
check is running, the second says what is actually stored. Collapsing them was
|
||||
what made a healthy machine look neglected.
|
||||
|
||||
### A host with several kinds is judged by its WORST one
|
||||
|
||||
A marker bay runs both an NTLARS backup and a part-marker backup. Reporting the
|
||||
freshest of them would let a healthy kind mask a dead one: the part-marker
|
||||
capture could stop for a month and the host would stay green on the strength of
|
||||
its NTLARS row. So the row is reduced twice - the freshest revision per
|
||||
`(host, kind)`, and then the **stalest** of those kinds decides the verdict.
|
||||
|
||||
Stalest **among kinds that can be judged**. A kind whose newest revision has
|
||||
never been confirmed - `lastseenat` NULL, which means a backfilled row or one
|
||||
written before the column existed - is not stale, it is UNMEASURABLE, and it
|
||||
must not win the comparison. Treating NULL as the worst value looked right and
|
||||
was wrong: a host with a healthy NTLARS backup plus one never-confirmed row
|
||||
showed NO BADGE at all, because the verdict for an unconfirmed kind is `None`.
|
||||
A working backup rendering as nothing is the one outcome the badge must never
|
||||
produce. When EVERY kind is unmeasurable the host still reports which kind it
|
||||
has, with a `null` verdict and no badge.
|
||||
|
||||
This is the same rule `staleness.py` applies per chain: a chain becomes
|
||||
measurable the first time its PC posts, and says nothing until then.
|
||||
|
||||
Known consequence: a device retired from a PC leaves its last revision behind,
|
||||
so that host stays stale until the revision is pruned. That is the honest
|
||||
failure direction - it complains about a backup that genuinely is not running
|
||||
any more, rather than going quiet about one.
|
||||
|
||||
`backupok` is deliberately **tri-state**. `null` means there is no revision for
|
||||
that host, or the check is disabled - and it renders as no badge, never green.
|
||||
"Never seen" must not read as healthy.
|
||||
|
||||
@@ -22,7 +22,8 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin
|
||||
| `slides` | Slides for the lobby display and shop-floor screensaver | Upload / reorder / delete per surface. Management gated on the `slides.manage` permission, grantable to non-admin curators; display routes are public. |
|
||||
| `tools` | Technician utilities: barcode and QR label generation, unit conversions, Secure Send | Mostly browser-side by design, so an air-gapped bench gets them with no network. Secure Send is the exception and is why this plugin has a blueprint, a table and a migration chain: see [SECURE-SEND.md](SECURE-SEND.md). |
|
||||
| `warranty` | Asset warranty tracking | Manual entry now, Dell / Lenovo / HP provider lookups later. Derived coverage status with report buckets. |
|
||||
| `backups` | Per-asset configuration backups with revision history: NTLARS/DNC registry, part marker, UDC, CMM | Kind-pluggable per [BACKUP-KINDS.md](BACKUP-KINDS.md); a kind is one class in `services/registry.py`. Parseable kinds store a JSON projection and re-render to the native format on download; opaque vendor formats stay on the SFLD share with ShopDB holding metadata and the UNC pointer. Fed by the collector, so ingest authorizes on a `collector.ingest` service token, not a user JWT. `backups.view` and `backups.download` are separate permissions because a `.reg` is a restorable artifact. Not to be confused with [BACKUP-RESTORE.md](BACKUP-RESTORE.md), which is the ShopDB database itself. |
|
||||
| `guides` | Step-by-step procedures written in ShopDB: typed blocks, pasted screenshots, live checkboxes, tags, drafts and revision history | Content is stored as typed blocks rather than markup, so the same guide renders to screen, dark mode and paper. Readers see the PUBLISHED revision only - a technician following a procedure must not have it change under them mid-task. Checkbox ticks are per-viewer and never edit the guide. See [GUIDES.md](GUIDES.md). |
|
||||
| `backups` | Per-asset configuration backups with revision history: NTLARS/DNC registry and part marker | Kind-pluggable per [BACKUP-KINDS.md](BACKUP-KINDS.md); a kind is one class in `services/registry.py`. Parseable kinds store a JSON projection and re-render to the native format on download; opaque vendor formats stay on the SFLD share with ShopDB holding metadata and the UNC pointer. Fed by the collector, so ingest authorizes on a `collector.ingest` service token, not a user JWT. `backups.view` and `backups.download` are separate permissions because a `.reg` is a restorable artifact. Not to be confused with [BACKUP-RESTORE.md](BACKUP-RESTORE.md), which is the ShopDB database itself. |
|
||||
|
||||
## Plugin permissions
|
||||
|
||||
|
||||
@@ -88,6 +88,6 @@ Manifest-less directories under `plugins/` are core frontend surface and always
|
||||
|
||||
## Size
|
||||
|
||||
- test functions defined: **1305** (parametrised cases collect higher)
|
||||
- documented API paths: **285** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`)
|
||||
- test functions defined: **1333** (parametrised cases collect higher)
|
||||
- documented API paths: **299** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`)
|
||||
|
||||
|
||||
@@ -31,6 +31,13 @@ Then [ADOPTING-AT-ANOTHER-SITE](ADOPTING-AT-ANOTHER-SITE.md), which has worked
|
||||
examples for Intune, GE-Enforce and manual installation, and says where each
|
||||
artifact comes from.
|
||||
|
||||
## I am writing a procedure for other people to follow
|
||||
|
||||
Guides live in ShopDB itself: [GUIDES.md](GUIDES.md). Typed blocks rather than
|
||||
markup, pasted screenshots, checkboxes a technician can tick as they work, and a
|
||||
draft/publish split so a procedure cannot change under somebody halfway through
|
||||
it. Use it instead of leaving a Word document on a share.
|
||||
|
||||
## I am writing a plugin
|
||||
|
||||
1. [PLUGIN-QUICKSTART](PLUGIN-QUICKSTART.md) - scaffold and first run.
|
||||
|
||||
@@ -535,6 +535,22 @@
|
||||
"params": "none",
|
||||
"example": "curl http://localhost:5001/api/auditlogs/stats -H \"Authorization: Bearer $JWT\""
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/auditlogs/retention",
|
||||
"auth": "jwt + audit.view",
|
||||
"params": "none",
|
||||
"purpose": "Audit log size (total entries, oldest/newest timestamps) and, when a retention period is configured, how many entries a purge would delete. Drives the Site & Facility retention card so the count is seen before anything is deleted",
|
||||
"example": "curl http://localhost:5001/api/auditlogs/retention -H \"Authorization: Bearer $TOKEN\""
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/auditlogs/purge",
|
||||
"auth": "jwt + audit.purge",
|
||||
"params": "body: days (optional, defaults to the audit_retentiondays setting), dryrun (optional bool)",
|
||||
"purpose": "Delete audit entries older than the retention window, in committed batches. Requires audit.purge, not merely audit.view. Batch-capped so the request cannot outlive a proxy timeout; an incomplete run returns complete=false with the number still remaining rather than reporting a finished job. Refuses with 400 when no retention period is configured",
|
||||
"example": "curl -X POST http://localhost:5001/api/auditlogs/purge -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"days\":365,\"dryrun\":true}'"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/plugins",
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
},
|
||||
{
|
||||
"name": "core-map"
|
||||
},
|
||||
{
|
||||
"name": "guides"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
@@ -3039,6 +3042,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auditlogs/retention": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Audit log size (total entries, oldest/newest timestamps) and, when a retention period is configured, how many entries a...",
|
||||
"description": "Audit log size (total entries, oldest/newest timestamps) and, when a retention period is configured, how many entries a purge would delete. Drives the Site & Facility retention card so the count is seen before anything is deleted\n\n**Auth:** jwt + audit.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs/retention -H \"Authorization: Bearer $TOKEN\"\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/auditlogs/purge": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"core-platform"
|
||||
],
|
||||
"summary": "Delete audit entries older than the retention window, in committed batches. Requires audit.purge, not merely...",
|
||||
"description": "Delete audit entries older than the retention window, in committed batches. Requires audit.purge, not merely audit.view. Batch-capped so the request cannot outlive a proxy timeout; an incomplete run returns complete=false with the number still remaining rather than reporting a finished job. Refuses with 400 when no retention period is configured\n\n**Auth:** jwt + audit.purge\n\n**Params:** body: days (optional, defaults to the audit_retentiondays setting), dryrun (optional bool)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auditlogs/purge -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"days\":365,\"dryrun\":true}'\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: days (optional, defaults to the audit_retentiondays setting), dryrun (optional bool)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/plugins": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -19901,6 +19992,744 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/guides": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "List guides. Published guides plus the caller's own drafts; someone with guides.edit sees every draft",
|
||||
"description": "List guides. Published guides plus the caller's own drafts; someone with guides.edit sees every draft\n\n**Auth:** jwt + guides.view\n\n**Params:** query: q (searches title, summary, tags and body), tag\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/guides?tag=partmarker' -H \"Authorization: Bearer $TOK\"\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."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Create a guide as a draft. Every block needs a unique, stable id: the id is what a reader's tick is remembered against...",
|
||||
"description": "Create a guide as a draft. Every block needs a unique, stable id: the id is what a reader's tick is remembered against, and a save without one is refused\n\n**Auth:** jwt + guides.create\n\n**Params:** body: title (required), summary, tags (comma separated), blocks (array), note\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"title\":\"Swap a controller\",\"tags\":\"partmarker\",\"blocks\":[{\"id\":\"b1\",\"type\":\"paragraph\",\"text\":\"Power it off.\"}]}'\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: title (required), summary, tags (comma separated), blocks (array), note"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/guides/tags": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Every tag in use with a count, for filter chips",
|
||||
"description": "Every tag in use with a count, for filter chips\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/tags -H \"Authorization: Bearer $TOK\"\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/guides/{slug}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Read a guide as a READER: returns the PUBLISHED revision, not the working copy, so an edit in progress does not change...",
|
||||
"description": "Read a guide as a READER: returns the PUBLISHED revision, not the working copy, so an edit in progress does not change what somebody following the procedure sees. 404 (not 403) for an unpublished guide the caller cannot edit\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/replace-a-marker -H \"Authorization: Bearer $TOK\"\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": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/guides/{guideid}/working": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "The working copy, for the editor. Requires guides.edit, or guides.create while the guide is your own and unpublished",
|
||||
"description": "The working copy, for the editor. Requires guides.edit, or guides.create while the guide is your own and unpublished\n\n**Auth:** jwt + guides.view (+ edit rights)\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/12/working -H \"Authorization: Bearer $TOK\"\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": "guideid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/guides/{guideid}/revisions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Version history, newest first, flagging which revision is published",
|
||||
"description": "Version history, newest first, flagging which revision is published\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/12/revisions -H \"Authorization: Bearer $TOK\"\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": "guideid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/guides/revisions/{revisionid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "One revision including its blocks, for viewing or comparing an old version",
|
||||
"description": "One revision including its blocks, for viewing or comparing an old version\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/revisions/34 -H \"Authorization: Bearer $TOK\"\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": "revisionid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/guides/{guideid}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Save the working copy. Writes a new revision ONLY when the content hash changes, so an autosave that changed nothing...",
|
||||
"description": "Save the working copy. Writes a new revision ONLY when the content hash changes, so an autosave that changed nothing records nothing; the response carries saved=false in that case\n\n**Auth:** jwt + guides.view (+ edit rights)\n\n**Params:** body: any of title, summary, tags, blocks, note\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/guides/12 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"blocks\":[]}'\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": "guideid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "body: any of title, summary, tags, blocks, note"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Delete a guide and its revisions and images",
|
||||
"description": "Delete a guide and its revisions and images\n\n**Auth:** jwt + guides.delete\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/guides/12 -H \"Authorization: Bearer $TOK\"\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": "guideid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/guides/{guideid}/publish": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Point readers at the latest saved revision. Promotes an existing revision rather than snapshotting, so a publish cannot...",
|
||||
"description": "Point readers at the latest saved revision. Promotes an existing revision rather than snapshotting, so a publish cannot ship unsaved edits nobody reviewed\n\n**Auth:** jwt + guides.publish\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/12/publish -H \"Authorization: Bearer $TOK\"\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": "guideid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/guides/{guideid}/unpublish": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Withdraw from readers without deleting anything; revisions survive so the guide can be corrected and published again",
|
||||
"description": "Withdraw from readers without deleting anything; revisions survive so the guide can be corrected and published again\n\n**Auth:** jwt + guides.publish\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/12/unpublish -H \"Authorization: Bearer $TOK\"\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": "guideid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/guides/{guideid}/restore/{revisionid}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Bring an old revision back as a NEW revision. History is never rewritten, so what was published in between stays on...",
|
||||
"description": "Bring an old revision back as a NEW revision. History is never rewritten, so what was published in between stays on record\n\n**Auth:** jwt + guides.view (+ edit rights)\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/12/restore/34 -H \"Authorization: Bearer $TOK\"\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": "guideid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "revisionid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/guides/images": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Upload an image for a guide. guideid is optional because an image is pasted before a new guide has an id; the editor...",
|
||||
"description": "Upload an image for a guide. guideid is optional because an image is pasted before a new guide has an id; the editor attaches it on save\n\n**Auth:** jwt + guides.create\n\n**Params:** multipart: file (png/jpeg/gif/webp, max 8 MB), guideid (optional)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/images -H \"Authorization: Bearer $TOK\" -F file=@screenshot.png -F guideid=12\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": "multipart: file (png/jpeg/gif/webp, max 8 MB), guideid (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/guides/images/{imageid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"guides"
|
||||
],
|
||||
"summary": "Serve an uploaded image; cached immutably since an image is replaced rather than edited",
|
||||
"description": "Serve an uploaded image; cached immutably since an image is replaced rather than edited\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/images/5 -H \"Authorization: Bearer $TOK\" --output shot.png\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": "imageid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user