Guides image + draft gating, audit retention, ADR-015 gate, backup credential redaction

This commit is contained in:
2026-08-26 15:08:53 -04:00
parent 4e38303326
commit 0738d71525
37 changed files with 1953 additions and 129 deletions

View File

@@ -12,6 +12,104 @@ 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;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
and were already soft deletes: the record leaves the lists while its
calibration history, backups and relationships stay put, and it can be
restored by setting `isactive` back. Called Retire rather than Delete for
that reason, and the confirm says what is kept. USB is not included - it
retires through a status column in the external CMMC database and needs its
own decision.
- **`flask catalog dedupe`, `merge` and `rename`: catalog hygiene.** The legacy
data carried space-stripped twins - `DellInc.` beside `Dell Inc.`,
`Windows10` beside `Windows 10` - which rode into the shipped seed catalog