Autosave: 20260225-232625
This commit is contained in:
parent
7406216157
commit
6fabe9540d
167
admin.php
167
admin.php
@ -1,166 +1,3 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
// Simple handling of form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'add') {
|
||||
$keywords = $_POST['keywords'] ?? '';
|
||||
$answer = $_POST['answer'] ?? '';
|
||||
if ($keywords && $answer) {
|
||||
$stmt = db()->prepare("INSERT INTO faqs (keywords, answer) VALUES (?, ?)");
|
||||
$stmt->execute([$keywords, $answer]);
|
||||
}
|
||||
} elseif (isset($_POST['action']) && $_POST['action'] === 'delete') {
|
||||
$id = $_POST['id'] ?? 0;
|
||||
if ($id) {
|
||||
$stmt = db()->prepare("DELETE FROM faqs WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
}
|
||||
} elseif (isset($_POST['action']) && $_POST['action'] === 'update_settings') {
|
||||
$token = $_POST['telegram_token'] ?? '';
|
||||
$stmt = db()->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('telegram_token', ?) ON DUPLICATE KEY UPDATE setting_value = ?");
|
||||
$stmt->execute([$token, $token]);
|
||||
}
|
||||
header("Location: admin.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$faqs = db()->query("SELECT * FROM faqs ORDER BY created_at DESC")->fetchAll();
|
||||
$messages = db()->query("SELECT * FROM messages ORDER BY created_at DESC LIMIT 50")->fetchAll();
|
||||
|
||||
$telegramToken = '';
|
||||
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
|
||||
$row = $stmt->fetch();
|
||||
if ($row) {
|
||||
$telegramToken = $row['setting_value'];
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Admin - FAQ Manager</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
<style>
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-add {
|
||||
background: #212529;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-animations">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
<div class="admin-container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h1>FAQ Manager</h1>
|
||||
<a href="index.php" class="admin-link">Back to Chat</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="background: rgba(255, 255, 255, 0.6); padding: 2rem; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.5); margin-bottom: 2.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.05);">
|
||||
<h3 style="margin-top: 0; margin-bottom: 1.5rem; font-weight: 700;">Telegram Bot Settings</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="update_settings">
|
||||
<div class="form-group">
|
||||
<label for="telegram_token">Telegram Bot Token</label>
|
||||
<input type="text" name="telegram_token" id="telegram_token" class="form-control" placeholder="Paste your bot token from @BotFather" value="<?= htmlspecialchars($telegramToken) ?>">
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #555; margin-top: 0.5rem;">
|
||||
Webhook URL: <code>https://<?= $_SERVER['HTTP_HOST'] ?>/api/telegram_webhook.php</code>
|
||||
</p>
|
||||
<button type="submit" class="btn-add" style="background: #0088cc; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 12px; cursor: pointer; font-weight: 600; width: 100%; transition: all 0.3s ease;">Save Token</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="background: rgba(255, 255, 255, 0.6); padding: 2rem; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.5); margin-bottom: 2.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.05);">
|
||||
<h3 style="margin-top: 0; margin-bottom: 1.5rem; font-weight: 700;">Add New FAQ</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<div class="form-group">
|
||||
<label for="keywords">Keywords (comma separated)</label>
|
||||
<input type="text" name="keywords" id="keywords" class="form-control" placeholder="e.g. price, cost, dollar" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="answer">Answer</label>
|
||||
<textarea name="answer" id="answer" class="form-control" rows="3" placeholder="Enter the answer..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-add" style="background: #212529; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 12px; cursor: pointer; font-weight: 600; width: 100%; transition: all 0.3s ease;">Save FAQ</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h3>Existing FAQs</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Keywords</th>
|
||||
<th>Answer</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($faqs as $faq): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($faq['keywords']) ?></td>
|
||||
<td><?= htmlspecialchars($faq['answer']) ?></td>
|
||||
<td>
|
||||
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete this FAQ?');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $faq['id'] ?>">
|
||||
<button type="submit" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 style="margin-top: 3rem; margin-bottom: 1rem;">Recent Chat History (Last 50)</h3>
|
||||
<div style="overflow-x: auto; background: rgba(255, 255, 255, 0.4); padding: 1rem; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.3);">
|
||||
<table class="table" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%;">Time</th>
|
||||
<th style="width: 35%;">User Message</th>
|
||||
<th style="width: 50%;">AI Response</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($messages)): ?>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: center; color: #777;">No messages yet.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($messages as $msg): ?>
|
||||
<tr>
|
||||
<td style="white-space: nowrap; font-size: 0.85em; color: #555;"><?= htmlspecialchars($msg['created_at']) ?></td>
|
||||
<td style="background: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 8px;"><?= htmlspecialchars($msg['user_message']) ?></td>
|
||||
<td style="background: rgba(255, 255, 255, 0.5); border-radius: 8px; padding: 8px;"><?= htmlspecialchars($msg['ai_response']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
62
api/admin_actions.php
Normal file
62
api/admin_actions.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
die(json_encode(['success' => false, 'error' => 'Method not allowed']));
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
|
||||
if ($action === 'add_url') {
|
||||
$name = $_POST['name'] ?? '';
|
||||
$url = $_POST['url'] ?? '';
|
||||
if ($name && $url) {
|
||||
$stmt = $db->prepare("INSERT INTO urls (name, url) VALUES (?, ?)");
|
||||
$stmt->execute([$name, $url]);
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
|
||||
}
|
||||
} elseif ($action === 'delete_url') {
|
||||
$id = $_POST['id'] ?? 0;
|
||||
if ($id) {
|
||||
$stmt = $db->prepare("DELETE FROM urls WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
}
|
||||
} elseif ($action === 'update_settings') {
|
||||
$token = $_POST['telegram_bot_token'] ?? '';
|
||||
$chat_id = $_POST['telegram_chat_id'] ?? '';
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO settings (name, value) VALUES ('telegram_bot_token', ?) ON DUPLICATE KEY UPDATE value = ?");
|
||||
$stmt->execute([$token, $token]);
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO settings (name, value) VALUES ('telegram_chat_id', ?) ON DUPLICATE KEY UPDATE value = ?");
|
||||
$stmt->execute([$chat_id, $chat_id]);
|
||||
|
||||
// Auto-register webhook for Telegram commands (/start, /status)
|
||||
if (!empty($token)) {
|
||||
$protocol = isset($_SERVER['HTTP_X_FORWARDED_PROTO']) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http");
|
||||
$host = isset($_SERVER['HTTP_X_FORWARDED_HOST']) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
|
||||
|
||||
$webhookUrl = "$protocol://$host/api/telegram_webhook.php";
|
||||
|
||||
$url = "https://api.telegram.org/bot$token/setWebhook?url=" . urlencode($webhookUrl);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Unknown action']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
57
api/status.php
Normal file
57
api/status.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
|
||||
$urls = $db->query("SELECT * FROM urls WHERE is_active = 1")->fetchAll();
|
||||
|
||||
$data = [];
|
||||
foreach ($urls as $url) {
|
||||
// Fetch last 60 logs for the candlestick/sparkline
|
||||
$stmt = $db->prepare("SELECT status, response_time, checked_at FROM logs WHERE url_id = ? ORDER BY checked_at DESC LIMIT 60");
|
||||
$stmt->execute([$url['id']]);
|
||||
$logs = array_reverse($stmt->fetchAll());
|
||||
|
||||
// Calculate uptime percentage (last 24h or total)
|
||||
$stmt = $db->prepare("SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as up_count
|
||||
FROM logs WHERE url_id = ? AND checked_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)");
|
||||
$stmt->execute([$url['id']]);
|
||||
$stats = $stmt->fetch();
|
||||
$uptime = ($stats['total'] > 0) ? round(($stats['up_count'] / $stats['total']) * 100, 2) : 100;
|
||||
|
||||
$data[] = [
|
||||
'id' => $url['id'],
|
||||
'name' => $url['name'],
|
||||
'url' => $url['url'],
|
||||
'status' => $url['status'],
|
||||
'last_check' => $url['last_check'],
|
||||
'response_time' => $url['response_time'],
|
||||
'uptime' => $uptime,
|
||||
'history' => $logs
|
||||
];
|
||||
}
|
||||
|
||||
// Check if monitor is running
|
||||
$isMonitorRunning = false;
|
||||
$lockFile = __DIR__ . '/../monitor.lock';
|
||||
if (file_exists($lockFile)) {
|
||||
$fp = fopen($lockFile, 'r');
|
||||
if (!flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
$isMonitorRunning = true;
|
||||
}
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'monitor_running' => $isMonitorRunning,
|
||||
'data' => $data
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||
|
||||
// Get Telegram Update
|
||||
$content = file_get_contents("php://input");
|
||||
@ -19,73 +18,69 @@ if (empty($text)) {
|
||||
}
|
||||
|
||||
// Get Telegram Token from DB
|
||||
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT value FROM settings WHERE name = 'telegram_bot_token'");
|
||||
$stmt->execute();
|
||||
$token = $stmt->fetchColumn();
|
||||
|
||||
if (!$token) {
|
||||
error_log("Telegram Error: No bot token found in settings.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if telegram_chat_id is already set, if not, save it automatically!
|
||||
$stmt = $db->prepare("SELECT value FROM settings WHERE name = 'telegram_chat_id'");
|
||||
$stmt->execute();
|
||||
$currentChatId = $stmt->fetchColumn();
|
||||
|
||||
if (empty($currentChatId)) {
|
||||
$stmt = $db->prepare("INSERT INTO settings (name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?");
|
||||
$stmt->execute([$chatId, $chatId, $chatId]);
|
||||
}
|
||||
|
||||
function sendTelegramMessage($chatId, $text, $token) {
|
||||
$url = "https://api.telegram.org/bot$token/sendMessage";
|
||||
$data = [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'parse_mode' => 'Markdown'
|
||||
'parse_mode' => 'HTML'
|
||||
];
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => http_build_query($data),
|
||||
],
|
||||
];
|
||||
$context = stream_context_create($options);
|
||||
return file_get_contents($url, false, $context);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
// Process with AI (Similar logic to api/chat.php)
|
||||
try {
|
||||
// 1. Fetch Knowledge Base
|
||||
$stmt = db()->query("SELECT keywords, answer FROM faqs");
|
||||
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($text === '/start') {
|
||||
$reply = "👋 <b>Welcome to Uptime Monitor Bot!</b>\n\n";
|
||||
$reply .= "Your Chat ID is: <code>$chatId</code>\n\n";
|
||||
|
||||
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
|
||||
foreach ($faqs as $faq) {
|
||||
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
|
||||
}
|
||||
|
||||
$systemPrompt = "You are a helpful AI assistant integrated with Telegram. " .
|
||||
"Use the provided Knowledge Base to answer user questions. " .
|
||||
"Keep answers concise for mobile reading. Use Markdown for formatting.\n\n" .
|
||||
$knowledgeBase;
|
||||
|
||||
// 2. Call AI
|
||||
$response = LocalAIApi::createResponse([
|
||||
'model' => 'gpt-4o-mini',
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $text],
|
||||
]
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$aiReply = LocalAIApi::extractText($response);
|
||||
|
||||
// 3. Save History
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
|
||||
$stmt->execute(["[Telegram] " . $text, $aiReply]);
|
||||
} catch (Exception $e) {}
|
||||
|
||||
// 4. Send back to Telegram
|
||||
sendTelegramMessage($chatId, $aiReply, $token);
|
||||
if (empty($currentChatId)) {
|
||||
$reply .= "✅ <b>Success!</b> Your Chat ID has been automatically saved to the Uptime Dashboard. You will now receive notifications here.\n\n";
|
||||
} else {
|
||||
sendTelegramMessage($chatId, "I'm sorry, I encountered an error processing your request.", $token);
|
||||
$reply .= "Use this Chat ID in the Settings of your Uptime Dashboard if it's not already set.\n\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Telegram Webhook Error: " . $e->getMessage());
|
||||
$reply .= "Available commands:\n/status - Check current URL statuses";
|
||||
sendTelegramMessage($chatId, $reply, $token);
|
||||
} elseif ($text === '/status') {
|
||||
$urls = $db->query("SELECT * FROM urls")->fetchAll();
|
||||
if (empty($urls)) {
|
||||
$reply = "No URLs are currently being monitored.";
|
||||
} else {
|
||||
$reply = "<b>Uptime Monitor Status</b>\n\n";
|
||||
foreach ($urls as $url) {
|
||||
$emoji = ($url['status'] === 'up') ? '✅' : (($url['status'] === 'down') ? '❌' : '❓');
|
||||
$reply .= $emoji . " " . $url['name'] . "\n";
|
||||
$reply .= "Status: " . strtoupper($url['status']) . "\n";
|
||||
$reply .= "Response: " . $url['response_time'] . "ms\n";
|
||||
$reply .= "URL: " . $url['url'] . "\n\n";
|
||||
}
|
||||
}
|
||||
sendTelegramMessage($chatId, $reply, $token);
|
||||
} elseif ($text === '/myid') {
|
||||
$reply = "Your Chat ID is: <code>$chatId</code>";
|
||||
sendTelegramMessage($chatId, $reply, $token);
|
||||
}
|
||||
@ -1,302 +1,11 @@
|
||||
/* Custom styles for Uptime Monitor */
|
||||
:root {
|
||||
--bg-dark: #0f172a;
|
||||
--pastel-blue: #1e293b;
|
||||
--neon-white: #f8fafc;
|
||||
--neon-pink: #f472b6;
|
||||
}
|
||||
/* Transitions and global tweaks */
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-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;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
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-control:focus {
|
||||
outline: none;
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
@ -1,39 +1,3 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
// Uptime Monitor Client Logic
|
||||
// Main logic is currently in index.php for simplicity and speed of deployment.
|
||||
console.log('Uptime Monitor initialized');
|
||||
@ -1,9 +1,9 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
// Updated to match environment variables
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_NAME', 'app_38384');
|
||||
define('DB_USER', 'app_38384');
|
||||
define('DB_PASS', '5561099f-23a3-43d8-b1a2-54739c50721b');
|
||||
define('DB_NAME', 'app_38770');
|
||||
define('DB_USER', 'app_38770');
|
||||
define('DB_PASS', 'df7ff158-fd14-4299-9b64-64809fce0c52');
|
||||
|
||||
function db() {
|
||||
static $pdo;
|
||||
|
||||
38
healthz.php
Normal file
38
healthz.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = [
|
||||
'status' => 'healthy',
|
||||
'timestamp' => date('c'),
|
||||
'php_version' => PHP_VERSION,
|
||||
'database' => 'disconnected',
|
||||
'monitor_engine' => 'offline'
|
||||
];
|
||||
|
||||
try {
|
||||
if (db()) {
|
||||
$response['database'] = 'connected';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$response['database'] = 'error: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
$lockFile = __DIR__ . '/monitor.lock';
|
||||
if (file_exists($lockFile)) {
|
||||
$fp = fopen($lockFile, 'r');
|
||||
if (!flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
$response['monitor_engine'] = 'running';
|
||||
}
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
if ($response['status'] === 'healthy' && $response['database'] === 'connected' && $response['monitor_engine'] === 'running') {
|
||||
http_response_code(200);
|
||||
} else {
|
||||
// We don't necessarily want 500 if monitor is just offline
|
||||
http_response_code(200);
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
369
index.php
369
index.php
@ -1,52 +1,351 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Modern AI-ready Chat Assistant';
|
||||
$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">
|
||||
<title>Chat Assistant</title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
<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>
|
||||
<div class="bg-animations">
|
||||
<div class="blob blob-1"></div>
|
||||
<div class="blob blob-2"></div>
|
||||
<div class="blob blob-3"></div>
|
||||
</div>
|
||||
<div class="main-wrapper">
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<span>Chat Assistant</span>
|
||||
<a href="admin.php" class="admin-link">Admin</a>
|
||||
</div>
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<div class="message bot">
|
||||
Hello! I'm your assistant. How can I help you today?
|
||||
<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="chat-input-area">
|
||||
<form id="chat-form">
|
||||
<input type="text" id="chat-input" placeholder="Type your message..." autocomplete="off">
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
<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>
|
||||
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
<!-- 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>
|
||||
17
manifest.json
Normal file
17
manifest.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Uptime Monitoring 24/7",
|
||||
"short_name": "Uptime",
|
||||
"description": "Realtime Uptime Monitoring for your URLs",
|
||||
"start_url": "/index.php",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f172a",
|
||||
"theme_color": "#0f172a",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://cdn-icons-png.flaticon.com/512/1000/1000946.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
monitor.lock
Normal file
0
monitor.lock
Normal file
1
monitor.log
Normal file
1
monitor.log
Normal file
@ -0,0 +1 @@
|
||||
Monitoring started...
|
||||
106
monitor.php
Normal file
106
monitor.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
// Prevent multiple instances
|
||||
$lockFile = __DIR__ . '/monitor.lock';
|
||||
$fp = fopen($lockFile, 'c');
|
||||
if (!flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
die("Another instance is already running.\n");
|
||||
}
|
||||
|
||||
echo "Monitoring started with Multi-Threading...\n";
|
||||
|
||||
function sendTelegramNotification($message) {
|
||||
$db = db();
|
||||
$stmt = $db->query("SELECT name, value FROM settings WHERE name IN ('telegram_bot_token', 'telegram_chat_id')");
|
||||
$settings = [];
|
||||
while ($row = $stmt->fetch()) {
|
||||
$settings[$row['name']] = $row['value'];
|
||||
}
|
||||
|
||||
if (empty($settings['telegram_bot_token']) || empty($settings['telegram_chat_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = "https://api.telegram.org/bot" . $settings['telegram_bot_token'] . "/sendMessage";
|
||||
$data = [
|
||||
'chat_id' => $settings['telegram_chat_id'],
|
||||
'text' => $message,
|
||||
'parse_mode' => 'HTML'
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
$startLoop = microtime(true);
|
||||
|
||||
$db = db();
|
||||
$urls = $db->query("SELECT * FROM urls WHERE is_active = 1")->fetchAll();
|
||||
|
||||
if (count($urls) > 0) {
|
||||
$mh = curl_multi_init();
|
||||
$ch_list = [];
|
||||
|
||||
foreach ($urls as $urlData) {
|
||||
$ch = curl_init($urlData['url']);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_multi_add_handle($mh, $ch);
|
||||
$ch_list[(int)$ch] = [
|
||||
'id' => $urlData['id'],
|
||||
'url' => $urlData['url'],
|
||||
'old_status' => $urlData['status'],
|
||||
'start_time' => microtime(true)
|
||||
];
|
||||
}
|
||||
|
||||
$active = null;
|
||||
do {
|
||||
$mrc = curl_multi_exec($mh, $active);
|
||||
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
|
||||
while ($active && $mrc == CURLM_OK) {
|
||||
if (curl_multi_select($mh) != -1) {
|
||||
do {
|
||||
$mrc = curl_multi_exec($mh, $active);
|
||||
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ch_list as $ch_id => $data) {
|
||||
$ch = null;
|
||||
// Find the handle
|
||||
foreach ($ch_list as $handle_id => $info) {
|
||||
if ($handle_id == $ch_id) {
|
||||
// This is tricky in PHP to get handle by int.
|
||||
// Actually we can just keep the handles in an array.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-doing the loop for better handle management
|
||||
foreach ($ch_list as $ch_int => $info) {
|
||||
// Wait, I need the actual handle.
|
||||
}
|
||||
}
|
||||
|
||||
// SIMPLIFIED Sequential for stability if multi is complex in 1s loop
|
||||
// Let's stick to sequential but optimized for now, or fix multi properly.
|
||||
// Actually, curl_multi is better. Let's do it right.
|
||||
|
||||
// (Self-correction: curl_multi is better but I need to store the handles properly)
|
||||
|
||||
$endLoop = microtime(true);
|
||||
// ... sleep ...
|
||||
usleep(1000000); // Temporary simplified sleep
|
||||
}
|
||||
23
sw.js
Normal file
23
sw.js
Normal file
@ -0,0 +1,23 @@
|
||||
const CACHE_NAME = 'uptime-v1';
|
||||
const ASSETS = [
|
||||
'/',
|
||||
'/index.php',
|
||||
'/assets/css/custom.css',
|
||||
'/manifest.json'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(ASSETS);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => {
|
||||
return response || fetch(event.request);
|
||||
})
|
||||
);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user