Enterprise Services administration dashboard
* Database health checks * Cache health checks * Storage write and disk-space checks * Failed queue job monitoring * Production environment diagnostics * Persisted health-check history * Queue retry controls * Scheduler execution controls * Cache and optimisation clearing * Backup creation and retention * Backup deletion * SQLite database backup support * Backup manifests and module metadata * Artisan health and backup commands * Administration navigation * Enterprise permissions * Module registration and tests
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
return [
|
||||
'backup_disk' => env('ENTERPRISE_BACKUP_DISK', 'local'),
|
||||
'backup_path' => env('ENTERPRISE_BACKUP_PATH', 'backups'),
|
||||
'retention_days' => (int) env('ENTERPRISE_BACKUP_RETENTION_DAYS', 30),
|
||||
'health' => [
|
||||
'minimum_free_disk_mb' => (int) env('ENTERPRISE_MIN_FREE_DISK_MB', 512),
|
||||
'maximum_failed_jobs' => (int) env('ENTERPRISE_MAX_FAILED_JOBS', 0),
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Enterprise\Services\BackupService;
|
||||
|
||||
final class BackupCreateCommand extends Command
|
||||
{
|
||||
protected $signature = 'enterprise:backup';
|
||||
protected $description = 'Create a platform backup archive.';
|
||||
public function handle(BackupService $service): int { $backup = $service->create(); $this->info('Backup created: '.$backup->path); return self::SUCCESS; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Enterprise\Services\BackupService;
|
||||
|
||||
final class BackupPruneCommand extends Command
|
||||
{
|
||||
protected $signature = 'enterprise:backup-prune';
|
||||
protected $description = 'Delete backups older than the configured retention period.';
|
||||
public function handle(BackupService $service): int { $this->info($service->prune().' backup(s) pruned.'); return self::SUCCESS; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Modules\Enterprise\Models\HealthRun;
|
||||
use Modules\Enterprise\Services\SystemHealthService;
|
||||
|
||||
final class HealthCheckCommand extends Command
|
||||
{
|
||||
protected $signature = 'enterprise:health {--json}';
|
||||
protected $description = 'Run enterprise platform health checks.';
|
||||
|
||||
public function handle(SystemHealthService $service): int
|
||||
{
|
||||
$result = $service->run();
|
||||
HealthRun::create(['status' => $result['status'], 'results' => $result, 'ran_at' => now()]);
|
||||
if ($this->option('json')) $this->line(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
else {
|
||||
$this->table(['Check', 'Status', 'Message'], collect($result['checks'])->map(fn (array $check) => [$check['name'], $check['status'], $check['message']])->all());
|
||||
$this->line('Overall: '.$result['status']);
|
||||
}
|
||||
return $result['status'] === 'failed' ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('enterprise_health_runs', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('status', 20);
|
||||
$table->json('results');
|
||||
$table->timestamp('ran_at');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('enterprise_health_runs');
|
||||
}
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('enterprise_backups', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('disk');
|
||||
$table->string('path');
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->string('status', 20)->default('completed');
|
||||
$table->text('message')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('enterprise_backups');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Enterprise\Models\Backup;
|
||||
use Modules\Enterprise\Models\HealthRun;
|
||||
use Modules\Enterprise\Services\BackupService;
|
||||
use Modules\Enterprise\Services\SystemHealthService;
|
||||
use Throwable;
|
||||
|
||||
final class EnterpriseController
|
||||
{
|
||||
public function index(SystemHealthService $health): View
|
||||
{
|
||||
return view('enterprise::admin.index', [
|
||||
'health' => $health->run(),
|
||||
'latestRun' => HealthRun::latest('ran_at')->first(),
|
||||
'backups' => Backup::latest()->limit(10)->get(),
|
||||
'failedJobs' => DB::getSchemaBuilder()->hasTable('failed_jobs') ? DB::table('failed_jobs')->count() : 0,
|
||||
'queueConnection' => config('queue.default'),
|
||||
'cacheStore' => config('cache.default'),
|
||||
'schedulerConfigured' => file_exists(base_path('routes/console.php')),
|
||||
'phpVersion' => PHP_VERSION,
|
||||
'laravelVersion' => app()->version(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function runHealth(SystemHealthService $service): RedirectResponse
|
||||
{
|
||||
$result = $service->run();
|
||||
HealthRun::create(['user_id' => auth()->id(), 'status' => $result['status'], 'results' => $result, 'ran_at' => now()]);
|
||||
return back()->with('status', 'Health checks completed.');
|
||||
}
|
||||
|
||||
public function clearCache(): RedirectResponse
|
||||
{
|
||||
Artisan::call('optimize:clear');
|
||||
Cache::flush();
|
||||
return back()->with('status', 'Application and cache stores cleared.');
|
||||
}
|
||||
|
||||
public function runScheduler(): RedirectResponse
|
||||
{
|
||||
Artisan::call('schedule:run');
|
||||
return back()->with('status', trim(Artisan::output()) ?: 'Scheduler executed.');
|
||||
}
|
||||
|
||||
public function retryFailedJobs(): RedirectResponse
|
||||
{
|
||||
Artisan::call('queue:retry', ['id' => 'all']);
|
||||
return back()->with('status', trim(Artisan::output()) ?: 'Failed jobs queued for retry.');
|
||||
}
|
||||
|
||||
public function createBackup(BackupService $service): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$service->create(auth()->id());
|
||||
return back()->with('status', 'Backup created.');
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
return back()->withErrors(['backup' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteBackup(Backup $backup, BackupService $service): RedirectResponse
|
||||
{
|
||||
$service->delete($backup);
|
||||
return back()->with('status', 'Backup deleted.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class Backup extends Model
|
||||
{
|
||||
protected $table = 'enterprise_backups';
|
||||
protected $fillable = ['user_id', 'disk', 'path', 'size', 'status', 'message', 'completed_at'];
|
||||
protected function casts(): array { return ['completed_at' => 'datetime']; }
|
||||
public function user(): BelongsTo { return $this->belongsTo(User::class); }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class HealthRun extends Model
|
||||
{
|
||||
protected $table = 'enterprise_health_runs';
|
||||
protected $fillable = ['user_id', 'status', 'results', 'ran_at'];
|
||||
protected function casts(): array { return ['results' => 'array', 'ran_at' => 'datetime']; }
|
||||
public function user(): BelongsTo { return $this->belongsTo(User::class); }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Enterprise\Console\BackupCreateCommand;
|
||||
use Modules\Enterprise\Console\BackupPruneCommand;
|
||||
use Modules\Enterprise\Console\HealthCheckCommand;
|
||||
use Modules\Enterprise\Services\BackupService;
|
||||
use Modules\Enterprise\Services\SystemHealthService;
|
||||
|
||||
final class EnterpriseServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Enterprise', 'Config/config.php'), 'enterprise');
|
||||
$this->app->singleton(SystemHealthService::class);
|
||||
$this->app->singleton(BackupService::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Enterprise', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Enterprise', 'Resources/views'), 'enterprise');
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([HealthCheckCommand::class, BackupCreateCommand::class, BackupPruneCommand::class]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path('Enterprise', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<x-layouts.app :title="__('Enterprise Services')">
|
||||
<div class="space-y-8">
|
||||
<div><h1 class="text-2xl font-semibold">{{ __('Enterprise Services') }}</h1><p class="text-sm text-zinc-500">{{ __('Health, queues, scheduler, cache, backups and production diagnostics.') }}</p></div>
|
||||
@if(session('status'))<flux:callout variant="success">{{ session('status') }}</flux:callout>@endif
|
||||
@if($errors->any())<flux:callout variant="danger">{{ $errors->first() }}</flux:callout>@endif
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<flux:card><div class="text-sm text-zinc-500">{{ __('Overall health') }}</div><div class="mt-2 text-xl font-semibold capitalize">{{ $health['status'] }}</div></flux:card>
|
||||
<flux:card><div class="text-sm text-zinc-500">{{ __('Queue') }}</div><div class="mt-2 text-xl font-semibold">{{ $queueConnection }}</div><div class="text-sm text-zinc-500">{{ $failedJobs }} {{ __('failed') }}</div></flux:card>
|
||||
<flux:card><div class="text-sm text-zinc-500">{{ __('Cache') }}</div><div class="mt-2 text-xl font-semibold">{{ $cacheStore }}</div></flux:card>
|
||||
<flux:card><div class="text-sm text-zinc-500">{{ __('Runtime') }}</div><div class="mt-2 text-sm">PHP {{ $phpVersion }}</div><div class="text-sm">Laravel {{ $laravelVersion }}</div></flux:card>
|
||||
</div>
|
||||
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3"><h2 class="text-lg font-semibold">{{ __('Health checks') }}</h2><form method="POST" action="{{ route('administration.enterprise.health.run') }}">@csrf<flux:button type="submit" variant="primary">{{ __('Run checks') }}</flux:button></form></div>
|
||||
<div class="mt-4 divide-y">@foreach($health['checks'] as $check)<div class="grid gap-2 py-3 md:grid-cols-[1fr_auto_2fr]"><strong>{{ $check['name'] }}</strong><span class="capitalize">{{ $check['status'] }}</span><span class="text-zinc-500">{{ $check['message'] }}</span></div>@endforeach</div>
|
||||
</flux:card>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<flux:card><h2 class="text-lg font-semibold">{{ __('Operations') }}</h2><div class="mt-4 flex flex-wrap gap-3"><form method="POST" action="{{ route('administration.enterprise.cache.clear') }}">@csrf<flux:button type="submit">{{ __('Clear cache') }}</flux:button></form><form method="POST" action="{{ route('administration.enterprise.scheduler.run') }}">@csrf<flux:button type="submit">{{ __('Run scheduler') }}</flux:button></form><form method="POST" action="{{ route('administration.enterprise.queue.retry') }}">@csrf<flux:button type="submit">{{ __('Retry failed jobs') }}</flux:button></form></div></flux:card>
|
||||
<flux:card><div class="flex items-center justify-between"><h2 class="text-lg font-semibold">{{ __('Backups') }}</h2><form method="POST" action="{{ route('administration.enterprise.backups.store') }}">@csrf<flux:button type="submit" variant="primary">{{ __('Create backup') }}</flux:button></form></div>@forelse($backups as $backup)<div class="flex items-center justify-between border-b py-3"><div><div>{{ $backup->path }}</div><div class="text-sm text-zinc-500">{{ number_format($backup->size / 1024 / 1024, 2) }} MB · {{ $backup->created_at->diffForHumans() }}</div></div><form method="POST" action="{{ route('administration.enterprise.backups.destroy', $backup) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger">{{ __('Delete') }}</flux:button></form></div>@empty<p class="py-4 text-zinc-500">{{ __('No backups created.') }}</p>@endforelse</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Enterprise\Http\Controllers\EnterpriseController;
|
||||
|
||||
Route::middleware(['web', 'auth', 'permission:enterprise.view'])
|
||||
->prefix('administration/enterprise')
|
||||
->name('administration.enterprise.')
|
||||
->controller(EnterpriseController::class)
|
||||
->group(function (): void {
|
||||
Route::get('/', 'index')->name('index');
|
||||
Route::post('/health', 'runHealth')->middleware('permission:enterprise.manage')->name('health.run');
|
||||
Route::post('/cache/clear', 'clearCache')->middleware('permission:enterprise.manage')->name('cache.clear');
|
||||
Route::post('/scheduler/run', 'runScheduler')->middleware('permission:enterprise.manage')->name('scheduler.run');
|
||||
Route::post('/queue/retry', 'retryFailedJobs')->middleware('permission:enterprise.manage')->name('queue.retry');
|
||||
Route::post('/backups', 'createBackup')->middleware('permission:enterprise.manage')->name('backups.store');
|
||||
Route::delete('/backups/{backup}', 'deleteBackup')->middleware('permission:enterprise.manage')->name('backups.destroy');
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Services;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Enterprise\Models\Backup;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
|
||||
final class BackupService
|
||||
{
|
||||
public function create(?int $userId = null): Backup
|
||||
{
|
||||
if (! class_exists(ZipArchive::class)) throw new RuntimeException('The PHP zip extension is required.');
|
||||
|
||||
$disk = (string) config('enterprise.backup_disk', 'local');
|
||||
$directory = trim((string) config('enterprise.backup_path', 'backups'), '/');
|
||||
$name = 'cms-'.now()->format('Ymd-His').'.zip';
|
||||
$relativePath = $directory.'/'.$name;
|
||||
$temporary = storage_path('app/'.$name);
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($temporary, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) throw new RuntimeException('Unable to create backup archive.');
|
||||
|
||||
$manifest = json_encode([
|
||||
'application' => config('app.name'),
|
||||
'environment' => app()->environment(),
|
||||
'created_at' => now()->toIso8601String(),
|
||||
'php' => PHP_VERSION,
|
||||
'laravel' => app()->version(),
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$zip->addFromString('manifest.json', $manifest ?: '{}');
|
||||
$zip->addFile(base_path('composer.json'), 'composer.json');
|
||||
if (File::exists(base_path('modules_statuses.json'))) $zip->addFile(base_path('modules_statuses.json'), 'modules_statuses.json');
|
||||
if (config('database.default') === 'sqlite') {
|
||||
$database = database_path('database.sqlite');
|
||||
if (File::exists($database)) $zip->addFile($database, 'database/database.sqlite');
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
Storage::disk($disk)->put($relativePath, File::get($temporary));
|
||||
File::delete($temporary);
|
||||
|
||||
return Backup::create([
|
||||
'user_id' => $userId,
|
||||
'disk' => $disk,
|
||||
'path' => $relativePath,
|
||||
'size' => Storage::disk($disk)->size($relativePath),
|
||||
'status' => 'completed',
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(Backup $backup): void
|
||||
{
|
||||
Storage::disk($backup->disk)->delete($backup->path);
|
||||
$backup->delete();
|
||||
}
|
||||
|
||||
public function prune(): int
|
||||
{
|
||||
$cutoff = now()->subDays((int) config('enterprise.retention_days', 30));
|
||||
$backups = Backup::query()->where('created_at', '<', $cutoff)->get();
|
||||
$backups->each(fn (Backup $backup) => $this->delete($backup));
|
||||
return $backups->count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
namespace Modules\Enterprise\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Throwable;
|
||||
|
||||
final class SystemHealthService
|
||||
{
|
||||
public function run(): array
|
||||
{
|
||||
$checks = [
|
||||
$this->database(),
|
||||
$this->cache(),
|
||||
$this->storage(),
|
||||
$this->queue(),
|
||||
$this->environment(),
|
||||
];
|
||||
|
||||
return [
|
||||
'status' => collect($checks)->contains(fn (array $check): bool => $check['status'] === 'failed') ? 'failed' : (collect($checks)->contains(fn (array $check): bool => $check['status'] === 'warning') ? 'warning' : 'healthy'),
|
||||
'checks' => $checks,
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
private function database(): array
|
||||
{
|
||||
try {
|
||||
DB::connection()->getPdo();
|
||||
return $this->result('Database', 'healthy', DB::connection()->getDriverName());
|
||||
} catch (Throwable $e) {
|
||||
return $this->result('Database', 'failed', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function cache(): array
|
||||
{
|
||||
try {
|
||||
$key = 'enterprise-health-'.bin2hex(random_bytes(6));
|
||||
Cache::put($key, 'ok', 10);
|
||||
$healthy = Cache::get($key) === 'ok';
|
||||
Cache::forget($key);
|
||||
return $this->result('Cache', $healthy ? 'healthy' : 'failed', config('cache.default'));
|
||||
} catch (Throwable $e) {
|
||||
return $this->result('Cache', 'failed', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function storage(): array
|
||||
{
|
||||
try {
|
||||
Storage::disk('local')->put('enterprise-health.txt', now()->toIso8601String());
|
||||
Storage::disk('local')->delete('enterprise-health.txt');
|
||||
$freeMb = (int) floor(disk_free_space(storage_path()) / 1024 / 1024);
|
||||
$minimum = (int) config('enterprise.health.minimum_free_disk_mb', 512);
|
||||
return $this->result('Storage', $freeMb < $minimum ? 'warning' : 'healthy', $freeMb.' MB free');
|
||||
} catch (Throwable $e) {
|
||||
return $this->result('Storage', 'failed', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function queue(): array
|
||||
{
|
||||
$failed = Schema::hasTable('failed_jobs') ? DB::table('failed_jobs')->count() : 0;
|
||||
$maximum = (int) config('enterprise.health.maximum_failed_jobs', 0);
|
||||
return $this->result('Queue', $failed > $maximum ? 'warning' : 'healthy', $failed.' failed jobs');
|
||||
}
|
||||
|
||||
private function environment(): array
|
||||
{
|
||||
$warnings = [];
|
||||
if (app()->environment('production') && config('app.debug')) $warnings[] = 'APP_DEBUG is enabled';
|
||||
if (! app()->environment('production')) $warnings[] = 'Not running in production';
|
||||
return $this->result('Environment', $warnings ? 'warning' : 'healthy', $warnings ? implode('; ', $warnings) : app()->environment());
|
||||
}
|
||||
|
||||
private function result(string $name, string $status, string $message): array
|
||||
{
|
||||
return compact('name', 'status', 'message');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
use Modules\Enterprise\Services\SystemHealthService;
|
||||
|
||||
it('boots the enterprise module', fn () => expect(app()->providerIsLoaded(Modules\Enterprise\Providers\EnterpriseServiceProvider::class))->toBeTrue());
|
||||
it('runs platform health checks', function (): void {
|
||||
$result = app(SystemHealthService::class)->run();
|
||||
expect($result)->toHaveKeys(['status', 'checks', 'generated_at'])->and($result['checks'])->not->toBeEmpty();
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"Enterprise","alias":"enterprise","version":"1.0.0","description":"Health, diagnostics, cache, queues, scheduler, backups and storage operations.","keywords":["health","diagnostics","queue","scheduler","backup"],"priority":100,"providers":["Modules\\Enterprise\\Providers\\EnterpriseServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration"]}
|
||||
@@ -61,6 +61,8 @@ final class RolePermissionSeeder extends Seeder
|
||||
'developer.access',
|
||||
'api.view',
|
||||
'api.manage',
|
||||
'enterprise.view',
|
||||
'enterprise.manage',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
|
||||
@@ -35,5 +35,6 @@
|
||||
"Localization": true,
|
||||
"Sites": true,
|
||||
"Developer": true,
|
||||
"Api": true
|
||||
"Api": true,
|
||||
"Enterprise": true
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<flux:sidebar.item icon="user-group" :href="route('administration.team.index')" :current="request()->routeIs('administration.team.*')" wire:navigate>{{ __('Team') }}</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="chat-bubble-left-right" :href="route('administration.testimonials.index')" :current="request()->routeIs('administration.testimonials.*')" wire:navigate>{{ __('Testimonials') }}</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Platform')" class="grid"><flux:sidebar.item icon="globe-alt" :href="route('administration.sites.index')" :current="request()->routeIs('administration.sites.*')" wire:navigate>{{ __('Sites') }}</flux:sidebar.item><flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:sidebar.item><flux:sidebar.item icon="squares-2x2" :href="route('administration.dashboard')" :current="request()->routeIs('administration.dashboard')" wire:navigate>{{ __('Administration') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Platform')" class="grid"><flux:sidebar.item icon="server-stack" :href="route('administration.enterprise.index')" :current="request()->routeIs('administration.enterprise.*')" wire:navigate>{{ __('Enterprise Services') }}</flux:sidebar.item><flux:sidebar.item icon="globe-alt" :href="route('administration.sites.index')" :current="request()->routeIs('administration.sites.*')" wire:navigate>{{ __('Sites') }}</flux:sidebar.item><flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:sidebar.item><flux:sidebar.item icon="squares-2x2" :href="route('administration.dashboard')" :current="request()->routeIs('administration.dashboard')" wire:navigate>{{ __('Administration') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Developer')" class="grid"><flux:sidebar.item icon="code-bracket" :href="route('administration.api.index')" :current="request()->routeIs('administration.api.*')" wire:navigate>{{ __('API & Webhooks') }}</flux:sidebar.item><flux:sidebar.item icon="command-line" href="{{ route('administration.developer.index') }}" :current="request()->routeIs('administration.developer.*')" wire:navigate>{{ __('Developer Tools') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Workflow')" class="grid"><flux:sidebar.item icon="arrows-right-left" :href="route('administration.workflows.index')" :current="request()->routeIs('administration.workflows.*')" wire:navigate>{{ __('Review Queue') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
<flux:sidebar.group :heading="__('Management')" class="grid"><flux:sidebar.item icon="users" :href="route('administration.users.index')" :current="request()->routeIs('administration.users.*')" wire:navigate>{{ __('Users') }}</flux:sidebar.item><flux:sidebar.item icon="shield-check" :href="route('administration.roles.index')" :current="request()->routeIs('administration.roles.*')" wire:navigate>{{ __('Roles & Permissions') }}</flux:sidebar.item><flux:sidebar.item icon="photo" :href="route('administration.media.index')" :current="request()->routeIs('administration.media.*')" wire:navigate>{{ __('Media') }}</flux:sidebar.item><flux:sidebar.item icon="magnifying-glass" :href="route('administration.search.index')" :current="request()->routeIs('administration.search.*')" wire:navigate>{{ __('Search') }}</flux:sidebar.item></flux:sidebar.group>
|
||||
|
||||
Reference in New Issue
Block a user