diff --git a/api/icon.php b/api/icon.php new file mode 100644 index 0000000..79db115 --- /dev/null +++ b/api/icon.php @@ -0,0 +1,18 @@ + 'URL is required', 'status' => 0, 'time' => 0]); + exit; +} + +// Validate URL format +if (!filter_var($url, FILTER_VALIDATE_URL)) { + echo json_encode(['error' => 'Invalid URL format', 'status' => 0, 'time' => 0]); + exit; +} + +$start = microtime(true); +$ch = curl_init($url); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_TIMEOUT, 10); +curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); +curl_setopt($ch, CURLOPT_USERAGENT, 'NativeUptimeMonitor/2.0'); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + +$response = curl_exec($ch); +$info = curl_getinfo($ch); +$error = curl_error($ch); +curl_close($ch); + +$end = microtime(true); +$timeMs = round(($end - $start) * 1000); + +// Determine final status code +$statusCode = $info['http_code']; + +if ($error) { + echo json_encode([ + 'url' => $url, + 'status' => 0, // 0 for network/timeout error + 'time' => $timeMs, + 'error_detail' => $error + ]); +} else { + echo json_encode([ + 'url' => $url, + 'status' => $statusCode, + 'time' => $timeMs + ]); +} +?> \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 50e0502..a0529fe 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,302 +1,274 @@ +:root { + --bg-color: #1a2639; + --neon-white: #ffffff; + --neon-white-glow: 0 0 5px #ffffff, 0 0 10px #ffffff; + --neon-pink: #ff33cc; + --neon-pink-glow: 0 0 5px #ff33cc, 0 0 15px #ff33cc; + --bar-bg: rgba(255, 255, 255, 0.1); +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + background-color: var(--bg-color); + color: var(--neon-white); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + overflow-x: hidden; + text-shadow: var(--neon-white-glow); } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.container { + padding: 10px; + max-width: 1200px; + margin: 0 auto; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 0; + border-bottom: 1px solid rgba(255,255,255,0.2); + margin-bottom: 20px; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +h1 { + margin: 0; + font-size: 1.5rem; + letter-spacing: 2px; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.settings-btn { + background: none; + border: 1px solid var(--neon-white); + color: var(--neon-white); + padding: 5px 15px; + cursor: pointer; + border-radius: 5px; + box-shadow: var(--neon-white-glow); + text-shadow: var(--neon-white-glow); + transition: all 0.3s; +} +.settings-btn:active { + transform: scale(0.95); } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.settings-panel { + display: none; + background: rgba(0,0,0,0.3); + padding: 20px; + border-radius: 10px; + margin-bottom: 20px; + border: 1px solid rgba(255,255,255,0.1); } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.settings-panel.active { + display: block; } -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.admin-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.admin-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - .form-group { - margin-bottom: 1.25rem; + margin-bottom: 15px; } .form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; + display: block; + margin-bottom: 5px; + font-size: 0.9rem; } -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; +.form-group input, .form-group textarea { + width: 100%; + padding: 10px; + box-sizing: border-box; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.2); + color: var(--neon-white); + border-radius: 5px; + outline: none; +} +.form-group input:focus, .form-group textarea:focus { + border-color: var(--neon-white); + box-shadow: var(--neon-white-glow); } -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); +.btn-save { + background: var(--neon-white); + color: var(--bg-color); + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + box-shadow: var(--neon-white-glow); + width: 100%; +} + +.monitor-card { + background: rgba(0,0,0,0.2); + border-radius: 10px; + padding: 15px; + margin-bottom: 15px; + border: 1px solid rgba(255,255,255,0.1); + display: flex; + flex-direction: column; +} + +.monitor-card.error { + border-color: var(--neon-pink); + box-shadow: var(--neon-pink-glow); + color: var(--neon-pink); + text-shadow: var(--neon-pink-glow); +} + +.monitor-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.url-title { + font-size: 1.1rem; + font-weight: bold; + word-break: break-all; +} + +.status-badge { + padding: 3px 8px; + border-radius: 3px; + font-size: 0.8rem; + border: 1px solid var(--neon-white); + text-transform: uppercase; +} +.monitor-card.error .status-badge { + border-color: var(--neon-pink); + color: var(--neon-pink); +} + +.stats { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + margin-bottom: 15px; +} + +/* Battery Health Bar - Refined */ +.battery-wrapper { + margin-bottom: 15px; +} +.battery-container { + width: 100%; + height: 26px; + border: 2px solid var(--neon-white); + border-radius: 4px; + padding: 2px; + box-sizing: border-box; + position: relative; + background: rgba(0,0,0,0.2); +} +.battery-container::after { + content: ''; + position: absolute; + right: -7px; + top: 5px; + width: 5px; + height: 12px; + background: var(--neon-white); + border-radius: 0 2px 2px 0; + box-shadow: var(--neon-white-glow); +} +.monitor-card.error .battery-container { border-color: var(--neon-pink); } +.monitor-card.error .battery-container::after { + background: var(--neon-pink); + box-shadow: var(--neon-pink-glow); +} + +.battery-level { + height: 100%; + background: var(--neon-white); + width: 100%; + transition: width 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + box-shadow: var(--neon-white-glow); + border-radius: 1px; +} +.monitor-card.error .battery-level { + background: var(--neon-pink); + box-shadow: var(--neon-pink-glow); +} + +.battery-text { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: bold; + color: var(--bg-color); + text-shadow: none; + z-index: 2; + mix-blend-mode: screen; /* Inverts color over the filled bar vs empty background */ +} +/* Fallback for mix-blend-mode if unsupported/hard to read */ +@supports not (mix-blend-mode: screen) { + .battery-text { + color: #fff; + mix-blend-mode: normal; + } +} +.monitor-card.error .battery-text { + mix-blend-mode: normal; + color: #fff; +} + +/* Candlestick Chart (simulated) */ +.chart-container { + height: 80px; + display: flex; + align-items: flex-end; + gap: 2px; + overflow: hidden; + border-bottom: 2px solid rgba(255,255,255,0.4); + padding-bottom: 2px; + border-radius: 0 0 4px 4px; + background: rgba(0,0,0,0.1); + padding: 5px; +} +.monitor-card.error .chart-container { + border-bottom-color: rgba(255,51,204,0.6); +} + +.chart-bar-container { + flex: 1; + height: 100%; + display: flex; + align-items: flex-end; + justify-content: center; +} +.chart-bar-container.empty { + /* empty placeholder */ +} + +.chart-bar { + width: 80%; + background: var(--neon-white); + min-height: 2px; /* Minimum visual height even for 0ms */ + transition: height 0.2s ease-out; + box-shadow: var(--neon-white-glow); + border-radius: 1px 1px 0 0; +} +.chart-bar.error { + background: var(--neon-pink); + box-shadow: var(--neon-pink-glow); +} + +@media (min-width: 768px) { + .dashboard { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; + } } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..aa7a67f 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,316 @@ -document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); +// 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)); + }); +} - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; +// State +let urls = []; +let botToken = ''; +let chatId = ''; +let monitorTimers = {}; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; +// URL Data Store +const urlData = {}; - appendMessage(message, 'visitor'); - chatInput.value = ''; +// 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'); - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); +// 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(); diff --git a/index.php b/index.php index 7205f3d..80462ff 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,48 @@ - - - + + - - - New Style - - - - - - - - - - - - - - - - - - - + + + Uptime Monitor + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+
+

⚡ UPTIME

+ +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ +

+ *Note: Untuk berjalan penuh di background saat layar mati (tanpa dibatasi browser), buka Chrome menu -> Add to Home screen (Install PWA). Kami menggunakan WakeLock API agar layar tidak mati jika aplikasi dibiarkan terbuka. +

+
+ +
+ +
-
- + + - + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f24b489 --- /dev/null +++ b/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Uptime Monitor", + "short_name": "UptimeMon", + "description": "Realtime Uptime Monitoring with Telegram Alerts", + "start_url": "/?source=pwa", + "display": "standalone", + "background_color": "#1a2639", + "theme_color": "#1a2639", + "icons": [ + { + "src": "/api/icon.php?size=192", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/api/icon.php?size=512", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..69a8447 --- /dev/null +++ b/sw.js @@ -0,0 +1,45 @@ +const CACHE_NAME = 'uptime-monitor-v2'; +const urlsToCache = [ + '/', + '/index.php', + '/assets/css/custom.css', + '/assets/js/main.js', + '/manifest.json' +]; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => cache.addAll(urlsToCache)) + ); + self.skipWaiting(); +}); + +self.addEventListener('fetch', event => { + // Bypassing API and external calls from cache + if (event.request.url.includes('api/ping.php') || event.request.url.includes('api.telegram.org')) { + return; + } + event.respondWith( + caches.match(event.request) + .then(response => { + if (response) return response; + return fetch(event.request); + }) + ); +}); + +self.addEventListener('activate', event => { + const cacheWhitelist = [CACHE_NAME]; + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (cacheWhitelist.indexOf(cacheName) === -1) { + return caches.delete(cacheName); + } + }) + ); + }).then(() => self.clients.claim()) + ); +});