mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-08-02 12:58:24 +00:00
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>
46 lines
2.4 KiB
Markdown
46 lines
2.4 KiB
Markdown
# 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.
|