assessment-model/docs/adr/0011-app-owned-task-marker-in-task-source.md
Khalim Conn-Kowlessar 34dd3fe7ae feat(live-reporting): bulk document download from the Documents tab
Add bulk document download as a row-selection flow on the Live Reporting
Documents tab: a "Bulk download" button flips the existing DocumentTable
into selection mode (per-row tickers + select-all across all filtered
pages); "Download selected" zips those properties' documents and emails a
link (shown in-app while the page stays open). Scoped by the tab's
existing Project/group selector.

Two-step trigger (ADR-0008 convention): insert an app-owned tasks row
(task_source "app:bulk_document_download", selection config JSON in
tasks.inputs, source_id = portfolio) then dispatch only { task_id } to
POST /v1/documents/bulk-download. Documents are matched by
landlord_property_id, so the ticked ids ARE the selection — a property
with no landlord id has no documents and is un-tickable.

Resilience: the client now parses responses defensively (a platform 504's
HTML body no longer surfaces as "Unexpected token 'A'…"), and the POST
route sets maxDuration=60 so the synchronous backend resolution has
headroom before the browser gets a 504.

Docs: CONTEXT.md gains Project + Bulk document download; ADR-0011 records
why the marker lives in task_source (worker re-reads the task) vs service.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:11:42 +00:00

2.4 KiB
Raw Blame History

11. App-owned, backend-executed tasks carry their marker in task_source

Date: 2026-07-08

Status

Accepted

Context

Bulk document download follows the app-created-task convention (ADR-0008): Next.js inserts a tasks row whose inputs column carries the selection config, then hands the backend only { task_id }. Unlike a Modelling run — where the distributor receives the whole config in the dispatch payload and never looks at the task again — the document worker gets only the id, re-reads the tasks row, and resolves the selection from inputs. It therefore needs a durable, in-row way to recognise "this is a bulk-document-download task I'm allowed to act on" before it assembles anything.

Modelling runs put that marker in service (modelling_run) and leave task_source human-readable ("Modelling run 2 scenarios"). Reusing service here would work, but it invites a future engineer to "unify" the two conventions and move the marker — silently breaking the worker's guard.

Decision

  • The marker is task_source = "app:bulk_document_download" — a single, stable string the worker guards on. The app: prefix denotes an app-created task (as opposed to a backend-created one). It is engineer-facing only (Settings → Logs), so a machine-shaped string is acceptable; no friendly-label mapping is added.
  • service is left null. For a task whose marker already lives in task_source, a second marker in service is redundant and can drift. In-app status polling scopes by id = task_id AND source_id = <portfolio> AND task_source = "app:bulk_document_download".
  • Portfolio attachment is unchanged and uniform: source = "portfolio_id", source_id = <portfolio_id>. This is what the Portfolio Logs page filters on, so the run shows up there for free — the same as every other app-owned task.

Consequences

  • Do not move this marker into service to match Modelling runs. The asymmetry is deliberate: the marker lives where the executor reads it — service for a payload-fed distributor, task_source for a task-re-reading worker.
  • A large selection never travels in an HTTP body: it lives in inputs, and the only thing dispatched is task_id (ADR-0008 lineage).
  • Future app-owned, backend-executed tasks should follow this shape: task_source = "app:<name>", config in inputs, source/source_id for scoping.