Fix autoload duplicates and improve footer/linting
This commit is contained in:
6
.stylelintignore
Normal file
6
.stylelintignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
public/build/
|
||||||
|
public/vendor/
|
||||||
|
public/fonts/
|
||||||
|
public/css/filament/
|
||||||
18
.stylelintrc.json
Normal file
18
.stylelintrc.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"at-rule-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignoreAtRules": [
|
||||||
|
"tailwind",
|
||||||
|
"apply",
|
||||||
|
"layer",
|
||||||
|
"variants",
|
||||||
|
"responsive",
|
||||||
|
"screen",
|
||||||
|
"theme"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
class BackupSchedule extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'user_id',
|
|
||||||
'destination_id',
|
|
||||||
'name',
|
|
||||||
'is_active',
|
|
||||||
'is_server_backup',
|
|
||||||
'frequency',
|
|
||||||
'time',
|
|
||||||
'day_of_week',
|
|
||||||
'day_of_month',
|
|
||||||
'include_files',
|
|
||||||
'include_databases',
|
|
||||||
'include_mailboxes',
|
|
||||||
'include_dns',
|
|
||||||
'domains',
|
|
||||||
'databases',
|
|
||||||
'mailboxes',
|
|
||||||
'users',
|
|
||||||
'retention_count',
|
|
||||||
'last_run_at',
|
|
||||||
'next_run_at',
|
|
||||||
'last_status',
|
|
||||||
'last_error',
|
|
||||||
'metadata',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'is_server_backup' => 'boolean',
|
|
||||||
'include_files' => 'boolean',
|
|
||||||
'include_databases' => 'boolean',
|
|
||||||
'include_mailboxes' => 'boolean',
|
|
||||||
'include_dns' => 'boolean',
|
|
||||||
'domains' => 'array',
|
|
||||||
'databases' => 'array',
|
|
||||||
'mailboxes' => 'array',
|
|
||||||
'users' => 'array',
|
|
||||||
'metadata' => 'array',
|
|
||||||
'retention_count' => 'integer',
|
|
||||||
'day_of_week' => 'integer',
|
|
||||||
'day_of_month' => 'integer',
|
|
||||||
'last_run_at' => 'datetime',
|
|
||||||
'next_run_at' => 'datetime',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destination(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(BackupDestination::class, 'destination_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function backups(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(Backup::class, 'schedule_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the schedule should run now.
|
|
||||||
*/
|
|
||||||
public function shouldRun(): bool
|
|
||||||
{
|
|
||||||
if (! $this->is_active) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->next_run_at) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->next_run_at->isPast();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate and set the next run time.
|
|
||||||
*/
|
|
||||||
public function calculateNextRun(): Carbon
|
|
||||||
{
|
|
||||||
$timezone = $this->getSystemTimezone();
|
|
||||||
$now = Carbon::now($timezone);
|
|
||||||
$time = explode(':', $this->time);
|
|
||||||
$hour = (int) ($time[0] ?? 2);
|
|
||||||
$minute = (int) ($time[1] ?? 0);
|
|
||||||
|
|
||||||
$next = $now->copy()->setTime($hour, $minute, 0);
|
|
||||||
|
|
||||||
// If time already passed today, start from tomorrow
|
|
||||||
if ($next->isPast()) {
|
|
||||||
$next->addDay();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($this->frequency) {
|
|
||||||
case 'hourly':
|
|
||||||
$next = $now->copy()->addHour()->startOfHour();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'daily':
|
|
||||||
// Already set to next occurrence
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'weekly':
|
|
||||||
$targetDay = $this->day_of_week ?? 0; // Default to Sunday
|
|
||||||
while ($next->dayOfWeek !== $targetDay) {
|
|
||||||
$next->addDay();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'monthly':
|
|
||||||
$targetDay = $this->day_of_month ?? 1;
|
|
||||||
$next->day = min($targetDay, $next->daysInMonth);
|
|
||||||
if ($next->isPast()) {
|
|
||||||
$next->addMonth();
|
|
||||||
$next->day = min($targetDay, $next->daysInMonth);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$nextUtc = $next->copy()->setTimezone('UTC');
|
|
||||||
$this->attributes['next_run_at'] = $nextUtc->format($this->getDateFormat());
|
|
||||||
|
|
||||||
return $nextUtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get frequency label for UI.
|
|
||||||
*/
|
|
||||||
public function getFrequencyLabelAttribute(): string
|
|
||||||
{
|
|
||||||
$base = match ($this->frequency) {
|
|
||||||
'hourly' => 'Every hour',
|
|
||||||
'daily' => 'Daily at '.$this->time,
|
|
||||||
'weekly' => 'Weekly on '.$this->getDayName().' at '.$this->time,
|
|
||||||
'monthly' => 'Monthly on day '.($this->day_of_month ?? 1).' at '.$this->time,
|
|
||||||
default => ucfirst($this->frequency),
|
|
||||||
};
|
|
||||||
|
|
||||||
return $base;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get day name for weekly schedules.
|
|
||||||
*/
|
|
||||||
protected function getDayName(): string
|
|
||||||
{
|
|
||||||
$days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
||||||
|
|
||||||
return $days[$this->day_of_week ?? 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getSystemTimezone(): string
|
|
||||||
{
|
|
||||||
static $timezone = null;
|
|
||||||
if ($timezone === null) {
|
|
||||||
$timezone = trim((string) @file_get_contents('/etc/timezone'));
|
|
||||||
if ($timezone === '') {
|
|
||||||
$timezone = trim((string) @shell_exec('timedatectl show -p Timezone --value 2>/dev/null'));
|
|
||||||
}
|
|
||||||
if ($timezone === '') {
|
|
||||||
$timezone = 'UTC';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $timezone;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope for active schedules.
|
|
||||||
*/
|
|
||||||
public function scopeActive($query)
|
|
||||||
{
|
|
||||||
return $query->where('is_active', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope for due schedules.
|
|
||||||
*/
|
|
||||||
public function scopeDue($query)
|
|
||||||
{
|
|
||||||
return $query->active()
|
|
||||||
->where(function ($q) {
|
|
||||||
$q->whereNull('next_run_at')
|
|
||||||
->orWhere('next_run_at', '<=', now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope for user schedules.
|
|
||||||
*/
|
|
||||||
public function scopeForUser($query, int $userId)
|
|
||||||
{
|
|
||||||
return $query->where('user_id', $userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scope for server backup schedules.
|
|
||||||
*/
|
|
||||||
public function scopeServerBackups($query)
|
|
||||||
{
|
|
||||||
return $query->where('is_server_backup', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get last status color for UI.
|
|
||||||
*/
|
|
||||||
public function getLastStatusColorAttribute(): string
|
|
||||||
{
|
|
||||||
return match ($this->last_status) {
|
|
||||||
'success' => 'success',
|
|
||||||
'failed' => 'danger',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1616
app/Backups.php
1616
app/Backups.php
File diff suppressed because it is too large
Load Diff
@@ -185,5 +185,53 @@ class BackupSchedule extends Model
|
|||||||
return $timezone;
|
return $timezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for active schedules.
|
||||||
|
*/
|
||||||
|
public function scopeActive($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for due schedules.
|
||||||
|
*/
|
||||||
|
public function scopeDue($query)
|
||||||
|
{
|
||||||
|
return $query->active()
|
||||||
|
->where(function ($q) {
|
||||||
|
$q->whereNull('next_run_at')
|
||||||
|
->orWhere('next_run_at', '<=', now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for user schedules.
|
||||||
|
*/
|
||||||
|
public function scopeForUser($query, int $userId)
|
||||||
|
{
|
||||||
|
return $query->where('user_id', $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope for server backup schedules.
|
||||||
|
*/
|
||||||
|
public function scopeServerBackups($query)
|
||||||
|
{
|
||||||
|
return $query->where('is_server_backup', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last status color for UI.
|
||||||
|
*/
|
||||||
|
public function getLastStatusColorAttribute(): string
|
||||||
|
{
|
||||||
|
return match ($this->last_status) {
|
||||||
|
'success' => 'success',
|
||||||
|
'failed' => 'danger',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
\Log::warning("Failed to delete email forwarders for user {$user->username}: ".$e->getMessage());
|
\Log::warning("Failed to delete email forwarders for user {$user->username}: ".$e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +100,7 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
$masterUser = $user->username.'_admin';
|
$masterUser = $user->username.'_admin';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (class_exists(\mysqli::class)) {
|
||||||
// Use credentials from environment variables
|
// Use credentials from environment variables
|
||||||
$mysqli = new \mysqli(
|
$mysqli = new \mysqli(
|
||||||
config('database.connections.mysql.host', 'localhost'),
|
config('database.connections.mysql.host', 'localhost'),
|
||||||
@@ -120,12 +121,16 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
$mysqli->query("DROP USER IF EXISTS '{$escapedUser}'@'localhost'");
|
$mysqli->query("DROP USER IF EXISTS '{$escapedUser}'@'localhost'");
|
||||||
$mysqli->close();
|
$mysqli->close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Delete stored credentials
|
} catch (\Throwable $e) {
|
||||||
\App\Models\MysqlCredential::where('user_id', $user->id)->delete();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
\Log::error('Failed to delete master MySQL user: '.$e->getMessage());
|
\Log::error('Failed to delete master MySQL user: '.$e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
\App\Models\MysqlCredential::where('user_id', $user->id)->delete();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Log::error('Failed to delete stored MySQL credentials: '.$e->getMessage());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,53 @@
|
|||||||
@php
|
@php
|
||||||
$jabaliVersion = '1.0.1';
|
$jabaliVersion = 'unknown';
|
||||||
$versionFile = base_path('VERSION');
|
$versionFile = base_path('VERSION');
|
||||||
if (file_exists($versionFile)) {
|
if (file_exists($versionFile)) {
|
||||||
$content = file_get_contents($versionFile);
|
$content = file_get_contents($versionFile);
|
||||||
if (preg_match('/VERSION=(.+)/', $content, $matches)) {
|
if (preg_match('/^VERSION=(.+)$/m', $content, $matches)) {
|
||||||
$jabaliVersion = trim($matches[1]);
|
$jabaliVersion = trim($matches[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@endphp
|
@endphp
|
||||||
<style>
|
|
||||||
.dark .jabali-footer-logo { filter: invert(1) brightness(2); }
|
<footer class="mt-auto border-t border-gray-200/20 px-6 py-5 dark:border-white/10">
|
||||||
</style>
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div style="border-top: 1px solid rgba(128,128,128,0.1); padding: 20px 24px; margin-top: auto;">
|
<div class="flex items-center gap-3">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;">
|
<img
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
src="{{ asset('images/jabali_logo.svg') }}"
|
||||||
<img src="{{ asset('images/jabali_logo.svg') }}" alt="Jabali" style="height: 32px; width: 32px;" class="jabali-footer-logo">
|
alt="{{ __('Jabali') }}"
|
||||||
<div>
|
class="h-8 w-8 dark:filter dark:invert dark:brightness-200"
|
||||||
<div style="font-weight: 600; font-size: 15px;" class="text-gray-700 dark:text-gray-200">Jabali Panel</div>
|
>
|
||||||
<div style="font-size: 12px;" class="text-gray-500 dark:text-gray-400">Web Hosting Control Panel</div>
|
|
||||||
|
<div class="leading-tight">
|
||||||
|
<div class="text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||||
|
{{ __('Jabali Panel') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ __('Web Hosting Control Panel') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; gap: 16px; font-size: 13px;" class="text-gray-500 dark:text-gray-400">
|
</div>
|
||||||
<a href="https://github.com/shukiv/jabali-panel" target="_blank" style="display: flex; align-items: center; gap: 6px; text-decoration: none;" class="text-gray-500 dark:text-gray-400 hover:text-blue-500">
|
|
||||||
<svg style="width: 16px; height: 16px;" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
GitHub
|
<x-filament::link
|
||||||
</a>
|
tag="a"
|
||||||
<span style="color: rgba(128,128,128,0.3);">•</span>
|
href="https://github.com/shukiv/jabali-panel"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
color="gray"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ __('GitHub') }}
|
||||||
|
</x-filament::link>
|
||||||
|
|
||||||
|
<span class="text-gray-400/50 dark:text-white/20">•</span>
|
||||||
|
|
||||||
<span>© {{ date('Y') }} Jabali</span>
|
<span>© {{ date('Y') }} Jabali</span>
|
||||||
<span style="background: linear-gradient(135deg, #3b82f6, #8b5cf6); color: white; padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 600;">v{{ $jabaliVersion }}</span>
|
|
||||||
</div>
|
<x-filament::badge size="sm" color="gray">
|
||||||
|
v{{ $jabaliVersion }}
|
||||||
|
</x-filament::badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</footer>
|
||||||
|
|||||||
Reference in New Issue
Block a user