Operator Docs Site: Your Runbook Inside the UI¶
Your deployment already has documentation — a runbook, an architecture page,
a “what does this label mean” note. ui.docs lets the worker serve that
documentation itself, at /docs/, and link into it from the exact UI
surfaces it explains: a label chip, a sink row, a timeline event type, a
declared page.
ui:
docs:
site_dir: /srv/operator-docs # any prebuilt static site
title: "Ranking Pipeline Docs" # nav-entry label
anchors:
- match: {label: module, value: vendor}
path: architecture/scanners/#vendor-scanning
title: "Vendor scanning"
Two things follow from that block. The site appears as a nav entry in the
operator UI, opening /docs/ in a new tab. And every label chip whose
module is vendor grows a small book icon that opens
architecture/scanners/#vendor-scanning in a side drawer, next to the live
view rather than replacing it.
The worker serves the built site — it never builds anything. An mkdocs
site/, a Sphinx _build/html/, a directory of hand-written HTML: if a
static file server could serve it, so can the worker.
Configuration¶
ui.docs¶
| Field | Type | Default | Description |
|---|---|---|---|
site_dir |
str |
'' |
Directory of a prebuilt static site served at /docs/. Empty disables the feature. |
title |
str |
Docs |
Nav-entry label, and the drawer heading for an anchor that declares no title of its own. |
anchors |
list |
[] |
Contextual links from UI surfaces into the site; at most 200. |
site_dir and title take the usual environment overrides —
DK_UI__DOCS__SITE_DIR, DK_UI__DOCS__TITLE — so one image can carry the
anchors while each environment points site_dir at its own staged site.
Declare anchors in the YAML file: like the other structured lists in
ui, the loader does not read structured lists from the environment.
One anchor¶
| Field | Type | Default | Description |
|---|---|---|---|
match |
object | required | Which UI surface shows this anchor; exactly one selector. |
path |
str |
required | Relative path into the site, with at most one #fragment. |
title |
str |
'' |
Hover text on the icon and the drawer heading; falls back to ui.docs.title. |
match carries exactly one of:
| Selector | Names | Where the icon appears today |
|---|---|---|
label |
A task label key | The label chips on a task’s detail page |
label + value |
One specific value of that key | The same chips, but only those carrying that value |
sink |
A configured sink instance name (sinks.<type>.<name>) |
The sinks table on the dashboard |
event |
A declared timeline event type name | The Live timeline’s event-type list, and the hover strip of an instance |
page |
A declared UI page slug | The page’s own heading |
The right-hand column describes where the current UI bundle draws the icon; the backend only declares the binding, so a newer bundle may surface the same anchor in more places.
Most specific wins: an anchor for {label: module, value: vendor} beats
the key-wide {label: module} on a vendored task, and the key-wide one
still covers every other value. The two are different matches, not
duplicates.
path is confined to the site: a URL scheme (https:, and single-colon
ones like javascript: too), a leading /, or a .. segment all fail
config load, naming the rule they broke. So does a second anchor whose
match is identical to an earlier one — it could never be reached.
Building the site with MkDocs¶
Nothing here is required — any static site works — but MkDocs is the common case, and a few of its settings interact with the mount.
# mkdocs.yml of your deployment's own docs
site_name: Ranking Pipeline Docs
theme:
name: material
font: false # no webfont fetches; deployments are often firewalled
use_directory_urls: true # the default
use_directory_urls: true(the default) writes each page asarchitecture/scanners/index.html, so anchor paths readarchitecture/scanners/#vendor-scanning. A directory request serves itsindex.html, so this works unchanged under/docs/. Set it tofalseand the same anchor becomesarchitecture/scanners.html#vendor-scanning.- Relative links only. MkDocs writes relative links between pages by
default, which survive the
/docs/prefix. A hand-written absolute link to/architecture/does not — it leaves the mount. - Self-contained assets. The site is served verbatim, so whatever it
fetches from a CDN it will still try to fetch from the operator’s browser.
font: false(or an equivalent) keeps a firewalled deployment from rendering half-styled. - Headings are the anchor targets. A
#fragmentmust exist as a realidin the built HTML. With MkDocs that is the heading slug — check the built file, not the Markdown, when an anchor scrolls nowhere. - The drawer is narrow. It opens over the live view at roughly 40% of the window, so a section an anchor points at reads better if it stands on its own rather than continuing an argument from three headings up.
Anchor cookbook¶
Explain a label your operators keep asking about. One anchor for the key covers every value:
Single out one value of it. Narrower, so it wins wherever it applies:
- match: {label: module, value: vendor}
path: architecture/scanners/#vendor-scanning
title: "Vendor scanning"
Point a sink at its runbook — what the database is for, who owns it, what to do when its deliveries start failing:
Document a timeline event type, so an operator who sees an unfamiliar band knows what produced it:
Document a declared page you added with ui_pages:
Linking into the site from your own templates¶
Whenever site_dir is set, the site is also available to every link
template as the built-in {docs} base — probe-details
links, declared page widgets, and timeline event
link templates alike:
{docs} resolves to the mount path this worker serves. It is reserved:
ui.link_bases may not define a base named docs, and config load rejects
one that tries — otherwise a deployment could silently redirect every
built-in docs link somewhere else.
Serving rules¶
GET /docs/ is registered unconditionally, whatever the configuration
says, so the path never falls through to the operator UI’s catch-all and
answers with the UI shell.
| Request | Answer |
|---|---|
/docs (no trailing slash) |
307 to /docs/, so the site’s relative links resolve |
| A file inside the site | The file, verbatim |
A directory (/docs/, /docs/architecture/) |
That directory’s index.html |
| A file that is not there | 404 — never the UI shell, so a wrong docs URL reads as wrong |
A path escaping the site (.., an absolute path, a symlink pointing out) |
404 |
| Feature off, or the directory missing | 404 with a JSON hint naming ui.docs.site_dir |
site_dir is turned into an absolute path once, at startup, and the
symlinks along it are followed then — so a path whose symlinks change
later needs a restart. Whether the directory is there is re-checked on
every request: a site staged after boot at the same path starts serving on
its own, and one deleted or swapped out under a running worker answers the
hint-404 rather than an error page.
Symlinks are followed and then checked: a site whose assets/ is a
symlink into a shared directory elsewhere on the host answers 404 for
everything under it. Stage real files inside site_dir.
Auth and trust¶
The mount sits behind the same gate as the operator UI’s own pages: open
when ui.auth_token is empty, token-required when it is set — including
the hint-404, which must not confirm the feature’s state to an anonymous
caller.
Browser-issued requests carry the token as ?token=<token> — the same
query-parameter path downloads and the WebSocket already use, because a
browser cannot set a header on a framed page or a nav link. Current UI
bundles build every docs link that way, so the nav entry and the drawer
work unchanged on a token-gated worker.
Reaching the site outside the UI
An older UI bundle, a bookmark, or a curl has to supply the token
itself: /docs/?token=<token>, an Authorization: Bearer <token>
header where the client can set one, or an ingress that injects the
credential for the whole worker.
Treat the site’s content as trusted code. It is served from the
worker’s own origin and framed inside the operator UI, so a script in it
runs with the same privileges as the UI itself, including access to the
browser storage the UI keeps its token in. This is the same posture
ui.custom_renderers_path has:
config points at deployment-owned files, and deployment-owned files are as
trusted as the config that named them. Do not point site_dir at a
directory anyone outside the deployment can write to.
Degradation¶
The whole feature is optional at every step, and nothing about it can stop a worker:
- No
site_dir(the default): no nav entry, no icons, nothing on the identity payload./docs/still answers — with the hint-404. - An anchor naming something that does not exist: config load accepts
it — the cross-check needs the whole deployment, so it runs at startup —
and startup logs one warning per unresolved anchor —
docs_anchor_unknown_sink,docs_anchor_unknown_event,docs_anchor_unknown_page— naming the target and where to declare it. The anchor renders nothing. Label anchors are not checked: label keys come from runtime task labels, not from config, so there is no declared set to check them against. - A
paththat names a file the site does not have: the mount answers 404 (never the UI shell), and the drawer shows that. A#fragmentwith no matchingidis quieter still — the page opens, scrolled to the top — so verify fragments against the built HTML. - An older UI bundle that predates the feature ignores the identity key and renders as it always did.
Swagger moved to /api-docs¶
/docs used to serve the built-in Swagger UI over the worker’s OpenAPI
document. That path now belongs to the operator docs site, and Swagger
moved to GET /api-docs (its assets to /api-docs/swagger-ui-bundle.js
and /api-docs/swagger-ui.css). The document itself is unchanged and still
served at GET /api/v1/openapi.json.
This is a breaking change for anything bookmarking or scripting /docs as
the API docs — update those to /api-docs. Nothing in the operator UI
linked the old path.
In the integration harness¶
The integration harness runs the whole feature: a
deliberately hand-written site under integration/worker/docs-site/
(three pages and a stylesheet — not an mkdocs build, to show that any
static directory works), wired up in integration/worker/drakkar.yaml’s
ui.docs block with four anchors covering a label key, one specific label
value, a sink instance, and a timeline event type. The repository root is
already mounted read-only inside the workers, so site_dir points straight
at it and docker compose up is the whole setup.
See also¶
- UI Enrichment for
ui.link_basesand the link templates the built-in{docs}base plugs into. - Custom Timeline Events for the event types an
eventanchor can name. - Declared UI Pages for the page slugs a
pageanchor can name. - Observability — Operator UI for the rest of the UI the anchors are scattered across.
- Security Posture for what the UI exposes and what
ui.auth_tokendoes and does not cover.