// 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 = `
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);
});
}