feat(feedback): attachments upload + admin thumbnails + BFF download proxy #111

Merged
YousifShkara merged 1 commit from feat/feedback-attachments-upload-and-download into main 2026-06-11 06:03:33 +02:00
Owner

Closes BUNYIP-86. Final PR of the three-part feedback parity series. Stacks on BUNYIP-85.

bunyip-api already accepts up to 3 attachments at up to 5 MB each across image/png|jpeg|webp|gif and text/plain (bunyip-api/src/handlers/feedback.rs:29-35, 103-310) and serves them back via GET /v1/admin/feedback/{id}/attachments/{attachment_id} (handlers/feedback.rs:583-607, routed at routes/admin.rs:125-128). bunyip-web exposed neither the upload UI nor any way to render or download the files in admin triage.

Browser-facing form transport:

  • bunyip-web/Cargo.toml: enable axum's multipart feature. Drop the now-unused form_urlencoded dep (the BUNYIP-79 urlencoded path is gone).
  • bunyip-web/src/handlers/content.rs::feedback_form: add enctype="multipart/form-data" and a <input type="file" name="attachments" multiple accept="image/png,image/jpeg,image/webp,image/gif,text/plain"> block under the message field, with a small hint line ("Up to 3 files, 5 MB each").
  • Swap feedback_post(body: Bytes) for feedback_post(mut multipart: Multipart). Replace parse_feedback_form with an async read_feedback_multipart that mirrors the API's per-form ceilings (3 attachments, 5 MB, allowed MIMEs) upstream of the reqwest hop so users see an inline error banner instead of the API's bare 422 after a long upload. Empty file-input slots (zero-byte browser parts) are skipped, not turned into 0-byte rejections. Filenames are basename-sanitized to match the API's strip.
  • bunyip-web/src/main.rs: raise the axum default 2 MB body limit on /feedback only via route_layer(DefaultBodyLimit::max(FEEDBACK_BODY_LIMIT_BYTES)) (16 MB ceiling = 3 × 5 MB + headroom). Other routes stay on the default.

BFF -> API hop:

  • bunyip-web/src/api/calls.rs: add FeedbackAttachment { filename, mime, bytes } and attachments: Vec<FeedbackAttachment> on FeedbackInput. submit_feedback now appends a reqwest::multipart::Part::bytes(...).file_name(...).mime_str(...) per file under the part name attachments (the API identifies file parts by Content-Disposition.filename, not by part name).

Admin detail rendering:

  • bunyip-web/src/api/types.rs: add FeedbackAttachmentMeta { id, filename, mime_type, size_bytes }. Add #[serde(default)] attachments: Vec<...> to AdminFeedbackDetail so an older API that does not emit the field deserializes as an empty list.
  • bunyip-web/src/handlers/admin.rs::feedback_detail_view: render an attachments section under the message when non-empty. Image MIMEs get an inline <img> thumbnail loaded from the BFF proxy URL; non-image (text/plain) stays as a labelled download link. Each row shows filename + human-readable size + MIME.

Admin attachment download proxy:

  • bunyip-web/src/handlers/admin.rs::feedback_attachment: GET /admin/feedback/:id/attachments/:attachment_id mirroring the existing feedback_export and dashboard::download_asset pattern. Re-auth via admin_guard, forward the session cookie to the API, stream the response body back with the upstream Content-Type and Content-Disposition. 401 forces re-auth; any other non-2xx bounces back to the detail page so the browser never saves an error blob.
  • bunyip-web/src/main.rs: register the route between the :id detail and :id/respond routes.

just check-container clean (modulo the pre-existing test_config_defaults parallel-env-var flake on main; 188 other tests pass; fmt + clippy + build clean).

#BUNYIP-86

Closes BUNYIP-86. Final PR of the three-part feedback parity series. Stacks on BUNYIP-85. bunyip-api already accepts up to 3 attachments at up to 5 MB each across image/png|jpeg|webp|gif and text/plain (`bunyip-api/src/handlers/feedback.rs:29-35, 103-310`) and serves them back via `GET /v1/admin/feedback/{id}/attachments/{attachment_id}` (handlers/feedback.rs:583-607, routed at routes/admin.rs:125-128). bunyip-web exposed neither the upload UI nor any way to render or download the files in admin triage. Browser-facing form transport: - `bunyip-web/Cargo.toml`: enable axum's `multipart` feature. Drop the now-unused `form_urlencoded` dep (the BUNYIP-79 urlencoded path is gone). - `bunyip-web/src/handlers/content.rs::feedback_form`: add `enctype="multipart/form-data"` and a `<input type="file" name="attachments" multiple accept="image/png,image/jpeg,image/webp,image/gif,text/plain">` block under the message field, with a small hint line ("Up to 3 files, 5 MB each"). - Swap `feedback_post(body: Bytes)` for `feedback_post(mut multipart: Multipart)`. Replace `parse_feedback_form` with an async `read_feedback_multipart` that mirrors the API's per-form ceilings (3 attachments, 5 MB, allowed MIMEs) upstream of the reqwest hop so users see an inline error banner instead of the API's bare 422 after a long upload. Empty file-input slots (zero-byte browser parts) are skipped, not turned into 0-byte rejections. Filenames are basename-sanitized to match the API's strip. - `bunyip-web/src/main.rs`: raise the axum default 2 MB body limit on `/feedback` only via `route_layer(DefaultBodyLimit::max(FEEDBACK_BODY_LIMIT_BYTES))` (16 MB ceiling = 3 × 5 MB + headroom). Other routes stay on the default. BFF -> API hop: - `bunyip-web/src/api/calls.rs`: add `FeedbackAttachment { filename, mime, bytes }` and `attachments: Vec<FeedbackAttachment>` on `FeedbackInput`. `submit_feedback` now appends a `reqwest::multipart::Part::bytes(...).file_name(...).mime_str(...)` per file under the part name `attachments` (the API identifies file parts by `Content-Disposition.filename`, not by part name). Admin detail rendering: - `bunyip-web/src/api/types.rs`: add `FeedbackAttachmentMeta { id, filename, mime_type, size_bytes }`. Add `#[serde(default)] attachments: Vec<...>` to `AdminFeedbackDetail` so an older API that does not emit the field deserializes as an empty list. - `bunyip-web/src/handlers/admin.rs::feedback_detail_view`: render an attachments section under the message when non-empty. Image MIMEs get an inline `<img>` thumbnail loaded from the BFF proxy URL; non-image (`text/plain`) stays as a labelled download link. Each row shows filename + human-readable size + MIME. Admin attachment download proxy: - `bunyip-web/src/handlers/admin.rs::feedback_attachment`: GET `/admin/feedback/:id/attachments/:attachment_id` mirroring the existing `feedback_export` and `dashboard::download_asset` pattern. Re-auth via `admin_guard`, forward the session cookie to the API, stream the response body back with the upstream Content-Type and Content-Disposition. 401 forces re-auth; any other non-2xx bounces back to the detail page so the browser never saves an error blob. - `bunyip-web/src/main.rs`: register the route between the `:id` detail and `:id/respond` routes. `just check-container` clean (modulo the pre-existing `test_config_defaults` parallel-env-var flake on `main`; 188 other tests pass; fmt + clippy + build clean). #BUNYIP-86
feat(feedback): attachments upload + admin thumbnails + BFF download proxy
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 2m30s
4406f1dcdc
Closes BUNYIP-86. Final PR of the three-part feedback parity series. Stacks on BUNYIP-85.

bunyip-api already accepts up to 3 attachments at up to 5 MB each across image/png|jpeg|webp|gif and text/plain (`bunyip-api/src/handlers/feedback.rs:29-35, 103-310`) and serves them back via `GET /v1/admin/feedback/{id}/attachments/{attachment_id}` (handlers/feedback.rs:583-607, routed at routes/admin.rs:125-128). bunyip-web exposed neither the upload UI nor any way to render or download the files in admin triage.

Browser-facing form transport:

- `bunyip-web/Cargo.toml`: enable axum's `multipart` feature. Drop the now-unused `form_urlencoded` dep (the BUNYIP-79 urlencoded path is gone).
- `bunyip-web/src/handlers/content.rs::feedback_form`: add `enctype="multipart/form-data"` and a `<input type="file" name="attachments" multiple accept="image/png,image/jpeg,image/webp,image/gif,text/plain">` block under the message field, with a small hint line ("Up to 3 files, 5 MB each").
- Swap `feedback_post(body: Bytes)` for `feedback_post(mut multipart: Multipart)`. Replace `parse_feedback_form` with an async `read_feedback_multipart` that mirrors the API's per-form ceilings (3 attachments, 5 MB, allowed MIMEs) upstream of the reqwest hop so users see an inline error banner instead of the API's bare 422 after a long upload. Empty file-input slots (zero-byte browser parts) are skipped, not turned into 0-byte rejections. Filenames are basename-sanitized to match the API's strip.
- `bunyip-web/src/main.rs`: raise the axum default 2 MB body limit on `/feedback` only via `route_layer(DefaultBodyLimit::max(FEEDBACK_BODY_LIMIT_BYTES))` (16 MB ceiling = 3 × 5 MB + headroom). Other routes stay on the default.

BFF -> API hop:

- `bunyip-web/src/api/calls.rs`: add `FeedbackAttachment { filename, mime, bytes }` and `attachments: Vec<FeedbackAttachment>` on `FeedbackInput`. `submit_feedback` now appends a `reqwest::multipart::Part::bytes(...).file_name(...).mime_str(...)` per file under the part name `attachments` (the API identifies file parts by `Content-Disposition.filename`, not by part name).

Admin detail rendering:

- `bunyip-web/src/api/types.rs`: add `FeedbackAttachmentMeta { id, filename, mime_type, size_bytes }`. Add `#[serde(default)] attachments: Vec<...>` to `AdminFeedbackDetail` so an older API that does not emit the field deserializes as an empty list.
- `bunyip-web/src/handlers/admin.rs::feedback_detail_view`: render an attachments section under the message when non-empty. Image MIMEs get an inline `<img>` thumbnail loaded from the BFF proxy URL; non-image (`text/plain`) stays as a labelled download link. Each row shows filename + human-readable size + MIME.

Admin attachment download proxy:

- `bunyip-web/src/handlers/admin.rs::feedback_attachment`: GET `/admin/feedback/:id/attachments/:attachment_id` mirroring the existing `feedback_export` and `dashboard::download_asset` pattern. Re-auth via `admin_guard`, forward the session cookie to the API, stream the response body back with the upstream Content-Type and Content-Disposition. 401 forces re-auth; any other non-2xx bounces back to the detail page so the browser never saves an error blob.
- `bunyip-web/src/main.rs`: register the route between the `:id` detail and `:id/respond` routes.

`just check-container` clean (modulo the pre-existing `test_config_defaults` parallel-env-var flake on `main`; 188 other tests pass; fmt + clippy + build clean).

#BUNYIP-86
YousifShkara deleted branch feat/feedback-attachments-upload-and-download 2026-06-11 06:03:33 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
psa-systems/bunyip!111
No description provided.