351 lines
15 KiB
PHP
351 lines
15 KiB
PHP
<?php
|
|
require_once __DIR__ . '/db/config.php';
|
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Uptime Monitoring 24/7';
|
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
|
<meta name="theme-color" content="#0f172a">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<title>Uptime Monitor</title>
|
|
<link rel="manifest" href="manifest.json">
|
|
<script>
|
|
if ("serviceWorker" in navigator) {
|
|
navigator.serviceWorker.register("sw.js");
|
|
}
|
|
</script>
|
|
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
:root {
|
|
--bg-dark: #0f172a;
|
|
--pastel-blue: #1e293b;
|
|
--neon-white: #f8fafc;
|
|
--neon-pink: #f472b6;
|
|
--neon-white-glow: 0 0 10px rgba(248, 250, 252, 0.5);
|
|
--neon-pink-glow: 0 0 10px rgba(244, 114, 182, 0.5);
|
|
}
|
|
body {
|
|
background-color: var(--bg-dark);
|
|
color: var(--neon-white);
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow-x: hidden;
|
|
-webkit-tap-highlight-color: transparent;
|
|
padding-top: env(safe-area-inset-top);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
.bg-gradient {
|
|
background: radial-gradient(circle at top right, #1e293b, #0f172a);
|
|
min-height: 100vh; /* Fallback */
|
|
min-height: 100dvh; /* Mobile browsers */
|
|
}
|
|
.card {
|
|
background: rgba(30, 41, 59, 0.7);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(248, 250, 252, 0.1);
|
|
border-radius: 1.5rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
.card:hover {
|
|
border-color: rgba(248, 250, 252, 0.3);
|
|
transform: translateY(-2px);
|
|
}
|
|
.status-up {
|
|
color: var(--neon-white);
|
|
text-shadow: var(--neon-white-glow);
|
|
}
|
|
.status-down {
|
|
color: var(--neon-pink);
|
|
text-shadow: var(--neon-pink-glow);
|
|
}
|
|
.health-bar-container {
|
|
width: 50px;
|
|
height: 24px;
|
|
border: 2px solid rgba(248, 250, 252, 0.3);
|
|
border-radius: 4px;
|
|
position: relative;
|
|
padding: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
.health-bar-container::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: -6px;
|
|
top: 6px;
|
|
width: 4px;
|
|
height: 8px;
|
|
background: rgba(248, 250, 252, 0.3);
|
|
border-radius: 0 2px 2px 0;
|
|
}
|
|
.health-fill {
|
|
height: 100%;
|
|
background: var(--neon-white);
|
|
box-shadow: var(--neon-white-glow);
|
|
transition: width 0.5s ease;
|
|
}
|
|
.health-fill.down {
|
|
background: var(--neon-pink);
|
|
box-shadow: var(--neon-pink-glow);
|
|
}
|
|
.candlestick-chart {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 1px;
|
|
height: 50px;
|
|
width: 100%;
|
|
margin-top: 4px;
|
|
}
|
|
.candle {
|
|
flex: 1;
|
|
min-width: 1px;
|
|
border-radius: 1px;
|
|
transition: height 0.3s ease;
|
|
}
|
|
.candle.up {
|
|
background: rgba(248, 250, 252, 0.5);
|
|
}
|
|
.candle.down {
|
|
background: rgba(244, 114, 182, 0.8);
|
|
}
|
|
.pulse {
|
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: .5; }
|
|
}
|
|
/* Mobile fixes */
|
|
input[type="text"], input[type="url"] {
|
|
font-size: 16px; /* Prevents iOS/Android auto-zoom on focus */
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gradient">
|
|
|
|
<!-- Force Mobile App Layout (Max Width MD, always single column) -->
|
|
<div class="w-full max-w-md mx-auto px-4 py-6">
|
|
<header class="flex flex-col gap-5 mb-6">
|
|
<div class="w-full flex justify-between items-start">
|
|
<div>
|
|
<h1 class="text-3xl font-bold tracking-tighter mb-1">UPTIME<span class="status-down">MONITOR</span></h1>
|
|
<div id="monitor-status" class="flex items-center gap-2 text-xs opacity-70">
|
|
<div class="w-2 h-2 rounded-full bg-gray-500"></div>
|
|
<span>Checking system...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3 w-full">
|
|
<button onclick="openModal('settings-modal')" class="p-3 card hover:bg-slate-800 flex justify-center items-center active:scale-95 transition-transform">
|
|
<i data-lucide="settings" class="w-5 h-5"></i>
|
|
</button>
|
|
<button onclick="openModal('add-modal')" class="p-3 card hover:bg-slate-800 flex-1 flex items-center justify-center gap-2 active:scale-95 transition-transform">
|
|
<i data-lucide="plus" class="w-5 h-5"></i>
|
|
<span class="font-medium text-sm">Add URL</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Changed grid to flex column so it stays stacked like a mobile app -->
|
|
<div id="urls-container" class="flex flex-col gap-4 pb-24">
|
|
<!-- URL Cards will be injected here -->
|
|
<div class="card p-6 flex flex-col items-center justify-center opacity-50 border-dashed min-h-[150px]">
|
|
<p class="text-sm">Loading monitor data...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add URL Modal -->
|
|
<div id="add-modal" class="fixed inset-0 bg-black/60 backdrop-blur-sm hidden z-50 flex items-center justify-center p-4">
|
|
<div class="card w-full max-w-md p-6 bg-slate-900 max-h-[90vh] overflow-y-auto">
|
|
<h2 class="text-xl font-bold mb-5">Add New URL</h2>
|
|
<form id="add-url-form">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-1 opacity-70">Name</label>
|
|
<input type="text" name="name" required class="w-full bg-slate-800 border border-slate-700 rounded-xl p-3 focus:outline-none focus:border-pink-400 transition-colors">
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium mb-1 opacity-70">Endpoint URL</label>
|
|
<input type="url" name="url" required placeholder="https://example.com" class="w-full bg-slate-800 border border-slate-700 rounded-xl p-3 focus:outline-none focus:border-pink-400 transition-colors">
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('add-modal')" class="flex-1 p-3 rounded-xl text-sm font-medium opacity-70 hover:opacity-100 transition-opacity bg-slate-800 active:scale-95 transition-transform">Cancel</button>
|
|
<button type="submit" class="flex-1 p-3 bg-pink-500 rounded-xl text-sm font-bold text-white shadow-lg shadow-pink-500/20 hover:bg-pink-600 transition-colors active:scale-95 transition-transform">Monitor</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Modal -->
|
|
<div id="settings-modal" class="fixed inset-0 bg-black/60 backdrop-blur-sm hidden z-50 flex items-center justify-center p-4">
|
|
<div class="card w-full max-w-md p-6 bg-slate-900 max-h-[90vh] overflow-y-auto">
|
|
<h2 class="text-xl font-bold mb-5">Telegram Settings</h2>
|
|
<form id="settings-form">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-1 opacity-70">Bot Token</label>
|
|
<input type="text" name="telegram_bot_token" id="set_bot_token" class="w-full bg-slate-800 border border-slate-700 rounded-xl p-3 focus:outline-none focus:border-pink-400 transition-colors">
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium mb-1 opacity-70">Chat ID</label>
|
|
<input type="text" name="telegram_chat_id" id="set_chat_id" class="w-full bg-slate-800 border border-slate-700 rounded-xl p-3 focus:outline-none focus:border-pink-400 transition-colors">
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('settings-modal')" class="flex-1 p-3 rounded-xl text-sm font-medium opacity-70 hover:opacity-100 transition-opacity bg-slate-800 active:scale-95 transition-transform">Cancel</button>
|
|
<button type="submit" class="flex-1 p-3 bg-white text-slate-900 rounded-xl text-sm font-bold shadow-lg shadow-white/20 hover:bg-gray-200 transition-colors active:scale-95 transition-transform">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
lucide.createIcons();
|
|
|
|
function openModal(id) {
|
|
document.getElementById(id).classList.remove('hidden');
|
|
document.body.style.overflow = 'hidden'; // prevent background scrolling on mobile
|
|
}
|
|
|
|
function closeModal(id) {
|
|
document.getElementById(id).classList.add('hidden');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
async function fetchData() {
|
|
try {
|
|
const response = await fetch('api/status.php');
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
updateUI(result.data, result.monitor_running);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
}
|
|
}
|
|
|
|
function updateUI(urls, monitorRunning) {
|
|
const container = document.getElementById('urls-container');
|
|
const statusEl = document.getElementById('monitor-status');
|
|
|
|
if (monitorRunning) {
|
|
statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-green-400 pulse shrink-0"></div><span class="truncate">Monitoring Running (24/7)</span>';
|
|
} else {
|
|
statusEl.innerHTML = '<div class="w-2 h-2 rounded-full bg-red-400 shrink-0"></div><span class="truncate">Monitor Offline - Check monitor.php</span>';
|
|
}
|
|
|
|
if (urls.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="card p-6 col-span-full flex flex-col items-center justify-center border-dashed text-center min-h-[200px]">
|
|
<i data-lucide="activity" class="w-10 h-10 mb-4 opacity-20"></i>
|
|
<p class="opacity-50 text-sm">No URLs monitored yet.</p>
|
|
<button onclick="openModal('add-modal')" class="mt-4 text-pink-400 font-medium text-sm p-2 active:scale-95 transition-transform">Add your first URL</button>
|
|
</div>
|
|
`;
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = urls.map(url => {
|
|
const isUp = url.status === 'up';
|
|
const history = url.history || [];
|
|
|
|
// Health bar width based on uptime %
|
|
const healthWidth = url.uptime + '%';
|
|
|
|
return `
|
|
<div class="card p-4 flex flex-col gap-3 w-full">
|
|
<div class="flex justify-between items-start">
|
|
<div class="flex-1 overflow-hidden pr-2">
|
|
<h3 class="text-lg font-bold truncate">${url.name}</h3>
|
|
<p class="text-[11px] opacity-50 truncate">${url.url}</p>
|
|
</div>
|
|
<div class="flex flex-col items-end shrink-0">
|
|
<div class="health-bar-container">
|
|
<div class="health-fill ${isUp ? '' : 'down'}" style="width: ${healthWidth}"></div>
|
|
</div>
|
|
<span class="text-[9px] font-medium opacity-50 mt-1">${url.uptime}% UPTIME</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center my-1">
|
|
<div class="flex flex-col">
|
|
<span class="text-2xl font-bold ${isUp ? 'status-up' : 'status-down'}">
|
|
${isUp ? 'RUNNING' : 'DOWN'}
|
|
</span>
|
|
<span class="text-[9px] opacity-50 font-medium">LAST STATUS</span>
|
|
</div>
|
|
<div class="flex flex-col items-end">
|
|
<span class="text-xl font-bold">${url.response_time}ms</span>
|
|
<span class="text-[9px] opacity-50 font-medium">RESP TIME</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="candlestick-chart">
|
|
${history.map(log => {
|
|
// Max height for 2000ms
|
|
const height = Math.min((log.response_time / 2000) * 100, 100);
|
|
const candleClass = log.status === 'up' ? 'up' : 'down';
|
|
return `<div class="candle ${candleClass}" style="height: ${Math.max(height, 5)}%"></div>`;
|
|
}).join('')}
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center mt-1">
|
|
<span class="text-[9px] opacity-40">${url.last_check || 'Never checked'}</span>
|
|
<button onclick="deleteUrl(${url.id})" class="text-xs p-2 -mr-2 opacity-40 hover:opacity-100 hover:text-pink-400 active:scale-95 transition-transform">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
async function deleteUrl(id) {
|
|
if (!confirm('Stop monitoring this URL?')) return;
|
|
const formData = new FormData();
|
|
formData.append('action', 'delete_url');
|
|
formData.append('id', id);
|
|
await fetch('api/admin_actions.php', { method: 'POST', body: formData });
|
|
fetchData();
|
|
}
|
|
|
|
document.getElementById('add-url-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
formData.append('action', 'add_url');
|
|
const response = await fetch('api/admin_actions.php', { method: 'POST', body: formData });
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
closeModal('add-modal');
|
|
e.target.reset();
|
|
fetchData();
|
|
} else {
|
|
alert(result.error);
|
|
}
|
|
};
|
|
|
|
document.getElementById('settings-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
formData.append('action', 'update_settings');
|
|
const response = await fetch('api/admin_actions.php', { method: 'POST', body: formData });
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
closeModal('settings-modal');
|
|
alert('Settings saved!');
|
|
}
|
|
};
|
|
|
|
setInterval(fetchData, 1000);
|
|
fetchData();
|
|
</script>
|
|
</body>
|
|
</html>
|