initial commit

This commit is contained in:
2026-03-27 14:46:28 +07:00
commit 50f7445c1c
7 changed files with 1469 additions and 0 deletions

BIN
src/audio/Down3.wav Normal file

Binary file not shown.

BIN
src/audio/Up3.wav Normal file

Binary file not shown.

187
src/functions.php Normal file
View File

@@ -0,0 +1,187 @@
<?php
// Function to fetch and parse metrics
function fetchMetrics($metricsUrl, $context) {
// Create HTTP context with Authorization header
$options = [
'http' => [
'header' => "Authorization: Basic " . base64_encode("user:$apiToken") . "\r\n",
'method' => 'GET',
'timeout' => 5
]
];
// Fetch metrics
$metricsData = @file_get_contents($metricsUrl, false, $context);
if ($metricsData === false) {
return [
'error' => true,
'message' => "Error: Unable to fetch metrics from Thamrin Monitoring."
];
}
// Initialize counters and lists
$up = 0;
$down = 0;
$pending = 0;
$maintenance = 0;
$downMonitors = [];
$mtMonitors = [];
$expiringCerts = [];
$total = 0;
// Parse Prometheus metrics
// Monitor Status
foreach (explode("\n", $metricsData) as $line) {
if (strpos($line, 'monitor_status{') === 0) {
if (preg_match('/monitor_name="([^"]+)".*?}\s([0-3])$/', trim($line), $matches)) {
$monitorName = $matches[1];
// Only count if monitor_name contains a dash
if (strpos($monitorName, '-') === false) continue;
$status = (int)$matches[2];
$total++;
switch ($status) {
case 0:
$down++;
$downMonitors[] = $monitorName;
break;
case 1: $up++; break;
case 2: $pending++; break;
case 3:
$maintenance++;
$mtMonitors[] = $monitorName;
break;
}
}
}
// Process monitor_cert_days_remaining inside the loop
if (strpos($line, 'monitor_cert_days_remaining{') === 0) {
if (preg_match('/monitor_name="([^"]+)".*?}\s(\d+)$/', trim($line), $matches)) {
$monitorName = $matches[1];
$daysRemaining = (int)$matches[2];
if ($daysRemaining < 30) { // strictly below 30
$expiringCerts[] = ['name' => $monitorName, 'days' => $daysRemaining];
}
}
}
}
// Sort expiring certificates by days ascending
usort($expiringCerts, function($a, $b) {
return $a['days'] <=> $b['days'];
});
// Get last refresh time
$lastRefresh = date('Y-m-d H:i:s');
return [
'error' => false,
'up' => $up,
'down' => $down,
'pending' => $pending,
'maintenance' => $maintenance,
'downMonitors' => $downMonitors,
'mtMonitors' => $mtMonitors,
'expiringCerts' => $expiringCerts,
'lastRefresh' => $lastRefresh
];
}
// Indonesian Holidays Function
function getIndonesianHoliday($date = null) {
if ($date === null) $date = date('Y-m-d');
$url = "https://libur.deno.dev/api";
// Call API
$response = @file_get_contents($url);
if ($response === false) return null;
$data = json_decode($response, true);
if (!is_array($data)) return null;
// Search for matching date
foreach ($data as $holiday) {
if (isset($holiday['date']) && $holiday['date'] === $date) {
return $holiday['name'];
}
}
return null;
}
// Function to fetch Grafana alerts
function fetchGrafanaAlerts($grafanaUrl, $grafanaApiKey = null) {
$url = rtrim($grafanaUrl, '/') . '/api/alertmanager/grafana/api/v2/alerts';
$headers = [
'Accept: application/json',
];
if ($grafanaApiKey) {
$headers[] = 'Authorization: Bearer ' . $grafanaApiKey;
}
$opts = [
'http' => [
'method' => 'GET',
'header' => implode("\r\n", $headers) . "\r\n",
'timeout' => 5
]
];
$context = stream_context_create($opts);
$result = @file_get_contents($url, false, $context);
if ($result === false) {
return [ 'error' => true, 'alerts' => [], 'message' => 'Unable to fetch alerts from Grafana.' ];
}
$alerts = json_decode($result, true);
if (!is_array($alerts)) {
return [ 'error' => true, 'alerts' => [], 'message' => 'Invalid response from Grafana.' ];
}
// Only return firing alerts
$activeAlerts = array_filter($alerts, function($alert) {
return isset($alert['status']['state']) && $alert['status']['state'] === 'active';
});
// Format for frontend — only show summary/description
$summary = array_map(function($alert) {
return trim(
$alert['annotations']['summary']
?? $alert['annotations']['description']
?? 'No description'
);
}, $activeAlerts);
return [ 'error' => false, 'alerts' => array_values($summary) ];
}
// Function to fetch Pending Tasks
function fetchTasks($url, $useMock = false, $mockFile = '') {
// Check if we are mocking the API
if ($useMock && file_exists($mockFile)) {
$result = @file_get_contents($mockFile);
if ($result !== false) {
$tasks = json_decode($result, true);
if (is_array($tasks)) {
return ['error' => false, 'tasks' => $tasks];
}
}
return ['error' => true, 'tasks' => [], 'message' => 'Failed to read valid JSON from mock file.'];
}
// If you haven't set the URL properly, return empty gracefully.
if (strpos($url, '#TODO') !== false) {
return ['error' => true, 'tasks' => [], 'message' => 'Cannot connect to task API URL.'];
}
$opts = [
'http' => [
'method' => 'GET',
'timeout' => 5
]
];
$context = stream_context_create($opts);
$result = @file_get_contents($url, false, $context);
if ($result === false) {
return ['error' => true, 'tasks' => [], 'message' => 'Unable to fetch tasks from server.'];
}
$tasks = json_decode($result, true);
if (!is_array($tasks)) {
return ['error' => true, 'tasks' => [], 'message' => 'Invalid tasks response format.'];
}
return ['error' => false, 'tasks' => $tasks];
}

450
src/main.js Normal file
View File

@@ -0,0 +1,450 @@
// 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 => `<li>${name}</li>`).join('');
textHtml = `<div class="popup-list-container"><ul class="popup-list">${listItems}</ul></div>`;
} else {
let singleText = Array.isArray(names) ? names[0] : names;
textHtml = `<span style="font-size:1.2em; display:block; margin:10px 0;">${singleText}</span>`;
}
popup.innerHTML = `${icon} <br>${textHtml}<span style="font-size:1em; display:block; margin-top:10px;">is ${label}</span>`;
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 = '<div class="loading"></div>';
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, `<li style="color:#dc3545;">${alertData.message || 'Error fetching alerts from Grafana.'}</li>`);
document.getElementById('alert-count').textContent = '0';
} else if (!alertData.alerts.length) {
renderListWithPagination('alert-list', [], null, `<li style="color:#aaa;">No active alerts.</li>`);
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;
}, '<li style="color:#aaa;">No active alerts.</li>');
}
})
.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, `<li style="color:#dc3545;">Error fetching alerts from Grafana (network or server error).</li>`);
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, `<div style="color:#aaa; padding: 20px;">No tasks available.</div>`, 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, `<div style="color:#aaa; padding: 20px;">No matching tasks found.</div>`, 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 = `<span style="color:#dc3545; font-style:italic;"> (${Math.abs(diffDays)} days ago)</span>`;
} else if (diffDays === 0) {
daysRemainingText = `<span style="color:#ffc107; font-style:italic;"> (Due Today)</span>`;
} else {
daysRemainingText = `<span style="color:#17A2B8; font-style:italic;"> (${diffDays} days left)</span>`;
}
}
item.className = `task-item task-item-${statusClass}`;
item.innerHTML = `
<div class="task-header">
<h4>${desc}</h4>
<span class="task-status badge-${statusClass}">${currentStatus}</span>
</div>
<div class="task-body">
<div class="task-detail"><strong>From:</strong> ${fromUser} &nbsp;|&nbsp; <strong>To:</strong> ${toUser}</div>
<div class="task-detail"><strong>Created:</strong> ${createdDate} &nbsp;|&nbsp; <strong>Due:</strong> ${dueDate}${daysRemainingText}</div>
</div>
`;
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, `<div style="color:#aaa; padding: 20px;">${data.message || 'Error fetching tasks.'}</div>`, 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, `<div style="color:#aaa; padding: 20px;">Error fetching tasks. API not accessible or JSON error.</div>`, 5);
});
}

34
src/mock_tasks.json Normal file
View File

@@ -0,0 +1,34 @@
[
{
"task_created_date": "2026-03-17 08:00",
"from": "Putut",
"to": "Ops-Server",
"task_description": "Install OS dan Aplikasi untuk PC Admin",
"due_date": "2026-03-25 20:00",
"current_status": "Pending"
},
{
"task_created_date": "2026-03-16 15:30",
"from": "Farizal",
"to": "SE-Server",
"task_description": "Pembelian Domain pokegocoin.com",
"due_date": "2026-03-11 12:00",
"current_status": "Pending"
},
{
"task_created_date": "2026-03-16 10:15",
"from": "Finance",
"to": "Irfan",
"task_description": "Pembuatan Server domain accfi.lyto.net untuk tool system approval",
"due_date": "2026-03-26 12:00",
"current_status": "Pending"
},
{
"task_created_date": "2026-03-15 10:15",
"from": "Test 2",
"to": "Irfan 2",
"task_description": "Update OS Web Server gnjoy.asia dan upgrade PHP",
"due_date": "2026-03-29 12:00",
"current_status": "Pending"
}
]

594
src/style.css Normal file
View File

@@ -0,0 +1,594 @@
/* style.css - extracted from index.php */
#time-container {
position: absolute;
top: 10px;
left: 10px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.date-holiday-row {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
}
#digital-clock {
color: #fff;
font-size: 2.5rem;
font-family: 'Segoe UI', monospace;
letter-spacing: 2px;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.8);
}
#digital-date {
color: #fff;
font-size: 1.3rem;
font-family: 'Segoe UI', monospace;
letter-spacing: 1px;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.8);
}
#holiday-info {
color: #ff4d4d;
font-size: 1rem;
font-weight: bold;
font-family: 'Segoe UI', sans-serif;
letter-spacing: 1px;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.8);
}
.desktop-title {
display: block;
}
.mobile-title {
display: none;
}
body {
font-family: 'Segoe UI', sans-serif;
background: #121212;
color: #f1f1f1;
margin: 0;
padding: 20px;
position: relative;
}
h1 {
text-align: center;
color: #ffffff;
margin-bottom: 5px;
}
.refresh-info {
text-align: center;
color: #888;
font-size: 12px;
margin-bottom: 15px;
}
.refresh-button {
position: absolute;
top: 20px;
right: 20px;
background: #2c3e50;
color: white;
border: none;
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s, transform 0.2s;
}
.refresh-button:hover {
background: #34495e;
}
.refresh-button:active {
transform: scale(0.95);
}
.dashboard {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
margin-top: 20px;
}
.box {
box-sizing: border-box;
background: #1e1e1e;
padding: 20px;
border-radius: 12px;
flex: 1 1 160px;
max-width: 260px;
height: 160px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
transition: 0.3s ease;
}
.box:hover {
transform: scale(1.05);
}
.box h2 {
font-size: clamp(16px, 2vw + 6px, 24px);
margin-bottom: 5px;
color: #ccc;
white-space: nowrap;
}
.box span {
font-size: clamp(36px, 5vw, 68px);
font-weight: bold;
}
.up {
border-top: 4px solid #28a745;
color: #28a745;
}
.down {
border-top: 4px solid #dc3545;
color: #dc3545;
}
.alert-box {
border-top: 4px solid #fd7e14;
color: #fd7e14;
}
.task-box {
border-top: 4px solid #ffc107;
color: #ffc107;
}
.maintenance {
border-top: 4px solid #17a2b8;
color: #17a2b8;
}
.all-good {
text-align: center;
margin-top: 30px;
color: #28a745;
font-size: 18px;
font-weight: bold;
background: #1e1e1e;
border: 2px solid #28a745;
border-radius: 8px;
padding: 10px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.main-content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
max-width: 1600px;
margin: 50px auto 20px;
align-items: start;
}
.unified-lists-container {
background: #1e1e1e;
border-radius: 10px;
padding: 30px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
max-height: 80vh;
overflow-y: auto;
}
.unified-lists-container h3 {
margin-top: 10px;
margin-bottom: 5px;
border-bottom: 1px solid #333;
padding-bottom: 5px;
font-size: 1.1em;
}
.unified-lists-container h3:first-child {
margin-top: 0;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
border-bottom: 1px solid #333;
padding-bottom: 15px;
}
.filter-btn {
background: #2c3e50;
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
transition: background 0.3s;
}
.filter-btn:hover {
background: #34495e;
}
.filter-btn.active {
background: #4CAF50;
}
.hidden-by-filter {
display: none !important;
}
.unified-lists-container ul {
list-style: none;
padding-left: 0;
margin-top: 0;
margin-bottom: 10px;
}
.unified-lists-container li {
border-bottom: 1px solid #333;
padding: 6px 0;
font-size: 0.95em;
}
.unified-lists-container .alert-message {
color: #aaa;
margin-bottom: 10px;
}
/* Task Container Styles */
.task-container {
background: #1e1e1e;
border-radius: 10px;
padding: 30px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
max-height: 80vh;
overflow-y: auto;
}
.task-container h3 {
margin-top: 0;
margin-bottom: 8px;
border-bottom: 1px solid #333;
padding-bottom: 5px;
color: #4CAF50;
font-size: 1.1em;
}
.task-item {
background: #2a2a2a;
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 10px;
border-left: 4px solid #555;
/* Default border */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.task-item-pending {
border-left-color: #ffc107;
}
.task-item-in-progress {
border-left-color: #17A2B8;
}
.task-item-done,
.task-item-completed {
border-left-color: #28a745;
}
.task-item-overdue {
border-left-color: #dc3545;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 5px;
}
.task-item h4 {
margin: 0;
color: #fff;
font-size: 1em;
flex: 1;
padding-right: 10px;
}
.task-status {
font-size: 0.75em;
padding: 2px 6px;
border-radius: 4px;
background: #333;
white-space: nowrap;
border: 1px solid #555;
font-weight: bold;
}
.badge-pending {
color: #ffc107;
border-color: #ffc107;
}
.badge-in-progress {
color: #17A2B8;
border-color: #17A2B8;
}
.badge-done,
.badge-completed {
color: #28a745;
border-color: #28a745;
}
.badge-overdue {
color: #dc3545;
border-color: #dc3545;
}
.task-body {
display: flex;
flex-direction: column;
gap: 3px;
}
.task-detail {
font-size: 0.85em;
color: #bbb;
margin-bottom: 0;
}
.task-detail strong {
color: #ddd;
margin-right: 2px;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, .3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 1024px) {
.main-content-grid {
grid-template-columns: 1fr;
}
}
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
margin-top: 15px;
gap: 15px;
}
.pagination-controls button {
background: #34495e;
color: #fff;
border: none;
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background 0.2s;
}
.pagination-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-controls button:hover:not(:disabled) {
background: #4CAF50;
}
.pagination-controls .pag-info {
color: #ccc;
font-size: 0.9em;
font-family: 'Segoe UI', sans-serif;
}
@media (max-width: 768px) {
.desktop-title {
display: none;
}
.mobile-title {
display: block;
}
#digital-clock,
#digital-date,
#holiday-info {
display: none !important;
}
}
@media (max-width: 600px) {
.box {
flex: 1 1 100%;
max-width: 100%;
height: auto;
padding: 10px;
}
.total-box span {
font-size: 32px;
}
.box span {
font-size: 24px;
}
.refresh-button {
top: 10px;
right: 10px;
width: 30px;
height: 30px;
font-size: 14px;
}
}
#status-popup {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.7);
background: rgba(30, 30, 30, 0.97);
color: #fff;
padding: 40px 60px;
border-radius: 20px;
box-shadow: 0 0 40px 10px #000a;
z-index: 9999;
font-size: 2rem;
font-weight: bold;
text-align: center;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
pointer-events: auto;
/* Re-enabled so user can scroll */
width: 90vw;
max-width: 700px;
}
.popup-list-container {
max-height: 35vh;
overflow-y: auto;
margin: 15px 0;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.popup-list {
font-size: 0.55em;
/* Drops text slightly relative to 2rem parent */
text-align: left;
margin: 0;
padding-left: 20px;
list-style-type: square;
font-weight: normal;
}
.popup-list li {
margin-bottom: 8px;
word-break: break-word;
/* Prevent text overflow */
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
/* Soft divider */
padding-bottom: 6px;
line-height: 1.4;
}
.popup-list li:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
#status-popup.show {
display: block;
opacity: 1;
transform: translate(-50%, -50%) scale(1.1);
animation: popup-zoom 0.3s cubic-bezier(.68, -0.55, .27, 1.55) forwards, popup-hide 0.3s 4.7s forwards;
}
@keyframes popup-zoom {
0% {
transform: translate(-50%, -50%) scale(0.7);
opacity: 0;
}
80% {
transform: translate(-50%, -50%) scale(1.15);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(1.1);
opacity: 1;
}
}
@keyframes popup-hide {
to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.7);
}
}
#status-popup.down {
border: 4px solid #dc3545;
color: #dc3545;
background: #1e1e1e;
}
#status-popup.up {
border: 4px solid #28a745;
color: #28a745;
background: #1e1e1e;
}
/* Custom Thin Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #1e1e1e;
}
::-webkit-scrollbar-thumb {
background: #444;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* Firefox compatibility */
* {
scrollbar-width: thin;
scrollbar-color: #444 #1e1e1e;
}