From fc99f17db35baf4c623601ea7cb6f80818032ec8 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Tue, 10 Feb 2026 13:40:53 +0100 Subject: [PATCH] Add admin view for deleted feedback items + permanent delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User request: Allow admins to view deleted items and permanently delete them (hard delete) to clean up database and remove screenshots. Features: 1. Admin-only "Show deleted" checkbox on feedback list 2. Deleted items shown with gray background + "Deleted" badge 3. Permanent delete button (only for soft-deleted items) 4. Hard delete removes item + all attachments from database 5. Admins can view detail pages of deleted items Backend (routes_feedback.py): - Added show_deleted parameter (admin only) - Modified feedback_page query to optionally include deleted items - Added deleted_at, deleted_by to query results - Modified feedback_detail to allow admins to view deleted items - New route: feedback_permanent_delete (hard delete) - Only works on already soft-deleted items (safety check) - Uses db.session.delete() - CASCADE removes attachments - Shows attachment count in confirmation message Frontend: - feedback.html: - "Show deleted items" checkbox (auto-submits form) - Deleted items: gray background (table-secondary) - Shows deleted timestamp - "Permanent Delete" button in Actions column - Confirmation dialog warns about permanent deletion - feedback_detail.html: - "Deleted" badge in header - Actions sidebar shows warning + "Permanent Delete" button - Normal actions (resolve/delete) hidden for deleted items Benefits: - Audit trail preserved with soft delete - Database can be cleaned up later by removing old deleted items - Screenshots (BYTEA) don't accumulate forever - Two-stage safety: soft delete → permanent delete Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_feedback.py | 48 ++++++++++++++++++- .../src/templates/main/feedback.html | 34 ++++++++++++- .../src/templates/main/feedback_detail.html | 40 +++++++++++----- docs/changelog-claude.md | 8 ++++ 4 files changed, 114 insertions(+), 16 deletions(-) diff --git a/containers/backupchecks/src/backend/app/main/routes_feedback.py b/containers/backupchecks/src/backend/app/main/routes_feedback.py index 7744496..8318548 100644 --- a/containers/backupchecks/src/backend/app/main/routes_feedback.py +++ b/containers/backupchecks/src/backend/app/main/routes_feedback.py @@ -69,7 +69,14 @@ def feedback_page(): if sort not in ("votes", "newest", "updated"): sort = "votes" - where = ["fi.deleted_at IS NULL"] + # Admin-only: show deleted items + show_deleted = False + if get_active_role() == "admin": + show_deleted = request.args.get("show_deleted", "0") in ("1", "true", "yes", "on") + + where = [] + if not show_deleted: + where.append("fi.deleted_at IS NULL") params = {"user_id": int(current_user.id)} if item_type: @@ -106,6 +113,8 @@ def feedback_page(): fi.status, fi.created_at, fi.updated_at, + fi.deleted_at, + fi.deleted_by_user_id, u.username AS created_by, COALESCE(v.vote_count, 0) AS vote_count, EXISTS ( @@ -143,6 +152,8 @@ def feedback_page(): "created_by": r["created_by"] or "-", "vote_count": int(r["vote_count"] or 0), "user_voted": bool(r["user_voted"]), + "is_deleted": bool(r["deleted_at"]), + "deleted_at": _format_datetime(r["deleted_at"]) if r["deleted_at"] else "", } ) @@ -153,6 +164,7 @@ def feedback_page(): status=status, q=q, sort=sort, + show_deleted=show_deleted, ) @@ -221,7 +233,8 @@ def feedback_new(): @roles_required("admin", "operator", "reporter", "viewer") def feedback_detail(item_id: int): item = FeedbackItem.query.get_or_404(item_id) - if item.deleted_at is not None: + # Allow admins to view deleted items + if item.deleted_at is not None and get_active_role() != "admin": abort(404) vote_count = ( @@ -438,6 +451,37 @@ def feedback_delete(item_id: int): return redirect(url_for("main.feedback_page")) +@main_bp.route("/feedback//permanent-delete", methods=["POST"]) +@login_required +@roles_required("admin") +def feedback_permanent_delete(item_id: int): + """Permanently delete a feedback item and all its attachments from the database. + + This is a hard delete - the item and all associated data will be removed permanently. + Only available for items that are already soft-deleted. + """ + item = FeedbackItem.query.get_or_404(item_id) + + # Only allow permanent delete on already soft-deleted items + if item.deleted_at is None: + flash("Item must be deleted first before permanent deletion.", "warning") + return redirect(url_for("main.feedback_detail", item_id=item.id)) + + # Get attachment count for feedback message + attachment_count = FeedbackAttachment.query.filter_by(feedback_item_id=item.id).count() + + # Hard delete - CASCADE will automatically delete: + # - feedback_votes + # - feedback_replies + # - feedback_attachments (via replies CASCADE) + # - feedback_attachments (direct, via item CASCADE) + db.session.delete(item) + db.session.commit() + + flash(f"Feedback item permanently deleted ({attachment_count} screenshot(s) removed).", "success") + return redirect(url_for("main.feedback_page", show_deleted="1")) + + @main_bp.route("/feedback/attachment/") @login_required @roles_required("admin", "operator", "reporter", "viewer") diff --git a/containers/backupchecks/src/templates/main/feedback.html b/containers/backupchecks/src/templates/main/feedback.html index c2757af..6a553c0 100644 --- a/containers/backupchecks/src/templates/main/feedback.html +++ b/containers/backupchecks/src/templates/main/feedback.html @@ -34,6 +34,16 @@
+ {% if active_role == 'admin' %} +
+
+ + +
+
+ {% endif %}
@@ -46,6 +56,9 @@ Component Status Created + {% if active_role == 'admin' and show_deleted %} + Actions + {% endif %} @@ -56,20 +69,30 @@ {% endif %} {% for i in items %} - + + {% if not i.is_deleted %}
+ {% else %} + + {{ i.vote_count }} + {% endif %} {{ i.title }} + {% if i.is_deleted %} + Deleted + {% endif %} {% if i.created_by %}
by {{ i.created_by }}
{% endif %} + {% if i.is_deleted and i.deleted_at %} +
Deleted {{ i.deleted_at|local_datetime }}
+ {% endif %} {% if i.item_type == 'bug' %} @@ -90,6 +113,15 @@
{{ i.created_at|local_datetime }}
Updated {{ i.updated_at|local_datetime }}
+ {% if active_role == 'admin' and show_deleted %} + + {% if i.is_deleted %} +
+ +
+ {% endif %} + + {% endif %} {% endfor %} diff --git a/containers/backupchecks/src/templates/main/feedback_detail.html b/containers/backupchecks/src/templates/main/feedback_detail.html index 0109af7..d483e0d 100644 --- a/containers/backupchecks/src/templates/main/feedback_detail.html +++ b/containers/backupchecks/src/templates/main/feedback_detail.html @@ -15,6 +15,9 @@ {% else %} Open {% endif %} + {% if item.deleted_at %} + Deleted + {% endif %} by {{ created_by_name }}
@@ -133,21 +136,32 @@

Actions

{% if active_role == 'admin' %} - {% if item.status == 'resolved' %} -
- - -
+ {% if item.deleted_at %} + {# Item is deleted - show permanent delete option #} +
+ This item is deleted. +
+
+ +
{% else %} -
- - -
- {% endif %} + {# Item is not deleted - show normal actions #} + {% if item.status == 'resolved' %} +
+ + +
+ {% else %} +
+ + +
+ {% endif %} -
- -
+
+ +
+ {% endif %} {% else %}
Only administrators can resolve or delete items.
{% endif %} diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index a49f00c..166002f 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -14,6 +14,14 @@ This file documents all changes made to this project via Claude Code. - File validation: image type verification, size limits, secure filename handling - New route: `/feedback/attachment/` to serve images (access-controlled) - Database migration: auto-creates `feedback_attachments` table with indexes on startup +- Added admin-only deleted items view and permanent delete functionality to Feedback system + - "Show deleted items" checkbox on feedback list page (admin only) + - Deleted items shown with gray background and "Deleted" badge + - Permanent delete action removes item + all attachments from database (hard delete) + - Attachment count shown in deletion confirmation message + - Admins can view detail pages of deleted items + - Two-stage delete: soft delete (audit trail) → permanent delete (database cleanup) + - Prevents accidental permanent deletion (requires item to be soft-deleted first) ### Fixed - Fixed Autotask ticket not being automatically linked to new runs when internal ticket is resolved by implementing independent Autotask propagation strategy (now checks for most recent non-deleted and non-resolved Autotask ticket on job regardless of internal ticket status, ensuring PSA ticket reference persists across runs until explicitly resolved or deleted)