// main.js - extracted from index.php window.paginationState = {}; function renderListWithPagination(listId, data, renderItemFn, emptyMsgHtml = '', itemsPerPage = 5) { const listEl = document.getElementById(listId); if (!listEl) return; if (!data || data.length === 0) { listEl.innerHTML = emptyMsgHtml; const pageContainer = document.getElementById(listId + '-pagination'); if (pageContainer) pageContainer.style.display = 'none'; return; } listEl.innerHTML = ''; const totalPages = Math.ceil(data.length / itemsPerPage); let currentPage = window.paginationState[listId] || 1; if (currentPage > totalPages) currentPage = totalPages; window.paginationState[listId] = currentPage; const startIdx = (currentPage - 1) * itemsPerPage; const currentData = data.slice(startIdx, startIdx + itemsPerPage); currentData.forEach(item => { const itemEl = renderItemFn(item); if (itemEl) listEl.appendChild(itemEl); }); let pageContainer = document.getElementById(listId + '-pagination'); if (!pageContainer) { pageContainer = document.createElement('div'); pageContainer.id = listId + '-pagination'; pageContainer.className = 'pagination-controls'; listEl.parentNode.insertBefore(pageContainer, listEl.nextSibling); } if (totalPages > 1) { pageContainer.innerHTML = ''; const prevBtn = document.createElement('button'); prevBtn.textContent = 'ยซ Prev'; prevBtn.disabled = currentPage === 1; prevBtn.onclick = () => { window.paginationState[listId]--; renderListWithPagination(listId, data, renderItemFn, emptyMsgHtml, itemsPerPage); }; const info = document.createElement('span'); info.className = 'pag-info'; info.textContent = ` Page ${currentPage} of ${totalPages} `; const nextBtn = document.createElement('button'); nextBtn.textContent = 'Next ยป'; nextBtn.disabled = currentPage === totalPages; nextBtn.onclick = () => { window.paginationState[listId]++; renderListWithPagination(listId, data, renderItemFn, emptyMsgHtml, itemsPerPage); }; pageContainer.appendChild(prevBtn); pageContainer.appendChild(info); pageContainer.appendChild(nextBtn); pageContainer.style.display = 'flex'; } else { pageContainer.style.display = 'none'; } } function updateClock() { const now = new Date(); const pad = n => n.toString().padStart(2, '0'); const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())} `; const clock = document.getElementById('digital-clock'); if (clock) clock.textContent = timeStr; const dateStr = now.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); const date = document.getElementById('digital-date'); if (date) date.textContent = dateStr; } function updateHolidayInfo(dateObj) { const y = dateObj.getFullYear(); const m = String(dateObj.getMonth() + 1).padStart(2, '0'); const d = String(dateObj.getDate()).padStart(2, '0'); const ymd = `${y}-${m}-${d}`; fetch(`?fetch=holiday&date=${ymd}`) .then(resp => resp.json()) .then(data => { const info = document.getElementById('holiday-info'); if (data.holiday) { info.textContent = `(${data.holiday})`; info.style.display = 'block'; } else { info.style.display = 'none'; } }) .catch(() => { const info = document.getElementById('holiday-info'); if (info) info.style.display = 'none'; }); } setInterval(updateClock, 1000); updateClock(); let refreshCountdown; const refreshInterval = 10; function startCountdown() { let secondsLeft = refreshInterval; const refreshInfoElement = document.getElementById('refresh-info'); const originalText = refreshInfoElement.textContent; refreshCountdown = setInterval(() => { secondsLeft--; if (secondsLeft <= 0) { fetchMetrics(); fetchTasks(); secondsLeft = refreshInterval; } refreshInfoElement.textContent = `${originalText.split('(')[0]} `; }, 1000); } let previousDownMonitors = []; const offlineAudio = new Audio('src/audio/Down3.wav'); const onlineAudio = new Audio('src/audio/Up3.wav'); offlineAudio.volume = 1.0; onlineAudio.volume = 1.0; function playOfflineSound() { offlineAudio.currentTime = 0; offlineAudio.play(); } function playOnlineSound() { onlineAudio.currentTime = 0; onlineAudio.play(); } function arrayDiff(a, b) { return a.filter(x => !b.includes(x)); } function showStatusPopup(names, status) { const popup = document.getElementById('status-popup'); popup.className = ''; popup.classList.add('show'); popup.classList.add(status === 'down' ? 'down' : 'up'); let label = status === 'down' ? 'DOWN' : 'UP'; let icon = status === 'down' ? '๐Ÿ”ป' : 'โœ…'; let textHtml = ''; if (Array.isArray(names) && names.length > 1) { let listItems = names.map(name => `
  • ${name}
  • `).join(''); textHtml = ``; } else { let singleText = Array.isArray(names) ? names[0] : names; textHtml = `${singleText}`; } popup.innerHTML = `${icon}
    ${textHtml}is ${label}`; popup.style.display = 'block'; // Automatically increase the display time if there's a huge scrollable list! const duration = Array.isArray(names) && names.length > 1 ? 10000 : 5000; setTimeout(() => { popup.classList.remove('show'); setTimeout(() => { popup.style.display = 'none'; }, 300); }, duration); } function fetchMetrics() { const refreshButton = document.getElementById('refresh-button'); if (refreshButton.disabled) return; refreshButton.innerHTML = '
    '; refreshButton.disabled = true; fetch('?fetch=metrics') .then(response => response.json()) .then(data => { if (data.error) { console.error(data.message); return; } document.getElementById('up-count').textContent = data.up; document.getElementById('down-count').textContent = data.down; document.getElementById('maintenance-count').textContent = data.maintenance; const refreshInfoElement = document.getElementById('refresh-info'); refreshInfoElement.textContent = `Last updated: ${data.lastRefresh}`; const downMonitorsList = document.getElementById('down-monitors-list'); if (data.down === 0) { document.getElementById('down-list-container').style.display = 'none'; document.getElementById('all-good-container').style.display = 'block'; renderListWithPagination('down-monitors-list', [], null); } else { document.getElementById('down-list-container').style.display = 'block'; document.getElementById('all-good-container').style.display = 'none'; renderListWithPagination('down-monitors-list', data.downMonitors, name => { const li = document.createElement('li'); li.textContent = name; return li; }); } const mtMonitorsList = document.getElementById('mt-monitors-list'); if (data.mtMonitors.length === 0) { document.getElementById('mt-list-container').style.display = 'none'; renderListWithPagination('mt-monitors-list', [], null); } else { document.getElementById('mt-list-container').style.display = 'block'; renderListWithPagination('mt-monitors-list', data.mtMonitors, name => { const li = document.createElement('li'); li.textContent = name; return li; }); } const expiringCertList = document.getElementById('expiring-cert-list'); if (data.expiringCerts.length === 0) { document.getElementById('cert-warning-container').style.display = 'none'; renderListWithPagination('expiring-cert-list', [], null); } else { document.getElementById('cert-warning-container').style.display = 'block'; renderListWithPagination('expiring-cert-list', data.expiringCerts, cert => { const li = document.createElement('li'); li.textContent = `${cert.name} - ${cert.days} days remaining`; return li; }); } const currentDown = data.downMonitors || []; const newDown = arrayDiff(currentDown, previousDownMonitors); const backOnline = arrayDiff(previousDownMonitors, currentDown); if (newDown.length > 0) { playOfflineSound(); showStatusPopup(newDown, 'down'); } if (backOnline.length > 0) { playOnlineSound(); showStatusPopup(backOnline, 'up'); } previousDownMonitors = currentDown; document.querySelectorAll('.rrd-graph img').forEach(img => { let src = img.getAttribute('src').split('?')[0]; img.setAttribute('src', src + '?_t=' + Date.now()); }); clearInterval(refreshCountdown); startCountdown(); }) .catch(error => { console.error('Error fetching metrics:', error); }) .finally(() => { refreshButton.innerHTML = '๐Ÿ”„'; refreshButton.disabled = false; }); // --- Fetch and update Grafana alerts --- fetch('?fetch=alerts') .then(resp => resp.json()) .then(alertData => { const alertContainer = document.getElementById('alert-list-container'); alertContainer.style.display = 'block'; if (alertData.error) { renderListWithPagination('alert-list', [], null, `
  • ${alertData.message || 'Error fetching alerts from Grafana.'}
  • `); document.getElementById('alert-count').textContent = '0'; } else if (!alertData.alerts.length) { renderListWithPagination('alert-list', [], null, `
  • No active alerts.
  • `); document.getElementById('alert-count').textContent = '0'; } else { document.getElementById('alert-count').textContent = alertData.alerts.length; renderListWithPagination('alert-list', alertData.alerts, alert => { const li = document.createElement('li'); li.textContent = alert; return li; }, '
  • No active alerts.
  • '); } }) .catch(err => { console.error('Alert fetch error:', err); const alertContainer = document.getElementById('alert-list-container'); if (alertContainer) alertContainer.style.display = 'block'; renderListWithPagination('alert-list', [], null, `
  • Error fetching alerts from Grafana (network or server error).
  • `); const alertCount = document.getElementById('alert-count'); if (alertCount) alertCount.textContent = '0'; }); } let currentFilter = 'all'; let currentTaskFilter = 'all'; window.lastFetchedTasks = []; document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.unified-lists-container .filter-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.unified-lists-container .filter-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); currentFilter = e.target.getAttribute('data-filter'); applyFilters(); }); }); document.querySelectorAll('.task-filter-buttons .filter-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.task-filter-buttons .filter-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); currentTaskFilter = e.target.getAttribute('data-task-filter'); applyTaskFilters(); }); }); }); function applyFilters() { const containers = { 'offline': document.getElementById('down-list-container'), 'maintenance': document.getElementById('mt-list-container'), 'ssl': document.getElementById('cert-warning-container'), 'alerts': document.getElementById('alert-list-container') }; for (const [key, element] of Object.entries(containers)) { if (!element) continue; if (currentFilter === 'all' || currentFilter === key) { element.classList.remove('hidden-by-filter'); } else { element.classList.add('hidden-by-filter'); } } const allGood = document.getElementById('all-good-container'); if (allGood) { if (currentFilter === 'all' || currentFilter === 'offline') { allGood.classList.remove('hidden-by-filter'); } else { allGood.classList.add('hidden-by-filter'); } } } window.addEventListener('load', () => { fetchMetrics(); fetchTasks(); startCountdown(); updateHolidayInfo(new Date()); // Set up midnight check to update holiday info setInterval(() => { const now = new Date(); if (now.getHours() === 0 && now.getMinutes() === 0 && now.getSeconds() < 2) { updateHolidayInfo(now); } }, 1000); }); document.getElementById('refresh-button').addEventListener('click', () => { clearInterval(refreshCountdown); fetchMetrics(); fetchTasks(); }); function applyTaskFilters() { const data = window.lastFetchedTasks; const taskList = document.getElementById('task-list'); if (!taskList) return; if (!data || data.length === 0) { renderListWithPagination('task-list', [], null, `
    No tasks available.
    `, 5); document.getElementById('task-count').textContent = '0'; return; } // Filter out "Done" tasks completely, and exclusively count "Pending" tasks for the dashboard let activeTasks = data.filter(t => (t.current_status || t.status || '').toLowerCase() !== 'done'); let pendingCount = data.filter(t => (t.current_status || t.status || 'Pending').toLowerCase() === 'pending').length; document.getElementById('task-count').textContent = pendingCount; let filteredTasks = activeTasks; if (currentTaskFilter !== 'all') { filteredTasks = activeTasks.filter(task => { let statusClass = (task.current_status || task.status || 'Pending').toLowerCase().replace(/\s+/g, '-'); const dueDate = task.due_date || task.deadline || task.task_deadline || 'N/A'; if (dueDate !== 'N/A') { const due = new Date(dueDate); const now = new Date(); const diffTime = due - now; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays < 0 && statusClass !== 'done') { statusClass = 'overdue'; } } return statusClass === currentTaskFilter; }); } // Sort ascending by remaining days (closest/most overdue first), put N/A at bottom filteredTasks.sort((a, b) => { const getDueTime = (task) => { const dueDate = task.due_date || task.deadline || task.task_deadline || 'N/A'; if (dueDate === 'N/A' || !dueDate) return Infinity; // Push N/A to bottom const time = new Date(dueDate).getTime(); return isNaN(time) ? Infinity : time; }; return getDueTime(a) - getDueTime(b); }); if (!filteredTasks || filteredTasks.length === 0) { renderListWithPagination('task-list', [], null, `
    No matching tasks found.
    `, 5); return; } renderListWithPagination('task-list', filteredTasks, task => { const item = document.createElement('div'); let currentStatus = task.current_status || task.status || 'Pending'; let statusClass = currentStatus.toLowerCase().replace(/\s+/g, '-'); /* Adjust field names according to your API response JSON */ const desc = task.task_description || task.description || task.desc || 'Unknown Task'; const fromUser = task.from || task.assigner || task.assigned_by || task.who_assign || 'N/A'; const toUser = task.to || task.receiver || task.assigned_to || task.who_receive || 'N/A'; const createdDate = task.task_created_date || task.date_assigned || task.assigned_date || 'N/A'; const dueDate = task.due_date || task.deadline || task.task_deadline || 'N/A'; let daysRemainingText = ''; if (dueDate !== 'N/A') { const due = new Date(dueDate); const now = new Date(); const diffTime = due - now; const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays < 0) { statusClass = 'overdue'; currentStatus = 'Overdue'; daysRemainingText = ` (${Math.abs(diffDays)} days ago)`; } else if (diffDays === 0) { daysRemainingText = ` (Due Today)`; } else { daysRemainingText = ` (${diffDays} days left)`; } } item.className = `task-item task-item-${statusClass}`; item.innerHTML = `

    ${desc}

    ${currentStatus}
    From: ${fromUser}  |  To: ${toUser}
    Created: ${createdDate}  |  Due: ${dueDate}${daysRemainingText}
    `; return item; }, '', 5); } function fetchTasks() { const taskList = document.getElementById('task-list'); if (!taskList) return; // Prevent errors if DOM isn't ready fetch('?fetch=tasks') .then(resp => resp.json()) .then(data => { if (data.error) { renderListWithPagination('task-list', [], null, `
    ${data.message || 'Error fetching tasks.'}
    `, 5); document.getElementById('task-count').textContent = '0'; return; } window.lastFetchedTasks = data.tasks || []; applyTaskFilters(); }) .catch(err => { document.getElementById('task-count').textContent = '0'; renderListWithPagination('task-list', [], null, `
    Error fetching tasks. API not accessible or JSON error.
    `, 5); }); }