Deployment environments

* Development, staging, and production environment types
* Protected-environment support
* Deployment targets
* Local, SSH, Docker, and Kubernetes driver foundation
* Deployment pipelines
* Configurable pipeline steps
* Manual and approval-based releases
* Deployment release history
* Individual deployment-step tracking
* HTTP health checks
* Response-time tracking
* Rollback records
* Secret-reference metadata
* Encrypted target configuration
* Encrypted environment variables
* Administration dashboard
* Authenticated API endpoints
* Health-check command
* Release-history pruning command
* Permissions
* Sidebar integration
* Module registration
* Feature test
This commit is contained in:
Mark Gregory
2026-07-24 13:57:56 +01:00
parent f09a680531
commit bc9427ac33
26 changed files with 377 additions and 1 deletions
+9
View File
@@ -0,0 +1,9 @@
<?php
return [
'enabled' => env('CLOUD_DEVOPS_ENABLED', true),
'allow_execution' => env('CLOUD_DEVOPS_ALLOW_EXECUTION', false),
'default_timeout' => (int) env('CLOUD_DEVOPS_TIMEOUT', 900),
'retention_days' => (int) env('CLOUD_DEVOPS_RETENTION_DAYS', 90),
'allowed_drivers' => ['local', 'ssh', 'docker', 'kubernetes'],
];
@@ -0,0 +1,6 @@
<?php
namespace Modules\CloudDevOps\Console\Commands;
use Illuminate\Console\Command;
use Modules\CloudDevOps\Models\DeploymentHealthCheck;
use Modules\CloudDevOps\Services\HealthCheckRunner;
final class CloudDevOpsHealthCommand extends Command { protected $signature='cloud-devops:health'; protected $description='Run all active deployment health checks'; public function handle(HealthCheckRunner $runner): int { DeploymentHealthCheck::query()->each(fn($check)=>$runner->run($check)); $this->info('Health checks completed.'); return self::SUCCESS; } }
@@ -0,0 +1,5 @@
<?php
namespace Modules\CloudDevOps\Console\Commands;
use Illuminate\Console\Command;
use Modules\CloudDevOps\Models\DeploymentRelease;
final class CloudDevOpsPruneCommand extends Command { protected $signature='cloud-devops:prune {--days=}'; protected $description='Prune old deployment release history'; public function handle(): int { $days=(int)($this->option('days') ?: config('clouddevops.retention_days',90)); $count=DeploymentRelease::where('created_at','<',now()->subDays($days))->whereIn('status',['succeeded','failed','cancelled'])->delete(); $this->info("Pruned {$count} releases."); return self::SUCCESS; } }
@@ -0,0 +1,121 @@
<?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('deployment_environments', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('type')->default('production');
$table->string('status')->default('active');
$table->boolean('is_protected')->default(false);
$table->json('variables')->nullable();
$table->timestamps();
});
Schema::create('deployment_targets', function (Blueprint $table): void {
$table->id();
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
$table->string('name');
$table->string('driver')->default('ssh');
$table->string('host')->nullable();
$table->unsignedInteger('port')->nullable();
$table->string('path')->nullable();
$table->json('configuration')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('deployment_pipelines', function (Blueprint $table): void {
$table->id();
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
$table->string('name');
$table->string('trigger')->default('manual');
$table->json('steps');
$table->boolean('requires_approval')->default(false);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('deployment_releases', function (Blueprint $table): void {
$table->id();
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
$table->foreignId('pipeline_id')->nullable()->constrained('deployment_pipelines')->nullOnDelete();
$table->string('version');
$table->string('commit_hash')->nullable();
$table->string('status')->default('pending');
$table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->text('failure_message')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['environment_id', 'status']);
});
Schema::create('deployment_steps', function (Blueprint $table): void {
$table->id();
$table->foreignId('release_id')->constrained('deployment_releases')->cascadeOnDelete();
$table->string('name');
$table->unsignedInteger('position')->default(0);
$table->string('status')->default('pending');
$table->text('output')->nullable();
$table->timestamp('started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
Schema::create('deployment_health_checks', function (Blueprint $table): void {
$table->id();
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
$table->string('name');
$table->string('type')->default('http');
$table->string('endpoint')->nullable();
$table->unsignedInteger('expected_status')->default(200);
$table->string('status')->default('unknown');
$table->unsignedInteger('response_time_ms')->nullable();
$table->timestamp('checked_at')->nullable();
$table->text('message')->nullable();
$table->timestamps();
});
Schema::create('deployment_rollbacks', function (Blueprint $table): void {
$table->id();
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
$table->foreignId('from_release_id')->constrained('deployment_releases')->cascadeOnDelete();
$table->foreignId('to_release_id')->nullable()->constrained('deployment_releases')->nullOnDelete();
$table->string('status')->default('pending');
$table->foreignId('requested_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('reason')->nullable();
$table->timestamps();
});
Schema::create('deployment_secrets', function (Blueprint $table): void {
$table->id();
$table->foreignId('environment_id')->constrained('deployment_environments')->cascadeOnDelete();
$table->string('key');
$table->string('provider')->default('database');
$table->string('reference')->nullable();
$table->timestamp('rotated_at')->nullable();
$table->timestamps();
$table->unique(['environment_id', 'key']);
});
}
public function down(): void
{
Schema::dropIfExists('deployment_secrets');
Schema::dropIfExists('deployment_rollbacks');
Schema::dropIfExists('deployment_health_checks');
Schema::dropIfExists('deployment_steps');
Schema::dropIfExists('deployment_releases');
Schema::dropIfExists('deployment_pipelines');
Schema::dropIfExists('deployment_targets');
Schema::dropIfExists('deployment_environments');
}
};
@@ -0,0 +1,7 @@
<?php
namespace Modules\CloudDevOps\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Modules\CloudDevOps\Models\DeploymentEnvironment;
use Modules\CloudDevOps\Models\DeploymentRelease;
final class CloudDevOpsApiController extends Controller { public function environments(): JsonResponse { return response()->json(['data'=>DeploymentEnvironment::withCount(['targets','pipelines','releases'])->get()]); } public function releases(): JsonResponse { return response()->json(['data'=>DeploymentRelease::with(['environment','steps'])->latest()->paginate()]); } }
@@ -0,0 +1,47 @@
<?php
namespace Modules\CloudDevOps\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Modules\CloudDevOps\Models\DeploymentEnvironment;
use Modules\CloudDevOps\Models\DeploymentHealthCheck;
use Modules\CloudDevOps\Models\DeploymentPipeline;
use Modules\CloudDevOps\Models\DeploymentRelease;
use Modules\CloudDevOps\Services\DeploymentManager;
use Modules\CloudDevOps\Services\HealthCheckRunner;
final class CloudDevOpsController extends Controller
{
public function index(): View
{
return view('clouddevops::index', [
'environments' => DeploymentEnvironment::withCount(['targets', 'pipelines', 'releases'])->latest()->get(),
'releases' => DeploymentRelease::with('environment')->latest()->limit(10)->get(),
'checks' => DeploymentHealthCheck::with('environment')->latest('checked_at')->limit(10)->get(),
]);
}
public function storeEnvironment(Request $request): RedirectResponse
{
$data = $request->validate(['name'=>'required|string|max:120','slug'=>'required|alpha_dash|max:120|unique:deployment_environments,slug','type'=>'required|in:development,staging,production']);
DeploymentEnvironment::create($data);
return back()->with('status', 'Environment created.');
}
public function deploy(Request $request, DeploymentEnvironment $environment, DeploymentManager $manager): RedirectResponse
{
$data = $request->validate(['pipeline_id'=>'required|integer','version'=>'nullable|string|max:120','commit_hash'=>'nullable|string|max:120']);
$pipeline = DeploymentPipeline::where('environment_id', $environment->id)->findOrFail($data['pipeline_id']);
$manager->createRelease($environment, $pipeline, $data + ['requested_by' => $request->user()?->id]);
return back()->with('status', 'Release created.');
}
public function runCheck(DeploymentHealthCheck $check, HealthCheckRunner $runner): RedirectResponse
{
$runner->run($check);
return back()->with('status', 'Health check completed.');
}
}
@@ -0,0 +1,16 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class DeploymentEnvironment extends Model
{
protected $guarded = [];
protected function casts(): array { return ['variables' => 'encrypted:array', 'is_protected' => 'boolean']; }
public function targets(): HasMany { return $this->hasMany(DeploymentTarget::class, 'environment_id'); }
public function pipelines(): HasMany { return $this->hasMany(DeploymentPipeline::class, 'environment_id'); }
public function releases(): HasMany { return $this->hasMany(DeploymentRelease::class, 'environment_id'); }
public function healthChecks(): HasMany { return $this->hasMany(DeploymentHealthCheck::class, 'environment_id'); }
}
@@ -0,0 +1,4 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
final class DeploymentHealthCheck extends Model { protected $guarded = []; protected function casts(): array { return ['checked_at' => 'datetime']; } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
final class DeploymentPipeline extends Model { protected $guarded = []; protected function casts(): array { return ['steps' => 'array', 'requires_approval' => 'boolean', 'is_active' => 'boolean']; } }
@@ -0,0 +1,6 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class DeploymentRelease extends Model { protected $guarded = []; protected function casts(): array { return ['metadata' => 'array', 'started_at' => 'datetime', 'completed_at' => 'datetime']; } public function environment(): BelongsTo { return $this->belongsTo(DeploymentEnvironment::class, 'environment_id'); } public function steps(): HasMany { return $this->hasMany(DeploymentStep::class, 'release_id')->orderBy('position'); } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
final class DeploymentRollback extends Model { protected $guarded = []; }
@@ -0,0 +1,4 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
final class DeploymentSecret extends Model { protected $guarded = []; protected function casts(): array { return ['rotated_at' => 'datetime']; } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
final class DeploymentStep extends Model { protected $guarded = []; protected function casts(): array { return ['started_at' => 'datetime', 'completed_at' => 'datetime']; } }
@@ -0,0 +1,4 @@
<?php
namespace Modules\CloudDevOps\Models;
use Illuminate\Database\Eloquent\Model;
final class DeploymentTarget extends Model { protected $guarded = []; protected function casts(): array { return ['configuration' => 'encrypted:array', 'is_active' => 'boolean']; } }
@@ -0,0 +1,6 @@
<?php
namespace Modules\CloudDevOps\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\CloudDevOps\Console\Commands\CloudDevOpsHealthCommand;
use Modules\CloudDevOps\Console\Commands\CloudDevOpsPruneCommand;
final class CloudDevOpsServiceProvider extends ServiceProvider { public function register(): void { $this->mergeConfigFrom(module_path('CloudDevOps','Config/config.php'),'clouddevops'); $this->app->register(RouteServiceProvider::class); } public function boot(): void { $this->loadViewsFrom(module_path('CloudDevOps','Resources/views'),'clouddevops'); $this->loadMigrationsFrom(module_path('CloudDevOps','Database/Migrations')); if ($this->app->runningInConsole()) { $this->commands([CloudDevOpsHealthCommand::class,CloudDevOpsPruneCommand::class]); } } }
@@ -0,0 +1,5 @@
<?php
namespace Modules\CloudDevOps\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('CloudDevOps','Routes/web.php')); Route::middleware('api')->prefix('api')->group(module_path('CloudDevOps','Routes/api.php')); } }
@@ -0,0 +1,44 @@
<x-layouts.app :title="__('Cloud & DevOps')">
<div class="flex h-full w-full flex-1 flex-col gap-6 rounded-xl">
<div>
<flux:heading size="xl">Cloud & DevOps</flux:heading>
<flux:subheading>Manage environments, deployment releases, pipelines and service health.</flux:subheading>
</div>
@if (session('status'))
<flux:callout variant="success">{{ session('status') }}</flux:callout>
@endif
<div class="grid gap-4 md:grid-cols-3">
<div class="rounded-xl border p-5"><p class="text-sm text-zinc-500">Environments</p><p class="mt-2 text-3xl font-semibold">{{ $environments->count() }}</p></div>
<div class="rounded-xl border p-5"><p class="text-sm text-zinc-500">Recent releases</p><p class="mt-2 text-3xl font-semibold">{{ $releases->count() }}</p></div>
<div class="rounded-xl border p-5"><p class="text-sm text-zinc-500">Healthy checks</p><p class="mt-2 text-3xl font-semibold">{{ $checks->where('status', 'healthy')->count() }}</p></div>
</div>
@can('cloud-devops.manage')
<form method="POST" action="{{ route('administration.cloud-devops.environments.store') }}" class="grid gap-3 rounded-xl border p-5 md:grid-cols-4">
@csrf
<flux:input name="name" label="Environment name" required />
<flux:input name="slug" label="Slug" required />
<flux:select name="type" label="Type"><option value="development">Development</option><option value="staging">Staging</option><option value="production">Production</option></flux:select>
<div class="flex items-end"><flux:button type="submit" variant="primary">Create environment</flux:button></div>
</form>
@endcan
<section class="space-y-3">
<flux:heading size="lg">Environments</flux:heading>
<div class="grid gap-4 lg:grid-cols-2">
@forelse ($environments as $environment)
<article class="rounded-xl border p-5">
<div class="flex items-center justify-between gap-3"><div><h2 class="font-semibold">{{ $environment->name }}</h2><p class="text-sm text-zinc-500">{{ ucfirst($environment->type) }} · {{ ucfirst($environment->status) }}</p></div>@if ($environment->is_protected)<flux:badge color="amber">Protected</flux:badge>@endif</div>
<div class="mt-4 grid grid-cols-3 gap-3 text-sm"><div><span class="text-zinc-500">Targets</span><p>{{ $environment->targets_count }}</p></div><div><span class="text-zinc-500">Pipelines</span><p>{{ $environment->pipelines_count }}</p></div><div><span class="text-zinc-500">Releases</span><p>{{ $environment->releases_count }}</p></div></div>
</article>
@empty
<div class="col-span-full rounded-xl border p-8 text-center text-zinc-500">No deployment environments configured.</div>
@endforelse
</div>
</section>
<section class="space-y-3"><flux:heading size="lg">Recent releases</flux:heading><div class="overflow-hidden rounded-xl border"><table class="w-full text-sm"><thead class="bg-zinc-50 dark:bg-zinc-900"><tr><th class="p-3 text-left">Version</th><th class="p-3 text-left">Environment</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Created</th></tr></thead><tbody>@forelse ($releases as $release)<tr class="border-t"><td class="p-3">{{ $release->version }}</td><td class="p-3">{{ $release->environment?->name }}</td><td class="p-3">{{ ucfirst(str_replace('_', ' ', $release->status)) }}</td><td class="p-3">{{ $release->created_at?->diffForHumans() }}</td></tr>@empty<tr><td colspan="4" class="p-6 text-center text-zinc-500">No releases yet.</td></tr>@endforelse</tbody></table></div></section>
</div>
</x-layouts.app>
+4
View File
@@ -0,0 +1,4 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\CloudDevOps\Http\Controllers\CloudDevOpsApiController;
Route::middleware(['auth:sanctum','permission:cloud-devops.view'])->prefix('v1/cloud-devops')->group(function (): void { Route::get('/environments',[CloudDevOpsApiController::class,'environments']); Route::get('/releases',[CloudDevOpsApiController::class,'releases']); });
+14
View File
@@ -0,0 +1,14 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\CloudDevOps\Http\Controllers\CloudDevOpsController;
Route::middleware(['web', 'auth', 'verified', 'permission:cloud-devops.view'])
->prefix('administration/cloud-devops')
->name('administration.cloud-devops.')
->group(function (): void {
Route::get('/', [CloudDevOpsController::class, 'index'])->name('index');
Route::post('/environments', [CloudDevOpsController::class, 'storeEnvironment'])->middleware('permission:cloud-devops.manage')->name('environments.store');
Route::post('/environments/{environment}/deploy', [CloudDevOpsController::class, 'deploy'])->middleware('permission:cloud-devops.deploy')->name('deploy');
Route::post('/health-checks/{check}/run', [CloudDevOpsController::class, 'runCheck'])->middleware('permission:cloud-devops.manage')->name('checks.run');
});
@@ -0,0 +1,39 @@
<?php
namespace Modules\CloudDevOps\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Modules\CloudDevOps\Models\DeploymentEnvironment;
use Modules\CloudDevOps\Models\DeploymentPipeline;
use Modules\CloudDevOps\Models\DeploymentRelease;
final class DeploymentManager
{
public function createRelease(DeploymentEnvironment $environment, DeploymentPipeline $pipeline, array $attributes = []): DeploymentRelease
{
return DB::transaction(function () use ($environment, $pipeline, $attributes): DeploymentRelease {
$release = $environment->releases()->create([
'pipeline_id' => $pipeline->getKey(),
'version' => $attributes['version'] ?? now()->format('YmdHis').'-'.Str::lower(Str::random(6)),
'commit_hash' => $attributes['commit_hash'] ?? null,
'status' => $pipeline->requires_approval ? 'awaiting_approval' : 'pending',
'requested_by' => $attributes['requested_by'] ?? null,
'metadata' => $attributes['metadata'] ?? [],
]);
foreach ($pipeline->steps as $position => $step) {
$release->steps()->create(['name' => $step['name'] ?? 'Step '.($position + 1), 'position' => $position]);
}
return $release->load('steps');
});
}
public function execute(DeploymentRelease $release): DeploymentRelease
{
abort_unless(config('clouddevops.allow_execution'), 403, 'Deployment execution is disabled.');
$release->update(['status' => 'running', 'started_at' => now()]);
return $release->refresh();
}
}
@@ -0,0 +1,5 @@
<?php
namespace Modules\CloudDevOps\Services;
use Illuminate\Support\Facades\Http;
use Modules\CloudDevOps\Models\DeploymentHealthCheck;
final class HealthCheckRunner { public function run(DeploymentHealthCheck $check): DeploymentHealthCheck { $started = microtime(true); try { $response = Http::timeout(10)->get((string) $check->endpoint); $status = $response->status() === $check->expected_status ? 'healthy' : 'unhealthy'; $message = 'HTTP '.$response->status(); } catch (\Throwable $e) { $status = 'unhealthy'; $message = $e->getMessage(); } $check->update(['status'=>$status,'response_time_ms'=>(int)((microtime(true)-$started)*1000),'checked_at'=>now(),'message'=>$message]); return $check->refresh(); } }
@@ -0,0 +1,6 @@
<?php
namespace Modules\CloudDevOps\Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\CloudDevOps\Models\DeploymentEnvironment;
use Tests\TestCase;
final class CloudDevOpsModuleTest extends TestCase { use RefreshDatabase; public function test_environment_can_be_created(): void { $environment=DeploymentEnvironment::create(['name'=>'Production','slug'=>'production','type'=>'production']); $this->assertDatabaseHas('deployment_environments',['id'=>$environment->id,'slug'=>'production']); } }
+9
View File
@@ -0,0 +1,9 @@
{
"name": "CloudDevOps",
"alias": "clouddevops",
"description": "Cloud deployment and DevOps operations for Content Manager.",
"keywords": ["cloud", "devops", "deployments", "pipelines"],
"priority": 18,
"providers": ["Modules\\CloudDevOps\\Providers\\CloudDevOpsServiceProvider"],
"files": []
}
@@ -81,6 +81,7 @@ final class RolePermissionSeeder extends Seeder
'analytics.export',
'builder.view', 'builder.manage', 'builder.publish',
'marketplace.view', 'marketplace.install', 'marketplace.update', 'marketplace.remove',
'cloud-devops.view', 'cloud-devops.manage', 'cloud-devops.deploy', 'cloud-devops.rollback',
];
foreach ($permissions as $permission) {
+2 -1
View File
@@ -43,5 +43,6 @@
"AI": true,
"Analytics": true,
"Builder": true,
"Marketplace": true
"Marketplace": true,
"CloudDevOps": true
}
@@ -42,6 +42,7 @@
<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>
<flux:sidebar.group :heading="__('Configuration')" class="grid"><flux:sidebar.item icon="language" :href="route('administration.localization.index')" :current="request()->routeIs('administration.localization.*')" wire:navigate>{{ __('Localisation') }}</flux:sidebar.item><flux:sidebar.item icon="globe-alt" :href="route('administration.seo.index')" :current="request()->routeIs('administration.seo.*')" wire:navigate>{{ __('SEO') }}</flux:sidebar.item><flux:sidebar.item icon="cog-6-tooth" :href="route('administration.settings.index')" :current="request()->routeIs('administration.settings.*')" wire:navigate>{{ __('Settings') }}</flux:sidebar.item><flux:sidebar.item icon="cube" :href="route('administration.modules.index')" :current="request()->routeIs('administration.modules.*')" wire:navigate>{{ __('Modules') }}</flux:sidebar.item><flux:sidebar.item icon="clipboard-document-list" :href="route('administration.audit.index')" :current="request()->routeIs('administration.audit.*')" wire:navigate>{{ __('Audit Log') }}</flux:sidebar.item></flux:sidebar.group>
<flux:sidebar.group :heading="__('Operations')" class="grid"><flux:sidebar.item icon="cloud-arrow-up" :href="route('administration.cloud-devops.index')" :current="request()->routeIs('administration.cloud-devops.*')" wire:navigate>{{ __('Cloud & DevOps') }}</flux:sidebar.item></flux:sidebar.group>
</flux:sidebar.nav><flux:spacer /><x-stylist-theme-common::desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" /></flux:sidebar>
<flux:header class="lg:hidden"><flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" /><flux:spacer /><flux:dropdown position="top" align="end"><flux:profile :initials="auth()->user()->initials()" icon-trailing="chevron-down" /><flux:menu><flux:menu.radio.group><div class="p-0 text-sm font-normal"><div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm"><flux:avatar :name="auth()->user()->name" :initials="auth()->user()->initials()" /><div class="grid flex-1 text-start text-sm leading-tight"><flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading><flux:text class="truncate">{{ auth()->user()->email }}</flux:text></div></div></div></flux:menu.radio.group><flux:menu.separator /><flux:menu.radio.group><flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item></flux:menu.radio.group><flux:menu.separator /><form method="POST" action="{{ route('logout') }}" class="w-full">@csrf<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full cursor-pointer" data-test="logout-button">{{ __('Log out') }}</flux:menu.item></form></flux:menu></flux:dropdown></flux:header>
{{ $slot }}