diff --git a/api/uptime.php b/api/uptime.php new file mode 100644 index 0000000..5880446 --- /dev/null +++ b/api/uptime.php @@ -0,0 +1,262 @@ + ($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 = " +
+

Website Down Alert

+

The following website is currently unresponsive:

+

URL: $url

+

Error: $error

+

Time: $time

+
+

This is an automated notification from your Uptime Monitor.

+
+ "; + } elseif ($oldStatus === 'error' && $newStatus === 'ok') { + $subject = "๐ŸŸข RESOLVED: Website Up - $url"; + $html = " +
+

Website Back Online

+

The following website is responding normally again:

+

URL: $url

+

Time: $time

+
+

This is an automated notification from your Uptime Monitor.

+
+ "; + } 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 = "

You have been invited to join a team.

Accept Invitation

"; + 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; +} \ No newline at end of file diff --git a/db/migrations/001_create_tables.sql b/db/migrations/001_create_tables.sql new file mode 100644 index 0000000..fcc2eac --- /dev/null +++ b/db/migrations/001_create_tables.sql @@ -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; diff --git a/db/migrations/002_add_settings.sql b/db/migrations/002_add_settings.sql new file mode 100644 index 0000000..58100eb --- /dev/null +++ b/db/migrations/002_add_settings.sql @@ -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`; diff --git a/db/migrations/003_expand_features.sql b/db/migrations/003_expand_features.sql new file mode 100644 index 0000000..90ddbdf --- /dev/null +++ b/db/migrations/003_expand_features.sql @@ -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; diff --git a/index.php b/index.php index 7205f3d..953f4e7 100644 --- a/index.php +++ b/index.php @@ -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'); ?> - - New Style - - - - - - - - - - - - - - - + + ๐Œ๐จ๐ง๐ข๐ญ๐จ๐ซ ๐”๐ฉ๐ญ๐ข๐ฆ๐ž ๐›๐ฒ ๐˜๐ฎ๐ฆ๐ž๐ž + - + + + + +
-
-

Analyzing your requirements and generating your websiteโ€ฆ

-
- Loadingโ€ฆ +
+
+ [NETWORK HEALTH: 100%] [NODES ACTIVE: 12]
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP โ€” UTC

+
+ + 00:00:00 +
+
+ +
+
+
+
+ Total Assets + 0 +
+
+ Avg Latency + 0ms +
+
+ Health + 100% +
+
+ +
+

Asset Status (Battery View)

+
+
+ +
+

Latency Graph

+ +
+
+ +
+
+

Add Asset

+
+ + +
+
+
+ + + +
URLStatusLatencyActions
+
+
+ +

Team Management

Manage your monitoring teams here.

+

API Keys

Generate keys for external integrations.

- + + +
+
+
+
+ REALTIME MONITOR +
+
+ + +
+
+
+
+
00:00:00
+
100% HEALTH
+
+
+ + - + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..4d496ad --- /dev/null +++ b/manifest.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..9ea1342 --- /dev/null +++ b/sw.js @@ -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); + }) + ); +});