// 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("🤖 TEST UPTIME MONITOR\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 = `
${url}
WAIT
100% HEALTH
Uptime: 100% Ping: - ms
`; 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(`🚨 DOWN ALERT\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(`✅ RECOVERY ALERT\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();