From b3fde8f4314159362d8829d30c52f2c1cc9d18c4 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 8 Jan 2026 12:54:15 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-08 12:54:15) --- .last-branch | 2 +- .../src/backend/app/main/routes.py | 2 +- .../src/backend/app/main/routes_admin_mail.py | 135 +++++++++ .../src/templates/layout/base.html | 8 +- .../src/templates/main/admin_all_mail.html | 262 ++++++++++-------- docs/changelog.md | 6 + 6 files changed, 291 insertions(+), 124 deletions(-) create mode 100644 containers/backupchecks/src/backend/app/main/routes_admin_mail.py diff --git a/.last-branch b/.last-branch index 3941804..53c4b67 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260108-27-admin-all-mail-audit-page +v20260108-28-admin-all-mail-open-fix diff --git a/containers/backupchecks/src/backend/app/main/routes.py b/containers/backupchecks/src/backend/app/main/routes.py index 48d891a..b996b1b 100644 --- a/containers/backupchecks/src/backend/app/main/routes.py +++ b/containers/backupchecks/src/backend/app/main/routes.py @@ -10,7 +10,7 @@ from .routes_shared import main_bp, roles_required # noqa: F401 from . import routes_core # noqa: F401 from . import routes_news # noqa: F401 from . import routes_inbox # noqa: F401 -from . import routes_mail_audit # noqa: F401 +from . import routes_admin_mail # noqa: F401 from . import routes_customers # noqa: F401 from . import routes_jobs # noqa: F401 from . import routes_settings # noqa: F401 diff --git a/containers/backupchecks/src/backend/app/main/routes_admin_mail.py b/containers/backupchecks/src/backend/app/main/routes_admin_mail.py new file mode 100644 index 0000000..1850e16 --- /dev/null +++ b/containers/backupchecks/src/backend/app/main/routes_admin_mail.py @@ -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, + }, + ) diff --git a/containers/backupchecks/src/templates/layout/base.html b/containers/backupchecks/src/templates/layout/base.html index 54d9897..a0ff520 100644 --- a/containers/backupchecks/src/templates/layout/base.html +++ b/containers/backupchecks/src/templates/layout/base.html @@ -72,14 +72,12 @@ Inbox {% if active_role == 'admin' %} - - {% endif %} - {% if active_role == 'admin' %} + {% endif %}