Skip to content

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.

Admin status dashboard: version, uptime, request count, per-cache stats, per-source health, and per-route latency metrics

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 to dev)
  • Process uptime
  • Total HTTP request count since startup
  • Per-cache slot: backend name, entries, bytes ( if the backend doesn't implement cache.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, or other — 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

Configuration editor: curated server, logging, and image-pipeline fields, each tagged live or restart

/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

Caches page: per-slot backend, entry count, and byte totals with purge-all and single-key purge controls

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 for public-tier entries (/iiif/3/sample.jpg/full/max/0/default.jpg), or the same path suffixed |v1|full / |v1|substitute for gated tiers — see iiif-auth.md.
  • info: <host><url-path> for the public tier; suffixed |v1|full for gated identifiers.
  • origin: <source-name>:<identifier>, e.g. s3:photos/cat.tif (origin keys are not auth-tiered).

File browser

File browser: directory listing of the configured source with file names and sizes

/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

Per-image view: thumbnail, IIIF URLs for info.json/full/thumbnail, and an OpenSeadragon zoomable viewer

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:

Authorization page: session backend and allow/deny tally, profiles and rules tables, and a recent-decisions log

  • Session backend name (heap / filesystem / s3) and active session count, with per-profile breakdown when the backend supports it (heap and filesystem do; s3 reports the total only — a per-profile breakdown would require one GetObject per 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/config subset — 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/auth is read-only — no UI to mint test tokens, force-revoke a session, or trace probe responses. Hit the service routes directly with curl for now.

Limitations

  • Stats from S3-backed caches show (would require listing the bucket).
  • PurgeAll against 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).