From cde29a9feb7c6e1a140f3a6c599e748d5c042e1c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 25 Feb 2026 06:46:57 +0000 Subject: [PATCH] Uptime --- android-yumee-panels/app/build.gradle.kts | 86 ++++ .../app/src/main/AndroidManifest.xml | 54 +++ .../kotlin/com/yumee/panels/MainActivity.kt | 86 ++++ .../main/kotlin/com/yumee/panels/YumeeApp.kt | 18 + .../com/yumee/panels/data/local/Database.kt | 39 ++ .../com/yumee/panels/data/local/Entities.kt | 29 ++ .../com/yumee/panels/service/BootReceiver.kt | 14 + .../yumee/panels/service/MonitorService.kt | 155 ++++++ .../com/yumee/panels/ui/components/Visuals.kt | 65 +++ .../kotlin/com/yumee/panels/ui/theme/Theme.kt | 37 ++ .../kotlin/com/yumee/panels/ui/theme/Type.kt | 29 ++ .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 6 + android-yumee-panels/build.gradle.kts | 6 + android-yumee-panels/settings.gradle.kts | 16 + api/monitor.php | 77 +++ assets/css/custom.css | 447 +++++++----------- assets/js/main.js | 301 +++++++++++- assets/pasted-20260225-062901-2d735e56.jpg | Bin 0 -> 21447 bytes index.php | 247 ++++------ worker.lock | 0 worker.php | 130 +++++ yumee_panels_android_source.tar.gz | Bin 0 -> 6891 bytes 23 files changed, 1404 insertions(+), 441 deletions(-) create mode 100644 android-yumee-panels/app/build.gradle.kts create mode 100644 android-yumee-panels/app/src/main/AndroidManifest.xml create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/MainActivity.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/YumeeApp.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Database.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Entities.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/BootReceiver.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/MonitorService.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/components/Visuals.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Theme.kt create mode 100644 android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Type.kt create mode 100644 android-yumee-panels/app/src/main/res/values/strings.xml create mode 100644 android-yumee-panels/app/src/main/res/values/themes.xml create mode 100644 android-yumee-panels/build.gradle.kts create mode 100644 android-yumee-panels/settings.gradle.kts create mode 100644 api/monitor.php create mode 100644 assets/pasted-20260225-062901-2d735e56.jpg create mode 100644 worker.lock create mode 100644 worker.php create mode 100644 yumee_panels_android_source.tar.gz diff --git a/android-yumee-panels/app/build.gradle.kts b/android-yumee-panels/app/build.gradle.kts new file mode 100644 index 0000000..3ea124a --- /dev/null +++ b/android-yumee-panels/app/build.gradle.kts @@ -0,0 +1,86 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") +} + +android { + namespace = "com.yumee.panels" + compileSdk = 34 + + defaultConfig { + applicationId = "com.yumee.panels" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + val compose_version = "1.5.4" + val room_version = "2.6.1" + val work_version = "2.9.0" + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2023.10.01")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + + // Room + implementation("androidx.room:room-runtime:$room_version") + implementation("androidx.room:room-ktx:$room_version") + ksp("androidx.room:room-compiler:$room_version") + + // WorkManager + implementation("androidx.work:work-runtime-ktx:$work_version") + + // Network + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.10.1") + + // Lifecycle + implementation("androidx.lifecycle:lifecycle-service:2.7.0") + + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/android-yumee-panels/app/src/main/AndroidManifest.xml b/android-yumee-panels/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0d0f015 --- /dev/null +++ b/android-yumee-panels/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/MainActivity.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/MainActivity.kt new file mode 100644 index 0000000..b8229d4 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/MainActivity.kt @@ -0,0 +1,86 @@ +package com.yumee.panels + +import android.content.* +import android.os.* +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.* +import androidx.compose.ui.unit.dp +import androidx.room.Room +import com.yumee.panels.data.local.* +import com.yumee.panels.service.MonitorService +import com.yumee.panels.ui.theme.* +import com.yumee.panels.ui.components.* +import kotlinx.coroutines.flow.collect + +class MainActivity : ComponentActivity() { + private lateinit var db: YumeeDatabase + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + db = Room.databaseBuilder(applicationContext, YumeeDatabase::class.java, "yumee_db").build() + + setContent { + YumeePanelsTheme { + DashboardScreen(db) { + val intent = Intent(this, MonitorService::class.java) + startForegroundService(intent) + } + } + } + } +} + +@Composable +fun DashboardScreen(db: YumeeDatabase, onStartService: () -> Unit) { + val monitors by db.monitorDao().getAllMonitors().collectAsState(initial = emptyList()) + + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + Column(modifier = Modifier.padding(16.dp)) { + Text("YUMEE PANELS", style = MaterialTheme.typography.headlineLarge, color = NeonWhite) + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onStartService, + colors = ButtonDefaults.buttonColors(containerColor = NeonWhite, contentColor = DarkerBlue) + ) { + Text("START MONITORING SERVICE") + } + + Spacer(modifier = Modifier.height(24.dp)) + + LazyColumn { + items(monitors.size) { index -> + MonitorItem(monitors[index]) + } + } + } + } +} + +@Composable +fun MonitorItem(monitor: Monitor) { + Card( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + colors = CardDefaults.cardColors(containerColor = DarkerBlue) + ) { + Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + BatteryHealthBar( + isOnline = monitor.isOnline, + health = if (monitor.isOnline) 100 else 0, + modifier = Modifier.size(width = 40.dp, height = 80.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text(monitor.name, style = MaterialTheme.typography.titleLarge, color = NeonWhite) + Text(monitor.url, style = MaterialTheme.typography.bodySmall, color = NeonWhite.copy(alpha = 0.7f)) + Text("Status: ${if (monitor.isOnline) "ONLINE" else "OFFLINE"}", color = if (monitor.isOnline) NeonGreen else NeonRed) + Text("Response: ${monitor.lastResponseTime}ms | Code: ${monitor.lastStatusCode}", color = NeonWhite) + } + } + } +} diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/YumeeApp.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/YumeeApp.kt new file mode 100644 index 0000000..7bf35d3 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/YumeeApp.kt @@ -0,0 +1,18 @@ +package com.yumee.panels + +import android.app.Application +import androidx.room.Room +import com.yumee.panels.data.local.YumeeDatabase + +class YumeeApp : Application() { + companion object { + lateinit var database: YumeeDatabase + } + + override fun onCreate() { + super.onCreate() + database = Room.databaseBuilder(this, YumeeDatabase::class.java, "yumee_db") + .fallbackToDestructiveMigration() + .build() + } +} diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Database.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Database.kt new file mode 100644 index 0000000..f1a02b0 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Database.kt @@ -0,0 +1,39 @@ +package com.yumee.panels.data.local + +import androidx.room.* +import kotlinx.coroutines.flow.Flow + +@Dao +interface MonitorDao { + @Query("SELECT * FROM monitors") + fun getAllMonitors(): Flow> + + @Query("SELECT * FROM monitors WHERE isActive = 1") + suspend fun getActiveMonitors(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMonitor(monitor: Monitor): Long + + @Update + suspend fun updateMonitor(monitor: Monitor) + + @Delete + suspend fun deleteMonitor(monitor: Monitor) + + @Query("SELECT * FROM monitor_logs WHERE monitorId = :monitorId ORDER BY timestamp DESC LIMIT 100") + fun getLogsForMonitor(monitorId: Long): Flow> + + @Insert + suspend fun insertLog(log: MonitorLog) + + @Query("SELECT COUNT(*) FROM monitor_logs WHERE monitorId = :monitorId AND isOnline = 1") + suspend fun getOnlineCount(monitorId: Long): Int + + @Query("SELECT COUNT(*) FROM monitor_logs WHERE monitorId = :monitorId") + suspend fun getTotalCount(monitorId: Long): Int +} + +@Database(entities = [Monitor::class, MonitorLog::class], version = 1) +abstract class YumeeDatabase : RoomDatabase() { + abstract fun monitorDao(): MonitorDao +} diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Entities.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Entities.kt new file mode 100644 index 0000000..21a2111 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/data/local/Entities.kt @@ -0,0 +1,29 @@ +package com.yumee.panels.data.local + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "monitors") +data class Monitor( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val name: String, + val url: String, + val intervalSeconds: Int = 30, + val isActive: Boolean = true, + val lastStatusCode: Int = 0, + val lastResponseTime: Long = 0, + val lastChecked: Long = 0, + val isOnline: Boolean = false, + val uptimePercent: Float = 100f +) + +@Entity(tableName = "monitor_logs") +data class MonitorLog( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val monitorId: Long, + val timestamp: Long, + val statusCode: Int, + val responseTime: Long, + val isOnline: Boolean, + val message: String? = null +) diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/BootReceiver.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/BootReceiver.kt new file mode 100644 index 0000000..0092135 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/BootReceiver.kt @@ -0,0 +1,14 @@ +package com.yumee.panels.service + +import android.content.* +import com.yumee.panels.service.MonitorService + +class BootReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED || + intent.action == "android.intent.action.QUICKBOOT_POWERON") { + val serviceIntent = Intent(context, MonitorService::class.java) + context.startForegroundService(serviceIntent) + } + } +} diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/MonitorService.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/MonitorService.kt new file mode 100644 index 0000000..99d21c5 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/service/MonitorService.kt @@ -0,0 +1,155 @@ +package com.yumee.panels.service + +import android.app.* +import android.content.* +import android.net.ConnectivityManager +import android.os.* +import androidx.core.app.NotificationCompat +import androidx.room.Room +import com.yumee.panels.MainActivity +import com.yumee.panels.R +import com.yumee.panels.data.local.* +import kotlinx.coroutines.* +import okhttp3.OkHttpClient +import okhttp3.Request +import java.util.concurrent.TimeUnit + +class MonitorService : Service() { + private val serviceJob = SupervisorJob() + private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + private lateinit var db: YumeeDatabase + private val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() + + private var wakeLock: PowerManager.WakeLock? = null + + companion object { + const val CHANNEL_ID = "monitor_service_channel" + const val NOTIFICATION_ID = 101 + } + + override fun onCreate() { + super.onCreate() + db = Room.databaseBuilder(applicationContext, YumeeDatabase::class.java, "yumee_db").build() + createNotificationChannel() + acquireWakeLock() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val notification = createNotification("Uptime Monitoring Running") + startForeground(NOTIFICATION_ID, notification) + + startMonitoring() + return START_STICKY + } + + private fun startMonitoring() { + serviceScope.launch { + while (isActive) { + val monitors = db.monitorDao().getActiveMonitors() + monitors.forEach { monitor -> + // Simplified: In a real app, track individual interval timers + // Here we check if it's time to ping based on lastChecked + if (System.currentTimeMillis() - monitor.lastChecked >= monitor.intervalSeconds * 1000) { + launch { pingMonitor(monitor) } + } + } + delay(1000) // check every second + } + } + } + + private suspend fun pingMonitor(monitor: Monitor) { + val startTime = System.currentTimeMillis() + var isOnline = false + var statusCode = 0 + var responseTime = 0L + var message: String? = null + + try { + val request = Request.Builder().url(monitor.url).get().build() + client.newCall(request).execute().use { response -> + statusCode = response.code + isOnline = statusCode == 200 + responseTime = System.currentTimeMillis() - startTime + } + } catch (e: Exception) { + message = e.message + isOnline = false + responseTime = System.currentTimeMillis() - startTime + } + + // Update database + val updatedMonitor = monitor.copy( + lastStatusCode = statusCode, + lastResponseTime = responseTime, + lastChecked = System.currentTimeMillis(), + isOnline = isOnline + ) + db.monitorDao().updateMonitor(updatedMonitor) + + db.monitorDao().insertLog(MonitorLog( + monitorId = monitor.id, + timestamp = System.currentTimeMillis(), + statusCode = statusCode, + responseTime = responseTime, + isOnline = isOnline, + message = message + )) + + if (!isOnline) { + sendAlert(monitor.name, "URL is down! Code: $statusCode") + } + } + + private fun sendAlert(name: String, message: String) { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val alertNotification = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) // Placeholder + .setContentTitle("Alert: $name") + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .build() + notificationManager.notify(System.currentTimeMillis().toInt(), alertNotification) + } + + private fun createNotification(text: String): Notification { + val intent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Yumee Panels") + .setContentText(text) + .setSmallIcon(R.drawable.ic_launcher_foreground) // Placeholder + .setOngoing(true) + .setContentIntent(pendingIntent) + .build() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = NotificationChannel( + CHANNEL_ID, "Monitoring Service Channel", + NotificationManager.IMPORTANCE_LOW + ) + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(serviceChannel) + } + } + + private fun acquireWakeLock() { + val powerManager = getSystemService(POWER_SERVICE) as PowerManager + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Yumee::MonitorLock") + wakeLock?.acquire() + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + serviceJob.cancel() + wakeLock?.let { if (it.isHeld) it.release() } + super.onDestroy() + } +} diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/components/Visuals.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/components/Visuals.kt new file mode 100644 index 0000000..210889b --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/components/Visuals.kt @@ -0,0 +1,65 @@ +package com.yumee.panels.ui.components + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.* +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.dp +import com.yumee.panels.ui.theme.* + +@Composable +fun BatteryHealthBar( + isOnline: Boolean, + health: Int, // 0 to 100 + modifier: Modifier = Modifier +) { + val color = if (isOnline) NeonWhite else NeonRed + + Column(modifier = modifier) { + Box( + modifier = Modifier + .width(40.dp) + .height(80.dp) + .border(2.dp, color, RoundedCornerShape(4.dp)) + .padding(4.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(health / 100f) + .align(Alignment.BottomCenter) + .background(color) + ) + } + } +} + +@Composable +fun CandlestickChart( + logs: List, + modifier: Modifier = Modifier +) { + Canvas(modifier = modifier) { + val width = size.width + val height = size.height + val barWidth = width / logs.size.coerceAtLeast(1) + val maxResponseTime = logs.maxOfOrNull { it.responseTime }?.coerceAtLeast(100) ?: 1000 + + logs.forEachIndexed { index, log -> + val color = if (log.isOnline) NeonWhite else NeonRed + val barHeight = (log.responseTime.toFloat() / maxResponseTime) * height + val x = index * barWidth + + drawRect( + color = color, + topLeft = Offset(x + 2, height - barHeight), + size = androidx.compose.ui.geometry.Size(barWidth - 4, barHeight), + alpha = 0.8f + ) + } + } +} diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Theme.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Theme.kt new file mode 100644 index 0000000..b74c655 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Theme.kt @@ -0,0 +1,37 @@ +package com.yumee.panels.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +val DarkPastelBlue = Color(0xFF1B263B) +val DarkerBlue = Color(0xFF0D1B2A) +val NeonWhite = Color(0xFFE0E1DD) +val NeonRed = Color(0xFFFF4D4D) +val NeonGreen = Color(0xFF00FFCC) + +private val DarkColorScheme = darkColorScheme( + primary = NeonWhite, + secondary = NeonWhite, + tertiary = NeonWhite, + background = DarkPastelBlue, + surface = DarkerBlue, + onPrimary = DarkerBlue, + onSecondary = DarkerBlue, + onTertiary = DarkerBlue, + onBackground = NeonWhite, + onSurface = NeonWhite, +) + +@Composable +fun YumeePanelsTheme( + content: @Composable () -> Unit +) { + MaterialTheme( + colorScheme = DarkColorScheme, + typography = Typography, + content = content + ) +} diff --git a/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Type.kt b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Type.kt new file mode 100644 index 0000000..6d8b983 --- /dev/null +++ b/android-yumee-panels/app/src/main/kotlin/com/yumee/panels/ui/theme/Type.kt @@ -0,0 +1,29 @@ +package com.yumee.panels.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + headlineLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + letterSpacing = 0.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + letterSpacing = 0.sp + ) +) diff --git a/android-yumee-panels/app/src/main/res/values/strings.xml b/android-yumee-panels/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..dd1032d --- /dev/null +++ b/android-yumee-panels/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Yumee Panels + diff --git a/android-yumee-panels/app/src/main/res/values/themes.xml b/android-yumee-panels/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..871f1fd --- /dev/null +++ b/android-yumee-panels/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + diff --git a/android-yumee-panels/build.gradle.kts b/android-yumee-panels/build.gradle.kts new file mode 100644 index 0000000..07c50fe --- /dev/null +++ b/android-yumee-panels/build.gradle.kts @@ -0,0 +1,6 @@ +// Root build.gradle.kts +plugins { + id("com.android.application") version "8.2.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false +} diff --git a/android-yumee-panels/settings.gradle.kts b/android-yumee-panels/settings.gradle.kts new file mode 100644 index 0000000..11903cb --- /dev/null +++ b/android-yumee-panels/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "Yumee Panels" +include(":app") diff --git a/api/monitor.php b/api/monitor.php new file mode 100644 index 0000000..91abad6 --- /dev/null +++ b/api/monitor.php @@ -0,0 +1,77 @@ +query("SELECT setting_value FROM settings WHERE setting_key = 'worker_heartbeat'"); + $lastHeartbeat = (int)$stmtStatus->fetchColumn(); + $workerActive = (time() - $lastHeartbeat < 10); + + $stmt = db()->query("SELECT * FROM monitors ORDER BY created_at DESC"); + $monitors = $stmt->fetchAll(); + + foreach ($monitors as &$m) { + // Fetch last 30 logs + $logStmt = db()->prepare("SELECT status_code, latency, checked_at FROM monitor_logs WHERE monitor_id = ? ORDER BY checked_at DESC LIMIT 30"); + $logStmt->execute([$m['id']]); + $m['history'] = array_reverse($logStmt->fetchAll()); + } + + echo json_encode([ + 'success' => true, + 'data' => $monitors, + 'worker_active' => $workerActive + ]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } + break; + + case 'add': + $data = json_decode(file_get_contents('php://input'), true); + $name = $data['name'] ?? ''; + $url = $data['url'] ?? ''; + $interval = (int)($data['interval'] ?? 1); + + if (empty($name) || empty($url)) { + echo json_encode(['success' => false, 'error' => 'Name and URL are required.']); + exit; + } + + try { + $stmt = db()->prepare("INSERT INTO monitors (name, url, interval_min) VALUES (?, ?, ?)"); + $stmt->execute([$name, $url, $interval]); + echo json_encode(['success' => true]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } + break; + + case 'delete': + $id = $_GET['id'] ?? null; + if (!$id) { + echo json_encode(['success' => false, 'error' => 'Missing ID.']); + exit; + } + try { + $stmt = db()->prepare("DELETE FROM monitors WHERE id = ?"); + $stmt->execute([$id]); + // Also cleanup logs + $stmt = db()->prepare("DELETE FROM monitor_logs WHERE monitor_id = ?"); + $stmt->execute([$id]); + echo json_encode(['success' => true]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } + break; + + default: + echo json_encode(['success' => false, 'error' => 'Invalid action.']); + break; +} \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 50e0502..c2c94b7 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,302 +1,207 @@ +:root { + --bg-dark: #05070a; + --panel-bg: rgba(10, 11, 30, 0.7); + --neon-cyan: #00f3ff; + --neon-magenta: #ff00ff; + --text-main: #e0e0e0; + --status-online: #00ff88; + --status-offline: #ff4d4d; + --font-inter: 'Inter', sans-serif; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; + background-color: var(--bg-dark); + color: var(--text-main); + font-family: var(--font-inter); margin: 0; + overflow-x: hidden; min-height: 100vh; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} - -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { +/* Background Stars/Sparkle Effect */ +body::before { + content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; + background: radial-gradient(circle at 50% 50%, rgba(0, 243, 255, 0.05) 0%, transparent 70%), + url('data:image/svg+xml,'); + background-repeat: repeat; + z-index: -1; + animation: twinkle 5s linear infinite; } -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); +@keyframes twinkle { + 0% { opacity: 0.5; } + 50% { opacity: 0.8; } + 100% { opacity: 0.5; } } -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); +.navbar { + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--neon-cyan); + box-shadow: 0 0 15px rgba(0, 243, 255, 0.3); } -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.admin-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.admin-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; +.navbar-brand { + font-weight: 700; + color: var(--neon-cyan) !important; text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; + letter-spacing: 2px; + text-shadow: 0 0 10px var(--neon-cyan); } -.table td { - background: #fff; - padding: 1rem; - border: none; +.card { + background: var(--panel-bg); + border: 1px solid rgba(0, 243, 255, 0.2); + border-radius: 12px; + backdrop-filter: blur(15px); + transition: transform 0.3s ease, box-shadow 0.3s ease; } -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; +.card:hover { + transform: translateY(-5px); + box-shadow: 0 0 20px rgba(0, 243, 255, 0.2); } -.form-group label { - display: block; - margin-bottom: 0.5rem; +.neon-btn { + background: transparent; + border: 1px solid var(--neon-cyan); + color: var(--neon-cyan); + padding: 8px 20px; + border-radius: 50px; + text-transform: uppercase; + font-size: 0.8rem; font-weight: 600; - font-size: 0.9rem; + transition: all 0.3s ease; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.2); } +.neon-btn:hover { + background: var(--neon-cyan); + color: var(--bg-dark); + box-shadow: 0 0 20px var(--neon-cyan); +} + +.monitor-card { + padding: 1.5rem; +} + +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; + box-shadow: 0 0 8px currentColor; +} + +.status-online { color: var(--status-online); background-color: var(--status-online); } +.status-offline { color: var(--status-offline); background-color: var(--status-offline); } +.status-unknown { color: #888; background-color: #888; } + .form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(0, 243, 255, 0.2); + color: white; } .form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); + background: rgba(255, 255, 255, 0.1); + border-color: var(--neon-cyan); + color: white; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.3); +} + +.stats-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--neon-cyan); +} + +.stats-label { + font-size: 0.75rem; + text-transform: uppercase; + opacity: 0.7; +} + +/* Battery Bars */ +.health-bars { + display: flex; + gap: 2px; + height: 24px; + align-items: flex-end; +} + +.health-segment { + flex: 1; + height: 100%; + border-radius: 1px; + min-width: 3px; + transition: all 0.3s ease; +} + +.segment-up { + background: var(--status-online); + box-shadow: 0 0 5px var(--status-online); +} + +.segment-down { + background: var(--status-offline); + box-shadow: 0 0 5px var(--status-offline); +} + +.segment-none { + background: rgba(255, 255, 255, 0.1); +} + +/* Candlestick Realtime Chart */ +.sparkline-container { + width: 100%; + height: 60px; + margin: 15px 0; + position: relative; + border-bottom: 1px solid rgba(0, 243, 255, 0.1); +} + +.sparkline-canvas { + width: 100%; + height: 100%; +} + +.pulse-mini { + animation: pulse-mini 1s infinite alternate; +} + +@keyframes pulse-mini { + from { opacity: 1; transform: scale(1); } + to { opacity: 0.7; transform: scale(0.95); } +} + +.pulse-dot { + display: inline-block; + width: 8px; + height: 8px; + background-color: #ff00ff; + border-radius: 50%; + margin-right: 5px; + box-shadow: 0 0 8px #ff00ff; + animation: pulse-dot 1s infinite; +} + +@keyframes pulse-dot { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.5); opacity: 0.5; } + 100% { transform: scale(1); opacity: 1; } +} + +.neon-glow-green { + filter: drop-shadow(0 0 5px var(--status-online)); +} + +.neon-glow-red { + filter: drop-shadow(0 0 5px var(--status-offline)); } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..22615a1 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,288 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + const monitorList = document.getElementById('monitor-list'); + const addForm = document.getElementById('add-monitor-form'); + const serviceStatus = document.getElementById('service-status'); + let isFetching = false; - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; + const fetchMonitors = async (action = 'list') => { + if (isFetching) return; + isFetching = true; + + try { + const response = await fetch(`api/monitor.php?action=${action}`); + const result = await response.json(); + if (result.success) { + renderMonitors(result.data); + updateSummary(result.data); + updateServiceStatus(result.worker_active); + } + } catch (error) { + console.error('Error fetching monitors:', error); + } finally { + isFetching = false; + } }; - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + const updateServiceStatus = (active) => { + if (!serviceStatus) return; + const text = serviceStatus.querySelector('.status-text'); + if (active) { + serviceStatus.classList.remove('border-danger'); + serviceStatus.classList.add('border-cyan'); + text.innerText = 'Online'; + text.className = 'status-text text-success'; + } else { + serviceStatus.classList.remove('border-cyan'); + serviceStatus.classList.add('border-danger'); + text.innerText = 'Offline (Restarting...)'; + text.className = 'status-text text-danger'; + } + }; - appendMessage(message, 'visitor'); - chatInput.value = ''; + const updateSummary = (monitors) => { + const total = monitors.length; + const online = monitors.filter(m => m.last_status >= 200 && m.last_status < 400).length; + const offline = total - online; + + const onlineEl = document.getElementById('summary-online'); + const offlineEl = document.getElementById('summary-offline'); + const totalEl = document.getElementById('summary-total'); + + if (onlineEl) onlineEl.innerText = online; + if (offlineEl) offlineEl.innerText = offline; + if (totalEl) totalEl.innerText = total; + }; + + const renderMonitors = (monitors) => { + if (monitors.length === 0) { + monitorList.innerHTML = '
No URLs added yet. Start by adding one!
'; + return; + } + + const currentCount = monitorList.querySelectorAll('.monitor-card').length; + + if (currentCount !== monitors.length) { + monitorList.innerHTML = monitors.map(m => createMonitorHTML(m)).join(''); + attachListeners(); + } else { + monitors.forEach(m => updateMonitorDOM(m)); + } + + monitors.forEach(m => { + const canvas = document.getElementById(`sparkline-${m.id}`); + if (canvas) drawSparkline(canvas, m.history || []); + }); + }; + + const createMonitorHTML = (m) => { + const history = m.history || []; + const segments = Array(30).fill('
'); + history.forEach((h, idx) => { + const isUp = h.status_code >= 200 && h.status_code < 400; + const segIdx = segments.length - history.length + idx; + segments[segIdx] = `
`; + }); + + return ` +
+
+
+
${m.name}
+
+ + ${m.last_status || '---'} +
+
+

${m.url}

+ +
Health Status (Real-time)
+
+ ${segments.join('')} +
+ +
+ +
+ +
+
+
Latency
+
${m.last_latency || 0}ms
+
+
+
Live Feed
+
LIVE
+
+
+ +
+ + +
+
+
+ `; + }; + + const updateMonitorDOM = (m) => { + const card = document.getElementById(`monitor-${m.id}`); + if (!card) return; + + const indicator = card.querySelector('.status-indicator'); + indicator.className = `status-indicator ${getStatusClass(m.last_status)}`; + + const statusText = card.querySelector('.status-text'); + statusText.innerText = m.last_status || '---'; + + const latencyVal = card.querySelector('.latency-value'); + latencyVal.innerText = (m.last_latency || 0) + 'ms'; + + const history = m.history || []; + const barContainer = card.querySelector('.health-bars'); + const segments = Array(30).fill('
'); + history.forEach((h, idx) => { + const isUp = h.status_code >= 200 && h.status_code < 400; + const segIdx = segments.length - history.length + idx; + segments[segIdx] = `
`; + }); + barContainer.innerHTML = segments.join(''); + }; + + const getStatusClass = (code) => { + if (!code || code === 0) return 'status-unknown'; + if (code >= 200 && code < 400) return 'status-online'; + return 'status-offline'; + }; + + const drawSparkline = (canvas, data) => { + const ctx = canvas.getContext('2d'); + const w = canvas.width; + const h = canvas.height; + ctx.clearRect(0, 0, w, h); + + if (data.length < 2) return; + + const points = data.length; + const xStep = w / (points - 1); + const latencies = data.map(d => parseInt(d.latency) || 0); + const maxLat = Math.max(...latencies, 500); + + const getColor = (status) => (status >= 200 && status < 400) ? '#00ff88' : '#ff4d4d'; + + ctx.beginPath(); + ctx.moveTo(0, h); + data.forEach((d, idx) => { + const x = idx * xStep; + const val = parseInt(d.latency) || 0; + const y = h - (val / maxLat) * (h * 0.7) - 15; + ctx.lineTo(x, y); + }); + ctx.lineTo(w, h); + const fillGrad = ctx.createLinearGradient(0, 0, 0, h); + fillGrad.addColorStop(0, 'rgba(0, 243, 255, 0.15)'); + fillGrad.addColorStop(1, 'rgba(0, 243, 255, 0)'); + ctx.fillStyle = fillGrad; + ctx.fill(); + + ctx.lineWidth = 3; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + for (let i = 0; i < data.length - 1; i++) { + const d1 = data[i]; + const d2 = data[i + 1]; + const x1 = i * xStep; + const y1 = h - (parseInt(d1.latency) || 0) / maxLat * (h * 0.7) - 15; + const x2 = (i + 1) * xStep; + const y2 = h - (parseInt(d2.latency) || 0) / maxLat * (h * 0.7) - 15; + + const color = getColor(d2.status_code); + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.shadowBlur = 8; + ctx.shadowColor = color; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + }; + + const attachListeners = () => { + document.querySelectorAll('.ping-btn').forEach(btn => { + const newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + newBtn.addEventListener('click', () => pingMonitor(newBtn.dataset.id, newBtn)); + }); + document.querySelectorAll('.delete-btn').forEach(btn => { + const newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + newBtn.addEventListener('click', () => deleteMonitor(newBtn.dataset.id)); + }); + }; + + const pingMonitor = async (id, btn) => { + btn.disabled = true; + btn.innerHTML = ''; + try { + // Using list to refresh since worker is active + await fetchMonitors('list'); + } finally { + setTimeout(() => { + btn.disabled = false; + btn.innerHTML = ' Test'; + }, 500); + } + }; + + const deleteMonitor = async (id) => { + if (!confirm('Are you sure you want to delete this monitor?')) return; + try { + const response = await fetch(`api/monitor.php?action=delete&id=${id}`); + const result = await response.json(); + if (result.success) { + fetchMonitors('list'); + } + } catch (error) { + console.error('Delete error:', error); + } + }; + + addForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const submitBtn = addForm.querySelector('button[type="submit"]'); + submitBtn.disabled = true; + + const formData = { + name: document.getElementById('mon-name').value, + url: document.getElementById('mon-url').value, + interval: document.getElementById('mon-interval').value + }; try { - const response = await fetch('api/chat.php', { + const response = await fetch('api/monitor.php?action=add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) + body: JSON.stringify(formData) }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); + const result = await response.json(); + if (result.success) { + addForm.reset(); + bootstrap.Modal.getInstance(document.getElementById('addMonitorModal')).hide(); + fetchMonitors('list'); + } else { + alert('Error adding monitor: ' + result.error); + } } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); + console.error('Add error:', error); + } finally { + submitBtn.disabled = false; } }); -}); + + fetchMonitors('list'); + setInterval(() => fetchMonitors('ping_all'), 1000); +}); \ No newline at end of file diff --git a/assets/pasted-20260225-062901-2d735e56.jpg b/assets/pasted-20260225-062901-2d735e56.jpg new file mode 100644 index 0000000000000000000000000000000000000000..196e944b3f797a212df044bf5404fdb3999cc81e GIT binary patch literal 21447 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 literal 0 HcmV?d00001 diff --git a/index.php b/index.php index 7205f3d..a372d9a 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,109 @@ - - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + Yumee Panels - Real-time Uptime Monitor + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
-
- Page updated: (UTC) -
+ + +
+ +
+
+
+
Online Monitors
+
0
+
+
+
+
+
Offline / Error
+
0
+
+
+
+
+
Total URLs
+
0
+
+
+
+ + +
+ +
+
+ Loading... +
+
+
+
+ + + + + + - + \ No newline at end of file diff --git a/worker.lock b/worker.lock new file mode 100644 index 0000000..e69de29 diff --git a/worker.php b/worker.php new file mode 100644 index 0000000..ba827b2 --- /dev/null +++ b/worker.php @@ -0,0 +1,130 @@ +query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'"); + $token = $stmt->fetchColumn(); + $stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_chat_id'"); + $chatId = $stmt->fetchColumn(); + + if (!$token || !$chatId) return false; + + $url = "https://api.telegram.org/bot$token/sendMessage"; + $data = [ + 'chat_id' => $chatId, + 'text' => "🔔 *Uptime Alert*\n" . $text, + 'parse_mode' => 'Markdown' + ]; + + $options = [ + 'http' => [ + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => http_build_query($data), + ], + ]; + $context = stream_context_create($options); + return file_get_contents($url, false, $context); + } catch (Exception $e) { + return false; + } +} + +echo "Yumee Panels Background Worker Started (v1.1)... +"; + +while (true) { + try { + $start_time = microtime(true); + + // Fetch monitors + $stmt = db()->query("SELECT * FROM monitors"); + $monitors = $stmt->fetchAll(); + + if (!empty($monitors)) { + $mh = curl_multi_init(); + $handles = []; + + foreach ($monitors as $m) { + $ch = curl_init($m['url']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'YumeePinger-Worker/1.1'); + curl_multi_add_handle($mh, $ch); + $handles[$m['id']] = ['ch' => $ch, 'data' => $m]; + } + + $running = null; + do { + curl_multi_exec($mh, $running); + usleep(5000); + } while ($running); + + foreach ($handles as $id => $h) { + $ch = $h['ch']; + $m = $h['data']; + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $latency = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000); + curl_multi_remove_handle($mh, $ch); + curl_close($ch); + + $is_up = ($http_code >= 200 && $http_code < 400); + $prev_status = $m['last_status']; + $prev_notified = $m['last_notified_status']; + + // Update monitor + $upd = db()->prepare("UPDATE monitors SET last_status = ?, last_latency = ? WHERE id = ?"); + $upd->execute([$http_code, $latency, $id]); + + // Log entry + $log = db()->prepare("INSERT INTO monitor_logs (monitor_id, status_code, latency) VALUES (?, ?, ?)"); + $log->execute([$id, $http_code, $latency]); + + // Notification logic: only notify on transition + if ($prev_notified !== null) { + $prev_was_up = ($prev_notified >= 200 && $prev_notified < 400); + if ($is_up !== $prev_was_up) { + $status_str = $is_up ? "✅ UP" : "❌ DOWN"; + $msg = "Monitor *{$m['name']}* is now $status_str\nURL: {$m['url']}\nStatus: $http_code\nLatency: {$latency}ms"; + sendTelegramMessage($msg); + + // Update last notified status + $upd_notify = db()->prepare("UPDATE monitors SET last_notified_status = ? WHERE id = ?"); + $upd_notify->execute([$http_code, $id]); + } + } else { + // Initial notification status set + $upd_notify = db()->prepare("UPDATE monitors SET last_notified_status = ? WHERE id = ?"); + $upd_notify->execute([$http_code, $id]); + } + } + curl_multi_close($mh); + } + + // Record heartbeat in settings for UI to check + db()->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('worker_heartbeat', UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")->execute(); + + $end_time = microtime(true); + $execution_time = ($end_time - $start_time); + $sleep_time = max(0.1, 1 - $execution_time); + usleep($sleep_time * 1000000); + + } catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + sleep(2); + } +} \ No newline at end of file diff --git a/yumee_panels_android_source.tar.gz b/yumee_panels_android_source.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..04c517a79bbb0854f558bba0278028fa63ec8ee0 GIT binary patch literal 6891 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 literal 0 HcmV?d00001