317 lines
9.4 KiB
JavaScript
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();
|