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)