Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f618b155 | ||
|
|
c16e47476d | ||
|
|
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")
|
||||||
|
}
|
||||||
65
android-yumee-panels/app/src/main/AndroidManifest.xml
Normal file
65
android-yumee-panels/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?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: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>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.MonitorWidget"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="Yumee Panels Status">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
<action android:name="com.yumee.panels.UPDATE_WIDGET" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
package com.yumee.panels
|
||||||
|
|
||||||
|
import android.content.*
|
||||||
|
import android.os.*
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.*
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
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.*
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
private lateinit var db: YumeeDatabase
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
db = YumeeDatabase.getDatabase(applicationContext)
|
||||||
|
|
||||||
|
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())
|
||||||
|
var isVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "YUMEE PANELS",
|
||||||
|
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Bold),
|
||||||
|
color = NeonWhite,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "REALTIME DASHBOARD",
|
||||||
|
style = MaterialTheme.typography.labelMedium.copy(letterSpacing = 2.dp),
|
||||||
|
color = NeonWhite.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onStartService,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = NeonWhite, contentColor = DarkPastelBlue),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
.shadow(8.dp, RoundedCornerShape(12.dp), spotColor = NeonWhiteGlow, ambientColor = NeonWhiteGlow)
|
||||||
|
) {
|
||||||
|
Text("START MONITORING", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(bottom = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(monitors.size) { index ->
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isVisible,
|
||||||
|
enter = fadeIn(animationSpec = tween(500, delayMillis = index * 100)) +
|
||||||
|
slideInVertically(initialOffsetY = { 50 }, animationSpec = tween(500, delayMillis = index * 100))
|
||||||
|
) {
|
||||||
|
MonitorItem(monitors[index], db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MonitorItem(monitor: Monitor, db: YumeeDatabase) {
|
||||||
|
val logs by db.monitorDao().getLogsForMonitor(monitor.id).collectAsState(initial = emptyList())
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(6.dp, RoundedCornerShape(16.dp), spotColor = NeonWhiteGlow, ambientColor = NeonWhiteGlow),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PanelBackground)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
BatteryHealthBar(
|
||||||
|
isOnline = monitor.isOnline,
|
||||||
|
health = monitor.uptimePercent.toInt().coerceIn(0, 100),
|
||||||
|
modifier = Modifier.size(width = 44.dp, height = 84.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = monitor.name,
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = NeonWhite
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = monitor.url,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = NeonWhite.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (monitor.isOnline) "ONLINE" else "OFFLINE",
|
||||||
|
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold),
|
||||||
|
color = if (monitor.isOnline) NeonWhite else NeonRed
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${monitor.lastResponseTime} ms",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = NeonWhite.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CandlestickChart if we have logs
|
||||||
|
if (logs.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
Text(
|
||||||
|
text = "RESPONSE HISTORY",
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(letterSpacing = 1.dp),
|
||||||
|
color = NeonWhite.copy(alpha = 0.4f),
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
CandlestickChart(
|
||||||
|
logs = logs.reversed(), // Render chronological since DB query is DESC
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(80.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.yumee.panels
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.yumee.panels.data.local.YumeeDatabase
|
||||||
|
|
||||||
|
class YumeeApp : Application() {
|
||||||
|
companion object {
|
||||||
|
lateinit var database: YumeeDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
database = YumeeDatabase.getDatabase(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.yumee.panels.data.local
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface MonitorDao {
|
||||||
|
@Query("SELECT * FROM monitors")
|
||||||
|
fun getAllMonitors(): Flow<List<Monitor>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM monitors")
|
||||||
|
suspend fun getAllMonitorsSync(): 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
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: YumeeDatabase? = null
|
||||||
|
|
||||||
|
fun getDatabase(context: Context): YumeeDatabase {
|
||||||
|
return INSTANCE ?: synchronized(this) {
|
||||||
|
val instance = Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
YumeeDatabase::class.java,
|
||||||
|
"yumee_database"
|
||||||
|
).build()
|
||||||
|
INSTANCE = instance
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,161 @@
|
|||||||
|
package com.yumee.panels.service
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.*
|
||||||
|
import android.os.*
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
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 = YumeeDatabase.getDatabase(applicationContext)
|
||||||
|
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()
|
||||||
|
var updated = false
|
||||||
|
monitors.forEach { monitor ->
|
||||||
|
if (System.currentTimeMillis() - monitor.lastChecked >= monitor.intervalSeconds * 1000) {
|
||||||
|
pingMonitor(monitor)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
updateWidget()
|
||||||
|
}
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWidget() {
|
||||||
|
val intent = Intent("com.yumee.panels.UPDATE_WIDGET")
|
||||||
|
intent.setPackage(packageName)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.mipmap.ic_launcher)
|
||||||
|
.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.mipmap.ic_launcher)
|
||||||
|
.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,103 @@
|
|||||||
|
package com.yumee.panels.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.yumee.panels.ui.theme.*
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BatteryHealthBar(
|
||||||
|
isOnline: Boolean,
|
||||||
|
health: Int, // 0 to 100
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val targetColor = if (isOnline) NeonWhite else NeonRed
|
||||||
|
val targetGlowColor = if (isOnline) NeonWhiteGlow else NeonRedGlow
|
||||||
|
|
||||||
|
val color by animateColorAsState(targetValue = targetColor, animationSpec = tween(500))
|
||||||
|
val glowColor by animateColorAsState(targetValue = targetGlowColor, animationSpec = tween(500))
|
||||||
|
val animatedHealth by animateFloatAsState(targetValue = health / 100f, animationSpec = tween(800))
|
||||||
|
|
||||||
|
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||||
|
// Outer Glow
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(44.dp)
|
||||||
|
.height(84.dp)
|
||||||
|
.background(glowColor, RoundedCornerShape(8.dp))
|
||||||
|
)
|
||||||
|
// Battery Container
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(40.dp)
|
||||||
|
.height(80.dp)
|
||||||
|
.background(DarkPastelBlue, RoundedCornerShape(6.dp))
|
||||||
|
.border(2.dp, color, RoundedCornerShape(6.dp))
|
||||||
|
.padding(3.dp)
|
||||||
|
) {
|
||||||
|
// Battery Level with smooth animation
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(animatedHealth)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.background(color, RoundedCornerShape(3.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 glowColor = if (log.isOnline) NeonWhiteGlow else NeonRedGlow
|
||||||
|
val barHeight = (log.responseTime.toFloat() / maxResponseTime) * height
|
||||||
|
val x = index * barWidth
|
||||||
|
|
||||||
|
// Draw glow
|
||||||
|
drawRoundRect(
|
||||||
|
color = glowColor,
|
||||||
|
topLeft = Offset(x + 1f, height - barHeight - 4f),
|
||||||
|
size = Size(barWidth - 2f, barHeight + 8f),
|
||||||
|
cornerRadius = CornerRadius(4f, 4f),
|
||||||
|
alpha = 0.5f,
|
||||||
|
style = Fill
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw core
|
||||||
|
drawRoundRect(
|
||||||
|
color = color,
|
||||||
|
topLeft = Offset(x + 2f, height - barHeight),
|
||||||
|
size = Size(barWidth - 4f, barHeight),
|
||||||
|
cornerRadius = CornerRadius(2f, 2f),
|
||||||
|
alpha = 1.0f,
|
||||||
|
style = Fill
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.yumee.panels.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val DarkPastelBlue = Color(0xFF131B2F) // Background biru pastel gelap
|
||||||
|
val PanelBackground = Color(0xFF1B2640) // Background panel sedikit lebih terang
|
||||||
|
val NeonWhite = Color(0xFFFFFFFF) // Putih neon
|
||||||
|
val NeonWhiteGlow = Color(0x66FFFFFF) // Glow putih neon
|
||||||
|
val NeonRed = Color(0xFFFF4D4D) // Merah neon
|
||||||
|
val NeonRedGlow = Color(0x66FF4D4D)
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = NeonWhite,
|
||||||
|
secondary = NeonWhite,
|
||||||
|
tertiary = NeonWhite,
|
||||||
|
background = DarkPastelBlue,
|
||||||
|
surface = PanelBackground,
|
||||||
|
onPrimary = DarkPastelBlue,
|
||||||
|
onSecondary = DarkPastelBlue,
|
||||||
|
onTertiary = DarkPastelBlue,
|
||||||
|
onBackground = NeonWhite,
|
||||||
|
onSurface = NeonWhite,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun YumeePanelsTheme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = DarkColorScheme,
|
||||||
|
// Using default typography, can be customized later
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.yumee.panels.widget
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import com.yumee.panels.MainActivity
|
||||||
|
import com.yumee.panels.R
|
||||||
|
import com.yumee.panels.data.local.YumeeDatabase
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
class MonitorWidget : AppWidgetProvider() {
|
||||||
|
|
||||||
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
|
for (appWidgetId in appWidgetIds) {
|
||||||
|
updateAppWidget(context, appWidgetManager, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
if (intent.action == "com.yumee.panels.UPDATE_WIDGET") {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, MonitorWidget::class.java))
|
||||||
|
for (appWidgetId in appWidgetIds) {
|
||||||
|
updateAppWidget(context, appWidgetManager, appWidgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.widget_layout)
|
||||||
|
|
||||||
|
// Launch app on click
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_title, pendingIntent)
|
||||||
|
|
||||||
|
// Fetch counts from DB
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val db = YumeeDatabase.getDatabase(context)
|
||||||
|
val allMonitors = db.monitorDao().getAllMonitorsSync()
|
||||||
|
val onlineCount = allMonitors.count { it.isOnline }
|
||||||
|
val offlineCount = allMonitors.count { !it.isOnline }
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
views.setTextViewText(R.id.widget_online_count, "Online: $onlineCount")
|
||||||
|
views.setTextViewText(R.id.widget_offline_count, "Offline: $offlineCount")
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#131B2F" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
<stroke android:width="2dp" android:color="#FFFFFF" />
|
||||||
|
</shape>
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/widget_background"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="YUMEE PANELS"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:shadowColor="#66FFFFFF"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="10" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_online_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Online: 0"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:shadowColor="#66FFFFFF"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="10" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_offline_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Offline: 0"
|
||||||
|
android:textColor="#FF4D4D"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:shadowColor="#66FF4D4D"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="10" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widget_refresh"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:src="@android:drawable/stat_notify_sync"
|
||||||
|
android:contentDescription="Refresh"
|
||||||
|
android:tint="#FFFFFF" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
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>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:minWidth="110dp"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:updatePeriodMillis="1800000"
|
||||||
|
android:initialLayout="@layout/widget_layout"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:widgetCategory="home_screen">
|
||||||
|
</appwidget-provider>
|
||||||
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 {
|
body {
|
||||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
background-color: var(--bg-dark);
|
||||||
background-size: 400% 400%;
|
color: var(--text-main);
|
||||||
animation: gradient 15s ease infinite;
|
font-family: var(--font-inter);
|
||||||
color: #212529;
|
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
/* Background Stars/Sparkle Effect */
|
||||||
display: flex;
|
body::before {
|
||||||
align-items: center;
|
content: '';
|
||||||
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 {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 0;
|
background: radial-gradient(circle at 50% 50%, rgba(0, 243, 255, 0.05) 0%, transparent 70%),
|
||||||
overflow: hidden;
|
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>');
|
||||||
pointer-events: none;
|
background-repeat: repeat;
|
||||||
|
z-index: -1;
|
||||||
|
animation: twinkle 5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob {
|
@keyframes twinkle {
|
||||||
position: absolute;
|
0% { opacity: 0.5; }
|
||||||
width: 500px;
|
50% { opacity: 0.8; }
|
||||||
height: 500px;
|
100% { opacity: 0.5; }
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-1 {
|
.navbar {
|
||||||
top: -10%;
|
background: rgba(0, 0, 0, 0.8);
|
||||||
left: -10%;
|
backdrop-filter: blur(10px);
|
||||||
background: rgba(238, 119, 82, 0.4);
|
border-bottom: 1px solid var(--neon-cyan);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 243, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-2 {
|
.navbar-brand {
|
||||||
bottom: -10%;
|
font-weight: 700;
|
||||||
right: -10%;
|
color: var(--neon-cyan) !important;
|
||||||
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;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 0.75rem;
|
letter-spacing: 2px;
|
||||||
letter-spacing: 1px;
|
text-shadow: 0 0 10px var(--neon-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table td {
|
.card {
|
||||||
background: #fff;
|
background: var(--panel-bg);
|
||||||
padding: 1rem;
|
border: 1px solid rgba(0, 243, 255, 0.2);
|
||||||
border: none;
|
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; }
|
.card:hover {
|
||||||
.table tr td:last-child { border-radius: 0 12px 12px 0; }
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 243, 255, 0.2);
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.neon-btn {
|
||||||
display: block;
|
background: transparent;
|
||||||
margin-bottom: 0.5rem;
|
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-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 {
|
.form-control {
|
||||||
width: 100%;
|
background: rgba(255, 255, 255, 0.05);
|
||||||
padding: 0.75rem 1rem;
|
border: 1px solid rgba(0, 243, 255, 0.2);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
color: white;
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
outline: none;
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border-color: #23a6d5;
|
border-color: var(--neon-cyan);
|
||||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const chatForm = document.getElementById('chat-form');
|
const monitorList = document.getElementById('monitor-list');
|
||||||
const chatInput = document.getElementById('chat-input');
|
const addForm = document.getElementById('add-monitor-form');
|
||||||
const chatMessages = document.getElementById('chat-messages');
|
const serviceStatus = document.getElementById('service-status');
|
||||||
|
let isFetching = false;
|
||||||
|
|
||||||
const appendMessage = (text, sender) => {
|
const fetchMonitors = async (action = 'list') => {
|
||||||
const msgDiv = document.createElement('div');
|
if (isFetching) return;
|
||||||
msgDiv.classList.add('message', sender);
|
isFetching = true;
|
||||||
msgDiv.textContent = text;
|
|
||||||
chatMessages.appendChild(msgDiv);
|
try {
|
||||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
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) => {
|
const updateServiceStatus = (active) => {
|
||||||
e.preventDefault();
|
if (!serviceStatus) return;
|
||||||
const message = chatInput.value.trim();
|
const text = serviceStatus.querySelector('.status-text');
|
||||||
if (!message) return;
|
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');
|
const updateSummary = (monitors) => {
|
||||||
chatInput.value = '';
|
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 {
|
try {
|
||||||
const response = await fetch('api/chat.php', {
|
const response = await fetch('api/monitor.php?action=add', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ message })
|
body: JSON.stringify(formData)
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
// Artificial delay for realism
|
addForm.reset();
|
||||||
setTimeout(() => {
|
bootstrap.Modal.getInstance(document.getElementById('addMonitorModal')).hide();
|
||||||
appendMessage(data.reply, 'bot');
|
fetchMonitors('list');
|
||||||
}, 500);
|
} else {
|
||||||
|
alert('Error adding monitor: ' + result.error);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Add error:', error);
|
||||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
} 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
|
<!DOCTYPE html>
|
||||||
declare(strict_types=1);
|
|
||||||
@ini_set('display_errors', '1');
|
|
||||||
@error_reporting(E_ALL);
|
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>New Style</title>
|
<title>Yumee Panels - Real-time Uptime Monitor</title>
|
||||||
<?php
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
// Read project preview data from environment
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
<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">
|
||||||
<?php if ($projectDescription): ?>
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
<!-- 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>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
|
||||||
<div class="card">
|
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
<div class="container">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<a class="navbar-brand" href="#">
|
||||||
<span class="sr-only">Loading…</span>
|
<i class="bi bi-activity"></i> Yumee Panels
|
||||||
</div>
|
</a>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
<span class="badge rounded-pill bg-dark border border-magenta py-2 px-3">
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
<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>
|
</div>
|
||||||
</main>
|
</nav>
|
||||||
<footer>
|
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<div class="container my-5">
|
||||||
</footer>
|
<!-- 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>
|
</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