Admin UI¶
Operator UI mounted at /admin/. Pages: status dashboard, configuration,
caches, authorization, file browser, and a per-image view. Server-rendered
HTML with a sidebar layout; the only client-side JavaScript is the
dashboard's live-update loop (no framework, no build step). Styling is a
single stylesheet embedded at internal/admin/static/style.css.
Auth¶
HTTP Basic, gated by env vars:
IIIRIS_ADMIN_USER=admin
IIIRIS_ADMIN_PASS=<password>
When either is empty, /admin/* returns 404 (treated as
not-mounted). Secure by default — operators opt in by setting credentials.
There is intentionally no YAML equivalent for these credentials; env vars are the right surface for secrets.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
| GET | /admin/ |
Status dashboard |
| GET | /admin/stats/stream |
Server-Sent Events stream of dashboard stats (1s cadence; source health every 10s) |
| GET | /admin/config |
Config editor (curated subset) + read-only effective configuration |
| POST | /admin/config |
Validate, persist, and hot-apply a config edit (form: one field per curated key) |
| GET | /admin/cache |
Per-cache stats + purge controls |
| POST | /admin/cache/purge |
Form: cache=<render\|info\|origin>, key=<cache key>. Deletes one entry. |
| POST | /admin/cache/purge-all |
Form: cache=<render\|info\|origin>. Calls Purger.PurgeAll. Returns 501 if the cache backend isn't a Purger. |
| GET | /admin/auth |
IIIF Auth subsystem: profiles, rules, active session count, recent decisions |
| GET | /admin/browse |
Source root listing |
| GET | /admin/browse/{path...} |
List a sub-directory in the source |
| GET | /admin/view/{path...} |
View a single image: thumbnail, IIIF URLs, OpenSeadragon zoomable viewer |
| GET | /admin/static/... |
Vendored static assets (OpenSeadragon JS + sprite images) |
What the dashboard shows¶
- Version (set via
-ldflags="-X main.Version=..."; defaults todev) - Process uptime
- Total HTTP request count since startup
- Per-cache slot: backend name, entries, bytes (
—if the backend doesn't implementcache.Stater) - Per-source health: name, backend, healthy/unhealthy, last error
- Per-route metrics: requests, errors, bytes served, latency p50 / p95 / p99
over the most recent 1024 requests per route, plus a p95 sparkline of the
last 60 live samples. Routes are classified as
health,admin,iiif/3 info,iiif/3 image,iiif/2 info,iiif/2 image, orother— coarse-grained on purpose so the table doesn't explode for a server with many identifiers.
Live updates¶
The dashboard updates itself in place — no page refresh. On load it opens
a Server-Sent Events
connection to GET /admin/stats/stream, which emits a JSON snapshot once
per second: uptime, request count, cache stats, and per-route metrics.
Source health is more expensive to probe (an S3 Healthchecker makes a
real network call), so it rides the stream only every tenth tick (~10s).
A "live / reconnecting…" indicator next to the page title reflects the
connection state; EventSource reconnects automatically if the stream
drops. The per-route p95 sparkline is built client-side from the streamed
samples and resets when the page is reloaded (60 points max).
The stream endpoint is deliberately excluded from the request counter and
per-route stats (classifyRoute returns "" for it) — its connection
stays open for the lifetime of the dashboard tab, and recording that as a
single multi-minute request would distort the admin route's latency
quantiles. The other admin pages (/admin/config, /admin/cache,
/admin/auth, /admin/browse) remain plain server-rendered HTML and need
a refresh to update.
Configuration editor¶
/admin/config is a structured editor over a curated subset of config
fields, plus the full effective configuration rendered read-only as YAML
below it. The editable subset:
| Section | Fields | Apply |
|---|---|---|
| Server | addr, read_timeout, write_timeout, shutdown_timeout |
restart |
| Logging | log.level |
live |
| Logging | log.format |
restart |
| Image pipeline | image.max_pixel_area, image.max_concurrent |
live |
| Image pipeline | image.max_output_area, image.max_output_width, image.max_output_height, image.tile_size |
restart |
Everything else (caches, sources, hook, auth) is not exposed in the form; those fields are preserved verbatim when the file is rewritten — edit the YAML directly and restart to change them.
Live vs. restart. Three fields hot-reload — log.level rides a
slog.LevelVar; image.max_pixel_area (the source-area guard) and
image.max_concurrent (the pipeline's concurrency semaphore, which is
resized in place — it gates renders and info.json probes through separate
same-sized semaphores so neither path starves the other) have no other
coupling. The image output caps and
tile_size are restart-only on purpose: each is paired with an
info.json advertisement (maxArea / maxWidth / maxHeight / tiles),
and hot-reloading the pipeline guard without the advertisement would make
info.json lie about what the server accepts. A save reports which changed
fields still need a restart.
Saving. A save validates every field; on any error nothing is written
and the form re-renders with per-field messages. On success the whole
config is written back to the -config file — atomically (temp file +
rename) and in fully-expanded, normalized YAML (comments and original
field order are not preserved, consistent with how the page already
displays config). The live fields are applied immediately; restart-only
fields are on disk and take effect on the next start.
When the editor is read-only. The form is editable only when iiirisd
was started with -config <path> — there must be a file to persist edits
to. In zero-config mode the fields render disabled with an explanatory
note. A field whose value is currently set by an environment variable
(IIIRIS_ADDR, IIIRIS_LOG_LEVEL, IIIRIS_LOG_FORMAT,
IIIRIS_MAX_PIXEL_AREA) is individually locked: the env var shadows the
file at startup, so editing the file value would have no effect — change
the env var instead.
The HTTP Basic gate on /admin/* is the only protection on this write
surface; there is no separate CSRF token (the same posture as the cache
purge endpoints).
Caches¶
The /admin/cache page lists each cache slot (render / info / origin) with
its backend, entry count, and byte total, plus per-slot purge all and a
purge a single key form.
Cache keys (for /admin/cache/purge)¶
render: the IIIF URL path forpublic-tier entries (/iiif/3/sample.jpg/full/max/0/default.jpg), or the same path suffixed|v1|full/|v1|substitutefor gated tiers — seeiiif-auth.md.info:<host><url-path>for thepublictier; suffixed|v1|fullfor gated identifiers.origin:<source-name>:<identifier>, e.g.s3:photos/cat.tif(origin keys are not auth-tiered).
File browser¶
/admin/browse lists the configured default source as a directory tree.
Clicking a sub-directory descends; clicking a file opens /admin/view/...
which shows:
- A thumbnail rendered through iiiris itself (
/iiif/3/{path}/full/!300,300/0/default.jpg) - The full IIIF URLs for
info.json, the full image, and the thumbnail (clickable to copy / inspect) - An OpenSeadragon zoomable viewer pointed at the IIIF info.json — pan/zoom of the full-resolution image with tile-based loading
Browsing is available when the configured default source implements
source.Lister. Today the filesystem and s3 backends qualify (S3
uses ListObjectsV2 with Delimiter="/" for directory-style listings);
HTTP shows "this backend does not support browsing." source.AsLister
finds the capability through the Cached OriginCache wrapper.
OpenSeadragon (5.0.1) is vendored under internal/admin/static/openseadragon/
and served by iiirisd itself at /admin/static/openseadragon/.... No
external CDN is contacted; the viewer works in air-gapped deployments.
IIIF Authorization vs. admin auth¶
The /admin/* HTTP Basic protection is separate from the IIIF
Authorization Flow 2.0 services that may be mounted at
/iiif/auth/.... The admin Basic-auth gate protects operator endpoints
on this server; IIIF auth gates access to specific images for end users
of the IIIF API. They share no credentials, no session store, and no
configuration — set them up independently. The IIIF auth subsystem is
documented in iiif-auth.md.
The /admin/auth page surfaces the IIIF Auth subsystem in read-only
form:
- Session backend name (
heap/filesystem/s3) and active session count, with per-profile breakdown when the backend supports it (heapandfilesystemdo;s3reports the total only — a per-profile breakdown would require oneGetObjectper session and is intentionally skipped). The total includes expired-but-not-yet- evicted records on the S3 backend (where Sweep is a no-op and lifecycle policy handles long-term cleanup). - Last-24h allow/deny tally, drawn from a bounded ring buffer
(
auth.DecisionLog) sized at 256 most-recent decisions. Anything older than the buffer's capacity rolls off. - Profiles table: name, pattern, access-service backend, the first
English label, the credential source (
inline/htpasswd: <path>/—), and the configured substitute size. - Rules table: match expression → profile name, in evaluation order.
- Recent decisions table: most-recent-first, latest 64 events from
the ring buffer. Each row shows time, identifier, matched profile
(empty = public), verdict (
allow/deny/deny → substitute/deny → redirect), and HTTP status.
The page is read-only; mutation (purging sessions, evicting tokens) is not exposed today. Restart iiirisd to clear the heap session store; for filesystem/S3, delete records on disk / in the bucket.
What the admin UI does not do (yet)¶
- Source backend status (last error, configured endpoints)
- Logs viewer
- Live updates on pages other than the dashboard (config / cache / auth / browse still need a refresh)
- Editing config fields outside the curated
/admin/configsubset — caches, sources, hook, and auth are not in the form; edit the YAML directly and restart - Per-identifier metrics (only per-route classification today)
- IIIF auth interactive controls:
/admin/authis read-only — no UI to mint test tokens, force-revoke a session, or trace probe responses. Hit the service routes directly withcurlfor now.
Limitations¶
- Stats from S3-backed caches show
—(would require listing the bucket). PurgeAllagainst an S3 cache returns 501 — use bucket lifecycle or the AWS CLI.- The file browser only works for sources that implement
source.Lister(filesystem and S3; not HTTP).