Merge pull request 'v20260108-28-admin-all-mail-open-fix' (#63) from v20260108-28-admin-all-mail-open-fix into main
Reviewed-on: #63
This commit is contained in:
commit
c8a078bc45
@ -1 +1 @@
|
||||
v20260108-27-admin-all-mail-audit-page
|
||||
v20260108-28-admin-all-mail-open-fix
|
||||
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
@ -219,84 +219,92 @@
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var table = document.getElementById('mailAuditTable');
|
||||
var modalEl = document.getElementById('mailMessageModal');
|
||||
if (!table || !modalEl) return;
|
||||
function initAdminAllMailPopup() {
|
||||
var table = document.getElementById('mailAuditTable');
|
||||
var modalEl = document.getElementById('mailMessageModal');
|
||||
if (!table || !modalEl) return;
|
||||
|
||||
var modal = new bootstrap.Modal(modalEl);
|
||||
// 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;
|
||||
|
||||
function setText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.textContent = value || '';
|
||||
}
|
||||
var modal = new bootstrap.Modal(modalEl);
|
||||
|
||||
function renderObjects(objects) {
|
||||
var container = document.getElementById('msg_objects_container');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!objects || !objects.length) {
|
||||
container.innerHTML = '<div class="text-muted">No objects stored.</div>';
|
||||
return;
|
||||
function setText(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.textContent = value || '';
|
||||
}
|
||||
|
||||
var tableHtml = '<div class="table-responsive"><table class="table table-sm table-hover align-middle">' +
|
||||
'<thead class="table-light"><tr><th>Name</th><th>Type</th><th>Status</th><th>Error</th></tr></thead><tbody>';
|
||||
function renderObjects(objects) {
|
||||
var container = document.getElementById('msg_objects_container');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
var o = objects[i] || {};
|
||||
tableHtml += '<tr>' +
|
||||
'<td>' + (o.name || '') + '</td>' +
|
||||
'<td>' + (o.type || '') + '</td>' +
|
||||
'<td>' + (o.status || '') + '</td>' +
|
||||
'<td style="white-space: pre-wrap;">' + (o.error_message || '') + '</td>' +
|
||||
'</tr>';
|
||||
if (!objects || !objects.length) {
|
||||
container.innerHTML = '<div class="text-muted">No objects stored.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var tableHtml = '<div class="table-responsive"><table class="table table-sm table-hover align-middle">' +
|
||||
'<thead class="table-light"><tr><th>Name</th><th>Type</th><th>Status</th><th>Error</th></tr></thead><tbody>';
|
||||
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
var o = objects[i] || {};
|
||||
tableHtml += '<tr>' +
|
||||
'<td>' + (o.name || '') + '</td>' +
|
||||
'<td>' + (o.type || '') + '</td>' +
|
||||
'<td>' + (o.status || '') + '</td>' +
|
||||
'<td style="white-space: pre-wrap;">' + (o.error_message || '') + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
|
||||
tableHtml += '</tbody></table></div>';
|
||||
container.innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
tableHtml += '</tbody></table></div>';
|
||||
container.innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
function setIframeHtml(html) {
|
||||
var iframe = document.getElementById('msg_body_container_iframe');
|
||||
if (!iframe) return;
|
||||
iframe.srcdoc = html || '<p>No message content stored.</p>';
|
||||
}
|
||||
|
||||
async function openMessage(messageId) {
|
||||
try {
|
||||
var res = await fetch('{{ url_for('main.inbox_message_detail', message_id=0) }}'.replace('/0', '/' + messageId));
|
||||
if (!res.ok) throw new Error('Failed to load message');
|
||||
var data = await res.json();
|
||||
if (!data || data.status !== 'ok') throw new Error('Invalid response');
|
||||
|
||||
var meta = data.meta || {};
|
||||
setText('msg_from', meta.from_address);
|
||||
setText('msg_backup', meta.backup_software);
|
||||
setText('msg_type', meta.backup_type);
|
||||
setText('msg_job', meta.job_name);
|
||||
setText('msg_overall', meta.overall_status);
|
||||
setText('msg_customer', meta.customer_name);
|
||||
setText('msg_received', meta.received_at);
|
||||
setText('msg_parsed', meta.parsed_at);
|
||||
setText('msg_overall_message', meta.overall_message);
|
||||
|
||||
setIframeHtml(data.body_html);
|
||||
renderObjects(data.objects);
|
||||
|
||||
modal.show();
|
||||
} catch (e) {
|
||||
alert('Unable to open message details.');
|
||||
function setIframeHtml(html) {
|
||||
var iframe = document.getElementById('msg_body_container_iframe');
|
||||
if (!iframe) return;
|
||||
iframe.srcdoc = html || '<p>No message content stored.</p>';
|
||||
}
|
||||
|
||||
async function openMessage(messageId) {
|
||||
try {
|
||||
var res = await fetch('{{ url_for('main.inbox_message_detail', message_id=0) }}'.replace('/0', '/' + messageId));
|
||||
if (!res.ok) throw new Error('Failed to load message');
|
||||
var data = await res.json();
|
||||
if (!data || data.status !== 'ok') throw new Error('Invalid response');
|
||||
|
||||
var meta = data.meta || {};
|
||||
setText('msg_from', meta.from_address);
|
||||
setText('msg_backup', meta.backup_software);
|
||||
setText('msg_type', meta.backup_type);
|
||||
setText('msg_job', meta.job_name);
|
||||
setText('msg_overall', meta.overall_status);
|
||||
setText('msg_customer', meta.customer_name);
|
||||
setText('msg_received', meta.received_at);
|
||||
setText('msg_parsed', meta.parsed_at);
|
||||
setText('msg_overall_message', meta.overall_message);
|
||||
|
||||
setIframeHtml(data.body_html);
|
||||
renderObjects(data.objects);
|
||||
|
||||
modal.show();
|
||||
} catch (e) {
|
||||
alert('Unable to open message details.');
|
||||
}
|
||||
}
|
||||
|
||||
table.addEventListener('click', function (e) {
|
||||
var tr = e.target.closest('tr.mail-row');
|
||||
if (!tr) return;
|
||||
var id = tr.getAttribute('data-message-id');
|
||||
if (!id) return;
|
||||
openMessage(id);
|
||||
});
|
||||
}
|
||||
|
||||
table.addEventListener('click', function (e) {
|
||||
var tr = e.target.closest('tr.mail-row');
|
||||
if (!tr) return;
|
||||
var id = tr.getAttribute('data-message-id');
|
||||
if (!id) return;
|
||||
openMessage(id);
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', initAdminAllMailPopup);
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@ -14,6 +14,12 @@
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user