Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,86 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">Yumee Panels</string>
|
|
||||||
</resources>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rootProject.name = "Yumee Panels"
|
|
||||||
include(":app")
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
<?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,207 +1,302 @@
|
|||||||
: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-color: var(--bg-dark);
|
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||||
color: var(--text-main);
|
background-size: 400% 400%;
|
||||||
font-family: var(--font-inter);
|
animation: gradient 15s ease infinite;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Background Stars/Sparkle Effect */
|
.main-wrapper {
|
||||||
body::before {
|
display: flex;
|
||||||
content: '';
|
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 {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: radial-gradient(circle at 50% 50%, rgba(0, 243, 255, 0.05) 0%, transparent 70%),
|
z-index: 0;
|
||||||
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>');
|
overflow: hidden;
|
||||||
background-repeat: repeat;
|
pointer-events: none;
|
||||||
z-index: -1;
|
|
||||||
animation: twinkle 5s linear infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes twinkle {
|
.blob {
|
||||||
0% { opacity: 0.5; }
|
position: absolute;
|
||||||
50% { opacity: 0.8; }
|
width: 500px;
|
||||||
100% { opacity: 0.5; }
|
height: 500px;
|
||||||
}
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--neon-cyan) !important;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
text-shadow: 0 0 10px var(--neon-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 0 20px rgba(0, 243, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
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%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
filter: blur(80px);
|
||||||
margin-right: 8px;
|
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
||||||
box-shadow: 0 0 8px currentColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-online { color: var(--status-online); background-color: var(--status-online); }
|
.blob-1 {
|
||||||
.status-offline { color: var(--status-offline); background-color: var(--status-offline); }
|
top: -10%;
|
||||||
.status-unknown { color: #888; background-color: #888; }
|
left: -10%;
|
||||||
|
background: rgba(238, 119, 82, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
background: #fff;
|
||||||
|
padding: 1rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
width: 100%;
|
||||||
border: 1px solid rgba(0, 243, 255, 0.2);
|
padding: 0.75rem 1rem;
|
||||||
color: white;
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
outline: none;
|
||||||
border-color: var(--neon-cyan);
|
border-color: #23a6d5;
|
||||||
color: white;
|
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||||
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,288 +1,39 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const monitorList = document.getElementById('monitor-list');
|
const chatForm = document.getElementById('chat-form');
|
||||||
const addForm = document.getElementById('add-monitor-form');
|
const chatInput = document.getElementById('chat-input');
|
||||||
const serviceStatus = document.getElementById('service-status');
|
const chatMessages = document.getElementById('chat-messages');
|
||||||
let isFetching = false;
|
|
||||||
|
|
||||||
const fetchMonitors = async (action = 'list') => {
|
const appendMessage = (text, sender) => {
|
||||||
if (isFetching) return;
|
const msgDiv = document.createElement('div');
|
||||||
isFetching = true;
|
msgDiv.classList.add('message', sender);
|
||||||
|
msgDiv.textContent = text;
|
||||||
try {
|
chatMessages.appendChild(msgDiv);
|
||||||
const response = await fetch(`api/monitor.php?action=${action}`);
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateServiceStatus = (active) => {
|
chatForm.addEventListener('submit', async (e) => {
|
||||||
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';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
e.preventDefault();
|
||||||
const submitBtn = addForm.querySelector('button[type="submit"]');
|
const message = chatInput.value.trim();
|
||||||
submitBtn.disabled = true;
|
if (!message) return;
|
||||||
|
|
||||||
const formData = {
|
appendMessage(message, 'visitor');
|
||||||
name: document.getElementById('mon-name').value,
|
chatInput.value = '';
|
||||||
url: document.getElementById('mon-url').value,
|
|
||||||
interval: document.getElementById('mon-interval').value
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('api/monitor.php?action=add', {
|
const response = await fetch('api/chat.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(formData)
|
body: JSON.stringify({ message })
|
||||||
});
|
|
||||||
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('Add error:', error);
|
|
||||||
} finally {
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
fetchMonitors('list');
|
// Artificial delay for realism
|
||||||
setInterval(() => fetchMonitors('ping_all'), 1000);
|
setTimeout(() => {
|
||||||
|
appendMessage(data.reply, 'bot');
|
||||||
|
}, 500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
239
index.php
239
index.php
@ -1,109 +1,150 @@
|
|||||||
<!DOCTYPE html>
|
<?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>
|
||||||
<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.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Yumee Panels - Real-time Uptime Monitor</title>
|
<title>New Style</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<?php
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
// 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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<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>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
<div class="card">
|
||||||
<div class="container">
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
<a class="navbar-brand" href="#">
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<i class="bi bi-activity"></i> Yumee Panels
|
<span class="sr-only">Loading…</span>
|
||||||
</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>
|
||||||
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</main>
|
||||||
|
<footer>
|
||||||
<div class="container my-5">
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
<!-- Summary Cards -->
|
</footer>
|
||||||
<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>
|
||||||
130
worker.php
130
worker.php
@ -1,130 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user