You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Bulk Export page offers Since but no Until, so a user cannot bound an export to a time window from the UI.
Blocked on #657._until is currently applied by only the S3 backend; SQLite, PostgreSQL, and MongoDB accept it and silently ignore it. Shipping this control before that is fixed would produce silently wrong exports on the default backend.
Current behavior
/ui/bulk-export's "Narrow it down" panel offers _elements, _typeFilter, and a Since preset with a custom-instant escape hatch (crates/ui/templates/pages/bulk-export.html:65-93). There is no Until control anywhere on the form.
Nothing in the UI carries the parameter: StartForm (crates/ui/src/bulk_export.rs:284-293) and parse_start_form (bulk_export.rs:295-312) have no until field, ExportJob (bulk_export.rs:55) stores only since, and kickoff() builds the query from _type, _elements, _typeFilter, and _since only (bulk_export.rs:369-380).
Expected behavior
An Until control beside Since, producing an _until query parameter, giving a bounded [_since, _until] export window. Leaving it empty keeps today's open-ended behavior.
Evidence
The REST layer already parses _until and threads it into ExportRequest.until — crates/rest/src/handlers/bulk_export.rs:112-120 and :251, declared at crates/persistence/src/core/bulk_export.rs:354-356. The kick-off path needs no server-side change; only the backend query paths do, which is #657.
Suggested approach
Follow the Since precedent exactly:
Template — an Until block after the Since fields (bulk-export.html:76-93). Since is a preset <select> plus a custom-instant text input; Until has no natural preset list, so a single instant input with the same 2026-08-01T00:00:00Z placeholder is probably right.
StartForm + parse_start_form (bulk_export.rs:284-312) — add until. The form is parsed by hand because repeated types fields defeat axum::Form, so a new key is a one-line match arm.
ExportJob (bulk_export.rs:55) — add until beside since. Check that stored jobs still deserialize with the new field absent; existing jobs must not break.
kickoff() (bulk_export.rs:369-380) — push ("_until", &job.until) when non-empty.
Active Exports card (templates/partials/bulk_export_card.html) — surface it wherever Since is shown, so a running job states its window.
The Bulk Export form offers an Until input; leaving it empty is unchanged behavior.
Submitting an Until sends _until on the kick-off and the export respects it end to end.
Since and Until together produce a bounded window.
An invalid instant is reported to the user rather than producing a failed job (parse_instant returns a 400; kickoff() records non-202s onto the job at bulk_export.rs:405-412).
The window is visible on the Active Exports card.
Existing stored export jobs still deserialize after the ExportJob change.
Labels exist in en, es, and de.
crates/ui/e2e/tests/a11y.spec.ts passes with the new field.
Scope / out of scope
Out of scope: the backend gap, split out into #657.
Related: #655 (the Patients scope needs a patient-id field), #537 (built this workspace).
Summary
The Bulk Export page offers Since but no Until, so a user cannot bound an export to a time window from the UI.
Blocked on #657.
_untilis currently applied by only the S3 backend; SQLite, PostgreSQL, and MongoDB accept it and silently ignore it. Shipping this control before that is fixed would produce silently wrong exports on the default backend.Current behavior
/ui/bulk-export's "Narrow it down" panel offers_elements,_typeFilter, and a Since preset with a custom-instant escape hatch (crates/ui/templates/pages/bulk-export.html:65-93). There is no Until control anywhere on the form.Nothing in the UI carries the parameter:
StartForm(crates/ui/src/bulk_export.rs:284-293) andparse_start_form(bulk_export.rs:295-312) have nountilfield,ExportJob(bulk_export.rs:55) stores onlysince, andkickoff()builds the query from_type,_elements,_typeFilter, and_sinceonly (bulk_export.rs:369-380).Expected behavior
An Until control beside Since, producing an
_untilquery parameter, giving a bounded[_since, _until]export window. Leaving it empty keeps today's open-ended behavior.Evidence
The REST layer already parses
_untiland threads it intoExportRequest.until—crates/rest/src/handlers/bulk_export.rs:112-120and:251, declared atcrates/persistence/src/core/bulk_export.rs:354-356. The kick-off path needs no server-side change; only the backend query paths do, which is #657.Suggested approach
Follow the Since precedent exactly:
bulk-export.html:76-93). Since is a preset<select>plus a custom-instant text input; Until has no natural preset list, so a single instant input with the same2026-08-01T00:00:00Zplaceholder is probably right.StartForm+parse_start_form(bulk_export.rs:284-312) — adduntil. The form is parsed by hand because repeatedtypesfields defeataxum::Form, so a new key is a one-line match arm.ExportJob(bulk_export.rs:55) — adduntilbesidesince. Check that stored jobs still deserialize with the new field absent; existing jobs must not break.kickoff()(bulk_export.rs:369-380) — push("_until", &job.until)when non-empty.templates/partials/bulk_export_card.html) — surface it wherever Since is shown, so a running job states its window.locales/{en,es,de}/main.ftl, next to thebulk-export-since-*block. UI copy: no capitalization convention — headings and buttons mix Title Case and sentence case #652 is settling the capitalization convention; new labels should land conforming.Acceptance criteria
_untilis honored by the active backend._untilon the kick-off and the export respects it end to end.parse_instantreturns a 400;kickoff()records non-202s onto the job atbulk_export.rs:405-412).ExportJobchange.en,es, andde.crates/ui/e2e/tests/a11y.spec.tspasses with the new field.Scope / out of scope
Out of scope: the backend gap, split out into #657.
Related: #655 (the Patients scope needs a patient-id field), #537 (built this workspace).