
New screens for journal records, audit maintenance, language servers, backups, Interoperability productions, the REST APIs of the instance and status checks with a fix for each. A change log with the read-back outcome of every change, a list of who loses access before a role change, background operations in the top bar, accessibility, a prebuilt image on ghcr.io and a demo sandbox you can change. Full list: https://github.com/DawidKrynski/iris-admin-deck/blob/main/CHANGELOG.md
A web UI for administering one InterSystems IRIS 2026.2+ instance through the SysAdmin REST API
(/api/admin/v2). It covers web applications, users, roles and resources, wallet and certificates, tasks,
processes, databases, namespaces, journals, backups, language servers and Interoperability productions. On
top of that it adds a log timeline, a viewer for rotated messages.old_* files and a similar-incident
search, which the API doesn’t have. Every request the page sends is listed in the API console and can be
copied as curl.
I wrote it for the person who gets the call when something on an IRIS instance breaks: they need to see
what went wrong, find out whether it happened before, fix it, and be sure the fix actually took.
demo / demooperator / operator to see how the navigation hides what the %Operator/sandbox/, roles Sandbox*, users sandbox_*). That is enough to see an edit read back, adocker run -d -p 127.0.0.1:52785:52773 ghcr.io/dawidkrynski/iris-admin-deck, then openSuperUser / SYS. Or docker compose up -d --buildzpm "install iris-admin-deck" on an existing 2026.2 instance (details in Run it below).Where the contest’s technology bonuses are:
What changed in each version: https://github.com/DawidKrynski/iris-admin-deck/blob/main/CHANGELOG.md. How the code is laid out: https://github.com/DawidKrynski/iris-admin-deck/blob/main/docs/ARCHITECTURE.md.

The demo data contains a small incident, so you can walk through it in the demo or locally:
reports: the Sales export task, which runs every 5 minutes,<PROTECT> on ^Daily in the REPORTS directory. Below it are the dismount in messages.logSteps 4 and 5 also work on the public demo (REPORTS and the Sales export task are part of its sandbox).
docker run -d -p 127.0.0.1:52785:52773 ghcr.io/dawidkrynski/iris-admin-deck
An image is published for each release tag, for amd64. The command uses latest;
use a release tag instead to keep a specific version, for example :v1.0.1.
Open http://localhost:52785/admindeck/index.html and sign in with SuperUser / SYS.
That login is for the local demo only; change the passwords for anything else.
Requires Docker with Compose.
git clone https://github.com/DawidKrynski/iris-admin-deck.git
cd iris-admin-deck
docker compose up -d --build
Open http://localhost:52785/admindeck/index.html and sign in with SuperUser / SYS. That login is for the
local demo container only; change the passwords for anything else. If port 52785 is taken, pick another one:
ADMINDECK_PORT=8080 docker compose up -d --build.
The first build pulls a 3.6 GB base image and takes a few minutes; the running container uses about 1.2 GB
of RAM. The instance lives inside the container: docker compose stop / start keep your changes,
docker compose down throws them away and the next up starts again from the demo state.
The build seeds demo data through the SysAdmin API itself (https://github.com/DawidKrynski/iris-admin-deck/blob/main/scripts/demo_seed.py):
two X.509 credentials (one expires in 12 days, so the expiry warning has something to show), a wallet
collection with dummy secrets, a rotated messages.old_* log and a limited user demo_operator / operator
(role %Operator) to see how the navigation hides screens you have no privilege for.
It also sets up the incident described above: database REPORTS and a Sales export task that reads it.
The container’s startup hook dismounts REPORTS after every start, so the export starts failing
within 5 minutes. Build with --build-arg DEMO=0 to skip the seed.
If the page doesn’t open:
docker compose ps # the iris service should be "healthy" about a minute after start
docker compose logs --tail 50 iris
curl -sI http://localhost:52785/admindeck/index.html
Smoke test against a running instance:
scripts/smoke.sh http://localhost:52785
From a clone of this repository:
USER> zpm "load /path/to/iris-admin-deck"
or, once it is published to the Open Exchange registry:
USER> zpm "install iris-admin-deck"
Then open http://<host>:<port>/admindeck/index.html. The package creates two web applications:
/admindeck (the static UI) and /admindeck/api (a small extension REST API, JWT enabled).
intersystemsdc/iris-community:latest is still 2026.1, so the https://github.com/DawidKrynski/iris-admin-deck/blob/main/Dockerfile usesintersystemsdc/iris-community:2026.2-zpm. I tested it on that image and onintersystemsdc/irishealth-community:2026.2-zpmdocker compose build --build-arg IMAGE=intersystemsdc/irishealth-community:2026.2-zpm), amd64 only;/proc, so they work on Linux and in%Admin_* resources for the screens they need (e.g. %All).set ^AdminDeck("Settings","HideHostDetails")=1 (in the package namespace)| Screen | What you can do there |
|---|---|
| Dashboard | Health checks, global refs/s and CPU charts, CPU/memory/disk, license use, upcoming tasks, and a “needs attention” list (errors, certificates expiring soon, dismounted databases, recurring log warnings) |
| Status | Checks first: about ten of them (dismounted databases, journaling, size limits, certificates, backups, failed and suspended tasks, license, disk and journal space, the metrics sampler, auditing), failing ones on top, each with the object and number it is based on, one sentence of advice and a fix, or why it was not checked. Then one row per component (instance state, Web Gateway latency, database read latency, SQL runtime, CPU) with a bar per minute for the last hour, plus warnings/errors per hour for the last day |
| Web apps & REST | List, create from a REST or static preset, edit, enable/disable, delete. System applications can’t be deleted |
| Users, Roles & permissions | Users, roles, resources, %Service_* services, an access matrix (which role or public permission gives a user each resource), SQL object/column/admin privileges |
| Secrets & certificates | Wallet collections and secrets (write-only values), X.509 credentials with expiry, SSL/TLS configurations with a test, OAuth2 server definitions and client configurations |
| Tasks | Run now, suspend/resume, edit, history, upcoming runs, suspend/resume the Task Manager itself |
| Processes & locks | Processes (suspend, resume, terminate, broadcast), locks, CSP/web sessions |
| Databases & system | Databases (create, edit, delete, mount/dismount, expand/compact/truncate, integrity check, which namespaces use each one), namespaces with global/routine/package mappings, journal files and a record browser (filter by global, PID or time; old and new value of each change; who last changed a global), backups (last good backup per type, history, the tasks that run them), devices, license, background jobs |
| Interoperability | Productions per namespace: state, items with errors and queue counts, recent errors; start, update, recover, and stop after typing the production name. Enables Interoperability on a namespace that doesn’t have it |
| Language servers | External language servers (Python, Java, .NET gateways): state, start/stop, settings, create, delete, recent activity |
| Logs & insights | Timeline, log viewer (current and rotated files), recurring problems, similar incidents |
| Audit trail | Search audit records, turn individual audit events on or off, turn auditing on if it’s off. Maintenance: IRISAUDIT size and date range, copy records to a namespace, purge records older than N days (never the newest ones; you type the number of records first). Changes made here: every change made through Admin Deck, who made it, the calls and whether the read-back matched |
| API explorer | All 190 paths (273 operations) from the 2026.2 OpenAPI spec, with an example body and a preview before sending. REST APIs on this instance: every web application with a dispatch class, its routes from the OpenAPI spec or the UrlMap, a Try it form (other methods than GET after a confirmation) and Copy curl |
Ctrl+K opens a palette that finds screens, users, roles, web applications and tasks by name.
The Timeline merges seven sources: messages.log, alerts.log, SystemMonitor.log, journal.log,
application errors (^ERRORS from every namespace via SYS.ApplicationError), the security audit and Task
Manager history. Each source shows whether it loaded, is not permitted for your user, or is unavailable.
Where a problem is shown, the matching action sits next to it: a dismounted database gets Mount, a failed task
run gets Run again, an expiring certificate links to its credential, and a burst of failed logins links to
the user.
The audit screen can turn auditing on, but it has no switch to turn it off. To disable auditing, use SMP or
the API console.
Quick tour, if you only have a couple of minutes:
{ } API console in the top bar and copy one of the calls as curl.Things I ran into while building on the API. Most of them are pinned down in tests/integration/
or in a comment next to the workaround in web/js/.
database-dir/info, integrity-check, security/audit/records, …) answer202 Accepted with an empty body. The task id is only in the Location header, so I read it from thereGET /v2/async-result?id=....PUT /v2/web-app: when it creates an application, ServeFiles and UseCookies are validated as the0-3), while GET returns strings ("Always", …) and edits accept strings. The spec’s"Never" for ServeFiles; IRIS reports "No" and rejects "Never"./v2/monitor/dashboard/main: Licensing.LicenseUse and LicenseUseHigh are percentages, butLicenseLimit right next to them is in license units. Easy to misread as “13 of 8 units” (I did).POST /v2/task wants every Task field (notification lists, expiration, output file, …), even for aServerDefinition; the spec calls the field OAuth2ServerDefinition./v2/database-dir) plus a definition (/v2/database).DELETE /v2/database refuses (409) while a namespace still uses the database, so the UI deletes thePOST /v2/database-dir without GlobalJournalState creates aPUT /v2/namespace needs Routines on create, although the spec marks no field as required.GET /v2/security/sql-privileges returns Object/Action, not the Name/PrivilegeGrantedVia: "Schema Privilege". A column200 and records nothing. Admin privileges are granted onePublicPermission is rejected with a bare 400 and no message, so a resource withoutGET reports "" for existing202 and say nothing about how many records they processed. A bad date or anAuditReport record of its own.PUT /v2/ext-lang-server refuses an update without Type (#40301), althoughactivity call per server,start blocks until the process is up (about 10 s for Python) and returns its log as HTML.maxRows returns half of what you ask for (10 gives 5, 200 gives 100), initialOffset ismatchOperator or a column name in the wrong case returns an empty list instead^Api.Admin.Util.AsyncTaskD inOldValue exists only for changes made inside a transaction.<collection>.<secret>./api/admin sends no CORS headers, so a UI has to be served by IRIS itself or through a same-origin proxy./admindeck on the same web server.
The API has no ETags or object versions. So before a PUT, the form reads the object again and refuses to
save if a field you changed was also changed by someone else since you opened the form. After the write it
reads the object once more and tells you whether the fields match what you sent or which ones differ.
Passwords and wallet values can’t be read back, so for those you only get “accepted”. Background operations
are also reported as accepted, not read back, and the API explorer sends raw calls without any of this.
Each change, including a refused or failed one, is recorded with its outcome in Audit trail, Changes made
here (paths only, never request bodies); the IRIS security audit stays the authoritative record.
This is not a lock. A change made in the few milliseconds between the re-read and the write is not caught.
Before a role is deleted, loses a permission or is taken from a user, the confirmation lists the enabled
users who lose access and what they lose. The UI refuses any change that would leave no enabled user holding
%All.
Deleting a named object asks you to type its name. Terminating a process first checks that the PID still
belongs to the same job. The UI won’t disable or delete its own web applications, %Service_WebGateway
or the account you are signed in with.
The API has no log endpoints, so the package adds /admindeck/api (JWT, requires %Admin_Operate:USE).
Embedded Python parses the log files and groups messages into patterns: numbers, paths and quoted values are
masked, identical messages are counted once, sorted by severity and then count.
Similar incidents is lexical, not semantic. Each log line is hashed into a 256-dimension vector (word
unigrams and bigrams, character trigrams), stored in a %Vector column with an HNSW index and queried with
VECTOR_COSINE. It finds the same message with different numbers or paths well. It will not match
“disk full” with “no space left on device”. Nothing is sent outside IRIS and there is no model to download.
messages.log is indexed on install and a Task Manager task, Admin Deck: reindex logs, refreshes the index
daily at 03:15. Other or rotated files can be indexed from Similar incidents with Build / refresh index.
/proc, statvfs).JobType number, which the API doesn’t/api/monitor/metrics. Behind a proxy that only forwards /api/admin/admindeck, that column stays empty.SuperUser/SYS login bound tobrowser ── same origin ──► IRIS web server
├── /admindeck/ static single-page app (plain ES modules)
├── /api/admin/v2/* built-in SysAdmin API (all management actions)
└── /admindeck/api/* extension REST API (JWT)
├── logs Embedded Python log parser (current + rotated files)
├── apperrors application errors of all namespaces (SYS.ApplicationError)
├── os Embedded Python CPU / memory / disk metrics
├── search IRIS Vector Search over log entries
├── backups backup definitions, tasks and history (read-only)
├── interop production status, items, queues, errors; start/stop/update/recover
└── changes log of changes made through Admin Deck, with their read-back outcome
Changes go through /api/admin/v2, with one exception: Interoperability productions, which the SysAdmin API
doesn’t cover. Start, stop, update and recover call Ens.Director, and the server checks the caller’s
%Admin_Operate and %Ens_ProductionRun on every request. The change log is written to ^AdminDeck("Changes")
(journaled, the newest 5000 entries). Everything else in the extension API only reads:
log files from an allow-list in the manager directory, SYS.ApplicationError, /proc, backup history
(Backup.Task) and the vector index. The front end is plain ES modules with no build step and no
third-party runtime dependencies; IPM installs it as files. A small sampler (AdminDeck.Metrics) records a
sample every 5 seconds and keeps the last 720 in IRISTEMP, which is not journaled, so the charts show the
last hour as soon as you open them.
Sign-in uses the SysAdmin API’s JWT login. The browser keeps the short-lived tokens in sessionStorage
(access 60 s, refresh 15 min) and never the password. A script injected into the page could read those
tokens, which is why the page carries a strict Content-Security-Policy (no inline scripts, same-origin only)
and renders server text as text, never as HTML. An HttpOnly cookie would need a session layer in front of the
SysAdmin API, which takes tokens in a header.
More detail: https://github.com/DawidKrynski/iris-admin-deck/blob/main/docs/ARCHITECTURE.md.
Timeline filtered by reports: the failed export, the dismount and who did it |
Ctrl+K palette |
An edit read back after saving |
Status: checks with evidence and a fix, then the last hour per component |
Similar incidents |
Recurring problems in messages.log |
Tasks |
API console |
Access matrix |
X.509 credentials with expiry |
Web apps & REST |
API explorer |
Databases |
Journal records |
Interoperability productions |
Changes made here, with their read-back outcome |
REST APIs on this instance |
# UI served live from ./web (edit and reload) docker compose -f https://github.com/DawidKrynski/iris-admin-deck/blob/main/docker-compose.yml -f docker-compose.dev.yml up -dUnit tests: JavaScript (edit checks, incident actions, palette ranking) and Python (log parsing, embeddings)
node --test tests/js cd python && python -m pytest -q tests && cd ..
Integration tests against the running container: the endpoints the UI reads, the write flows
on throw-away objects, async tasks, the extension API, auth and CORS
python3 -m unittest discover -s tests/integration -v
End-to-end UI tests (headless Chromium)
uvx --with playwright python tests/e2e/test_ui.py
ObjectScript unit tests
docker compose exec iris iris session IRIS -U USER '##class(%ZPM.PackageManager).Shell("iris-admin-deck test -only -v",1,1)'
Look up any SysAdmin API endpoint in the OpenAPI spec
python3 scripts/endpoint.py /v2/task/run --schemas
CI (GitHub Actions) runs these tests on every push, against IRIS built from the https://github.com/DawidKrynski/iris-admin-deck/blob/main/Dockerfile.
Project layout:
src/AdminDeck/ ObjectScript: REST dispatch, installer, log/OS/vector/metrics classes
https://github.com/DawidKrynski/iris-admin-deck/blob/main/python/admindeck/ Embedded Python helpers (log parsing, OS metrics, embeddings)
web/ Single-page app (index.html, js/, css/, openapi.json)
tests/ %UnitTest classes, JS/Python unit tests, integration and e2e tests
https://github.com/DawidKrynski/iris-admin-deck/blob/main/module.xml IPM package definition
This implements DPI-I-966, Option to show older messages.log in IRIS SMP:
rotated messages.old_* and alerts.old_* files can be browsed, filtered and searched without shell access
to the server.
Submitted to the InterSystems Programming Contest: Build Your Own Management Portal (2026).
MIT, see https://github.com/DawidKrynski/iris-admin-deck/blob/main/LICENSE.
Dawid Kryński: InterSystems Developer Community profile · GitHub