feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands\AI;
|
||||
|
||||
use App\Services\AI\AiService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class GenerateSwaggerAnnotations extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ai:swagger {controller : The path to the controller file}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate Swagger/OpenAPI annotations for a controller using AI';
|
||||
|
||||
public function __construct(
|
||||
protected AiService $aiService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$path = $this->argument('controller');
|
||||
|
||||
if (! File::exists($path)) {
|
||||
// Try to find it in app/Http/Controllers
|
||||
$fullPath = app_path('Http/Controllers/'.ltrim($path, '/'));
|
||||
if (! File::exists($fullPath)) {
|
||||
$this->error("Controller not found at: {$path}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
$path = $fullPath;
|
||||
}
|
||||
|
||||
$content = File::get($path);
|
||||
|
||||
$this->info('Analyzing controller: '.basename($path));
|
||||
|
||||
$prompt = "Generate PHP Swagger (L5-Swagger) annotations for the following Laravel controller.
|
||||
Focus on @OA\Get, @OA\Post, etc. with proper @OA\Response and @OA\Parameter.
|
||||
|
||||
Guidelines:
|
||||
- Use modern OpenAPI 3.0 standards.
|
||||
- Include common responses like 200, 401, 403, and 500.
|
||||
- Identify request parameters from the code.
|
||||
- OUTPUT ONLY THE PHP CODE FOR THE ANNOTATIONS, no extra explanation.
|
||||
|
||||
CONTROLLER CODE:
|
||||
{$content}";
|
||||
|
||||
$result = $this->aiService->provider()->generate($prompt);
|
||||
|
||||
if (isset($result['success']) && $result['success']) {
|
||||
$annotations = $result['response'];
|
||||
|
||||
$this->warn('AI Generated Annotations:');
|
||||
$this->line($annotations);
|
||||
|
||||
if ($this->confirm('Do you want to prepend these annotations to the file?')) {
|
||||
// Find where to insert (usually before the class declaration)
|
||||
$pattern = '/class\s+'.basename($path, '.php').'/';
|
||||
if (preg_match($pattern, $content)) {
|
||||
$newContent = preg_replace($pattern, $annotations."\n".'$0', $content);
|
||||
File::put($path, $newContent);
|
||||
$this->success('Annotations added to '.basename($path));
|
||||
} else {
|
||||
$this->error('Could not find class declaration to insert annotations.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->error('AI Error: '.($result['error'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class AuditPermissions extends Command
|
||||
{
|
||||
protected $signature = 'permissions:audit
|
||||
{--fix : Create missing permissions in the database}
|
||||
{--json : Output results as JSON}';
|
||||
|
||||
protected $description = 'Compare route middleware permissions against the database and report gaps';
|
||||
|
||||
public function __construct(private Router $router)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$routePerms = $this->collectRoutePermissions();
|
||||
$dbPerms = Permission::pluck('name')->map(fn ($n) => strtolower($n))->toArray();
|
||||
|
||||
$missing = $routePerms->filter(fn ($p) => ! in_array(strtolower($p), $dbPerms))->values();
|
||||
$orphaned = collect($dbPerms)->filter(fn ($p) => ! $routePerms->map(fn ($r) => strtolower($r))->contains($p))->values();
|
||||
|
||||
if ($this->option('json')) {
|
||||
$this->line(json_encode(compact('missing', 'orphaned'), JSON_PRETTY_PRINT));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('=== Permission Audit ===');
|
||||
$this->newLine();
|
||||
|
||||
if ($missing->isEmpty()) {
|
||||
$this->line('<fg=green>✓ All route permissions exist in database.</>');
|
||||
} else {
|
||||
$this->warn("Missing in database ({$missing->count()} permissions used in routes but not in DB):");
|
||||
$missing->each(fn ($p) => $this->line(" - {$p}"));
|
||||
|
||||
if ($this->option('fix')) {
|
||||
$missing->each(fn ($p) => Permission::findOrCreate($p, 'web'));
|
||||
$this->info("Created {$missing->count()} missing permission(s).");
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
|
||||
if ($orphaned->isEmpty()) {
|
||||
$this->line('<fg=green>✓ No orphaned permissions in database.</>');
|
||||
} else {
|
||||
$this->warn("Orphaned in database ({$orphaned->count()} permissions in DB but not used in any route):");
|
||||
$orphaned->each(fn ($p) => $this->line(" - {$p}"));
|
||||
}
|
||||
|
||||
return $missing->isEmpty() ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
private function collectRoutePermissions(): Collection
|
||||
{
|
||||
$permissions = collect();
|
||||
|
||||
foreach ($this->router->getRoutes() as $route) {
|
||||
$middleware = $route->gatherMiddleware();
|
||||
|
||||
foreach ($middleware as $mw) {
|
||||
if (is_string($mw) && str_starts_with($mw, 'permission:')) {
|
||||
$perm = trim(str_replace('permission:', '', $mw));
|
||||
$permissions->push($perm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions->unique()->sort()->values();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Events\DashboardStatsUpdated;
|
||||
use App\Services\Monitoring\SystemMonitoringService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class BroadcastDashboardStats extends Command
|
||||
{
|
||||
protected $signature = 'dashboard:broadcast-stats';
|
||||
|
||||
protected $description = 'Broadcast live system stats to admin.monitoring channel';
|
||||
|
||||
public function handle(SystemMonitoringService $monitor): void
|
||||
{
|
||||
// Bust the cache so we always get fresh data for the broadcast
|
||||
Cache::forget('monitoring_full_bundle');
|
||||
|
||||
$stats = $monitor->getAll();
|
||||
|
||||
// Slim payload — only what the dashboard widgets need
|
||||
$payload = [
|
||||
'cpu' => $stats['cpu'],
|
||||
'ram' => $stats['ram'],
|
||||
'disk' => $stats['disk'],
|
||||
'users' => $stats['users'],
|
||||
'queues' => $stats['queues'],
|
||||
'uptime' => $stats['uptime'],
|
||||
'has_reverb' => $stats['has_reverb'],
|
||||
'last_update'=> $stats['last_update'],
|
||||
];
|
||||
|
||||
DashboardStatsUpdated::dispatch($payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mail\SystemHealthDigest;
|
||||
use App\Models\User;
|
||||
use App\Services\AI\AiService;
|
||||
use App\Services\Monitoring\SystemMonitoringService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class SendSystemHealthDigest extends Command
|
||||
{
|
||||
protected $signature = 'system:send-digest';
|
||||
|
||||
protected $description = 'Generate and send AI-powered system health digest to administrators';
|
||||
|
||||
public function __construct(
|
||||
protected SystemMonitoringService $monitoringService,
|
||||
protected AiService $aiService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Gathering system metrics...');
|
||||
$stats = $this->monitoringService->getAll();
|
||||
|
||||
if (! get_setting('ai_enabled', false)) {
|
||||
$this->error('AI Service is disabled. Cannot generate analysis.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info('Generating AI analysis...');
|
||||
|
||||
$prompt = 'As a Senior Systems Architect, analyze the following system metrics and provide a concise, professional summary of the system health.
|
||||
Detect any issues and provide recommendations.
|
||||
|
||||
METRICS:
|
||||
'.json_encode($stats, JSON_PRETTY_PRINT);
|
||||
|
||||
$result = $this->aiService->provider()->generate($prompt);
|
||||
|
||||
if (isset($result['success']) && $result['success']) {
|
||||
$analysis = $result['response'];
|
||||
|
||||
$admins = User::role(['Developer', 'Administrator'])->get();
|
||||
$this->info('Sending digest to '.$admins->count().' administrators...');
|
||||
|
||||
foreach ($admins as $admin) {
|
||||
Mail::to($admin->email)->send(new SystemHealthDigest($analysis, $stats));
|
||||
}
|
||||
|
||||
$this->info('Digest sent successfully!');
|
||||
} else {
|
||||
$this->error('AI Analysis failed: '.($result['error'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SystemCheck extends Command
|
||||
{
|
||||
protected $signature = 'system:check';
|
||||
|
||||
protected $description = 'Perform a comprehensive system health check';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->title('BIIProject System Health Check');
|
||||
|
||||
$rows = [];
|
||||
|
||||
// 1. Database
|
||||
try {
|
||||
DB::connection()->getPdo();
|
||||
$rows[] = ['Database', 'PostgreSQL', '<fg=green>CONNECTED</>'];
|
||||
} catch (\Exception $e) {
|
||||
$rows[] = ['Database', 'PostgreSQL', '<fg=red>FAILED</>'];
|
||||
}
|
||||
|
||||
// 2. Redis
|
||||
try {
|
||||
Redis::ping();
|
||||
$rows[] = ['Cache', 'Redis', '<fg=green>CONNECTED</>'];
|
||||
} catch (\Exception $e) {
|
||||
$rows[] = ['Cache', 'Redis', '<fg=red>FAILED</>'];
|
||||
}
|
||||
|
||||
// 3. Storage
|
||||
$storageOk = true;
|
||||
try {
|
||||
Storage::disk('local')->put('health-check.txt', 'ok');
|
||||
Storage::disk('local')->delete('health-check.txt');
|
||||
} catch (\Exception $e) {
|
||||
$storageOk = false;
|
||||
}
|
||||
$rows[] = ['Storage', 'Local Writable', $storageOk ? '<fg=green>OK</>' : '<fg=red>FAILED</>'];
|
||||
|
||||
// 4. AI
|
||||
$aiEnabled = get_setting('ai_enabled', false);
|
||||
$aiProvider = get_setting('ai_provider', 'N/A');
|
||||
$rows[] = ['Intelligence', 'AI Service', $aiEnabled ? "<fg=green>ENABLED ({$aiProvider})</>" : '<fg=yellow>DISABLED</>'];
|
||||
|
||||
// 5. Broadcast
|
||||
$rows[] = ['Real-time', 'Reverb', config('reverb') ? '<fg=green>CONFIGURED</>' : '<fg=yellow>NOT CONFIGURED</>'];
|
||||
|
||||
$this->table(['Component', 'Service', 'Status'], $rows);
|
||||
|
||||
$this->newLine();
|
||||
$this->info('System check completed at '.now()->toDateTimeString());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function title($text)
|
||||
{
|
||||
$this->newLine();
|
||||
$this->line('<options=bold;bg=blue;fg=white> '.strtoupper($text).' </>');
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file is strictly prohibited without prior written
|
||||
* permission from the author.
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Monitoring\SystemMonitoringService;
|
||||
use App\Services\Notification\TelegramService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SystemHealthCheck extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'system:health-check {--force : Force send notification even if within cool-down period}';
|
||||
|
||||
/**
|
||||
* The description of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Monitor system health (CPU, RAM, Disk, DB) and send alerts to Telegram if thresholds are exceeded.';
|
||||
|
||||
/**
|
||||
* Thresholds for alerts.
|
||||
*/
|
||||
protected $thresholds = [
|
||||
'cpu' => 80, // percentage
|
||||
'ram' => 90, // percentage
|
||||
'disk' => 90, // percentage
|
||||
];
|
||||
|
||||
/**
|
||||
* Cool-down period in minutes to prevent spamming notifications.
|
||||
*/
|
||||
protected $cooldownMinutes = 30;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(SystemMonitoringService $monitor, TelegramService $telegram)
|
||||
{
|
||||
$this->info('Starting System Health Check...');
|
||||
|
||||
$issues = [];
|
||||
$metrics = [];
|
||||
|
||||
// 1. Check Database Connectivity
|
||||
try {
|
||||
DB::connection()->getPdo();
|
||||
$metrics[] = '✅ Database: Connected';
|
||||
} catch (\Exception $e) {
|
||||
$issues[] = '❌ <b>DATABASE DOWN:</b> Unable to connect to the database. Error: '.$e->getMessage();
|
||||
}
|
||||
|
||||
// 2. Check CPU Usage
|
||||
$cpu = $monitor->getCpuUsage();
|
||||
$metrics[] = "📊 CPU Usage: {$cpu}%";
|
||||
if ($cpu >= $this->thresholds['cpu']) {
|
||||
$issues[] = "⚠️ <b>High CPU Usage:</b> {$cpu}% (Threshold: {$this->thresholds['cpu']}%)";
|
||||
}
|
||||
|
||||
// 3. Check RAM Usage
|
||||
$ram = $monitor->getRamUsage();
|
||||
$metrics[] = "📊 RAM Usage: {$ram}%";
|
||||
if ($ram >= $this->thresholds['ram']) {
|
||||
$issues[] = "⚠️ <b>High RAM Usage:</b> {$ram}% (Threshold: {$this->thresholds['ram']}%)";
|
||||
}
|
||||
|
||||
// 4. Check Disk Usage
|
||||
$disk = $monitor->getDiskUsage();
|
||||
$metrics[] = "📊 Disk Usage: {$disk}%";
|
||||
if ($disk >= $this->thresholds['disk']) {
|
||||
$issues[] = "⚠️ <b>High Disk Usage:</b> {$disk}% (Threshold: {$this->thresholds['disk']}%)";
|
||||
}
|
||||
|
||||
// Output to console
|
||||
foreach ($metrics as $metric) {
|
||||
$this->line($metric);
|
||||
}
|
||||
|
||||
if (empty($issues)) {
|
||||
$this->info('System health is optimal. No issues detected.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->error(count($issues).' issue(s) detected!');
|
||||
|
||||
// Check for cool-down
|
||||
$cacheKey = 'system_health_alert_last_sent';
|
||||
if (Cache::has($cacheKey) && ! $this->option('force')) {
|
||||
$this->warn('Issues detected, but notification is in cool-down period. Use --force to override.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Prepare Telegram Message
|
||||
$hostname = gethostname();
|
||||
$ip = request()->server('SERVER_ADDR') ?? gethostbyname($hostname);
|
||||
$time = now()->format('Y-m-d H:i:s');
|
||||
|
||||
$message = "🚨 <b>SYSTEM HEALTH ALERT</b> 🚨\n";
|
||||
$message .= "--------------------------------\n";
|
||||
$message .= "<b>Host:</b> {$hostname} ({$ip})\n";
|
||||
$message .= "<b>Time:</b> {$time}\n";
|
||||
$message .= "--------------------------------\n\n";
|
||||
$message .= implode("\n", $issues)."\n\n";
|
||||
$message .= '<i>Please check the system dashboard for more details.</i>';
|
||||
|
||||
// Send Notification
|
||||
if ($telegram->sendMessage($message)) {
|
||||
$this->info('Alert notification sent to Telegram.');
|
||||
Cache::put($cacheKey, true, now()->addMinutes($this->cooldownMinutes));
|
||||
} else {
|
||||
$this->error('Failed to send Telegram notification. Check laravel.log for details.');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file is strictly prohibited without prior written
|
||||
* permission from the author.
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class SystemOptimize extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'system:optimize';
|
||||
|
||||
/**
|
||||
* The description of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Perform a full system optimization (Cache, Routes, Views, and Pruning) in one command.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🚀 Starting Full System Optimization...');
|
||||
|
||||
$steps = [
|
||||
'Clearing Cache...' => 'cache:clear',
|
||||
'Caching Configuration...' => 'config:cache',
|
||||
'Caching Routes...' => 'route:cache',
|
||||
'Caching Views...' => 'view:cache',
|
||||
'Caching Events...' => 'event:cache',
|
||||
'Pruning Database Logs...' => 'model:prune',
|
||||
];
|
||||
|
||||
$bar = $this->output->createProgressBar(count($steps));
|
||||
$bar->start();
|
||||
|
||||
foreach ($steps as $label => $command) {
|
||||
$this->line("\n".$label);
|
||||
try {
|
||||
Artisan::call($command);
|
||||
$this->info("✓ Done: {$command}");
|
||||
} catch (\Exception $e) {
|
||||
$this->error("✗ Failed: {$command}. Error: ".$e->getMessage());
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->line("\n");
|
||||
$this->info('✨ System Optimization Complete! Everything is running at peak performance.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file is strictly prohibited without prior written
|
||||
* permission from the author.
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Notifications\BackupFailedNotification;
|
||||
use App\Services\Notification\TelegramService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VerifyBackups extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'backups:verify
|
||||
{--disk=local : Storage disk to check}
|
||||
{--max-age=7 : Alert if newest backup is older than N days}
|
||||
{--min-size=1024 : Alert if backup is smaller than N bytes}';
|
||||
|
||||
/**
|
||||
* The description of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Verify backups exist, are recent, and have non-trivial file sizes';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(TelegramService $telegram): int
|
||||
{
|
||||
$disk = $this->option('disk');
|
||||
$maxAgeDays = (int) $this->option('max-age');
|
||||
$minBytes = (int) $this->option('min-size');
|
||||
|
||||
$this->info("Verifying backups on disk: {$disk}");
|
||||
|
||||
$storage = Storage::disk($disk);
|
||||
|
||||
if (! $storage->exists('Laravel')) {
|
||||
$reason = 'Backup directory "Laravel" not found on disk.';
|
||||
$this->error($reason);
|
||||
$this->notifyFailure($reason, $disk, $telegram);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$files = collect($storage->allFiles('Laravel'))
|
||||
->filter(fn ($f) => Str::endsWith($f, ['.zip', '.gz', '.sql']))
|
||||
->sortByDesc(fn ($f) => $storage->lastModified($f))
|
||||
->values();
|
||||
|
||||
if ($files->isEmpty()) {
|
||||
$reason = 'No backup files found.';
|
||||
$this->error($reason);
|
||||
$this->notifyFailure($reason, $disk, $telegram);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$latest = $files->first();
|
||||
$latestModified = $storage->lastModified($latest);
|
||||
$latestSize = $storage->size($latest);
|
||||
$ageDays = (int) round((time() - $latestModified) / 86400);
|
||||
|
||||
$this->table(['File', 'Age (days)', 'Size (KB)', 'Status'], [[
|
||||
basename($latest),
|
||||
$ageDays,
|
||||
round($latestSize / 1024, 1),
|
||||
$this->statusLabel($ageDays, $latestSize, $maxAgeDays, $minBytes),
|
||||
]]);
|
||||
|
||||
$problems = [];
|
||||
|
||||
if ($ageDays > $maxAgeDays) {
|
||||
$problems[] = "Latest backup is {$ageDays} days old (threshold: {$maxAgeDays} days)";
|
||||
}
|
||||
|
||||
if ($latestSize < $minBytes) {
|
||||
$problems[] = "Latest backup is suspiciously small ({$latestSize} bytes)";
|
||||
}
|
||||
|
||||
if (! empty($problems)) {
|
||||
foreach ($problems as $problem) {
|
||||
$this->warn($problem);
|
||||
}
|
||||
$this->notifyFailure(implode('; ', $problems), $disk, $telegram);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("✓ Backup verification passed. {$files->count()} backup(s) found.");
|
||||
Log::channel('single')->info('[VerifyBackups] OK', ['count' => $files->count(), 'age_days' => $ageDays]);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function statusLabel(int $age, int $size, int $maxAge, int $minBytes): string
|
||||
{
|
||||
if ($age > $maxAge || $size < $minBytes) {
|
||||
return '⚠ WARN';
|
||||
}
|
||||
|
||||
return '✓ OK';
|
||||
}
|
||||
|
||||
private function notifyFailure(string $reason, string $disk, TelegramService $telegram): void
|
||||
{
|
||||
Log::channel('single')->error('[VerifyBackups] FAILED', compact('reason', 'disk'));
|
||||
|
||||
// Prepare Telegram Alert
|
||||
$hostname = gethostname();
|
||||
$message = "🚨 <b>BACKUP VERIFICATION FAILED</b> 🚨\n";
|
||||
$message .= "--------------------------------\n";
|
||||
$message .= "<b>Host:</b> {$hostname}\n";
|
||||
$message .= "<b>Disk:</b> {$disk}\n";
|
||||
$message .= "<b>Reason:</b> {$reason}\n";
|
||||
$message .= "--------------------------------\n";
|
||||
$message .= '<i>Please check the backup logs immediately!</i>';
|
||||
|
||||
$telegram->sendMessage($message);
|
||||
|
||||
// Notify developer role via database notification
|
||||
try {
|
||||
$developers = User::role('Developer')->get();
|
||||
foreach ($developers as $dev) {
|
||||
$dev->notify(new BackupFailedNotification($reason));
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Silently skip if notification classes don't exist yet
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user