Uploads: per-recording delete, nav link, and the unverified deploy facts (ingress body limit, spool volume) #37

Closed
opened 2026-10-01 15:09:40 +00:00 by mathias · 3 comments
Owner

Deferred from #33 / ADR-033, each deliberately not built in the PR.

  1. Per-recording delete. A private-content store without a delete button is a gap. Account deletion cascades (uploaded_transcripts.user_id ... ON DELETE CASCADE, tested), but a user cannot remove one recording. Needs POST /uploads/{id}/delete, owner-scoped (404 for others), with the isolation test extended.
  2. Discoverability. /uploads is mounted but nothing links to it; the header nav is static in Layout and has no access to App. Needs a conditional nav link or a link from the list page.
  3. Tapir's own ingress body limit is unverified. The default cap is 200 MB. The public LLM gateway ingress rejects ~10 MB (measured, 413); Tapir's ingress lives in infra and was not checked. Verify proxy-body-size (or equivalent) for tapir.d-ma.be ≥ 200 MB, or lower TAPIR_UPLOAD_MAX_MB to what it allows.
  4. Spool volume. Uploads and converted WAVs (~115 MB/hour of audio) are written to TAPIR_UPLOAD_TMP_DIR (default os.TempDir()). With a read-only root filesystem this needs a writable emptyDir with a size limit; none is configured. A 200 MB upload plus its WAV needs ~0.4 GB headroom per in-flight job.
  5. Single-replica. The transcription queue is process-local (ADR-018 gains a second dependent). The deployment must stay at 1 replica; this is logged at start-up, not enforced.

Refs: #33, ADR-033.


Created via git-mcp on behalf of @mathias

Deferred from #33 / ADR-033, each deliberately not built in the PR. 1. **Per-recording delete.** A private-content store without a delete button is a gap. Account deletion cascades (`uploaded_transcripts.user_id ... ON DELETE CASCADE`, tested), but a user cannot remove one recording. Needs `POST /uploads/{id}/delete`, owner-scoped (404 for others), with the isolation test extended. 2. **Discoverability.** `/uploads` is mounted but nothing links to it; the header nav is static in `Layout` and has no access to `App`. Needs a conditional nav link or a link from the list page. 3. **Tapir's own ingress body limit is unverified.** The default cap is 200 MB. The public LLM gateway ingress rejects ~10 MB (measured, 413); Tapir's ingress lives in `infra` and was not checked. Verify `proxy-body-size` (or equivalent) for `tapir.d-ma.be` ≥ 200 MB, or lower `TAPIR_UPLOAD_MAX_MB` to what it allows. 4. **Spool volume.** Uploads and converted WAVs (~115 MB/hour of audio) are written to `TAPIR_UPLOAD_TMP_DIR` (default `os.TempDir()`). With a read-only root filesystem this needs a writable `emptyDir` with a size limit; none is configured. A 200 MB upload plus its WAV needs ~0.4 GB headroom per in-flight job. 5. **Single-replica.** The transcription queue is process-local (ADR-018 gains a second dependent). The deployment must stay at 1 replica; this is logged at start-up, not enforced. Refs: #33, ADR-033. --- _Created via git-mcp on behalf of @mathias_
mathias added the risk/mediumsize/stype/feat labels 2026-10-01 15:09:40 +00:00
Author
Owner

Status after the production enablement (infra#520, 2026-10-03). Done: the dedicated spool (emptyDir at /spool, sizeLimit 6 Gi, TAPIR_UPLOAD_TMP_DIR=/spool); the ingress body limit was measured (no limit at NPM or Traefik up to 210 MiB); resource limits raised for ffmpeg (512 Mi, 1 CPU); uploads are mounted behind auth. Still open from this issue: the nav link to /uploads, per-recording deletion (account deletion cascades; there is no delete button for a private-content store), and the other plumbing listed in the description. Uploads are reachable at https://tapir.d-ma.be/uploads for logged-in users, but there is no link to it yet.


Created via git-mcp on behalf of @mathias

Status after the production enablement (infra#520, 2026-10-03). **Done:** the dedicated spool (`emptyDir` at `/spool`, `sizeLimit` 6 Gi, `TAPIR_UPLOAD_TMP_DIR=/spool`); the ingress body limit was measured (no limit at NPM or Traefik up to 210 MiB); resource limits raised for ffmpeg (512 Mi, 1 CPU); uploads are mounted behind auth. **Still open from this issue:** the nav link to `/uploads`, per-recording deletion (account deletion cascades; there is no delete button for a private-content store), and the other plumbing listed in the description. Uploads are reachable at `https://tapir.d-ma.be/uploads` for logged-in users, but there is no link to it yet. --- _Created via git-mcp on behalf of @mathias_
mathias added this to the v0.32.0 - audio upload follow-ups milestone 2026-10-03 18:50:00 +00:00
Author
Owner

Shipped in v0.32.0 (PR #58, merge 85f4cc3; production image now 7b7da65). Left OPEN on purpose: the visible behaviour needs one authenticated look that I cannot do (OIDC login).

What is built: the home page links to /uploads ("Recordings") when uploads are mounted; POST /uploads/{id}/delete is owner-scoped (a non-owner or a non-uuid id gets 404, and the store checks the owner twice: in the DELETE predicate and under RLS), refuses a queued or transcribing recording with 409 ("This recording is still being transcribed. You can delete it once it has finished."), and a finished or failed one is deleted and 303-redirects to /uploads. The recording page shows a confirm-then-delete control only in the done and failed states. Decision D3 (refuse until it ends) is built as recommended.

Evidence so far: red tests first at store and handler level; the adversarial review executed cross-user delete with the real non-superuser RLS pool (a user deleting another's id gets not-found and the row survives), the active-row case in the status fragment, the detail page and the list (no delete control for queued or transcribing), and a race where a row finishes between the DELETE and the existence check (409 once, row intact, retry deletes: documented in DECISIONS). Live: a foreign-origin POST /uploads/<id>/delete on production returns 403 and a same-origin one reaches the login redirect (302), so the cross-origin gate covers the new route.

To close, one look as a logged-in user (a 1-minute check): (1) the home page shows "Recordings"; (2) open a finished recording, open "Delete this recording…", confirm, and land on /uploads without it; (3) a recording that is still transcribing offers no delete control. I will close this on your word, or on a screenshot.


Created via git-mcp on behalf of @mathias

**Shipped in v0.32.0** (PR #58, merge 85f4cc3; production image now 7b7da65). Left OPEN on purpose: the visible behaviour needs one authenticated look that I cannot do (OIDC login). What is built: the home page links to `/uploads` ("Recordings") when uploads are mounted; `POST /uploads/{id}/delete` is owner-scoped (a non-owner or a non-uuid id gets 404, and the store checks the owner twice: in the DELETE predicate and under RLS), refuses a queued or transcribing recording with 409 ("This recording is still being transcribed. You can delete it once it has finished."), and a finished or failed one is deleted and 303-redirects to `/uploads`. The recording page shows a confirm-then-delete control only in the done and failed states. Decision D3 (refuse until it ends) is built as recommended. Evidence so far: red tests first at store and handler level; the adversarial review executed cross-user delete with the real non-superuser RLS pool (a user deleting another's id gets not-found and the row survives), the active-row case in the status fragment, the detail page and the list (no delete control for queued or transcribing), and a race where a row finishes between the DELETE and the existence check (409 once, row intact, retry deletes: documented in DECISIONS). Live: a foreign-origin `POST /uploads/<id>/delete` on production returns 403 and a same-origin one reaches the login redirect (302), so the cross-origin gate covers the new route. **To close, one look as a logged-in user** (a 1-minute check): (1) the home page shows "Recordings"; (2) open a finished recording, open "Delete this recording…", confirm, and land on `/uploads` without it; (3) a recording that is still transcribing offers no delete control. I will close this on your word, or on a screenshot. --- _Created via git-mcp on behalf of @mathias_
Author
Owner

Verified by the owner on production, 2026-10-04 (logged in, the checklist above: Recordings link, confirm-then-delete, no control while transcribing). Shipped in v0.32.0 (PR #58, 85f4cc3). Closing.


Created via git-mcp on behalf of @mathias

**Verified by the owner on production, 2026-10-04** (logged in, the checklist above: Recordings link, confirm-then-delete, no control while transcribing). Shipped in v0.32.0 (PR #58, 85f4cc3). Closing. --- _Created via git-mcp on behalf of @mathias_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/tapir#37