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 = `
+
+
+
+ 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…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+
+
+ ⚡ UPTIME
+ ⚙️ Config
+
+
+
+
+ URLs to Monitor (one per line):
+
+
+
+ Telegram Bot Token:
+
+
+
+
SAVE & START
+
+ *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.
+
+
+
+
+
+
-
-
- Page updated: = htmlspecialchars($now) ?> (UTC)
-
+
+
-
+
\ 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())
+ );
+});