feat(admin-feedback): spam filter + mark/unmark spam + delete + Close-into-Closed-tab #114
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
psa-systems/bunyip!114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/feedback-admin-ops-spam-and-delete"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BUNYIP-92. Stacks on BUNYIP-85.
Several gaps in the admin feedback console:
FeedbackRepository::list_paginateddid not filter is_spam; only the CSV-exportlist_allexcluded it. A bot that triggered the honeypot landed in the working queue indistinguishable from real feedback.delete_feedback; the BFF and SSR layers did not expose it. Worse,FeedbackRepository::deletequietly requiredstatus = 'closed', so even a future Delete button would 404 on Active rows.Changes:
AuditAction::FeedbackMarkedSpam+FeedbackUnmarkedSpam, register both as admin actions and serialize tofeedback_marked_spam/feedback_unmarked_spam.list_paginatedto take abucket: Option<&str>parameter (active/closed/spam). Each bucket maps to a static SQL predicate (no bind values, SQL-injection-safe by construction). Active defaults excludeis_spam = TRUE AND status = 'closed'; Spam includes onlyis_spam = TRUE. Addset_is_spamfor the mark/unmark flow. Drop the legacyAND status = 'closed'constraint ondelete(admin role + JS confirm + audit log already provide three safety layers).?bucket=throughlist_feedback(validated against the allowed set; bad values 4xx). Addmark_feedback_spamandunmark_feedback_spamhandlers. Both write the matching audit log on success.feedback()to require the bucket arg. New helpersmark_feedback_spam,unmark_feedback_spam,delete_feedback.FeedbackTabfrom {Active, Archive} to {Active, Closed, Spam, Archive}. Add tab-awarebucket()andpath()methods. Add tab links tofeedback_tabs(). Extractrender_feedback_list(...)+feedback_row(f, tab)so the three list handlers (Active / Closed / Spam) share the page chrome but each row gets tab-appropriate buttons. New handlersfeedback_closed,feedback_spam,feedback_mark_spam,feedback_unmark_spam,feedback_delete. Status form now carries afromhidden field so the redirect lands the admin back on the originating tab with a?toast_ok=confirmation; same for the spam / delete handlers. The detail page (BUNYIP-85) gains Mark Spam + Delete buttons.:iddetail catchall so axum's matcher does not interpret the literals as a feedback id.Result: clicking Close on an Active row visibly moves the row into the Closed tab. Spam is auto-filtered from Active and reachable via its own tab. Mark Spam and Delete are one click away from every row and from the detail page; Delete prompts for confirmation. False-positive spam is recoverable via Unmark Spam.
just check-containerclean (modulo the pre-existingtest_config_defaultsparallel-env-var flake on main; 188 other tests pass; fmt + clippy + build clean).#BUNYIP-92
Closes BUNYIP-92. Stacks on BUNYIP-85. Several gaps in the admin feedback console: 1. Spam leaked into the active queue. `FeedbackRepository::list_paginated` did not filter is_spam; only the CSV-export `list_all` excluded it. A bot that triggered the honeypot landed in the working queue indistinguishable from real feedback. 2. "Close" felt like a no-op: the row's badge flipped to "closed" but the row stayed in the list, so admins thought nothing happened. 3. No "Mark as Spam" path: spam detection was only the honeypot field on submission; a human-written abuse / off-topic submission had no path to spam. 4. No hard delete: the API had `delete_feedback`; the BFF and SSR layers did not expose it. Worse, `FeedbackRepository::delete` quietly required `status = 'closed'`, so even a future Delete button would 404 on Active rows. Changes: - crates/bunyip-domain/src/models/audit.rs: add `AuditAction::FeedbackMarkedSpam` + `FeedbackUnmarkedSpam`, register both as admin actions and serialize to `feedback_marked_spam` / `feedback_unmarked_spam`. - crates/bunyip-domain/src/repositories/feedback.rs: refactor `list_paginated` to take a `bucket: Option<&str>` parameter (`active` / `closed` / `spam`). Each bucket maps to a static SQL predicate (no bind values, SQL-injection-safe by construction). Active defaults exclude `is_spam = TRUE AND status = 'closed'`; Spam includes only `is_spam = TRUE`. Add `set_is_spam` for the mark/unmark flow. Drop the legacy `AND status = 'closed'` constraint on `delete` (admin role + JS confirm + audit log already provide three safety layers). - bunyip-api/src/handlers/feedback.rs: thread `?bucket=` through `list_feedback` (validated against the allowed set; bad values 4xx). Add `mark_feedback_spam` and `unmark_feedback_spam` handlers. Both write the matching audit log on success. - bunyip-api/src/routes/admin.rs: register the two new POST routes adjacent to the existing status / respond / delete routes. - bunyip-web/src/api/admin.rs: extend `feedback()` to require the bucket arg. New helpers `mark_feedback_spam`, `unmark_feedback_spam`, `delete_feedback`. - bunyip-web/src/handlers/admin.rs: extend `FeedbackTab` from {Active, Archive} to {Active, Closed, Spam, Archive}. Add tab-aware `bucket()` and `path()` methods. Add tab links to `feedback_tabs()`. Extract `render_feedback_list(...)` + `feedback_row(f, tab)` so the three list handlers (Active / Closed / Spam) share the page chrome but each row gets tab-appropriate buttons. New handlers `feedback_closed`, `feedback_spam`, `feedback_mark_spam`, `feedback_unmark_spam`, `feedback_delete`. Status form now carries a `from` hidden field so the redirect lands the admin back on the originating tab with a `?toast_ok=` confirmation; same for the spam / delete handlers. The detail page (BUNYIP-85) gains Mark Spam + Delete buttons. - bunyip-web/src/main.rs: register the new SSR routes. Closed / spam tab routes register BEFORE the `:id` detail catchall so axum's matcher does not interpret the literals as a feedback id. Result: clicking Close on an Active row visibly moves the row into the Closed tab. Spam is auto-filtered from Active and reachable via its own tab. Mark Spam and Delete are one click away from every row and from the detail page; Delete prompts for confirmation. False-positive spam is recoverable via Unmark Spam. `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-92