{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.crashmail_queue.page_title', {}, locale, ['common']) }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2"><i class="fas fa-bolt me-2"></i>{{ t('ui.admin.crashmail_queue.heading', {}, locale, ['common']) }}</h1>
<div class="btn-toolbar">
<button class="btn btn-warning me-2" id="attemptDelivery">
<i class="fas fa-paper-plane me-1"></i>{{ t('ui.admin.crashmail_queue.attempt_delivery', {}, locale, ['common']) }}
</button>
<button class="btn btn-outline-primary me-2" id="refreshQueue">
<i class="fas fa-sync-alt me-1"></i>{{ t('ui.common.refresh', {}, locale, ['common']) }}
</button>
<a href="/admin" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>{{ t('ui.admin.crashmail_queue.back_to_dashboard', {}, locale, ['common']) }}
</a>
</div>
</div>
<!-- Queue Statistics -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">{{ t('ui.admin.crashmail_queue.pending', {}, locale, ['common']) }}</div>
<div class="h5 mb-0 font-weight-bold" id="statPending">-</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">{{ t('ui.admin.crashmail_queue.attempting', {}, locale, ['common']) }}</div>
<div class="h5 mb-0 font-weight-bold" id="statAttempting">-</div>
</div>
<div class="col-auto">
<i class="fas fa-spinner fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">{{ t('ui.admin.crashmail_queue.sent_24h', {}, locale, ['common']) }}</div>
<div class="h5 mb-0 font-weight-bold" id="statSent">-</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-danger shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">{{ t('ui.admin.crashmail_queue.failed', {}, locale, ['common']) }}</div>
<div class="h5 mb-0 font-weight-bold" id="statFailed">-</div>
</div>
<div class="col-auto">
<i class="fas fa-times-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filter -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.crashmail_queue.filter_queue', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">{{ t('ui.admin.crashmail_queue.status', {}, locale, ['common']) }}</label>
<select class="form-select" id="filterStatus">
<option value="">{{ t('ui.common.all', {}, locale, ['common']) }}</option>
<option value="pending">{{ t('ui.admin.crashmail_queue.pending', {}, locale, ['common']) }}</option>
<option value="attempting">{{ t('ui.admin.crashmail_queue.attempting', {}, locale, ['common']) }}</option>
<option value="sent">{{ t('ui.admin.crashmail_queue.sent', {}, locale, ['common']) }}</option>
<option value="failed">{{ t('ui.admin.crashmail_queue.failed', {}, locale, ['common']) }}</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">{{ t('ui.admin.crashmail_queue.limit', {}, locale, ['common']) }}</label>
<select class="form-select" id="filterLimit">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button class="btn btn-primary" id="applyFilter">
<i class="fas fa-filter me-1"></i>{{ t('ui.admin.crashmail_queue.apply_filter', {}, locale, ['common']) }}
</button>
</div>
</div>
</div>
</div>
<!-- Queue Items -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{ t('ui.admin.crashmail_queue.queue_items', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="queueTable">
<thead>
<tr>
<th>{{ t('ui.common.id', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.crashmail_queue.to', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.crashmail_queue.subject', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.crashmail_queue.destination', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.crashmail_queue.status', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.crashmail_queue.attempts', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.crashmail_queue.next_attempt', {}, locale, ['common']) }}</th>
<th>{{ t('ui.admin.crashmail_queue.actions', {}, locale, ['common']) }}</th>
</tr>
</thead>
<tbody id="queueTableBody">
<tr>
<td colspan="8" class="text-center">{{ t('ui.common.loading', {}, locale, ['common']) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
.border-left-primary { border-left: 0.25rem solid #4e73df !important; }
.border-left-success { border-left: 0.25rem solid #1cc88a !important; }
.border-left-info { border-left: 0.25rem solid #36b9cc !important; }
.border-left-warning { border-left: 0.25rem solid #f6c23e !important; }
.border-left-danger { border-left: 0.25rem solid #e74a3b !important; }
</style>
<script>
function apiMessage(payload, fallback) {
if (window.getApiMessage) {
return window.getApiMessage(payload, fallback);
}
return payload && payload.message ? payload.message : fallback;
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
return payload && payload.error ? payload.error : fallback;
}
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
document.addEventListener('DOMContentLoaded', function() {
loadI18nNamespaces(['common', 'errors']).then(function() {
loadStats();
loadQueue();
});
document.getElementById('refreshQueue').addEventListener('click', function() {
loadStats();
loadQueue();
});
document.getElementById('attemptDelivery').addEventListener('click', function() {
if (!confirm(uiT('ui.admin.crashmail_queue.attempt_delivery_confirm', 'Attempt delivery for pending crashmail now?'))) return;
fetch('/admin/api/crashmail/poll', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert(apiMessage(data, uiT('ui.admin.crashmail_queue.delivery_attempt_started', 'Delivery attempt started.')));
loadStats();
loadQueue();
} else {
alert(uiT('ui.admin.crashmail_queue.delivery_attempt_failed_prefix', 'Delivery attempt failed: ') + apiError(data, uiT('ui.common.unknown_error', 'Unknown error')));
}
})
.catch(error => {
alert(uiT('ui.admin.crashmail_queue.error_prefix', 'Error: ') + error.message);
});
});
document.getElementById('applyFilter').addEventListener('click', loadQueue);
document.getElementById('filterStatus').addEventListener('change', loadQueue);
});
function loadStats() {
fetch('/admin/api/crashmail/stats')
.then(response => response.json())
.then(data => {
document.getElementById('statPending').textContent = data.pending || 0;
document.getElementById('statAttempting').textContent = data.attempting || 0;
document.getElementById('statSent').textContent = data.sent_24h || 0;
document.getElementById('statFailed').textContent = data.failed || 0;
})
.catch(error => {
console.error(uiT('ui.admin.crashmail_queue.load_stats_failed', 'Error loading stats') + ':', error);
});
}
function loadQueue() {
const status = document.getElementById('filterStatus').value;
const limit = document.getElementById('filterLimit').value;
let url = `/admin/api/crashmail/queue?limit=${limit}`;
if (status) url += `&status=${status}`;
fetch(url)
.then(response => response.json())
.then(data => {
const tbody = document.getElementById('queueTableBody');
if (!data.items || data.items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">' + uiT('ui.admin.crashmail_queue.no_items', 'No items in queue') + '</td></tr>';
return;
}
tbody.innerHTML = data.items.map(item => `
<tr>
<td>${item.id}</td>
<td>
<strong>${escapeHtml(item.to_name || '')}</strong><br>
<small class="text-muted">${escapeHtml(item.destination_address)}</small>
</td>
<td>${escapeHtml(item.subject || uiT('ui.admin.crashmail_queue.no_subject', '(no subject)'))}</td>
<td>
${item.destination_host ? `${escapeHtml(item.destination_host)}:${item.destination_port}` : '<em>' + uiT('ui.admin.crashmail_queue.unresolved', 'Unresolved') + '</em>'}
</td>
<td>${getStatusBadge(item.status)}</td>
<td>${item.attempts} / ${item.max_attempts}</td>
<td>${item.next_attempt_at ? formatDate(item.next_attempt_at) : '-'}</td>
<td>
${item.status === 'failed' ? `
<button class="btn btn-sm btn-warning" onclick="retryItem(${item.id})">
<i class="fas fa-redo"></i>
</button>
` : ''}
${item.status !== 'sent' ? `
<button class="btn btn-sm btn-danger" onclick="cancelItem(${item.id})">
<i class="fas fa-times"></i>
</button>
` : ''}
</td>
</tr>
`).join('');
})
.catch(error => {
console.error(uiT('ui.admin.crashmail_queue.load_queue_failed', 'Error loading queue') + ':', error);
document.getElementById('queueTableBody').innerHTML =
'<tr><td colspan="8" class="text-center text-danger">' + uiT('ui.admin.crashmail_queue.load_queue_failed', 'Error loading queue') + '</td></tr>';
});
}
function getStatusBadge(status) {
const badges = {
'pending': '<span class="badge bg-warning">' + uiT('ui.admin.crashmail_queue.pending', 'Pending') + '</span>',
'attempting': '<span class="badge bg-info">' + uiT('ui.admin.crashmail_queue.attempting', 'Attempting') + '</span>',
'sent': '<span class="badge bg-success">' + uiT('ui.admin.crashmail_queue.sent', 'Sent') + '</span>',
'failed': '<span class="badge bg-danger">' + uiT('ui.admin.crashmail_queue.failed', 'Failed') + '</span>'
};
return badges[status] || `<span class="badge bg-secondary">${status}</span>`;
}
function retryItem(id) {
if (!confirm(uiT('ui.admin.crashmail_queue.retry_confirm', 'Retry this crashmail delivery?'))) return;
fetch(`/admin/api/crashmail/${id}/retry`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert(apiMessage(data, uiT('ui.admin.crashmail_queue.retry_success', 'Crashmail retry queued.')));
loadStats();
loadQueue();
} else {
alert(uiT('ui.admin.crashmail_queue.retry_failed_prefix', 'Failed to retry: ') + apiError(data, uiT('ui.common.unknown_error', 'Unknown error')));
}
})
.catch(error => {
alert(uiT('ui.admin.crashmail_queue.error_prefix', 'Error: ') + error.message);
});
}
function cancelItem(id) {
if (!confirm(uiT('ui.admin.crashmail_queue.cancel_confirm', 'Cancel this crashmail? The message will remain in your outbox but won\'t be crash-delivered.'))) return;
fetch(`/admin/api/crashmail/${id}`, { method: 'DELETE' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert(apiMessage(data, uiT('ui.admin.crashmail_queue.cancel_success', 'Crashmail cancelled.')));
loadStats();
loadQueue();
} else {
alert(uiT('ui.admin.crashmail_queue.cancel_failed_prefix', 'Failed to cancel: ') + apiError(data, uiT('ui.common.unknown_error', 'Unknown error')));
}
})
.catch(error => {
alert(uiT('ui.admin.crashmail_queue.error_prefix', 'Error: ') + error.message);
});
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleString();
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}
|