From 19f4b59e23ec0b1aaa2aaa4abb64e0fd9e37c558 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Tue, 6 Jan 2026 10:02:17 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-06 10:02:17) --- .last-branch | 2 +- .../src/backend/app/main/routes_inbox.py | 58 +++++++ .../src/templates/main/inbox.html | 142 +++++++++++++++++- docs/changelog.md | 8 + 4 files changed, 208 insertions(+), 2 deletions(-) diff --git a/.last-branch b/.last-branch index 50618f4..532f1a2 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260106-01-m365-combined-job-name-merge +v20260106-02-inbox-bulk-delete diff --git a/containers/backupchecks/src/backend/app/main/routes_inbox.py b/containers/backupchecks/src/backend/app/main/routes_inbox.py index e3a38b8..5440c9f 100644 --- a/containers/backupchecks/src/backend/app/main/routes_inbox.py +++ b/containers/backupchecks/src/backend/app/main/routes_inbox.py @@ -69,6 +69,8 @@ def inbox(): has_prev=has_prev, has_next=has_next, customers=customer_rows, + can_bulk_delete=(get_active_role() in ("admin", "operator")), + is_admin=(get_active_role() == "admin"), ) @@ -296,6 +298,62 @@ def inbox_message_delete(message_id: int): return redirect(url_for("main.inbox")) + + +@main_bp.post("/api/inbox/delete") +@login_required +@roles_required("admin", "operator") +def api_inbox_bulk_delete(): + """Bulk delete inbox messages (soft delete -> move to Deleted).""" + data = request.get_json(silent=True) or {} + message_ids = data.get("message_ids") or [] + + try: + message_ids = [int(x) for x in message_ids] + except Exception: + return jsonify({"status": "error", "message": "Invalid message_ids."}), 400 + + if not message_ids: + return jsonify({"status": "ok", "updated": 0, "skipped": 0, "missing": 0}) + + msgs = MailMessage.query.filter(MailMessage.id.in_(message_ids)).all() + msg_map = {int(m.id): m for m in msgs} + + now = datetime.utcnow() + updated = 0 + skipped = 0 + missing = 0 + + for mid in message_ids: + msg = msg_map.get(int(mid)) + if not msg: + missing += 1 + continue + + if getattr(msg, "location", "inbox") != "inbox": + skipped += 1 + continue + + if hasattr(msg, "location"): + msg.location = "deleted" + if hasattr(msg, "deleted_at"): + msg.deleted_at = now + if hasattr(msg, "deleted_by_user_id"): + msg.deleted_by_user_id = current_user.id + + updated += 1 + + try: + db.session.commit() + except Exception as exc: + db.session.rollback() + _log_admin_event("inbox_bulk_delete_error", f"Failed to bulk delete inbox messages {message_ids}: {exc}") + return jsonify({"status": "error", "message": "Database error while deleting messages."}), 500 + + _log_admin_event("inbox_bulk_delete", f"Deleted inbox messages: {message_ids}") + return jsonify({"status": "ok", "updated": updated, "skipped": skipped, "missing": missing}) + + @main_bp.route("/inbox/deleted") @login_required @roles_required("admin") diff --git a/containers/backupchecks/src/templates/main/inbox.html b/containers/backupchecks/src/templates/main/inbox.html index 95e8c78..760c141 100644 --- a/containers/backupchecks/src/templates/main/inbox.html +++ b/containers/backupchecks/src/templates/main/inbox.html @@ -56,10 +56,26 @@ {{ pager("top", page, total_pages, has_prev, has_next) }} +{% if can_bulk_delete %} +
+
+ +
+
+
+{% endif %} + + +
- +
+ {% if can_bulk_delete %} + + {% endif %} @@ -75,6 +91,11 @@ {% if rows %} {% for row in rows %} + {% if can_bulk_delete %} + + {% endif %} @@ -190,6 +211,125 @@ (function () { var customers = {{ customers|tojson|safe }}; + var table = document.getElementById('inboxTable'); + var selectAll = document.getElementById('inbox_select_all'); + var btnDeleteSelected = document.getElementById('btn_inbox_delete_selected'); + var statusEl = document.getElementById('inbox_status'); + + function getSelectedMessageIds() { + if (!table) return []; + var cbs = table.querySelectorAll('tbody .inbox_row_cb'); + var ids = []; + cbs.forEach(function (cb) { + if (cb.checked) ids.push(parseInt(cb.value, 10)); + }); + return ids.filter(function (x) { return Number.isFinite(x); }); + } + + function refreshRowHighlights() { + if (!table) return; + var cbs = table.querySelectorAll('tbody .inbox_row_cb'); + cbs.forEach(function (cb) { + var tr = cb.closest ? cb.closest('tr') : null; + if (!tr) return; + if (cb.checked) tr.classList.add('table-active'); + else tr.classList.remove('table-active'); + }); + } + + function refreshSelectAll() { + if (!selectAll || !table) return; + var cbs = table.querySelectorAll('tbody .inbox_row_cb'); + var total = cbs.length; + var checked = 0; + cbs.forEach(function (cb) { if (cb.checked) checked++; }); + selectAll.indeterminate = checked > 0 && checked < total; + selectAll.checked = total > 0 && checked === total; + } + + function updateBulkDeleteUi() { + var ids = getSelectedMessageIds(); + refreshRowHighlights(); + if (btnDeleteSelected) btnDeleteSelected.disabled = ids.length === 0; + if (statusEl) statusEl.textContent = ids.length ? (ids.length + ' selected') : ''; + refreshSelectAll(); + } + + function postJson(url, payload) { + return fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify(payload || {}) + }).then(function (r) { + return r.json().then(function (data) { return { ok: r.ok, status: r.status, data: data }; }); + }); + } + + if (selectAll && table) { + function setAllSelection(checked) { + var cbs = table.querySelectorAll('tbody .inbox_row_cb'); + cbs.forEach(function (cb) { cb.checked = !!checked; }); + selectAll.indeterminate = false; + selectAll.checked = !!checked; + setTimeout(function () { + selectAll.indeterminate = false; + selectAll.checked = !!checked; + }, 0); + updateBulkDeleteUi(); + } + + selectAll.addEventListener('click', function (e) { + e.stopPropagation(); + }); + + selectAll.addEventListener('change', function () { + setAllSelection(selectAll.checked); + }); + } + + if (table) { + table.addEventListener('change', function (e) { + var t = e.target; + if (t && t.classList && t.classList.contains('inbox_row_cb')) { + updateBulkDeleteUi(); + } + }); + } + + if (btnDeleteSelected) { + btnDeleteSelected.addEventListener('click', function () { + var ids = getSelectedMessageIds(); + if (!ids.length) return; + + var msg = 'Delete ' + ids.length + ' selected message' + (ids.length === 1 ? '' : 's') + ' from the Inbox?'; + if (!confirm(msg)) return; + + if (statusEl) statusEl.textContent = 'Deleting...'; + + postJson('{{ url_for('main.api_inbox_bulk_delete') }}', { message_ids: ids }) + .then(function (res) { + if (!res.ok || !res.data || res.data.status !== 'ok') { + var err = (res.data && (res.data.message || res.data.error)) ? (res.data.message || res.data.error) : 'Request failed.'; + if (statusEl) statusEl.textContent = err; + alert(err); + return; + } + window.location.reload(); + }) + .catch(function () { + var err = 'Request failed.'; + if (statusEl) statusEl.textContent = err; + alert(err); + }); + }); + } + + // Initialize UI state + updateBulkDeleteUi(); + + + function wrapMailHtml(html) { diff --git a/docs/changelog.md b/docs/changelog.md index 0b9151f..e7a5b00 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,14 @@ - Job names containing the suffix "(Combined)" are now mapped to the same job as the corresponding job without this suffix. - This ensures that combined and non-combined backup result emails are aggregated under a single job in reports and statistics. +--- + +## v20260106-02-inbox-bulk-delete + +- Added multi-select checkboxes to the Inbox for users with the operator or admin role. +- Added a “Delete selected” bulk action that soft-deletes selected inbox messages (moves them to Deleted), including select-all support and a selected-count indicator. +- Implemented a new POST API endpoint to bulk delete inbox messages with proper validation, skip/missing counts, and admin event logging. + ================================================================================================================================================ ## v0.1.16
+ + From Subject Date / time
+ + {{ row.from_address }} {{ row.subject }} {{ row.received_at }}