enterprise_assest_managemen.../node_api/public/job-dashboard.html

551 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Asset Management - Job Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.6;
}
.header {
background: #2c3e50;
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 {
margin: 0;
font-size: 1.5rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border: 1px solid #e1e8ed;
}
.card h2 {
color: #2c3e50;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.status-indicator {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
text-transform: uppercase;
}
.status-healthy { background: #d4edda; color: #155724; }
.status-degraded { background: #fff3cd; color: #856404; }
.status-unhealthy { background: #f8d7da; color: #721c24; }
.status-running { background: #cce5ff; color: #004085; }
.status-completed { background: #d4edda; color: #155724; }
.status-failed { background: #f8d7da; color: #721c24; }
.status-scheduled { background: #e2e3e5; color: #383d41; }
.metric {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #e1e8ed;
}
.metric:last-child {
border-bottom: none;
}
.metric-label {
font-weight: 500;
color: #555;
}
.metric-value {
font-weight: 600;
color: #2c3e50;
}
.button {
background: #3498db;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.2s;
}
.button:hover {
background: #2980b9;
}
.button.danger {
background: #e74c3c;
}
.button.danger:hover {
background: #c0392b;
}
.button.success {
background: #27ae60;
}
.button.success:hover {
background: #229954;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.job-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.refresh-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.last-updated {
color: #666;
font-size: 0.875rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #e1e8ed;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #2c3e50;
}
tr:hover {
background: #f8f9fa;
}
.full-width {
grid-column: 1 / -1;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.refresh-controls {
flex-direction: column;
gap: 1rem;
}
}
</style>
</head>
<body>
<div class="header">
<h1>Asset Management - Job Dashboard</h1>
</div>
<div class="container">
<div class="refresh-controls">
<div>
<button class="button" onclick="refreshDashboard()">Refresh Data</button>
<button class="button success" onclick="runDepreciationJob()">Run Depreciation Job</button>
</div>
<div class="last-updated" id="lastUpdated">Last updated: Never</div>
</div>
<div id="errorContainer"></div>
<div class="dashboard-grid">
<!-- Job Manager Status -->
<div class="card">
<h2>Job Manager Status</h2>
<div id="jobManagerStatus" class="loading">Loading...</div>
</div>
<!-- System Health -->
<div class="card">
<h2>System Health</h2>
<div id="systemHealth" class="loading">Loading...</div>
</div>
<!-- Depreciation Job Status -->
<div class="card">
<h2>Depreciation Job</h2>
<div id="depreciationJobStatus" class="loading">Loading...</div>
</div>
<!-- Recent Job History -->
<div class="card full-width">
<h2>Recent Job Executions</h2>
<div id="jobHistory" class="loading">Loading...</div>
</div>
<!-- Depreciation Summary -->
<div class="card full-width">
<h2>Monthly Depreciation Summary</h2>
<div id="depreciationSummary" class="loading">Loading...</div>
</div>
</div>
</div>
<script>
let refreshInterval;
// API base URL
const API_BASE = '/api/jobs';
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
refreshDashboard();
// Auto-refresh every 30 seconds
refreshInterval = setInterval(refreshDashboard, 30000);
});
// Refresh all dashboard data
async function refreshDashboard() {
try {
await Promise.all([
loadJobStatus(),
loadSystemHealth(),
loadJobHistory(),
loadDepreciationSummary()
]);
document.getElementById('lastUpdated').textContent =
`Last updated: ${new Date().toLocaleString()}`;
clearError();
} catch (error) {
showError('Failed to refresh dashboard: ' + error.message);
}
}
// Load job manager status
async function loadJobStatus() {
try {
const response = await fetch(API_BASE + '/status');
const data = await response.json();
if (!response.ok) throw new Error(data.error);
renderJobStatus(data);
} catch (error) {
document.getElementById('jobManagerStatus').innerHTML =
'<div class="error">Error loading job status</div>';
}
}
// Load system health
async function loadSystemHealth() {
try {
const response = await fetch(API_BASE + '/health');
const data = await response.json();
renderSystemHealth(data);
} catch (error) {
document.getElementById('systemHealth').innerHTML =
'<div class="error">Error loading system health</div>';
}
}
// Load job history
async function loadJobHistory() {
try {
const response = await fetch(API_BASE + '/history/depreciation?limit=10');
const data = await response.json();
if (!response.ok) throw new Error(data.error);
renderJobHistory(data);
} catch (error) {
document.getElementById('jobHistory').innerHTML =
'<div class="error">Error loading job history</div>';
}
}
// Load depreciation summary
async function loadDepreciationSummary() {
try {
const response = await fetch(API_BASE + '/depreciation/summary');
const data = await response.json();
if (!response.ok) throw new Error(data.error);
renderDepreciationSummary(data);
} catch (error) {
document.getElementById('depreciationSummary').innerHTML =
'<div class="error">Error loading depreciation summary</div>';
}
}
// Render job status
function renderJobStatus(data) {
const container = document.getElementById('jobManagerStatus');
const managerStatus = data.managerStatus;
let html = `
<div class="metric">
<span class="metric-label">Initialized</span>
<span class="metric-value">${managerStatus.initialized ? 'Yes' : 'No'}</span>
</div>
<div class="metric">
<span class="metric-label">Running</span>
<span class="metric-value">${managerStatus.running ? 'Yes' : 'No'}</span>
</div>
`;
container.innerHTML = html;
// Render individual job status
if (data.jobs && data.jobs.depreciation) {
renderDepreciationJobStatus(data.jobs.depreciation);
}
}
// Render depreciation job status
function renderDepreciationJobStatus(job) {
const container = document.getElementById('depreciationJobStatus');
let html = `
<div class="metric">
<span class="metric-label">Status</span>
<span class="status-indicator status-${job.status}">${job.status}</span>
</div>
<div class="metric">
<span class="metric-label">Schedule</span>
<span class="metric-value">${job.schedule}</span>
</div>
<div class="metric">
<span class="metric-label">Last Run</span>
<span class="metric-value">${job.lastRun ? new Date(job.lastRun).toLocaleString() : 'Never'}</span>
</div>
<div class="metric">
<span class="metric-label">Next Run</span>
<span class="metric-value">${job.nextRun ? new Date(job.nextRun).toLocaleString() : 'Not scheduled'}</span>
</div>
`;
container.innerHTML = html;
}
// Render system health
function renderSystemHealth(data) {
const container = document.getElementById('systemHealth');
let html = `
<div class="metric">
<span class="metric-label">Overall Status</span>
<span class="status-indicator status-${data.status}">${data.status}</span>
</div>
<div class="metric">
<span class="metric-label">Database</span>
<span class="metric-value">${data.details.database ? 'Connected' : 'Disconnected'}</span>
</div>
<div class="metric">
<span class="metric-label">Timestamp</span>
<span class="metric-value">${new Date(data.timestamp).toLocaleString()}</span>
</div>
`;
container.innerHTML = html;
}
// Render job history
function renderJobHistory(data) {
const container = document.getElementById('jobHistory');
if (!data || data.length === 0) {
container.innerHTML = '<p>No job history available</p>';
return;
}
let html = `
<table>
<thead>
<tr>
<th>Execution ID</th>
<th>Status</th>
<th>Started</th>
<th>Duration</th>
<th>Details</th>
</tr>
</thead>
<tbody>
`;
data.forEach(job => {
const duration = job.duration_ms ? `${job.duration_ms}ms` : 'N/A';
const details = job.details ? JSON.stringify(job.details).substring(0, 100) + '...' : 'N/A';
html += `
<tr>
<td>${job.execution_id}</td>
<td><span class="status-indicator status-${job.status}">${job.status}</span></td>
<td>${new Date(job.started_at).toLocaleString()}</td>
<td>${duration}</td>
<td>${details}</td>
</tr>
`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// Render depreciation summary
function renderDepreciationSummary(data) {
const container = document.getElementById('depreciationSummary');
if (!data.summary || !data.summary.overall) {
container.innerHTML = '<p>No depreciation data available</p>';
return;
}
const overall = data.summary.overall;
const byMethod = data.summary.byMethod || [];
let html = `
<div class="metric">
<span class="metric-label">Total Assets</span>
<span class="metric-value">${overall.total_assets || 0}</span>
</div>
<div class="metric">
<span class="metric-label">Total Monthly Depreciation</span>
<span class="metric-value">$${parseFloat(overall.total_monthly_depreciation || 0).toLocaleString()}</span>
</div>
<div class="metric">
<span class="metric-label">Average Monthly Depreciation</span>
<span class="metric-value">$${parseFloat(overall.avg_monthly_depreciation || 0).toLocaleString()}</span>
</div>
`;
if (byMethod.length > 0) {
html += '<h3 style="margin-top: 1rem;">By Method</h3>';
byMethod.forEach(method => {
html += `
<div class="metric">
<span class="metric-label">${method.depreciation_method}</span>
<span class="metric-value">${method.method_count} assets ($${parseFloat(method.total_monthly_depreciation || 0).toLocaleString()})</span>
</div>
`;
});
}
container.innerHTML = html;
}
// Run depreciation job manually
async function runDepreciationJob() {
try {
const response = await fetch(API_BASE + '/run/depreciation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
showSuccess('Depreciation job started successfully');
// Refresh dashboard after a short delay
setTimeout(refreshDashboard, 2000);
} catch (error) {
showError('Failed to start depreciation job: ' + error.message);
}
}
// Show error message
function showError(message) {
const container = document.getElementById('errorContainer');
container.innerHTML = `<div class="error">${message}</div>`;
// Auto-hide after 5 seconds
setTimeout(clearError, 5000);
}
// Show success message
function showSuccess(message) {
const container = document.getElementById('errorContainer');
container.innerHTML = `<div class="success" style="background: #d4edda; color: #155724; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">${message}</div>`;
// Auto-hide after 3 seconds
setTimeout(clearError, 3000);
}
// Clear error message
function clearError() {
document.getElementById('errorContainer').innerHTML = '';
}
</script>
</body>
</html>