Deferred from #33 / ADR-033, each deliberately not built in the PR.
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.
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.
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.
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.
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.
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_
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.
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_
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.
**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_
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.
**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_
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Deferred from #33 / ADR-033, each deliberately not built in the PR.
uploaded_transcripts.user_id ... ON DELETE CASCADE, tested), but a user cannot remove one recording. NeedsPOST /uploads/{id}/delete, owner-scoped (404 for others), with the isolation test extended./uploadsis mounted but nothing links to it; the header nav is static inLayoutand has no access toApp. Needs a conditional nav link or a link from the list page.infraand was not checked. Verifyproxy-body-size(or equivalent) fortapir.d-ma.be≥ 200 MB, or lowerTAPIR_UPLOAD_MAX_MBto what it allows.TAPIR_UPLOAD_TMP_DIR(defaultos.TempDir()). With a read-only root filesystem this needs a writableemptyDirwith a size limit; none is configured. A 200 MB upload plus its WAV needs ~0.4 GB headroom per in-flight job.Refs: #33, ADR-033.
Created via git-mcp on behalf of @mathias
Status after the production enablement (infra#520, 2026-10-03). Done: the dedicated spool (
emptyDirat/spool,sizeLimit6 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 athttps://tapir.d-ma.be/uploadsfor logged-in users, but there is no link to it yet.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}/deleteis 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>/deleteon 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
/uploadswithout 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
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