Autosave: 20260225-000344

This commit is contained in:
Flatlogic Bot 2026-02-25 00:03:44 +00:00
parent 79c3e954d4
commit 65a6c22962
7 changed files with 924 additions and 111 deletions

262
api/uptime.php Normal file
View File

@ -0,0 +1,262 @@
<?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) {
$recipient = 'yumeecute@aol.com';
$time = date('Y-m-d H:i:s');
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 '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']."'>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;
}

View File

@ -0,0 +1,19 @@
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;

View File

@ -0,0 +1,6 @@
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`;

View File

@ -0,0 +1,44 @@
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;

660
index.php
View File

@ -3,148 +3,586 @@ declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?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; ?>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>๐Œ๐จ๐ง๐ข๐ญ๐จ๐ซ ๐”๐ฉ๐ญ๐ข๐ฆ๐ž ๐›๐ฒ ๐˜๐ฎ๐ฆ๐ž๐ž</title>
<link rel="manifest" href="manifest.json">
<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;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
--crypto-green: #00ff88;
--crypto-red: #ff3366;
--crypto-blue: #00d4ff;
--dark-bg: #0b0e11;
--dark-card: #181a20;
--dark-sidebar: #1e2329;
--text-main: #eaecef;
--text-dim: #848e9c;
--neon-glow: 0 0 10px rgba(0, 255, 136, 0.5);
--border: #2b3139;
}
* { box-sizing: border-box; }
body {
margin: 0;
margin: 0; padding: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
background: var(--dark-bg);
color: var(--text-main);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
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;
align-items: center;
gap: 12px;
}
.nav-item:hover, .nav-item.active {
color: #fff;
background: rgba(255,255,255,0.05);
border-left: 3px solid var(--crypto-blue);
}
/* Main Content */
main {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.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; }
/* Cards & Components */
.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;
}
body::before {
content: '';
position: absolute;
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;
.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; }
/* Battery Bars */
.battery-container { display: flex; gap: 2px; height: 20px; margin-top: 10px; }
.battery-bar { flex: 1; background: #2b3139; border-radius: 2px; position: relative; }
.battery-bar.ok { background: var(--crypto-green); box-shadow: 0 0 5px rgba(0, 255, 136, 0.3); }
.battery-bar.err { background: var(--crypto-red); }
/* Mobile Monitor Specific (CRYPTO STYLE) */
#mobile-monitor {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: #000;
z-index: 2000;
display: none;
flex-direction: column;
padding: 5px;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
#mobile-monitor.active { display: flex; }
.mm-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #111;
padding: 8px 12px;
border-radius: 8px;
margin-bottom: 5px;
}
main {
padding: 2rem;
.mm-grid {
display: grid;
grid-template-columns: 1fr;
gap: 5px;
flex: 1;
overflow-y: auto;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
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-row {
background: #080808;
padding: 10px;
border-radius: 8px;
border: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
gap: 5px;
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
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 {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
.mm-url-text { font-size: 0.6rem; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60%; }
.mm-lat-text { font-size: 0.7rem; font-weight: bold; }
.mm-spark-container {
height: 60px;
width: 100%;
position: relative;
background: rgba(255,255,255,0.02);
border-radius: 4px;
overflow: hidden;
}
.hint {
opacity: 0.9;
.mm-canvas {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
/* List Tables */
.data-table { width: 100%; border-collapse: collapse; }
.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); }
/* Inputs & Buttons */
input, select {
background: #1e2329;
border: 1px solid var(--border);
color: #fff;
padding: 10px 12px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
outline: none;
width: 100%;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
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); }
/* Sections */
.section { display: none; }
.section.active { display: block; }
/* Responsive */
@media (max-width: 768px) {
aside { display: none; }
.top-bar { padding: 0 1rem; }
.content-area { padding: 1rem; }
.dashboard-grid { grid-template-columns: 1fr !important; }
}
</style>
</head>
<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>
</ul>
<div style="padding: 1rem; border-top: 1px solid var(--border);">
<button class="btn-glow" style="width:100%; margin-bottom:10px;" onclick="enterMobileMonitor()">๐Ÿ“ฑ CRYPTO MODE</button>
<button id="global-toggle" style="width:100%; font-size: 0.8rem;" onclick="toggleMonitoring()">STATION: OFFLINE</button>
</div>
</aside>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your websiteโ€ฆ</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loadingโ€ฆ</span>
<div class="top-bar">
<div id="ticker" style="font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--crypto-green);">
[NETWORK HEALTH: 100%] [NODES ACTIVE: 12]
</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 style="display:flex; align-items:center; gap: 15px;">
<span id="live-indicator" style="width:10px; height:10px; border-radius:50%; background: #555;"></span>
<span id="clock" style="font-family:'JetBrains Mono'; font-size:0.8rem;">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">
<span class="stat-label">Total Assets</span>
<span class="stat-val" id="stat-total">0</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>
<div class="card" style="margin-bottom: 2rem;">
<h3>Asset Status (Battery View)</h3>
<div id="battery-rows"></div>
</div>
<div class="card">
<h3>Latency Graph</h3>
<canvas id="globalChart" style="height: 200px;"></canvas>
</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"><h3>Team Management</h3><p>Manage your monitoring teams here.</p></div></div>
<div id="api" class="section"><div class="card"><h3>API Keys</h3><p>Generate keys for external integrations.</p></div></div>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<!-- MOBILE MONITOR OVERLAY (CRYPTO STYLE) -->
<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.7rem; color:#fff; font-family:'JetBrains Mono'">REALTIME 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:8px 12px; border-radius:8px; margin-top:5px; display:flex; justify-content:space-between; align-items:center;">
<div id="mm-clock" style="font-family:'JetBrains Mono'; font-size:0.65rem; color:var(--crypto-blue)">00:00:00</div>
<div id="mm-health-pct" style="font-family:'JetBrains Mono'; font-size:0.65rem; color:var(--crypto-green)">100% HEALTH</div>
</div>
</div>
<script>
let isMonitoring = false;
let mainChart = null;
const sparklines = {};
class CryptoSparkline {
constructor(canvasId, initialData = []) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.data = initialData.length ? initialData : Array(30).fill(0);
this.color = '#00ff88';
this.resize();
window.addEventListener('resize', () => this.resize());
}
resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
this.canvas.width = rect.width * window.devicePixelRatio;
this.canvas.height = rect.height * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this.draw();
}
update(val, status) {
this.data.push(val);
if (this.data.length > 50) this.data.shift();
this.color = status === 'ok' ? '#00ff88' : '#ff3366';
this.draw();
}
draw() {
const w = this.canvas.width / window.devicePixelRatio;
const h = this.canvas.height / window.devicePixelRatio;
this.ctx.clearRect(0, 0, w, h);
if (this.data.length < 2) return;
const min = Math.min(...this.data) * 0.8;
const max = Math.max(...this.data) * 1.2 || 100;
const range = max - min;
this.ctx.beginPath();
this.ctx.strokeStyle = this.color;
this.ctx.lineWidth = 2;
this.ctx.lineJoin = 'round';
this.ctx.lineCap = 'round';
const step = w / (this.data.length - 1);
for (let i = 0; i < this.data.length; i++) {
const x = i * step;
const y = h - ((this.data[i] - min) / range * h);
if (i === 0) this.ctx.moveTo(x, y);
else this.ctx.lineTo(x, y);
}
this.ctx.stroke();
// Gradient fill
const fillGrad = this.ctx.createLinearGradient(0, 0, 0, h);
fillGrad.addColorStop(0, this.color + '44');
fillGrad.addColorStop(1, this.color + '00');
this.ctx.lineTo(w, h);
this.ctx.lineTo(0, h);
this.ctx.fillStyle = fillGrad;
this.ctx.fill();
}
}
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'));
refreshData();
}
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';
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');
const data = await resp.json();
isMonitoring = data.monitoring_enabled;
updateStatusUI();
initChart();
refreshData();
setInterval(refreshData, 10000);
setInterval(runPulse, 15000);
}
function initChart() {
const ctx = document.getElementById('globalChart').getContext('2d');
mainChart = new Chart(ctx, {
type: 'line',
data: {
labels: Array(20).fill(''),
datasets: [{
data: Array(20).fill(0),
borderColor: '#00d4ff',
backgroundColor: 'rgba(0, 212, 255, 0.05)',
fill: true, tension: 0.4, pointRadius: 0
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { x: { display: false }, y: { grid: { color: '#2b3139' }, ticks: { color: '#848e9c', font: { size: 9 } } } },
plugins: { legend: { display: false } }
}
});
}
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();
}
async function refreshData() {
const statResp = await fetch('api/uptime.php?action=stats');
const stats = await statResp.json();
document.getElementById('stat-total').innerText = stats.total;
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';
if (mainChart) {
mainChart.data.datasets[0].data.push(stats.avg_latency);
mainChart.data.datasets[0].data.shift();
mainChart.update('none');
}
const listResp = await fetch('api/uptime.php?action=list');
const assets = await listResp.json();
const inventory = document.getElementById('inventory-list');
const batteryRows = document.getElementById('battery-rows');
const mmCryptoGrid = document.getElementById('mm-crypto-grid');
inventory.innerHTML = '';
batteryRows.innerHTML = '';
assets.forEach(u => {
const isOk = u.last_status === 'ok';
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>`;
// Dashboard Battery Rows
const brow = document.createElement('div');
brow.style.marginBottom = '10px';
brow.innerHTML = `<div style="font-size:0.7rem; color:#888; display:flex; justify-content:space-between"><span>${u.url}</span><span>${u.last_latency}ms</span></div>`;
const bcont = document.createElement('div');
bcont.className = 'battery-container';
for(let i=0; i<24; i++) {
const b = document.createElement('div');
b.className = 'battery-bar' + (isOk ? ' ok' : ' err');
bcont.appendChild(b);
}
brow.appendChild(bcont);
batteryRows.appendChild(brow);
// Mobile Crypto Grid
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.split('//')[1]}</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 class="mm-spark-container">
<canvas id="mm-canvas-${u.id}" class="mm-canvas"></canvas>
</div>
`;
mmCryptoGrid.appendChild(mrow);
sparklines[u.id] = new CryptoSparkline(`mm-canvas-${u.id}`);
} 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[u.id]) sparklines[u.id].update(u.last_latency, u.last_status);
}
});
}
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();
</script>
</body>
</html>
</html>

21
manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"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 Normal file
View File

@ -0,0 +1,23 @@
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);
})
);
});