From b7f057f0b57f9944324d47ccbea61fedab555f74 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 8 Jan 2026 12:40:27 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-08 12:40:27) --- .last-branch | 2 +- .../src/backend/app/main/routes.py | 1 + .../src/backend/app/main/routes_mail_audit.py | 139 ++++++++ .../src/templates/layout/base.html | 5 + .../src/templates/main/admin_all_mail.html | 303 ++++++++++++++++++ docs/changelog.md | 8 + 6 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 containers/backupchecks/src/backend/app/main/routes_mail_audit.py create mode 100644 containers/backupchecks/src/templates/main/admin_all_mail.html diff --git a/.last-branch b/.last-branch index 5bac2fe..3941804 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260108-26-mail-move-only-after-successful-import +v20260108-27-admin-all-mail-audit-page diff --git a/containers/backupchecks/src/backend/app/main/routes.py b/containers/backupchecks/src/backend/app/main/routes.py index 60f2719..48d891a 100644 --- a/containers/backupchecks/src/backend/app/main/routes.py +++ b/containers/backupchecks/src/backend/app/main/routes.py @@ -10,6 +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_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_mail_audit.py b/containers/backupchecks/src/backend/app/main/routes_mail_audit.py new file mode 100644 index 0000000..1e41d71 --- /dev/null +++ b/containers/backupchecks/src/backend/app/main/routes_mail_audit.py @@ -0,0 +1,139 @@ +from .routes_shared import * # noqa: F401,F403 +from .routes_shared import _format_datetime + +from datetime import datetime + +from sqlalchemy import or_ + + +def _parse_datetime_local(value: str): + if not value: + return None + value = value.strip() + if not value: + return None + try: + # Accept HTML datetime-local values like 2026-01-08T10:30 + return datetime.fromisoformat(value) + except Exception: + return None + + +@main_bp.route("/admin/mails") +@login_required +@roles_required("admin") +def admin_all_mails(): + 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_raw = (request.args.get("received_from", "") or "").strip() + received_to_raw = (request.args.get("received_to", "") or "").strip() + only_unlinked = (request.args.get("only_unlinked", "") or "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + + received_from = _parse_datetime_local(received_from_raw) + received_to = _parse_datetime_local(received_to_raw) + + query = MailMessage.query + + # Text filters + 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 on message, but also match linked job name + query = query.outerjoin(Job, Job.id == MailMessage.job_id).filter( + or_( + MailMessage.job_name.ilike(f"%{job_name_q}%"), + Job.job_name.ilike(f"%{job_name_q}%"), + ) + ) + + # Time window + if received_from: + query = query.filter(MailMessage.received_at >= received_from) + if received_to: + query = query.filter(MailMessage.received_at <= received_to) + + # Linked/unlinked + if only_unlinked: + query = query.filter(MailMessage.job_id.is_(None)) + + 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: + linked = bool(msg.job_id) + 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 ""), + "linked": linked, + "parsed_at": _format_datetime(msg.parsed_at), + "overall_status": msg.overall_status or "", + "has_eml": bool(getattr(msg, "eml_stored_at", None)), + } + ) + + has_prev = page > 1 + has_next = page < total_pages + + filter_params = { + "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_raw, + "received_to": received_to_raw, + "only_unlinked": "1" if only_unlinked else "", + } + + return render_template( + "main/admin_all_mail.html", + rows=rows, + page=page, + total_pages=total_pages, + has_prev=has_prev, + has_next=has_next, + filter_params=filter_params, + ) diff --git a/containers/backupchecks/src/templates/layout/base.html b/containers/backupchecks/src/templates/layout/base.html index a0cc910..54d9897 100644 --- a/containers/backupchecks/src/templates/layout/base.html +++ b/containers/backupchecks/src/templates/layout/base.html @@ -72,6 +72,11 @@ Inbox {% if active_role == 'admin' %} + + {% endif %} + {% if active_role == 'admin' %} diff --git a/containers/backupchecks/src/templates/main/admin_all_mail.html b/containers/backupchecks/src/templates/main/admin_all_mail.html new file mode 100644 index 0000000..7adab43 --- /dev/null +++ b/containers/backupchecks/src/templates/main/admin_all_mail.html @@ -0,0 +1,303 @@ +{% extends "layout/base.html" %} + + + +{# Pager macro must be defined before it is used #} +{% macro pager(position, page, total_pages, has_prev, has_next, filter_params) -%} +
+
+ {% if has_prev %} + Previous + {% else %} + + {% endif %} + {% if has_next %} + Next + {% else %} + + {% endif %} +
+ +
+ Page {{ page }} of {{ total_pages }} +
+ {% for k, v in filter_params.items() %} + {% if v %} + + {% endif %} + {% endfor %} + + + +
+
+
+{%- endmacro %} + +{% block content %} +

All Mail

+ +
+
+ Search Filters +
+ Clear Filter Values + +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ +{{ pager("top", page, total_pages, has_prev, has_next, filter_params) }} + +
+ + + + + + + + + + + + + + + + {% if rows %} + {% for row in rows %} + + + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
ReceivedFromSubjectBackupTypeJob nameLinkedParsedEML
{{ row.received_at }}{{ row.from_address }}{{ row.subject }}{{ row.backup_software }}{{ row.backup_type }}{{ row.job_name }} + {% if row.linked %} + Linked + {% else %} + Unlinked + {% endif %} + {{ row.parsed_at }} + {% if row.has_eml %} + EML + {% endif %} +
No messages found.
+
+ +{{ pager("bottom", page, total_pages, has_prev, has_next, filter_params) }} + + + + + +{% endblock %} diff --git a/docs/changelog.md b/docs/changelog.md index f884e22..98df995 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,14 @@ - Added explicit commit and rollback handling to ensure database integrity before mail state changes. - Improved logging around import and commit failures to better trace skipped or retried mails. +--- + +## v20260108-27-admin-all-mail-audit-page +- Added an admin-only “All Mail” page showing all received mail messages with 50 items per page. +- Implemented always-visible search filters with AND-combined criteria: From, Subject, Backup, Type, Job name, and a received datetime window. +- Added “Only unlinked” filter to quickly find messages not linked to a job. +- 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. ================================================================================================================================================