Uptime
This commit is contained in:
parent
f071b853eb
commit
cde29a9feb
86
android-yumee-panels/app/build.gradle.kts
Normal file
86
android-yumee-panels/app/build.gradle.kts
Normal file
@ -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")
|
||||
}
|
||||
54
android-yumee-panels/app/src/main/AndroidManifest.xml
Normal file
54
android-yumee-panels/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".YumeeApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.YumeePanels"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.YumeePanels">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.MonitorService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="health" />
|
||||
|
||||
<receiver
|
||||
android:name=".service.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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<List<Monitor>>
|
||||
|
||||
@Query("SELECT * FROM monitors WHERE isActive = 1")
|
||||
suspend fun getActiveMonitors(): List<Monitor>
|
||||
|
||||
@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<List<MonitorLog>>
|
||||
|
||||
@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
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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<com.yumee.panels.data.local.MonitorLog>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
3
android-yumee-panels/app/src/main/res/values/strings.xml
Normal file
3
android-yumee-panels/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Yumee Panels</string>
|
||||
</resources>
|
||||
6
android-yumee-panels/app/src/main/res/values/themes.xml
Normal file
6
android-yumee-panels/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.YumeePanels" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="android:statusBarColor">#1B263B</item>
|
||||
<item name="android:navigationBarColor">#0D1B2A</item>
|
||||
</style>
|
||||
</resources>
|
||||
6
android-yumee-panels/build.gradle.kts
Normal file
6
android-yumee-panels/build.gradle.kts
Normal file
@ -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
|
||||
}
|
||||
16
android-yumee-panels/settings.gradle.kts
Normal file
16
android-yumee-panels/settings.gradle.kts
Normal file
@ -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")
|
||||
77
api/monitor.php
Normal file
77
api/monitor.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
case 'list':
|
||||
case 'ping_all':
|
||||
try {
|
||||
// Worker status check
|
||||
$stmtStatus = db()->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;
|
||||
}
|
||||
@ -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,<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2" viewBox="0 0 2 2"><circle cx="1" cy="1" r="0.5" fill="white" opacity="0.3"/></svg>');
|
||||
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));
|
||||
}
|
||||
@ -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 = '<div class="col-12 text-center opacity-50 p-5">No URLs added yet. Start by adding one!</div>';
|
||||
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('<div class="health-segment segment-none"></div>');
|
||||
history.forEach((h, idx) => {
|
||||
const isUp = h.status_code >= 200 && h.status_code < 400;
|
||||
const segIdx = segments.length - history.length + idx;
|
||||
segments[segIdx] = `<div class="health-segment ${isUp ? 'segment-up' : 'segment-down'}"></div>`;
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card monitor-card h-100" id="monitor-${m.id}">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title mb-0 text-truncate" style="max-width: 70%; font-weight: 700;">${m.name}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="status-indicator ${getStatusClass(m.last_status)}"></span>
|
||||
<small class="status-text opacity-75">${m.last_status || '---'}</small>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-text small text-muted text-truncate mb-3">${m.url}</p>
|
||||
|
||||
<div class="stats-label mb-1">Health Status (Real-time)</div>
|
||||
<div class="health-bars mb-3">
|
||||
${segments.join('')}
|
||||
</div>
|
||||
|
||||
<div class="sparkline-container mb-3">
|
||||
<canvas class="sparkline-canvas" id="sparkline-${m.id}" width="400" height="100" style="width: 100%; height: 60px;"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3 text-center">
|
||||
<div class="col-6">
|
||||
<div class="stats-label">Latency</div>
|
||||
<div class="stats-value latency-value">${m.last_latency || 0}ms</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stats-label">Live Feed</div>
|
||||
<div class="stats-value"><span class="badge bg-primary pulse-mini">LIVE</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between gap-2 mt-auto">
|
||||
<button class="neon-btn flex-grow-1 ping-btn" data-id="${m.id}">
|
||||
<i class="bi bi-play-fill"></i> Test
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm border-0 opacity-50 delete-btn" data-id="${m.id}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
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('<div class="health-segment segment-none"></div>');
|
||||
history.forEach((h, idx) => {
|
||||
const isUp = h.status_code >= 200 && h.status_code < 400;
|
||||
const segIdx = segments.length - history.length + idx;
|
||||
segments[segIdx] = `<div class="health-segment ${isUp ? 'segment-up' : 'segment-down'}"></div>`;
|
||||
});
|
||||
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 = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
try {
|
||||
// Using list to refresh since worker is active
|
||||
await fetchMonitors('list');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill"></i> 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);
|
||||
});
|
||||
BIN
assets/pasted-20260225-062901-2d735e56.jpg
Normal file
BIN
assets/pasted-20260225-062901-2d735e56.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
247
index.php
247
index.php
@ -1,150 +1,109 @@
|
||||
<?php
|
||||
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>
|
||||
<!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; ?>
|
||||
<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">
|
||||
<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);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.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;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Yumee Panels - Real-time Uptime Monitor</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">
|
||||
<i class="bi bi-activity"></i> Yumee Panels
|
||||
</a>
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<span class="badge rounded-pill bg-dark border border-magenta py-2 px-3">
|
||||
<span class="pulse-dot"></span> REAL-TIME PERDETIK
|
||||
</span>
|
||||
<span id="service-status" class="badge rounded-pill bg-dark border border-cyan py-2 px-3">
|
||||
<i class="bi bi-cpu"></i> Monitoring Service: <span class="status-text">Checking...</span>
|
||||
</span>
|
||||
<button class="neon-btn ms-lg-3" data-bs-toggle="modal" data-bs-target="#addMonitorModal">
|
||||
<i class="bi bi-plus-lg"></i> Add URL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</nav>
|
||||
|
||||
<div class="container my-5">
|
||||
<!-- Summary Cards -->
|
||||
<div class="row mb-5 summary-card">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-4 text-center mb-3">
|
||||
<div class="stats-label">Online Monitors</div>
|
||||
<div class="stats-value text-success" id="summary-online">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-4 text-center mb-3">
|
||||
<div class="stats-label">Offline / Error</div>
|
||||
<div class="stats-value text-danger" id="summary-offline">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-4 text-center mb-3">
|
||||
<div class="stats-label">Total URLs</div>
|
||||
<div class="stats-value text-info" id="summary-total">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monitor List -->
|
||||
<div id="monitor-list" class="row">
|
||||
<!-- JS will populate this -->
|
||||
<div class="col-12 text-center p-5">
|
||||
<div class="spinner-border text-cyan" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Monitor Modal -->
|
||||
<div class="modal fade" id="addMonitorModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark border-cyan text-light">
|
||||
<div class="modal-header border-secondary">
|
||||
<h5 class="modal-title">Monitor New URL</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="add-monitor-form">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Friendly Name</label>
|
||||
<input type="text" id="mon-name" class="form-control" placeholder="e.g. My Website" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="url" id="mon-url" class="form-control" placeholder="https://example.com" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Check Interval</label>
|
||||
<select id="mon-interval" class="form-control">
|
||||
<option value="1">Real-time (1s)</option>
|
||||
<option value="5">5 Minutes</option>
|
||||
<option value="15">15 Minutes</option>
|
||||
</select>
|
||||
<small class="text-muted mt-1 d-block">All URLs are currently monitored per second by global pinger.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="neon-btn">Start Monitoring</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
0
worker.lock
Normal file
0
worker.lock
Normal file
130
worker.php
Normal file
130
worker.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Yumee Panels - Persistent Background Worker (Foreground Service Equivalent)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
// Prevent multiple instances
|
||||
$lockFile = __DIR__ . '/worker.lock';
|
||||
$lock = fopen($lockFile, 'c');
|
||||
if (!flock($lock, LOCK_EX | LOCK_NB)) {
|
||||
die("Worker is already running.\n");
|
||||
}
|
||||
|
||||
function sendTelegramMessage($text) {
|
||||
global $pdo; // Assume db() uses a global or similar, let's fetch from settings
|
||||
try {
|
||||
$stmt = db()->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);
|
||||
}
|
||||
}
|
||||
BIN
yumee_panels_android_source.tar.gz
Normal file
BIN
yumee_panels_android_source.tar.gz
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user