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

46 lines
2.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.