Skip to content

What's new in v1.17.2

Released on 28 August 2026.

The focus is the audit webview: authentication with roles, four new pages — device catalogue, destination catalogue, statistics and operations log —, a shared time-range filter, the now-visible Q/R arc and a hardening package. On the router itself a correction that affects every evaluation — the audit-trail timestamps were not in UTC —, plus schema v9 and a series of smaller fixes.

Before the update

Audit timestamps were not in UTC until now

Up to 1.17.1 the router wrote the audit-trail timestamps in the node's local time zone instead of UTC. On a node in Europe/Zurich, all rows written before the update in associations, dimse_operations and router_events are therefore off by 1–2 hours — and with them everything that reads those columns: the webview's display and time-range filter, Grafana, the retention statements (which delete too early as a result) and any correlation with PACS or modality logs. On a node that itself runs in UTC nothing is shifted. Rows written from 1.17.2 on are correct; an updated database therefore has a jump at the point of the update.

If you want to correct the existing rows, there is a one-time correction script in share/audit-timestamp-utc-fix.sql.example. What matters:

  • Before stopping the old instance — that is, before the update, not just before running the script — let the audit spool drain (dicom-router-ctl statusaudit-backlog: 0). A row that 1.17.1 wrote into the spool is delivered by 1.17.2 with a correct timestamp; the script would wrongly shift it along.
  • The script takes the node's time zone as input (as an IANA name, e.g. Europe/Zurich), runs per node and with the router stopped (instructions in the script).
  • It is not idempotent — a second run shifts again.

Whether to correct at all is an operational decision; the router works either way.

Schema v9: the index build briefly blocks writers

The migration to schema v9 is additive except for dropping a never-used column default (behavior-neutral) — beyond that, an index on dimse_operations.rq_at and the view device_capabilities; a running 1.17.1 node keeps writing against a v9 database unchanged. A 1.17.1 node that restarts against the migrated database, however, refuses to boot (downgrade guard) — update such a node instead of restarting it. The index build, however, takes a write lock on dimse_operations. On a large database the update sits there for a while; the audit buffers through its spool in the meantime. To avoid the window, create the index in advance with CREATE INDEX CONCURRENTLY under the same name idx_dimse_rq_at — the migration then finds it and skips it. For Grafana, a GRANT SELECT on device_capabilities is added.

Webview: network-bound only with authentication

Up to 1.17.1 the webview could bind to any address and merely warned in the log. An installation with WEBVIEW_LISTEN=0.0.0.0:8406 (or a fixed network address) no longer starts after the update — under systemd it runs into a restart loop because of Restart=on-failure; the log says "refusing to listen on … without authentication". Three ways out: back to 127.0.0.1 with access through an SSH tunnel, WEBVIEW_AUTH=file with a user file and TLS, or WEBVIEW_AUTH=proxy behind a reverse proxy that already authenticates. The instructions are in webview/README.md. A bind to loopback does not change.

Webview: no more iframe embedding

Every response carries frame-ancestors 'none' and X-Frame-Options: DENY. If you have embedded the viewer in a portal page, you will need a link instead of a frame from now on.

Webview: grants for the new pages

Device catalogue, destination catalogue and statistics read the base tables associations, presentation_contexts, presentation_context_transfer_syntaxes and dimse_operations directly. If the database user webview_ro lacks SELECT on one of them, the page answers with a database error. The complete grant block is in webview/README.md.

dicom-router-ctl: exit code 3 on refusal

drain, resume, reload and shutdown now exit with 3 when the router refuses the command — previously that was a 0, and automation under set -e read success while, say, the old configuration kept running. 1 remains "not reachable". A drain on an already draining router and a resume on an already ready one stay at 0, so retry loops do not break.

The audit webview

Authentication with roles

Three modes: none (default, only permitted on loopback), file with a login form against a bcrypt user file, proxy with the identity taken from a header of a trusted reverse proxy. On top of that the roles phi and nophi: nophi sees patient name and patient ID masked and cannot search by them — the masking sits in the SQL query, not merely in the display. An access log records who searched for what and when, and optionally the webview terminates TLS itself.

Device catalogue and destination catalogue

/devices shows which device calls from which IP, with which implementation, which presentation contexts it proposes, which are accepted and which are rejected — the latter answers, without SQL, why a device cannot send something. The device page condenses "what goes where" into one row per SOP class and destination, counted in associations rather than individual contexts. /destinations is the opposite direction: the places the router sends to — with the destination's implementation identifier, what it accepts and what it does not, and the DIMSE status codes it returns, the difference between "reachable" and "healthy". The two catalogues cross-link the same peer, and both lists are sortable via the column headers.

Statistics

/stats shows, over the selected time range, traffic matrix, top talkers, SOP classes, response times, DIMSE errors and rejected associations — the operational overview for installations without Grafana. Response times list the 95th percentile next to the mean, because neither the mean nor the maximum answers "does this destination stall regularly".

Operations log

/events lists the centrally collected WARN and ERROR messages of all nodes — including the events without an association (startup, reload failures, database warnings) that were previously only visible via SQL. Filtering works by time range, level, node and message text; the text search is only available to the phi role, because the messages are masked for nophi.

Shared time range and the Q/R arc

One time-range filter — 24 hours, 7 days, 30 days (default), everything, or a free from–to — applies to both catalogues, the statistics and the operations log and is remembered; a time range in the URL wins, so shared links work. And the Q/R arc is visible: the detail view links the two associations of a retrieve in both directions and shows the MessageID assigned by the router, through which the C-STOREs of one leg can be matched to the C-MOVE of the other.

Convenience with and without JavaScript

A slim, static JavaScript layer adds copy buttons to the monospace fields (UIDs, AE titles, IPs; they require HTTPS or localhost), makes preset and selection filters submit immediately on change, and shows the browser's local time as a tooltip on every UTC timestamp. Inline scripts remain blocked by the Content-Security-Policy, and every page keeps working without JavaScript — sorting and filtering happen server-side.

On top of that, a hardening package: security headers on every response, HTTP timeouts and a graceful shutdown, a login lockout that holds up under parallel attempts, and CSRF protection for the login.

Database

The new index on dimse_operations.rq_at speeds up queries over narrow time windows on the largest table considerably (measured on 1.69 million rows: 4.6 ms instead of 107 ms for one hour). The new view device_capabilities answers "what can device X do" in one place: one row per device, destination and SOP class with both outcomes, counted in associations — the existing views remain for existing dashboards. And the timestamp correction itself: the write path subtracted the zone offset twice; new rows are in UTC, and for existing rows the section above under "Before the update" applies.

Further fixes

  • --render-haproxy to stdout stays clean: neither regular log output nor Logback status messages end up in the redirected HAProxy configuration any more.
  • Locale hardening: the state words of the management responses and the command names in commands: selectors are processed locale-independently — under a Turkish system locale both previously misfired.
  • The webview again accepts hostnames for WEBVIEW_LISTEN that resolve exclusively to loopback (say, an /etc/hosts entry pointing to 127.0.0.1); the error message on refusal now names the actual resolution.
  • dicom-router-ctl drain-and-shutdown recognises "already draining" by the precise state word and no longer treats a refusal as success.

There is also a series of smaller fixes in the webview and the scripts; the complete list is in the package's CHANGELOG.