Auto-commit local changes before build (2026-01-06 10:02:17) #36

Merged
ivooskamp merged 1 commits from v20260106-02-inbox-bulk-delete into main 2026-01-13 11:05:56 +01:00
4 changed files with 208 additions and 2 deletions
Showing only changes of commit 19f4b59e23 - Show all commits

View File

@ -1 +1 @@
v20260106-01-m365-combined-job-name-merge v20260106-02-inbox-bulk-delete

View File

@ -69,6 +69,8 @@ def inbox():
has_prev=has_prev, has_prev=has_prev,
has_next=has_next, has_next=has_next,
customers=customer_rows, 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")) 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") @main_bp.route("/inbox/deleted")
@login_required @login_required
@roles_required("admin") @roles_required("admin")

View File

@ -56,10 +56,26 @@
{{ pager("top", page, total_pages, has_prev, has_next) }} {{ pager("top", page, total_pages, has_prev, has_next) }}
{% if can_bulk_delete %}
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-danger" id="btn_inbox_delete_selected" disabled>Delete selected</button>
</div>
</div>
<div class="small text-muted mb-2" id="inbox_status"></div>
{% endif %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-hover align-middle"> <table class="table table-sm table-hover align-middle" id="inboxTable">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
{% if can_bulk_delete %}
<th scope="col" style="width: 34px;">
<input class="form-check-input" type="checkbox" id="inbox_select_all" />
</th>
{% endif %}
<th scope="col">From</th> <th scope="col">From</th>
<th scope="col">Subject</th> <th scope="col">Subject</th>
<th scope="col">Date / time</th> <th scope="col">Date / time</th>
@ -75,6 +91,11 @@
{% if rows %} {% if rows %}
{% for row in rows %} {% for row in rows %}
<tr class="inbox-row" data-message-id="{{ row.id }}" style="cursor: pointer;"> <tr class="inbox-row" data-message-id="{{ row.id }}" style="cursor: pointer;">
{% if can_bulk_delete %}
<td onclick="event.stopPropagation();">
<input class="form-check-input inbox_row_cb" type="checkbox" value="{{ row.id }}" />
</td>
{% endif %}
<td>{{ row.from_address }}</td> <td>{{ row.from_address }}</td>
<td>{{ row.subject }}</td> <td>{{ row.subject }}</td>
<td>{{ row.received_at }}</td> <td>{{ row.received_at }}</td>
@ -190,6 +211,125 @@
(function () { (function () {
var customers = {{ customers|tojson|safe }}; 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) { function wrapMailHtml(html) {

View File

@ -4,6 +4,14 @@
- Job names containing the suffix "(Combined)" are now mapped to the same job as the corresponding job without this suffix. - 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. - 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 ## v0.1.16