2026-02-25 23:13:00 +00:00

317 lines
9.4 KiB
JavaScript

// Register Service Worker for PWA
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('SW registered!', reg);
}).catch(err => console.log('SW registration failed', err));
});
}
// State
let urls = [];
let botToken = '';
let chatId = '';
let monitorTimers = {};
// URL Data Store
const urlData = {};
// DOM Elements
const settingsPanel = document.getElementById('settings-panel');
const btnSettings = document.getElementById('btn-settings');
const btnSave = document.getElementById('btn-save');
const inputUrls = document.getElementById('config-urls');
const inputBotToken = document.getElementById('config-bot-token');
const inputChatId = document.getElementById('config-chat-id');
const btnTestTg = document.getElementById('btn-test-tg');
const dashboard = document.getElementById('dashboard');
// Init
function init() {
loadConfig();
// Request Notification Permission
if ("Notification" in window && Notification.permission !== "granted") {
Notification.requestPermission();
}
btnSettings.addEventListener('click', () => {
settingsPanel.classList.toggle('active');
});
btnSave.addEventListener('click', () => {
saveConfig();
settingsPanel.classList.remove('active');
startMonitoring();
});
if (btnTestTg) {
btnTestTg.addEventListener('click', async () => {
saveConfig();
if (!botToken || !chatId) {
alert("Isi Bot Token dan Chat ID dulu!");
return;
}
try {
const res = await sendTelegramMessage("🤖 <b>TEST UPTIME MONITOR</b>\nJika pesan ini masuk, notifikasi sudah berfungsi!");
if (res && res.ok) {
alert("Test notifikasi terkirim ke Telegram Anda!");
} else {
const data = await res.json();
alert("Gagal: " + (data.description || "Cek Token/ID"));
}
} catch (e) {
alert("Error: " + e.message);
}
});
}
if (urls.length > 0) {
startMonitoring();
} else {
settingsPanel.classList.add('active');
}
}
function loadConfig() {
const savedUrls = localStorage.getItem('uptime_urls');
botToken = localStorage.getItem('uptime_bot_token') || '';
chatId = localStorage.getItem('uptime_chat_id') || '';
if (savedUrls) {
urls = savedUrls.split('\n').filter(u => u.trim() !== '');
inputUrls.value = urls.join('\n');
}
inputBotToken.value = botToken;
inputChatId.value = chatId;
}
function saveConfig() {
urls = inputUrls.value.split('\n').filter(u => u.trim() !== '');
botToken = inputBotToken.value.trim();
chatId = inputChatId.value.trim();
localStorage.setItem('uptime_urls', urls.join('\n'));
localStorage.setItem('uptime_bot_token', botToken);
localStorage.setItem('uptime_chat_id', chatId);
}
function sendTelegramMessage(message) {
if (!botToken || !chatId) return Promise.resolve(null);
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML'
})
}).catch(e => {
console.error("Telegram API Error:", e);
throw e;
});
}
function showNativeNotification(title, body) {
if ("Notification" in window && Notification.permission === "granted") {
new Notification(title, { body, icon: '/api/icon.php?size=192' });
}
}
function buildDashboard() {
dashboard.innerHTML = '';
urls.forEach(url => {
if (!urlData[url]) {
urlData[url] = {
history: Array.from({length: 40}).map(() => ({ time: 0, status: 0, isError: false, empty: true })),
totalPings: 0,
successPings: 0,
isDown: false
};
}
const safeId = btoa(url).replace(/[^a-zA-Z0-9]/g, '');
const card = document.createElement('div');
card.className = 'monitor-card';
card.id = `card-${safeId}`;
card.innerHTML = `
<div class="monitor-header">
<div class="url-title">${url}</div>
<div class="status-badge" id="badge-${safeId}">WAIT</div>
</div>
<div class="battery-wrapper">
<div class="battery-container" id="battery-box-${safeId}">
<div class="battery-level" id="battery-${safeId}" style="width: 100%;"></div>
<div class="battery-text" id="battery-txt-${safeId}">100% HEALTH</div>
</div>
</div>
<div class="stats">
<span>Uptime: <strong id="uptime-${safeId}">100%</strong></span>
<span>Ping: <strong id="ping-${safeId}">- ms</strong></span>
</div>
<div class="chart-container" id="chart-${safeId}">
</div>
`;
dashboard.appendChild(card);
// Initial empty chart render
const chart = document.getElementById(`chart-${safeId}`);
urlData[url].history.forEach(() => {
const barContainer = document.createElement('div');
barContainer.className = 'chart-bar-container empty';
chart.appendChild(barContainer);
});
});
}
async function pingUrl(url, retry = 1) {
try {
const res = await fetch(`/api/ping.php?url=${encodeURIComponent(url)}`);
const data = await res.json();
if (data.status !== 200 && retry > 0) {
return await pingUrl(url, 0); // Retry once
}
return {
status: data.status || 0,
time: data.time || 0,
isError: data.status !== 200,
empty: false
};
} catch (e) {
if (retry > 0) return await pingUrl(url, 0);
return { status: 0, time: 0, isError: true, empty: false };
}
}
function updateUI(url, result) {
const safeId = btoa(url).replace(/[^a-zA-Z0-9]/g, '');
const data = urlData[url];
data.totalPings++;
if (!result.isError) data.successPings++;
// Update history array
data.history.shift();
data.history.push(result);
const uptimePercent = ((data.successPings / data.totalPings) * 100).toFixed(2);
const card = document.getElementById(`card-${safeId}`);
const badge = document.getElementById(`badge-${safeId}`);
const uptimeEl = document.getElementById(`uptime-${safeId}`);
const pingEl = document.getElementById(`ping-${safeId}`);
const battery = document.getElementById(`battery-${safeId}`);
const batteryTxt = document.getElementById(`battery-txt-${safeId}`);
const chart = document.getElementById(`chart-${safeId}`);
if (!card) return; // UI not ready
// Update logic for DOWN status
if (result.isError) {
if (!data.isDown) {
data.isDown = true;
card.classList.add('error');
badge.textContent = `ERR ${result.status}`;
sendTelegramMessage(`🚨 <b>DOWN ALERT</b>\nURL: ${url}\nStatus: ${result.status}\nTime: ${new Date().toLocaleTimeString()}`);
showNativeNotification('URL Down', `${url} returned status ${result.status}`);
} else {
badge.textContent = `ERR ${result.status}`;
}
} else {
if (data.isDown) {
data.isDown = false;
card.classList.remove('error');
badge.textContent = '200 OK';
sendTelegramMessage(`✅ <b>RECOVERY ALERT</b>\nURL: ${url}\nBack online.\nTime: ${new Date().toLocaleTimeString()}`);
showNativeNotification('URL Recovered', `${url} is back online.`);
} else {
badge.textContent = `${result.status} OK`;
}
}
uptimeEl.textContent = `${uptimePercent}%`;
pingEl.textContent = `${result.time} ms`;
battery.style.width = `${uptimePercent}%`;
batteryTxt.textContent = `${uptimePercent}% HEALTH`;
// Render chart
chart.innerHTML = '';
data.history.forEach(item => {
const barContainer = document.createElement('div');
barContainer.className = 'chart-bar-container' + (item.empty ? ' empty' : '');
if (!item.empty) {
const bar = document.createElement('div');
bar.className = 'chart-bar' + (item.isError ? ' error' : '');
let h = 0;
if (item.time > 0 || item.isError) {
h = Math.max(5, Math.min(100, (item.time / 1000) * 100)); // 1000ms = 100% height
}
if (item.isError && item.time === 0) h = 100; // Max height for error with 0 time (timeout)
bar.style.height = `${h}%`;
barContainer.appendChild(bar);
}
chart.appendChild(barContainer);
});
}
let activeWakeLock = null;
async function requestWakeLock() {
try {
if ('wakeLock' in navigator) {
activeWakeLock = await navigator.wakeLock.request('screen');
}
} catch (err) {
console.log('Wake Lock request failed:', err.name, err.message);
}
}
async function schedulePing(url) {
if (monitorTimers[url]) clearTimeout(monitorTimers[url]);
const startTime = Date.now();
const result = await pingUrl(url);
updateUI(url, result);
const elapsed = Date.now() - startTime;
const nextDelay = Math.max(0, 1000 - elapsed);
monitorTimers[url] = setTimeout(() => {
schedulePing(url);
}, nextDelay);
}
function startMonitoring() {
// Clear any existing timers
Object.values(monitorTimers).forEach(timer => clearTimeout(timer));
monitorTimers = {};
buildDashboard();
requestWakeLock();
if (Notification.permission === "granted") {
setTimeout(() => {
showNativeNotification('Uptime Monitoring Running', 'App is now monitoring in the background/foreground.');
}, 500);
}
// Start pinging each URL independently per second
urls.forEach(url => {
schedulePing(url);
});
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
requestWakeLock();
}
});
init();