v20260108-28-admin-all-mail-open-fix #63

Merged
ivooskamp merged 3 commits from v20260108-28-admin-all-mail-open-fix into main 2026-01-13 11:24:38 +01:00
4 changed files with 217 additions and 68 deletions

View File

@ -1 +1 @@
v20260108-27-admin-all-mail-audit-page v20260108-28-admin-all-mail-open-fix

View File

@ -0,0 +1,135 @@
from .routes_shared import * # noqa: F401,F403
from .routes_shared import _format_datetime
@main_bp.route("/admin/all-mail")
@login_required
@roles_required("admin")
def admin_all_mail_page():
# Pagination
try:
page = int(request.args.get("page", "1"))
except ValueError:
page = 1
if page < 1:
page = 1
per_page = 50
# Filters (AND combined)
from_q = (request.args.get("from_q") or "").strip()
subject_q = (request.args.get("subject_q") or "").strip()
backup_q = (request.args.get("backup_q") or "").strip()
type_q = (request.args.get("type_q") or "").strip()
job_name_q = (request.args.get("job_name_q") or "").strip()
received_from = (request.args.get("received_from") or "").strip()
received_to = (request.args.get("received_to") or "").strip()
only_unlinked = (request.args.get("only_unlinked") or "").strip().lower() in (
"1",
"true",
"yes",
"on",
)
query = db.session.query(MailMessage).outerjoin(Job, MailMessage.job_id == Job.id)
if from_q:
query = query.filter(MailMessage.from_address.ilike(f"%{from_q}%"))
if subject_q:
query = query.filter(MailMessage.subject.ilike(f"%{subject_q}%"))
if backup_q:
query = query.filter(MailMessage.backup_software.ilike(f"%{backup_q}%"))
if type_q:
query = query.filter(MailMessage.backup_type.ilike(f"%{type_q}%"))
if job_name_q:
# Prefer stored job_name, but also allow matching the linked Job name.
query = query.filter(
or_(
MailMessage.job_name.ilike(f"%{job_name_q}%"),
Job.name.ilike(f"%{job_name_q}%"),
)
)
if only_unlinked:
query = query.filter(MailMessage.job_id.is_(None))
# Datetime window (received_at)
# Use dateutil.parser when available, otherwise a simple ISO parse fallback.
def _parse_dt(value: str):
if not value:
return None
try:
from dateutil import parser as dtparser # type: ignore
return dtparser.parse(value)
except Exception:
try:
# Accept "YYYY-MM-DDTHH:MM" from datetime-local.
return datetime.fromisoformat(value)
except Exception:
return None
dt_from = _parse_dt(received_from)
dt_to = _parse_dt(received_to)
if dt_from is not None:
query = query.filter(MailMessage.received_at >= dt_from)
if dt_to is not None:
query = query.filter(MailMessage.received_at <= dt_to)
total_items = query.count()
total_pages = max(1, math.ceil(total_items / per_page)) if total_items else 1
if page > total_pages:
page = total_pages
messages = (
query.order_by(
MailMessage.received_at.desc().nullslast(),
MailMessage.id.desc(),
)
.offset((page - 1) * per_page)
.limit(per_page)
.all()
)
rows = []
for msg in messages:
rows.append(
{
"id": msg.id,
"from_address": msg.from_address or "",
"subject": msg.subject or "",
"received_at": _format_datetime(msg.received_at),
"backup_software": msg.backup_software or "",
"backup_type": msg.backup_type or "",
"job_name": (msg.job_name or "") or (msg.job.name if msg.job else ""),
"linked": bool(msg.job_id),
"has_eml": bool(getattr(msg, "eml_stored_at", None)),
}
)
has_prev = page > 1
has_next = page < total_pages
return render_template(
"main/admin_all_mail.html",
rows=rows,
page=page,
total_pages=total_pages,
has_prev=has_prev,
has_next=has_next,
filters={
"from_q": from_q,
"subject_q": subject_q,
"backup_q": backup_q,
"type_q": type_q,
"job_name_q": job_name_q,
"received_from": received_from,
"received_to": received_to,
"only_unlinked": only_unlinked,
},
)

View File

@ -219,10 +219,15 @@
<script> <script>
(function () { (function () {
function initAdminAllMailPopup() {
var table = document.getElementById('mailAuditTable'); var table = document.getElementById('mailAuditTable');
var modalEl = document.getElementById('mailMessageModal'); var modalEl = document.getElementById('mailMessageModal');
if (!table || !modalEl) return; if (!table || !modalEl) return;
// base.html loads Bootstrap JS after the page content. Initialize after DOMContentLoaded
// so bootstrap.Modal is guaranteed to be available.
if (typeof bootstrap === 'undefined' || !bootstrap.Modal) return;
var modal = new bootstrap.Modal(modalEl); var modal = new bootstrap.Modal(modalEl);
function setText(id, value) { function setText(id, value) {
@ -297,6 +302,9 @@
if (!id) return; if (!id) return;
openMessage(id); openMessage(id);
}); });
}
document.addEventListener('DOMContentLoaded', initAdminAllMailPopup);
})(); })();
</script> </script>

View File

@ -14,6 +14,12 @@
- Reused the existing Inbox message detail modal behavior to open and inspect messages from the All Mail page. - Reused the existing Inbox message detail modal behavior to open and inspect messages from the All Mail page.
- Added navigation entry for admins to access the All Mail page. - Added navigation entry for admins to access the All Mail page.
---
## v20260108-28-admin-all-mail-open-fix
- Fixed All Mail row-click handling by switching to event delegation, ensuring message rows reliably open the detail modal.
- Ensured EML link clicks no longer trigger row-click modal opening.
================================================================================================================================================ ================================================================================================================================================
## v0.1.18 ## v0.1.18