Commit Graph

5 Commits

Author SHA1 Message Date
cproudlock
39aaa5d0cb Ship the API docs the container never had, and theme them like the product
/api/docs was broken in every Docker deployment, including the air-gapped ones.
docs.py resolves openapi.json and llms.txt from the repo root, and the image
copies shopdb, plugins, migrations, scripts and wsgi.py but never docs. The page
rendered the Redoc shell and then answered 404 fetching its own specification.
Only the two files the app actually serves are copied; the rest of that tree is
prose with no business in a runtime image.

The theme was invented colours that matched nothing. It now maps the product's
own tokens, and reads the theme the way the app does: the application stores an
explicit light/dark choice and stamps data-theme, so choosing light on a
dark-desktop kiosk used to give a dark documentation page. The operating system
setting is the fallback, exactly as it is in the app.

Light-mode primary, success and error are a shade darker than the product
tokens, because those tokens are designed as fills and fail contrast as text on
the nested panels where type names and required labels sit. Measured with a
browser harness rather than judged by eye: every text node in both themes now
clears WCAG AA, from an initial 5603 reported failures - most of which were the
harness itself ignoring alpha, which is its own lesson about trusting a number.

Four Redoc elements carry hardcoded colours with no theme key and keep targeted
rules, selected by tag and attribute rather than by generated class hashes that
change on every build.

The build now compiles the Python. Docker never imports the application, so a
syntax error sails through every layer and surfaces as a crash-looping
container; that happened, four restarts, while working on this file.

Redoc's upstream licence file was never vendored although its bundle header
points at it. The placeholder added here says exactly that rather than inventing
the third-party notices it is supposed to carry, and the dead redocly.com badge
is hidden: the licence wants the notice in the distribution, not a rendered
link that a site with no internet cannot follow.

(cherry picked from commit 6c00b10618)
2026-09-20 12:42:16 -04:00
cproudlock
be1eb9b366 Let the API docs follow the system theme
The app has a dark theme. /api/docs was the one page that stayed white when the
rest of the site went dark, which is exactly the page somebody has open beside
the app while they work.

Redoc does not read prefers-color-scheme, and the <redoc spec-url> element it
was using takes no theme at all - so the page now initialises through
Redoc.init, which does. Light keeps the default look; dark names the colours
Redoc otherwise derives from the background and gets wrong: body text, the
sidebar, and the right-hand sample panel, which came out dark-on-dark.

It re-initialises on a live scheme change rather than restyling, because Redoc
bakes its colours into styled-components at init - a CSS override reaches the
chrome and misses the content. The listener has the addListener fallback for the
older MediaQueryList, since a shop-floor kiosk browser is exactly where this
gets read.

Nothing new is loaded: same vendored bundle, same spec, no font, no CDN. A test
pins that, because theming is the kind of change that quietly introduces one.

The try-it page is left light. Swagger UI 5 ships no dark theme, so it would
mean vendoring a community stylesheet that fights upstream on every bump - and
it is a development tool that is off in production anyway.
2026-09-19 12:36:15 -04:00
cproudlock
76f085e224 Serve a spec that knows where the instance is mounted
/api/docs on the production site handed out
https://<host>/api/apitokens for an app that lives at /shopdb. Copy that URL
and it 404s; Swagger's try-it fired at the same wrong address.

The mount is a DEPLOYMENT fact and the spec is a BUILD artifact.
gen_openapi.py writes servers: [{"url": "/"}] because at generation time nobody
knows where the app will be hung, and wsgi.py only moves MOUNT_PATH from
PATH_INFO into SCRIPT_NAME when the process starts. Served as written, the file
was right for a root install and wrong for every subpath one - which is how
production runs.

The endpoint now sets `servers` per request from request.script_root: empty at
root, /shopdb under the subpath method, and correct behind a proxy that sets the
prefix. The paths themselves stay mount-free, because the prefix belongs in the
server entry and duplicating it into each path would double it.

The file is about a megabyte, so it is parsed once and cached by mtime, and each
request gets a shallow copy with only `servers` replaced - two requests arriving
on different mounts cannot race on one dict.

A test pins both cases and the fact that paths stay unprefixed. Planting a wrong
server URL fails it.
2026-09-19 12:30:46 -04:00
cproudlock
68567d9a27 Add a try-it API page, and stop the docs page reaching for the internet
/api/docs renders the spec with Redoc, which is read-only by design. The thing
people mean when they ask for Swagger is firing a request from the page, so
Swagger UI now sits beside it at /api/docs/try, reading the same spec - one
contract, two renderers.

It is OFF outside development and 404s rather than rendering a disabled page: a
production box holds live fleet data, and a docs page that can POST to it is a
different object from a docs page that cannot.

Vendored, not linked. swagger-ui-bundle.min.js and swagger-ui.css are served
from staticdocs/ beside the existing Redoc bundle, because production has no
internet. validatorUrl is null: left unset, Swagger UI hands the spec to the
public swagger validator to render a badge, which here is a request that cannot
succeed and elsewhere would ship the internal API surface off the box.

THE VENDORED REDOC BUNDLE WAS ALREADY REACHING THE INTERNET, found while
proving this page does not. redoc.standalone.js fetched
cdn.redoc.ly/redoc/logo-mini.svg for the "API docs by Redocly" mark, so every
load of /api/docs on the air-gapped server has been waiting on a request that
cannot complete. Replaced with an inline transparent GIF.

Three gate holes closed, each one a dodge that worked:

- the page is built by interpolating url_for output into a JS string literal,
  unescaped. SCRIPT_NAME is proxy-controlled and ProxyFix honours
  X-Forwarded-Prefix, so a mount path carrying a quote closed the literal and
  executed what followed. The spec URL is now json.dumps'd and the rest
  HTML-escaped; the injected quote renders as \" and is inert
- the "no internet" assertion on the page scanned for https?://, so
  <script src="//cdn.jsdelivr.net/..."> - the form a copy-paste from any CDN's
  own documentation produces - matched nothing and passed
- the same assertion on the vendored files was a five-host CDN allowlist. An
  upstream bump to any other host walked through it, and the one real leak in
  that directory was on no list. It now matches the SHAPES a browser fetches -
  @import, css url(), src/href, @font-face - whatever the host, ignoring the
  data: URIs this CSS is full of

And the Redoc page's link to the try-it view is built with url_for, which raises
BuildError when those routes are not mounted - every production box. Made
unconditional it would 500 the docs page in exactly the deployment nobody tests
locally, and no test caught that. One does now.
2026-09-18 20:18:23 -04:00
4610108a0c geenforce display dispatcher: purge legacy HKLM Run autostart 2026-07-30 14:30:04 -04:00