712 lines
24 KiB
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> |