Skip to content

feat(ui): History & Versions diff — the prototype (#236)#278

Open
angela-helios wants to merge 2 commits into
mainfrom
feat/236-history-diff
Open

feat(ui): History & Versions diff — the prototype (#236)#278
angela-helios wants to merge 2 commits into
mainfrom
feat/236-history-diff

Conversation

@angela-helios

@angela-helios angela-helios commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Closes #236. This is the prototype you asked for on #246 ("implement a prototype and we can evaluate from there"), built to the decision recorded in docs/history-diff-rendering.md.

What it does

Compares two versions of a resource from _history, rendering the diff server-side in the two layers the decision doc settled on — no client-side diff library, works as a plain page with JS off degrading to the base view.

default diff

Semantic layer (json-patch) — the field-level change list, the exact inverse of the RFC 6902 PATCH the server already applies:

REPLACE  /name/0/family   Smith → Smythe
REPLACE  /name/0/given/0  John  → Johanna
ADD      /name/1          {"family":"Smith","given":["John"],"use":"nickname"}
ADD      /telecom/1       {"system":"email","value":"j@x.org"}

json-patch was already in the workspace lock (a default dep of rest/persistence), so it adds zero new transitive weight.

Textual layer (similar, inline) — paired lines, +/- gutters, per-side line numbers, and word-level highlighting of the run that changed within a line. Selecting v1 → v3 shows it well: "Jon""John" highlights only the inserted letter.

v1 to v3

Following the decision doc

  • Metadata noise floor: meta.versionId/lastUpdated move every version → filtered by default, behind a "show metadata" toggle that says how many ops it hid.
  • Version selection: adjacent (n vs n-1) by default; two selects, htmx-swapped.
  • Where computed: server-side, in the helios-ui handler. No new FHIR-facing endpoint — the browser reads versions from the ordinary _history/vread API and posts them to /ui/history/diff.
  • Deleted versions: a banner, not a diff against a tombstone. Identical versions say so.

Following Brett's design

The Figma "History & Versions" frame: the Instance / Type feed / System feed tabs, the version rail (newest first, each tagged create / update / patch), and the diff panel. The nav's History & Versions entry is now a real link.

Verified in a real browser

Driven end to end against a Patient with three real versions (create → update → update). One detail only surfaced by driving it: this server carries the version id and timestamp in the history Bundle's entry.response etag/lastModified, not in resource.meta — so the rail reads there, with resource.meta as a fallback for servers that stamp it onto the resource.

Tests

17 unit tests over the diff layer — including the exact array-churn scenario the decision doc's spike characterizes — plus 6 HTTP round-trip tests, locale parity across en/es/de. fmt + clippy clean.

Deliberately deferred (matches the decision doc)

  • Array-churn move detection: a front-inserted name[] still cascades across name/0..2 instead of a single add /name/0. The doc explicitly accepts this for v1; move detection is a presentation post-process that can be added later without changing the architecture, and the textual layer already shows the insert legibly.
  • The Type feed / System feed tabs are present (Brett's design) but not yet wired to /{type}/_history and /_history.
  • The metadata toggle does nothing visible against this server, because it does not stamp meta onto the resource body — but the filter is implemented and unit-tested against resources that carry meta (which most real resources do).

Steve asked on PR #246 to "implement a prototype and we can evaluate from
there." This is it, built to the decision recorded in
docs/history-diff-rendering.md.

The diff between two versions is rendered server-side, in two layers, into
Askama templates — no client-side diff library:

 - Semantic layer (json-patch): the field-level change list, the exact inverse
   of the RFC 6902 PATCH the server already applies. `replace /name/0/family
   Smith → Smythe`. json-patch was already in the workspace lock, so it adds no
   new transitive weight.
 - Textual layer (similar, +inline): the pretty-printed JSON of both versions,
   paired lines with +/- gutters and per-side line numbers, with word-level
   highlighting of the run that changed within a line (`"Jon"` → `"John"`
   marks only the inserted letter). similar has no HTML formatter, so its
   changes are iterated into template rows.

meta.versionId / meta.lastUpdated move on every version, so they are filtered
from the semantic view by default, behind a "show metadata" toggle that says
how many ops it hid. Adjacent versions (n vs n-1) are the default comparison;
two selects, htmx-swapped. A deleted version renders as a banner, not a diff
against a tombstone. Identical versions say so.

The screen follows Brett's "History & Versions" frame: the Instance / Type feed
/ System feed tabs, the version rail (newest first, each tagged with the
interaction that produced it — create / update / patch), and the diff panel.
Versions are read through the ordinary _history and vread FHIR API; the browser
fetches them and posts them to /ui/history/diff, so the essence — the diff — is
computed server-side while no new FHIR-facing endpoint is added.

Verified end to end in a real browser against a Patient with three real
versions (create → update → update). The version id and timestamp are read from
the history Bundle's entry.response etag/lastModified, which is where this
server carries them (resource.meta is a fallback) — a detail that only surfaced
by driving it.

17 unit tests over the diff layer (including the exact array-churn scenario the
decision doc characterizes) + 6 HTTP round-trip tests. UI strings in en/es/de
(parity enforced). The nav's History & Versions entry is now a real link. fmt +
clippy clean.

Deliberately deferred, matching the decision doc: array-churn move detection
(a front-insert still cascades — accepted for v1), and the Type feed / System
feed tabs are present but not yet wired to /_history and /{type}/_history.
# Conflicts:
#	Cargo.lock
#	crates/ui/Cargo.toml
#	crates/ui/src/lib.rs
#	crates/ui/templates/layouts/base.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Research: htmx-friendly diff rendering for the History & Versions view

1 participant