agmission/Development/server/public/dlq-monitor.html

712 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DLQ Monitor</title>
<link rel="shortcut icon" href="data:," />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, white 0%, #4CAF50 50%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: white;
margin-bottom: 10px;
text-align: center;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.subtitle {
color: rgba(255, 255, 255, 0.9);
text-align: center;
margin-bottom: 30px;
font-size: 1.1em;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.stat-card h3 {
color: #667eea;
font-size: 0.9em;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-card .value {
font-size: 2.5em;
font-weight: bold;
color: #333;
}
.stat-card.danger .value {
color: #e74c3c;
}
.stat-card.success .value {
color: #27ae60;
}
.stat-card.warning .value {
color: #f39c12;
}
.stat-card .sub-value {
font-size: 0.9em;
color: #7f8c8d;
margin-top: 5px;
}
.actions {
background: white;
border-radius: 10px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.actions h2 {
color: #667eea;
margin-bottom: 20px;
}
.button-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
button {
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-warning {
background: #f39c12;
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
.messages-panel {
background: white;
border-radius: 10px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.messages-panel h2 {
color: #667eea;
margin-bottom: 20px;
}
#messages-list {
max-height: 500px;
overflow-y: auto;
}
.message-item {
background: #f8f9fa;
border-left: 4px solid #667eea;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
}
.message-item .header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.message-item .task-type {
font-weight: bold;
color: #2c3e50;
}
.message-item .time {
color: #7f8c8d;
font-size: 0.85em;
}
.message-item .details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-bottom: 10px;
font-size: 0.9em;
}
.message-item .detail strong {
color: #667eea;
}
.message-item .error {
background: #fee;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 0.85em;
color: #c0392b;
margin-top: 10px;
max-height: 100px;
overflow-y: auto;
}
.error-message,
.success-message {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
}
.error-message {
background: #e74c3c;
color: white;
}
.success-message {
background: #27ae60;
color: white;
}
.last-updated {
text-align: center;
color: white;
margin-top: 20px;
font-size: 0.9em;
}
.category-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 3px;
font-size: 0.8em;
font-weight: bold;
}
.category-transient {
background: #3498db;
color: white;
}
.category-validation {
background: #e74c3c;
color: white;
}
.category-processing {
background: #f39c12;
color: white;
}
.category-infrastructure {
background: #95a5a6;
color: white;
}
.category-partner_api {
background: #9b59b6;
color: white;
}
.category-unknown {
background: #7f8c8d;
color: white;
}
.queue-selector {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.queue-selector label {
color: #667eea;
font-weight: bold;
margin-right: 10px;
}
.queue-selector select {
padding: 8px 12px;
border: 2px solid #667eea;
border-radius: 5px;
font-size: 1em;
}
</style>
</head>
<body>
<div class="container">
<h1>🔍 Dead Letter Queue Monitor</h1>
<p class="subtitle">Real-time monitoring and management</p>
<div id="error-alert" class="error-message"></div>
<div id="success-alert" class="success-message"></div>
<div class="queue-selector">
<label>Queue:</label>
<select id="queue-select" onchange="refreshAll()">
<option value="dev_partner_tasks">dev_partner_tasks</option>
<option value="partner_tasks">partner_tasks</option>
</select>
</div>
<div class="stats-grid">
<div class="stat-card" id="dlq-card">
<h3>DLQ Messages</h3>
<div class="value" id="dlq-count">-</div>
<div class="sub-value" id="dlq-status">Loading...</div>
</div>
<div class="stat-card">
<h3>Retention Period</h3>
<div class="value" id="retention-days">-</div>
<div class="sub-value">days until auto-archive</div>
</div>
<div class="stat-card" id="alert-card">
<h3>Alert Threshold</h3>
<div class="value" id="alert-threshold">-</div>
<div class="sub-value">messages before alert</div>
</div>
<div class="stat-card">
<h3>Consumers</h3>
<div class="value" id="consumer-count">-</div>
<div class="sub-value">active</div>
</div>
</div>
<div class="actions">
<h2>⚙️ Queue Operations</h2>
<div class="button-group">
<button class="btn-primary" onclick="refreshAll()">🔄 Refresh</button>
<button class="btn-success" onclick="retryAll()">↩️ Retry All</button>
<button class="btn-warning" onclick="retryByHeader()">🏷️ Retry by Header</button>
<button class="btn-primary" onclick="processDLQ()">⚡ Auto-Process</button>
<button class="btn-danger" onclick="purgeDLQ()">🗑️ Purge</button>
<button class="btn-danger" onclick="logout()" style="margin-left: auto;">🚪 Logout</button>
</div>
</div>
<div class="messages-panel">
<h2>📋 Recent Messages</h2>
<div id="messages-list">Loading...</div>
</div>
<div class="last-updated" id="last-updated">Last updated: Never</div>
</div>
<script>
let authToken = localStorage.getItem('agm_auth_token') || null;
let isLoggingIn = false;
async function login(username, password) {
try {
console.log('Attempting login for:', username);
const response = await fetch('/api/users/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
console.log('Login response status:', response.status);
if (!response.ok) {
const error = await response.json();
console.error('Login failed:', error);
throw new Error(error.error?.message || 'Login failed');
}
const data = await response.json();
console.log('Login response data:', data);
console.log('User info:', {
userType: data.roles?.[0],
userId: data._id,
isAdmin: data.roles?.[0] === '0'
});
if (!data.token) {
throw new Error('No token received from login');
}
// Check if user is admin (roles[0] should be '0')
const userType = data.roles?.[0];
if (userType !== '0' && userType !== 0) {
throw new Error('Access denied. Only admin users can access this dashboard. Your user type: ' + userType);
}
authToken = data.token;
localStorage.setItem('agm_auth_token', authToken);
console.log('Token saved to localStorage');
return authToken;
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
function getAuthToken() {
// Check if token exists and is not empty
if (!authToken || authToken.trim() === '') {
if (isLoggingIn) return null; // Prevent recursive prompts
isLoggingIn = true;
const username = prompt('Enter admin username (or email):');
if (!username || username.trim() === '') {
isLoggingIn = false;
throw new Error('Authentication required. Please provide a username.');
}
const password = prompt('Enter admin password:');
if (!password || password.trim() === '') {
isLoggingIn = false;
throw new Error('Authentication required. Please provide a password.');
}
// Login and get token
login(username, password)
.then(() => {
isLoggingIn = false;
showSuccess('Login successful! Refreshing...');
setTimeout(() => location.reload(), 1000);
})
.catch(error => {
isLoggingIn = false;
localStorage.removeItem('agm_auth_token');
authToken = null;
showError('Login failed: ' + error.message);
});
return null; // Return null for now, page will reload after login
}
return authToken;
}
async function authFetch(url, options = {}) {
const token = getAuthToken();
if (!token) {
throw new Error('Authentication in progress. Please wait...');
}
const headers = {
'Content-Type': 'application/json',
...options.headers,
'Authorization': `Bearer ${token}`
};
console.log('Making authenticated request to:', url, 'with token:', token.substring(0, 20) + '...');
const response = await fetch(url, { ...options, headers });
if (response.status === 401 || response.status === 403) {
console.error('401/403 Unauthorized - token invalid or insufficient permissions');
// Try to get error details
try {
const errorData = await response.json();
console.error('Auth error details:', errorData);
} catch (e) {
console.error('Could not parse error response');
}
localStorage.removeItem('agm_auth_token');
authToken = null;
showError('Authentication failed. Your account may not have admin privileges. Please refresh to login again.');
throw new Error('Authentication failed. Token invalid, expired, or insufficient permissions.');
}
if (!response.ok) {
let errorMsg = `HTTP ${response.status}: ${response.statusText}`;
try {
const errorData = await response.json();
if (errorData.error) {
errorMsg = errorData.error.message || errorData.error;
}
} catch (e) {
// Response not JSON, use default message
}
console.error('Request failed:', errorMsg);
throw new Error(errorMsg);
}
return response;
}
function showError(msg) {
const el = document.getElementById('error-alert');
el.textContent = msg;
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 5000);
}
function showSuccess(msg) {
const el = document.getElementById('success-alert');
el.textContent = msg;
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 5000);
}
async function refreshStats() {
try {
const queueName = document.getElementById('queue-select').value;
const res = await authFetch(`/api/dlq/${queueName}/stats`);
const data = await res.json();
if (data && data.dlq) {
const msgCount = data.dlq.messageCount || 0;
document.getElementById('dlq-count').textContent = msgCount;
document.getElementById('consumer-count').textContent = data.dlq.consumerCount || 0;
// Static values for retention and threshold (can be made configurable later)
document.getElementById('retention-days').textContent = 365;
document.getElementById('alert-threshold').textContent = 20;
const card = document.getElementById('dlq-card');
const status = document.getElementById('dlq-status');
card.classList.remove('danger', 'warning', 'success');
// Thresholds: critical >= 50, warning >= 20
if (msgCount >= 50) {
card.classList.add('danger');
status.textContent = '🔴 CRITICAL';
} else if (msgCount >= 20) {
card.classList.add('warning');
status.textContent = '🟡 WARNING';
} else {
card.classList.add('success');
status.textContent = '🟢 Normal';
}
}
} catch (error) {
showError('Failed to refresh stats: ' + error.message);
}
}
async function refreshMessages() {
const list = document.getElementById('messages-list');
try {
const queueName = document.getElementById('queue-select').value;
const res = await authFetch(`/api/dlq/${queueName}/messages?limit=20`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
if (!data.messages || !data.messages.length) {
list.innerHTML = '<p style="text-align:center;color:#7f8c8d;padding:20px;">No messages</p>';
} else {
list.innerHTML = data.messages.map((msg, i) => {
const task = msg.taskInfo || {};
const category = msg.headers?.['x-error-category'] || 'unknown';
const severity = msg.headers?.['x-severity'] || 'low';
const partner = msg.headers?.['x-partner-code'] || 'N/A';
return `
<div class="message-item">
<div class="header">
<span class="task-type">${task.logFileName || 'Unknown'}</span>
<span class="time">Position: ${i}</span>
</div>
<div class="details">
<div class="detail"><strong>Partner:</strong> ${partner}</div>
<div class="detail"><strong>Category:</strong> <span class="category-badge category-${category}">${category}</span></div>
<div class="detail"><strong>Severity:</strong> ${severity}</div>
</div>
${msg.errorMessage ? `<div class="error">${msg.errorMessage}</div>` : ''}
<button class="btn-success" onclick="retryByPosition(${i})" style="margin-top:10px;">↩️ Retry Position ${i}</button>
</div>
`;
}).join('');
}
document.getElementById('last-updated').textContent = `Last updated: ${new Date().toLocaleString()}`;
} catch (error) {
list.innerHTML = '<p style="text-align:center;color:#e74c3c;padding:20px;">❌ Failed to load messages<br><small>' + error.message + '</small></p>';
showError('Failed to load messages: ' + error.message);
}
}
async function retryAll() {
if (!confirm('Retry all DLQ messages?')) return;
try {
const queue = document.getElementById('queue-select').value;
const res = await authFetch(`/api/dlq/${queue}/retryAll`, {
method: 'POST',
body: JSON.stringify({ maxMessages: 1000 })
});
const data = await res.json();
showSuccess(`Retried ${data.retriedCount} messages!`);
refreshAll();
} catch (error) {
showError('Failed to retry: ' + error.message);
}
}
async function retryByPosition(pos) {
try {
const queue = document.getElementById('queue-select').value;
await authFetch(`/api/dlq/${queue}/retryByPosition`, {
method: 'POST',
body: JSON.stringify({ position: pos })
});
showSuccess(`Retried message at position ${pos}!`);
refreshAll();
} catch (error) {
showError('Failed: ' + error.message);
}
}
async function retryByHeader() {
const name = prompt('Header name (e.g., x-partner-code):');
if (!name) return;
const value = prompt('Header value (e.g., SATLOC):');
if (!value) return;
try {
const queue = document.getElementById('queue-select').value;
const res = await authFetch(`/api/dlq/${queue}/retryByHeader`, {
method: 'POST',
body: JSON.stringify({ headerName: name, headerValue: value, maxMessages: 100 })
});
const data = await res.json();
showSuccess(`Retried ${data.retriedCount} messages!`);
refreshAll();
} catch (error) {
showError('Failed: ' + error.message);
}
}
async function processDLQ() {
if (!confirm('Auto-process DLQ? Categorizes errors and retries/archives.')) return;
try {
const queueName = document.getElementById('queue-select').value;
const res = await authFetch(`/api/dlq/${queueName}/process`, {
method: 'POST',
body: JSON.stringify({ maxMessages: 100 })
});
const data = await res.json();
showSuccess(`Processed ${data.processed}: ${data.retried} retried, ${data.archived} archived`);
refreshAll();
} catch (error) {
showError('Failed: ' + error.message);
}
}
async function purgeDLQ() {
if (prompt('⚠️ WARNING: Delete ALL messages! Type "PURGE":') !== 'PURGE') return;
try {
const queueName = document.getElementById('queue-select').value;
const res = await authFetch(`/api/dlq/${queueName}/purge`, {
method: 'DELETE',
body: JSON.stringify({ confirm: true })
});
const data = await res.json();
showSuccess(`Purged ${data.purgedCount} messages`);
refreshAll();
} catch (error) {
showError('Failed: ' + error.message);
}
}
function logout() {
if (confirm('Logout and clear authentication?')) {
localStorage.removeItem('agm_auth_token');
authToken = null;
location.reload();
}
}
async function refreshAll() {
try {
await Promise.all([refreshStats(), refreshMessages()]);
} catch (error) {
// Errors already shown by individual functions
console.error('Refresh error:', error);
}
}
// Initial load and periodic refresh
setInterval(refreshAll, 30000);
refreshAll();
</script>
</body>
</html>