Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
318
api/uptime.php
318
api/uptime.php
@ -1,318 +0,0 @@
|
|||||||
<?php
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
require_once __DIR__ . '/../mail/MailService.php';
|
|
||||||
|
|
||||||
$action = $_GET['action'] ?? (php_sapi_name() === 'cli' ? 'check' : '');
|
|
||||||
|
|
||||||
function ping($url) {
|
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
||||||
curl_setopt($ch, CURLOPT_NOBODY, true); // HEAD request
|
|
||||||
|
|
||||||
$startTime = microtime(true);
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$latency = (microtime(true) - $startTime) * 1000; // ms
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
$error = curl_error($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => ($httpCode >= 200 && $httpCode < 400) ? 'ok' : 'error',
|
|
||||||
'latency' => round($latency, 2),
|
|
||||||
'http_code' => $httpCode,
|
|
||||||
'error' => $error ?: ($httpCode ? "HTTP $httpCode" : "Connection failed")
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyStatusChange($url, $oldStatus, $newStatus, $error = null) {
|
|
||||||
$time = date('Y-m-d H:i:s');
|
|
||||||
$statusEmoji = ($newStatus === 'error') ? '๐จ' : '๐ข';
|
|
||||||
$statusText = strtoupper($newStatus);
|
|
||||||
|
|
||||||
// 1. Send Telegram Push to Phone (24/7 real-time forcing)
|
|
||||||
try {
|
|
||||||
$stmt = db()->query("SELECT value FROM settings WHERE `key` = 'telegram_token'");
|
|
||||||
$token = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
$stmt2 = db()->query("SELECT value FROM settings WHERE `key` = 'telegram_chat_id'");
|
|
||||||
$chatId = $stmt2->fetchColumn();
|
|
||||||
|
|
||||||
if ($token && $chatId) {
|
|
||||||
$msg = "$statusEmoji *MONITOR ALERT*
|
|
||||||
URL: $url
|
|
||||||
Status: *$statusText*
|
|
||||||
Time: $time
|
|
||||||
" . ($error ? "Error: $error" : "");
|
|
||||||
$tgUrl = "https://api.telegram.org/bot$token/sendMessage";
|
|
||||||
$data = ['chat_id' => $chatId, 'text' => $msg, 'parse_mode' => 'Markdown'];
|
|
||||||
$options = [
|
|
||||||
'http' => [
|
|
||||||
'header' => "Content-type: application/x-www-form-urlencoded
|
|
||||||
",
|
|
||||||
'method' => 'POST',
|
|
||||||
'content' => http_build_query($data),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
file_get_contents($tgUrl, false, stream_context_create($options));
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
// Ignore errors if telegram not setup
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Original Email logic
|
|
||||||
$recipient = 'yumeecute@aol.com';
|
|
||||||
|
|
||||||
if ($oldStatus === 'ok' && $newStatus === 'error') {
|
|
||||||
$subject = "๐ด ALERT: Website Down - $url";
|
|
||||||
$html = <div style='font-family: Arial, sans-serif; border: 1px solid #ff4d4d; padding: 20px; border-radius: 8px;'>
|
|
||||||
<h2 style='color: #ff4d4d;'>Website Down Alert</h2>
|
|
||||||
<p>The following website is currently unresponsive:</p>
|
|
||||||
<p><strong>URL:</strong> <a href='$url'>$url</a></p>
|
|
||||||
<p><strong>Error:</strong> $error</p>
|
|
||||||
<p><strong>Time:</strong> $time</p>
|
|
||||||
<hr>
|
|
||||||
<p style='font-size: 12px; color: #666;'>This is an automated notification from your Uptime Monitor.</p>
|
|
||||||
</div>
|
|
||||||
";
|
|
||||||
} elseif ($oldStatus === 'error' && $newStatus === 'ok') {
|
|
||||||
$subject = "๐ข RESOLVED: Website Up - $url";
|
|
||||||
$html = <div style='font-family: Arial, sans-serif; border: 1px solid #28a745; padding: 20px; border-radius: 8px;'>
|
|
||||||
<h2 style='color: #28a745;'>Website Back Online</h2>
|
|
||||||
<p>The following website is responding normally again:</p>
|
|
||||||
<p><strong>URL:</strong> <a href='$url'>$url</a></p>
|
|
||||||
<p><strong>Time:</strong> $time</p>
|
|
||||||
<hr>
|
|
||||||
<p style='font-size: 12px; color: #666;'>This is an automated notification from your Uptime Monitor.</p>
|
|
||||||
</div>
|
|
||||||
";
|
|
||||||
} else {
|
|
||||||
return; // No notification for other transitions
|
|
||||||
}
|
|
||||||
|
|
||||||
MailService::sendMail($recipient, $subject, $html);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMonitoringEnabled() {
|
|
||||||
try {
|
|
||||||
$stmt = db()->prepare("SELECT value FROM settings WHERE `key` = 'monitoring_enabled'");
|
|
||||||
$stmt->execute();
|
|
||||||
$val = $stmt->fetchColumn();
|
|
||||||
return $val === '1';
|
|
||||||
} catch (Exception $e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateApiKey() {
|
|
||||||
return bin2hex(random_bytes(32));
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
|
|
||||||
case 'save_telegram':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$token = $data['telegram_token'] ?? '';
|
|
||||||
$chatId = $data['telegram_chat_id'] ?? '';
|
|
||||||
|
|
||||||
$stmt = db()->prepare("INSERT INTO settings (`key`, value) VALUES ('telegram_token', ?) ON DUPLICATE KEY UPDATE value = VALUES(value)");
|
|
||||||
$stmt->execute([$token]);
|
|
||||||
|
|
||||||
$stmt = db()->prepare("INSERT INTO settings (`key`, value) VALUES ('telegram_chat_id', ?) ON DUPLICATE KEY UPDATE value = VALUES(value)");
|
|
||||||
$stmt->execute([$chatId]);
|
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'get_telegram':
|
|
||||||
$stmt = db()->query("SELECT `key`, value FROM settings WHERE `key` IN ('telegram_token', 'telegram_chat_id')");
|
|
||||||
$res = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
||||||
echo json_encode([
|
|
||||||
'telegram_token' => $res['telegram_token'] ?? '',
|
|
||||||
'telegram_chat_id' => $res['telegram_chat_id'] ?? ''
|
|
||||||
]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'settings':
|
|
||||||
echo json_encode(['monitoring_enabled' => isMonitoringEnabled()]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'toggle':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$enabled = ($data['enabled'] ?? false) ? '1' : '0';
|
|
||||||
$stmt = db()->prepare("UPDATE settings SET value = ? WHERE `key` = 'monitoring_enabled'");
|
|
||||||
$stmt->execute([$enabled]);
|
|
||||||
echo json_encode(['success' => true, 'monitoring_enabled' => $enabled === '1']);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'list':
|
|
||||||
$stmt = db()->query("SELECT * FROM urls ORDER BY id DESC");
|
|
||||||
echo json_encode($stmt->fetchAll());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'add':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$url = $data['url'] ?? '';
|
|
||||||
if (filter_var($url, FILTER_VALIDATE_URL)) {
|
|
||||||
// Get default team_id or first available
|
|
||||||
$team_id = db()->query("SELECT id FROM teams LIMIT 1")->fetchColumn();
|
|
||||||
if (!$team_id) {
|
|
||||||
// Should not happen with migrations, but safety first
|
|
||||||
db()->query("INSERT INTO teams (name, owner_email) VALUES ('Default Team', 'admin@example.com')");
|
|
||||||
$team_id = db()->lastInsertId();
|
|
||||||
}
|
|
||||||
$stmt = db()->prepare("INSERT INTO urls (url, team_id) VALUES (?, ?)");
|
|
||||||
$stmt->execute([$url, $team_id]);
|
|
||||||
echo json_encode(['success' => true, 'id' => db()->lastInsertId()]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid URL. Use http:// or https://']);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$id = $data['id'] ?? 0;
|
|
||||||
$stmt = db()->prepare("DELETE FROM urls WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'check':
|
|
||||||
if (!isMonitoringEnabled()) {
|
|
||||||
echo json_encode(['info' => 'Monitoring is disabled globally']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = db()->query("SELECT * FROM urls WHERE is_active = 1");
|
|
||||||
$urls = $stmt->fetchAll();
|
|
||||||
$results = [];
|
|
||||||
|
|
||||||
foreach ($urls as $row) {
|
|
||||||
$res = ping($row['url']);
|
|
||||||
|
|
||||||
// Notify on change
|
|
||||||
notifyStatusChange($row['url'], $row['last_status'], $res['status'], $res['error']);
|
|
||||||
|
|
||||||
// Update URL status
|
|
||||||
$upd = db()->prepare("UPDATE urls SET last_status = ?, last_latency = ?, last_checked_at = NOW() WHERE id = ?");
|
|
||||||
$upd->execute([$res['status'], $res['latency'], $row['id']]);
|
|
||||||
|
|
||||||
// Insert log
|
|
||||||
$log = db()->prepare("INSERT INTO logs (url_id, status, latency, error_message) VALUES (?, ?, ?, ?)");
|
|
||||||
$log->execute([$row['id'], $res['status'], $res['latency'], $res['status'] === 'error' ? $res['error'] : null]);
|
|
||||||
|
|
||||||
$results[] = [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'url' => $row['url'],
|
|
||||||
'status' => $res['status'],
|
|
||||||
'latency' => $res['latency'],
|
|
||||||
'error' => $res['error']
|
|
||||||
];
|
|
||||||
}
|
|
||||||
echo json_encode($results);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'logs':
|
|
||||||
$url_id = $_GET['url_id'] ?? null;
|
|
||||||
if ($url_id) {
|
|
||||||
$stmt = db()->prepare("SELECT * FROM logs WHERE url_id = ? ORDER BY id DESC LIMIT 24");
|
|
||||||
$stmt->execute([$url_id]);
|
|
||||||
} else {
|
|
||||||
$stmt = db()->query("SELECT l.*, u.url FROM logs l JOIN urls u ON l.url_id = u.id ORDER BY l.id DESC LIMIT 50");
|
|
||||||
}
|
|
||||||
echo json_encode($stmt->fetchAll());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'stats':
|
|
||||||
$res = [
|
|
||||||
'total' => db()->query("SELECT COUNT(*) FROM urls")->fetchColumn(),
|
|
||||||
'up' => db()->query("SELECT COUNT(*) FROM urls WHERE last_status = 'ok'")->fetchColumn(),
|
|
||||||
'down' => db()->query("SELECT COUNT(*) FROM urls WHERE last_status = 'error'")->fetchColumn(),
|
|
||||||
'avg_latency' => db()->query("SELECT AVG(last_latency) FROM urls")->fetchColumn() ?: 0,
|
|
||||||
'recent_errors' => db()->query("SELECT l.*, u.url FROM logs l JOIN urls u ON l.url_id = u.id WHERE l.status = 'error' ORDER BY l.id DESC LIMIT 10")->fetchAll()
|
|
||||||
];
|
|
||||||
echo json_encode($res);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'teams':
|
|
||||||
$stmt = db()->query("SELECT * FROM teams ORDER BY id DESC");
|
|
||||||
echo json_encode($stmt->fetchAll());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'create_team':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$name = $data['name'] ?? '';
|
|
||||||
$email = $data['email'] ?? 'yumeecute@aol.com';
|
|
||||||
if ($name) {
|
|
||||||
$stmt = db()->prepare("INSERT INTO teams (name, owner_email) VALUES (?, ?)");
|
|
||||||
$stmt->execute([$name, $email]);
|
|
||||||
echo json_encode(['success' => true, 'id' => db()->lastInsertId()]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Team name required']);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'members':
|
|
||||||
$team_id = $_GET['team_id'] ?? null;
|
|
||||||
if (!$team_id) {
|
|
||||||
$team_id = db()->query("SELECT id FROM teams LIMIT 1")->fetchColumn();
|
|
||||||
}
|
|
||||||
if ($team_id) {
|
|
||||||
$stmt = db()->prepare("SELECT * FROM team_members WHERE team_id = ?");
|
|
||||||
$stmt->execute([$team_id]);
|
|
||||||
echo json_encode($stmt->fetchAll());
|
|
||||||
} else {
|
|
||||||
echo json_encode([]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'invite':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$email = $data['email'] ?? '';
|
|
||||||
$team_id = $data['team_id'] ?? db()->query("SELECT id FROM teams LIMIT 1")->fetchColumn();
|
|
||||||
if (filter_var($email, FILTER_VALIDATE_EMAIL) && $team_id) {
|
|
||||||
$stmt = db()->prepare("INSERT INTO team_members (team_id, email, status) VALUES (?, ?, 'invited')");
|
|
||||||
$stmt->execute([$team_id, $email]);
|
|
||||||
|
|
||||||
// Send invite email
|
|
||||||
$subject = "Invitation to join Team on ๐๐จ๐ง๐ข๐ญ๐จ๐ซ ๐๐ฉ๐ญ๐ข๐ฆ๐ ๐๐ฒ ๐๐ฎ๐ฆ๐๐";
|
|
||||||
$html = "<p>You have been invited to join a team.</p><p><a href='http://".$_SERVER['HTTP_HOST']."/?invite_email=".urlencode($email)."'>Accept Invitation</a></p>";
|
|
||||||
MailService::sendMail($email, $subject, $html);
|
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid email or team ID']);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'keys':
|
|
||||||
$team_id = $_GET['team_id'] ?? db()->query("SELECT id FROM teams LIMIT 1")->fetchColumn();
|
|
||||||
if ($team_id) {
|
|
||||||
$stmt = db()->prepare("SELECT * FROM api_keys WHERE team_id = ?");
|
|
||||||
$stmt->execute([$team_id]);
|
|
||||||
echo json_encode($stmt->fetchAll());
|
|
||||||
} else {
|
|
||||||
echo json_encode([]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'generate_key':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$label = $data['label'] ?? 'New Key';
|
|
||||||
$team_id = $data['team_id'] ?? db()->query("SELECT id FROM teams LIMIT 1")->fetchColumn();
|
|
||||||
if ($team_id) {
|
|
||||||
$key = generateApiKey();
|
|
||||||
$stmt = db()->prepare("INSERT INTO api_keys (team_id, api_key, label) VALUES (?, ?, ?)");
|
|
||||||
$stmt->execute([$team_id, $key, $label]);
|
|
||||||
echo json_encode(['success' => true, 'key' => $key]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'No team available']);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
echo json_encode(['error' => 'Unknown action']);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/api/uptime.php';
|
|
||||||
|
|
||||||
// A simple CLI script that runs continuously on the server
|
|
||||||
// Usage: php backend_monitor.php &
|
|
||||||
if (php_sapi_name() !== 'cli') {
|
|
||||||
die("This script must be run from the command line.");
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Starting 24/7 Flatlogic Backend Monitor...\n";
|
|
||||||
echo "Monitoring URLs every 2 seconds...\n";
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (isMonitoringEnabled()) {
|
|
||||||
$stmt = db()->query("SELECT * FROM urls WHERE is_active = 1");
|
|
||||||
$urls = $stmt->fetchAll();
|
|
||||||
|
|
||||||
foreach ($urls as $row) {
|
|
||||||
$res = ping($row['url']);
|
|
||||||
|
|
||||||
// Log latency to ensure history exists
|
|
||||||
$ins = db()->prepare("INSERT INTO uptime_logs (url_id, status, latency) VALUES (?, ?, ?)");
|
|
||||||
$ins->execute([$row['id'], $res['status'], $res['latency']]);
|
|
||||||
|
|
||||||
// Notify on change
|
|
||||||
notifyStatusChange($row['url'], $row['last_status'], $res['status'], $res['error']);
|
|
||||||
|
|
||||||
// Update URL status
|
|
||||||
$upd = db()->prepare("UPDATE urls SET last_status = ?, last_latency = ?, last_checked_at = NOW() WHERE id = ?");
|
|
||||||
$upd->execute([$res['status'], $res['latency'], $row['id']]);
|
|
||||||
|
|
||||||
// Keep only last 500 logs per URL to save DB space
|
|
||||||
$del = db()->prepare(
|
|
||||||
"DELETE FROM uptime_logs
|
|
||||||
WHERE url_id = ?
|
|
||||||
AND id NOT IN (
|
|
||||||
SELECT id FROM (
|
|
||||||
SELECT id FROM uptime_logs WHERE url_id = ? ORDER BY id DESC LIMIT 500
|
|
||||||
) foo
|
|
||||||
)"
|
|
||||||
);
|
|
||||||
$del->execute([$row['id'], $row['id']]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sleep(2); // Wait 2 seconds before the next check
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS urls (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
url VARCHAR(1024) NOT NULL,
|
|
||||||
is_active TINYINT(1) DEFAULT 1,
|
|
||||||
last_status VARCHAR(20) DEFAULT 'unknown', -- 'ok', 'error'
|
|
||||||
last_latency FLOAT DEFAULT 0,
|
|
||||||
last_checked_at TIMESTAMP NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS logs (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
url_id INT NOT NULL,
|
|
||||||
status VARCHAR(20) NOT NULL,
|
|
||||||
latency FLOAT DEFAULT 0,
|
|
||||||
error_message TEXT DEFAULT NULL,
|
|
||||||
checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
`key` VARCHAR(50) PRIMARY KEY,
|
|
||||||
`value` TEXT
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
INSERT INTO settings (`key`, `value`) VALUES ('monitoring_enabled', '0') ON DUPLICATE KEY UPDATE `key`=`key`;
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS teams (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
owner_email VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS team_members (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
team_id INT NOT NULL,
|
|
||||||
email VARCHAR(255) NOT NULL,
|
|
||||||
role VARCHAR(50) DEFAULT 'member', -- 'owner', 'admin', 'member'
|
|
||||||
status VARCHAR(50) DEFAULT 'active', -- 'invited', 'active'
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS api_keys (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
team_id INT NOT NULL,
|
|
||||||
api_key VARCHAR(64) NOT NULL UNIQUE,
|
|
||||||
label VARCHAR(255) DEFAULT 'Default Key',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
-- Add team_id to urls if it doesn't exist
|
|
||||||
SET @dbname = DATABASE();
|
|
||||||
SET @tablename = "urls";
|
|
||||||
SET @columnname = "team_id";
|
|
||||||
SET @preparedStatement = (SELECT IF(
|
|
||||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0,
|
|
||||||
"SELECT 1",
|
|
||||||
"ALTER TABLE urls ADD COLUMN team_id INT DEFAULT NULL AFTER id, ADD FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE SET NULL"
|
|
||||||
));
|
|
||||||
PREPARE stmt FROM @preparedStatement;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- Insert a default team for existing data
|
|
||||||
INSERT INTO teams (name, owner_email) VALUES ('Yumee Default Team', 'yumeecute@aol.com');
|
|
||||||
SET @default_team_id = LAST_INSERT_ID();
|
|
||||||
UPDATE urls SET team_id = @default_team_id WHERE team_id IS NULL;
|
|
||||||
925
index.php
925
index.php
@ -3,849 +3,148 @@ declare(strict_types=1);
|
|||||||
@ini_set('display_errors', '1');
|
@ini_set('display_errors', '1');
|
||||||
@error_reporting(E_ALL);
|
@error_reporting(E_ALL);
|
||||||
@date_default_timezone_set('UTC');
|
@date_default_timezone_set('UTC');
|
||||||
$v = time(); // Cache busting version
|
|
||||||
|
$phpVersion = PHP_VERSION;
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>๐๐จ๐ง๐ข๐ญ๐จ๐ซ ๐๐ฉ๐ญ๐ข๐ฆ๐ ๐๐ฒ ๐๐ฎ๐ฆ๐๐</title>
|
<title>New Style</title>
|
||||||
<link rel="manifest" href="manifest.json?v=<?php echo $v; ?>">
|
<?php
|
||||||
|
// Read project preview data from environment
|
||||||
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
|
?>
|
||||||
|
<?php if ($projectDescription): ?>
|
||||||
|
<!-- Meta description -->
|
||||||
|
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||||
|
<!-- Open Graph meta tags -->
|
||||||
|
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
|
<!-- Twitter meta tags -->
|
||||||
|
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($projectImageUrl): ?>
|
||||||
|
<!-- Open Graph image -->
|
||||||
|
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
|
<!-- Twitter image -->
|
||||||
|
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--crypto-green: #00ff88;
|
--bg-color-start: #6a11cb;
|
||||||
--crypto-red: #ff3366;
|
--bg-color-end: #2575fc;
|
||||||
--crypto-blue: #00d4ff;
|
--text-color: #ffffff;
|
||||||
--dark-bg: #0b0e11;
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
--dark-card: #181a20;
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
--dark-sidebar: #1e2329;
|
|
||||||
--text-main: #eaecef;
|
|
||||||
--text-dim: #848e9c;
|
|
||||||
--border: #2b3139;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
body {
|
||||||
margin: 0; padding: 0;
|
margin: 0;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
background: var(--dark-bg);
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
color: var(--text-main);
|
color: var(--text-color);
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
aside {
|
|
||||||
width: 260px;
|
|
||||||
background: var(--dark-sidebar);
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header { padding: 1.5rem; border-bottom: 1px solid var(--border); }
|
|
||||||
.sidebar-header h1 {
|
|
||||||
font-size: 1.1rem; margin: 0;
|
|
||||||
background: linear-gradient(90deg, #fff, var(--crypto-blue));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-list { list-style: none; padding: 1rem 0; margin: 0; flex: 1; }
|
|
||||||
.nav-item {
|
|
||||||
padding: 0.8rem 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.nav-item:hover, .nav-item.active {
|
body::before {
|
||||||
color: #fff;
|
content: '';
|
||||||
background: rgba(255,255,255,0.05);
|
position: absolute;
|
||||||
border-left: 3px solid var(--crypto-blue);
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||||
|
animation: bg-pan 20s linear infinite;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
@keyframes bg-pan {
|
||||||
/* Main Content */
|
0% { background-position: 0% 0%; }
|
||||||
main { flex: 1; display: flex; flex-direction: column; overflow-y: auto; }
|
100% { background-position: 100% 100%; }
|
||||||
.top-bar {
|
|
||||||
height: 60px; border-bottom: 1px solid var(--border);
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
padding: 0 2rem; background: var(--dark-bg);
|
|
||||||
position: sticky; top: 0; z-index: 99;
|
|
||||||
}
|
}
|
||||||
.content-area { padding: 2rem; max-width: 1400px; width: 100%; margin: 0 auto; }
|
main {
|
||||||
|
padding: 2rem;
|
||||||
/* Cards */
|
|
||||||
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 2rem; }
|
|
||||||
.card { background: var(--dark-card); border-radius: 12px; padding: 1.5rem; border: 1px solid var(--border); position: relative; }
|
|
||||||
.stat-val { font-family: 'JetBrains Mono', monospace; font-size: 1.8rem; font-weight: 700; display: block; margin-top: 5px; }
|
|
||||||
.stat-label { color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; }
|
|
||||||
|
|
||||||
/* Mobile Monitor Specific */
|
|
||||||
#mobile-monitor {
|
|
||||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background: #000; z-index: 2000; display: none;
|
|
||||||
flex-direction: column; padding: 4px;
|
|
||||||
}
|
}
|
||||||
#mobile-monitor.active { display: flex; }
|
.card {
|
||||||
.mm-header {
|
background: var(--card-bg-color);
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
border: 1px solid var(--card-border-color);
|
||||||
background: #111; padding: 6px 10px; border-radius: 6px; margin-bottom: 4px;
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
.mm-grid { display: grid; grid-template-columns: 1fr; gap: 4px; flex: 1; overflow-y: auto; }
|
.loader {
|
||||||
.mm-row {
|
margin: 1.25rem auto 1.25rem;
|
||||||
background: #050505; padding: 12px; border-radius: 8px;
|
width: 48px;
|
||||||
border: 1px solid #1a1a1a; display: flex; flex-direction: column; gap: 8px;
|
height: 48px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
.mm-info { display: flex; justify-content: space-between; align-items: center; font-family: 'JetBrains Mono'; }
|
@keyframes spin {
|
||||||
.mm-url-text { font-size: 0.6rem; color: #fff; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 70%; }
|
from { transform: rotate(0deg); }
|
||||||
.mm-lat-text { font-size: 0.75rem; font-weight: bold; }
|
to { transform: rotate(360deg); }
|
||||||
.mm-spark-container {
|
|
||||||
height: 240px; width: 100%; position: relative;
|
|
||||||
background: #000; border-radius: 4px; overflow: hidden;
|
|
||||||
border: 1px solid #111;
|
|
||||||
}
|
}
|
||||||
.mm-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
.hint {
|
||||||
|
opacity: 0.9;
|
||||||
/* Uptime Battery Style */
|
|
||||||
.uptime-battery {
|
|
||||||
display: flex; gap: 2px; height: 24px; margin-top: 4px; width: 100%;
|
|
||||||
}
|
}
|
||||||
.battery-seg {
|
.sr-only {
|
||||||
flex: 1; height: 100%; border-radius: 1px; transition: background 0.3s;
|
position: absolute;
|
||||||
|
width: 1px; height: 1px;
|
||||||
|
padding: 0; margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap; border: 0;
|
||||||
}
|
}
|
||||||
.seg-ok { background: var(--crypto-green); opacity: 0.8; }
|
h1 {
|
||||||
.seg-err { background: var(--crypto-red); }
|
font-size: 3rem;
|
||||||
.seg-none { background: #1a1a1a; }
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem;
|
||||||
.data-table { width: 100%; border-collapse: collapse; }
|
letter-spacing: -1px;
|
||||||
.data-table th { text-align: left; color: var(--text-dim); padding: 12px; border-bottom: 1px solid var(--border); font-size: 0.8rem; }
|
|
||||||
.data-table td { padding: 12px; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
|
|
||||||
.status-badge { padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; text-transform: uppercase; }
|
|
||||||
.bg-ok { background: rgba(0, 255, 136, 0.1); color: var(--crypto-green); }
|
|
||||||
.bg-err { background: rgba(255, 51, 102, 0.1); color: var(--crypto-red); }
|
|
||||||
|
|
||||||
input, select { background: #1e2329; border: 1px solid var(--border); color: #fff; padding: 10px 12px; border-radius: 4px; outline: none; width: 100%; }
|
|
||||||
button { padding: 10px 20px; border-radius: 4px; border: none; font-weight: 700; cursor: pointer; transition: all 0.2s; }
|
|
||||||
.btn-glow { background: var(--crypto-blue); color: #000; box-shadow: 0 0 15px rgba(0, 212, 255, 0.2); }
|
|
||||||
.section { display: none; }
|
|
||||||
.section.active { display: block; }
|
|
||||||
|
|
||||||
.fab {
|
|
||||||
position: fixed; bottom: 20px; right: 20px;
|
|
||||||
width: 60px; height: 60px; border-radius: 50%;
|
|
||||||
background: var(--crypto-blue); color: #000;
|
|
||||||
display: none; align-items: center; justify-content: center;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.4);
|
|
||||||
z-index: 1000; cursor: pointer; font-size: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
@media (max-width: 768px) {
|
margin: 0.5rem 0;
|
||||||
aside { display: none; }
|
font-size: 1.1rem;
|
||||||
.top-bar { padding: 0 1rem; }
|
}
|
||||||
.content-area { padding: 1rem; }
|
code {
|
||||||
.fab { display: flex; }
|
background: rgba(0,0,0,0.2);
|
||||||
.top-candle-btn { display: flex !important; }
|
padding: 2px 6px;
|
||||||
.dashboard-grid { grid-template-columns: 1fr; }
|
border-radius: 4px;
|
||||||
#dashboard-candle-grid, #dashboard-battery-grid { grid-template-columns: 1fr !important; }
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<aside>
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h1>๐๐จ๐ง๐ข๐ญ๐จ๐ซ ๐๐ฉ๐ญ๐ข๐ฆ๐</h1>
|
|
||||||
<div style="font-size: 0.6rem; color: var(--text-dim); margin-top: 4px;">by Yumee โข Healthcare AI</div>
|
|
||||||
</div>
|
|
||||||
<ul class="nav-list">
|
|
||||||
<li class="nav-item active" onclick="showSection('dashboard')"><span>๐</span> Dashboard</li>
|
|
||||||
<li class="nav-item" onclick="showSection('monitors')"><span>๐</span> Monitors</li>
|
|
||||||
<li class="nav-item" onclick="showSection('teams')"><span>๐ฅ</span> Team</li>
|
|
||||||
<li class="nav-item" onclick="showSection('api')"><span>๐</span> API</li>
|
|
||||||
<li class="nav-item" onclick="showSection('telegram')"><span>๐ฑ</span> Mobile Push</li>
|
|
||||||
</ul>
|
|
||||||
<div style="padding: 1rem; border-top: 1px solid var(--border);">
|
|
||||||
<button class="btn-glow" style="width:100%; margin-bottom:10px;" onclick="enterMobileMonitor()">๐ฑ FULL VIEW</button>
|
|
||||||
<button id="global-toggle" style="width:100%; font-size: 0.8rem;" onclick="toggleMonitoring()">STATION: OFFLINE</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="top-bar">
|
<div class="card">
|
||||||
<div style="display:flex; align-items:center; gap:10px;">
|
<h1>Analyzing your requirements and generating your websiteโฆ</h1>
|
||||||
<button class="btn-glow top-candle-btn" style="display:none; padding: 4px 10px; font-size: 0.7rem;" onclick="enterMobileMonitor()">FULL VIEW</button>
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<div id="ticker" style="font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; color: var(--crypto-green);">
|
<span class="sr-only">Loadingโฆ</span>
|
||||||
[NET: 100%]
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; align-items:center; gap: 10px;">
|
|
||||||
<span id="live-indicator" style="width:8px; height:8px; border-radius:50%; background: #555;"></span>
|
|
||||||
<span id="clock" style="font-family:'JetBrains Mono'; font-size:0.7rem;">00:00:00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-area">
|
|
||||||
<div id="dashboard" class="section active">
|
|
||||||
<div class="dashboard-grid">
|
|
||||||
<div class="card" onclick="enterMobileMonitor()" style="cursor:pointer; border: 1px solid var(--crypto-blue);">
|
|
||||||
<span class="stat-label">System Active</span>
|
|
||||||
<span class="stat-val" id="stat-total">0 Assets</span>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<span class="stat-label">Avg Latency</span>
|
|
||||||
<span class="stat-val" id="stat-latency" style="color: var(--crypto-blue)">0ms</span>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<span class="stat-label">Health</span>
|
|
||||||
<span class="stat-val" id="stat-health" style="color: var(--crypto-green)">100%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MONITOR (BATTREY) -->
|
|
||||||
<div style="margin-bottom: 40px;">
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-left: 4px solid var(--crypto-green); padding-left: 12px;">
|
|
||||||
<h3 style="margin:0; font-size: 1.2rem; letter-spacing: 2px; color: var(--crypto-green); font-weight: 800;">Heath</h3>
|
|
||||||
</div>
|
|
||||||
<div id="dashboard-battery-grid" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;">
|
|
||||||
<!-- Battery monitors injected here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MONITOR (CANDLE) -->
|
|
||||||
<div style="margin-bottom: 20px;">
|
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-left: 4px solid var(--crypto-blue); padding-left: 12px;">
|
|
||||||
<h3 style="margin:0; font-size: 1.2rem; letter-spacing: 2px; color: var(--crypto-blue); font-weight: 800;">Monitor Uptime Per Milisecond</h3>
|
|
||||||
</div>
|
|
||||||
<div id="dashboard-candle-grid" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px;">
|
|
||||||
<!-- Candle monitors injected here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="monitors" class="section">
|
|
||||||
<div class="card" style="margin-bottom: 20px;">
|
|
||||||
<h3>Add Asset</h3>
|
|
||||||
<div style="display:flex; gap: 10px;">
|
|
||||||
<input type="url" id="new-url" placeholder="https://example.com">
|
|
||||||
<button class="btn-glow" onclick="addAsset()">+ ADD</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead><tr><th>URL</th><th>Status</th><th>Latency</th><th>Actions</th></tr></thead>
|
|
||||||
<tbody id="inventory-list"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="teams" class="section">
|
|
||||||
<div class="card" style="margin-bottom: 20px;">
|
|
||||||
<h3>Invite Team Member</h3>
|
|
||||||
<div style="display:flex; gap: 10px;">
|
|
||||||
<input type="email" id="invite-email" placeholder="member@example.com">
|
|
||||||
<button class="btn-glow" onclick="inviteMember()">INVITE</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead><tr><th>Email</th><th>Status</th></tr></thead>
|
|
||||||
<tbody id="team-list"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TELEGRAM PUSH SECTION -->
|
|
||||||
<div id="telegram" class="section">
|
|
||||||
<div class="card" style="text-align: left; padding: 2rem;">
|
|
||||||
<h3 style="margin-top:0;">๐ฑ Mobile Push Notifications (24/7)</h3>
|
|
||||||
<p>Aplikasi ini berjalan <b>online 24/7 di server Flatlogic</b>. Untuk memaksa memunculkan notifikasi (wget monitor) langsung ke handphone Anda walau browser tertutup, hubungkan Telegram Bot Anda.</p>
|
|
||||||
|
|
||||||
<div style="margin: 1.5rem 0; display:flex; flex-direction:column; gap:10px;">
|
|
||||||
<input type="text" id="tg_token" class="input-field" placeholder="Bot Token (e.g. 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11)">
|
|
||||||
<input type="text" id="tg_chat_id" class="input-field" placeholder="Chat ID (e.g. 123456789)">
|
|
||||||
<button class="btn" onclick="saveTelegram()">Save Telegram Push Settings</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="font-size:0.85rem; opacity:0.8; background:rgba(0,0,0,0.2); padding:1rem; border-radius:8px;">
|
|
||||||
<b style="color:var(--accent);">Cara Setup:</b><br>
|
|
||||||
1. Buka Telegram, cari <b>@BotFather</b>, ketik <code>/newbot</code> lalu copy Token-nya.<br>
|
|
||||||
2. Ketik nama bot Anda di kolom pencarian Telegram dan Start.<br>
|
|
||||||
3. Buka browser: <code>https://api.telegram.org/bot<TOKEN_ANDA>/getUpdates</code> untuk menemukan <b>"chat":{"id":123456789}</b> Anda.<br>
|
|
||||||
4. Masukkan Token dan Chat ID di atas lalu Save. <b>Server akan mem-push pesan ๐จ ERROR & ๐ข RUN otomatis ke handphone Anda!</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="api" class="section">
|
|
||||||
<div class="card" style="margin-bottom: 20px;">
|
|
||||||
<h3>Generate API Key</h3>
|
|
||||||
<div style="display:flex; gap: 10px;">
|
|
||||||
<input type="text" id="api-label" placeholder="Key Label (e.g., Production)">
|
|
||||||
<button class="btn-glow" onclick="generateApiKey()">GENERATE</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead><tr><th>Label</th><th>API Key</th></tr></thead>
|
|
||||||
<tbody id="api-keys-list"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> โ UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<footer>
|
||||||
<!-- Floating Action Button for Mobile -->
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
<div class="fab" onclick="enterMobileMonitor()">๐ฏ๏ธ</div>
|
</footer>
|
||||||
|
|
||||||
<div id="mobile-monitor">
|
|
||||||
<div class="mm-header">
|
|
||||||
<div style="display:flex; align-items:center;">
|
|
||||||
<div id="mm-status-dot" style="width:8px; height:8px; border-radius:50%; background:#555; margin-right:8px;"></div>
|
|
||||||
<span style="font-weight:bold; font-size:0.8rem; color:#fff; font-family:'JetBrains Mono'; letter-spacing:1px;">MONITOR</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; gap:5px;">
|
|
||||||
<button onclick="toggleFullscreen()" style="background:#1a1a1a; color:#888; border:1px solid #333; padding:4px 8px; font-size:0.5rem; border-radius:4px;">FS</button>
|
|
||||||
<button onclick="exitMobileMonitor()" style="background:#300; color:#f55; border:1px solid #500; padding:4px 8px; font-size:0.5rem; border-radius:4px;">EXIT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="mm-crypto-grid" class="mm-grid"></div>
|
|
||||||
<div style="background:#111; padding:6px 10px; border-radius:6px; margin-top:4px; display:flex; justify-content:space-between; align-items:center;">
|
|
||||||
<div id="mm-clock" style="font-family:'JetBrains Mono'; font-size:0.6rem; color:var(--crypto-blue)">00:00:00</div>
|
|
||||||
<div id="mm-health-pct" style="font-family:'JetBrains Mono'; font-size:0.6rem; color:var(--crypto-green)">100% HEALTH</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
let audioAlertError = new Audio('https://assets.mixkit.co/active_storage/sfx/995/995-preview.mp3'); // Sirene / Alert
|
|
||||||
let audioAlertRun = new Audio('https://assets.mixkit.co/active_storage/sfx/1435/1435-preview.mp3'); // Chime / Success
|
|
||||||
let previousStatuses = {};
|
|
||||||
|
|
||||||
if ("Notification" in window && Notification.permission !== "granted" && Notification.permission !== "denied") {
|
|
||||||
Notification.requestPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkNotifications(assets) {
|
|
||||||
for (const u of assets) {
|
|
||||||
// First run: just set the status so it doesn't alert immediately
|
|
||||||
if (!previousStatuses.hasOwnProperty(u.id)) {
|
|
||||||
previousStatuses[u.id] = u.last_status;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousStatuses[u.id] !== u.last_status) {
|
|
||||||
if (u.last_status === 'error') {
|
|
||||||
if ("Notification" in window && Notification.permission === "granted") {
|
|
||||||
new Notification("๐จ ALARM: MONITOR ERROR!", {
|
|
||||||
body: `[ DOWN ] ${u.url} is offline/error!`,
|
|
||||||
icon: 'https://cdn-icons-png.flaticon.com/512/190/190406.png'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
audioAlertError.play().catch(e => console.log('Audio error failed', e));
|
|
||||||
} else if (u.last_status === 'ok') {
|
|
||||||
if ("Notification" in window && Notification.permission === "granted") {
|
|
||||||
new Notification("โ
INFO: MONITOR RUN!", {
|
|
||||||
body: `[ RUN ] ${u.url} is back online!`,
|
|
||||||
icon: 'https://cdn-icons-png.flaticon.com/512/190/190411.png'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
audioAlertRun.play().catch(e => console.log('Audio run failed', e));
|
|
||||||
}
|
|
||||||
previousStatuses[u.id] = u.last_status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchTeams() {
|
|
||||||
const resp = await fetch('api/uptime.php?action=members');
|
|
||||||
const members = await resp.json();
|
|
||||||
const tbody = document.getElementById('team-list');
|
|
||||||
if (tbody) {
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
members.forEach(m => {
|
|
||||||
tbody.innerHTML += `<tr><td>${m.email}</td><td><span class="status-badge ${m.status==='active'?'bg-ok':'bg-err'}">${m.status}</span></td></tr>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function inviteMember() {
|
|
||||||
const email = document.getElementById('invite-email').value;
|
|
||||||
if (!email) return;
|
|
||||||
await fetch('api/uptime.php?action=invite', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ email })
|
|
||||||
});
|
|
||||||
document.getElementById('invite-email').value = '';
|
|
||||||
fetchTeams();
|
|
||||||
alert('Invitation sent via Email!');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchApiKeys() {
|
|
||||||
const resp = await fetch('api/uptime.php?action=keys');
|
|
||||||
const keys = await resp.json();
|
|
||||||
const tbody = document.getElementById('api-keys-list');
|
|
||||||
if (tbody) {
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
keys.forEach(k => {
|
|
||||||
tbody.innerHTML += `<tr><td>${k.label}</td><td style="font-family: monospace;">${k.api_key}</td></tr>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateApiKey() {
|
|
||||||
const label = document.getElementById('api-label').value;
|
|
||||||
await fetch('api/uptime.php?action=generate_key', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ label: label || 'New Key' })
|
|
||||||
});
|
|
||||||
document.getElementById('api-label').value = '';
|
|
||||||
fetchApiKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
let isMonitoring = false;
|
|
||||||
const sparklines = {};
|
|
||||||
const uptimeHistory = {};
|
|
||||||
let lastDataTime = 0;
|
|
||||||
|
|
||||||
class CryptoSparkline {
|
|
||||||
constructor(canvasId, initialLogs = []) {
|
|
||||||
this.canvas = document.getElementById(canvasId);
|
|
||||||
if (!this.canvas) return;
|
|
||||||
this.ctx = this.canvas.getContext('2d');
|
|
||||||
this.data = [];
|
|
||||||
this.maxPoints = 60;
|
|
||||||
|
|
||||||
if (initialLogs && initialLogs.length > 0) {
|
|
||||||
[...initialLogs].reverse().forEach(log => {
|
|
||||||
this.data.push({ val: parseFloat(log.latency), ok: log.status === 'ok' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
for(let i=0; i<this.maxPoints; i++) {
|
|
||||||
this.data.push({ val: 20 + Math.random() * 20, ok: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resize();
|
|
||||||
window.addEventListener('resize', () => this.resize());
|
|
||||||
setTimeout(() => this.resize(), 100);
|
|
||||||
|
|
||||||
this.animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
animate() {
|
|
||||||
this.draw();
|
|
||||||
requestAnimationFrame(() => this.animate());
|
|
||||||
}
|
|
||||||
|
|
||||||
resize() {
|
|
||||||
if (!this.canvas) return;
|
|
||||||
const rect = this.canvas.parentElement.getBoundingClientRect();
|
|
||||||
if (rect.width <= 0) return;
|
|
||||||
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
this.canvas.width = rect.width * dpr;
|
|
||||||
this.canvas.height = rect.height * dpr;
|
|
||||||
this.ctx.scale(dpr, dpr);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(val, status) {
|
|
||||||
// Add subtle jitter to make it look "live" every second
|
|
||||||
const jitter = (Math.random() - 0.5) * 1.5;
|
|
||||||
const finalVal = Math.max(0, val + (isMonitoring ? jitter : 0));
|
|
||||||
|
|
||||||
this.data.push({ val: finalVal, ok: status === 'ok' });
|
|
||||||
if (this.data.length > this.maxPoints) this.data.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
draw() {
|
|
||||||
if (!this.canvas) return;
|
|
||||||
const w = this.canvas.width / (window.devicePixelRatio || 1);
|
|
||||||
const h = this.canvas.height / (window.devicePixelRatio || 1);
|
|
||||||
if (w <= 0 || h <= 0) return;
|
|
||||||
|
|
||||||
this.ctx.clearRect(0, 0, w, h);
|
|
||||||
|
|
||||||
// Draw Perspective Grid
|
|
||||||
this.ctx.strokeStyle = '#0d2218';
|
|
||||||
this.ctx.lineWidth = 0.5;
|
|
||||||
|
|
||||||
for(let i=0; i<10; i++) {
|
|
||||||
const y = h - (i * (h/10));
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(0, y);
|
|
||||||
this.ctx.lineTo(w, y);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = (Date.now() / 40) % 30;
|
|
||||||
for(let i=-30; i<w; i+=30) {
|
|
||||||
const x = i + offset;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(x, 0);
|
|
||||||
this.ctx.lineTo(x, h);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.data.length < 2) return;
|
|
||||||
|
|
||||||
const allVals = this.data.map(d => d.val);
|
|
||||||
let minV = Math.min(...allVals);
|
|
||||||
let maxV = Math.max(...allVals);
|
|
||||||
const padding = (maxV - minV) * 0.3 || 10;
|
|
||||||
minV = Math.max(0, minV - padding);
|
|
||||||
maxV = maxV + padding;
|
|
||||||
const range = maxV - minV;
|
|
||||||
|
|
||||||
const step = w / (this.data.length - 1);
|
|
||||||
|
|
||||||
// Draw Area
|
|
||||||
const gradient = this.ctx.createLinearGradient(0, 0, 0, h);
|
|
||||||
gradient.addColorStop(0, 'rgba(0, 255, 136, 0.3)');
|
|
||||||
gradient.addColorStop(1, 'rgba(0, 255, 136, 0)');
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(0, h);
|
|
||||||
this.data.forEach((d, i) => {
|
|
||||||
const x = i * step;
|
|
||||||
const y = h - ((d.val - minV) / range * h);
|
|
||||||
this.ctx.lineTo(x, y);
|
|
||||||
});
|
|
||||||
this.ctx.lineTo(w, h);
|
|
||||||
this.ctx.closePath();
|
|
||||||
this.ctx.fillStyle = gradient;
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
// Draw Top Line
|
|
||||||
this.ctx.shadowBlur = 12;
|
|
||||||
this.ctx.shadowColor = '#00ff88';
|
|
||||||
this.ctx.strokeStyle = '#00ff88';
|
|
||||||
this.ctx.lineWidth = 2.5;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.data.forEach((d, i) => {
|
|
||||||
const x = i * step;
|
|
||||||
const y = h - ((d.val - minV) / range * h);
|
|
||||||
if (i === 0) this.ctx.moveTo(x, y);
|
|
||||||
else this.ctx.lineTo(x, y);
|
|
||||||
});
|
|
||||||
this.ctx.stroke();
|
|
||||||
this.ctx.shadowBlur = 0;
|
|
||||||
|
|
||||||
const lastPoint = this.data[this.data.length - 1];
|
|
||||||
const lx = w;
|
|
||||||
const ly = h - ((lastPoint.val - minV) / range * h);
|
|
||||||
|
|
||||||
const pulse = (Math.sin(Date.now() / 150) + 1) / 2;
|
|
||||||
this.ctx.fillStyle = '#00ff88';
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(lx, ly, 4 + (pulse * 4), 0, Math.PI * 2);
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
const beamGrad = this.ctx.createLinearGradient(0, 0, 0, h);
|
|
||||||
beamGrad.addColorStop(0, 'rgba(0, 255, 136, 0)');
|
|
||||||
beamGrad.addColorStop(0.5, 'rgba(0, 255, 136, 0.25)');
|
|
||||||
beamGrad.addColorStop(1, 'rgba(0, 255, 136, 0)');
|
|
||||||
this.ctx.fillStyle = beamGrad;
|
|
||||||
this.ctx.fillRect(lx - 20, 0, 40, h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function showSection(id) {
|
|
||||||
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
|
||||||
const sec = document.getElementById(id);
|
|
||||||
if (sec) sec.classList.add('active');
|
|
||||||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
||||||
|
|
||||||
if (id === 'teams') fetchTeams();
|
|
||||||
if (id === 'api') fetchApiKeys();
|
|
||||||
if (id === 'telegram') fetchTelegram();
|
|
||||||
|
|
||||||
refreshData();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchTelegram() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('api/uptime.php?action=get_telegram');
|
|
||||||
const data = await res.json();
|
|
||||||
document.getElementById('tg_token').value = data.telegram_token || '';
|
|
||||||
document.getElementById('tg_chat_id').value = data.telegram_chat_id || '';
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveTelegram() {
|
|
||||||
const token = document.getElementById('tg_token').value;
|
|
||||||
const chatId = document.getElementById('tg_chat_id').value;
|
|
||||||
try {
|
|
||||||
const res = await fetch('api/uptime.php?action=save_telegram', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({ telegram_token: token, telegram_chat_id: chatId })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert('Telegram Settings Saved! Handphone kamu akan menerima notifikasi 24/7 sekarang.');
|
|
||||||
} else {
|
|
||||||
alert('Failed to save.');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
alert('Error saving settings.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
const time = new Date().toISOString().split('T')[1].split('.')[0];
|
|
||||||
document.getElementById('clock').innerText = time + ' UTC';
|
|
||||||
document.getElementById('mm-clock').innerText = time + ' UTC';
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
function toggleFullscreen() {
|
|
||||||
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
|
|
||||||
else document.exitFullscreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
function enterMobileMonitor() {
|
|
||||||
document.getElementById('mobile-monitor').style.display = 'flex';
|
|
||||||
Object.values(sparklines).forEach(s => s.resize());
|
|
||||||
refreshData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitMobileMonitor() {
|
|
||||||
document.getElementById('mobile-monitor').style.display = 'none';
|
|
||||||
if (document.fullscreenElement) document.exitFullscreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const resp = await fetch('api/uptime.php?action=settings&v=<?php echo $v; ?>');
|
|
||||||
const data = await resp.json();
|
|
||||||
isMonitoring = data.monitoring_enabled;
|
|
||||||
|
|
||||||
// Auto-login from email invite bypasses login screen
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (urlParams.has('invite_email')) {
|
|
||||||
localStorage.setItem('userEmail', urlParams.get('invite_email'));
|
|
||||||
window.history.replaceState({}, document.title, "/");
|
|
||||||
alert('Welcome! You are now logged in as ' + localStorage.getItem('userEmail'));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatusUI();
|
|
||||||
refreshData();
|
|
||||||
|
|
||||||
// REFRESH UI DATA EVERY 1 SECOND
|
|
||||||
setInterval(refreshData, 1000);
|
|
||||||
|
|
||||||
// RUN BACKEND CHECK EVERY 2 SECONDS
|
|
||||||
setInterval(runPulse, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStatusUI() {
|
|
||||||
const btn = document.getElementById('global-toggle');
|
|
||||||
const ind = document.getElementById('live-indicator');
|
|
||||||
const mmDot = document.getElementById('mm-status-dot');
|
|
||||||
const color = isMonitoring ? 'var(--crypto-green)' : '#555';
|
|
||||||
btn.innerText = isMonitoring ? 'STATION: ONLINE' : 'STATION: OFFLINE';
|
|
||||||
btn.style.background = isMonitoring ? 'var(--crypto-green)' : '#2b3139';
|
|
||||||
btn.style.color = isMonitoring ? '#000' : '#fff';
|
|
||||||
ind.style.background = color;
|
|
||||||
ind.style.boxShadow = isMonitoring ? '0 0 10px ' + color : 'none';
|
|
||||||
mmDot.style.background = color;
|
|
||||||
mmDot.style.boxShadow = isMonitoring ? '0 0 8px ' + color : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleMonitoring() {
|
|
||||||
isMonitoring = !isMonitoring;
|
|
||||||
await fetch('api/uptime.php?action=toggle', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ enabled: isMonitoring })
|
|
||||||
});
|
|
||||||
updateStatusUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBattery(id, status) {
|
|
||||||
if (!uptimeHistory[id]) uptimeHistory[id] = Array(30).fill('none');
|
|
||||||
uptimeHistory[id].push(status);
|
|
||||||
if (uptimeHistory[id].length > 30) uptimeHistory[id].shift();
|
|
||||||
|
|
||||||
const containers = [
|
|
||||||
document.getElementById(`dash-battery-${id}`),
|
|
||||||
document.getElementById(`mm-battery-${id}`)
|
|
||||||
];
|
|
||||||
|
|
||||||
containers.forEach(container => {
|
|
||||||
if (!container) return;
|
|
||||||
container.innerHTML = '';
|
|
||||||
uptimeHistory[id].forEach(s => {
|
|
||||||
const seg = document.createElement('div');
|
|
||||||
seg.className = 'battery-seg ' + (s === 'ok' ? 'seg-ok' : (s === 'err' ? 'seg-err' : 'seg-none'));
|
|
||||||
container.appendChild(seg);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshData() {
|
|
||||||
try {
|
|
||||||
const statResp = await fetch('api/uptime.php?action=stats&v=<?php echo $v; ?>');
|
|
||||||
const stats = await statResp.json();
|
|
||||||
document.getElementById('stat-total').innerText = stats.total + ' Assets';
|
|
||||||
document.getElementById('stat-latency').innerText = Math.round(stats.avg_latency) + 'ms';
|
|
||||||
const health = stats.total > 0 ? ((stats.up / stats.total) * 100).toFixed(1) : 100;
|
|
||||||
document.getElementById('stat-health').innerText = health + '%';
|
|
||||||
document.getElementById('stat-health').style.color = health < 99 ? 'var(--crypto-red)' : 'var(--crypto-green)';
|
|
||||||
document.getElementById('mm-health-pct').innerText = health + '% HEALTH';
|
|
||||||
|
|
||||||
const listResp = await fetch('api/uptime.php?action=list&v=<?php echo $v; ?>');
|
|
||||||
const assets = await listResp.json();
|
|
||||||
|
|
||||||
const inventory = document.getElementById('inventory-list');
|
|
||||||
const mmCryptoGrid = document.getElementById('mm-crypto-grid');
|
|
||||||
const dashCandleGrid = document.getElementById('dashboard-candle-grid');
|
|
||||||
const dashBatteryGrid = document.getElementById('dashboard-battery-grid');
|
|
||||||
|
|
||||||
if (inventory) inventory.innerHTML = '';
|
|
||||||
|
|
||||||
for (const u of assets) {
|
|
||||||
const isOk = u.last_status === 'ok';
|
|
||||||
if (inventory) {
|
|
||||||
inventory.innerHTML += `<tr><td>${u.url}</td><td><span class="status-badge ${isOk?'bg-ok':'bg-err'}">${u.last_status}</span></td><td>${u.last_latency}ms</td><td><button onclick="deleteAsset(${u.id})" style="color:var(--crypto-red); background:none; padding:0;">DEL</button></td></tr>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!document.getElementById(`dash-battery-row-${u.id}`)) {
|
|
||||||
const brow = document.createElement('div');
|
|
||||||
brow.id = `dash-battery-row-${u.id}`;
|
|
||||||
brow.className = 'card';
|
|
||||||
brow.style.padding = '12px';
|
|
||||||
brow.innerHTML = `
|
|
||||||
<div class="mm-info" style="margin-bottom:8px;">
|
|
||||||
<span class="mm-url-text" style="font-size:0.75rem;">${u.url.replace('https://','').replace('http://','')}</span>
|
|
||||||
<span class="status-badge ${isOk?'bg-ok':'bg-err'}" style="font-size:0.6rem;">${u.last_status}</span>
|
|
||||||
</div>
|
|
||||||
<div id="dash-battery-${u.id}" class="uptime-battery"></div>
|
|
||||||
`;
|
|
||||||
dashBatteryGrid.appendChild(brow);
|
|
||||||
} else {
|
|
||||||
const badge = document.getElementById(`dash-battery-row-${u.id}`).querySelector('.status-badge');
|
|
||||||
if (badge) {
|
|
||||||
badge.innerText = u.last_status;
|
|
||||||
badge.className = `status-badge ${isOk?'bg-ok':'bg-err'}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!document.getElementById(`dash-candle-row-${u.id}`)) {
|
|
||||||
const crow = document.createElement('div');
|
|
||||||
crow.id = `dash-candle-row-${u.id}`;
|
|
||||||
crow.className = 'mm-row';
|
|
||||||
crow.innerHTML = `
|
|
||||||
<div class="mm-info">
|
|
||||||
<span class="mm-url-text">${u.url.replace('https://','').replace('http://','')}</span>
|
|
||||||
<span id="dash-lat-${u.id}" class="mm-lat-text" style="color:${isOk?'var(--crypto-green)':'var(--crypto-red)'}">${u.last_latency}ms</span>
|
|
||||||
</div>
|
|
||||||
<div class="mm-spark-container">
|
|
||||||
<canvas id="dash-canvas-${u.id}" class="mm-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
dashCandleGrid.appendChild(crow);
|
|
||||||
|
|
||||||
const logResp = await fetch(`api/uptime.php?action=logs&url_id=${u.id}&v=<?php echo $v; ?>`);
|
|
||||||
const logs = await logResp.json();
|
|
||||||
sparklines[`dash-${u.id}`] = new CryptoSparkline(`dash-canvas-${u.id}`, logs);
|
|
||||||
} else {
|
|
||||||
const latEl = document.getElementById(`dash-lat-${u.id}`);
|
|
||||||
if (latEl) {
|
|
||||||
latEl.innerText = u.last_latency + 'ms';
|
|
||||||
latEl.style.color = isOk ? 'var(--crypto-green)' : 'var(--crypto-red)';
|
|
||||||
}
|
|
||||||
if (sparklines[`dash-${u.id}`]) sparklines[`dash-${u.id}`].update(u.last_latency, u.last_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!document.getElementById(`mm-row-${u.id}`)) {
|
|
||||||
const mrow = document.createElement('div');
|
|
||||||
mrow.id = `mm-row-${u.id}`;
|
|
||||||
mrow.className = 'mm-row';
|
|
||||||
mrow.innerHTML = `
|
|
||||||
<div class="mm-info">
|
|
||||||
<span class="mm-url-text">${u.url.replace('https://','').replace('http://','')}</span>
|
|
||||||
<span id="mm-lat-${u.id}" class="mm-lat-text" style="color:${isOk?'var(--crypto-green)':'var(--crypto-red)'}">${u.last_latency}ms</span>
|
|
||||||
</div>
|
|
||||||
<div id="mm-battery-${u.id}" class="uptime-battery"></div>
|
|
||||||
<div class="mm-spark-container">
|
|
||||||
<canvas id="mm-canvas-${u.id}" class="mm-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
mmCryptoGrid.appendChild(mrow);
|
|
||||||
|
|
||||||
const logResp = await fetch(`api/uptime.php?action=logs&url_id=${u.id}&v=<?php echo $v; ?>`);
|
|
||||||
const logs = await logResp.json();
|
|
||||||
sparklines[`mm-${u.id}`] = new CryptoSparkline(`mm-canvas-${u.id}`, logs);
|
|
||||||
} else {
|
|
||||||
const latEl = document.getElementById(`mm-lat-${u.id}`);
|
|
||||||
if (latEl) {
|
|
||||||
latEl.innerText = u.last_latency + 'ms';
|
|
||||||
latEl.style.color = isOk ? 'var(--crypto-green)' : 'var(--crypto-red)';
|
|
||||||
}
|
|
||||||
if (sparklines[`mm-${u.id}`]) sparklines[`mm-${u.id}`].update(u.last_latency, u.last_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBattery(u.id, u.last_status);
|
|
||||||
}
|
|
||||||
checkNotifications(assets);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Data refresh failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runPulse() {
|
|
||||||
if (!isMonitoring) return;
|
|
||||||
await fetch('api/uptime.php?action=check');
|
|
||||||
refreshData();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addAsset() {
|
|
||||||
const url = document.getElementById('new-url').value;
|
|
||||||
if (!url) return;
|
|
||||||
await fetch('api/uptime.php?action=add', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ url })
|
|
||||||
});
|
|
||||||
document.getElementById('new-url').value = '';
|
|
||||||
refreshData();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteAsset(id) {
|
|
||||||
if (!confirm('Delete?')) return;
|
|
||||||
await fetch('api/uptime.php?action=delete', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ id })
|
|
||||||
});
|
|
||||||
refreshData();
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
if ("serviceWorker" in navigator) {
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
navigator.serviceWorker.register("sw.js").then(reg => console.log("SW registered"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "๐๐จ๐ง๐ข๐ญ๐จ๐ซ ๐๐ฉ๐ญ๐ข๐ฆ๐ ๐๐ฒ ๐๐ฎ๐ฆ๐๐",
|
|
||||||
"short_name": "YumeeMonitor",
|
|
||||||
"description": "Real-time 24/7 Uptime & Crypto-style Statistics Monitor",
|
|
||||||
"start_url": "/index.php",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#1e1e2e",
|
|
||||||
"theme_color": "#06d6a0",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "https://placehold.co/192x192/1e1e2e/06d6a0?text=Yumee",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "https://placehold.co/512x512/1e1e2e/06d6a0?text=Yumee",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
23
sw.js
23
sw.js
@ -1,23 +0,0 @@
|
|||||||
const CACHE_NAME = 'uptime-sparkle-v1';
|
|
||||||
const urlsToCache = [
|
|
||||||
'/',
|
|
||||||
'/index.php',
|
|
||||||
'manifest.json'
|
|
||||||
];
|
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
|
||||||
event.waitUntil(
|
|
||||||
caches.open(CACHE_NAME)
|
|
||||||
.then(cache => cache.addAll(urlsToCache))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', event => {
|
|
||||||
event.respondWith(
|
|
||||||
caches.match(event.request)
|
|
||||||
.then(response => {
|
|
||||||
if (response) return response;
|
|
||||||
return fetch(event.request);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Loadingโฆ
x
Reference in New Issue
Block a user