diff --git a/AI/Config/config.php b/AI/Config/config.php
deleted file mode 100644
index 573f94d..0000000
--- a/AI/Config/config.php
+++ /dev/null
@@ -1,9 +0,0 @@
- env('AI_DEFAULT_PROVIDER'),
- 'timeout' => (int) env('AI_TIMEOUT', 60),
- 'max_output_tokens' => (int) env('AI_MAX_OUTPUT_TOKENS', 2048),
- 'temperature' => (float) env('AI_TEMPERATURE', 0.3),
- 'retain_executions_days' => (int) env('AI_RETAIN_EXECUTIONS_DAYS', 90),
-];
diff --git a/AI/Console/PruneAIExecutions.php b/AI/Console/PruneAIExecutions.php
deleted file mode 100644
index 5993517..0000000
--- a/AI/Console/PruneAIExecutions.php
+++ /dev/null
@@ -1,22 +0,0 @@
-option('days') ?: config('ai.retain_executions_days', 90));
- $count = AIExecution::where('created_at', '<', now()->subDays($days))->delete();
- $this->info("Deleted {$count} AI executions.");
-
- return self::SUCCESS;
- }
-}
diff --git a/AI/Console/TestAIProvider.php b/AI/Console/TestAIProvider.php
deleted file mode 100644
index 7b192bd..0000000
--- a/AI/Console/TestAIProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-testProvider((int) $this->argument('provider'));
- $this->{$result['ok'] ? 'info' : 'error'}($result['message']);
-
- return $result['ok'] ? self::SUCCESS : self::FAILURE;
- }
-}
diff --git a/AI/Contracts/AIManager.php b/AI/Contracts/AIManager.php
deleted file mode 100644
index d7cfe3a..0000000
--- a/AI/Contracts/AIManager.php
+++ /dev/null
@@ -1,13 +0,0 @@
-id();
- $table->string('name');
- $table->string('driver', 40)->default('openai_compatible');
- $table->string('base_url')->nullable();
- $table->text('api_key')->nullable();
- $table->string('default_model')->nullable();
- $table->json('models')->nullable();
- $table->json('options')->nullable();
- $table->boolean('is_active')->default(true)->index();
- $table->boolean('is_default')->default(false)->index();
- $table->timestamp('last_tested_at')->nullable();
- $table->string('last_test_status', 20)->nullable();
- $table->text('last_test_message')->nullable();
- $table->timestamps();
- });
- Schema::create('ai_prompts', function (Blueprint $table) {
- $table->id();
- $table->string('name');
- $table->string('slug')->unique();
- $table->string('purpose')->nullable()->index();
- $table->text('system_prompt')->nullable();
- $table->longText('user_template');
- $table->json('variables')->nullable();
- $table->unsignedInteger('version')->default(1);
- $table->boolean('is_active')->default(true)->index();
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- });
- Schema::create('ai_executions', function (Blueprint $table) {
- $table->id();
- $table->uuid('execution_id')->unique();
- $table->foreignId('ai_provider_id')->nullable()->constrained('ai_providers')->nullOnDelete();
- $table->foreignId('ai_prompt_id')->nullable()->constrained('ai_prompts')->nullOnDelete();
- $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
- $table->string('model')->nullable();
- $table->string('purpose')->nullable()->index();
- $table->longText('input')->nullable();
- $table->longText('output')->nullable();
- $table->json('context')->nullable();
- $table->json('usage')->nullable();
- $table->string('status', 20)->default('pending')->index();
- $table->text('error')->nullable();
- $table->unsignedInteger('duration_ms')->nullable();
- $table->timestamp('completed_at')->nullable();
- $table->timestamps();
- });
- Schema::create('ai_knowledge_sources', function (Blueprint $table) {
- $table->id();
- $table->string('name');
- $table->string('source_kind', 40)->default('manual')->index();
- $table->nullableMorphs('source');
- $table->longText('content')->nullable();
- $table->string('checksum', 64)->nullable()->index();
- $table->json('metadata')->nullable();
- $table->string('status', 20)->default('pending')->index();
- $table->unsignedInteger('chunk_count')->default(0);
- $table->timestamp('indexed_at')->nullable();
- $table->timestamps();
- });
- Schema::create('ai_knowledge_chunks', function (Blueprint $table) {
- $table->id();
- $table->foreignId('ai_knowledge_source_id')->constrained('ai_knowledge_sources')->cascadeOnDelete();
- $table->unsignedInteger('position');
- $table->longText('content');
- $table->json('metadata')->nullable();
- $table->timestamps();
- $table->unique(['ai_knowledge_source_id', 'position']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('ai_knowledge_chunks');
- Schema::dropIfExists('ai_knowledge_sources');
- Schema::dropIfExists('ai_executions');
- Schema::dropIfExists('ai_prompts');
- Schema::dropIfExists('ai_providers');
- }
-};
diff --git a/AI/Drivers/OpenAICompatibleDriver.php b/AI/Drivers/OpenAICompatibleDriver.php
deleted file mode 100644
index 74a7d5a..0000000
--- a/AI/Drivers/OpenAICompatibleDriver.php
+++ /dev/null
@@ -1,36 +0,0 @@
-timeout((int) config('ai.timeout', 60));
-
- return filled($provider->api_key) ? $client->withToken($provider->api_key) : $client;
- }
-
- public function generate(AIProvider $provider, array $messages, array $options = []): array
- {
- $base = rtrim((string) ($provider->base_url ?: 'https://api.openai.com/v1'), '/');
- $payload = ['model' => $options['model'] ?? $provider->default_model, 'messages' => $messages, 'temperature' => $options['temperature'] ?? config('ai.temperature', 0.3), 'max_tokens' => $options['max_tokens'] ?? config('ai.max_output_tokens', 2048)];
- $response = $this->client($provider)->post($base.'/chat/completions', array_filter($payload, fn ($v) => $v !== null));
- $response->throw();
- $json = $response->json();
-
- return ['text' => (string) data_get($json, 'choices.0.message.content', ''), 'usage' => (array) data_get($json, 'usage', []), 'raw' => $json];
- }
-
- public function test(AIProvider $provider): array
- {
- $result = $this->generate($provider, [['role' => 'user', 'content' => 'Reply with OK only.']], ['max_tokens' => 8, 'temperature' => 0]);
-
- return ['ok' => true, 'message' => trim($result['text']) ?: 'Provider responded successfully.'];
- }
-}
diff --git a/AI/Http/Controllers/AIApiController.php b/AI/Http/Controllers/AIApiController.php
deleted file mode 100644
index 77000a1..0000000
--- a/AI/Http/Controllers/AIApiController.php
+++ /dev/null
@@ -1,20 +0,0 @@
-validate(['variables' => 'nullable|array', 'provider_id' => 'nullable|integer', 'model' => 'nullable|string|max:150']);
- $execution = $manager->execute($prompt, $data['variables'] ?? [], array_filter(['provider_id' => $data['provider_id'] ?? null, 'model' => $data['model'] ?? null]));
-
- return response()->json(['data' => ['execution_id' => $execution->execution_id, 'status' => $execution->status, 'output' => $execution->output, 'usage' => $execution->usage, 'error' => $execution->error]], $execution->status === 'completed' ? 200 : 422);
- }
-}
diff --git a/AI/Http/Controllers/AIController.php b/AI/Http/Controllers/AIController.php
deleted file mode 100644
index 96c1f5f..0000000
--- a/AI/Http/Controllers/AIController.php
+++ /dev/null
@@ -1,69 +0,0 @@
- AIProvider::latest()->get(), 'prompts' => AIPrompt::latest()->paginate(15, '*', 'prompts'), 'executions' => AIExecution::latest()->limit(15)->get(), 'sources' => AIKnowledgeSource::latest()->limit(15)->get()]);
- }
-
- public function storeProvider(Request $request): RedirectResponse
- {
- $data = $request->validate(['name' => 'required|string|max:100', 'driver' => 'required|in:openai_compatible,openai,lm_studio,ollama', 'base_url' => 'nullable|url', 'api_key' => 'nullable|string', 'default_model' => 'required|string|max:150', 'is_default' => 'nullable|boolean']);
- if ($request->boolean('is_default')) {
- AIProvider::query()->update(['is_default' => false]);
- }
- $data['is_default'] = $request->boolean('is_default');
- AIProvider::create($data);
-
- return back()->with('status', 'AI provider created.');
- }
-
- public function testProvider(AIProvider $provider, AIManager $manager): RedirectResponse
- {
- $result = $manager->testProvider($provider->id);
-
- return back()->with($result['ok'] ? 'status' : 'error', $result['message']);
- }
-
- public function storePrompt(Request $request): RedirectResponse
- {
- $data = $request->validate(['name' => 'required|string|max:120', 'slug' => 'nullable|alpha_dash|max:140', 'purpose' => 'nullable|string|max:100', 'system_prompt' => 'nullable|string', 'user_template' => 'required|string']);
- $data['slug'] = $data['slug'] ?: Str::slug($data['name']);
- $data['created_by'] = auth()->id();
- AIPrompt::create($data);
-
- return back()->with('status', 'Prompt created.');
- }
-
- public function execute(Request $request, AIPrompt $prompt, AIManager $manager): RedirectResponse
- {
- $data = $request->validate(['input' => 'required|string|max:50000', 'provider_id' => 'nullable|exists:ai_providers,id']);
- $execution = $manager->execute($prompt, ['input' => $data['input']], ['provider_id' => $data['provider_id'] ?? null]);
-
- return back()->with($execution->status === 'completed' ? 'status' : 'error', $execution->status === 'completed' ? 'AI execution completed.' : $execution->error);
- }
-
- public function storeSource(Request $request, KnowledgeIndexer $indexer): RedirectResponse
- {
- $data = $request->validate(['name' => 'required|string|max:150', 'content' => 'required|string|max:1000000']);
- $source = AIKnowledgeSource::create(['name' => $data['name'], 'source_kind' => 'manual', 'content' => $data['content'], 'status' => 'pending']);
- $indexer->index($source);
-
- return back()->with('status','Knowledge source indexed.');
- }
-}
diff --git a/AI/Models/AIExecution.php b/AI/Models/AIExecution.php
deleted file mode 100644
index 62ea778..0000000
--- a/AI/Models/AIExecution.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'array', 'usage' => 'array', 'completed_at' => 'datetime'];
-}
diff --git a/AI/Models/AIKnowledgeChunk.php b/AI/Models/AIKnowledgeChunk.php
deleted file mode 100644
index 632ed3c..0000000
--- a/AI/Models/AIKnowledgeChunk.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'array'];
-}
diff --git a/AI/Models/AIKnowledgeSource.php b/AI/Models/AIKnowledgeSource.php
deleted file mode 100644
index f863a52..0000000
--- a/AI/Models/AIKnowledgeSource.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'array', 'indexed_at' => 'datetime'];
-
- public function source(): MorphTo
- {
- return $this->morphTo();
- }
-
- public function chunks(): HasMany
- {
- return $this->hasMany(AIKnowledgeChunk::class);
- }
-}
diff --git a/AI/Models/AIPrompt.php b/AI/Models/AIPrompt.php
deleted file mode 100644
index 5e1a23f..0000000
--- a/AI/Models/AIPrompt.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'array', 'is_active' => 'boolean'];
-
- public function render(array $variables): string
- {
- return preg_replace_callback('/\{\{\s*([\w.]+)\s*\}\}/', fn ($m) => (string) data_get($variables, $m[1], ''), $this->user_template) ?? $this->user_template;
- }
-}
diff --git a/AI/Models/AIProvider.php b/AI/Models/AIProvider.php
deleted file mode 100644
index f96e235..0000000
--- a/AI/Models/AIProvider.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'encrypted', 'models' => 'array', 'options' => 'array', 'is_active' => 'boolean', 'is_default' => 'boolean', 'last_tested_at' => 'datetime'];
-
- protected $hidden = ['api_key'];
-}
diff --git a/AI/Providers/AIServiceProvider.php b/AI/Providers/AIServiceProvider.php
deleted file mode 100644
index e7fec1b..0000000
--- a/AI/Providers/AIServiceProvider.php
+++ /dev/null
@@ -1,28 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('AI', 'Config/config.php'), 'ai');
- $this->app->singleton(AIManagerContract::class, AIManager::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('AI', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('AI', 'Resources/views'), 'ai');
- if ($this->app->runningInConsole()) {
- $this->commands([TestAIProvider::class, PruneAIExecutions::class]);
- }
- }
-}
diff --git a/AI/Providers/RouteServiceProvider.php b/AI/Providers/RouteServiceProvider.php
deleted file mode 100644
index 4306ef6..0000000
--- a/AI/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('AI', 'Routes/web.php'));
- Route::middleware('api')->prefix('api/v1')->group(module_path('AI', 'Routes/api.php'));
- }
-}
diff --git a/AI/Resources/views/admin/index.blade.php b/AI/Resources/views/admin/index.blade.php
deleted file mode 100644
index 3948fa0..0000000
--- a/AI/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
AI Platform Configure hosted or local models, reusable prompts, execution history and knowledge sources.
-@if(session('status'))
{{ session('status') }} @endif
-@if(session('error'))
{{ session('error') }} @endif
-
-Providers {{ $providers->count() }}
-Prompts {{ $prompts->total() }}
-Executions {{ \Modules\AI\Models\AIExecution::count() }}
-Knowledge sources {{ \Modules\AI\Models\AIKnowledgeSource::count() }}
-
-
-
Provider
-@foreach($providers as $provider)
{{ $provider->name }} {{ $provider->driver }} · {{ $provider->default_model }}
@endforeach
-
Reusable prompt
-
Knowledge source @foreach($sources as $source)
{{ $source->name }} {{ $source->status }} · {{ $source->chunk_count }} chunks
@endforeach
-
Recent executions @forelse($executions as $execution)
{{ $execution->purpose ?: 'Prompt execution' }} {{ $execution->status }}
{{ $execution->model }} · {{ $execution->duration_ms }} ms · {{ $execution->created_at?->diffForHumans() }}
@empty
No executions yet. @endforelse
-
diff --git a/AI/Routes/api.php b/AI/Routes/api.php
deleted file mode 100644
index 775f54c..0000000
--- a/AI/Routes/api.php
+++ /dev/null
@@ -1,6 +0,0 @@
-middleware(['auth.api', 'ability:ai.execute'])->name('api.ai.execute');
diff --git a/AI/Routes/web.php b/AI/Routes/web.php
deleted file mode 100644
index c2acf21..0000000
--- a/AI/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/ai')->name('administration.ai.')->group(function () {
- Route::get('/', [AIController::class, 'index'])->name('index');
- Route::post('/providers', [AIController::class, 'storeProvider'])->middleware('permission:ai.manage')->name('providers.store');
- Route::post('/providers/{provider}/test', [AIController::class, 'testProvider'])->middleware('permission:ai.manage')->name('providers.test');
- Route::post('/prompts', [AIController::class, 'storePrompt'])->middleware('permission:ai.manage')->name('prompts.store');
- Route::post('/prompts/{prompt}/execute', [AIController::class, 'execute'])->middleware('permission:ai.execute')->name('prompts.execute');
- Route::post('/knowledge', [AIController::class, 'storeSource'])->middleware('permission:ai.manage')->name('knowledge.store');
-});
diff --git a/AI/Services/AIManager.php b/AI/Services/AIManager.php
deleted file mode 100644
index 2f1c2c0..0000000
--- a/AI/Services/AIManager.php
+++ /dev/null
@@ -1,65 +0,0 @@
-where('is_active', true)->firstOrFail() : $prompt;
- $provider = $this->provider($options['provider_id'] ?? null);
- $input = $promptModel->render($variables);
- $execution = AIExecution::create(['execution_id' => (string) Str::uuid(), 'ai_provider_id' => $provider->id, 'ai_prompt_id' => $promptModel->id, 'user_id' => auth()->id(), 'model' => $options['model'] ?? $provider->default_model, 'purpose' => $promptModel->purpose, 'input' => $input, 'context' => $variables, 'status' => 'running']);
- $started = microtime(true);
- try {
- $messages = [];
- if (filled($promptModel->system_prompt)) {
- $messages[] = ['role' => 'system', 'content' => $promptModel->system_prompt];
- }
- $messages[] = ['role' => 'user', 'content' => $input];
- $result = $this->driver($provider)->generate($provider, $messages, $options);
- $execution->update(['output' => $result['text'], 'usage' => $result['usage'], 'status' => 'completed', 'duration_ms' => (int) ((microtime(true) - $started) * 1000), 'completed_at' => now()]);
- } catch (\Throwable $e) {
- report($e);
- $execution->update(['status' => 'failed', 'error' => $e->getMessage(), 'duration_ms' => (int) ((microtime(true) - $started) * 1000), 'completed_at' => now()]);
- }
-
- return $execution->refresh();
- }
-
- public function testProvider(int $providerId): array
- {
- $provider = AIProvider::findOrFail($providerId);
- try {
- $result = $this->driver($provider)->test($provider);
- $provider->update(['last_tested_at' => now(), 'last_test_status' => 'passed', 'last_test_message' => $result['message']]);
-
- return $result;
- } catch (\Throwable $e) {
- $provider->update(['last_tested_at' => now(), 'last_test_status' => 'failed', 'last_test_message' => $e->getMessage()]);
-
- return ['ok' => false, 'message' => $e->getMessage()];
- }
- }
-
- private function provider(?int $id): AIProvider
- {
- return AIProvider::query()->when($id, fn ($q) => $q->whereKey($id))->when(! $id, fn ($q) => $q->where('is_default', true))->where('is_active', true)->first()
- ?? AIProvider::where('is_active', true)->firstOrFail();
- }
-
- private function driver(AIProvider $provider): AIProviderDriver
- {
- return match ($provider->driver) {
- 'openai_compatible','openai','lm_studio','ollama' => app(OpenAICompatibleDriver::class), default => throw new \InvalidArgumentException("Unsupported AI driver [{$provider->driver}].")
- };
- }
-}
diff --git a/AI/Services/KnowledgeIndexer.php b/AI/Services/KnowledgeIndexer.php
deleted file mode 100644
index 9c21d83..0000000
--- a/AI/Services/KnowledgeIndexer.php
+++ /dev/null
@@ -1,39 +0,0 @@
-content);
- $source->chunks()->delete();
- $chunks = $content === '' ? [] : $this->split($content, max(500, $chunkSize));
- foreach ($chunks as $position => $chunk) {
- $source->chunks()->create(['position' => $position + 1, 'content' => $chunk]);
- }
- $source->update(['checksum' => hash('sha256', $content), 'status' => 'indexed', 'chunk_count' => count($chunks), 'indexed_at' => now()]);
-
- return $source->refresh();
- }
-
- private function split(string $content, int $size): array
- {
- $paragraphs = preg_split('/\R{2,}/', $content) ?: [$content];
- $chunks = [];
- $buffer = '';
- foreach ($paragraphs as $paragraph) {
- if (strlen($buffer) + strlen($paragraph) + 2 > $size && $buffer !== '') {
- $chunks[] = trim($buffer);
- $buffer = '';
- } $buffer .= $paragraph."\n\n";
- }
- if (trim($buffer) !== '') {
- $chunks[] = trim($buffer);
- }
-
-return $chunks;
- }
-}
diff --git a/AI/Tests/Feature/ModuleBootTest.php b/AI/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index ebc1cac..0000000
--- a/AI/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,16 +0,0 @@
-assertTrue(class_exists(AIServiceProvider::class));
- $this->assertTrue(app()->bound(AIManager::class));
- }
-}
diff --git a/AI/module.json b/AI/module.json
deleted file mode 100644
index dda2c66..0000000
--- a/AI/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"AI","alias":"ai","version":"1.0.0","description":"Provider-neutral artificial intelligence services, reusable prompts, execution history and knowledge sources.","keywords":["ai","llm","prompts","knowledge","rag"],"priority":140,"providers":["Modules\\AI\\Providers\\AIServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Media","Documents","Automation"]}
diff --git a/Administration/Config/config.php b/Administration/Config/config.php
deleted file mode 100644
index bb128b8..0000000
--- a/Administration/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Administration',
- 'enabled' => true,
-];
diff --git a/Administration/Http/Controllers/DashboardController.php b/Administration/Http/Controllers/DashboardController.php
deleted file mode 100644
index fd399d5..0000000
--- a/Administration/Http/Controllers/DashboardController.php
+++ /dev/null
@@ -1,82 +0,0 @@
-backend()->entryView(), [
- 'userCount' => User::count(),
- 'roleCount' => Schema::hasTable('roles') ? Role::count() : 0,
- 'mediaCount' => Schema::hasTable('media') ? Media::count() : 0,
- 'auditCount' => Schema::hasTable('audit_logs') ? AuditLog::count() : 0,
- 'recentAudit' => $this->recentAudit(),
- 'modules' => $this->modules(),
- ]);
- }
-
- private function recentAudit(): LengthAwarePaginator
- {
- if (! Schema::hasTable('audit_logs')) {
- return new LengthAwarePaginator(
- items: [],
- total: 0,
- perPage: 8,
- currentPage: Paginator::resolveCurrentPage('audit_page'),
- options: [
- 'path' => request()->url(),
- 'pageName' => 'audit_page',
- ],
- );
- }
-
- return AuditLog::query()
- ->latest('created_at')
- ->paginate(8, ['*'], 'audit_page')
- ->withQueryString();
- }
-
- private function modules(): LengthAwarePaginator
- {
- $statuses = json_decode((string) file_get_contents(base_path('modules_statuses.json')), true) ?: [];
- $modules = [];
-
- foreach (glob(base_path('Modules/*/module.json')) ?: [] as $manifest) {
- $data = json_decode((string) file_get_contents($manifest), true) ?: [];
- $name = $data['name'] ?? basename(dirname($manifest));
-
- $modules[] = [
- 'name' => $name,
- 'version' => $data['version'] ?? '1.0.0',
- 'enabled' => (bool) ($statuses[$name] ?? false),
- ];
- }
-
- usort($modules, fn (array $a, array $b): int => strcasecmp($a['name'], $b['name']));
-
- $pageName = 'modules_page';
- $perPage = 8;
- $currentPage = Paginator::resolveCurrentPage($pageName);
-
- return (new LengthAwarePaginator(
- items: array_slice($modules, ($currentPage - 1) * $perPage, $perPage),
- total: count($modules),
- perPage: $perPage,
- currentPage: $currentPage,
- options: [
- 'path' => request()->url(),
- 'pageName' => $pageName,
- ],
- ))->withQueryString();
- }
-}
diff --git a/Administration/Http/Controllers/ModuleController.php b/Administration/Http/Controllers/ModuleController.php
deleted file mode 100644
index e277d1c..0000000
--- a/Administration/Http/Controllers/ModuleController.php
+++ /dev/null
@@ -1,27 +0,0 @@
-map(function (string $manifest) use ($statuses): array {
- $data = json_decode((string) file_get_contents($manifest), true) ?: [];
- $name = $data['name'] ?? basename(dirname($manifest));
-
- return [
- 'name' => $name,
- 'description' => $data['description'] ?? 'Laravel application module.',
- 'version' => $data['version'] ?? '1.0.0',
- 'enabled' => (bool) ($statuses[$name] ?? false),
- 'providers' => $data['providers'] ?? [],
- ];
- })->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE)->values();
-
- return view('administration::modules.index', compact('modules'));
- }
-}
diff --git a/Administration/Providers/AdministrationServiceProvider.php b/Administration/Providers/AdministrationServiceProvider.php
deleted file mode 100644
index 9cf7292..0000000
--- a/Administration/Providers/AdministrationServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Administration/Providers/RouteServiceProvider.php b/Administration/Providers/RouteServiceProvider.php
deleted file mode 100644
index 2f44254..0000000
--- a/Administration/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Administration', 'Routes/web.php'));
- });
- }
-}
diff --git a/Administration/Resources/views/modules/index.blade.php b/Administration/Resources/views/modules/index.blade.php
deleted file mode 100644
index 69bbd42..0000000
--- a/Administration/Resources/views/modules/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Modules Installed platform modules and runtime status.
Module Version Status Providers @foreach($modules as $module){{ $module['name'] }}
{{ $module['description'] }}
{{ $module['version'] }} {{ $module['enabled'] ? 'Enabled' : 'Disabled' }} {{ implode(', ', $module['providers']) ?: '—' }} @endforeach
diff --git a/Administration/Routes/web.php b/Administration/Routes/web.php
deleted file mode 100644
index ab08cb2..0000000
--- a/Administration/Routes/web.php
+++ /dev/null
@@ -1,8 +0,0 @@
-prefix('administration')->name('administration.')->group(function (): void {
- Route::get('/modules', [ModuleController::class, 'index'])->name('modules.index');
-});
diff --git a/Administration/Support/ModuleInfo.php b/Administration/Support/ModuleInfo.php
deleted file mode 100644
index 51ef6a8..0000000
--- a/Administration/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Administration');
-});
-
-it('uses the primary dashboard for administration overview content', function (): void {
- $dashboard = Route::getRoutes()->getByName('dashboard');
-
- expect($dashboard)->not->toBeNull()
- ->and($dashboard->getActionName())->toBe(DashboardController::class)
- ->and(view()->exists('stylist-theme-admin::dashboard'))->toBeTrue()
- ->and(Route::has('administration.dashboard'))->toBeFalse();
-});
diff --git a/Administration/composer.json b/Administration/composer.json
deleted file mode 100644
index 3a6aff0..0000000
--- a/Administration/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/administration-module",
- "description": "Administration foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Administration\\": ""
- }
- }
-}
diff --git a/Administration/module.json b/Administration/module.json
deleted file mode 100644
index 74cfa82..0000000
--- a/Administration/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Administration",
- "alias": "administration",
- "version": "1.0.0",
- "description": "Administration foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "administration"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Administration\\Providers\\AdministrationServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Analytics/Config/config.php b/Analytics/Config/config.php
deleted file mode 100644
index b2d0b76..0000000
--- a/Analytics/Config/config.php
+++ /dev/null
@@ -1,10 +0,0 @@
- (bool) env('ANALYTICS_ENABLED', true),
- 'respect_dnt' => (bool) env('ANALYTICS_RESPECT_DNT', true),
- 'anonymize_ip' => (bool) env('ANALYTICS_ANONYMIZE_IP', true),
- 'retention_days' => (int) env('ANALYTICS_RETENTION_DAYS', 365),
- 'exclude_paths' => array_values(array_filter(array_map('trim', explode(',', (string) env('ANALYTICS_EXCLUDE_PATHS', 'administration,api'))))),
- 'queue' => env('ANALYTICS_QUEUE', 'default'),
-];
diff --git a/Analytics/Console/AggregateAnalytics.php b/Analytics/Console/AggregateAnalytics.php
deleted file mode 100644
index ae896e5..0000000
--- a/Analytics/Console/AggregateAnalytics.php
+++ /dev/null
@@ -1,20 +0,0 @@
-info('Metrics written: '.$aggregator->aggregate($this->option('date')));
-
- return self::SUCCESS;
- }
-}
diff --git a/Analytics/Console/PruneAnalytics.php b/Analytics/Console/PruneAnalytics.php
deleted file mode 100644
index ead8b95..0000000
--- a/Analytics/Console/PruneAnalytics.php
+++ /dev/null
@@ -1,21 +0,0 @@
-subDays(config('analytics.retention_days', 365)))->delete();
- $this->info("Deleted {$count} events.");
-
- return self::SUCCESS;
- }
-}
diff --git a/Analytics/Console/RunScheduledReports.php b/Analytics/Console/RunScheduledReports.php
deleted file mode 100644
index e169620..0000000
--- a/Analytics/Console/RunScheduledReports.php
+++ /dev/null
@@ -1,21 +0,0 @@
-whereNotNull('next_run_at')->where('next_run_at', '<=', now())->update(['last_run_at' => now(), 'next_run_at' => now()->addDay()]);
- $this->info("Processed {$count} scheduled reports.");
-
- return self::SUCCESS;
- }
-}
diff --git a/Analytics/Database/Migrations/2026_07_24_150000_create_analytics_tables.php b/Analytics/Database/Migrations/2026_07_24_150000_create_analytics_tables.php
deleted file mode 100644
index 4cea18d..0000000
--- a/Analytics/Database/Migrations/2026_07_24_150000_create_analytics_tables.php
+++ /dev/null
@@ -1,80 +0,0 @@
-id();
- $table->uuid('event_id')->unique();
- $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete();
- $table->string('event_type', 40)->index();
- $table->string('path', 2048)->nullable();
- $table->string('title')->nullable();
- $table->string('referrer', 2048)->nullable();
- $table->string('visitor_hash', 64)->nullable()->index();
- $table->string('session_hash', 64)->nullable()->index();
- $table->string('ip_hash', 64)->nullable();
- $table->string('country_code', 2)->nullable()->index();
- $table->string('device_type', 20)->nullable()->index();
- $table->string('browser', 40)->nullable();
- $table->string('operating_system', 40)->nullable();
- $table->nullableMorphs('subject');
- $table->json('properties')->nullable();
- $table->timestamp('occurred_at')->index();
- $table->timestamps();
- $table->index(['site_id', 'event_type', 'occurred_at']);
- });
- Schema::create('analytics_daily_metrics', function (Blueprint $table) {
- $table->id();
- $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete();
- $table->date('metric_date')->index();
- $table->string('metric', 60);
- $table->string('dimension', 120)->default('all');
- $table->unsignedBigInteger('value')->default(0);
- $table->json('metadata')->nullable();
- $table->timestamps();
- $table->unique(['site_id', 'metric_date', 'metric', 'dimension'], 'analytics_daily_unique');
- });
- Schema::create('analytics_reports', function (Blueprint $table) {
- $table->id();
- $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete();
- $table->string('name');
- $table->string('report_type', 40)->default('overview');
- $table->json('filters')->nullable();
- $table->json('columns')->nullable();
- $table->string('schedule')->nullable();
- $table->string('format', 10)->default('csv');
- $table->json('recipients')->nullable();
- $table->boolean('is_active')->default(true)->index();
- $table->timestamp('last_run_at')->nullable();
- $table->timestamp('next_run_at')->nullable()->index();
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- });
- Schema::create('analytics_report_runs', function (Blueprint $table) {
- $table->id();
- $table->foreignId('analytics_report_id')->constrained('analytics_reports')->cascadeOnDelete();
- $table->string('status', 20)->default('pending')->index();
- $table->string('disk')->nullable();
- $table->string('path')->nullable();
- $table->unsignedBigInteger('row_count')->default(0);
- $table->text('error')->nullable();
- $table->timestamp('started_at')->nullable();
- $table->timestamp('completed_at')->nullable();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('analytics_report_runs');
- Schema::dropIfExists('analytics_reports');
- Schema::dropIfExists('analytics_daily_metrics');
- Schema::dropIfExists('analytics_events');
- }
-};
diff --git a/Analytics/Http/Controllers/AnalyticsController.php b/Analytics/Http/Controllers/AnalyticsController.php
deleted file mode 100644
index 4552d84..0000000
--- a/Analytics/Http/Controllers/AnalyticsController.php
+++ /dev/null
@@ -1,43 +0,0 @@
-integer('days', 30)));
- $from = now()->subDays($days - 1)->startOfDay();
- $events = AnalyticsEvent::query()->where('occurred_at', '>=', $from);
-
- return view('analytics::index', [
- 'days' => $days, 'pageViews' => $events->clone()->where('event_type', 'page_view')->count(),
- 'visitors' => $events->clone()->whereNotNull('visitor_hash')->distinct('visitor_hash')->count('visitor_hash'),
- 'conversions' => $events->clone()->where('event_type', 'conversion')->count(),
- 'searches' => $events->clone()->where('event_type', 'search')->count(),
- 'topPages' => $events->clone()->where('event_type', 'page_view')->selectRaw('path, count(*) as total')->groupBy('path')->orderByDesc('total')->limit(10)->get(),
- 'recentEvents' => $events->clone()->latest('occurred_at')->limit(20)->get(), 'reports' => AnalyticsReport::latest()->limit(20)->get(),
- ]);
- }
-
- public function storeReport(Request $request): RedirectResponse
- {
- AnalyticsReport::create($request->validate(['name' => 'required|string|max:255', 'report_type' => 'required|in:overview,content,search,conversion,commerce', 'schedule' => 'nullable|string|max:50', 'format' => 'required|in:csv,json', 'is_active' => 'nullable|boolean']) + ['created_by' => $request->user()->id, 'is_active' => $request->boolean('is_active', true)]);
-
- return back()->with('status', 'Report created.');
- }
-
- public function destroyReport(AnalyticsReport $report): RedirectResponse
- {
- $report->delete();
-
- return back()->with('status','Report deleted.');
- }
-}
diff --git a/Analytics/Http/Controllers/TrackingController.php b/Analytics/Http/Controllers/TrackingController.php
deleted file mode 100644
index 0523987..0000000
--- a/Analytics/Http/Controllers/TrackingController.php
+++ /dev/null
@@ -1,25 +0,0 @@
-validate([
- 'event_type' => ['required', Rule::in(['page_view', 'search', 'conversion', 'content_view', 'form_submit', 'commerce'])],
- 'path' => ['nullable', 'string', 'max:2048'], 'title' => ['nullable', 'string', 'max:255'], 'referrer' => ['nullable', 'string', 'max:2048'],
- 'visitor_id' => ['nullable', 'string', 'max:255'], 'session_id' => ['nullable', 'string', 'max:255'],
- 'subject_type' => ['nullable', 'string', 'max:255'], 'subject_id' => ['nullable', 'integer'], 'properties' => ['nullable', 'array'],
- ]);
- $event = $tracker->track($request, $data);
-
- return response()->json(['recorded' => (bool) $event], $event ? 201 : 202);
- }
-}
diff --git a/Analytics/Models/AnalyticsEvent.php b/Analytics/Models/AnalyticsEvent.php
deleted file mode 100644
index 389e863..0000000
--- a/Analytics/Models/AnalyticsEvent.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'array', 'occurred_at' => 'datetime'];
-}
diff --git a/Analytics/Models/AnalyticsReport.php b/Analytics/Models/AnalyticsReport.php
deleted file mode 100644
index 3d8a81c..0000000
--- a/Analytics/Models/AnalyticsReport.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'array', 'columns' => 'array', 'recipients' => 'array', 'is_active' => 'boolean', 'last_run_at' => 'datetime', 'next_run_at' => 'datetime'];
-}
diff --git a/Analytics/Models/AnalyticsReportRun.php b/Analytics/Models/AnalyticsReportRun.php
deleted file mode 100644
index 53930be..0000000
--- a/Analytics/Models/AnalyticsReportRun.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'datetime', 'completed_at' => 'datetime'];
-}
diff --git a/Analytics/Models/DailyMetric.php b/Analytics/Models/DailyMetric.php
deleted file mode 100644
index 745d269..0000000
--- a/Analytics/Models/DailyMetric.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'date', 'metadata' => 'array'];
-}
diff --git a/Analytics/Providers/AnalyticsServiceProvider.php b/Analytics/Providers/AnalyticsServiceProvider.php
deleted file mode 100644
index ce1b3d5..0000000
--- a/Analytics/Providers/AnalyticsServiceProvider.php
+++ /dev/null
@@ -1,32 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Analytics', 'Config/config.php'), 'analytics');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Analytics', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Analytics', 'Resources/views'), 'analytics');
- if ($this->app->runningInConsole()) {
- $this->commands([AggregateAnalytics::class, PruneAnalytics::class, RunScheduledReports::class]);
- $this->callAfterResolving(Schedule::class, function (Schedule $schedule): void {
- $schedule->command('analytics:aggregate')->hourly()->withoutOverlapping();
- $schedule->command('analytics:reports')->everyFifteenMinutes()->withoutOverlapping();
- $schedule->command('analytics:prune')->dailyAt('02:30')->withoutOverlapping();
- });
- }
- }
-}
diff --git a/Analytics/Providers/RouteServiceProvider.php b/Analytics/Providers/RouteServiceProvider.php
deleted file mode 100644
index 9d62eec..0000000
--- a/Analytics/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Analytics', 'Routes/web.php'));
- Route::middleware('api')->prefix('api/v1')->group(module_path('Analytics', 'Routes/api.php'));
- }
-}
diff --git a/Analytics/Resources/views/index.blade.php b/Analytics/Resources/views/index.blade.php
deleted file mode 100644
index 66e7cd8..0000000
--- a/Analytics/Resources/views/index.blade.php
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
Analytics Visitor, content, search and conversion performance.
- @if(session('status'))
{{ session('status') }} @endif
-
- @foreach([['Page views',$pageViews],['Visitors',$visitors],['Conversions',$conversions],['Searches',$searches]] as [$label,$value])
{{ $label }}
{{ number_format($value) }}
@endforeach
-
-
-
Top pages @forelse($topPages as $page)
{{ $page->path ?: '/' }} {{ number_format($page->total) }}
@empty
No page views recorded.
@endforelse
-
Recent events @forelse($recentEvents as $event)
{{ str($event->event_type)->replace('_',' ')->title() }} {{ $event->occurred_at?->diffForHumans() }}
{{ $event->path ?: data_get($event->properties,'query','—') }}
@empty
No events recorded.
@endforelse
-
- @can('analytics.manage')
Scheduled reports @forelse($reports as $report)
{{ $report->name }} {{ ucfirst($report->report_type) }} · {{ strtoupper($report->format) }}
@empty
No reports configured.
@endforelse
@endcan
-
-
diff --git a/Analytics/Routes/api.php b/Analytics/Routes/api.php
deleted file mode 100644
index 4ae8da7..0000000
--- a/Analytics/Routes/api.php
+++ /dev/null
@@ -1,6 +0,0 @@
-middleware('throttle:60,1')->name('api.analytics.events.store');
diff --git a/Analytics/Routes/web.php b/Analytics/Routes/web.php
deleted file mode 100644
index 5a5437e..0000000
--- a/Analytics/Routes/web.php
+++ /dev/null
@@ -1,10 +0,0 @@
-prefix('administration/analytics')->name('administration.analytics.')->group(function () {
- Route::get('/', [AnalyticsController::class, 'index'])->name('index');
- Route::post('/reports', [AnalyticsController::class, 'storeReport'])->middleware('permission:analytics.manage')->name('reports.store');
- Route::delete('/reports/{report}', [AnalyticsController::class, 'destroyReport'])->middleware('permission:analytics.manage')->name('reports.destroy');
-});
diff --git a/Analytics/Services/AnalyticsAggregator.php b/Analytics/Services/AnalyticsAggregator.php
deleted file mode 100644
index 0f08c0d..0000000
--- a/Analytics/Services/AnalyticsAggregator.php
+++ /dev/null
@@ -1,32 +0,0 @@
-subHour()->toDateString();
- $written = 0;
- $base = AnalyticsEvent::query()->whereDate('occurred_at', $date);
- foreach ($base->clone()->select('site_id')->distinct()->pluck('site_id') as $siteId) {
- $query = $base->clone()->where('site_id', $siteId);
- $metrics = [
- ['page_views', 'all', $query->clone()->where('event_type', 'page_view')->count()],
- ['visitors', 'all', $query->clone()->whereNotNull('visitor_hash')->distinct('visitor_hash')->count('visitor_hash')],
- ['sessions', 'all', $query->clone()->whereNotNull('session_hash')->distinct('session_hash')->count('session_hash')],
- ['conversions', 'all', $query->clone()->where('event_type', 'conversion')->count()],
- ['searches', 'all', $query->clone()->where('event_type', 'search')->count()],
- ];
- foreach ($metrics as [$metric,$dimension,$value]) {
- DailyMetric::updateOrCreate(['site_id' => $siteId, 'metric_date' => $date, 'metric' => $metric, 'dimension' => $dimension], ['value' => $value]);
- $written++;
- }
- }
-
- return $written;
- }
-}
diff --git a/Analytics/Services/AnalyticsTracker.php b/Analytics/Services/AnalyticsTracker.php
deleted file mode 100644
index 4c37c77..0000000
--- a/Analytics/Services/AnalyticsTracker.php
+++ /dev/null
@@ -1,61 +0,0 @@
-header('DNT') === '1') {
- return null;
- }
-
- $path = ltrim((string) ($payload['path'] ?? ''), '/');
-
- foreach (config('analytics.exclude_paths', []) as $excluded) {
- if ($excluded !== '' && str_starts_with($path, trim($excluded, '/'))) {
- return null;
- }
- }
-
- $ip = (string) $request->ip();
- $visitorIdentifier = (string) ($payload['visitor_id'] ?? $request->cookie('analytics_visitor', 'guest'));
- $sessionIdentifier = (string) ($payload['session_id'] ?? $this->sessionIdentifier($request, $visitorIdentifier));
-
- return AnalyticsEvent::create([
- 'event_id' => (string) Str::uuid(),
- 'site_id' => app()->bound('currentSite') ? app('currentSite')?->id : null,
- 'event_type' => $payload['event_type'] ?? 'page_view',
- 'path' => '/'.$path,
- 'title' => $payload['title'] ?? null,
- 'referrer' => $payload['referrer'] ?? $request->headers->get('referer'),
- 'visitor_hash' => hash('sha256', $visitorIdentifier),
- 'session_hash' => hash('sha256', $sessionIdentifier),
- 'ip_hash' => config('analytics.anonymize_ip') ? hash('sha256', $ip.config('app.key')) : $ip,
- 'device_type' => $payload['device_type'] ?? null,
- 'browser' => $payload['browser'] ?? null,
- 'operating_system' => $payload['operating_system'] ?? null,
- 'subject_type' => $payload['subject_type'] ?? null,
- 'subject_id' => $payload['subject_id'] ?? null,
- 'properties' => $payload['properties'] ?? [],
- 'occurred_at' => now(),
- ]);
- }
-
- private function sessionIdentifier(Request $request, string $visitorIdentifier): string
- {
- if ($request->hasSession()) {
- return $request->session()->getId();
- }
-
- return (string) ($request->header('X-Analytics-Session') ?: $visitorIdentifier ?: 'stateless');
- }
-}
diff --git a/Analytics/Tests/Feature/AnalyticsModuleTest.php b/Analytics/Tests/Feature/AnalyticsModuleTest.php
deleted file mode 100644
index ce99fab..0000000
--- a/Analytics/Tests/Feature/AnalyticsModuleTest.php
+++ /dev/null
@@ -1,17 +0,0 @@
-postJson('/api/v1/analytics/events', ['event_type' => 'page_view', 'path' => '/about'])->assertCreated()->assertJson(['recorded' => true]);
- $this->assertDatabaseHas('analytics_events', ['event_type' => 'page_view', 'path' => '/about']);
- }
-}
diff --git a/Analytics/module.json b/Analytics/module.json
deleted file mode 100644
index 7afb408..0000000
--- a/Analytics/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Analytics","alias":"analytics","version":"1.0.0","description":"Privacy-conscious visitor, content, search, conversion and report analytics.","keywords":["analytics","reports","metrics","visitors","conversions"],"priority":150,"providers":["Modules\\Analytics\\Providers\\AnalyticsServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Sites","Search","Forms","Newsletter","Commerce"]}
diff --git a/Api/Config/config.php b/Api/Config/config.php
deleted file mode 100644
index b11bfae..0000000
--- a/Api/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'api/v1', 'token_prefix' => 'cms_', 'pagination' => 20, 'webhook_timeout' => 10, 'openapi' => ['title' => env('APP_NAME', 'Content Manager').' API', 'version' => '1.0.0']];
diff --git a/Api/Database/Migrations/2026_07_24_090000_create_api_tokens_table.php b/Api/Database/Migrations/2026_07_24_090000_create_api_tokens_table.php
deleted file mode 100644
index bb0d7b7..0000000
--- a/Api/Database/Migrations/2026_07_24_090000_create_api_tokens_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-id();
- $t->foreignId('user_id')->constrained()->cascadeOnDelete();
- $t->string('name');
- $t->string('token', 64)->unique();
- $t->json('abilities')->nullable();
- $t->timestamp('last_used_at')->nullable();
- $t->timestamp('expires_at')->nullable();
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('api_tokens');
- }
-};
diff --git a/Api/Database/Migrations/2026_07_24_090100_create_webhooks_tables.php b/Api/Database/Migrations/2026_07_24_090100_create_webhooks_tables.php
deleted file mode 100644
index 113840c..0000000
--- a/Api/Database/Migrations/2026_07_24_090100_create_webhooks_tables.php
+++ /dev/null
@@ -1,43 +0,0 @@
-id();
- $t->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete();
- $t->string('name');
- $t->string('url');
- $t->string('secret');
- $t->json('events');
- $t->boolean('is_active')->default(true);
- $t->unsignedSmallInteger('failure_count')->default(0);
- $t->timestamp('last_triggered_at')->nullable();
- $t->timestamps();
- });
- Schema::create('webhook_deliveries', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('webhook_id')->constrained()->cascadeOnDelete();
- $t->uuid('uuid')->unique();
- $t->string('event');
- $t->json('payload');
- $t->unsignedSmallInteger('response_status')->nullable();
- $t->text('response_body')->nullable();
- $t->unsignedSmallInteger('attempts')->default(0);
- $t->timestamp('delivered_at')->nullable();
- $t->timestamp('failed_at')->nullable();
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('webhook_deliveries');
- Schema::dropIfExists('webhooks');
- }
-};
diff --git a/Api/Http/Controllers/ApiController.php b/Api/Http/Controllers/ApiController.php
deleted file mode 100644
index fbe22bf..0000000
--- a/Api/Http/Controllers/ApiController.php
+++ /dev/null
@@ -1,49 +0,0 @@
- ApiToken::with('user')->latest()->paginate(20, '*', 'tokens'), 'webhooks' => Webhook::latest()->paginate(20, '*', 'webhooks')]);
- }
-
- public function storeToken(Request $r): RedirectResponse
- {
- $d = $r->validate(['name' => 'required|string|max:255', 'abilities' => 'nullable|string', 'expires_at' => 'nullable|date|after:now']);
- $abilities = array_values(array_filter(array_map('trim', explode(',', $d['abilities'] ?: '*'))));
- [,$plain] = ApiToken::issue($r->user(), $d['name'], $abilities, isset($d['expires_at']) ? new \DateTimeImmutable($d['expires_at']) : null);
-
- return back()->with('api_token', $plain)->with('status', 'API token created.');
- }
-
- public function destroyToken(ApiToken $token): RedirectResponse
- {
- $token->delete();
-
- return back()->with('status', 'API token revoked.');
- }
-
- public function storeWebhook(Request $r): RedirectResponse
- {
- $d = $r->validate(['name' => 'required|string|max:255', 'url' => 'required|url|max:2048', 'events' => 'required|string', 'site_id' => 'nullable|integer|exists:sites,id']);
- Webhook::create(['name' => $d['name'], 'url' => $d['url'], 'events' => array_values(array_filter(array_map('trim', explode(',', $d['events'])))), 'site_id' => $d['site_id'] ?? null, 'secret' => Str::random(48), 'is_active' => true]);
-
- return back()->with('status', 'Webhook created.');
- }
-
- public function destroyWebhook(Webhook $webhook): RedirectResponse
- {
- $webhook->delete();
-
- return back()->with('status','Webhook deleted.');
- }
-}
diff --git a/Api/Http/Controllers/ContentController.php b/Api/Http/Controllers/ContentController.php
deleted file mode 100644
index 5be30a0..0000000
--- a/Api/Http/Controllers/ContentController.php
+++ /dev/null
@@ -1,54 +0,0 @@
-many(Page::query()->where('status', 'published'));
- }
-
- public function blog(): AnonymousResourceCollection
- {
- return $this->many(Blog::query()->published());
- }
-
- public function news(): AnonymousResourceCollection
- {
- return $this->many(News::query()->published());
- }
-
- public function page(Page $page): ContentResource
- {
- abort_unless($page->status === 'published', 404);
-
- return new ContentResource($page);
- }
-
- public function blogPost(Blog $blog): ContentResource
- {
- abort_unless(Blog::query()->published()->whereKey($blog)->exists(), 404);
-
- return new ContentResource($blog);
- }
-
- public function newsArticle(News $news): ContentResource
- {
- abort_unless(News::query()->published()->whereKey($news)->exists(), 404);
-
- return new ContentResource($news);
- }
-
- private function many(Builder $q): AnonymousResourceCollection
- {
- return ContentResource::collection($q->latest('updated_at')->paginate((int) config('api.pagination', 20)));
- }
-}
diff --git a/Api/Http/Controllers/OpenApiController.php b/Api/Http/Controllers/OpenApiController.php
deleted file mode 100644
index f609f27..0000000
--- a/Api/Http/Controllers/OpenApiController.php
+++ /dev/null
@@ -1,13 +0,0 @@
-json(['openapi' => '3.1.0', 'info' => config('api.openapi'), 'servers' => [['url' => url('/'.config('api.prefix', 'api/v1'))]], 'components' => ['securitySchemes' => ['bearerAuth' => ['type' => 'http', 'scheme' => 'bearer']]], 'paths' => ['/pages' => ['get' => ['summary' => 'List published pages', 'responses' => ['200' => ['description' => 'OK']]]], '/blog' => ['get' => ['summary' => 'List published blog posts', 'responses' => ['200' => ['description' => 'OK']]]], '/news' => ['get' => ['summary' => 'List published news', 'responses' => ['200' => ['description' => 'OK']]]], '/user' => ['get' => ['summary' => 'Authenticated user', 'security' => [['bearerAuth' => []]], 'responses' => ['200' => ['description' => 'OK'], '401' => ['description' => 'Unauthenticated']]]]]]);
- }
-}
diff --git a/Api/Http/Middleware/AuthenticateApiToken.php b/Api/Http/Middleware/AuthenticateApiToken.php
deleted file mode 100644
index 236b1b5..0000000
--- a/Api/Http/Middleware/AuthenticateApiToken.php
+++ /dev/null
@@ -1,28 +0,0 @@
-bearerToken();
- if (! $plain) {
- return response()->json(['message' => 'Unauthenticated.'], 401);
- }$token = ApiToken::with('user')->where('token', hash('sha256', $plain))->first();
- if (! $token || ($token->expires_at && $token->expires_at->isPast()) || ! $token->user) {
- return response()->json(['message' => 'Invalid or expired API token.'], 401);
- }if ($ability && ! $token->allows($ability)) {
- return response()->json(['message' => 'Insufficient token ability.'], 403);
- }$token->update(['last_used_at' => now()]);
- auth()->setUser($token->user);
- $r->attributes->set('apiToken', $token);
-
- return $next($r);
- }
-}
diff --git a/Api/Http/Resources/ContentResource.php b/Api/Http/Resources/ContentResource.php
deleted file mode 100644
index 1ac7b0a..0000000
--- a/Api/Http/Resources/ContentResource.php
+++ /dev/null
@@ -1,14 +0,0 @@
- $this->id, 'type' => $this->resource->getTable(), 'title' => $this->title, 'slug' => $this->slug, 'excerpt' => $this->excerpt ?? null, 'content' => $this->content ?? null, 'status' => $this->status ?? null, 'featured_image' => $this->featured_image ?? null, 'published_at' => $this->published_at ?? null, 'updated_at' => $this->updated_at];
- }
-}
diff --git a/Api/Models/ApiToken.php b/Api/Models/ApiToken.php
deleted file mode 100644
index 6de3874..0000000
--- a/Api/Models/ApiToken.php
+++ /dev/null
@@ -1,40 +0,0 @@
- 'array', 'last_used_at' => 'datetime', 'expires_at' => 'datetime'];
- }
-
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class);
- }
-
- public static function issue(User $u, string $name, array $abilities = ['*'], ?\DateTimeInterface $expires = null): array
- {
- $plain = config('api.token_prefix', 'cms_').Str::random(64);
- $record = self::query()->create(['user_id' => $u->id, 'name' => $name, 'token' => hash('sha256', $plain), 'abilities' => $abilities, 'expires_at' => $expires]);
-
- return [$record, $plain];
- }
-
- public function allows(string $ability): bool
- {
- $a = $this->abilities ?: [];
-
- return in_array('*', $a, true) || in_array($ability, $a, true);
- }
-}
diff --git a/Api/Models/Webhook.php b/Api/Models/Webhook.php
deleted file mode 100644
index 28d08ef..0000000
--- a/Api/Models/Webhook.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'array', 'is_active' => 'boolean', 'last_triggered_at' => 'datetime'];
- }
-
- public function deliveries(): HasMany
- {
- return $this->hasMany(WebhookDelivery::class);
- }
-
- public function subscribesTo(string $event): bool
- {
- return in_array('*', $this->events ?: [], true) || in_array($event, $this->events ?: [], true);
- }
-}
diff --git a/Api/Models/WebhookDelivery.php b/Api/Models/WebhookDelivery.php
deleted file mode 100644
index a07b1a4..0000000
--- a/Api/Models/WebhookDelivery.php
+++ /dev/null
@@ -1,21 +0,0 @@
- 'array', 'delivered_at' => 'datetime', 'failed_at' => 'datetime'];
- }
-
- public function webhook(): BelongsTo
- {
- return $this->belongsTo(Webhook::class);
- }
-}
diff --git a/Api/Providers/ApiServiceProvider.php b/Api/Providers/ApiServiceProvider.php
deleted file mode 100644
index 1f1122c..0000000
--- a/Api/Providers/ApiServiceProvider.php
+++ /dev/null
@@ -1,22 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Api', 'Config/config.php'), 'api');
- $this->app->singleton(WebhookDispatcher::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Api', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Api', 'Resources/views'), 'api');
- }
-}
diff --git a/Api/Providers/RouteServiceProvider.php b/Api/Providers/RouteServiceProvider.php
deleted file mode 100644
index f6699cb..0000000
--- a/Api/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-prefix(config('api.prefix', 'api/v1'))->name('api.v1.')->group(module_path('Api', 'Routes/api.php'));
- Route::middleware('web')->group(module_path('Api', 'Routes/web.php'));
- }
-}
diff --git a/Api/Resources/views/admin/index.blade.php b/Api/Resources/views/admin/index.blade.php
deleted file mode 100644
index b5d5117..0000000
--- a/Api/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-{{ __('API & Webhooks') }} {{ __('Manage headless access, bearer tokens and webhook subscriptions.') }}
@if(session('status'))
{{ session('status') }} @endif @if(session('api_token'))
{{ __('Copy this token now') }} {{ session('api_token') }}
@endif
{{ __('Create token') }} {{ __('Create webhook') }}
@forelse($tokens as $token){{ $token->name }} {{ implode(', ', $token->abilities ?? []) }}
@empty{{ __('No tokens.') }}
@endforelse{{ $tokens->links() }} {{ __('Webhooks') }} @forelse($webhooks as $webhook){{ $webhook->name }} {{ $webhook->url }}
@empty{{ __('No webhooks.') }}
@endforelse{{ $webhooks->links() }}
diff --git a/Api/Routes/api.php b/Api/Routes/api.php
deleted file mode 100644
index 63c733c..0000000
--- a/Api/Routes/api.php
+++ /dev/null
@@ -1,18 +0,0 @@
-name('openapi');
-Route::controller(ContentController::class)->group(function (): void {
- Route::get('/pages', 'pages');
- Route::get('/pages/{page}', 'page');
- Route::get('/blog', 'blog');
- Route::get('/blog/{blog}', 'blogPost');
- Route::get('/news', 'news');
- Route::get('/news/{news}', 'newsArticle');
-});
-Route::middleware(AuthenticateApiToken::class)->get('/user', fn (Request $r) => $r->user());
diff --git a/Api/Routes/web.php b/Api/Routes/web.php
deleted file mode 100644
index 1b2d91a..0000000
--- a/Api/Routes/web.php
+++ /dev/null
@@ -1,12 +0,0 @@
-prefix('administration/api')->name('administration.api.')->controller(ApiController::class)->group(function (): void {
- Route::get('/', 'index')->name('index');
- Route::post('/tokens', 'storeToken')->name('tokens.store');
- Route::delete('/tokens/{token}', 'destroyToken')->name('tokens.destroy');
- Route::post('/webhooks', 'storeWebhook')->name('webhooks.store');
- Route::delete('/webhooks/{webhook}', 'destroyWebhook')->name('webhooks.destroy');
-});
diff --git a/Api/Services/WebhookDispatcher.php b/Api/Services/WebhookDispatcher.php
deleted file mode 100644
index 05da44d..0000000
--- a/Api/Services/WebhookDispatcher.php
+++ /dev/null
@@ -1,28 +0,0 @@
-get()->filter(fn (Webhook $h) => ($h->site_id === null || $h->site_id === $siteId) && $h->subscribesTo($event))->each(function (Webhook $h) use ($event, $payload): void {
- $uuid = (string) Str::uuid();
- $json = json_encode($payload, JSON_UNESCAPED_SLASHES) ?: '{}';
- $delivery = WebhookDelivery::create(['webhook_id' => $h->id, 'uuid' => $uuid, 'event' => $event, 'payload' => $payload, 'attempts' => 1]);
- try {
- $res = Http::timeout((int) config('api.webhook_timeout', 10))->withHeaders(['X-CMS-Event' => $event, 'X-CMS-Delivery' => $uuid, 'X-CMS-Signature' => 'sha256='.hash_hmac('sha256', $json, $h->getRawOriginal('secret'))])->withBody($json, 'application/json')->post($h->url);
- $delivery->update(['response_status' => $res->status(), 'response_body' => Str::limit($res->body(), 10000), 'delivered_at' => $res->successful() ? now() : null, 'failed_at' => $res->successful() ? null : now()]);
- $h->update(['last_triggered_at' => now(), 'failure_count' => $res->successful() ? 0 : $h->failure_count + 1]);
- } catch (\Throwable $e) {
- $delivery->update(['response_body' => $e->getMessage(), 'failed_at' => now()]);
- $h->increment('failure_count');
- }
- });
- }
-}
diff --git a/Api/Tests/Feature/ApiModuleBootTest.php b/Api/Tests/Feature/ApiModuleBootTest.php
deleted file mode 100644
index f3b4f0a..0000000
--- a/Api/Tests/Feature/ApiModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
- expect(app()->providerIsLoaded(ApiServiceProvider::class))->toBeTrue());
-it('hashes issued api tokens', function (): void {
- $u = User::factory()->create();
- [$t,$plain] = ApiToken::issue($u, 'Test', ['content.read']);
- expect($t->getRawOriginal('token'))->toBe(hash('sha256', $plain))->not->toBe($plain);
-});
-it('publishes openapi documentation', fn () => $this->getJson('/api/v1/openapi.json')->assertOk()->assertJsonPath('openapi', '3.1.0'));
diff --git a/Api/module.json b/Api/module.json
deleted file mode 100644
index e9574cd..0000000
--- a/Api/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Api","alias":"api","version":"1.0.0","description":"REST APIs, bearer tokens, webhooks and OpenAPI.","keywords":["api","headless","webhooks"],"priority":90,"providers":["Modules\\Api\\Providers\\ApiServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Sites"]}
diff --git a/Audit/Config/config.php b/Audit/Config/config.php
deleted file mode 100644
index b43f43a..0000000
--- a/Audit/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Audit',
- 'enabled' => true,
-];
diff --git a/Audit/Database/Migrations/2026_07_23_000200_create_audit_logs_table.php b/Audit/Database/Migrations/2026_07_23_000200_create_audit_logs_table.php
deleted file mode 100644
index 81771b0..0000000
--- a/Audit/Database/Migrations/2026_07_23_000200_create_audit_logs_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-id();
- $table->nullableMorphs('actor');
- $table->string('event')->index();
- $table->nullableMorphs('auditable');
- $table->json('old_values')->nullable();
- $table->json('new_values')->nullable();
- $table->string('ip_address', 45)->nullable();
- $table->text('user_agent')->nullable();
- $table->timestamp('created_at')->useCurrent();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('audit_logs');
- }
-};
diff --git a/Audit/Http/Controllers/AuditController.php b/Audit/Http/Controllers/AuditController.php
deleted file mode 100644
index f28a59b..0000000
--- a/Audit/Http/Controllers/AuditController.php
+++ /dev/null
@@ -1,14 +0,0 @@
- AuditLog::query()->latest('created_at')->paginate(50)]);
- }
-}
diff --git a/Audit/Models/AuditLog.php b/Audit/Models/AuditLog.php
deleted file mode 100644
index 6dce2ae..0000000
--- a/Audit/Models/AuditLog.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'array', 'new_values' => 'array', 'created_at' => 'datetime'];
- }
-}
diff --git a/Audit/Providers/AuditServiceProvider.php b/Audit/Providers/AuditServiceProvider.php
deleted file mode 100644
index 939ae99..0000000
--- a/Audit/Providers/AuditServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Audit/Providers/RouteServiceProvider.php b/Audit/Providers/RouteServiceProvider.php
deleted file mode 100644
index ce59b66..0000000
--- a/Audit/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Audit', 'Routes/web.php'));
- });
- }
-}
diff --git a/Audit/Resources/views/index.blade.php b/Audit/Resources/views/index.blade.php
deleted file mode 100644
index 7bbc774..0000000
--- a/Audit/Resources/views/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Audit log Recorded platform activity.
Event Actor IP Date @forelse($logs as $log){{ $log->event }} {{ $log->actor_type ? class_basename($log->actor_type).' #'.$log->actor_id : 'System' }} {{ $log->ip_address ?: '—' }} {{ optional($log->created_at)->format('d M Y H:i') }} @emptyNo audit events recorded. @endforelse
{{ $logs->links() }}
diff --git a/Audit/Routes/web.php b/Audit/Routes/web.php
deleted file mode 100644
index f4c314e..0000000
--- a/Audit/Routes/web.php
+++ /dev/null
@@ -1,6 +0,0 @@
-get('administration/audit', [AuditController::class, 'index'])->name('administration.audit.index');
diff --git a/Audit/Support/ModuleInfo.php b/Audit/Support/ModuleInfo.php
deleted file mode 100644
index 96f4251..0000000
--- a/Audit/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Audit');
-});
diff --git a/Audit/composer.json b/Audit/composer.json
deleted file mode 100644
index b14ddd2..0000000
--- a/Audit/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/audit-module",
- "description": "Audit foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Audit\\": ""
- }
- }
-}
diff --git a/Audit/module.json b/Audit/module.json
deleted file mode 100644
index 6a2042e..0000000
--- a/Audit/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Audit",
- "alias": "audit",
- "version": "1.0.0",
- "description": "Audit foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "audit"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Audit\\Providers\\AuditServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Authors/Config/config.php b/Authors/Config/config.php
deleted file mode 100644
index 9255ff8..0000000
--- a/Authors/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Authors'];
diff --git a/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php b/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php
deleted file mode 100644
index fb9ddce..0000000
--- a/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php
+++ /dev/null
@@ -1,31 +0,0 @@
-id();
- $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
- $table->string('name');
- $table->string('slug')->unique();
- $table->string('job_title')->nullable();
- $table->text('biography')->nullable();
- $table->string('avatar_path')->nullable();
- $table->json('social_links')->nullable();
- $table->string('meta_title')->nullable();
- $table->text('meta_description')->nullable();
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('authors');
- }
-};
diff --git a/Authors/Http/Controllers/AuthorController.php b/Authors/Http/Controllers/AuthorController.php
deleted file mode 100644
index a25fa53..0000000
--- a/Authors/Http/Controllers/AuthorController.php
+++ /dev/null
@@ -1,57 +0,0 @@
- Author::with('user')->latest()->paginate(25)]);
- }
-
- public function create(): View
- {
- return view('authors::admin.form', ['users' => User::orderBy('name')->get()]);
- }
-
- public function store(Request $r): RedirectResponse
- {
- Author::create($this->data($r));
-
- return redirect()->route('administration.authors.index')->with('status', 'Author created.');
- }
-
- public function edit(Author $author): View
- {
- return view('authors::admin.form', ['record' => $author, 'users' => User::orderBy('name')->get()]);
- }
-
- public function update(Request $r, Author $author): RedirectResponse
- {
- $author->update($this->data($r));
-
- return redirect()->route('administration.authors.index')->with('status', 'Author updated.');
- }
-
- public function destroy(Author $author): RedirectResponse
- {
- $author->delete();
-
- return back()->with('status', 'Author deleted.');
- }
-
- private function data(Request $r): array
- {
- $d = $r->validate(['user_id' => ['nullable', 'exists:users,id'], 'name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'job_title' => ['nullable', 'string', 'max:255'], 'biography' => ['nullable', 'string'], 'avatar_path' => ['nullable', 'string', 'max:255'], 'meta_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string'], 'is_active' => ['sometimes', 'boolean']]);
- $d['is_active'] = $r->boolean('is_active');
-
- return $d;
- }
-}
diff --git a/Authors/Models/Author.php b/Authors/Models/Author.php
deleted file mode 100644
index 7fa531e..0000000
--- a/Authors/Models/Author.php
+++ /dev/null
@@ -1,22 +0,0 @@
- 'array', 'is_active' => 'boolean'];
- }
-
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class);
- }
-}
diff --git a/Authors/Providers/AuthorsServiceProvider.php b/Authors/Providers/AuthorsServiceProvider.php
deleted file mode 100644
index 1df491a..0000000
--- a/Authors/Providers/AuthorsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Authors', 'Config/config.php'), 'authors');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Authors', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Authors', 'Resources/views'), 'authors');
- }
-}
diff --git a/Authors/Providers/RouteServiceProvider.php b/Authors/Providers/RouteServiceProvider.php
deleted file mode 100644
index f3e7ff9..0000000
--- a/Authors/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Authors', 'Routes/web.php'));
- }
-}
diff --git a/Authors/Resources/views/admin/form.blade.php b/Authors/Resources/views/admin/form.blade.php
deleted file mode 100644
index c224794..0000000
--- a/Authors/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/Authors/Resources/views/admin/index.blade.php b/Authors/Resources/views/admin/index.blade.php
deleted file mode 100644
index 5db3b07..0000000
--- a/Authors/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Authors Manage publishing authors.
Create @if(session('status')){{ session('status') }}
@endifName / title Status Actions @forelse($records as $record){{ $record->title ?? $record->name }} {{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }} Edit @emptyNo records yet. @endforelse
{{ $records->links() }}
diff --git a/Authors/Routes/web.php b/Authors/Routes/web.php
deleted file mode 100644
index 9209fa8..0000000
--- a/Authors/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/authors')->name('administration.authors.')->group(function (): void {
- Route::get('/', [AuthorController::class, 'index'])->name('index');
- Route::get('/create', [AuthorController::class, 'create'])->name('create');
- Route::post('/', [AuthorController::class, 'store'])->name('store');
- Route::get('/{author}/edit', [AuthorController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{author}', [AuthorController::class, 'update'])->name('update');
- Route::delete('/{author}', [AuthorController::class, 'destroy'])->name('destroy');
-});
diff --git a/Authors/Tests/Feature/ModuleBootTest.php b/Authors/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 256a4e6..0000000
--- a/Authors/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Authors', config('authors.name'));
- }
-}
diff --git a/Authors/module.json b/Authors/module.json
deleted file mode 100644
index c972181..0000000
--- a/Authors/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Authors",
- "alias": "authors",
- "version": "1.0.0",
- "description": "Authors publishing module.",
- "keywords": ["publishing", "authors"],
- "priority": 30,
- "providers": ["Modules\\Authors\\Providers\\AuthorsServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Automation/Config/config.php b/Automation/Config/config.php
deleted file mode 100644
index 0d56d36..0000000
--- a/Automation/Config/config.php
+++ /dev/null
@@ -1,8 +0,0 @@
- env('AUTOMATION_QUEUE', 'default'),
- 'max_steps' => (int) env('AUTOMATION_MAX_STEPS', 50),
- 'webhook_timeout' => (int) env('AUTOMATION_WEBHOOK_TIMEOUT', 10),
- 'retain_runs_days' => (int) env('AUTOMATION_RETAIN_RUNS_DAYS', 90),
-];
diff --git a/Automation/Console/DispatchScheduledAutomations.php b/Automation/Console/DispatchScheduledAutomations.php
deleted file mode 100644
index b80d5fd..0000000
--- a/Automation/Console/DispatchScheduledAutomations.php
+++ /dev/null
@@ -1,30 +0,0 @@
-where('status', 'active')->where('trigger_type', 'schedule')->whereNotNull('next_run_at')->where('next_run_at', '<=', now())->chunkById(100, function ($items) use ($runner, &$count) {
- foreach ($items as $automation) {
- $runner->dispatch($automation, ['scheduled_at' => now()->toIso8601String()], 'schedule');
- $interval = max(1, (int) ($automation->trigger_config['interval_minutes'] ?? 60));
- $automation->update(['next_run_at' => now()->addMinutes($interval)]);
- $count++;
- }
- });
- $this->info("Dispatched {$count} scheduled automation(s).");
-
- return self::SUCCESS;
- }
-}
diff --git a/Automation/Contracts/ActionRegistry.php b/Automation/Contracts/ActionRegistry.php
deleted file mode 100644
index 49c599c..0000000
--- a/Automation/Contracts/ActionRegistry.php
+++ /dev/null
@@ -1,12 +0,0 @@
-id();
- $table->string('name');
- $table->text('description')->nullable();
- $table->string('trigger_type')->index();
- $table->json('trigger_config')->nullable();
- $table->string('status', 20)->default('draft')->index();
- $table->boolean('stop_on_failure')->default(true);
- $table->unsignedInteger('run_count')->default(0);
- $table->timestamp('last_ran_at')->nullable();
- $table->timestamp('next_run_at')->nullable()->index();
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- });
- Schema::create('automation_steps', function (Blueprint $table) {
- $table->id();
- $table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete();
- $table->unsignedSmallInteger('position')->default(1);
- $table->string('action_type');
- $table->string('name')->nullable();
- $table->json('configuration')->nullable();
- $table->boolean('is_enabled')->default(true);
- $table->timestamps();
- $table->unique(['automation_id', 'position']);
- });
- Schema::create('automation_runs', function (Blueprint $table) {
- $table->id();
- $table->foreignId('automation_id')->constrained('automations')->cascadeOnDelete();
- $table->uuid('execution_id')->unique();
- $table->string('trigger_type');
- $table->json('payload')->nullable();
- $table->string('status', 20)->default('queued')->index();
- $table->unsignedSmallInteger('current_step')->default(0);
- $table->text('error')->nullable();
- $table->timestamp('started_at')->nullable();
- $table->timestamp('finished_at')->nullable();
- $table->timestamps();
- });
- Schema::create('automation_run_logs', function (Blueprint $table) {
- $table->id();
- $table->foreignId('automation_run_id')->constrained('automation_runs')->cascadeOnDelete();
- $table->foreignId('automation_step_id')->nullable()->constrained('automation_steps')->nullOnDelete();
- $table->string('level', 20)->default('info');
- $table->string('message');
- $table->json('context')->nullable();
- $table->timestamp('created_at')->useCurrent();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('automation_run_logs');
- Schema::dropIfExists('automation_runs');
- Schema::dropIfExists('automation_steps');
- Schema::dropIfExists('automations');
- }
-};
diff --git a/Automation/Http/Controllers/AutomationApiController.php b/Automation/Http/Controllers/AutomationApiController.php
deleted file mode 100644
index 72701c4..0000000
--- a/Automation/Http/Controllers/AutomationApiController.php
+++ /dev/null
@@ -1,24 +0,0 @@
-trigger_type === 'webhook', 404);
- $secret = (string) ($automation->trigger_config['secret'] ?? '');
- if ($secret !== '') {
- abort_unless(hash_equals($secret, (string) $request->header('X-Automation-Secret')), 401, 'Invalid automation secret.');
- }
- $run = $runner->dispatch($automation, $request->all(), 'webhook');
-
- return response()->json(['data' => ['execution_id' => $run->execution_id, 'status' => $run->status]], 202);
- }
-}
diff --git a/Automation/Http/Controllers/AutomationController.php b/Automation/Http/Controllers/AutomationController.php
deleted file mode 100644
index 38524c9..0000000
--- a/Automation/Http/Controllers/AutomationController.php
+++ /dev/null
@@ -1,63 +0,0 @@
- Automation::withCount(['steps', 'runs'])->latest()->paginate(20), 'recentRuns' => AutomationRun::with('automation')->latest()->limit(15)->get(), 'triggers' => $triggers->all(), 'actions' => $actions->all()]);
- }
-
- public function store(Request $request, TriggerRegistry $triggers): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'trigger_type' => ['required', 'string'], 'status' => ['required', 'in:draft,active,paused'], 'next_run_at' => ['nullable', 'date'], 'trigger_config' => ['nullable', 'json']]);
- abort_unless($triggers->has($data['trigger_type']), 422, 'Unknown trigger type.');
- $data['trigger_config'] = filled($data['trigger_config'] ?? null) ? json_decode($data['trigger_config'], true, 512, JSON_THROW_ON_ERROR) : [];
- $data['stop_on_failure'] = $request->boolean('stop_on_failure');
- $data['created_by'] = $request->user()->id;
- Automation::create($data);
-
- return back()->with('status', 'Automation created.');
- }
-
- public function addStep(Request $request, Automation $automation, ActionRegistry $actions): RedirectResponse
- {
- $data = $request->validate(['action_type' => ['required', 'string'], 'name' => ['nullable', 'string', 'max:255'], 'configuration' => ['nullable', 'json']]);
- abort_unless(array_key_exists($data['action_type'], $actions->all()), 422, 'Unknown action type.');
- $automation->steps()->create(['position' => (int) $automation->steps()->max('position') + 1, 'action_type' => $data['action_type'], 'name' => $data['name'] ?? null, 'configuration' => filled($data['configuration'] ?? null) ? json_decode($data['configuration'], true, 512, JSON_THROW_ON_ERROR) : [], 'is_enabled' => true]);
-
- return back()->with('status', 'Automation step added.');
- }
-
- public function run(Automation $automation, AutomationRunner $runner): RedirectResponse
- {
- $run = $runner->dispatch($automation, ['initiated_by' => request()->user()->id], 'manual');
-
- return back()->with('status', "Automation queued as {$run->execution_id}.");
- }
-
- public function toggle(Automation $automation): RedirectResponse
- {
- $automation->update(['status' => $automation->status === 'active' ? 'paused' : 'active']);
-
- return back()->with('status', 'Automation status updated.');
- }
-
- public function destroy(Automation $automation): RedirectResponse
- {
- $automation->delete();
-
- return back()->with('status','Automation deleted.');
- }
-}
diff --git a/Automation/Jobs/ExecuteAutomation.php b/Automation/Jobs/ExecuteAutomation.php
deleted file mode 100644
index b84d34d..0000000
--- a/Automation/Jobs/ExecuteAutomation.php
+++ /dev/null
@@ -1,46 +0,0 @@
-findOrFail($this->runId);
- $automation = $run->automation;
- $run->update(['status' => 'running', 'started_at' => now()]);
- try {
- foreach ($automation->steps->where('is_enabled', true) as $step) {
- $run->update(['current_step' => $step->position]);
- try {
- $result = $actions->execute($step, $run->payload ?? []);
- $run->logs()->create(['automation_step_id' => $step->id, 'level' => 'info', 'message' => $step->name ?: $step->action_type.' completed', 'context' => $result, 'created_at' => now()]);
- } catch (\Throwable $e) {
- $run->logs()->create(['automation_step_id' => $step->id, 'level' => 'error', 'message' => $e->getMessage(), 'created_at' => now()]);
- if ($automation->stop_on_failure) {
- throw $e;
- }
- }
- }
- $run->update(['status' => 'completed', 'finished_at' => now()]);
- } catch (\Throwable $e) {
- $run->update(['status' => 'failed', 'error' => $e->getMessage(), 'finished_at' => now()]);
- throw $e;
- } finally {
- $automation->increment('run_count');
- $automation->update(['last_ran_at' => now()]);
- }
- }
-}
diff --git a/Automation/Models/Automation.php b/Automation/Models/Automation.php
deleted file mode 100644
index 0a060ed..0000000
--- a/Automation/Models/Automation.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'array', 'stop_on_failure' => 'boolean', 'last_ran_at' => 'datetime', 'next_run_at' => 'datetime'];
-
- public function steps(): HasMany
- {
- return $this->hasMany(AutomationStep::class)->orderBy('position');
- }
-
- public function runs(): HasMany
- {
- return $this->hasMany(AutomationRun::class);
- }
-
- public function isActive(): bool
- {
- return $this->status === 'active';
- }
-}
diff --git a/Automation/Models/AutomationRun.php b/Automation/Models/AutomationRun.php
deleted file mode 100644
index 38b5fb5..0000000
--- a/Automation/Models/AutomationRun.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'array', 'started_at' => 'datetime', 'finished_at' => 'datetime'];
-
- public function automation(): BelongsTo
- {
- return $this->belongsTo(Automation::class);
- }
-
- public function logs(): HasMany
- {
- return $this->hasMany(AutomationRunLog::class);
- }
-}
diff --git a/Automation/Models/AutomationRunLog.php b/Automation/Models/AutomationRunLog.php
deleted file mode 100644
index 3d93839..0000000
--- a/Automation/Models/AutomationRunLog.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'array', 'created_at' => 'datetime'];
-}
diff --git a/Automation/Models/AutomationStep.php b/Automation/Models/AutomationStep.php
deleted file mode 100644
index 2627289..0000000
--- a/Automation/Models/AutomationStep.php
+++ /dev/null
@@ -1,18 +0,0 @@
- 'array', 'is_enabled' => 'boolean'];
-
- public function automation(): BelongsTo
- {
- return $this->belongsTo(Automation::class);
- }
-}
diff --git a/Automation/Providers/AutomationServiceProvider.php b/Automation/Providers/AutomationServiceProvider.php
deleted file mode 100644
index b4c3b7c..0000000
--- a/Automation/Providers/AutomationServiceProvider.php
+++ /dev/null
@@ -1,30 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Automation', 'Config/config.php'), 'automation');
- $this->app->singleton(ActionRegistry::class, AutomationActionRegistry::class);
- $this->app->singleton(TriggerRegistry::class, AutomationTriggerRegistry::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Automation', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Automation', 'Resources/views'), 'automation');
- if ($this->app->runningInConsole()) {
- $this->commands([DispatchScheduledAutomations::class]);
- }
- }
-}
diff --git a/Automation/Providers/RouteServiceProvider.php b/Automation/Providers/RouteServiceProvider.php
deleted file mode 100644
index f03573a..0000000
--- a/Automation/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Automation', 'Routes/web.php'));
- Route::middleware('api')->prefix('api/v1')->group(module_path('Automation', 'Routes/api.php'));
- }
-}
diff --git a/Automation/Resources/views/admin/index.blade.php b/Automation/Resources/views/admin/index.blade.php
deleted file mode 100644
index 1033167..0000000
--- a/Automation/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
Automation Platform Create event-driven, scheduled and webhook automations.
-@if(session('status'))
{{ session('status') }} @endif
-
-Automations {{ $automations->total() }}
-Active {{ \Modules\Automation\Models\Automation::where('status','active')->count() }}
-Completed runs {{ \Modules\Automation\Models\AutomationRun::where('status','completed')->count() }}
-Failed runs {{ \Modules\Automation\Models\AutomationRun::where('status','failed')->count() }}
-
-@can('automation.manage')
-
Create automation
-@endcan
-
-@forelse($automations as $automation)
{{ $automation->name }} {{ $automation->trigger_type }} · {{ $automation->steps_count }} steps · {{ $automation->runs_count }} runs
{{ ucfirst($automation->status) }}
-@if($automation->description){{ $automation->description }} @endif
-@can('automation.execute')@endcan @can('automation.manage')@endcan
-@can('automation.manage')@endcan
-@empty
No automations have been created. @endforelse
-{{ $automations->links() }}
Recent runs @forelse($recentRuns as $run)
{{ $run->automation?->name }} {{ $run->status }}
{{ $run->execution_id }} @empty
No runs yet. @endforelse
-
diff --git a/Automation/Routes/api.php b/Automation/Routes/api.php
deleted file mode 100644
index f90901d..0000000
--- a/Automation/Routes/api.php
+++ /dev/null
@@ -1,6 +0,0 @@
-middleware('throttle:60,1')->name('api.automations.trigger');
diff --git a/Automation/Routes/web.php b/Automation/Routes/web.php
deleted file mode 100644
index 342d7c0..0000000
--- a/Automation/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-name('administration.automation.')->middleware(['auth', 'permission:automation.view'])->group(function () {
- Route::get('/', [AutomationController::class, 'index'])->name('index');
- Route::post('/', [AutomationController::class, 'store'])->middleware('permission:automation.manage')->name('store');
- Route::post('/{automation}/steps', [AutomationController::class, 'addStep'])->middleware('permission:automation.manage')->name('steps.store');
- Route::post('/{automation}/run', [AutomationController::class, 'run'])->middleware('permission:automation.execute')->name('run');
- Route::patch('/{automation}/toggle', [AutomationController::class, 'toggle'])->middleware('permission:automation.manage')->name('toggle');
- Route::delete('/{automation}', [AutomationController::class, 'destroy'])->middleware('permission:automation.manage')->name('destroy');
-});
diff --git a/Automation/Services/AutomationActionRegistry.php b/Automation/Services/AutomationActionRegistry.php
deleted file mode 100644
index b8e7f2c..0000000
--- a/Automation/Services/AutomationActionRegistry.php
+++ /dev/null
@@ -1,79 +0,0 @@
- ['label' => 'Write log', 'fields' => ['message']],
- 'webhook' => ['label' => 'Send webhook', 'fields' => ['url', 'method']],
- 'email' => ['label' => 'Send email', 'fields' => ['to', 'subject', 'body']],
- 'delay' => ['label' => 'Delay', 'fields' => ['seconds']],
- ];
- }
-
- public function execute(AutomationStep $step, array $payload): array
- {
- $config = $step->configuration ?? [];
-
- return match ($step->action_type) {
- 'log' => $this->log($config, $payload),
- 'webhook' => $this->webhook($config, $payload),
- 'email' => $this->email($config, $payload),
- 'delay' => $this->delay($config),
- default => throw new \InvalidArgumentException("Unknown automation action [{$step->action_type}]."),
- };
- }
-
- private function log(array $config, array $payload): array
- {
- $message = (string) ($config['message'] ?? 'Automation executed');
- Log::info($message, $payload);
-
- return ['message' => $message];
- }
-
- private function webhook(array $config, array $payload): array
- {
- $url = (string) ($config['url'] ?? '');
- if ($url === '') {
- throw new \InvalidArgumentException('Webhook URL is required.');
- }
- $method = strtolower((string) ($config['method'] ?? 'post'));
- $response = Http::timeout(config('automation.webhook_timeout', 10))->send($method, $url, ['json' => $payload]);
- $response->throw();
-
- return ['status' => $response->status()];
- }
-
- private function email(array $config, array $payload): array
- {
- $to = (string) ($config['to'] ?? '');
- if ($to === '') {
- throw new \InvalidArgumentException('Email recipient is required.');
- }
- $subject = (string) ($config['subject'] ?? 'Automation notification');
- $body = (string) ($config['body'] ?? json_encode($payload, JSON_PRETTY_PRINT));
- Mail::raw($body, fn ($message) => $message->to($to)->subject($subject));
-
- return ['to' => $to];
- }
-
- private function delay(array $config): array
- {
- $seconds = max(0, min(30, (int) ($config['seconds'] ?? 1)));
- if ($seconds > 0) {
- sleep($seconds);
- }
-
-return ['seconds' => $seconds];
- }
-}
diff --git a/Automation/Services/AutomationRunner.php b/Automation/Services/AutomationRunner.php
deleted file mode 100644
index 254ba8a..0000000
--- a/Automation/Services/AutomationRunner.php
+++ /dev/null
@@ -1,46 +0,0 @@
-isActive(), 422, 'Automation is not active.');
- $run = $automation->runs()->create(['execution_id' => (string) Str::uuid(), 'trigger_type' => $triggerType ?? $automation->trigger_type, 'payload' => $payload, 'status' => 'queued']);
- ExecuteAutomation::dispatch($run->id)->onQueue(config('automation.queue', 'default'));
-
- return $run;
- }
-
- public function dispatchMatching(string $triggerType, array $payload = [], array $constraints = []): int
- {
- $query = Automation::query()->where('status', 'active')->where('trigger_type', $triggerType);
- $count = 0;
- foreach ($query->get() as $automation) {
- if ($this->matches($automation, $constraints)) {
- $this->dispatch($automation, $payload, $triggerType);
- $count++;
- }
- }
-
- return $count;
- }
-
- private function matches(Automation $automation, array $constraints): bool
- {
- $config = $automation->trigger_config ?? [];
- foreach ($constraints as $key => $value) {
- if (isset($config[$key]) && (string) $config[$key] !== (string) $value) {
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/Automation/Services/AutomationTriggerRegistry.php b/Automation/Services/AutomationTriggerRegistry.php
deleted file mode 100644
index c0716f4..0000000
--- a/Automation/Services/AutomationTriggerRegistry.php
+++ /dev/null
@@ -1,23 +0,0 @@
- ['label' => 'Manual', 'description' => 'Run from the administration area or API.'],
- 'schedule' => ['label' => 'Schedule', 'description' => 'Run when next_run_at is due.'],
- 'webhook' => ['label' => 'Incoming webhook', 'description' => 'Run from a signed API request.'],
- 'domain_event' => ['label' => 'Domain event', 'description' => 'Run when an application event is dispatched.'],
- ];
- }
-
- public function has(string $type): bool
- {
- return array_key_exists($type, $this->all());
- }
-}
diff --git a/Automation/Tests/Feature/ModuleBootTest.php b/Automation/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 984262f..0000000
--- a/Automation/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,16 +0,0 @@
-assertTrue(class_exists(AutomationServiceProvider::class));
- $this->assertTrue(app()->bound(ActionRegistry::class));
- }
-}
diff --git a/Automation/module.json b/Automation/module.json
deleted file mode 100644
index c39c7ae..0000000
--- a/Automation/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Automation","alias":"automation","version":"1.0.0","description":"Event-driven and scheduled automations with reusable triggers, actions, runs and execution logs.","keywords":["automation","events","triggers","actions","scheduler"],"priority":130,"providers":["Modules\\Automation\\Providers\\AutomationServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Api"]}
diff --git a/Banners/Config/config.php b/Banners/Config/config.php
deleted file mode 100644
index b2a20d0..0000000
--- a/Banners/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Banners'];
diff --git a/Banners/Http/Controllers/BannerController.php b/Banners/Http/Controllers/BannerController.php
deleted file mode 100644
index a66e3f0..0000000
--- a/Banners/Http/Controllers/BannerController.php
+++ /dev/null
@@ -1,60 +0,0 @@
- Banner::query()->latest()->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('banners::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $request->validate(['title' => ['required', 'string', 'max:255'],
- 'content' => ['nullable', 'string'],
- 'link_url' => ['nullable', 'string'],
- 'position' => ['required', 'string', 'max:255'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- Banner::create($data);
-
- return redirect()->route('administration.banners.index')->with('status', 'Banner created.');
- }
-
- public function edit(Banner $banner): View
- {
- return view('banners::admin.form', ['record' => $banner]);
- }
-
- public function update(Request $request, Banner $banner): RedirectResponse
- {
- $data = $request->validate(['title' => ['required', 'string', 'max:255'],
- 'content' => ['nullable', 'string'],
- 'link_url' => ['nullable', 'string'],
- 'position' => ['required', 'string', 'max:255'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- $banner->update($data);
-
- return redirect()->route('administration.banners.index')->with('status', 'Banner updated.');
- }
-
- public function destroy(Banner $banner): RedirectResponse
- {
- $banner->delete();
-
- return back()->with('status', 'Banner deleted.');
- }
-}
diff --git a/Banners/Models/Banner.php b/Banners/Models/Banner.php
deleted file mode 100644
index 7ba1279..0000000
--- a/Banners/Models/Banner.php
+++ /dev/null
@@ -1,23 +0,0 @@
- 'boolean',
- ];
- }
-}
diff --git a/Banners/Providers/BannersServiceProvider.php b/Banners/Providers/BannersServiceProvider.php
deleted file mode 100644
index eda98f7..0000000
--- a/Banners/Providers/BannersServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Banners', 'Config/config.php'), 'banners');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Banners', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Banners', 'Resources/views'), 'banners');
- }
-}
diff --git a/Banners/Providers/RouteServiceProvider.php b/Banners/Providers/RouteServiceProvider.php
deleted file mode 100644
index c948511..0000000
--- a/Banners/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Banners', 'Routes/web.php')));
- }
-}
diff --git a/Banners/Resources/views/admin/form.blade.php b/Banners/Resources/views/admin/form.blade.php
deleted file mode 100644
index b78be52..0000000
--- a/Banners/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
{{ isset($record) ? 'Edit' : 'Create' }} Banner
-
diff --git a/Banners/Resources/views/admin/index.blade.php b/Banners/Resources/views/admin/index.blade.php
deleted file mode 100644
index 4ac0c2d..0000000
--- a/Banners/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-
-Banners Manage banners content.
Add new
-@if(session('status'))
{{ session('status') }}
@endif
-
Title Content Link Url Actions @forelse($records as $record){{ is_array($record->title) ? json_encode($record->title) : Str::limit((string) $record->title, 70) }} {{ is_array($record->content) ? json_encode($record->content) : Str::limit((string) $record->content, 70) }} {{ is_array($record->link_url) ? json_encode($record->link_url) : Str::limit((string) $record->link_url, 70) }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
-
diff --git a/Banners/Routes/web.php b/Banners/Routes/web.php
deleted file mode 100644
index 3651a6c..0000000
--- a/Banners/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/banners')->name('administration.banners.')->group(function (): void {
- Route::get('/', [BannerController::class, 'index'])->name('index');
- Route::get('/create', [BannerController::class, 'create'])->name('create');
- Route::post('/', [BannerController::class, 'store'])->name('store');
- Route::get('/{banner}/edit', [BannerController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{banner}', [BannerController::class, 'update'])->name('update');
- Route::delete('/{banner}', [BannerController::class, 'destroy'])->name('destroy');
-});
diff --git a/Banners/Support/ModuleInfo.php b/Banners/Support/ModuleInfo.php
deleted file mode 100644
index 09e1bda..0000000
--- a/Banners/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBeTrue();
-});
diff --git a/Banners/module.json b/Banners/module.json
deleted file mode 100644
index 238aec6..0000000
--- a/Banners/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Banners",
- "alias": "banners",
- "version": "1.0.0",
- "description": "Banners CMS module.",
- "keywords": [
- "cms",
- "banners"
- ],
- "priority": 26,
- "providers": [
- "Modules\\Banners\\Providers\\BannersServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Blog/Config/config.php b/Blog/Config/config.php
deleted file mode 100644
index 97da7a5..0000000
--- a/Blog/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Blog'];
diff --git a/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php b/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php
deleted file mode 100644
index 30684a4..0000000
--- a/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php
+++ /dev/null
@@ -1,40 +0,0 @@
-id();
- $table->foreignId('author_id')->nullable()->constrained('authors')->nullOnDelete();
- $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
- $table->string('title');
- $table->string('slug')->unique();
- $table->text('excerpt')->nullable();
- $table->longText('content')->nullable();
- $table->string('featured_image')->nullable();
- $table->string('status')->default('draft')->index();
- $table->boolean('is_featured')->default(false);
- $table->timestamp('published_at')->nullable()->index();
- $table->timestamp('expires_at')->nullable();
- $table->string('meta_title')->nullable();
- $table->text('meta_description')->nullable();
- $table->timestamps();
- });
- Schema::create('blog_post_tag', function (Blueprint $table): void {
- $table->foreignId('blog_post_id')->constrained('blog_posts')->cascadeOnDelete();
- $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete();
- $table->primary(['blog_post_id', 'tag_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('blog_post_tag');
- Schema::dropIfExists('blog_posts');
- }
-};
diff --git a/Blog/Http/Controllers/BlogController.php b/Blog/Http/Controllers/BlogController.php
deleted file mode 100644
index 0e96878..0000000
--- a/Blog/Http/Controllers/BlogController.php
+++ /dev/null
@@ -1,66 +0,0 @@
- Blog::with(['author', 'category'])->latest()->paginate(25)]);
- }
-
- public function create(): View
- {
- return view('blog::admin.form', $this->lookups());
- }
-
- public function store(Request $r): RedirectResponse
- {
- $record = Blog::create($this->data($r));
- $record->tags()->sync($r->input('tag_ids', []));
-
- return redirect()->route('administration.blog.index')->with('status', 'Blog created.');
- }
-
- public function edit(Blog $blog_post): View
- {
- return view('blog::admin.form', array_merge($this->lookups(), ['record' => $blog_post]));
- }
-
- public function update(Request $r, Blog $blog_post): RedirectResponse
- {
- $blog_post->update($this->data($r));
- $blog_post->tags()->sync($r->input('tag_ids', []));
-
- return redirect()->route('administration.blog.index')->with('status', 'Blog updated.');
- }
-
- public function destroy(Blog $blog_post): RedirectResponse
- {
- $blog_post->delete();
-
- return back()->with('status', 'Blog deleted.');
- }
-
- private function lookups(): array
- {
- return ['authors' => Author::where('is_active', true)->orderBy('name')->get(), 'categories' => Category::where('is_active', true)->orderBy('name')->get(), 'tags' => Tag::orderBy('name')->get()];
- }
-
- private function data(Request $r): array
- {
- $d = $r->validate(['author_id' => ['nullable', 'exists:authors,id'], 'category_id' => ['nullable', 'exists:categories,id'], 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'excerpt' => ['nullable', 'string'], 'content' => ['nullable', 'string'], 'featured_image' => ['nullable', 'string', 'max:255'], 'status' => ['required', 'in:draft,review,published,archived'], 'is_featured' => ['sometimes', 'boolean'], 'published_at' => ['nullable', 'date'], 'expires_at' => ['nullable', 'date', 'after:published_at'], 'meta_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string']]);
- $d['is_featured'] = $r->boolean('is_featured');
-
- return $d;
- }
-}
diff --git a/Blog/Http/Controllers/PublicBlogController.php b/Blog/Http/Controllers/PublicBlogController.php
deleted file mode 100644
index 6fb2096..0000000
--- a/Blog/Http/Controllers/PublicBlogController.php
+++ /dev/null
@@ -1,20 +0,0 @@
- Blog::published()->with(['author', 'category'])->latest('published_at')->paginate(12)]);
- }
-
- public function show(string $slug): View
- {
- return view('blog::public.show', ['record' => Blog::published()->with(['author', 'category', 'tags'])->where('slug', $slug)->firstOrFail()]);
- }
-}
diff --git a/Blog/Models/Blog.php b/Blog/Models/Blog.php
deleted file mode 100644
index 6f58ea0..0000000
--- a/Blog/Models/Blog.php
+++ /dev/null
@@ -1,48 +0,0 @@
- 'boolean', 'published_at' => 'datetime', 'expires_at' => 'datetime'];
- }
-
- public function author(): BelongsTo
- {
- return $this->belongsTo(Author::class);
- }
-
- public function category(): BelongsTo
- {
- return $this->belongsTo(Category::class);
- }
-
- public function tags(): BelongsToMany
- {
- return $this->belongsToMany(Tag::class, 'blog_post_tag');
- }
-
- public function scopePublished(Builder $query): Builder
- {
- return $query->where('status', 'published')->where(fn (Builder $q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->where(fn (Builder $q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()));
- }
-}
diff --git a/Blog/Providers/BlogServiceProvider.php b/Blog/Providers/BlogServiceProvider.php
deleted file mode 100644
index 74e8bcf..0000000
--- a/Blog/Providers/BlogServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Blog', 'Config/config.php'), 'blog');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Blog', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Blog', 'Resources/views'), 'blog');
- }
-}
diff --git a/Blog/Providers/RouteServiceProvider.php b/Blog/Providers/RouteServiceProvider.php
deleted file mode 100644
index 6c7b0c0..0000000
--- a/Blog/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Blog', 'Routes/web.php'));
- }
-}
diff --git a/Blog/Resources/views/admin/form.blade.php b/Blog/Resources/views/admin/form.blade.php
deleted file mode 100644
index cf54f2b..0000000
--- a/Blog/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-@csrf @isset($record) @method('PUT') @endisset None @foreach($authors as $author)author_id ?? null)==$author->id)>{{ $author->name }} @endforeachNone @foreach($categories as $category)category_id ?? null)==$category->id)>{{ $category->name }} @endforeach@foreach(['draft','review','published','archived'] as $status)status ?? 'draft')===$status)>{{ $status }} @endforeach
{{ old('excerpt',$record->excerpt ?? '') }} {{ old('content',$record->content ?? '') }} {{ old('meta_description',$record->meta_description ?? '') }} Save
diff --git a/Blog/Resources/views/admin/index.blade.php b/Blog/Resources/views/admin/index.blade.php
deleted file mode 100644
index 9ec9eda..0000000
--- a/Blog/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Blog Manage publishing blog.
Create @if(session('status')){{ session('status') }}
@endifName / title Status Actions @forelse($records as $record){{ $record->title ?? $record->name }} {{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
diff --git a/Blog/Resources/views/public/index.blade.php b/Blog/Resources/views/public/index.blade.php
deleted file mode 100644
index b616fa9..0000000
--- a/Blog/Resources/views/public/index.blade.php
+++ /dev/null
@@ -1,31 +0,0 @@
-@extends('stylist-theme-saasland::layouts.app')
-
-@php
- $title = 'Blog';
- $description = 'Ideas, guides and updates from our team.';
- $eyebrow = 'Insights';
- $breadcrumbs = ['Blog' => null];
-@endphp
-
-@section('content')
-
-
-
- @forelse($records as $record)
-
-
-
-
{{ optional($record->published_at)->format('j F Y') ?: 'Latest article' }}
-
- @if($record->excerpt)
{{ $record->excerpt }}
@endif
-
Read article →
-
-
- @empty
-
No blog posts have been published yet.
- @endforelse
-
-
-
-
-@endsection
diff --git a/Blog/Resources/views/public/show.blade.php b/Blog/Resources/views/public/show.blade.php
deleted file mode 100644
index c710f44..0000000
--- a/Blog/Resources/views/public/show.blade.php
+++ /dev/null
@@ -1,23 +0,0 @@
-@extends('stylist-theme-saasland::layouts.app')
-
-@php
- $title = $record->title;
- $description = $record->excerpt;
- $eyebrow = 'Blog';
- $breadcrumbs = ['Blog' => route('blog.public.index'), $record->title => null];
-@endphp
-
-@section('content')
-
-
-
-
- @if($record->author){{ $record->author->name }} @endif
- @if($record->published_at){{ $record->published_at->format('j F Y') }} @endif
-
- @if($record->excerpt){{ $record->excerpt }}
@endif
- {!! nl2br(e($record->content)) !!}
-
-
-
-@endsection
diff --git a/Blog/Routes/web.php b/Blog/Routes/web.php
deleted file mode 100644
index 01f9b3d..0000000
--- a/Blog/Routes/web.php
+++ /dev/null
@@ -1,16 +0,0 @@
-prefix('administration/blog')->name('administration.blog.')->group(function (): void {
- Route::get('/', [BlogController::class, 'index'])->name('index');
- Route::get('/create', [BlogController::class, 'create'])->name('create');
- Route::post('/', [BlogController::class, 'store'])->name('store');
- Route::get('/{blog_post}/edit', [BlogController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{blog_post}', [BlogController::class, 'update'])->name('update');
- Route::delete('/{blog_post}', [BlogController::class, 'destroy'])->name('destroy');
-});
-Route::get('/blog', [PublicBlogController::class, 'index'])->name('blog.public.index');
-Route::get('/blog/{slug}', [PublicBlogController::class, 'show'])->name('blog.public.show');
diff --git a/Blog/module.json b/Blog/module.json
deleted file mode 100644
index e72c145..0000000
--- a/Blog/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Blog",
- "alias": "blog",
- "version": "1.0.0",
- "description": "Blog publishing module.",
- "keywords": ["publishing", "blog"],
- "priority": 30,
- "providers": ["Modules\\Blog\\Providers\\BlogServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Builder/Components/ContentComponent.php b/Builder/Components/ContentComponent.php
deleted file mode 100644
index 63dc588..0000000
--- a/Builder/Components/ContentComponent.php
+++ /dev/null
@@ -1,38 +0,0 @@
- ['type' => 'textarea', 'required' => true]];
- }
-
- public function defaults(): array
- {
- return ['content' => '
Add your content here.
'];
- }
-
- public function render(array $properties, array $context = []): string
- {
- return (string) ($properties['content'] ?? '');
- }
-}
diff --git a/Builder/Components/HeadingComponent.php b/Builder/Components/HeadingComponent.php
deleted file mode 100644
index e619242..0000000
--- a/Builder/Components/HeadingComponent.php
+++ /dev/null
@@ -1,40 +0,0 @@
- ['type' => 'text', 'required' => true], 'level' => ['type' => 'select', 'options' => [1, 2, 3, 4, 5, 6]]];
- }
-
- public function defaults(): array
- {
- return ['text' => 'Heading', 'level' => 2];
- }
-
- public function render(array $properties, array $context = []): string
- {
- $level = max(1, min(6, (int) ($properties['level'] ?? 2)));
-
- return ''.e($properties['text'] ?? '').' ';
- }
-}
diff --git a/Builder/Components/HeroComponent.php b/Builder/Components/HeroComponent.php
deleted file mode 100644
index f017dea..0000000
--- a/Builder/Components/HeroComponent.php
+++ /dev/null
@@ -1,38 +0,0 @@
- ['type' => 'text', 'required' => true], 'content' => ['type' => 'textarea'], 'button_label' => ['type' => 'text'], 'button_url' => ['type' => 'text']];
- }
-
- public function defaults(): array
- {
- return ['heading' => 'A compelling headline', 'content' => 'Add supporting content here.', 'button_label' => 'Learn more', 'button_url' => '#'];
- }
-
- public function render(array $properties, array $context = []): string
- {
- return view('builder::components.hero', ['properties' => $properties])->render();
- }
-}
diff --git a/Builder/Config/config.php b/Builder/Config/config.php
deleted file mode 100644
index b3a8362..0000000
--- a/Builder/Config/config.php
+++ /dev/null
@@ -1,8 +0,0 @@
- (int) env('BUILDER_MAX_DOCUMENT_SIZE_KB', 512),
- 'autosave_seconds' => (int) env('BUILDER_AUTOSAVE_SECONDS', 30),
- 'preview_route' => env('BUILDER_PREVIEW_ROUTE', 'builder.preview'),
- 'allowed_types' => ['page', 'form', 'dashboard', 'report', 'menu', 'email', 'query', 'workflow'],
-];
diff --git a/Builder/Contracts/BuilderComponent.php b/Builder/Contracts/BuilderComponent.php
deleted file mode 100644
index e16fad7..0000000
--- a/Builder/Contracts/BuilderComponent.php
+++ /dev/null
@@ -1,18 +0,0 @@
-id();
- $table->foreignId('site_id')->nullable()->constrained('sites')->nullOnDelete();
- $table->string('type', 30)->index();
- $table->string('name');
- $table->string('slug')->nullable();
- $table->string('status', 20)->default('draft')->index();
- $table->unsignedInteger('version')->default(1);
- $table->json('document')->nullable();
- $table->json('settings')->nullable();
- $table->nullableMorphs('buildable');
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamp('published_at')->nullable();
- $table->timestamps();
- $table->unique(['site_id', 'type', 'slug'], 'builder_document_site_type_slug_unique');
- });
- Schema::create('builder_document_versions', function (Blueprint $table) {
- $table->id();
- $table->foreignId('builder_document_id')->constrained('builder_documents')->cascadeOnDelete();
- $table->unsignedInteger('version');
- $table->json('document');
- $table->json('settings')->nullable();
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- $table->unique(['builder_document_id', 'version']);
- });
- Schema::create('builder_templates', function (Blueprint $table) {
- $table->id();
- $table->string('type', 30)->index();
- $table->string('name');
- $table->string('slug')->unique();
- $table->text('description')->nullable();
- $table->json('document');
- $table->json('settings')->nullable();
- $table->boolean('is_system')->default(false);
- $table->boolean('is_active')->default(true)->index();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('builder_templates');
- Schema::dropIfExists('builder_document_versions');
- Schema::dropIfExists('builder_documents');
- }
-};
diff --git a/Builder/Http/Controllers/BuilderApiController.php b/Builder/Http/Controllers/BuilderApiController.php
deleted file mode 100644
index 57296aa..0000000
--- a/Builder/Http/Controllers/BuilderApiController.php
+++ /dev/null
@@ -1,27 +0,0 @@
-json(['data' => $registry->components(), 'widgets' => $registry->widgets(), 'fields' => $registry->fields()]);
- }
-
- public function show(BuilderDocument $document): JsonResponse
- {
- return response()->json(['data' => $document]);
- }
-
- public function preview(BuilderDocument $document, BuilderRenderer $renderer): JsonResponse
- {
- return response()->json(['html' => $renderer->render($document->document ?? [])]);
- }
-}
diff --git a/Builder/Http/Controllers/BuilderController.php b/Builder/Http/Controllers/BuilderController.php
deleted file mode 100644
index 70443cc..0000000
--- a/Builder/Http/Controllers/BuilderController.php
+++ /dev/null
@@ -1,61 +0,0 @@
-string('type')->toString();
- $documents = BuilderDocument::query()->when($type, fn ($q) => $q->where('type', $type))->latest()->paginate(20)->withQueryString();
-
- return view('builder::index', compact('documents', 'type'));
- }
-
- public function create(BuilderRegistry $registry): View
- {
- return view('builder::editor', ['document' => new BuilderDocument(['type' => request('type', 'page'), 'document' => ['nodes' => []]]), 'components' => $registry->components()]);
- }
-
- public function edit(BuilderDocument $document, BuilderRegistry $registry): View
- {
- return view('builder::editor', ['document' => $document, 'components' => $registry->components()]);
- }
-
- public function store(Request $request, BuilderDocumentService $service): RedirectResponse
- {
- $document = $service->save(new BuilderDocument, $this->validated($request), $request->user()?->id);
-
- return redirect()->route('administration.builder.edit', $document)->with('status', 'Builder document created.');
- }
-
- public function update(Request $request, BuilderDocument $document, BuilderDocumentService $service): RedirectResponse
- {
- $service->save($document, $this->validated($request), $request->user()?->id);
-
- return back()->with('status', 'Builder document saved.');
- }
-
- public function destroy(BuilderDocument $document): RedirectResponse
- {
- $document->delete();
-
- return redirect()->route('administration.builder.index')->with('status', 'Builder document deleted.');
- }
-
- private function validated(Request $request): array
- {
- $data = $request->validate(['type' => ['required', 'in:'.implode(',', config('builder.allowed_types'))], 'name' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255'], 'status' => ['required', 'in:draft,published,archived'], 'document_json' => ['required', 'json'], 'settings_json' => ['nullable', 'json']]);
-
- return ['type' => $data['type'], 'name' => $data['name'], 'slug' => $data['slug'] ?: Str::slug($data['name']), 'status' => $data['status'], 'document' => json_decode($data['document_json'], true, 512, JSON_THROW_ON_ERROR), 'settings' => json_decode($data['settings_json'] ?: '{}', true, 512, JSON_THROW_ON_ERROR), 'published_at' => $data['status'] === 'published' ? now() : null];
- }
-}
diff --git a/Builder/Models/BuilderDocument.php b/Builder/Models/BuilderDocument.php
deleted file mode 100644
index edb53ad..0000000
--- a/Builder/Models/BuilderDocument.php
+++ /dev/null
@@ -1,18 +0,0 @@
- 'array', 'settings' => 'array', 'published_at' => 'datetime'];
-
- public function versions(): HasMany
- {
- return $this->hasMany(BuilderDocumentVersion::class);
- }
-}
diff --git a/Builder/Models/BuilderDocumentVersion.php b/Builder/Models/BuilderDocumentVersion.php
deleted file mode 100644
index 30d6a78..0000000
--- a/Builder/Models/BuilderDocumentVersion.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'array', 'settings' => 'array'];
-}
diff --git a/Builder/Models/BuilderTemplate.php b/Builder/Models/BuilderTemplate.php
deleted file mode 100644
index 68520d6..0000000
--- a/Builder/Models/BuilderTemplate.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'array', 'settings' => 'array', 'is_system' => 'boolean', 'is_active' => 'boolean'];
-}
diff --git a/Builder/Providers/BuilderServiceProvider.php b/Builder/Providers/BuilderServiceProvider.php
deleted file mode 100644
index d1d9c21..0000000
--- a/Builder/Providers/BuilderServiceProvider.php
+++ /dev/null
@@ -1,24 +0,0 @@
-app->singleton(BuilderRegistry::class);
- $this->app->alias(BuilderRegistry::class, 'builder.registry');
- $this->app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Builder', 'Config/config.php'), 'builder');
- }
-
- public function boot(BuilderRegistry $registry): void
- {
- $this->loadMigrationsFrom(module_path('Builder', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Builder', 'Resources/views'), 'builder');
- $registry->registerDefaults();
- }
-}
diff --git a/Builder/Providers/RouteServiceProvider.php b/Builder/Providers/RouteServiceProvider.php
deleted file mode 100644
index e63c673..0000000
--- a/Builder/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Builder', 'Routes/web.php'));
- Route::middleware('api')->prefix('api/v1')->group(module_path('Builder', 'Routes/api.php'));
- }
-}
diff --git a/Builder/Resources/views/components/hero.blade.php b/Builder/Resources/views/components/hero.blade.php
deleted file mode 100644
index 34083c8..0000000
--- a/Builder/Resources/views/components/hero.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/Builder/Resources/views/editor.blade.php b/Builder/Resources/views/editor.blade.php
deleted file mode 100644
index 82c8070..0000000
--- a/Builder/Resources/views/editor.blade.php
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
{{ $document->exists ? 'Edit' : 'Create' }} Builder Document Use the component palette and JSON document canvas. Visual drag-and-drop clients can use the component API.
-
@csrf @if($document->exists) @method('PUT') @endif
- @foreach(config('builder.allowed_types') as $type)type)===$type)>{{ str($type)->headline() }} @endforeach status)==='draft')>Draft status)==='published')>Published status)==='archived')>Archived
-
- {{ old('document_json',json_encode($document->document ?: ['nodes'=>[]],JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) }} {{ old('settings_json',json_encode($document->settings ?: new stdClass(),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) }}
- Save Cancel
-
-
diff --git a/Builder/Resources/views/index.blade.php b/Builder/Resources/views/index.blade.php
deleted file mode 100644
index c146805..0000000
--- a/Builder/Resources/views/index.blade.php
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
Low-Code Builder Pages, forms, dashboards, reports, menus, emails, queries and workflows.
@can('builder.manage')
Create @endcan
-
-
Name Type Status Version @forelse($documents as $document){{ $document->name }} {{ str($document->type)->headline() }} {{ ucfirst($document->status) }} {{ $document->version }} Edit @emptyNo builder documents yet. @endforelse
- {{ $documents->links() }}
-
diff --git a/Builder/Routes/api.php b/Builder/Routes/api.php
deleted file mode 100644
index 41b80ee..0000000
--- a/Builder/Routes/api.php
+++ /dev/null
@@ -1,11 +0,0 @@
-prefix('builder')->name('api.builder.')->group(function () {
- Route::get('/components', [BuilderApiController::class, 'components'])->name('components');
- Route::get('/documents/{document}', [BuilderApiController::class, 'show'])->name('show');
- Route::get('/documents/{document}/preview', [BuilderApiController::class, 'preview'])->name('preview');
-});
diff --git a/Builder/Routes/web.php b/Builder/Routes/web.php
deleted file mode 100644
index ed43359..0000000
--- a/Builder/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/builder')->name('administration.builder.')->group(function () {
- Route::get('/', [BuilderController::class, 'index'])->name('index');
- Route::get('/create', [BuilderController::class, 'create'])->middleware('permission:builder.manage')->name('create');
- Route::post('/', [BuilderController::class, 'store'])->middleware('permission:builder.manage')->name('store');
- Route::get('/{document}/edit', [BuilderController::class, 'edit'])->middleware('permission:builder.manage')->name('edit');
- Route::put('/{document}', [BuilderController::class, 'update'])->middleware('permission:builder.manage')->name('update');
- Route::delete('/{document}', [BuilderController::class, 'destroy'])->middleware('permission:builder.manage')->name('destroy');
-});
diff --git a/Builder/Services/BuilderDocumentService.php b/Builder/Services/BuilderDocumentService.php
deleted file mode 100644
index 29f2368..0000000
--- a/Builder/Services/BuilderDocumentService.php
+++ /dev/null
@@ -1,28 +0,0 @@
-exists && $document->isDirty()) {
- $document->versions()->create(['version' => $document->version, 'document' => $document->getOriginal('document') ?: [], 'settings' => $document->getOriginal('settings') ?: [], 'created_by' => $userId]);
- }
- $document->fill($payload);
- $document->updated_by = $userId;
- if (! $document->exists) {
- $document->created_by = $userId;
- } else {
- $document->version++;
- }
- $document->save();
-
- return $document->refresh();
- });
- }
-}
diff --git a/Builder/Services/BuilderRegistry.php b/Builder/Services/BuilderRegistry.php
deleted file mode 100644
index 0e63193..0000000
--- a/Builder/Services/BuilderRegistry.php
+++ /dev/null
@@ -1,56 +0,0 @@
-registerComponent(new $component);
- }
- foreach (['metric', 'chart', 'table', 'activity', 'calendar'] as $widget) {
- $this->widgets[$widget] = ['type' => $widget, 'label' => str($widget)->headline()->toString()];
- }
- foreach (['text', 'textarea', 'email', 'number', 'select', 'checkbox', 'date', 'file'] as $field) {
- $this->fields[$field] = ['type' => $field, 'label' => str($field)->headline()->toString()];
- }
- }
-
- public function registerComponent(BuilderComponent $component): void
- {
- $this->components[$component->type()] = $component;
- }
-
- public function component(string $type): BuilderComponent
- {
- return $this->components[$type] ?? throw new InvalidArgumentException("Unknown builder component [$type].");
- }
-
- public function components(): array
- {
- return array_map(fn (BuilderComponent $c) => ['type' => $c->type(), 'label' => $c->label(), 'category' => $c->category(), 'schema' => $c->schema(), 'defaults' => $c->defaults()], array_values($this->components));
- }
-
- public function widgets(): array
- {
- return array_values($this->widgets);
- }
-
- public function fields(): array
- {
- return array_values($this->fields);
- }
-}
diff --git a/Builder/Services/BuilderRenderer.php b/Builder/Services/BuilderRenderer.php
deleted file mode 100644
index 2b8a647..0000000
--- a/Builder/Services/BuilderRenderer.php
+++ /dev/null
@@ -1,19 +0,0 @@
-map(function (array $node) use ($context) {
- if (($node['kind'] ?? 'component') === 'container') {
- return ''.$this->render($node, $context).'
';
- }
-
- return $this->registry->component((string) ($node['type'] ?? 'content'))->render($node['properties'] ?? [], $context);
- })->implode("\n");
- }
-}
diff --git a/Builder/Tests/Feature/BuilderModuleTest.php b/Builder/Tests/Feature/BuilderModuleTest.php
deleted file mode 100644
index 0844567..0000000
--- a/Builder/Tests/Feature/BuilderModuleTest.php
+++ /dev/null
@@ -1,16 +0,0 @@
-components();
- $this->assertNotEmpty($components);
- $this->assertContains('hero', array_column($components, 'type'));
- }
-}
diff --git a/Builder/module.json b/Builder/module.json
deleted file mode 100644
index a17c5b0..0000000
--- a/Builder/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Builder","alias":"builder","version":"1.0.0","description":"Low-code visual builders for pages, forms, dashboards, reports, menus, emails, queries and workflows.","keywords":["builder","low-code","visual","components","layouts"],"priority":160,"providers":["Modules\\Builder\\Providers\\BuilderServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Pages","Forms","Menus","Workflow","Analytics","Sites"]}
diff --git a/CRM/Config/config.php b/CRM/Config/config.php
deleted file mode 100644
index c94cac3..0000000
--- a/CRM/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'CRM'];
diff --git a/CRM/Database/Migrations/2026_07_24_04000_create_crm_tables.php b/CRM/Database/Migrations/2026_07_24_04000_create_crm_tables.php
deleted file mode 100644
index cf3bc64..0000000
--- a/CRM/Database/Migrations/2026_07_24_04000_create_crm_tables.php
+++ /dev/null
@@ -1,79 +0,0 @@
-id();
- $t->string('name');
- $t->string('website')->nullable();
- $t->string('phone')->nullable();
- $t->string('email')->nullable();
- $t->string('industry')->nullable();
- $t->text('address')->nullable();
- $t->text('notes')->nullable();
- $t->boolean('is_active')->default(true);
- $t->timestamps();
- });
- Schema::create('crm_contacts', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('company_id')->nullable()->constrained('crm_companies')->nullOnDelete();
- $t->string('first_name');
- $t->string('last_name');
- $t->string('email')->nullable()->index();
- $t->string('phone')->nullable();
- $t->string('job_title')->nullable();
- $t->text('notes')->nullable();
- $t->boolean('is_active')->default(true);
- $t->timestamps();
- });
- Schema::create('crm_leads', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('contact_id')->nullable()->constrained('crm_contacts')->nullOnDelete();
- $t->string('title');
- $t->string('source')->nullable();
- $t->string('status')->default('new')->index();
- $t->decimal('estimated_value', 12, 2)->nullable();
- $t->text('notes')->nullable();
- $t->timestamp('next_action_at')->nullable();
- $t->timestamps();
- });
- Schema::create('crm_opportunities', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('company_id')->nullable()->constrained('crm_companies')->nullOnDelete();
- $t->foreignId('contact_id')->nullable()->constrained('crm_contacts')->nullOnDelete();
- $t->string('name');
- $t->string('stage')->default('qualification')->index();
- $t->decimal('value', 12, 2)->default(0);
- $t->unsignedTinyInteger('probability')->default(0);
- $t->date('expected_close_at')->nullable();
- $t->text('notes')->nullable();
- $t->timestamps();
- });
- Schema::create('crm_activities', function (Blueprint $t): void {
- $t->id();
- $t->nullableMorphs('subject');
- $t->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
- $t->string('type');
- $t->string('title');
- $t->text('description')->nullable();
- $t->timestamp('due_at')->nullable();
- $t->timestamp('completed_at')->nullable();
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('crm_activities');
- Schema::dropIfExists('crm_opportunities');
- Schema::dropIfExists('crm_leads');
- Schema::dropIfExists('crm_contacts');
- Schema::dropIfExists('crm_companies');
- }
-};
diff --git a/CRM/Http/Controllers/CRMController.php b/CRM/Http/Controllers/CRMController.php
deleted file mode 100644
index 0d85cd6..0000000
--- a/CRM/Http/Controllers/CRMController.php
+++ /dev/null
@@ -1,55 +0,0 @@
- Company::withCount('contacts')->latest()->paginate(20), 'contacts' => Contact::with('company')->latest()->limit(10)->get(), 'leads' => Lead::with('contact')->latest()->limit(10)->get(), 'opportunities' => Opportunity::with('company')->latest()->limit(10)->get(), 'pipelineValue' => Opportunity::whereNotIn('stage', ['won', 'lost'])->sum('value')]);
- }
-
- public function storeCompany(Request $r): RedirectResponse
- {
- Company::create($r->validate(['name' => ['required', 'string', 'max:255'], 'website' => ['nullable', 'url', 'max:255'], 'phone' => ['nullable', 'string', 'max:80'], 'email' => ['nullable', 'email', 'max:255'], 'industry' => ['nullable', 'string', 'max:120'], 'address' => ['nullable', 'string'], 'notes' => ['nullable', 'string']]) + ['is_active' => $r->boolean('is_active', true)]);
-
- return back()->with('status', 'Company created.');
- }
-
- public function storeContact(Request $r): RedirectResponse
- {
- Contact::create($r->validate(['company_id' => ['nullable', 'exists:crm_companies,id'], 'first_name' => ['required', 'string', 'max:120'], 'last_name' => ['required', 'string', 'max:120'], 'email' => ['nullable', 'email', 'max:255'], 'phone' => ['nullable', 'string', 'max:80'], 'job_title' => ['nullable', 'string', 'max:120'], 'notes' => ['nullable', 'string']]) + ['is_active' => $r->boolean('is_active', true)]);
-
- return back()->with('status', 'Contact created.');
- }
-
- public function storeLead(Request $r): RedirectResponse
- {
- Lead::create($r->validate(['contact_id' => ['nullable', 'exists:crm_contacts,id'], 'title' => ['required', 'string', 'max:255'], 'source' => ['nullable', 'string', 'max:120'], 'status' => ['required', 'in:new,qualified,proposal,won,lost'], 'estimated_value' => ['nullable', 'numeric', 'min:0'], 'notes' => ['nullable', 'string'], 'next_action_at' => ['nullable', 'date']]));
-
- return back()->with('status', 'Lead created.');
- }
-
- public function storeOpportunity(Request $r): RedirectResponse
- {
- Opportunity::create($r->validate(['company_id' => ['nullable', 'exists:crm_companies,id'], 'contact_id' => ['nullable', 'exists:crm_contacts,id'], 'name' => ['required', 'string', 'max:255'], 'stage' => ['required', 'in:qualification,proposal,negotiation,won,lost'], 'value' => ['required', 'numeric', 'min:0'], 'probability' => ['required', 'integer', 'between:0,100'], 'expected_close_at' => ['nullable', 'date'], 'notes' => ['nullable', 'string']]));
-
- return back()->with('status', 'Opportunity created.');
- }
-
- public function destroyCompany(Company $company): RedirectResponse
- {
- $company->delete();
-
- return back()->with('status', 'Company deleted.');
- }
-}
diff --git a/CRM/Models/Activity.php b/CRM/Models/Activity.php
deleted file mode 100644
index 1a2e8b6..0000000
--- a/CRM/Models/Activity.php
+++ /dev/null
@@ -1,23 +0,0 @@
- 'datetime', 'completed_at' => 'datetime'];
- }
-
- public function subject(): MorphTo
- {
- return $this->morphTo();
- }
-}
diff --git a/CRM/Models/Company.php b/CRM/Models/Company.php
deleted file mode 100644
index 76bcf10..0000000
--- a/CRM/Models/Company.php
+++ /dev/null
@@ -1,23 +0,0 @@
- 'boolean'];
- }
-
- public function contacts(): HasMany
- {
- return $this->hasMany(Contact::class);
- }
-}
diff --git a/CRM/Models/Contact.php b/CRM/Models/Contact.php
deleted file mode 100644
index e1f08cb..0000000
--- a/CRM/Models/Contact.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'boolean'];
- }
-
- public function company(): BelongsTo
- {
- return $this->belongsTo(Company::class);
- }
-
- public function getNameAttribute(): string
- {
- return trim($this->first_name.' '.$this->last_name);
- }
-}
diff --git a/CRM/Models/Lead.php b/CRM/Models/Lead.php
deleted file mode 100644
index 571b5dd..0000000
--- a/CRM/Models/Lead.php
+++ /dev/null
@@ -1,23 +0,0 @@
- 'decimal:2', 'next_action_at' => 'datetime'];
- }
-
- public function contact(): BelongsTo
- {
- return $this->belongsTo(Contact::class);
- }
-}
diff --git a/CRM/Models/Opportunity.php b/CRM/Models/Opportunity.php
deleted file mode 100644
index 6cd7884..0000000
--- a/CRM/Models/Opportunity.php
+++ /dev/null
@@ -1,28 +0,0 @@
- 'decimal:2', 'expected_close_at' => 'date'];
- }
-
- public function company(): BelongsTo
- {
- return $this->belongsTo(Company::class);
- }
-
- public function contact(): BelongsTo
- {
- return $this->belongsTo(Contact::class);
- }
-}
diff --git a/CRM/Providers/CRMServiceProvider.php b/CRM/Providers/CRMServiceProvider.php
deleted file mode 100644
index ba46a79..0000000
--- a/CRM/Providers/CRMServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('CRM', 'Config/config.php'), 'crm');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('CRM', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('CRM', 'Resources/views'), 'crm');
- }
-}
diff --git a/CRM/Providers/RouteServiceProvider.php b/CRM/Providers/RouteServiceProvider.php
deleted file mode 100644
index 94e56b2..0000000
--- a/CRM/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('CRM', 'Routes/web.php'));
- }
-}
diff --git a/CRM/Resources/views/admin/index.blade.php b/CRM/Resources/views/admin/index.blade.php
deleted file mode 100644
index 4a4868f..0000000
--- a/CRM/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-CRM Companies, contacts, leads and sales opportunities.
@if(session('status'))
{{ session('status') }}
@endif
Companies
{{ $companies->total() }}
Contacts
{{ \Modules\CRM\Models\Contact::count() }}
Open leads
{{ \Modules\CRM\Models\Lead::whereNotIn('status',['won','lost'])->count() }}
Pipeline
£{{ number_format($pipelineValue,2) }}
@csrfAdd company
Create company @csrfAdd contact None @foreach($companies as $company){{ $company->name }} @endforeach
Create contact Company Industry Contacts @forelse($companies as $company){{ $company->name }} {{ $company->industry ?: '—' }} {{ $company->contacts_count }} @csrf @method('DELETE')Delete @emptyNo companies yet. @endforelse
{{ $companies->links() }}
diff --git a/CRM/Routes/web.php b/CRM/Routes/web.php
deleted file mode 100644
index 5333122..0000000
--- a/CRM/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/crm')->name('administration.crm.')->group(function (): void {
- Route::get('/', [CRMController::class, 'index'])->name('index');
- Route::post('/companies', [CRMController::class, 'storeCompany'])->name('companies.store');
- Route::delete('/companies/{company}', [CRMController::class, 'destroyCompany'])->name('companies.destroy');
- Route::post('/contacts', [CRMController::class, 'storeContact'])->name('contacts.store');
- Route::post('/leads', [CRMController::class, 'storeLead'])->name('leads.store');
- Route::post('/opportunities', [CRMController::class, 'storeOpportunity'])->name('opportunities.store');
-});
diff --git a/CRM/Tests/Feature/ModuleBootTest.php b/CRM/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 3bbcf6f..0000000
--- a/CRM/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('CRM', config('crm.name'));
- }
-}
diff --git a/CRM/module.json b/CRM/module.json
deleted file mode 100644
index 8d2e7b2..0000000
--- a/CRM/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "CRM",
- "alias": "crm",
- "version": "1.0.0",
- "description": "Customer relationship management for companies, contacts, leads, opportunities and activities.",
- "keywords": ["business", "crm"],
- "priority": 40,
- "providers": ["Modules\\CRM\\Providers\\CRMServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Careers/Config/config.php b/Careers/Config/config.php
deleted file mode 100644
index 46a8b16..0000000
--- a/Careers/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Careers'];
diff --git a/Careers/Database/Migrations/2026_07_24_04300_create_careers_tables.php b/Careers/Database/Migrations/2026_07_24_04300_create_careers_tables.php
deleted file mode 100644
index c8627af..0000000
--- a/Careers/Database/Migrations/2026_07_24_04300_create_careers_tables.php
+++ /dev/null
@@ -1,43 +0,0 @@
-id();
- $t->string('title');
- $t->string('slug')->unique();
- $t->string('department')->nullable();
- $t->string('location')->nullable();
- $t->string('employment_type')->default('full-time');
- $t->longText('description')->nullable();
- $t->string('status')->default('draft')->index();
- $t->timestamp('published_at')->nullable();
- $t->timestamp('closes_at')->nullable();
- $t->timestamps();
- });
- Schema::create('career_applications', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('job_id')->constrained('career_jobs')->cascadeOnDelete();
- $t->string('name');
- $t->string('email')->index();
- $t->string('phone')->nullable();
- $t->string('cv_path')->nullable();
- $t->text('cover_letter')->nullable();
- $t->string('status')->default('new')->index();
- $t->text('notes')->nullable();
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('career_applications');
- Schema::dropIfExists('career_jobs');
- }
-};
diff --git a/Careers/Http/Controllers/CareerController.php b/Careers/Http/Controllers/CareerController.php
deleted file mode 100644
index d9c2bad..0000000
--- a/Careers/Http/Controllers/CareerController.php
+++ /dev/null
@@ -1,32 +0,0 @@
- Job::withCount('applications')->latest()->paginate(20), 'applications' => Application::latest()->limit(10)->get()]);
- }
-
- public function store(Request $r): RedirectResponse
- {
- Job::create($r->validate(['title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:career_jobs,slug'], 'department' => ['nullable', 'string', 'max:120'], 'location' => ['nullable', 'string', 'max:120'], 'employment_type' => ['required', 'in:full-time,part-time,contract,temporary,internship'], 'description' => ['nullable', 'string'], 'status' => ['required', 'in:draft,published,closed'], 'published_at' => ['nullable', 'date'], 'closes_at' => ['nullable', 'date', 'after:published_at']]));
-
- return back()->with('status', 'Job created.');
- }
-
- public function destroy(Job $job): RedirectResponse
- {
- $job->delete();
-
- return back()->with('status', 'Job deleted.');
- }
-}
diff --git a/Careers/Models/Application.php b/Careers/Models/Application.php
deleted file mode 100644
index e85d73e..0000000
--- a/Careers/Models/Application.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'datetime', 'closes_at' => 'datetime'];
- }
-
- public function applications(): HasMany
- {
- return $this->hasMany(Application::class);
- }
-}
diff --git a/Careers/Providers/CareersServiceProvider.php b/Careers/Providers/CareersServiceProvider.php
deleted file mode 100644
index 5f4cfff..0000000
--- a/Careers/Providers/CareersServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Careers', 'Config/config.php'), 'careers');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Careers', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Careers', 'Resources/views'), 'careers');
- }
-}
diff --git a/Careers/Providers/RouteServiceProvider.php b/Careers/Providers/RouteServiceProvider.php
deleted file mode 100644
index 4e15858..0000000
--- a/Careers/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Careers', 'Routes/web.php'));
- }
-}
diff --git a/Careers/Resources/views/admin/index.blade.php b/Careers/Resources/views/admin/index.blade.php
deleted file mode 100644
index bac71a2..0000000
--- a/Careers/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Careers Manage vacancies and candidate applications.
@if(session('status'))
{{ session('status') }}
@endif
@csrfCreate vacancy Full time Part time Contract Temporary Internship Draft Published Closed
Create vacancy Vacancy Department Status Applications @forelse($jobs as $job){{ $job->title }} {{ $job->department ?: '—' }} {{ ucfirst($job->status) }} {{ $job->applications_count }} @csrf @method('DELETE')Delete @emptyNo vacancies yet. @endforelse
{{ $jobs->links() }}
diff --git a/Careers/Routes/web.php b/Careers/Routes/web.php
deleted file mode 100644
index ed2f45f..0000000
--- a/Careers/Routes/web.php
+++ /dev/null
@@ -1,10 +0,0 @@
-prefix('administration/careers')->name('administration.careers.')->group(function (): void {
- Route::get('/', [CareerController::class, 'index'])->name('index');
- Route::post('/', [CareerController::class, 'store'])->name('store');
- Route::delete('/{job}', [CareerController::class, 'destroy'])->name('destroy');
-});
diff --git a/Careers/Tests/Feature/ModuleBootTest.php b/Careers/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 28b7a14..0000000
--- a/Careers/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Careers', config('careers.name'));
- }
-}
diff --git a/Careers/module.json b/Careers/module.json
deleted file mode 100644
index cda83cf..0000000
--- a/Careers/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Careers",
- "alias": "careers",
- "version": "1.0.0",
- "description": "Job vacancies and candidate application workflow.",
- "keywords": ["business", "careers"],
- "priority": 43,
- "providers": ["Modules\\Careers\\Providers\\CareersServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Categories/Config/config.php b/Categories/Config/config.php
deleted file mode 100644
index f310716..0000000
--- a/Categories/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Categories'];
diff --git a/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php b/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php
deleted file mode 100644
index 9796bc5..0000000
--- a/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-id();
- $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
- $table->string('name');
- $table->string('slug')->unique();
- $table->text('description')->nullable();
- $table->string('type')->default('content')->index();
- $table->unsignedInteger('sort_order')->default(0);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('categories');
- }
-};
diff --git a/Categories/Http/Controllers/CategoryController.php b/Categories/Http/Controllers/CategoryController.php
deleted file mode 100644
index feca75e..0000000
--- a/Categories/Http/Controllers/CategoryController.php
+++ /dev/null
@@ -1,56 +0,0 @@
- Category::with('parent')->orderBy('type')->orderBy('sort_order')->paginate(30)]);
- }
-
- public function create(): View
- {
- return view('categories::admin.form', ['parents' => Category::orderBy('name')->get()]);
- }
-
- public function store(Request $request): RedirectResponse
- {
- Category::create($this->validated($request));
-
- return redirect()->route('administration.categories.index')->with('status', 'Category created.');
- }
-
- public function edit(Category $category): View
- {
- return view('categories::admin.form', ['record' => $category, 'parents' => Category::whereKeyNot($category->getKey())->orderBy('name')->get()]);
- }
-
- public function update(Request $request, Category $category): RedirectResponse
- {
- $category->update($this->validated($request));
-
- return redirect()->route('administration.categories.index')->with('status', 'Category updated.');
- }
-
- public function destroy(Category $category): RedirectResponse
- {
- $category->delete();
-
- return back()->with('status', 'Category deleted.');
- }
-
- private function validated(Request $request): array
- {
- $data = $request->validate(['parent_id' => ['nullable', 'exists:categories,id'], 'name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'type' => ['required', 'string', 'max:50'], 'sort_order' => ['nullable', 'integer', 'min:0'], 'is_active' => ['sometimes', 'boolean']]);
- $data['is_active'] = $request->boolean('is_active');
-
- return $data;
- }
-}
diff --git a/Categories/Models/Category.php b/Categories/Models/Category.php
deleted file mode 100644
index 8835411..0000000
--- a/Categories/Models/Category.php
+++ /dev/null
@@ -1,27 +0,0 @@
- 'boolean', 'sort_order' => 'integer'];
- }
-
- public function parent(): BelongsTo
- {
- return $this->belongsTo(self::class, 'parent_id');
- }
-
- public function children(): HasMany
- {
- return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order');
- }
-}
diff --git a/Categories/Providers/CategoriesServiceProvider.php b/Categories/Providers/CategoriesServiceProvider.php
deleted file mode 100644
index 8a20b37..0000000
--- a/Categories/Providers/CategoriesServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Categories', 'Config/config.php'), 'categories');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Categories', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Categories', 'Resources/views'), 'categories');
- }
-}
diff --git a/Categories/Providers/RouteServiceProvider.php b/Categories/Providers/RouteServiceProvider.php
deleted file mode 100644
index 1423c92..0000000
--- a/Categories/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Categories', 'Routes/web.php'));
- }
-}
diff --git a/Categories/Resources/views/admin/form.blade.php b/Categories/Resources/views/admin/form.blade.php
deleted file mode 100644
index 3ec530b..0000000
--- a/Categories/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-@csrf @isset($record) @method('PUT') @endisset None @foreach($parents as $parent)parent_id ?? null)==$parent->id)>{{ $parent->name }} @endforeach{{ old('description',$record->description ?? '') }} Save
diff --git a/Categories/Resources/views/admin/index.blade.php b/Categories/Resources/views/admin/index.blade.php
deleted file mode 100644
index 8b15801..0000000
--- a/Categories/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Categories Manage publishing categories.
Create @if(session('status')){{ session('status') }}
@endifName / title Status Actions @forelse($records as $record){{ $record->title ?? $record->name }} {{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
diff --git a/Categories/Routes/web.php b/Categories/Routes/web.php
deleted file mode 100644
index 6ec5ed1..0000000
--- a/Categories/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/categories')->name('administration.categories.')->group(function (): void {
- Route::get('/', [CategoryController::class, 'index'])->name('index');
- Route::get('/create', [CategoryController::class, 'create'])->name('create');
- Route::post('/', [CategoryController::class, 'store'])->name('store');
- Route::get('/{category}/edit', [CategoryController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{category}', [CategoryController::class, 'update'])->name('update');
- Route::delete('/{category}', [CategoryController::class, 'destroy'])->name('destroy');
-});
diff --git a/Categories/Tests/Feature/ModuleBootTest.php b/Categories/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 479e0ae..0000000
--- a/Categories/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Categories', config('categories.name'));
- }
-}
diff --git a/Categories/module.json b/Categories/module.json
deleted file mode 100644
index bde3d86..0000000
--- a/Categories/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Categories",
- "alias": "categories",
- "version": "1.0.0",
- "description": "Categories publishing module.",
- "keywords": ["publishing", "categories"],
- "priority": 30,
- "providers": ["Modules\\Categories\\Providers\\CategoriesServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/CloudDevOps/Config/config.php b/CloudDevOps/Config/config.php
deleted file mode 100644
index 8278777..0000000
--- a/CloudDevOps/Config/config.php
+++ /dev/null
@@ -1,9 +0,0 @@
- 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'],
-];
diff --git a/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php b/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php
deleted file mode 100644
index 565e25b..0000000
--- a/CloudDevOps/Console/Commands/CloudDevOpsHealthCommand.php
+++ /dev/null
@@ -1,22 +0,0 @@
-each(fn ($check) => $runner->run($check));
- $this->info('Health checks completed.');
-
- return self::SUCCESS;
- }
-}
diff --git a/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php b/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php
deleted file mode 100644
index 4309b43..0000000
--- a/CloudDevOps/Console/Commands/CloudDevOpsPruneCommand.php
+++ /dev/null
@@ -1,22 +0,0 @@
-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;
- }
-}
diff --git a/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php b/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php
deleted file mode 100644
index 9b9d98e..0000000
--- a/CloudDevOps/Database/Migrations/2026_07_24_180000_create_cloud_devops_tables.php
+++ /dev/null
@@ -1,122 +0,0 @@
-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');
- }
-};
diff --git a/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php b/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php
deleted file mode 100644
index 54897f1..0000000
--- a/CloudDevOps/Http/Controllers/CloudDevOpsApiController.php
+++ /dev/null
@@ -1,21 +0,0 @@
-json(['data' => DeploymentEnvironment::withCount(['targets', 'pipelines', 'releases'])->get()]);
- }
-
- public function releases(): JsonResponse
- {
- return response()->json(['data' => DeploymentRelease::with(['environment', 'steps'])->latest()->paginate()]);
- }
-}
diff --git a/CloudDevOps/Http/Controllers/CloudDevOpsController.php b/CloudDevOps/Http/Controllers/CloudDevOpsController.php
deleted file mode 100644
index dcc212b..0000000
--- a/CloudDevOps/Http/Controllers/CloudDevOpsController.php
+++ /dev/null
@@ -1,50 +0,0 @@
- 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.');
- }
-}
diff --git a/CloudDevOps/Models/DeploymentEnvironment.php b/CloudDevOps/Models/DeploymentEnvironment.php
deleted file mode 100644
index 55bc24b..0000000
--- a/CloudDevOps/Models/DeploymentEnvironment.php
+++ /dev/null
@@ -1,36 +0,0 @@
- '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');
- }
-}
diff --git a/CloudDevOps/Models/DeploymentHealthCheck.php b/CloudDevOps/Models/DeploymentHealthCheck.php
deleted file mode 100644
index 2ff53fb..0000000
--- a/CloudDevOps/Models/DeploymentHealthCheck.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'datetime'];
- }
-}
diff --git a/CloudDevOps/Models/DeploymentPipeline.php b/CloudDevOps/Models/DeploymentPipeline.php
deleted file mode 100644
index 1aa3ad2..0000000
--- a/CloudDevOps/Models/DeploymentPipeline.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'array', 'requires_approval' => 'boolean', 'is_active' => 'boolean'];
- }
-}
diff --git a/CloudDevOps/Models/DeploymentRelease.php b/CloudDevOps/Models/DeploymentRelease.php
deleted file mode 100644
index 5eca011..0000000
--- a/CloudDevOps/Models/DeploymentRelease.php
+++ /dev/null
@@ -1,27 +0,0 @@
- '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');
- }
-}
diff --git a/CloudDevOps/Models/DeploymentRollback.php b/CloudDevOps/Models/DeploymentRollback.php
deleted file mode 100644
index 8c4cb66..0000000
--- a/CloudDevOps/Models/DeploymentRollback.php
+++ /dev/null
@@ -1,10 +0,0 @@
- 'datetime'];
- }
-}
diff --git a/CloudDevOps/Models/DeploymentStep.php b/CloudDevOps/Models/DeploymentStep.php
deleted file mode 100644
index fc2e05b..0000000
--- a/CloudDevOps/Models/DeploymentStep.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'datetime', 'completed_at' => 'datetime'];
- }
-}
diff --git a/CloudDevOps/Models/DeploymentTarget.php b/CloudDevOps/Models/DeploymentTarget.php
deleted file mode 100644
index d5de432..0000000
--- a/CloudDevOps/Models/DeploymentTarget.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'encrypted:array', 'is_active' => 'boolean'];
- }
-}
diff --git a/CloudDevOps/Providers/CloudDevOpsServiceProvider.php b/CloudDevOps/Providers/CloudDevOpsServiceProvider.php
deleted file mode 100644
index 763eab7..0000000
--- a/CloudDevOps/Providers/CloudDevOpsServiceProvider.php
+++ /dev/null
@@ -1,25 +0,0 @@
-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]);
- }
- }
-}
diff --git a/CloudDevOps/Providers/RouteServiceProvider.php b/CloudDevOps/Providers/RouteServiceProvider.php
deleted file mode 100644
index 1b62500..0000000
--- a/CloudDevOps/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('CloudDevOps', 'Routes/web.php'));
- Route::middleware('api')->prefix('api')->group(module_path('CloudDevOps', 'Routes/api.php'));
- }
-}
diff --git a/CloudDevOps/Resources/views/index.blade.php b/CloudDevOps/Resources/views/index.blade.php
deleted file mode 100644
index cefb0ce..0000000
--- a/CloudDevOps/Resources/views/index.blade.php
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
- Cloud & DevOps
- Manage environments, deployment releases, pipelines and service health.
-
-
- @if (session('status'))
-
{{ session('status') }}
- @endif
-
-
-
Environments
{{ $environments->count() }}
-
Recent releases
{{ $releases->count() }}
-
Healthy checks
{{ $checks->where('status', 'healthy')->count() }}
-
-
- @can('cloud-devops.manage')
-
- @csrf
-
-
- Development Staging Production
- Create environment
-
- @endcan
-
-
- Environments
-
- @forelse ($environments as $environment)
-
- {{ $environment->name }} {{ ucfirst($environment->type) }} · {{ ucfirst($environment->status) }}
@if ($environment->is_protected)
Protected @endif
- Targets {{ $environment->targets_count }}
Pipelines {{ $environment->pipelines_count }}
Releases {{ $environment->releases_count }}
-
- @empty
-
No deployment environments configured.
- @endforelse
-
-
-
-
Recent releases Version Environment Status Created @forelse ($releases as $release){{ $release->version }} {{ $release->environment?->name }} {{ ucfirst(str_replace('_', ' ', $release->status)) }} {{ $release->created_at?->diffForHumans() }} @emptyNo releases yet. @endforelse
-
-
diff --git a/CloudDevOps/Routes/api.php b/CloudDevOps/Routes/api.php
deleted file mode 100644
index 89d17bf..0000000
--- a/CloudDevOps/Routes/api.php
+++ /dev/null
@@ -1,9 +0,0 @@
-prefix('v1/cloud-devops')->group(function (): void {
- Route::get('/environments', [CloudDevOpsApiController::class, 'environments']);
- Route::get('/releases', [CloudDevOpsApiController::class, 'releases']);
-});
diff --git a/CloudDevOps/Routes/web.php b/CloudDevOps/Routes/web.php
deleted file mode 100644
index fb5ebf6..0000000
--- a/CloudDevOps/Routes/web.php
+++ /dev/null
@@ -1,14 +0,0 @@
-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');
- });
diff --git a/CloudDevOps/Services/DeploymentManager.php b/CloudDevOps/Services/DeploymentManager.php
deleted file mode 100644
index 1b9c665..0000000
--- a/CloudDevOps/Services/DeploymentManager.php
+++ /dev/null
@@ -1,40 +0,0 @@
-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();
- }
-}
diff --git a/CloudDevOps/Services/HealthCheckRunner.php b/CloudDevOps/Services/HealthCheckRunner.php
deleted file mode 100644
index 3c9f32a..0000000
--- a/CloudDevOps/Services/HealthCheckRunner.php
+++ /dev/null
@@ -1,24 +0,0 @@
-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();
- }
-}
diff --git a/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php b/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php
deleted file mode 100644
index 5425d71..0000000
--- a/CloudDevOps/Tests/Feature/CloudDevOpsModuleTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
- 'Production', 'slug' => 'production', 'type' => 'production']);
- $this->assertDatabaseHas('deployment_environments', ['id' => $environment->id, 'slug' => 'production']);
- }
-}
diff --git a/CloudDevOps/module.json b/CloudDevOps/module.json
deleted file mode 100644
index 5a14f70..0000000
--- a/CloudDevOps/module.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "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": []
-}
diff --git a/Comments/Config/config.php b/Comments/Config/config.php
deleted file mode 100644
index e205f05..0000000
--- a/Comments/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Comments'];
diff --git a/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php b/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php
deleted file mode 100644
index 89de713..0000000
--- a/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php
+++ /dev/null
@@ -1,31 +0,0 @@
-id();
- $table->nullableMorphs('commentable');
- $table->foreignId('parent_id')->nullable()->constrained('comments')->cascadeOnDelete();
- $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
- $table->string('author_name');
- $table->string('author_email');
- $table->text('body');
- $table->string('status')->default('pending')->index();
- $table->string('ip_address', 45)->nullable();
- $table->text('moderation_notes')->nullable();
- $table->timestamp('approved_at')->nullable();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('comments');
- }
-};
diff --git a/Comments/Http/Controllers/CommentController.php b/Comments/Http/Controllers/CommentController.php
deleted file mode 100644
index c8948af..0000000
--- a/Comments/Http/Controllers/CommentController.php
+++ /dev/null
@@ -1,38 +0,0 @@
-filled('status')) {
- $q->where('status', $r->string('status'));
- }
-
-return view('comments::admin.index', ['records' => $q->paginate(30)]);
- }
-
- public function update(Request $r, Comment $comment): RedirectResponse
- {
- $data = $r->validate(['status' => ['required', 'in:pending,approved,spam,rejected'], 'moderation_notes' => ['nullable', 'string']]);
- $data['approved_at'] = $data['status'] === 'approved' ? now() : null;
- $comment->update($data);
-
- return back()->with('status', 'Comment moderated.');
- }
-
- public function destroy(Comment $comment): RedirectResponse
- {
- $comment->delete();
-
- return back()->with('status', 'Comment deleted.');
- }
-}
diff --git a/Comments/Models/Comment.php b/Comments/Models/Comment.php
deleted file mode 100644
index 0da0238..0000000
--- a/Comments/Models/Comment.php
+++ /dev/null
@@ -1,21 +0,0 @@
- 'datetime'];
- }
-
- public function commentable(): MorphTo
- {
- return $this->morphTo();
- }
-}
diff --git a/Comments/Providers/CommentsServiceProvider.php b/Comments/Providers/CommentsServiceProvider.php
deleted file mode 100644
index d82e931..0000000
--- a/Comments/Providers/CommentsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Comments', 'Config/config.php'), 'comments');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Comments', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Comments', 'Resources/views'), 'comments');
- }
-}
diff --git a/Comments/Providers/RouteServiceProvider.php b/Comments/Providers/RouteServiceProvider.php
deleted file mode 100644
index b83240b..0000000
--- a/Comments/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Comments', 'Routes/web.php'));
- }
-}
diff --git a/Comments/Resources/views/admin/index.blade.php b/Comments/Resources/views/admin/index.blade.php
deleted file mode 100644
index a9e6652..0000000
--- a/Comments/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Comment moderation @if(session('status')){{ session('status') }}
@endif@forelse($records as $comment)
@empty
No comments awaiting moderation.
@endforelse
{{ $records->links() }}
diff --git a/Comments/Routes/web.php b/Comments/Routes/web.php
deleted file mode 100644
index d40cb18..0000000
--- a/Comments/Routes/web.php
+++ /dev/null
@@ -1,10 +0,0 @@
-prefix('administration/comments')->name('administration.comments.')->group(function (): void {
- Route::get('/', [CommentController::class, 'index'])->name('index');
- Route::put('/{comment}', [CommentController::class, 'update'])->name('update');
- Route::delete('/{comment}', [CommentController::class, 'destroy'])->name('destroy');
-});
diff --git a/Comments/Tests/Feature/ModuleBootTest.php b/Comments/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index bf969d3..0000000
--- a/Comments/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Comments', config('comments.name'));
- }
-}
diff --git a/Comments/module.json b/Comments/module.json
deleted file mode 100644
index ab9f85e..0000000
--- a/Comments/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Comments",
- "alias": "comments",
- "version": "1.0.0",
- "description": "Comments publishing module.",
- "keywords": ["publishing", "comments"],
- "priority": 30,
- "providers": ["Modules\\Comments\\Providers\\CommentsServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Commerce/Config/config.php b/Commerce/Config/config.php
deleted file mode 100644
index 7eb951b..0000000
--- a/Commerce/Config/config.php
+++ /dev/null
@@ -1,8 +0,0 @@
- env('COMMERCE_CURRENCY', 'GBP'),
- 'currency_symbol' => env('COMMERCE_CURRENCY_SYMBOL', '£'),
- 'prices_include_tax' => (bool) env('COMMERCE_PRICES_INCLUDE_TAX', true),
- 'low_stock_threshold' => (int) env('COMMERCE_LOW_STOCK_THRESHOLD', 5),
-];
diff --git a/Commerce/Contracts/InventoryProvider.php b/Commerce/Contracts/InventoryProvider.php
deleted file mode 100644
index d2aaa5c..0000000
--- a/Commerce/Contracts/InventoryProvider.php
+++ /dev/null
@@ -1,12 +0,0 @@
-id();
- $table->foreignId('parent_id')->nullable()->constrained('commerce_categories')->nullOnDelete();
- $table->string('name');
- $table->string('slug')->unique();
- $table->text('description')->nullable();
- $table->boolean('is_active')->default(true);
- $table->unsignedInteger('sort_order')->default(0);
- $table->timestamps();
- });
- Schema::create('commerce_products', function (Blueprint $table) {
- $table->id();
- $table->foreignId('category_id')->nullable()->constrained('commerce_categories')->nullOnDelete();
- $table->string('type')->default('physical');
- $table->string('name');
- $table->string('slug')->unique();
- $table->string('sku')->nullable()->unique();
- $table->text('summary')->nullable();
- $table->longText('description')->nullable();
- $table->decimal('price', 12, 2)->default(0);
- $table->decimal('compare_at_price', 12, 2)->nullable();
- $table->decimal('cost_price', 12, 2)->nullable();
- $table->string('currency', 3)->default('GBP');
- $table->boolean('track_inventory')->default(true);
- $table->integer('stock_quantity')->default(0);
- $table->integer('low_stock_threshold')->default(5);
- $table->boolean('is_featured')->default(false);
- $table->string('status')->default('draft');
- $table->timestamp('published_at')->nullable();
- $table->json('metadata')->nullable();
- $table->timestamps();
- $table->softDeletes();
- $table->index(['status', 'published_at']);
- });
- Schema::create('commerce_product_variants', function (Blueprint $table) {
- $table->id();
- $table->foreignId('product_id')->constrained('commerce_products')->cascadeOnDelete();
- $table->string('name');
- $table->string('sku')->unique();
- $table->decimal('price', 12, 2)->nullable();
- $table->integer('stock_quantity')->default(0);
- $table->json('options')->nullable();
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- Schema::create('commerce_customers', function (Blueprint $table) {
- $table->id();
- $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
- $table->string('email')->index();
- $table->string('first_name');
- $table->string('last_name');
- $table->string('company')->nullable();
- $table->string('phone')->nullable();
- $table->string('tax_number')->nullable();
- $table->json('billing_address')->nullable();
- $table->json('shipping_address')->nullable();
- $table->string('status')->default('active');
- $table->timestamps();
- });
- Schema::create('commerce_orders', function (Blueprint $table) {
- $table->id();
- $table->string('number')->unique();
- $table->foreignId('customer_id')->nullable()->constrained('commerce_customers')->nullOnDelete();
- $table->string('email');
- $table->string('status')->default('pending');
- $table->string('payment_status')->default('unpaid');
- $table->string('fulfilment_status')->default('unfulfilled');
- $table->string('currency', 3)->default('GBP');
- $table->decimal('subtotal', 12, 2)->default(0);
- $table->decimal('discount_total', 12, 2)->default(0);
- $table->decimal('shipping_total', 12, 2)->default(0);
- $table->decimal('tax_total', 12, 2)->default(0);
- $table->decimal('grand_total', 12, 2)->default(0);
- $table->json('billing_address')->nullable();
- $table->json('shipping_address')->nullable();
- $table->text('notes')->nullable();
- $table->timestamp('placed_at')->nullable();
- $table->timestamps();
- $table->index(['status', 'payment_status']);
- });
- Schema::create('commerce_order_items', function (Blueprint $table) {
- $table->id();
- $table->foreignId('order_id')->constrained('commerce_orders')->cascadeOnDelete();
- $table->foreignId('product_id')->nullable()->constrained('commerce_products')->nullOnDelete();
- $table->foreignId('variant_id')->nullable()->constrained('commerce_product_variants')->nullOnDelete();
- $table->string('name');
- $table->string('sku')->nullable();
- $table->unsignedInteger('quantity');
- $table->decimal('unit_price', 12, 2);
- $table->decimal('tax_total', 12, 2)->default(0);
- $table->decimal('line_total', 12, 2);
- $table->json('metadata')->nullable();
- $table->timestamps();
- });
- Schema::create('commerce_inventory_movements', function (Blueprint $table) {
- $table->id();
- $table->foreignId('product_id')->constrained('commerce_products')->cascadeOnDelete();
- $table->foreignId('variant_id')->nullable()->constrained('commerce_product_variants')->cascadeOnDelete();
- $table->integer('quantity');
- $table->string('reason');
- $table->string('reference_type')->nullable();
- $table->unsignedBigInteger('reference_id')->nullable();
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->text('notes')->nullable();
- $table->timestamps();
- $table->index(['reference_type', 'reference_id']);
- });
- Schema::create('commerce_promotions', function (Blueprint $table) {
- $table->id();
- $table->string('name');
- $table->string('code')->nullable()->unique();
- $table->string('type')->default('percentage');
- $table->decimal('value', 12, 2);
- $table->decimal('minimum_order', 12, 2)->nullable();
- $table->unsignedInteger('usage_limit')->nullable();
- $table->unsignedInteger('times_used')->default(0);
- $table->timestamp('starts_at')->nullable();
- $table->timestamp('ends_at')->nullable();
- $table->boolean('is_active')->default(true);
- $table->json('conditions')->nullable();
- $table->timestamps();
- });
- Schema::create('commerce_shipping_methods', function (Blueprint $table) {
- $table->id();
- $table->string('name');
- $table->string('code')->unique();
- $table->decimal('price', 12, 2)->default(0);
- $table->decimal('free_over', 12, 2)->nullable();
- $table->boolean('is_active')->default(true);
- $table->json('settings')->nullable();
- $table->timestamps();
- });
- Schema::create('commerce_tax_rates', function (Blueprint $table) {
- $table->id();
- $table->string('name');
- $table->string('country_code', 2)->nullable();
- $table->decimal('rate', 7, 4)->default(0);
- $table->boolean('is_default')->default(false);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- Schema::create('commerce_payment_transactions', function (Blueprint $table) {
- $table->id();
- $table->foreignId('order_id')->constrained('commerce_orders')->cascadeOnDelete();
- $table->string('provider');
- $table->string('provider_reference')->nullable()->index();
- $table->string('type')->default('payment');
- $table->string('status')->default('pending');
- $table->decimal('amount', 12, 2);
- $table->string('currency', 3);
- $table->json('payload')->nullable();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- foreach (['commerce_payment_transactions', 'commerce_tax_rates', 'commerce_shipping_methods', 'commerce_promotions', 'commerce_inventory_movements', 'commerce_order_items', 'commerce_orders', 'commerce_customers', 'commerce_product_variants', 'commerce_products', 'commerce_categories'] as $table) {
- Schema::dropIfExists($table);
- }
- }
-};
diff --git a/Commerce/Http/Controllers/CommerceController.php b/Commerce/Http/Controllers/CommerceController.php
deleted file mode 100644
index bf7eb83..0000000
--- a/Commerce/Http/Controllers/CommerceController.php
+++ /dev/null
@@ -1,53 +0,0 @@
- Product::latest()->paginate(20), 'orders' => Order::latest()->limit(10)->get(), 'revenue' => Order::where('payment_status', 'paid')->sum('grand_total'), 'lowStock' => Product::where('track_inventory', true)->whereColumn('stock_quantity', '<=', 'low_stock_threshold')->count(), 'customers' => Customer::count(), 'promotions' => Promotion::where('is_active', true)->count()]);
- }
-
- public function storeProduct(Request $request): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255', 'unique:commerce_products,slug'], 'sku' => ['nullable', 'string', 'max:120', 'unique:commerce_products,sku'], 'type' => ['required', 'in:physical,digital,service,bundle'], 'summary' => ['nullable', 'string'], 'description' => ['nullable', 'string'], 'price' => ['required', 'numeric', 'min:0'], 'stock_quantity' => ['required', 'integer'], 'status' => ['required', 'in:draft,active,archived']]);
- $data['slug'] = $data['slug'] ?: Str::slug($data['name']);
- $data['currency'] = config('commerce.currency', 'GBP');
- $data['published_at'] = $data['status'] === 'active' ? now() : null;
- Product::create($data);
-
- return back()->with('status', 'Product created.');
- }
-
- public function updateProduct(Request $request, Product $product): RedirectResponse
- {
- $product->update($request->validate(['name' => ['required', 'string', 'max:255'], 'price' => ['required', 'numeric', 'min:0'], 'stock_quantity' => ['required', 'integer'], 'status' => ['required', 'in:draft,active,archived']]));
-
- return back()->with('status', 'Product updated.');
- }
-
- public function destroyProduct(Product $product): RedirectResponse
- {
- $product->delete();
-
- return back()->with('status', 'Product deleted.');
- }
-
- public function updateOrder(Request $request, Order $order): RedirectResponse
- {
- $order->update($request->validate(['status' => ['required', 'in:pending,processing,completed,cancelled,refunded'], 'payment_status' => ['required', 'in:unpaid,authorised,paid,partially_refunded,refunded,failed'], 'fulfilment_status' => ['required', 'in:unfulfilled,partial,fulfilled,returned']]));
-
- return back()->with('status', 'Order updated.');
- }
-}
diff --git a/Commerce/Http/Controllers/ProductApiController.php b/Commerce/Http/Controllers/ProductApiController.php
deleted file mode 100644
index 07e871f..0000000
--- a/Commerce/Http/Controllers/ProductApiController.php
+++ /dev/null
@@ -1,20 +0,0 @@
-json(Product::query()->where('status', 'active')->where(fn ($q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->latest()->paginate(24));
- }
-
- public function show(string $product): JsonResponse
- {
- return response()->json(Product::where('status', 'active')->where(fn ($q) => $q->where('slug', $product)->orWhere('id', $product))->firstOrFail());
- }
-}
diff --git a/Commerce/Models/Category.php b/Commerce/Models/Category.php
deleted file mode 100644
index 76dce14..0000000
--- a/Commerce/Models/Category.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'boolean'];
- }
-}
diff --git a/Commerce/Models/Customer.php b/Commerce/Models/Customer.php
deleted file mode 100644
index 2ed400c..0000000
--- a/Commerce/Models/Customer.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'array', 'shipping_address' => 'array'];
- }
-}
diff --git a/Commerce/Models/InventoryMovement.php b/Commerce/Models/InventoryMovement.php
deleted file mode 100644
index 68c3cb1..0000000
--- a/Commerce/Models/InventoryMovement.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'decimal:2', 'discount_total' => 'decimal:2', 'shipping_total' => 'decimal:2', 'tax_total' => 'decimal:2', 'grand_total' => 'decimal:2', 'billing_address' => 'array', 'shipping_address' => 'array', 'placed_at' => 'datetime'];
- }
-}
diff --git a/Commerce/Models/OrderItem.php b/Commerce/Models/OrderItem.php
deleted file mode 100644
index 06072b7..0000000
--- a/Commerce/Models/OrderItem.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'decimal:2', 'tax_total' => 'decimal:2', 'line_total' => 'decimal:2', 'metadata' => 'array'];
- }
-}
diff --git a/Commerce/Models/Product.php b/Commerce/Models/Product.php
deleted file mode 100644
index a58482f..0000000
--- a/Commerce/Models/Product.php
+++ /dev/null
@@ -1,20 +0,0 @@
- 'decimal:2', 'compare_at_price' => 'decimal:2', 'cost_price' => 'decimal:2', 'track_inventory' => 'boolean', 'is_featured' => 'boolean', 'published_at' => 'datetime', 'metadata' => 'array'];
- }
-}
diff --git a/Commerce/Models/ProductVariant.php b/Commerce/Models/ProductVariant.php
deleted file mode 100644
index 72482ce..0000000
--- a/Commerce/Models/ProductVariant.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'decimal:2', 'options' => 'array', 'is_active' => 'boolean'];
- }
-}
diff --git a/Commerce/Models/Promotion.php b/Commerce/Models/Promotion.php
deleted file mode 100644
index e16cb92..0000000
--- a/Commerce/Models/Promotion.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'decimal:2', 'minimum_order' => 'decimal:2', 'starts_at' => 'datetime', 'ends_at' => 'datetime', 'is_active' => 'boolean', 'conditions' => 'array'];
- }
-}
diff --git a/Commerce/Models/Relations.php b/Commerce/Models/Relations.php
deleted file mode 100644
index 7814843..0000000
--- a/Commerce/Models/Relations.php
+++ /dev/null
@@ -1,3 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Commerce', 'Config/config.php'), 'commerce');
- $this->app->bind(InventoryProvider::class, DatabaseInventoryProvider::class);
- $this->app->bind(PaymentProvider::class, ManualPaymentProvider::class);
- $this->app->bind(ShippingProvider::class, FlatRateShippingProvider::class);
- $this->app->bind(TaxProvider::class, ConfiguredTaxProvider::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Commerce', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Commerce', 'Resources/views'), 'commerce');
- }
-}
diff --git a/Commerce/Providers/RouteServiceProvider.php b/Commerce/Providers/RouteServiceProvider.php
deleted file mode 100644
index 1dcfdb7..0000000
--- a/Commerce/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Commerce', 'Routes/web.php'));
- Route::prefix('api/v1')->middleware('api')->group(module_path('Commerce', 'Routes/api.php'));
- }
-}
diff --git a/Commerce/Resources/views/admin/index.blade.php b/Commerce/Resources/views/admin/index.blade.php
deleted file mode 100644
index 4960512..0000000
--- a/Commerce/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-Commerce Catalogue, stock, customers, orders and promotions.
@if(session('status'))
{{ session('status') }}
@endif
-
Products
{{ $products->total() }}
Orders
{{ \Modules\Commerce\Models\Order::count() }}
Revenue
{{ config('commerce.currency_symbol','£') }}{{ number_format($revenue,2) }}
Customers
{{ $customers }}
-
@csrfAdd product Physical Digital Service Bundle
Draft Active Archived Create product
-
Product SKU Price Stock Status @forelse($products as $product){{ $product->name }} {{ $product->sku ?: '—' }} {{ config('commerce.currency_symbol','£') }}{{ number_format($product->price,2) }} {{ $product->stock_quantity }} {{ ucfirst($product->status) }} @csrf @method('DELETE')Delete @emptyNo products yet. @endforelse
{{ $products->links() }}
-
Recent orders Order Customer Total Status Payment @forelse($orders as $order){{ $order->number }} {{ $order->email }} {{ $order->currency }} {{ number_format($order->grand_total,2) }} {{ ucfirst($order->status) }} {{ ucfirst($order->payment_status) }} @emptyNo orders yet. @endforelse
diff --git a/Commerce/Routes/api.php b/Commerce/Routes/api.php
deleted file mode 100644
index 16ddbdc..0000000
--- a/Commerce/Routes/api.php
+++ /dev/null
@@ -1,7 +0,0 @@
-name('administration.commerce.')->middleware(['auth', 'permission:commerce.view'])->group(function () {
- Route::get('/', [CommerceController::class, 'index'])->name('index');
- Route::post('/products', [CommerceController::class, 'storeProduct'])->middleware('permission:commerce.manage')->name('products.store');
- Route::put('/products/{product}', [CommerceController::class, 'updateProduct'])->middleware('permission:commerce.manage')->name('products.update');
- Route::delete('/products/{product}', [CommerceController::class, 'destroyProduct'])->middleware('permission:commerce.manage')->name('products.destroy');
- Route::put('/orders/{order}', [CommerceController::class, 'updateOrder'])->middleware('permission:commerce.manage')->name('orders.update');
-});
diff --git a/Commerce/Services/ConfiguredTaxProvider.php b/Commerce/Services/ConfiguredTaxProvider.php
deleted file mode 100644
index ee0a4ad..0000000
--- a/Commerce/Services/ConfiguredTaxProvider.php
+++ /dev/null
@@ -1,13 +0,0 @@
-increment('stock_quantity', $quantity);
- InventoryMovement::create(['product_id' => $product->id, 'quantity' => $quantity, 'reason' => $reason, 'created_by' => $userId]);
- });
- }
-
- public function available(Product $product): int
- {
- return max(0, (int) $product->stock_quantity);
- }
-}
diff --git a/Commerce/Services/FlatRateShippingProvider.php b/Commerce/Services/FlatRateShippingProvider.php
deleted file mode 100644
index 30e08ed..0000000
--- a/Commerce/Services/FlatRateShippingProvider.php
+++ /dev/null
@@ -1,13 +0,0 @@
-= 50 ? 0.0 : 4.95;
- }
-}
diff --git a/Commerce/Services/ManualPaymentProvider.php b/Commerce/Services/ManualPaymentProvider.php
deleted file mode 100644
index 5d7ebed..0000000
--- a/Commerce/Services/ManualPaymentProvider.php
+++ /dev/null
@@ -1,19 +0,0 @@
- 'pending', 'provider' => 'manual', 'reference' => null];
- }
-
- public function refund(Order $order, float $amount): array
- {
- return ['status' => 'pending', 'provider' => 'manual', 'amount' => $amount];
- }
-}
diff --git a/Commerce/Tests/Feature/ModuleBootTest.php b/Commerce/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 2932611..0000000
--- a/Commerce/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,19 +0,0 @@
-assertTrue(app('router')->has('administration.commerce.index'));
- }
-
- public function test_commerce_contracts_are_bound(): void
- {
- $this->assertInstanceOf(InventoryProvider::class, app(InventoryProvider::class));
- }
-}
diff --git a/Commerce/module.json b/Commerce/module.json
deleted file mode 100644
index f5f48cb..0000000
--- a/Commerce/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Commerce","alias":"commerce","version":"1.0.0","description":"Products, inventory, customers, orders, promotions, tax, shipping and payments.","keywords":["commerce","catalog","inventory","orders","customers"],"priority":110,"providers":["Modules\\Commerce\\Providers\\CommerceServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Media","Sites"]}
diff --git a/Content/Config/config.php b/Content/Config/config.php
deleted file mode 100644
index b0ba6d5..0000000
--- a/Content/Config/config.php
+++ /dev/null
@@ -1,5 +0,0 @@
- 'home',
- 'regions' => ['header', 'body', 'footer'],
-];
diff --git a/Content/Console/InstallSaaslandContent.php b/Content/Console/InstallSaaslandContent.php
deleted file mode 100644
index ba501a6..0000000
--- a/Content/Console/InstallSaaslandContent.php
+++ /dev/null
@@ -1,244 +0,0 @@
- ! Schema::hasTable($table)));
-
- if ($missingTables !== []) {
- $this->components->error('Content composition tables are missing: '.implode(', ', $missingTables).'.');
- $this->components->info('Run php artisan migrate before installing the Saasland composition.');
- return self::FAILURE;
- }
-
- DB::transaction(function (): void {
- $this->installSharedSettings();
- $this->installMenus();
- if ($this->option('force')) {
- PageContent::query()->where('page_key', 'home')->delete();
- }
-
- $order = [
- 'site-header' => ['header', 10], 'hero' => ['body', 10], 'feature-grid' => ['body', 20],
- 'product-feed' => ['body', 30], 'testimonial-feed' => ['body', 40], 'team-feed' => ['body', 50],
- 'blog-feed' => ['body', 60], 'news-feed' => ['body', 70], 'faq-feed' => ['body', 80],
- 'newsletter-form' => ['body', 90], 'call-to-action' => ['body', 100], 'site-footer' => ['footer', 10],
- ];
-
- foreach ($this->definitions() as $key => $definition) {
- $type = ContentType::query()->updateOrCreate(['key' => $key], $definition['type']);
- $document = ContentDocument::query()->firstOrCreate(
- ['slug' => 'saasland-'.$key],
- [
- 'content_type_id' => $type->id,
- 'title' => $definition['title'],
- 'data' => $definition['data'],
- 'status' => 'published',
- 'published_at' => now(),
- ]
- );
-
- $currentData = is_array($document->data) ? $document->data : [];
- $mergedData = $this->mergeMissingDefaults($currentData, $definition['data']);
-
- $document->forceFill([
- 'content_type_id' => $type->id,
- 'data' => $mergedData,
- ])->save();
- [$region, $sort] = $order[$key];
- PageContent::query()->updateOrCreate(
- ['page_key' => 'home', 'region' => $region, 'sort_order' => $sort],
- ['content_document_id' => $document->id, 'is_enabled' => true]
- );
- }
- });
-
- $this->components->info('Saasland content composition installed with editable schemas.');
- return self::SUCCESS;
- }
-
- private function installSharedSettings(): void
- {
- $settings = app(SettingsRepository::class);
- $legacy = [];
-
- if (Schema::hasTable('stylist_theme_settings')) {
- $stored = DB::table('stylist_theme_settings')->where('theme_key', 'saasland')->value('settings');
- $legacy = is_string($stored) ? (json_decode($stored, true) ?: []) : [];
- }
-
- $defaults = [
- 'site_name' => $legacy['site_title'] ?? config('app.name'),
- 'site_description' => $legacy['site_description'] ?? 'Modern software for modern companies.',
- 'company_name' => $legacy['site_title'] ?? config('app.name'),
- 'site_url' => url('/'),
- 'logo_url' => $legacy['logo_url'] ?? '',
- 'facebook_url' => $legacy['facebook_url'] ?? '',
- 'x_url' => $legacy['x_url'] ?? '',
- 'linkedin_url' => $legacy['linkedin_url'] ?? '',
- 'dashboard_label' => 'Dashboard',
- 'login_label' => 'Log in',
- 'registration_label' => 'Get started',
- ];
-
- foreach ($defaults as $key => $value) {
- if ($settings->get($key, null, 'site') === null) {
- $settings->put($key, $value, 'site', 'string', true);
- }
- }
- }
-
- private function installMenus(): void
- {
- if (! Schema::hasTable('menus')) {
- return;
- }
-
- $menus = [
- 'primary' => ['Primary navigation', [['enabled' => true, 'label' => 'Home', 'url' => '/'], ['enabled' => true, 'label' => 'Blog', 'url' => '/blog'], ['enabled' => true, 'label' => 'News', 'url' => '/news']]],
- 'footer' => ['Footer — Explore', [['enabled' => true, 'label' => 'Home', 'url' => '/'], ['enabled' => true, 'label' => 'Blog', 'url' => '/blog'], ['enabled' => true, 'label' => 'News', 'url' => '/news']]],
- 'footer_account' => ['Footer — Account', [['enabled' => true, 'label' => 'Dashboard', 'url' => '/dashboard'], ['enabled' => true, 'label' => 'Profile', 'url' => '/settings/profile']]],
- 'footer_legal' => ['Footer — Legal', []],
- ];
-
- foreach ($menus as $location => [$name, $items]) {
- Menu::query()->firstOrCreate(['location' => $location], ['name' => $name, 'items' => $items, 'enabled' => true]);
- }
- }
-
- private function mergeMissingDefaults(array $current, array $defaults): array
- {
- foreach ($defaults as $key => $defaultValue) {
- if (! array_key_exists($key, $current)) {
- $current[$key] = $defaultValue;
- continue;
- }
-
- if (
- is_array($defaultValue)
- && is_array($current[$key])
- && ! array_is_list($defaultValue)
- && ! array_is_list($current[$key])
- ) {
- $current[$key] = $this->mergeMissingDefaults(
- $current[$key],
- $defaultValue
- );
- }
- }
-
- return $current;
- }
-
- private function definitions(): array
- {
- $field = static fn (string $key, string $label, string $type = 'text', array $extra = []): array => array_merge(compact('key', 'label', 'type'), $extra);
- $type = static fn (string $name, string $renderer, array $regions, array $schema): array => [
- 'name' => $name, 'renderer' => $renderer, 'allowed_regions' => $regions,
- 'is_system' => true, 'is_active' => true, 'schema' => $schema,
- ];
- $section = [$field('enabled', 'Show this section', 'boolean', ['default' => true])];
- $feed = array_merge($section, [
- $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'),
- $field('limit', 'Maximum items', 'number', ['min' => 1, 'max' => 50]),
- $field('include_ids', 'Only show record IDs', 'csv', ['help' => 'Optional comma-separated IDs. Leave blank to use the latest records.']),
- $field('exclude_ids', 'Exclude record IDs', 'csv', ['help' => 'Optional comma-separated IDs to hide from this section.']),
- ]);
-
- return [
- 'site-header' => [
- 'title' => 'Main website header',
- 'type' => $type('Site header', 'site-header', ['header'], [
- $field('enabled', 'Show header', 'boolean', ['default' => true]),
- $field('menu_location', 'Navigation menu location', 'text', ['help' => 'Usually primary. Edit links from Administration → Menus.']),
- $field('show_navigation', 'Show navigation', 'boolean', ['default' => true]),
- $field('show_dashboard', 'Show dashboard link', 'boolean', ['default' => true]),
- $field('show_user_name', 'Show signed-in user', 'boolean', ['default' => true]),
- $field('show_login', 'Show login', 'boolean', ['default' => true]),
- $field('show_registration', 'Show registration', 'boolean', ['default' => true]),
- $field('show_mobile_menu', 'Show mobile menu', 'boolean', ['default' => true]),
- ]),
- 'data' => ['enabled' => true, 'menu_location' => 'primary', 'show_navigation' => true, 'show_dashboard' => true, 'show_user_name' => true, 'show_login' => true, 'show_registration' => true, 'show_mobile_menu' => true],
- ],
- 'hero' => [
- 'title' => 'Homepage hero',
- 'type' => $type('Hero', 'hero', ['body'], [
- ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'),
- $field('primary_label', 'Primary button label'), $field('primary_url', 'Primary button URL', 'url'),
- $field('secondary_label', 'Secondary button label'), $field('secondary_url', 'Secondary button URL', 'url'),
- $field('show_metrics', 'Show metrics panel', 'boolean', ['default' => true]),
- $field('metrics', 'Metrics', 'repeater', ['fields' => [
- $field('enabled', 'Show item', 'boolean', ['default' => true]), $field('label', 'Label'), $field('value', 'Value'), $field('change', 'Change'), $field('height', 'Chart height (%)', 'number', ['min' => 0, 'max' => 100]),
- ]]),
- ]),
- 'data' => ['enabled' => true, 'eyebrow' => 'A better way to run your business', 'heading' => 'Transform Your Business With Our SaaS Solution', 'description' => 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.', 'primary_label' => 'Get started free', 'primary_url' => '/register', 'secondary_label' => 'Book demo', 'secondary_url' => '#contact', 'show_metrics' => true, 'metrics' => [
- ['enabled' => true, 'label' => 'Monthly revenue', 'value' => '£48,240', 'change' => '+18.4%', 'height' => 62],
- ['enabled' => true, 'label' => 'Active users', 'value' => '12,842', 'change' => '+9.2%', 'height' => 78],
- ['enabled' => true, 'label' => 'Conversion', 'value' => '7.8%', 'change' => '+2.1%', 'height' => 92],
- ]],
- ],
- 'feature-grid' => [
- 'title' => 'Core features',
- 'type' => $type('Feature grid', 'feature-grid', ['body'], [
- ...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'),
- $field('items', 'Features', 'repeater', ['fields' => [$field('enabled', 'Show item', 'boolean', ['default' => true]), $field('title', 'Title'), $field('description', 'Description', 'textarea')]]),
- ]),
- 'data' => ['enabled' => true, 'eyebrow' => 'Powerful features', 'heading' => 'Everything your team needs to grow', 'description' => 'Planning, collaboration, reporting, publishing, commerce and customer insight in one extensible platform.', 'items' => [
- ['enabled' => true, 'title' => 'Smart automation', 'description' => 'Remove repetitive work and delays.'],
- ['enabled' => true, 'title' => 'Real-time analytics', 'description' => 'Turn activity into useful business insight.'],
- ['enabled' => true, 'title' => 'Content operations', 'description' => 'Create, review and publish confidently.'],
- ['enabled' => true, 'title' => 'Secure by design', 'description' => 'Protect users and data with dependable controls.'],
- ['enabled' => true, 'title' => 'Flexible integrations', 'description' => 'Connect the tools your organisation already uses.'],
- ['enabled' => true, 'title' => 'Modular architecture', 'description' => 'Activate only the capabilities each site needs.'],
- ]],
- ],
- 'product-feed' => ['title' => 'Pricing plans', 'type' => $type('Product feed', 'product-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Simple pricing', 'heading' => 'Choose a plan that fits', 'description' => 'Plans are managed from Products & Pricing in the CMS.', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']],
- 'testimonial-feed' => ['title' => 'Customer stories', 'type' => $type('Testimonial feed', 'testimonial-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Customer stories', 'heading' => 'Trusted by ambitious teams', 'description' => '', 'limit' => 6, 'include_ids' => '', 'exclude_ids' => '']],
- 'team-feed' => ['title' => 'Team', 'type' => $type('Team feed', 'team-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Our team', 'heading' => 'People behind the platform', 'description' => '', 'limit' => 8, 'include_ids' => '', 'exclude_ids' => '']],
- 'blog-feed' => ['title' => 'Latest blog posts', 'type' => $type('Blog feed', 'blog-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Insights', 'heading' => 'Latest from the blog', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']],
- 'news-feed' => ['title' => 'Latest news', 'type' => $type('News feed', 'news-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'Newsroom', 'heading' => 'Latest news', 'description' => '', 'limit' => 3, 'include_ids' => '', 'exclude_ids' => '']],
- 'faq-feed' => ['title' => 'Frequently asked questions', 'type' => $type('FAQ feed', 'faq-feed', ['body'], $feed), 'data' => ['enabled' => true, 'eyebrow' => 'FAQ', 'heading' => 'Answers before you begin', 'description' => '', 'limit' => 12, 'include_ids' => '', 'exclude_ids' => '']],
- 'newsletter-form' => [
- 'title' => 'Newsletter signup',
- 'type' => $type('Newsletter form', 'newsletter-form', ['body'], [...$section, $field('eyebrow', 'Eyebrow'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('placeholder', 'Email placeholder'), $field('button_label', 'Button label')]),
- 'data' => ['enabled' => true, 'eyebrow' => 'Newsletter', 'heading' => 'Useful updates, no noise', 'description' => 'Receive product news, publishing guidance, and practical platform updates.', 'placeholder' => 'you@example.com', 'button_label' => 'Subscribe'],
- ],
- 'call-to-action' => [
- 'title' => 'Homepage final call to action',
- 'type' => $type('Call to action', 'call-to-action', ['body'], [...$section, $field('anchor', 'Section anchor'), $field('heading', 'Heading'), $field('description', 'Description', 'textarea'), $field('button_label', 'Button label'), $field('button_url', 'Button URL', 'url')]),
- 'data' => ['enabled' => true, 'anchor' => 'contact', 'heading' => 'Ready to transform your business?', 'description' => 'Start building a faster, more connected operation today.', 'button_label' => 'Get started free', 'button_url' => '/register'],
- ],
- 'site-footer' => [
- 'title' => 'Main website footer',
- 'type' => $type('Site footer', 'site-footer', ['footer'], [
- $field('enabled', 'Show footer', 'boolean', ['default' => true]),
- $field('explore_heading', 'Explore column heading'),
- $field('explore_menu_location', 'Explore menu location'),
- $field('account_heading', 'Account column heading'),
- $field('account_menu_location', 'Account menu location'),
- $field('legal_heading', 'Legal column heading'),
- $field('legal_menu_location', 'Legal menu location'),
- $field('show_back_to_top', 'Show back-to-top link', 'boolean', ['default' => true]),
- $field('back_to_top_label', 'Back-to-top label'),
- ]),
- 'data' => ['enabled' => true, 'explore_heading' => 'Explore', 'explore_menu_location' => 'footer', 'account_heading' => 'Account', 'account_menu_location' => 'footer_account', 'legal_heading' => 'Legal', 'legal_menu_location' => 'footer_legal', 'show_back_to_top' => true, 'back_to_top_label' => 'Back to top ↑'],
- ],
- ];
- }
-}
diff --git a/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php b/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php
deleted file mode 100644
index 2fcd905..0000000
--- a/Content/Database/Migrations/2026_07_27_000000_create_content_composition_tables.php
+++ /dev/null
@@ -1,62 +0,0 @@
-id();
- $table->string('name');
- $table->string('key')->unique();
- $table->text('description')->nullable();
- $table->json('schema')->nullable();
- $table->string('renderer');
- $table->json('allowed_regions')->nullable();
- $table->boolean('is_system')->default(false);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
-
- Schema::create('content_documents', function (Blueprint $table): void {
- $table->id();
- $table->foreignId('content_type_id')->constrained('content_types')->cascadeOnDelete();
- $table->string('title');
- $table->string('slug')->unique();
- $table->json('data')->nullable();
- $table->string('status')->default('draft')->index();
- $table->timestamp('published_at')->nullable()->index();
- $table->timestamp('expires_at')->nullable();
- $table->string('language', 12)->nullable();
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- $table->softDeletes();
- });
-
- Schema::create('page_content', function (Blueprint $table): void {
- $table->id();
- $table->foreignId('page_id')->nullable()->constrained('pages')->cascadeOnDelete();
- $table->string('page_key')->nullable()->index();
- $table->foreignId('content_document_id')->constrained('content_documents')->cascadeOnDelete();
- $table->string('region')->index();
- $table->unsignedInteger('sort_order')->default(0);
- $table->boolean('is_enabled')->default(true);
- $table->json('visibility_rules')->nullable();
- $table->json('presentation')->nullable();
- $table->timestamp('starts_at')->nullable();
- $table->timestamp('ends_at')->nullable();
- $table->timestamps();
- $table->unique(['page_key', 'region', 'sort_order'], 'page_content_key_region_order_unique');
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('page_content');
- Schema::dropIfExists('content_documents');
- Schema::dropIfExists('content_types');
- }
-};
diff --git a/Content/Http/Controllers/ContentDocumentController.php b/Content/Http/Controllers/ContentDocumentController.php
deleted file mode 100644
index af35ce9..0000000
--- a/Content/Http/Controllers/ContentDocumentController.php
+++ /dev/null
@@ -1,146 +0,0 @@
-with('type')
- ->withCount('placements')
- ->when($request->string('type')->isNotEmpty(), fn ($query) => $query->whereHas('type', fn ($type) => $type->where('key', $request->string('type')->toString())))
- ->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')->toString()))
- ->when($request->string('search')->isNotEmpty(), fn ($query) => $query->where('title', 'like', '%'.$request->string('search')->toString().'%'))
- ->orderBy('title')
- ->paginate(30)
- ->withQueryString();
-
- return view('content::admin.index', [
- 'documents' => $documents,
- 'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(),
- ]);
- }
-
- public function create(Request $request): View
- {
- $types = ContentType::query()->where('is_active', true)->orderBy('name')->get();
- $selectedType = $types->firstWhere('id', (int) $request->integer('type')) ?? $types->first();
-
- return view('content::admin.form', [
- 'document' => new ContentDocument(['status' => 'draft', 'data' => []]),
- 'types' => $types,
- 'selectedType' => $selectedType,
- ]);
- }
-
- public function store(Request $request): RedirectResponse
- {
- $validated = $this->validateDocument($request);
- $type = ContentType::query()->where('is_active', true)->findOrFail($validated['content_type_id']);
-
- $document = ContentDocument::query()->create([
- 'content_type_id' => $type->id,
- 'title' => $validated['title'],
- 'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title']),
- 'status' => $validated['status'],
- 'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? now()) : null,
- 'expires_at' => $validated['expires_at'] ?? null,
- 'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR),
- 'created_by' => $request->user()?->id,
- 'updated_by' => $request->user()?->id,
- ]);
-
- return redirect()->route('content.documents.edit', $document)->with('status', 'Content document created.');
- }
-
- public function edit(ContentDocument $document): View
- {
- $document->load('type', 'placements');
-
- return view('content::admin.form', [
- 'document' => $document,
- 'types' => ContentType::query()->where('is_active', true)->orderBy('name')->get(),
- 'selectedType' => $document->type,
- ]);
- }
-
- public function update(Request $request, ContentDocument $document): RedirectResponse
- {
- $validated = $this->validateDocument($request, $document);
-
- $document->update([
- 'content_type_id' => $validated['content_type_id'],
- 'title' => $validated['title'],
- 'slug' => $this->uniqueSlug($validated['slug'] ?: $validated['title'], $document),
- 'status' => $validated['status'],
- 'published_at' => $validated['status'] === 'published' ? ($validated['published_at'] ?? $document->published_at ?? now()) : null,
- 'expires_at' => $validated['expires_at'] ?? null,
- 'data' => json_decode($validated['data_json'], true, 512, JSON_THROW_ON_ERROR),
- 'updated_by' => $request->user()?->id,
- ]);
-
- return back()->with('status', 'Content document updated.');
- }
-
- public function duplicate(Request $request, ContentDocument $document): RedirectResponse
- {
- $copy = $document->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']);
- $copy->title = $document->title.' copy';
- $copy->slug = $this->uniqueSlug($copy->title);
- $copy->status = 'draft';
- $copy->published_at = null;
- $copy->created_by = $request->user()?->id;
- $copy->updated_by = $request->user()?->id;
- $copy->save();
-
- return redirect()->route('content.documents.edit', $copy)->with('status', 'Independent draft copy created.');
- }
-
- public function destroy(ContentDocument $document): RedirectResponse
- {
- if ($document->placements()->exists()) {
- return back()->withErrors(['document' => 'Remove this document from every page before deleting it.']);
- }
-
- $document->delete();
-
- return redirect()->route('content.index')->with('status', 'Content document deleted.');
- }
-
- private function validateDocument(Request $request, ?ContentDocument $document = null): array
- {
- return $request->validate([
- 'content_type_id' => ['required', 'integer', Rule::exists('content_types', 'id')->where('is_active', true)],
- 'title' => ['required', 'string', 'max:255'],
- 'slug' => ['nullable', 'string', 'max:255'],
- 'status' => ['required', Rule::in(['draft', 'published'])],
- 'published_at' => ['nullable', 'date'],
- 'expires_at' => ['nullable', 'date', 'after:published_at'],
- 'data_json' => ['required', 'json'],
- ]);
- }
-
- private function uniqueSlug(string $value, ?ContentDocument $except = null): string
- {
- $base = Str::slug($value) ?: 'content-document';
- $slug = $base;
- $suffix = 2;
-
- while (ContentDocument::withTrashed()->where('slug', $slug)->when($except, fn ($query) => $query->whereKeyNot($except->getKey()))->exists()) {
- $slug = $base.'-'.$suffix++;
- }
-
- return $slug;
- }
-}
diff --git a/Content/Http/Controllers/PageCompositionController.php b/Content/Http/Controllers/PageCompositionController.php
deleted file mode 100644
index d3c21ad..0000000
--- a/Content/Http/Controllers/PageCompositionController.php
+++ /dev/null
@@ -1,189 +0,0 @@
-with('document.type')
- ->where('page_key', $pageKey)
- ->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END")
- ->orderBy('sort_order')
- ->get()
- ->groupBy('region');
-
- $documents = ContentDocument::query()
- ->with('type')
- ->withCount('placements')
- ->orderBy('title')
- ->get();
-
- return view('content::admin.composition', compact('pageKey', 'placements', 'documents'));
- }
-
-
- public function editPage(Page $page): View
- {
- $placements = PageContent::query()
- ->with('document.type')
- ->where(fn ($query) => $query->where('page_id', $page->id)->orWhere('page_key', $page->slug))
- ->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END")
- ->orderBy('sort_order')
- ->get();
-
- PageContent::query()
- ->whereNull('page_id')
- ->where('page_key', $page->slug)
- ->update(['page_id' => $page->id]);
-
- $documents = ContentDocument::query()
- ->with('type')
- ->withCount('placements')
- ->orderBy('title')
- ->get();
-
- return view('content::admin.composition', [
- 'pageKey' => $page->slug,
- 'page' => $page,
- 'placements' => $placements->groupBy('region'),
- 'documents' => $documents,
- ]);
- }
-
- public function preview(string $pageKey): View
- {
- abort_unless($pageKey === 'home', 404, 'This phase currently provides the Saasland homepage preview.');
-
- return view('stylist-theme-saasland::home', ['pageKey' => $pageKey]);
- }
-
- public function store(Request $request, string $pageKey): RedirectResponse
- {
- $validated = $request->validate([
- 'content_document_id' => ['required', 'integer', Rule::exists('content_documents', 'id')->whereNull('deleted_at')],
- 'region' => ['required', Rule::in(self::REGIONS)],
- ]);
-
- $document = ContentDocument::query()->with('type')->findOrFail($validated['content_document_id']);
- abort_unless(in_array($validated['region'], $document->type?->allowed_regions ?? [], true), 422, 'This content type is not allowed in the selected region.');
-
- $page = Page::query()->where('slug', $pageKey)->first();
- $nextOrder = ((int) PageContent::query()->where(fn ($query) => $query->where('page_key', $pageKey)->when($page, fn ($q) => $q->orWhere('page_id', $page->id)))->where('region', $validated['region'])->max('sort_order')) + 10;
- PageContent::query()->create([
- 'page_id' => $page?->id,
- 'page_key' => $pageKey,
- 'content_document_id' => $document->id,
- 'region' => $validated['region'],
- 'sort_order' => $nextOrder,
- 'is_enabled' => true,
- ]);
-
- return back()->with('status', 'Document added to the '.$validated['region'].' region.');
- }
-
- public function update(Request $request, PageContent $placement): RedirectResponse
- {
- $validated = $request->validate([
- 'is_enabled' => ['nullable', 'boolean'],
- 'starts_at' => ['nullable', 'date'],
- 'ends_at' => ['nullable', 'date', 'after:starts_at'],
- ]);
-
- $placement->update([
- 'is_enabled' => $request->boolean('is_enabled'),
- 'starts_at' => $validated['starts_at'] ?? null,
- 'ends_at' => $validated['ends_at'] ?? null,
- ]);
-
- return back()->with('status', 'Page placement updated.');
- }
-
- public function destroy(PageContent $placement): RedirectResponse
- {
- $placement->delete();
- $this->normaliseRegion($placement->page_key, $placement->region);
-
- return back()->with('status', 'Document removed from this page. The reusable document was not deleted.');
- }
-
- public function duplicate(Request $request, PageContent $placement): RedirectResponse
- {
- $source = $placement->document()->firstOrFail();
- $copy = DB::transaction(function () use ($request, $placement, $source): ContentDocument {
- $copy = $source->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']);
- $copy->title = $source->title.' copy';
- $copy->slug = $source->slug.'-copy-'.strtolower(str()->random(6));
- $copy->status = 'draft';
- $copy->published_at = null;
- $copy->created_by = $request->user()?->id;
- $copy->updated_by = $request->user()?->id;
- $copy->save();
-
- PageContent::query()->create([
- 'page_id' => $placement->page_id,
- 'page_key' => $placement->page_key,
- 'content_document_id' => $copy->id,
- 'region' => $placement->region,
- 'sort_order' => $placement->sort_order + 5,
- 'is_enabled' => false,
- 'visibility_rules' => $placement->visibility_rules,
- 'presentation' => $placement->presentation,
- ]);
-
- return $copy;
- });
-
- $this->normaliseRegion($placement->page_key, $placement->region);
-
- return redirect()->route('content.documents.edit', $copy)->with('status', 'Page-specific draft copy created and added after the original.');
- }
-
- public function reorder(Request $request, string $pageKey): JsonResponse|RedirectResponse
- {
- $validated = $request->validate([
- 'region' => ['required', Rule::in(self::REGIONS)],
- 'placements' => ['required', 'array'],
- 'placements.*' => ['required', 'integer'],
- ]);
-
- $ids = array_values(array_unique(array_map('intval', $validated['placements'])));
- $owned = PageContent::query()->where('page_key', $pageKey)->where('region', $validated['region'])->whereIn('id', $ids)->pluck('id')->all();
- abort_unless(count($owned) === count($ids), 422, 'One or more placements do not belong to this page region.');
-
- DB::transaction(function () use ($ids): void {
- PageContent::query()->whereIn('id', $ids)->increment('sort_order', 100000);
- foreach ($ids as $index => $id) {
- PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]);
- }
- });
-
- return $request->expectsJson()
- ? response()->json(['message' => 'Order saved.'])
- : back()->with('status', 'Page order updated.');
- }
-
- private function normaliseRegion(?string $pageKey, string $region): void
- {
- $ids = PageContent::query()->where('page_key', $pageKey)->where('region', $region)->orderBy('sort_order')->pluck('id');
- DB::transaction(function () use ($ids): void {
- foreach ($ids as $index => $id) {
- PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]);
- }
- });
- }
-}
diff --git a/Content/Models/ContentDocument.php b/Content/Models/ContentDocument.php
deleted file mode 100644
index edf93be..0000000
--- a/Content/Models/ContentDocument.php
+++ /dev/null
@@ -1,17 +0,0 @@
-'array','published_at'=>'datetime','expires_at'=>'datetime']; }
- public function type(): BelongsTo { return $this->belongsTo(ContentType::class, 'content_type_id'); }
- public function placements(): HasMany { return $this->hasMany(PageContent::class); }
- public function scopePublished($query) { return $query->where('status','published')->where(fn($q) => $q->whereNull('published_at')->orWhere('published_at','<=',now()))->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at','>',now())); }
-}
diff --git a/Content/Models/ContentType.php b/Content/Models/ContentType.php
deleted file mode 100644
index 6b0ffd7..0000000
--- a/Content/Models/ContentType.php
+++ /dev/null
@@ -1,12 +0,0 @@
-'array','allowed_regions'=>'array','is_system'=>'boolean','is_active'=>'boolean']; }
- public function documents(): HasMany { return $this->hasMany(ContentDocument::class); }
-}
diff --git a/Content/Models/PageContent.php b/Content/Models/PageContent.php
deleted file mode 100644
index cb204f3..0000000
--- a/Content/Models/PageContent.php
+++ /dev/null
@@ -1,15 +0,0 @@
-'boolean','visibility_rules'=>'array','presentation'=>'array','starts_at'=>'datetime','ends_at'=>'datetime']; }
- public function document(): BelongsTo { return $this->belongsTo(ContentDocument::class, 'content_document_id'); }
- public function page(): BelongsTo { return $this->belongsTo(Page::class); }
-}
diff --git a/Content/Providers/ContentServiceProvider.php b/Content/Providers/ContentServiceProvider.php
deleted file mode 100644
index 0ba0b27..0000000
--- a/Content/Providers/ContentServiceProvider.php
+++ /dev/null
@@ -1,22 +0,0 @@
-mergeConfigFrom(module_path('Content','Config/config.php'),'content');
- $this->app->singleton(PageRenderer::class);
- }
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Content','Database/Migrations'));
- $this->loadViewsFrom(module_path('Content','Resources/views'),'content');
- $this->loadRoutesFrom(module_path('Content','Routes/web.php'));
- if ($this->app->runningInConsole()) $this->commands([InstallSaaslandContent::class]);
- }
-}
diff --git a/Content/README.md b/Content/README.md
deleted file mode 100644
index b1a800d..0000000
--- a/Content/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Content Composer
-
-The Content module provides reusable typed documents and ordered page regions.
-
-## Phase 2 features
-
-- Header, body and footer composition editor.
-- Drag-and-drop ordering with server-side ownership validation.
-- Add existing published or draft documents to a page.
-- Remove a placement without deleting the reusable document.
-- Create an independent page-specific draft copy.
-- Shared-use warnings and deletion protection.
-- Placement enable/disable and date scheduling.
-- Content document creation, duplication, filtering and publishing controls.
-- Authenticated Saasland homepage preview, including draft labels.
-- Content Composer entry in the administration sidebar.
-
-## Setup
-
-```bash
-php artisan migrate
-php artisan content:install-saasland
-```
-
-Reset the seeded homepage composition when required:
-
-```bash
-php artisan content:install-saasland --force
-```
-
-Administration:
-
-- `/admin/content`
-- `/admin/content/pages/home`
-- `/admin/content/pages/home/preview`
-
-The public theme remains backward-compatible: when no composition exists, the legacy Saasland homepage, header and footer are rendered.
diff --git a/Content/Resources/views/admin/composition.blade.php b/Content/Resources/views/admin/composition.blade.php
deleted file mode 100644
index 32a92ce..0000000
--- a/Content/Resources/views/admin/composition.blade.php
+++ /dev/null
@@ -1,147 +0,0 @@
-
-
-
-
-
-
{{ isset($page) ? $page->title : ucfirst($pageKey).' page' }}
-
Add sections, edit their content and drag them into the order visitors should see.
-
-
- @isset($page)Page settings @endisset
- Create section
- @if(isset($page) && $page->status === 'published')Open live page @endif
-
-
-
- @if(session('status'))
{{ session('status') }}
@endif
- @if($errors->any())
@foreach($errors->all() as $error){{ $error }} @endforeach @endif
-
-
-
- @foreach(['header', 'body', 'footer'] as $region)
-
-
-
{{ ucfirst($region) }} {{ $region === 'body' ? 'Ordered content sections' : 'Page-level '.$region.' document' }}
-
{{ $placements->get($region, collect())->count() }} document{{ $placements->get($region, collect())->count() === 1 ? '' : 's' }}
-
-
- @forelse($placements->get($region, collect()) as $placement)
-
-
-
⋮⋮
-
-
-
{{ $placement->document?->title }}
- {{ $placement->document?->type?->name }}
- @if(($placement->document?->placements()->count() ?? 0) > 1)Shared @endif
- @unless($placement->is_enabled)Disabled @endunless
-
-
Order {{ $placement->sort_order }} · {{ $placement->document?->status === 'published' ? 'Published' : 'Draft — hidden from public rendering' }}
-
-
Edit
-
-
-
- Placement settings
-
-
- @csrf @method('PUT')
-
-
-
- is_enabled)> Enabled
- Save
-
-
-
-
-
@csrfCreate page-specific copy
- @csrf @method('DELETE')Remove from page
-
-
-
- @empty
-
No documents assigned to this region.
- @endforelse
-
-
- @endforeach
-
-
-
-
- Add content section Choose an existing section from the content library. Draft sections remain hidden from visitors.
-
- @csrf
-
- Header Body Footer
-
- Content section
-
- Select a content section
- @foreach($documents as $document)
- {{ $document->title }} — {{ $document->type?->name }}{{ $document->status === 'draft' ? ' (draft)' : '' }}
- @endforeach
-
-
- Add section
-
-
-
-
- Editing shared content
- Editing a shared document changes every placement. “Create page-specific copy” makes a disabled draft copy beside the original so it can be reviewed before switching.
-
-
-
- Live order
- Order is saved automatically after dropping a document. A confirmation appears here.
-
-
-
-
-
-
-@push('scripts')
-
-@endpush
-
diff --git a/Content/Resources/views/admin/form.blade.php b/Content/Resources/views/admin/form.blade.php
deleted file mode 100644
index 4a12768..0000000
--- a/Content/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,636 +0,0 @@
-
-
-
-
-
- Content documents
-
-
- {{ $document->exists ? $document->title : 'Create content document' }}
-
-
- Edit every frontend field below. JSON is retained only as an emergency advanced tool.
-
-
-
- @if ($document->exists)
-
-
- @csrf
- Create independent copy
-
-
-
- @csrf
- @method('DELETE')
- Delete
-
-
- @endif
-
-
- @if (session('status'))
-
- {{ session('status') }}
-
- @endif
-
- @if ($errors->any())
-
-
- @foreach ($errors->all() as $error)
- {{ $error }}
- @endforeach
-
-
- @endif
-
- @if ($document->exists && $document->placements->count() > 1)
-
- Shared document.
- Changes affect all {{ $document->placements->count() }} placements.
-
- @endif
-
-
- @csrf
- @if ($document->exists)
- @method('PUT')
- @endif
-
-
-
- Document
-
-
-
- @foreach ($types as $type)
- id) === $type->id)
- >
- {{ $type->name }}
-
- @endforeach
-
-
-
-
-
-
-
Frontend content
-
- Switches take effect when this document is saved.
-
-
-
-
-
-
-
-
- Advanced JSON (emergency use only)
-
- Normal editing should be performed with the controls above.
-
- {{ old('data_json', json_encode($document->data ?: [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) }}
-
-
-
-
-
-
- Publishing
-
- status ?: 'draft') === 'draft')>Draft
- status) === 'published')>Published
-
-
-
- Save document
-
-
- @if ($document->exists)
-
- Usage
-
-
- @endif
-
-
-
-
-
-
diff --git a/Content/Resources/views/admin/index.blade.php b/Content/Resources/views/admin/index.blade.php
deleted file mode 100644
index c219cb4..0000000
--- a/Content/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
Content documents
-
Create and reuse typed content across page regions.
-
-
- Compose homepage
- Create document
-
-
-
- @if(session('status'))
-
{{ session('status') }}
- @endif
- @error('document')
{{ $message }}
@enderror
-
-
-
-
- All types
- @foreach($types as $type)key)>{{ $type->name }} @endforeach
-
-
- All statuses
- Draft
- Published
-
- Filter Reset
-
-
-
-
- Document Type Status Uses Actions
-
- @forelse($documents as $document)
-
- {{ $document->title }}
{{ $document->slug }}
- {{ $document->type?->name }}
- {{ ucfirst($document->status) }}
- {{ $document->placements_count }}
- Edit
-
- @empty
- No content documents match the current filters.
- @endforelse
-
-
-
- {{ $documents->links() }}
-
-
diff --git a/Content/Resources/views/documents/unsupported.blade.php b/Content/Resources/views/documents/unsupported.blade.php
deleted file mode 100644
index b80545d..0000000
--- a/Content/Resources/views/documents/unsupported.blade.php
+++ /dev/null
@@ -1,3 +0,0 @@
-@if(app()->hasDebugModeEnabled())
-
-@endif
diff --git a/Content/Routes/web.php b/Content/Routes/web.php
deleted file mode 100644
index 910c7f6..0000000
--- a/Content/Routes/web.php
+++ /dev/null
@@ -1,24 +0,0 @@
-prefix('admin/content')->name('content.')->group(function (): void {
- Route::get('/', [ContentDocumentController::class, 'index'])->name('index');
- Route::get('/documents/create', [ContentDocumentController::class, 'create'])->name('documents.create');
- Route::post('/documents', [ContentDocumentController::class, 'store'])->name('documents.store');
- Route::get('/documents/{document}/edit', [ContentDocumentController::class, 'edit'])->name('documents.edit');
- Route::put('/documents/{document}', [ContentDocumentController::class, 'update'])->name('documents.update');
- Route::post('/documents/{document}/duplicate', [ContentDocumentController::class, 'duplicate'])->name('documents.duplicate');
- Route::delete('/documents/{document}', [ContentDocumentController::class, 'destroy'])->name('documents.destroy');
-
- Route::get('/page/{page}/compose', [PageCompositionController::class, 'editPage'])->name('pages.page.edit');
- Route::get('/pages/{pageKey}', [PageCompositionController::class, 'edit'])->name('pages.edit');
- Route::get('/pages/{pageKey}/preview', [PageCompositionController::class, 'preview'])->name('pages.preview');
- Route::post('/pages/{pageKey}/placements', [PageCompositionController::class, 'store'])->name('placements.store');
- Route::put('/placements/{placement}', [PageCompositionController::class, 'update'])->name('placements.update');
- Route::delete('/placements/{placement}', [PageCompositionController::class, 'destroy'])->name('placements.destroy');
- Route::post('/placements/{placement}/duplicate', [PageCompositionController::class, 'duplicate'])->name('placements.duplicate');
- Route::post('/pages/{pageKey}/reorder', [PageCompositionController::class, 'reorder'])->name('placements.reorder');
-});
diff --git a/Content/Services/PageRenderer.php b/Content/Services/PageRenderer.php
deleted file mode 100644
index d02c898..0000000
--- a/Content/Services/PageRenderer.php
+++ /dev/null
@@ -1,83 +0,0 @@
-where('page_key', $pageKey)->where('is_enabled', true)->exists();
- } catch (Throwable) {
- return false;
- }
- }
-
- public function renderRegion(string $pageKey, string $region): string
- {
- if (! $this->hasComposition($pageKey)) {
- return '';
- }
-
- $preview = Route::currentRouteNamed('content.pages.preview');
- $placements = PageContent::query()
- ->with('document.type')
- ->where('page_key', $pageKey)
- ->where('region', $region)
- ->where('is_enabled', true)
- ->where(fn ($query) => $query->whereNull('starts_at')->orWhere('starts_at', '<=', now()))
- ->where(fn ($query) => $query->whereNull('ends_at')->orWhere('ends_at', '>', now()))
- ->orderBy('sort_order')
- ->get();
-
- return $placements->map(function (PageContent $placement) use ($preview): string {
- $document = $placement->document;
- if (! $document || ! $document->type || ! $this->isRenderable($document, $preview)) {
- return '';
- }
-
- $view = 'stylist-theme-saasland::documents.'.$document->type->renderer;
- if (! View::exists($view)) {
- $view = 'content::documents.unsupported';
- }
-
- $rendered = View::make($view, [
- 'document' => $document,
- 'data' => $document->data ?? [],
- 'placement' => $placement,
- 'isCompositionPreview' => $preview,
- ])->render();
-
- if ($preview && $document->status !== 'published') {
- return 'Draft preview
'.$rendered.'
';
- }
-
- return $rendered;
- })->implode("\n");
- }
-
- private function isRenderable(ContentDocument $document, bool $preview): bool
- {
- if ($preview) {
- return true;
- }
-
- if ($document->status !== 'published') {
- return false;
- }
-
- if ($document->published_at?->isFuture()) {
- return false;
- }
-
- return ! $document->expires_at?->isPast();
- }
-}
diff --git a/Content/module.json b/Content/module.json
deleted file mode 100644
index e34671c..0000000
--- a/Content/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Content",
- "alias": "content",
- "version": "1.0.0",
- "description": "Composable typed content documents and page regions.",
- "keywords": ["cms", "content", "regions", "composition"],
- "priority": 22,
- "providers": ["Modules\\Content\\Providers\\ContentServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Pages", "Stylist"]
-}
diff --git a/Content/views/admin/form.blade.php b/Content/views/admin/form.blade.php
deleted file mode 100644
index 47ae90d..0000000
--- a/Content/views/admin/form.blade.php
+++ /dev/null
@@ -1,399 +0,0 @@
-
-
-
-
-
- Content documents
-
-
- {{ $document->exists ? $document->title : 'Create content document' }}
-
-
- Edit every frontend field below. JSON is retained only as an emergency advanced tool.
-
-
-
- @if ($document->exists)
-
-
- @csrf
- Create independent copy
-
-
-
- @csrf
- @method('DELETE')
- Delete
-
-
- @endif
-
-
- @if (session('status'))
-
- {{ session('status') }}
-
- @endif
-
- @if ($errors->any())
-
-
- @foreach ($errors->all() as $error)
- {{ $error }}
- @endforeach
-
-
- @endif
-
- @if ($document->exists && $document->placements->count() > 1)
-
- Shared document.
- Changes affect all {{ $document->placements->count() }} placements.
-
- @endif
-
-
- @csrf
- @if ($document->exists)
- @method('PUT')
- @endif
-
-
-
- Document
-
-
-
- @foreach ($types as $type)
- id) === $type->id)
- >
- {{ $type->name }}
-
- @endforeach
-
-
-
-
-
-
-
Frontend content
-
- Switches take effect when this document is saved.
-
-
-
-
-
-
-
-
- Advanced JSON (emergency use only)
-
- Normal editing should be performed with the controls above.
-
- {{ old('data_json', json_encode($document->data ?: [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) }}
-
-
-
-
-
-
- Publishing
-
- status ?: 'draft') === 'draft')>Draft
- status) === 'published')>Published
-
-
-
- Save document
-
-
- @if ($document->exists)
-
- Usage
-
-
- @endif
-
-
-
-
-
-
diff --git a/Core/Config/config.php b/Core/Config/config.php
deleted file mode 100644
index c00cc12..0000000
--- a/Core/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Core',
- 'enabled' => true,
-];
diff --git a/Core/Contracts/ModuleRegistry.php b/Core/Contracts/ModuleRegistry.php
deleted file mode 100644
index e05ca63..0000000
--- a/Core/Contracts/ModuleRegistry.php
+++ /dev/null
@@ -1,11 +0,0 @@
- */
- public function all(): array;
-
- public function enabled(string $name): bool;
-}
diff --git a/Core/Providers/CoreServiceProvider.php b/Core/Providers/CoreServiceProvider.php
deleted file mode 100644
index cee3091..0000000
--- a/Core/Providers/CoreServiceProvider.php
+++ /dev/null
@@ -1,24 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->app->singleton(ModuleRegistry::class, LaravelModuleRegistry::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Core/Providers/RouteServiceProvider.php b/Core/Providers/RouteServiceProvider.php
deleted file mode 100644
index d400e95..0000000
--- a/Core/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Core', 'Routes/web.php'));
- });
- }
-}
diff --git a/Core/Routes/web.php b/Core/Routes/web.php
deleted file mode 100644
index 4c3fffc..0000000
--- a/Core/Routes/web.php
+++ /dev/null
@@ -1,3 +0,0 @@
- $name,
- 'alias' => (string) ($data['alias'] ?? strtolower($name)),
- 'version' => (string) ($data['version'] ?? '0.0.0'),
- 'enabled' => (bool) ($statuses[$name] ?? false),
- ];
- }
- usort($modules, fn (array $a, array $b): int => $a['name'] <=> $b['name']);
-
- return $modules;
- }
-
- public function enabled(string $name): bool
- {
- foreach ($this->all() as $module) {
- if (strcasecmp($module['name'], $name) === 0) {
- return $module['enabled'];
- }
- }
-
- return false;
- }
-}
diff --git a/Core/Support/ModuleInfo.php b/Core/Support/ModuleInfo.php
deleted file mode 100644
index 5710024..0000000
--- a/Core/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Core');
-});
diff --git a/Core/composer.json b/Core/composer.json
deleted file mode 100644
index ca9d3b5..0000000
--- a/Core/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/core-module",
- "description": "Core foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Core\\": ""
- }
- }
-}
diff --git a/Core/module.json b/Core/module.json
deleted file mode 100644
index 0affe62..0000000
--- a/Core/module.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "Core",
- "alias": "core",
- "version": "1.0.0",
- "description": "Core foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "core"
- ],
- "priority": 100,
- "providers": [
- "Modules\\Core\\Providers\\CoreServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": []
-}
diff --git a/Developer/Config/config.php b/Developer/Config/config.php
deleted file mode 100644
index f3bc1fc..0000000
--- a/Developer/Config/config.php
+++ /dev/null
@@ -1,8 +0,0 @@
- 'Modules',
- 'modules_path' => base_path('Modules'),
- 'themes_path' => resource_path('themes'),
- 'overwrite' => false,
-];
diff --git a/Developer/Console/Commands/InspectModulesCommand.php b/Developer/Console/Commands/InspectModulesCommand.php
deleted file mode 100644
index 11f3078..0000000
--- a/Developer/Console/Commands/InspectModulesCommand.php
+++ /dev/null
@@ -1,44 +0,0 @@
-argument('name');
- $rows = [];
- foreach ($files as $file) {
- $manifest = json_decode((string) file_get_contents($file), true);
- if (! is_array($manifest)) {
- $this->error("Invalid JSON: {$file}");
-
- return self::FAILURE;
- }
- if ($name && strcasecmp((string) ($manifest['name'] ?? ''), (string) $name) !== 0) {
- continue;
- }
- $rows[] = [
- 'name' => $manifest['name'] ?? basename(dirname($file)),
- 'version' => $manifest['version'] ?? 'unknown',
- 'requires' => implode(', ', $manifest['requires'] ?? []),
- 'providers' => count($manifest['providers'] ?? []),
- ];
- }
- if ($this->option('json')) {
- $this->line(json_encode($rows, JSON_PRETTY_PRINT));
-
- return self::SUCCESS;
- }
- $this->table(['Module', 'Version', 'Requires', 'Providers'], $rows);
-
- return self::SUCCESS;
- }
-}
diff --git a/Developer/Console/Commands/MakeCrudCommand.php b/Developer/Console/Commands/MakeCrudCommand.php
deleted file mode 100644
index d615a48..0000000
--- a/Developer/Console/Commands/MakeCrudCommand.php
+++ /dev/null
@@ -1,37 +0,0 @@
-option('module') ?: $this->choice('Select the target module', $this->modules());
- try {
- $files = $generator->generate((string) $module, $this->argument('model'), (bool) $this->option('force'));
- } catch (Throwable $e) {
- $this->error($e->getMessage());
-
- return self::FAILURE;
- }
- foreach ($files as $file) {
- $this->line("created {$file}");
- }
- $this->warn('Register the generated controller routes in the target module Routes/web.php file.');
-
- return self::SUCCESS;
- }
-
- private function modules(): array
- {
- return collect(glob(base_path('Modules/*/module.json')) ?: [])->map(fn (string $file) => basename(dirname($file)))->sort()->values()->all();
- }
-}
diff --git a/Developer/Console/Commands/MakeModuleCommand.php b/Developer/Console/Commands/MakeModuleCommand.php
deleted file mode 100644
index 34ece37..0000000
--- a/Developer/Console/Commands/MakeModuleCommand.php
+++ /dev/null
@@ -1,32 +0,0 @@
-generate($this->argument('name'), (bool) $this->option('force'));
- } catch (Throwable $e) {
- $this->error($e->getMessage());
-
- return self::FAILURE;
- }
- foreach ($files as $file) {
- $this->line("created {$file}");
- }
- $this->newLine();
- $this->info('Enable the module in modules_statuses.json, then run composer dump-autoload.');
-
- return self::SUCCESS;
- }
-}
diff --git a/Developer/Console/Commands/MakeThemeCommand.php b/Developer/Console/Commands/MakeThemeCommand.php
deleted file mode 100644
index 2457559..0000000
--- a/Developer/Console/Commands/MakeThemeCommand.php
+++ /dev/null
@@ -1,30 +0,0 @@
-generate($this->argument('name'), (bool) $this->option('force'));
- } catch (Throwable $e) {
- $this->error($e->getMessage());
-
- return self::FAILURE;
- }
- foreach ($files as $file) {
- $this->line("created {$file}");
- }
-
- return self::SUCCESS;
- }
-}
diff --git a/Developer/Http/Controllers/DeveloperController.php b/Developer/Http/Controllers/DeveloperController.php
deleted file mode 100644
index d921e99..0000000
--- a/Developer/Http/Controllers/DeveloperController.php
+++ /dev/null
@@ -1,27 +0,0 @@
-map(function (string $file): array {
- $manifest = json_decode((string) file_get_contents($file), true) ?: [];
-
- return [
- 'name' => $manifest['name'] ?? basename(dirname($file)),
- 'version' => $manifest['version'] ?? 'unknown',
- 'description' => $manifest['description'] ?? '',
- 'requires' => $manifest['requires'] ?? [],
- ];
- })
- ->sortBy('name')
- ->values();
-
- return view('developer::admin.index', compact('modules'));
- }
-}
diff --git a/Developer/Providers/DeveloperServiceProvider.php b/Developer/Providers/DeveloperServiceProvider.php
deleted file mode 100644
index a5f09a9..0000000
--- a/Developer/Providers/DeveloperServiceProvider.php
+++ /dev/null
@@ -1,32 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Developer', 'Config/config.php'), 'developer');
- }
-
- public function boot(): void
- {
- $this->loadViewsFrom(module_path('Developer', 'Resources/views'), 'developer');
-
- if ($this->app->runningInConsole()) {
- $this->commands([
- MakeModuleCommand::class,
- MakeCrudCommand::class,
- MakeThemeCommand::class,
- InspectModulesCommand::class,
- ]);
- }
- }
-}
diff --git a/Developer/Providers/RouteServiceProvider.php b/Developer/Providers/RouteServiceProvider.php
deleted file mode 100644
index 50cbdde..0000000
--- a/Developer/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,14 +0,0 @@
-group(module_path('Developer', 'Routes/web.php'));
- }
-}
diff --git a/Developer/Resources/views/admin/index.blade.php b/Developer/Resources/views/admin/index.blade.php
deleted file mode 100644
index ca9fe9f..0000000
--- a/Developer/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
{{ __('Developer Tools') }}
-
{{ __('Generate modules, CRUD resources and themes using the platform conventions.') }}
-
-
-
-
{{ __('Module Generator') }} php artisan module:make Products
-
{{ __('CRUD Generator') }} php artisan crud:make Product --module=Products
-
{{ __('Theme Generator') }} php artisan theme:make Corporate
-
-
-
-
{{ __('Installed modules') }}
-
- @foreach ($modules as $module)
-
-
{{ $module['name'] }}
{{ $module['description'] }}
-
v{{ $module['version'] }}
{{ implode(', ', $module['requires']) }}
-
- @endforeach
-
-
-
-
diff --git a/Developer/Routes/web.php b/Developer/Routes/web.php
deleted file mode 100644
index 5a8712a..0000000
--- a/Developer/Routes/web.php
+++ /dev/null
@@ -1,11 +0,0 @@
-prefix('administration/developer')
- ->name('administration.developer.')
- ->group(function (): void {
- Route::get('/', DeveloperController::class)->name('index');
- });
diff --git a/Developer/Support/CrudGenerator.php b/Developer/Support/CrudGenerator.php
deleted file mode 100644
index 90db77c..0000000
--- a/Developer/Support/CrudGenerator.php
+++ /dev/null
@@ -1,69 +0,0 @@
-studly($module);
- $model = $this->studly($model);
- $table = str($model)->plural()->snake()->toString();
- $route = str($model)->plural()->kebab()->toString();
- $root = rtrim(config('developer.modules_path', base_path('Modules')), '/').'/'.$module;
- $timestamp = now()->format('Y_m_d_His');
-
- $files = [
- "$root/Models/{$model}.php" => $this->model($module, $model),
- "$root/Http/Controllers/{$model}Controller.php" => $this->controller($module, $model),
- "$root/Http/Requests/Store{$model}Request.php" => $this->request($module, $model),
- "$root/Database/Migrations/{$timestamp}_create_{$table}_table.php" => $this->migration($table),
- "$root/Resources/views/admin/{$route}/index.blade.php" => $this->indexView($model, $route),
- "$root/Resources/views/admin/{$route}/form.blade.php" => $this->formView($model),
- "$root/Tests/Feature/{$model}CrudTest.php" => $this->crudTest($module, $model),
- ];
-
- foreach ($files as $path => $contents) {
- $this->write($path, $contents, $force);
- }
-
- return array_keys($files);
- }
-
- private function model(string $module, string $model): string
- {
- return " 'boolean'];\n }\n}\n";
- }
-
- private function controller(string $module, string $model): string
- {
- $plural = str($model)->plural()->kebab();
-
- return "snake()."::admin.{$plural}.index', ['records' => {$model}::query()->latest()->paginate()]);\n }\n\n public function store(Store{$model}Request \$request): RedirectResponse\n {\n {$model}::query()->create(\$request->validated());\n return back()->with('status', '{$model} created.');\n }\n}\n";
- }
-
- private function request(string $module, string $model): string
- {
- return "user() !== null; }\n public function rules(): array { return ['name' => ['required', 'string', 'max:255'], 'is_active' => ['sometimes', 'boolean']]; }\n}\n";
- }
-
- private function migration(string $table): string
- {
- return "id(); \$table->string('name'); \$table->boolean('is_active')->default(true); \$table->timestamps(); }); }\n public function down(): void { Schema::dropIfExists('{$table}'); }\n};\n";
- }
-
- private function indexView(string $model, string $route): string
- {
- return "\n {$model} @foreach (\$records as \$record)
{{ \$record->name }}
@endforeach{{ \$records->links() }}
\n \n";
- }
-
- private function formView(string $model): string
- {
- return "@csrf{{ __('Save {$model}') }} \n";
- }
-
- private function crudTest(string $module, string $model): string
- {
- return "create(['name' => 'Example']))->toBeInstanceOf({$model}::class); });\n";
- }
-}
diff --git a/Developer/Support/Generator.php b/Developer/Support/Generator.php
deleted file mode 100644
index a8c8bf8..0000000
--- a/Developer/Support/Generator.php
+++ /dev/null
@@ -1,43 +0,0 @@
-files->isDirectory($path)) {
- $this->files->makeDirectory($path, 0755, true);
- }
- }
-
- protected function write(string $path, string $contents, bool $force = false): void
- {
- if ($this->files->exists($path) && ! $force) {
- throw new RuntimeException("File already exists: {$path}");
- }
-
- $this->createDirectory(dirname($path));
- $this->files->put($path, $contents);
- }
-
- protected function studly(string $value): string
- {
- return str($value)->studly()->toString();
- }
-
- protected function snake(string $value): string
- {
- return str($value)->snake()->toString();
- }
-
- protected function kebab(string $value): string
- {
- return str($value)->kebab()->toString();
- }
-}
diff --git a/Developer/Support/ModuleGenerator.php b/Developer/Support/ModuleGenerator.php
deleted file mode 100644
index a9668e3..0000000
--- a/Developer/Support/ModuleGenerator.php
+++ /dev/null
@@ -1,55 +0,0 @@
-studly($name);
- $alias = $this->kebab($name);
- $root = rtrim(config('developer.modules_path', base_path('Modules')), '/').'/'.$name;
- $provider = "Modules\\{$name}\\Providers\\{$name}ServiceProvider";
-
- $files = [
- "$root/module.json" => json_encode([
- 'name' => $name,
- 'alias' => $alias,
- 'version' => '1.0.0',
- 'description' => "{$name} module.",
- 'keywords' => [$alias],
- 'priority' => 100,
- 'providers' => [$provider],
- 'aliases' => (object) [],
- 'files' => [],
- 'requires' => ['Core', 'Administration'],
- ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL,
- "$root/Config/config.php" => " '{$name}'];\n",
- "$root/Providers/{$name}ServiceProvider.php" => $this->provider($name),
- "$root/Providers/RouteServiceProvider.php" => $this->routeProvider($name),
- "$root/Routes/web.php" => "prefix('administration/{$alias}')->name('administration.{$alias}.')->group(function (): void {\n // Register module routes here.\n});\n",
- "$root/Tests/Feature/{$name}ModuleBootTest.php" => $this->test($name),
- ];
-
- foreach ($files as $path => $contents) {
- $this->write($path, $contents, $force);
- }
-
- return array_keys($files);
- }
-
- private function provider(string $name): string
- {
- return "app->register(RouteServiceProvider::class);\n \$this->mergeConfigFrom(module_path('{$name}', 'Config/config.php'), '".str($name)->snake()."');\n }\n\n public function boot(): void\n {\n \$this->loadMigrationsFrom(module_path('{$name}', 'Database/Migrations'));\n \$this->loadViewsFrom(module_path('{$name}', 'Resources/views'), '".str($name)->snake()."');\n }\n}\n";
- }
-
- private function routeProvider(string $name): string
- {
- return "group(module_path('{$name}', 'Routes/web.php'));\n }\n}\n";
- }
-
- private function test(string $name): string
- {
- return "providerIsLoaded(Modules\\{$name}\\Providers\\{$name}ServiceProvider::class))->toBeTrue();\n});\n";
- }
-}
diff --git a/Developer/Support/ThemeGenerator.php b/Developer/Support/ThemeGenerator.php
deleted file mode 100644
index dd1de18..0000000
--- a/Developer/Support/ThemeGenerator.php
+++ /dev/null
@@ -1,24 +0,0 @@
-kebab($name);
- $root = rtrim(config('developer.themes_path', resource_path('themes')), '/').'/'.$name;
- $files = [
- "$root/theme.json" => json_encode(['name' => str($name)->headline()->toString(), 'slug' => $name, 'version' => '1.0.0', 'parent' => 'common'], JSON_PRETTY_PRINT).PHP_EOL,
- "$root/views/layouts/app.blade.php" => "getLocale()) }}\">{{ \$title ?? config('app.name') }} @vite(['resources/css/app.css', 'resources/js/app.js']){{ \$slot }}\n",
- "$root/views/home.blade.php" => "{{ config('app.name') }} \n",
- "$root/assets/css/theme.css" => ":root { --theme-radius: 0.5rem; }\n",
- "$root/README.md" => '# '.str($name)->headline()." Theme\n\nGenerated by `php artisan theme:make {$name}`.\n",
- ];
- foreach ($files as $path => $contents) {
- $this->write($path, $contents, $force);
- }
-
- return array_keys($files);
- }
-}
diff --git a/Developer/Tests/Feature/DeveloperModuleBootTest.php b/Developer/Tests/Feature/DeveloperModuleBootTest.php
deleted file mode 100644
index 80f181c..0000000
--- a/Developer/Tests/Feature/DeveloperModuleBootTest.php
+++ /dev/null
@@ -1,11 +0,0 @@
-providerIsLoaded(DeveloperServiceProvider::class))->toBeTrue();
-});
-
-it('registers developer commands', function (): void {
- $this->artisan('module:inspect')->assertSuccessful();
-});
diff --git a/Developer/module.json b/Developer/module.json
deleted file mode 100644
index 7df5745..0000000
--- a/Developer/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Developer",
- "alias": "developer",
- "version": "1.0.0",
- "description": "Developer generators, module inspection and scaffolding tools.",
- "keywords": ["developer", "generator", "scaffold"],
- "priority": 80,
- "providers": ["Modules\\Developer\\Providers\\DeveloperServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Documents/Config/config.php b/Documents/Config/config.php
deleted file mode 100644
index f780704..0000000
--- a/Documents/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Documents'];
diff --git a/Documents/Database/Migrations/2026_07_24_000500_create_documents_table.php b/Documents/Database/Migrations/2026_07_24_000500_create_documents_table.php
deleted file mode 100644
index 87aaf16..0000000
--- a/Documents/Database/Migrations/2026_07_24_000500_create_documents_table.php
+++ /dev/null
@@ -1,26 +0,0 @@
-id();
- $table->string('title');
- $table->text('description')->nullable();
- $table->string('file_path')->nullable();
- $table->boolean('is_public')->default(true);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('documents');
- }
-};
diff --git a/Documents/Http/Controllers/DocumentController.php b/Documents/Http/Controllers/DocumentController.php
deleted file mode 100644
index ba1fc4e..0000000
--- a/Documents/Http/Controllers/DocumentController.php
+++ /dev/null
@@ -1,58 +0,0 @@
- Document::query()->latest()->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('documents::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $request->validate(['title' => ['required', 'string', 'max:255'],
- 'description' => ['nullable', 'string'],
- 'file_path' => ['nullable', 'string'],
- 'is_public' => ['sometimes', 'boolean']]);
- $data['is_public'] = $request->boolean('is_public');
- Document::create($data);
-
- return redirect()->route('administration.documents.index')->with('status', 'Document created.');
- }
-
- public function edit(Document $document): View
- {
- return view('documents::admin.form', ['record' => $document]);
- }
-
- public function update(Request $request, Document $document): RedirectResponse
- {
- $data = $request->validate(['title' => ['required', 'string', 'max:255'],
- 'description' => ['nullable', 'string'],
- 'file_path' => ['nullable', 'string'],
- 'is_public' => ['sometimes', 'boolean']]);
- $data['is_public'] = $request->boolean('is_public');
- $document->update($data);
-
- return redirect()->route('administration.documents.index')->with('status', 'Document updated.');
- }
-
- public function destroy(Document $document): RedirectResponse
- {
- $document->delete();
-
- return back()->with('status', 'Document deleted.');
- }
-}
diff --git a/Documents/Models/Document.php b/Documents/Models/Document.php
deleted file mode 100644
index 2d9d2cf..0000000
--- a/Documents/Models/Document.php
+++ /dev/null
@@ -1,23 +0,0 @@
- 'boolean',
- ];
- }
-}
diff --git a/Documents/Providers/DocumentsServiceProvider.php b/Documents/Providers/DocumentsServiceProvider.php
deleted file mode 100644
index 5e19ae9..0000000
--- a/Documents/Providers/DocumentsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Documents', 'Config/config.php'), 'documents');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Documents', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Documents', 'Resources/views'), 'documents');
- }
-}
diff --git a/Documents/Providers/RouteServiceProvider.php b/Documents/Providers/RouteServiceProvider.php
deleted file mode 100644
index 7c25a1d..0000000
--- a/Documents/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Documents', 'Routes/web.php')));
- }
-}
diff --git a/Documents/Resources/views/admin/form.blade.php b/Documents/Resources/views/admin/form.blade.php
deleted file mode 100644
index d94c9d1..0000000
--- a/Documents/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
{{ isset($record) ? 'Edit' : 'Create' }} Document @csrf @isset($record) @method('PUT') @endisset
-Title @error('title')
{{ $message }}
@enderror
-Description {{ old('description', $record->description ?? '') }} @error('description')
{{ $message }}
@enderror
-File Path @error('file_path')
{{ $message }}
@enderror
- is_public ?? true))>Is Public
-
-
diff --git a/Documents/Resources/views/admin/index.blade.php b/Documents/Resources/views/admin/index.blade.php
deleted file mode 100644
index f79986e..0000000
--- a/Documents/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-
-Documents Manage documents content.
Add new
-@if(session('status'))
{{ session('status') }}
@endif
-
Title Description File Path Actions @forelse($records as $record){{ is_array($record->title) ? json_encode($record->title) : Str::limit((string) $record->title, 70) }} {{ is_array($record->description) ? json_encode($record->description) : Str::limit((string) $record->description, 70) }} {{ is_array($record->file_path) ? json_encode($record->file_path) : Str::limit((string) $record->file_path, 70) }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
-
diff --git a/Documents/Routes/web.php b/Documents/Routes/web.php
deleted file mode 100644
index 675d903..0000000
--- a/Documents/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/documents')->name('administration.documents.')->group(function (): void {
- Route::get('/', [DocumentController::class, 'index'])->name('index');
- Route::get('/create', [DocumentController::class, 'create'])->name('create');
- Route::post('/', [DocumentController::class, 'store'])->name('store');
- Route::get('/{document}/edit', [DocumentController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{document}', [DocumentController::class, 'update'])->name('update');
- Route::delete('/{document}', [DocumentController::class, 'destroy'])->name('destroy');
-});
diff --git a/Documents/Support/ModuleInfo.php b/Documents/Support/ModuleInfo.php
deleted file mode 100644
index c815ec2..0000000
--- a/Documents/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBeTrue();
-});
diff --git a/Documents/module.json b/Documents/module.json
deleted file mode 100644
index 318fef2..0000000
--- a/Documents/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Documents",
- "alias": "documents",
- "version": "1.0.0",
- "description": "Documents CMS module.",
- "keywords": [
- "cms",
- "documents"
- ],
- "priority": 25,
- "providers": [
- "Modules\\Documents\\Providers\\DocumentsServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Enterprise/Config/config.php b/Enterprise/Config/config.php
deleted file mode 100644
index 2dd4c3a..0000000
--- a/Enterprise/Config/config.php
+++ /dev/null
@@ -1,11 +0,0 @@
- 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),
- ],
-];
diff --git a/Enterprise/Console/BackupCreateCommand.php b/Enterprise/Console/BackupCreateCommand.php
deleted file mode 100644
index 3492f0c..0000000
--- a/Enterprise/Console/BackupCreateCommand.php
+++ /dev/null
@@ -1,21 +0,0 @@
-create();
- $this->info('Backup created: '.$backup->path);
-
- return self::SUCCESS;
- }
-}
diff --git a/Enterprise/Console/BackupPruneCommand.php b/Enterprise/Console/BackupPruneCommand.php
deleted file mode 100644
index bd7fbf4..0000000
--- a/Enterprise/Console/BackupPruneCommand.php
+++ /dev/null
@@ -1,20 +0,0 @@
-info($service->prune().' backup(s) pruned.');
-
- return self::SUCCESS;
- }
-}
diff --git a/Enterprise/Console/HealthCheckCommand.php b/Enterprise/Console/HealthCheckCommand.php
deleted file mode 100644
index 40d1b07..0000000
--- a/Enterprise/Console/HealthCheckCommand.php
+++ /dev/null
@@ -1,28 +0,0 @@
-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;
- }
-}
diff --git a/Enterprise/Database/Migrations/2026_07_24_100000_create_enterprise_health_runs_table.php b/Enterprise/Database/Migrations/2026_07_24_100000_create_enterprise_health_runs_table.php
deleted file mode 100644
index 1413a3f..0000000
--- a/Enterprise/Database/Migrations/2026_07_24_100000_create_enterprise_health_runs_table.php
+++ /dev/null
@@ -1,25 +0,0 @@
-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');
- }
-};
diff --git a/Enterprise/Database/Migrations/2026_07_24_100100_create_enterprise_backups_table.php b/Enterprise/Database/Migrations/2026_07_24_100100_create_enterprise_backups_table.php
deleted file mode 100644
index 4477ca8..0000000
--- a/Enterprise/Database/Migrations/2026_07_24_100100_create_enterprise_backups_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-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');
- }
-};
diff --git a/Enterprise/Http/Controllers/EnterpriseController.php b/Enterprise/Http/Controllers/EnterpriseController.php
deleted file mode 100644
index de0182f..0000000
--- a/Enterprise/Http/Controllers/EnterpriseController.php
+++ /dev/null
@@ -1,82 +0,0 @@
- $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.');
- }
-}
diff --git a/Enterprise/Models/Backup.php b/Enterprise/Models/Backup.php
deleted file mode 100644
index 14ecef9..0000000
--- a/Enterprise/Models/Backup.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'datetime'];
- }
-
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class);
- }
-}
diff --git a/Enterprise/Models/HealthRun.php b/Enterprise/Models/HealthRun.php
deleted file mode 100644
index 34824c2..0000000
--- a/Enterprise/Models/HealthRun.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'array', 'ran_at' => 'datetime'];
- }
-
- public function user(): BelongsTo
- {
- return $this->belongsTo(User::class);
- }
-}
diff --git a/Enterprise/Providers/EnterpriseServiceProvider.php b/Enterprise/Providers/EnterpriseServiceProvider.php
deleted file mode 100644
index a532d0a..0000000
--- a/Enterprise/Providers/EnterpriseServiceProvider.php
+++ /dev/null
@@ -1,30 +0,0 @@
-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]);
- }
- }
-}
diff --git a/Enterprise/Providers/RouteServiceProvider.php b/Enterprise/Providers/RouteServiceProvider.php
deleted file mode 100644
index c1150b7..0000000
--- a/Enterprise/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,14 +0,0 @@
-group(module_path('Enterprise', 'Routes/web.php'));
- }
-}
diff --git a/Enterprise/Resources/views/admin/index.blade.php b/Enterprise/Resources/views/admin/index.blade.php
deleted file mode 100644
index d337ff9..0000000
--- a/Enterprise/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
{{ __('Enterprise Services') }} {{ __('Health, queues, scheduler, cache, backups and production diagnostics.') }}
- @if(session('status'))
{{ session('status') }} @endif
- @if($errors->any())
{{ $errors->first() }} @endif
-
-
-
{{ __('Overall health') }}
{{ $health['status'] }}
-
{{ __('Queue') }}
{{ $queueConnection }}
{{ $failedJobs }} {{ __('failed') }}
-
{{ __('Cache') }}
{{ $cacheStore }}
-
{{ __('Runtime') }}
PHP {{ $phpVersion }}
Laravel {{ $laravelVersion }}
-
-
-
-
{{ __('Health checks') }} @csrf{{ __('Run checks') }}
- @foreach($health['checks'] as $check)
{{ $check['name'] }} {{ $check['status'] }} {{ $check['message'] }}
@endforeach
-
-
-
-
{{ __('Operations') }}
@csrf{{ __('Clear cache') }} @csrf{{ __('Run scheduler') }} @csrf{{ __('Retry failed jobs') }}
-
{{ __('Backups') }} @csrf{{ __('Create backup') }} @forelse($backups as $backup){{ $backup->path }}
{{ number_format($backup->size / 1024 / 1024, 2) }} MB · {{ $backup->created_at->diffForHumans() }}
@csrf @method('DELETE'){{ __('Delete') }} @empty{{ __('No backups created.') }}
@endforelse
-
-
-
diff --git a/Enterprise/Routes/web.php b/Enterprise/Routes/web.php
deleted file mode 100644
index 69e772f..0000000
--- a/Enterprise/Routes/web.php
+++ /dev/null
@@ -1,18 +0,0 @@
-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');
- });
diff --git a/Enterprise/Services/BackupService.php b/Enterprise/Services/BackupService.php
deleted file mode 100644
index 94463da..0000000
--- a/Enterprise/Services/BackupService.php
+++ /dev/null
@@ -1,76 +0,0 @@
-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();
- }
-}
diff --git a/Enterprise/Services/SystemHealthService.php b/Enterprise/Services/SystemHealthService.php
deleted file mode 100644
index b08b319..0000000
--- a/Enterprise/Services/SystemHealthService.php
+++ /dev/null
@@ -1,94 +0,0 @@
-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');
- }
-}
diff --git a/Enterprise/Tests/Feature/EnterpriseModuleBootTest.php b/Enterprise/Tests/Feature/EnterpriseModuleBootTest.php
deleted file mode 100644
index fe1d1d1..0000000
--- a/Enterprise/Tests/Feature/EnterpriseModuleBootTest.php
+++ /dev/null
@@ -1,10 +0,0 @@
- expect(app()->providerIsLoaded(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();
-});
diff --git a/Enterprise/module.json b/Enterprise/module.json
deleted file mode 100644
index fdce53e..0000000
--- a/Enterprise/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"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"]}
diff --git a/Events/Config/config.php b/Events/Config/config.php
deleted file mode 100644
index 6e87093..0000000
--- a/Events/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Events'];
diff --git a/Events/Database/Migrations/2026_07_24_04200_create_events_tables.php b/Events/Database/Migrations/2026_07_24_04200_create_events_tables.php
deleted file mode 100644
index a974f12..0000000
--- a/Events/Database/Migrations/2026_07_24_04200_create_events_tables.php
+++ /dev/null
@@ -1,51 +0,0 @@
-id();
- $t->string('name');
- $t->text('address')->nullable();
- $t->unsignedInteger('capacity')->nullable();
- $t->timestamps();
- });
- Schema::create('events', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('venue_id')->nullable()->constrained('event_venues')->nullOnDelete();
- $t->string('title');
- $t->string('slug')->unique();
- $t->text('summary')->nullable();
- $t->longText('description')->nullable();
- $t->timestamp('starts_at')->index();
- $t->timestamp('ends_at')->nullable();
- $t->string('status')->default('draft')->index();
- $t->unsignedInteger('capacity')->nullable();
- $t->decimal('price', 10, 2)->default(0);
- $t->boolean('registration_open')->default(false);
- $t->timestamps();
- });
- Schema::create('event_registrations', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('event_id')->constrained()->cascadeOnDelete();
- $t->string('name');
- $t->string('email')->index();
- $t->unsignedInteger('quantity')->default(1);
- $t->string('status')->default('confirmed')->index();
- $t->text('notes')->nullable();
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('event_registrations');
- Schema::dropIfExists('events');
- Schema::dropIfExists('event_venues');
- }
-};
diff --git a/Events/Http/Controllers/EventController.php b/Events/Http/Controllers/EventController.php
deleted file mode 100644
index 5668979..0000000
--- a/Events/Http/Controllers/EventController.php
+++ /dev/null
@@ -1,32 +0,0 @@
- Event::with(['venue'])->withCount('registrations')->orderBy('starts_at')->paginate(20), 'venues' => Venue::orderBy('name')->get()]);
- }
-
- public function store(Request $r): RedirectResponse
- {
- Event::create($r->validate(['venue_id' => ['nullable', 'exists:event_venues,id'], 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:events,slug'], 'summary' => ['nullable', 'string'], 'description' => ['nullable', 'string'], 'starts_at' => ['required', 'date'], 'ends_at' => ['nullable', 'date', 'after:starts_at'], 'status' => ['required', 'in:draft,published,cancelled,completed'], 'capacity' => ['nullable', 'integer', 'min:1'], 'price' => ['required', 'numeric', 'min:0']]) + ['registration_open' => $r->boolean('registration_open')]);
-
- return back()->with('status', 'Event created.');
- }
-
- public function destroy(Event $event): RedirectResponse
- {
- $event->delete();
-
- return back()->with('status', 'Event deleted.');
- }
-}
diff --git a/Events/Models/Event.php b/Events/Models/Event.php
deleted file mode 100644
index cb1d6d4..0000000
--- a/Events/Models/Event.php
+++ /dev/null
@@ -1,27 +0,0 @@
- 'datetime', 'ends_at' => 'datetime', 'price' => 'decimal:2', 'registration_open' => 'boolean'];
- }
-
- public function venue(): BelongsTo
- {
- return $this->belongsTo(Venue::class);
- }
-
- public function registrations(): HasMany
- {
- return $this->hasMany(Registration::class);
- }
-}
diff --git a/Events/Models/Registration.php b/Events/Models/Registration.php
deleted file mode 100644
index 1ea8d7f..0000000
--- a/Events/Models/Registration.php
+++ /dev/null
@@ -1,12 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Events', 'Config/config.php'), 'events');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Events', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Events', 'Resources/views'), 'events');
- }
-}
diff --git a/Events/Providers/RouteServiceProvider.php b/Events/Providers/RouteServiceProvider.php
deleted file mode 100644
index ca21554..0000000
--- a/Events/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Events', 'Routes/web.php'));
- }
-}
diff --git a/Events/Resources/views/admin/index.blade.php b/Events/Resources/views/admin/index.blade.php
deleted file mode 100644
index 8cab5c2..0000000
--- a/Events/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Events Manage events, venues and registrations.
@if(session('status'))
{{ session('status') }}
@endif
@csrfCreate event Draft Published Cancelled
Create event Event Starts Status Registrations @forelse($events as $event){{ $event->title }} {{ $event->starts_at->format('j M Y H:i') }} {{ ucfirst($event->status) }} {{ $event->registrations_count }} @csrf @method('DELETE')Delete @emptyNo events yet. @endforelse
{{ $events->links() }}
diff --git a/Events/Routes/web.php b/Events/Routes/web.php
deleted file mode 100644
index 7f27dfa..0000000
--- a/Events/Routes/web.php
+++ /dev/null
@@ -1,10 +0,0 @@
-prefix('administration/events')->name('administration.events.')->group(function (): void {
- Route::get('/', [EventController::class, 'index'])->name('index');
- Route::post('/', [EventController::class, 'store'])->name('store');
- Route::delete('/{event}', [EventController::class, 'destroy'])->name('destroy');
-});
diff --git a/Events/Tests/Feature/ModuleBootTest.php b/Events/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 27cd6b4..0000000
--- a/Events/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Events', config('events.name'));
- }
-}
diff --git a/Events/module.json b/Events/module.json
deleted file mode 100644
index 7990472..0000000
--- a/Events/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Events",
- "alias": "events",
- "version": "1.0.0",
- "description": "Events, venues and registration management.",
- "keywords": ["business", "events"],
- "priority": 42,
- "providers": ["Modules\\Events\\Providers\\EventsServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Faq/Config/config.php b/Faq/Config/config.php
deleted file mode 100644
index 47a7171..0000000
--- a/Faq/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Faq'];
diff --git a/Faq/Database/Migrations/2026_07_24_000700_create_faqs_table.php b/Faq/Database/Migrations/2026_07_24_000700_create_faqs_table.php
deleted file mode 100644
index 504d804..0000000
--- a/Faq/Database/Migrations/2026_07_24_000700_create_faqs_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-id();
- $table->string('question');
- $table->longText('answer');
- $table->string('category')->nullable();
- $table->unsignedInteger('sort_order')->default(0);
- $table->boolean('enabled')->default(true);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('faqs');
- }
-};
diff --git a/Faq/Http/Controllers/FaqController.php b/Faq/Http/Controllers/FaqController.php
deleted file mode 100644
index b38040d..0000000
--- a/Faq/Http/Controllers/FaqController.php
+++ /dev/null
@@ -1,60 +0,0 @@
- Faq::query()->latest()->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('faq::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $request->validate(['question' => ['required', 'string', 'max:255'],
- 'answer' => ['required', 'string'],
- 'category' => ['nullable', 'string'],
- 'sort_order' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- Faq::create($data);
-
- return redirect()->route('administration.faq.index')->with('status', 'Faq created.');
- }
-
- public function edit(Faq $faq): View
- {
- return view('faq::admin.form', ['record' => $faq]);
- }
-
- public function update(Request $request, Faq $faq): RedirectResponse
- {
- $data = $request->validate(['question' => ['required', 'string', 'max:255'],
- 'answer' => ['required', 'string'],
- 'category' => ['nullable', 'string'],
- 'sort_order' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- $faq->update($data);
-
- return redirect()->route('administration.faq.index')->with('status', 'Faq updated.');
- }
-
- public function destroy(Faq $faq): RedirectResponse
- {
- $faq->delete();
-
- return back()->with('status', 'Faq deleted.');
- }
-}
diff --git a/Faq/Models/Faq.php b/Faq/Models/Faq.php
deleted file mode 100644
index b2011f3..0000000
--- a/Faq/Models/Faq.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'integer',
- 'enabled' => 'boolean',
- ];
- }
-}
diff --git a/Faq/Providers/FaqServiceProvider.php b/Faq/Providers/FaqServiceProvider.php
deleted file mode 100644
index 43f9071..0000000
--- a/Faq/Providers/FaqServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Faq', 'Config/config.php'), 'faq');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Faq', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Faq', 'Resources/views'), 'faq');
- }
-}
diff --git a/Faq/Providers/RouteServiceProvider.php b/Faq/Providers/RouteServiceProvider.php
deleted file mode 100644
index 8c5175d..0000000
--- a/Faq/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Faq', 'Routes/web.php')));
- }
-}
diff --git a/Faq/Resources/views/admin/form.blade.php b/Faq/Resources/views/admin/form.blade.php
deleted file mode 100644
index 9998104..0000000
--- a/Faq/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
{{ isset($record) ? 'Edit' : 'Create' }} Faq @csrf @isset($record) @method('PUT') @endisset
-Question @error('question')
{{ $message }}
@enderror
-Answer {{ old('answer', $record->answer ?? '') }} @error('answer')
{{ $message }}
@enderror
-Category @error('category')
{{ $message }}
@enderror
-Sort Order @error('sort_order')
{{ $message }}
@enderror
- enabled ?? true))>Enabled
-
-
diff --git a/Faq/Resources/views/admin/index.blade.php b/Faq/Resources/views/admin/index.blade.php
deleted file mode 100644
index 5bceb8c..0000000
--- a/Faq/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-@if(session('status'))
{{ session('status') }}
@endif
-
Question Answer Category Actions @forelse($records as $record){{ is_array($record->question) ? json_encode($record->question) : Str::limit((string) $record->question, 70) }} {{ is_array($record->answer) ? json_encode($record->answer) : Str::limit((string) $record->answer, 70) }} {{ is_array($record->category) ? json_encode($record->category) : Str::limit((string) $record->category, 70) }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
-
diff --git a/Faq/Routes/web.php b/Faq/Routes/web.php
deleted file mode 100644
index 031dc8b..0000000
--- a/Faq/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/faq')->name('administration.faq.')->group(function (): void {
- Route::get('/', [FaqController::class, 'index'])->name('index');
- Route::get('/create', [FaqController::class, 'create'])->name('create');
- Route::post('/', [FaqController::class, 'store'])->name('store');
- Route::get('/{faq}/edit', [FaqController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{faq}', [FaqController::class, 'update'])->name('update');
- Route::delete('/{faq}', [FaqController::class, 'destroy'])->name('destroy');
-});
diff --git a/Faq/Support/ModuleInfo.php b/Faq/Support/ModuleInfo.php
deleted file mode 100644
index 34b932c..0000000
--- a/Faq/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBeTrue();
-});
diff --git a/Faq/module.json b/Faq/module.json
deleted file mode 100644
index 1996222..0000000
--- a/Faq/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Faq",
- "alias": "faq",
- "version": "1.0.0",
- "description": "Faq CMS module.",
- "keywords": [
- "cms",
- "faq"
- ],
- "priority": 27,
- "providers": [
- "Modules\\Faq\\Providers\\FaqServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Feeds/Config/config.php b/Feeds/Config/config.php
deleted file mode 100644
index 166f718..0000000
--- a/Feeds/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Feeds'];
diff --git a/Feeds/Http/Controllers/FeedController.php b/Feeds/Http/Controllers/FeedController.php
deleted file mode 100644
index 8f2fab8..0000000
--- a/Feeds/Http/Controllers/FeedController.php
+++ /dev/null
@@ -1,21 +0,0 @@
-view('feeds::rss', ['title' => config('app.name').' Blog', 'records' => Blog::published()->latest('published_at')->limit(50)->get(), 'routeName' => 'blog.public.show'])->header('Content-Type', 'application/rss+xml; charset=UTF-8');
- }
-
- public function news(): Response
- {
- return response()->view('feeds::rss', ['title' => config('app.name').' News', 'records' => News::published()->latest('published_at')->limit(50)->get(), 'routeName' => 'news.public.show'])->header('Content-Type', 'application/rss+xml; charset=UTF-8');
- }
-}
diff --git a/Feeds/Providers/FeedsServiceProvider.php b/Feeds/Providers/FeedsServiceProvider.php
deleted file mode 100644
index c1200c7..0000000
--- a/Feeds/Providers/FeedsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Feeds', 'Config/config.php'), 'feeds');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Feeds', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Feeds', 'Resources/views'), 'feeds');
- }
-}
diff --git a/Feeds/Providers/RouteServiceProvider.php b/Feeds/Providers/RouteServiceProvider.php
deleted file mode 100644
index 89c193a..0000000
--- a/Feeds/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Feeds', 'Routes/web.php'));
- }
-}
diff --git a/Feeds/Resources/views/rss.blade.php b/Feeds/Resources/views/rss.blade.php
deleted file mode 100644
index e516351..0000000
--- a/Feeds/Resources/views/rss.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-{!! '' !!}{{ $title }} {{ url('/') }}{{ $title }} @foreach($records as $record){{ $record->title }} {{ route($routeName, $record->slug) }}{{ route($routeName, $record->slug) }} excerpt }}]]> {{ optional($record->published_at)->toRssString() }} @endforeach
diff --git a/Feeds/Routes/web.php b/Feeds/Routes/web.php
deleted file mode 100644
index b9ba81f..0000000
--- a/Feeds/Routes/web.php
+++ /dev/null
@@ -1,7 +0,0 @@
-name('feeds.blog');
-Route::get('/feeds/news.xml', [FeedController::class, 'news'])->name('feeds.news');
diff --git a/Feeds/Tests/Feature/ModuleBootTest.php b/Feeds/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 7b90716..0000000
--- a/Feeds/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Feeds', config('feeds.name'));
- }
-}
diff --git a/Feeds/module.json b/Feeds/module.json
deleted file mode 100644
index 9569047..0000000
--- a/Feeds/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Feeds",
- "alias": "feeds",
- "version": "1.0.0",
- "description": "Feeds publishing module.",
- "keywords": ["publishing", "feeds"],
- "priority": 30,
- "providers": ["Modules\\Feeds\\Providers\\FeedsServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Forms/Config/config.php b/Forms/Config/config.php
deleted file mode 100644
index ba58e72..0000000
--- a/Forms/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Forms'];
diff --git a/Forms/Database/Migrations/2026_07_24_000400_create_forms_table.php b/Forms/Database/Migrations/2026_07_24_000400_create_forms_table.php
deleted file mode 100644
index aee029b..0000000
--- a/Forms/Database/Migrations/2026_07_24_000400_create_forms_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-id();
- $table->string('name');
- $table->string('slug');
- $table->text('description')->nullable();
- $table->json('schema')->nullable();
- $table->boolean('enabled')->default(true);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('forms');
- }
-};
diff --git a/Forms/Http/Controllers/FormController.php b/Forms/Http/Controllers/FormController.php
deleted file mode 100644
index eacb1b8..0000000
--- a/Forms/Http/Controllers/FormController.php
+++ /dev/null
@@ -1,66 +0,0 @@
- Form::query()->latest()->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('forms::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'],
- 'slug' => ['required', 'string', 'max:255'],
- 'description' => ['nullable', 'string'],
- 'schema' => ['nullable', 'array'],
- 'enabled' => ['sometimes', 'boolean']]);
- if ($request->filled('schema_json')) {
- $data['schema'] = json_decode($request->string('schema_json')->toString(), true, 512, JSON_THROW_ON_ERROR);
- }
- $data['enabled'] = $request->boolean('enabled');
- Form::create($data);
-
- return redirect()->route('administration.forms.index')->with('status', 'Form created.');
- }
-
- public function edit(Form $form): View
- {
- return view('forms::admin.form', ['record' => $form]);
- }
-
- public function update(Request $request, Form $form): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'],
- 'slug' => ['required', 'string', 'max:255'],
- 'description' => ['nullable', 'string'],
- 'schema' => ['nullable', 'array'],
- 'enabled' => ['sometimes', 'boolean']]);
- if ($request->filled('schema_json')) {
- $data['schema'] = json_decode($request->string('schema_json')->toString(), true, 512, JSON_THROW_ON_ERROR);
- }
- $data['enabled'] = $request->boolean('enabled');
- $form->update($data);
-
- return redirect()->route('administration.forms.index')->with('status', 'Form updated.');
- }
-
- public function destroy(Form $form): RedirectResponse
- {
- $form->delete();
-
- return back()->with('status', 'Form deleted.');
- }
-}
diff --git a/Forms/Models/Form.php b/Forms/Models/Form.php
deleted file mode 100644
index d72aec6..0000000
--- a/Forms/Models/Form.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'array',
- 'enabled' => 'boolean',
- ];
- }
-}
diff --git a/Forms/Providers/FormsServiceProvider.php b/Forms/Providers/FormsServiceProvider.php
deleted file mode 100644
index 53ce6d1..0000000
--- a/Forms/Providers/FormsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Forms', 'Config/config.php'), 'forms');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Forms', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Forms', 'Resources/views'), 'forms');
- }
-}
diff --git a/Forms/Providers/RouteServiceProvider.php b/Forms/Providers/RouteServiceProvider.php
deleted file mode 100644
index 1d81fce..0000000
--- a/Forms/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Forms', 'Routes/web.php')));
- }
-}
diff --git a/Forms/Resources/views/admin/form.blade.php b/Forms/Resources/views/admin/form.blade.php
deleted file mode 100644
index 12e5a85..0000000
--- a/Forms/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
{{ isset($record) ? 'Edit' : 'Create' }} Form
-
@csrf @isset($record)
- @method('PUT')
- @endisset
- Name @error('name')
-
{{ $message }}
@enderror
- Slug @error('slug')
-
{{ $message }}
@enderror
- Description {{ old('description', $record->description ?? '') }} @error('description')
-
{{ $message }}
@enderror
- Schema (JSON) {{ old('schema_json', isset($record) ? json_encode($record->schema, JSON_PRETTY_PRINT) : '') }}
-
- enabled ?? true))>Enabled
-
-
-
-
diff --git a/Forms/Resources/views/admin/index.blade.php b/Forms/Resources/views/admin/index.blade.php
deleted file mode 100644
index a8dd83d..0000000
--- a/Forms/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
Forms
-
Manage forms content.
-
Add new
-
- @if(session('status'))
-
{{ session('status') }}
- @endif
-
-
-
-
- Name
- Slug
- Description
- Actions
-
-
- @forelse($records as $record)
-
- {{ is_array($record->name) ? json_encode($record->name) : Str::limit((string) $record->name, 70) }}
- {{ is_array($record->slug) ? json_encode($record->slug) : Str::limit((string) $record->slug, 70) }}
- {{ is_array($record->description) ? json_encode($record->description) : Str::limit((string) $record->description, 70) }}
- Edit
- @csrf @method('DELETE')
- Delete
-
-
-
-
- @empty
-
- No records yet.
-
- @endforelse
-
-
{{ $records->links() }}
-
diff --git a/Forms/Routes/web.php b/Forms/Routes/web.php
deleted file mode 100644
index 4da162d..0000000
--- a/Forms/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/forms')->name('administration.forms.')->group(function (): void {
- Route::get('/', [FormController::class, 'index'])->name('index');
- Route::get('/create', [FormController::class, 'create'])->name('create');
- Route::post('/', [FormController::class, 'store'])->name('store');
- Route::get('/{form}/edit', [FormController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{form}', [FormController::class, 'update'])->name('update');
- Route::delete('/{form}', [FormController::class, 'destroy'])->name('destroy');
-});
diff --git a/Forms/Support/ModuleInfo.php b/Forms/Support/ModuleInfo.php
deleted file mode 100644
index 916091c..0000000
--- a/Forms/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBeTrue();
-});
diff --git a/Forms/module.json b/Forms/module.json
deleted file mode 100644
index b4450a2..0000000
--- a/Forms/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Forms",
- "alias": "forms",
- "version": "1.0.0",
- "description": "Forms CMS module.",
- "keywords": [
- "cms",
- "forms"
- ],
- "priority": 24,
- "providers": [
- "Modules\\Forms\\Providers\\FormsServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Licensing/Config/.gitkeep b/Licensing/Config/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Config/config.php b/Licensing/Config/config.php
deleted file mode 100644
index 3b94eda..0000000
--- a/Licensing/Config/config.php
+++ /dev/null
@@ -1,8 +0,0 @@
- 'Licensing',
- 'features' =>
- [
- ],
-];
diff --git a/Licensing/Console/.gitkeep b/Licensing/Console/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Console/SonarDeskLicenseDeliver.php b/Licensing/Console/SonarDeskLicenseDeliver.php
deleted file mode 100644
index 67e5050..0000000
--- a/Licensing/Console/SonarDeskLicenseDeliver.php
+++ /dev/null
@@ -1,20 +0,0 @@
-option('limit'), 100));
- $count = $delivery->deliverPending($limit);
- $this->info("Delivered {$count} licence email(s).");
- return self::SUCCESS;
- }
-}
diff --git a/Licensing/Console/SonarDeskLicenseIssue.php b/Licensing/Console/SonarDeskLicenseIssue.php
deleted file mode 100755
index 12469cb..0000000
--- a/Licensing/Console/SonarDeskLicenseIssue.php
+++ /dev/null
@@ -1,29 +0,0 @@
-argument('product'))->firstOrFail();
- [$l,$key] = $issuer->issue($p, $this->option('name') ?: null, $this->option('email') ?: null);
- if (! $key) {
- $this->warn('Licence already existed; plaintext key is intentionally not recoverable.');
-
- return self::FAILURE;
- } $this->line('Licence ID: '.$l->id);
- $this->line('Licence key: '.$key);
- $this->warn('This is the only time the plaintext key is shown.');
-
- return self::SUCCESS;
- }
-}
diff --git a/Licensing/Console/SonarDeskLicenseRevoke.php b/Licensing/Console/SonarDeskLicenseRevoke.php
deleted file mode 100755
index ad34c16..0000000
--- a/Licensing/Console/SonarDeskLicenseRevoke.php
+++ /dev/null
@@ -1,22 +0,0 @@
-argument('id'));
- $l->update(['revoked_at' => now(), 'revoke_reason' => $this->option('reason')]);
- $this->info('Licence revoked.');
-
- return self::SUCCESS;
- }
-}
diff --git a/Licensing/Console/SonarDeskProductCreate.php b/Licensing/Console/SonarDeskProductCreate.php
deleted file mode 100755
index 8622a85..0000000
--- a/Licensing/Console/SonarDeskProductCreate.php
+++ /dev/null
@@ -1,21 +0,0 @@
- $this->argument('code')], ['name' => $this->option('name'), 'edition' => $this->option('edition'), 'activation_limit' => (int) $this->option('activations'), 'max_major_version' => $this->option('max-major') !== '' ? (int) $this->option('max-major') : null, 'payment_provider' => $this->option('provider') ?: null, 'external_product_id' => $this->option('product-id') ?: null, 'external_price_id' => $this->option('price-id') ?: null, 'active' => true]);
- $this->info("Product {$p->code} ready.");
-
- return self::SUCCESS;
- }
-}
diff --git a/Licensing/Database/Migrations/.gitkeep b/Licensing/Database/Migrations/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Database/Migrations/2026_09_08_200000_create_sonardesk_licensing_tables.php b/Licensing/Database/Migrations/2026_09_08_200000_create_sonardesk_licensing_tables.php
deleted file mode 100755
index babc7a3..0000000
--- a/Licensing/Database/Migrations/2026_09_08_200000_create_sonardesk_licensing_tables.php
+++ /dev/null
@@ -1,92 +0,0 @@
-id();
- $table->string('code')->unique();
- $table->string('name');
- $table->string('edition')->default('Professional');
- $table->unsignedInteger('activation_limit')->default(1);
- $table->unsignedInteger('max_major_version')->nullable();
- $table->string('payment_provider')->nullable();
- $table->string('external_product_id')->nullable();
- $table->string('external_price_id')->nullable();
- $table->boolean('active')->default(true);
- $table->timestamps();
- $table->unique(['payment_provider', 'external_price_id']);
- });
-
- Schema::create('licenses', function (Blueprint $table) {
- $table->uuid('id')->primary();
- $table->foreignId('license_product_id')->constrained('license_products');
- $table->string('license_key_hash', 64)->unique();
- $table->string('license_key_last4', 4);
- $table->string('customer_name')->nullable();
- $table->string('customer_email')->nullable()->index();
- $table->string('edition');
- $table->unsignedInteger('activation_limit');
- $table->unsignedInteger('max_major_version')->nullable();
- $table->timestamp('expires_at')->nullable();
- $table->timestamp('revoked_at')->nullable();
- $table->string('revoke_reason')->nullable();
- $table->string('payment_provider')->nullable();
- $table->string('external_order_id')->nullable();
- $table->string('external_customer_id')->nullable();
- $table->timestamps();
- $table->unique(['payment_provider', 'external_order_id']);
- });
-
- Schema::create('license_delivery_outbox', function (Blueprint $table) {
- $table->id();
- $table->uuid('license_id')->unique();
- $table->text('encrypted_license_key');
- $table->string('recipient_email')->nullable();
- $table->timestamp('delivered_at')->nullable();
- $table->timestamps();
- $table->foreign('license_id')->references('id')->on('licenses')->cascadeOnDelete();
- });
-
- Schema::create('license_activations', function (Blueprint $table) {
- $table->id();
- $table->uuid('license_id');
- $table->uuid('installation_id');
- $table->string('platform', 32)->nullable();
- $table->string('app_version', 32)->nullable();
- $table->timestamp('activated_at');
- $table->timestamp('last_seen_at')->nullable();
- $table->timestamp('deactivated_at')->nullable();
- $table->foreign('license_id')->references('id')->on('licenses')->cascadeOnDelete();
- $table->unique(['license_id', 'installation_id']);
- $table->index(['license_id', 'deactivated_at']);
- });
-
- Schema::create('license_webhook_events', function (Blueprint $table) {
- $table->id();
- $table->string('provider');
- $table->string('external_event_id');
- $table->string('event_type');
- $table->string('payload_sha256', 64);
- $table->json('payload');
- $table->timestamp('processed_at')->nullable();
- $table->text('processing_error')->nullable();
- $table->timestamps();
- $table->unique(['provider', 'external_event_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('license_webhook_events');
- Schema::dropIfExists('license_activations');
- Schema::dropIfExists('license_delivery_outbox');
- Schema::dropIfExists('licenses');
- Schema::dropIfExists('license_products');
- }
-};
diff --git a/Licensing/Database/Migrations/2026_09_08_210000_add_sonardesk_portal_fields.php b/Licensing/Database/Migrations/2026_09_08_210000_add_sonardesk_portal_fields.php
deleted file mode 100755
index b302e12..0000000
--- a/Licensing/Database/Migrations/2026_09_08_210000_add_sonardesk_portal_fields.php
+++ /dev/null
@@ -1,30 +0,0 @@
- $table->boolean('is_admin')->default(false)->index());
- }
-
- if (Schema::hasTable('license_activations') && ! Schema::hasColumn('license_activations', 'installation_name')) {
- Schema::table('license_activations', fn (Blueprint $table) => $table->string('installation_name')->nullable()->after('installation_id'));
- }
- }
-
- public function down(): void
- {
- if (Schema::hasTable('license_activations') && Schema::hasColumn('license_activations', 'installation_name')) {
- Schema::table('license_activations', fn (Blueprint $table) => $table->dropColumn('installation_name'));
- }
-
- if (Schema::hasTable('users') && Schema::hasColumn('users', 'is_admin')) {
- Schema::table('users', fn (Blueprint $table) => $table->dropColumn('is_admin'));
- }
- }
-};
diff --git a/Licensing/Database/Seeders/.gitkeep b/Licensing/Database/Seeders/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Database/Seeders/LicensingDatabaseSeeder.php b/Licensing/Database/Seeders/LicensingDatabaseSeeder.php
deleted file mode 100644
index fbb0b58..0000000
--- a/Licensing/Database/Seeders/LicensingDatabaseSeeder.php
+++ /dev/null
@@ -1,21 +0,0 @@
-call("OthersTableSeeder");
- }
-}
diff --git a/Licensing/Database/factories/.gitkeep b/Licensing/Database/factories/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Helpers/.gitkeep b/Licensing/Helpers/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Helpers/Helper.php b/Licensing/Helpers/Helper.php
deleted file mode 100644
index 7607c60..0000000
--- a/Licensing/Helpers/Helper.php
+++ /dev/null
@@ -1,12 +0,0 @@
- License::query()->count(),
- 'activeCount' => License::query()->whereNull('revoked_at')->where(function ($q): void {
- $q->whereNull('expires_at')->orWhere('expires_at', '>', now());
- })->count(),
- 'productCount' => LicenseProduct::query()->count(),
- 'recentLicenses' => License::query()->with('product')->latest()->limit(10)->get(),
- ]);
- }
-
- public function licenses(): View
- {
- return view('licensing::admin.licenses', [
- 'licenses' => License::query()->with('product')->latest()->paginate(25),
- ]);
- }
-
- public function show(License $license): View
- {
- return view('licensing::admin.show', [
- 'license' => $license->load(['product', 'activations']),
- ]);
- }
-}
diff --git a/Licensing/Http/Controllers/Api/LicenseActivationController.php b/Licensing/Http/Controllers/Api/LicenseActivationController.php
deleted file mode 100755
index 7580871..0000000
--- a/Licensing/Http/Controllers/Api/LicenseActivationController.php
+++ /dev/null
@@ -1,164 +0,0 @@
-validate([
- 'licenseKey' => ['required', 'string', 'max:128'],
- 'installationID' => ['required', 'uuid'],
- 'appVersion' => ['required', 'string', 'max:32'],
- 'platform' => ['required', 'string', 'max:32'],
- ]);
-
- return DB::transaction(function () use ($data, $signer) {
- $license = License::where('license_key_hash', LicenseKey::hash($data['licenseKey']))
- ->lockForUpdate()
- ->first();
-
- if (! $license) {
- throw ValidationException::withMessages(['licenseKey' => 'The licence key is not valid.']);
- }
-
- if (! $license->is_usable) {
- return response()->json(['message' => 'This licence is expired or has been revoked.'], 403);
- }
-
- $installationID = strtolower($data['installationID']);
- $activation = LicenseActivation::where('license_id', $license->id)
- ->where('installation_id', $installationID)
- ->first();
-
- if ($activation && $activation->deactivated_at === null) {
- $activation->update([
- 'last_seen_at' => now(),
- 'platform' => $data['platform'],
- 'app_version' => $data['appVersion'],
- ]);
- } else {
- $activeCount = LicenseActivation::where('license_id', $license->id)
- ->whereNull('deactivated_at')
- ->count();
-
- if ($activeCount >= $license->activation_limit) {
- return response()->json(['message' => 'This licence has reached its activation limit. Deactivate another Mac first.'], 409);
- }
-
- if ($activation) {
- $activation->update([
- 'deactivated_at' => null,
- 'activated_at' => now(),
- 'last_seen_at' => now(),
- 'platform' => $data['platform'],
- 'app_version' => $data['appVersion'],
- ]);
- } else {
- LicenseActivation::create([
- 'license_id' => $license->id,
- 'installation_id' => $installationID,
- 'platform' => $data['platform'],
- 'app_version' => $data['appVersion'],
- 'activated_at' => now(),
- 'last_seen_at' => now(),
- ]);
- }
- }
-
- return response()->json([
- 'licenseToken' => $signer->sign($license, $data['installationID']),
- ]);
- });
- }
-
- public function validateInstallation(
- Request $request,
- LicenseTokenVerifier $verifier,
- LicenseSigner $signer,
- ): JsonResponse {
- $data = $request->validate([
- 'licenseToken' => ['required', 'string', 'max:8192'],
- 'installationID' => ['required', 'uuid'],
- 'appVersion' => ['required', 'string', 'max:32'],
- 'platform' => ['required', 'string', 'max:32'],
- ]);
-
- try {
- $payload = $verifier->verify($data['licenseToken']);
- } catch (\Throwable) {
- return response()->json(['message' => 'The licence token is invalid.'], 400);
- }
-
- if (strcasecmp((string) ($payload['installationID'] ?? ''), $data['installationID']) !== 0) {
- return response()->json(['message' => 'The installation does not match this licence token.'], 400);
- }
-
- return DB::transaction(function () use ($data, $payload, $signer) {
- $license = License::whereKey((string) ($payload['licenseID'] ?? ''))
- ->lockForUpdate()
- ->first();
-
- if (! $license || ! $license->is_usable) {
- return response()->json(['message' => 'This licence is expired or has been revoked.'], 403);
- }
-
- $activation = LicenseActivation::where('license_id', $license->id)
- ->where('installation_id', strtolower($data['installationID']))
- ->first();
-
- if (! $activation || $activation->deactivated_at !== null) {
- return response()->json(['message' => 'This Mac has been deactivated from the licence portal.'], 403);
- }
-
- $activation->update([
- 'last_seen_at' => now(),
- 'platform' => $data['platform'],
- 'app_version' => $data['appVersion'],
- ]);
-
- return response()->json([
- 'active' => true,
- // Refreshing the signed token also applies subscription edition/expiry changes
- // without requiring the user to reactivate manually.
- 'licenseToken' => $signer->sign($license, $data['installationID']),
- ]);
- });
- }
-
- public function deactivate(Request $request, LicenseTokenVerifier $verifier): JsonResponse
- {
- $data = $request->validate([
- 'licenseToken' => ['required', 'string', 'max:8192'],
- 'installationID' => ['required', 'uuid'],
- ]);
-
- try {
- $payload = $verifier->verify($data['licenseToken']);
- } catch (\Throwable) {
- return response()->json(['message' => 'The licence token is invalid.'], 400);
- }
-
- if (strcasecmp((string) ($payload['installationID'] ?? ''), $data['installationID']) !== 0) {
- return response()->json(['message' => 'The installation does not match this licence token.'], 400);
- }
-
- LicenseActivation::where('license_id', (string) ($payload['licenseID'] ?? ''))
- ->where('installation_id', strtolower($data['installationID']))
- ->whereNull('deactivated_at')
- ->update(['deactivated_at' => now(), 'last_seen_at' => now()]);
-
- return response()->json((object) []);
- }
-}
diff --git a/Licensing/Http/Controllers/Api/PaymentWebhookController.php b/Licensing/Http/Controllers/Api/PaymentWebhookController.php
deleted file mode 100755
index 61d9512..0000000
--- a/Licensing/Http/Controllers/Api/PaymentWebhookController.php
+++ /dev/null
@@ -1,191 +0,0 @@
-getContent();
-
- $verifier = match ($provider) {
- 'paddle' => app(PaddleWebhookVerifier::class),
- 'lemon-squeezy' => app(LemonSqueezyWebhookVerifier::class),
- default => null,
- };
-
- if (! $verifier) {
- return response()->json([
- 'message' => 'Unknown webhook provider.',
- ], 404);
- }
-
- try {
- $verifier->verify($request, $raw);
- } catch (Throwable $e) {
- report($e);
-
- return response()->json([
- 'message' => 'Invalid webhook signature.',
- ], 400);
- }
-
- try {
- $payload = json_decode(
- $raw,
- true,
- flags: JSON_THROW_ON_ERROR
- );
- } catch (Throwable) {
- return response()->json([
- 'message' => 'Invalid JSON.',
- ], 400);
- }
-
- $externalID = (string) (
- $provider === 'paddle'
- ? ($payload['event_id'] ?? '')
- : ($payload['meta']['event_name'] ?? '')
- .':'
- .($payload['data']['id'] ?? '')
- );
-
- $eventType = (string) (
- $provider === 'paddle'
- ? ($payload['event_type'] ?? 'unknown')
- : ($payload['meta']['event_name'] ?? 'unknown')
- );
-
- if ($externalID === '') {
- return response()->json([
- 'message' => 'Webhook event has no stable identifier.',
- ], 400);
- }
-
- $payloadHash = hash('sha256', $raw);
-
- try {
- /*
- * Paddle can deliver several related events concurrently.
- *
- * Laravel retries this complete transaction when MySQL reports
- * a deadlock. Five attempts gives short-lived lock contention
- * enough opportunity to resolve without acknowledging an event
- * that was not actually processed.
- */
- DB::transaction(
- function () use (
- $provider,
- $externalID,
- $eventType,
- $payload,
- $payloadHash,
- $provisioner
- ): void {
- /*
- * Insert the event if this is its first delivery.
- *
- * Once present, lock the row so duplicate deliveries of
- * the same event cannot provision concurrently.
- */
- LicenseWebhookEvent::firstOrCreate(
- [
- 'provider' => $provider,
- 'external_event_id' => $externalID,
- ],
- [
- 'event_type' => $eventType,
- 'payload_sha256' => $payloadHash,
- 'payload' => $payload,
- ]
- );
-
- $event = LicenseWebhookEvent::query()
- ->where('provider', $provider)
- ->where('external_event_id', $externalID)
- ->lockForUpdate()
- ->firstOrFail();
-
- if ($event->processed_at) {
- return;
- }
-
- if (! hash_equals(
- (string) $event->payload_sha256,
- $payloadHash
- )) {
- throw new RuntimeException(
- 'Webhook ID was reused with a different payload.'
- );
- }
-
- try {
- $provisioner->provision(
- $provider,
- $payload
- );
-
- $event->processed_at = now();
- $event->processing_error = null;
- $event->save();
- } catch (Throwable $e) {
- /*
- * This assignment is useful when the surrounding
- * transaction eventually succeeds. If processing
- * throws, the transaction itself is rolled back and
- * Paddle will retry the event.
- */
- $event->processing_error = mb_substr(
- $e->getMessage(),
- 0,
- 4000
- );
-
- $event->save();
-
- throw $e;
- }
- },
- 5
- );
- } catch (Throwable $e) {
- report($e);
- return response()->json([
- 'message' => 'Webhook could not be processed.',
- ], 500);
- }
-
- /*
- * Delivery deliberately happens outside the webhook transaction.
- *
- * A temporary SMTP failure must not undo a successfully processed
- * payment event. The outbox retains anything that still needs
- * delivery.
- */
- try {
- $delivery->deliverPending(10);
- } catch (Throwable $e) {
- report($e);
- }
-
- return response()->json([
- 'ok' => true,
- ]);
- }
-}
diff --git a/Licensing/Http/Controllers/LicensingController.php b/Licensing/Http/Controllers/LicensingController.php
deleted file mode 100644
index 8ece907..0000000
--- a/Licensing/Http/Controllers/LicensingController.php
+++ /dev/null
@@ -1,79 +0,0 @@
-whereRaw('lower(customer_email) = ?', [strtolower($request->user()->email)]);
- }
-
- public function index(Request $request)
- {
- $licenses = $this->owned($request)->latest()->get();
- $hasProfessional = $licenses->contains(fn (License $license) => $license->is_usable && $license->edition === 'Professional');
-
- return view('portal.index', compact('licenses', 'hasProfessional'));
- }
-
- public function show(Request $request, License $license)
- {
- $this->authorise($request, $license);
- $license->load(['product', 'activations']);
-
- return view('portal.show', compact('license'));
- }
-
- public function upgrade(Request $request, License $license): RedirectResponse
- {
- $this->authorise($request, $license);
- abort_unless($license->payment_provider === 'paddle' && $license->external_subscription_id, 422);
- abort_unless($license->edition === 'Personal' && $license->is_usable, 422);
-
- $targetPriceID = $this->professionalPriceFor($license);
- $targetProduct = LicenseProduct::query()
- ->where('payment_provider', 'paddle')
- ->where('external_price_id', $targetPriceID)
- ->where('active', true)
- ->firstOrFail();
-
- try {
- $subscription = $this->paddle->updateSubscriptionPrice($license->external_subscription_id, $targetPriceID);
- } catch (RuntimeException $exception) {
- return back()->withErrors(['licence' => $exception->getMessage()]);
- }
-
- // Apply the entitlement immediately. Paddle webhooks remain authoritative and
- // will reconcile subscription status/current-period details afterwards.
- $license->update([
- 'license_product_id' => $targetProduct->id,
- 'edition' => $targetProduct->edition,
- 'activation_limit' => $targetProduct->activation_limit,
- 'max_major_version' => $targetProduct->max_major_version,
- 'external_price_id' => $targetPriceID,
- 'subscription_status' => (string) ($subscription['status'] ?? $license->subscription_status),
- 'revoked_at' => null,
- 'revoke_reason' => null,
- ]);
-
- return redirect()->route('portal.show', $license)->with('status', 'Upgraded to Professional. Your existing Paddle subscription was changed; no second subscription was created.');
- }
-
- public function cancel(Request $request, License $license): RedirectResponse
- {
- $this->authorise($request, $license);
- abort_unless($license->payment_provider === 'paddle' && $license->external_subscription_id, 422);
- abort_unless(! in_array($license->subscription_status, ['canceled'], true), 422);
-
- try {
- $this->paddle->cancelSubscriptionImmediately($license->external_subscription_id);
- } catch (RuntimeException $exception) {
- return back()->withErrors(['licence' => $exception->getMessage()]);
- }
-
- $license->update([
- 'subscription_status' => 'canceled',
- 'expires_at' => now(),
- 'revoked_at' => now(),
- 'revoke_reason' => 'Subscription canceled from licence portal',
- 'cancel_scheduled_at' => null,
- ]);
-
- return redirect()->route('portal.index')->with('status', 'Subscription canceled in Paddle and the licence is no longer active.');
- }
-
- public function deactivate(Request $request, License $license, int $activation)
- {
- $this->authorise($request, $license);
- $item = $license->activations()->whereKey($activation)->firstOrFail();
- if (! $item->deactivated_at) {
- $item->update(['deactivated_at' => now()]);
- }
-
- return back()->with('status', 'Mac deactivated. SonarDesk will detect this through the licensing API.');
- }
-
- public function resend(Request $request, License $license)
- {
- $this->authorise($request, $license);
- $out = LicenseDeliveryOutbox::where('license_id', $license->id)->first();
- if (! $out) {
- return back()->withErrors(['licence' => 'This licence was issued manually and its full key is not recoverable. Contact support.']);
- }
-
- $out->update([
- 'recipient_email' => strtolower($request->user()->email),
- 'delivered_at' => null,
- ]);
- $this->delivery->deliverPending(1);
-
- return back()->with('status', 'Licence key sent to your verified email address.');
- }
-
- private function authorise(Request $request, License $license): void
- {
- abort_unless(strtolower((string) $license->customer_email) === strtolower($request->user()->email), 404);
- }
-
- private function professionalPriceFor(License $license): string
- {
- $paddle = config('paddle');
- $current = (string) $license->external_price_id;
-
- if ($current === (string) ($paddle['personal_annual_price_id'] ?? '')) {
- return (string) ($paddle['professional_annual_price_id'] ?? '');
- }
-
- return (string) ($paddle['professional_monthly_price_id'] ?? '');
- }
-}
diff --git a/Licensing/Http/Middleware/.gitkeep b/Licensing/Http/Middleware/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Http/Requests/.gitkeep b/Licensing/Http/Requests/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Models/.gitkeep b/Licensing/Models/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Models/License.php b/Licensing/Models/License.php
deleted file mode 100755
index 86e5c6a..0000000
--- a/Licensing/Models/License.php
+++ /dev/null
@@ -1,36 +0,0 @@
- 'datetime', 'revoked_at' => 'datetime', 'activation_limit' => 'integer', 'max_major_version' => 'integer', 'current_period_ends_at' => 'datetime', 'cancel_scheduled_at' => 'datetime'];
-
- public function product(): BelongsTo
- {
- return $this->belongsTo(LicenseProduct::class, 'license_product_id');
- }
-
- public function activations(): HasMany
- {
- return $this->hasMany(LicenseActivation::class);
- }
-
- public function getIsUsableAttribute(): bool
- {
- return $this->revoked_at === null && ($this->expires_at === null || $this->expires_at->isFuture());
- }
-}
diff --git a/Licensing/Models/LicenseActivation.php b/Licensing/Models/LicenseActivation.php
deleted file mode 100755
index fb454a0..0000000
--- a/Licensing/Models/LicenseActivation.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'datetime', 'last_seen_at' => 'datetime', 'deactivated_at' => 'datetime'];
-}
diff --git a/Licensing/Models/LicenseDeliveryOutbox.php b/Licensing/Models/LicenseDeliveryOutbox.php
deleted file mode 100755
index 89d1d5f..0000000
--- a/Licensing/Models/LicenseDeliveryOutbox.php
+++ /dev/null
@@ -1,20 +0,0 @@
- 'datetime'];
-
- public function license(): BelongsTo
- {
- return $this->belongsTo(License::class);
- }
-}
diff --git a/Licensing/Models/LicenseProduct.php b/Licensing/Models/LicenseProduct.php
deleted file mode 100755
index eb8a4cc..0000000
--- a/Licensing/Models/LicenseProduct.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'boolean', 'activation_limit' => 'integer', 'max_major_version' => 'integer'];
-}
diff --git a/Licensing/Models/LicenseWebhookEvent.php b/Licensing/Models/LicenseWebhookEvent.php
deleted file mode 100755
index a8cc130..0000000
--- a/Licensing/Models/LicenseWebhookEvent.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'array', 'processed_at' => 'datetime'];
-}
diff --git a/Licensing/Providers/.gitkeep b/Licensing/Providers/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Providers/IntegratedLicensingServiceProvider.php b/Licensing/Providers/IntegratedLicensingServiceProvider.php
deleted file mode 100644
index d55e06b..0000000
--- a/Licensing/Providers/IntegratedLicensingServiceProvider.php
+++ /dev/null
@@ -1,35 +0,0 @@
- [
- Limit::perMinute(12)->by($request->ip()),
- Limit::perMinute(6)->by(hash('sha256', (string) $request->input('installationID'))),
- ]);
-
- $this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
- $this->loadViewsFrom(__DIR__.'/../Resources/views', 'licensing');
- $this->mergeConfigFrom(__DIR__.'/../Config/config.php', 'licensing');
-
- // Explicit role-based authorization; deny access by default.
- Gate::define('manage-sonardesk-licensing', static fn (User $user): bool =>
- $user->hasAnyRole(['Super Admin', 'super-admin', 'Administrator'])
- );
-
- Route::prefix('api')->middleware('api')->group(__DIR__.'/../Routes/api.php');
- Route::group([], __DIR__.'/../Routes/admin.php');
- }
-}
diff --git a/Licensing/Providers/LicensingServiceProvider.php b/Licensing/Providers/LicensingServiceProvider.php
deleted file mode 100644
index c514a59..0000000
--- a/Licensing/Providers/LicensingServiceProvider.php
+++ /dev/null
@@ -1,171 +0,0 @@
- Limit::perMinute(5)
- ->by(strtolower((string) $request->input('email')).'|'.$request->ip()));
-
- RateLimiter::for('registration', fn (Request $request) =>
- Limit::perHour(5)->by($request->ip())
- );
-
- RateLimiter::for('license-activation', function (Request $request): array {
- return [
- Limit::perMinute(12)->by($request->ip()),
- Limit::perMinute(6)->by(hash('sha256', (string) $request->input('installationID'))),
- ];
- });
- $this->registerTranslations();
- $this->registerConfig();
- $this->registerViews();
- $this->registerHelpers();
- $this->registerCommands();
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- }
-
- /**
- * Register the service provider.
- *
- * @return void
- */
- public function register(): void
- {
- $this->app->register(RouteServiceProvider::class);
- }
-
- /**
- * Register the helpers.
- *
- * @return void
- */
- protected function registerHelpers(): void
- {
- foreach (glob(__DIR__.'/../Helpers/*.php') as $filename) {
- require_once $filename;
- }
- }
-
- /**
- * Dynamically Register Commands.
- *
- * @return void
- */
- public function registerCommands(): void
- {
- if (!$this->app->runningInConsole()) {
- return;
- }
- $commandPath = __DIR__ . '/../Console';
- $commands = collect(File::allFiles($commandPath))
- ->map(function ($file) {
- $class = 'Modules\\' . $this->moduleName . '\\Console\\' . $file->getFilenameWithoutExtension();
- return $class;
- })
- ->filter(function ($class) {
- return class_exists($class);
- })
- ->values()
- ->all();
- $this->commands($commands);
- }
-
- /**
- * Register config.
- *
- * @return void
- */
- protected function registerConfig(): void
- {
- $this->publishes([
- module_path(ModuleInfo::name(), 'Config/config.php') => config_path( ModuleInfo::nameLower(). '.php'),
- ], 'config');
- $this->mergeConfigFrom(
- module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower()
- );
- }
-
- /**
- * Register views.
- *
- * @return void
- */
- public function registerViews(): void
- {
- $viewPath = resource_path('views/modules/' . ModuleInfo::nameLower());
- $sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
- $this->publishes([
- $sourcePath => $viewPath
- ], ['views', ModuleInfo::nameLower() . '-module-views']);
- $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), ModuleInfo::nameLower());
- }
-
- /**
- * Register translations.
- *
- * @return void
- */
- public function registerTranslations(): void
- {
- $langPath = resource_path('lang/modules/' . ModuleInfo::nameLower());
-
- if (is_dir($langPath)) {
- $this->loadTranslationsFrom($langPath, ModuleInfo::nameLower());
- } else {
- $this->loadTranslationsFrom(module_path(ModuleInfo::name(), 'Resources/lang'), ModuleInfo::nameLower());
- }
- }
-
- /**
- * Regsiter View Paths
- * @return array
- */
- private function getPublishableViewPaths(): array
- {
- $paths = [];
- foreach (Config::get('view.paths') as $path) {
- if (is_dir($path . '/modules/' . ModuleInfo::nameLower())) {
- $paths[] = $path . '/modules/' . ModuleInfo::nameLower();
- }
- }
- return $paths;
- }
-}
diff --git a/Licensing/Providers/RouteServiceProvider.php b/Licensing/Providers/RouteServiceProvider.php
deleted file mode 100644
index c691ba8..0000000
--- a/Licensing/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,69 +0,0 @@
-mapApiRoutes();
-
- $this->mapWebRoutes();
- }
-
- /**
- * Define the "web" routes for the application.
- *
- * These routes all receive session state, CSRF protection, etc.
- *
- * @return void
- */
- protected function mapWebRoutes()
- {
- Route::middleware('web')
- ->namespace($this->moduleNamespace)
- ->group(module_path('Licensing', '/Routes/web.php'));
- }
-
- /**
- * Define the "api" routes for the application.
- *
- * These routes are typically stateless.
- *
- * @return void
- */
- protected function mapApiRoutes()
- {
- Route::prefix('api')
- ->middleware('api')
- ->namespace($this->moduleNamespace)
- ->group(module_path('Licensing', '/Routes/api.php'));
- }
-}
diff --git a/Licensing/Resources/assets/.gitkeep b/Licensing/Resources/assets/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Resources/assets/js/app.js b/Licensing/Resources/assets/js/app.js
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Resources/assets/sass/app.scss b/Licensing/Resources/assets/sass/app.scss
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Resources/lang/.gitkeep b/Licensing/Resources/lang/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Resources/views/.gitkeep b/Licensing/Resources/views/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Resources/views/admin/index.blade.php b/Licensing/Resources/views/admin/index.blade.php
deleted file mode 100644
index 8c72c09..0000000
--- a/Licensing/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,3 +0,0 @@
-@extends('licensing::admin.layout')
-@section('title', 'Licensing Overview')
-@section('content')SonarDesk licensing Total licences
{{ number_format($licenseCount) }}
Usable licences
{{ number_format($activeCount) }}
Products
{{ number_format($productCount) }}
Recent licences Customer Product Edition Status Created @forelse($recentLicenses as $license){{ $license->customer_email ?: 'Unknown customer' }} {{ $license->product?->name ?? '—' }} {{ $license->edition }} {{ $license->is_usable ? 'Usable' : 'Inactive' }} {{ $license->created_at?->format('d M Y') }} @emptyNo licences found. @endforelse
View all licences →
@endsection
diff --git a/Licensing/Resources/views/admin/layout.blade.php b/Licensing/Resources/views/admin/layout.blade.php
deleted file mode 100644
index 4bd80e4..0000000
--- a/Licensing/Resources/views/admin/layout.blade.php
+++ /dev/null
@@ -1,3 +0,0 @@
-@yield('title', 'SonarDesk Licensing') · ABSDev @yield('content')
diff --git a/Licensing/Resources/views/admin/licenses.blade.php b/Licensing/Resources/views/admin/licenses.blade.php
deleted file mode 100644
index 9f3f47d..0000000
--- a/Licensing/Resources/views/admin/licenses.blade.php
+++ /dev/null
@@ -1,3 +0,0 @@
-@extends('licensing::admin.layout')
-@section('title', 'Licences')
-@section('content')Licences Customer Product Edition Provider Status Created @forelse($licenses as $license){{ $license->customer_email ?: 'Unknown customer' }} {{ $license->product?->name ?? '—' }} {{ $license->edition }} {{ $license->payment_provider ?? '—' }} {{ $license->is_usable ? 'Usable' : 'Inactive' }} {{ $license->created_at?->format('d M Y') }} @emptyNo licences found. @endforelse
@endsection
diff --git a/Licensing/Resources/views/admin/show.blade.php b/Licensing/Resources/views/admin/show.blade.php
deleted file mode 100644
index 42f12b3..0000000
--- a/Licensing/Resources/views/admin/show.blade.php
+++ /dev/null
@@ -1,3 +0,0 @@
-@extends('licensing::admin.layout')
-@section('title', 'Licence details')
-@section('content')Licence details Customer and licence Customer {{ $license->customer_name ?? '—' }} Email {{ $license->customer_email ?? '—' }} Product {{ $license->product?->name ?? '—' }} Edition {{ $license->edition }} Key (last four only) •••• {{ $license->license_key_last4 }} Status {{ $license->is_usable ? 'Usable' : 'Inactive' }} Subscription Provider {{ $license->payment_provider ?? '—' }} Subscription status {{ $license->subscription_status ?? '—' }} Period ends {{ $license->current_period_ends_at?->format('d M Y') ?? '—' }} Expires {{ $license->expires_at?->format('d M Y') ?? '—' }} Activation limit {{ $license->activation_limit }} Device activations Platform Version Activated Last seen Status @forelse($license->activations as $activation){{ $activation->platform ?? '—' }} {{ $activation->app_version ?? '—' }} {{ $activation->activated_at?->format('d M Y H:i') ?? '—' }} {{ $activation->last_seen_at?->format('d M Y H:i') ?? '—' }} {{ $activation->deactivated_at ? 'Deactivated' : 'Active' }} @emptyNo device activations. @endforelse
@endsection
diff --git a/Licensing/Resources/views/index.blade.php b/Licensing/Resources/views/index.blade.php
deleted file mode 100644
index 4277b06..0000000
--- a/Licensing/Resources/views/index.blade.php
+++ /dev/null
@@ -1,9 +0,0 @@
-@extends('licensing::layouts.master')
-
-@section('content')
- Hello World
-
-
- This view is loaded from module: {!! config('licensing.name') !!}
-
-@endsection
diff --git a/Licensing/Resources/views/layouts/master.blade.php b/Licensing/Resources/views/layouts/master.blade.php
deleted file mode 100644
index 1e51f1c..0000000
--- a/Licensing/Resources/views/layouts/master.blade.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
- Module Licensing
-
- {{-- Laravel Mix - CSS File --}}
- {{-- --}}
-
-
-
- @yield('content')
-
- {{-- Laravel Mix - JS File --}}
- {{-- --}}
-
-
diff --git a/Licensing/Routes/.gitkeep b/Licensing/Routes/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Routes/admin.php b/Licensing/Routes/admin.php
deleted file mode 100644
index 592524a..0000000
--- a/Licensing/Routes/admin.php
+++ /dev/null
@@ -1,14 +0,0 @@
-prefix('administration/sonardesk')
- ->name('administration.sonardesk.')
- ->group(function (): void {
- Route::get('/', [LicensingDashboardController::class, 'index'])->name('index');
- Route::get('/licenses', [LicensingDashboardController::class, 'licenses'])->name('licenses');
- Route::get('/licenses/{license}', [LicensingDashboardController::class, 'show'])->name('licenses.show');
- });
diff --git a/Licensing/Routes/api.php b/Licensing/Routes/api.php
deleted file mode 100644
index a923714..0000000
--- a/Licensing/Routes/api.php
+++ /dev/null
@@ -1,12 +0,0 @@
-group(function (): void {
- Route::post('/activate', [LicenseActivationController::class, 'activate'])->middleware('throttle:license-activation');
- Route::post('/validate', [LicenseActivationController::class, 'validateInstallation'])->middleware('throttle:license-activation');
- Route::post('/deactivate', [LicenseActivationController::class, 'deactivate'])->middleware('throttle:license-activation');
- Route::post('/webhooks/{provider}', PaymentWebhookController::class)->whereIn('provider', ['paddle', 'lemon-squeezy']);
-});
diff --git a/Licensing/Routes/web.php b/Licensing/Routes/web.php
deleted file mode 100644
index cfe4d88..0000000
--- a/Licensing/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('portal')->name('portal.')->group(function (): void {
- Route::get('/', [PortalController::class, 'index'])->name('index');
- Route::get('/licences/{license}', [PortalController::class, 'show'])->name('show');
- Route::post('/licences/{license}/upgrade', [PortalController::class, 'upgrade'])->middleware('throttle:5,1')->name('upgrade');
- Route::delete('/licences/{license}/subscription', [PortalController::class, 'cancel'])->middleware('throttle:5,1')->name('cancel');
- Route::post('/licences/{license}/resend', [PortalController::class, 'resend'])->middleware('throttle:3,10')->name('resend');
- Route::delete('/licences/{license}/activations/{activation}', [PortalController::class, 'deactivate'])->name('deactivate');
-});
diff --git a/Licensing/Services/LicenseDeliveryService.php b/Licensing/Services/LicenseDeliveryService.php
deleted file mode 100755
index 053600d..0000000
--- a/Licensing/Services/LicenseDeliveryService.php
+++ /dev/null
@@ -1,54 +0,0 @@
-with('license')
- ->whereNull('delivered_at')
- ->whereNotNull('recipient_email')
- ->where('recipient_email', '!=', '')
- ->oldest()
- ->limit(max(1, min($limit, 100)))
- ->get();
-
- foreach ($pending as $outbox) {
- $license = $outbox->license;
- if (! $license) {
- continue;
- }
-
- $plain = Crypt::decryptString($outbox->encrypted_license_key);
- $recipient = strtolower(trim((string) $outbox->recipient_email));
- if ($recipient === '') {
- continue;
- }
-
- $data = [
- 'customerName' => $license->customer_name,
- 'edition' => $license->edition,
- 'licenseKey' => $plain,
- 'expiresAt' => $license->expires_at,
- 'portalUrl' => config('sonardesk.portal_url') ?: url('/portal'),
- 'downloadUrl' => route('release'),
- ];
-
- Mail::send(['html' => 'emails.license-key', 'text' => 'emails.license-key-text'], $data, function ($message) use ($recipient): void {
- $message->to($recipient)->subject('Your SonarDesk licence key');
- });
-
- $outbox->update(['delivered_at' => now()]);
- $delivered++;
- }
-
- return $delivered;
- }
-}
diff --git a/Licensing/Services/LicenseIssuer.php b/Licensing/Services/LicenseIssuer.php
deleted file mode 100755
index 7171dd8..0000000
--- a/Licensing/Services/LicenseIssuer.php
+++ /dev/null
@@ -1,46 +0,0 @@
-where('external_order_id', $orderID)->first();
- if ($existing) {
- return [$existing, null];
- }
- }
- $plain = LicenseKey::generate();
- $license = License::create([
- 'license_product_id' => $product->id,
- 'license_key_hash' => LicenseKey::hash($plain),
- 'license_key_last4' => substr(LicenseKey::normalize($plain), -4),
- 'customer_name' => $name,
- 'customer_email' => $email ? strtolower(trim($email)) : null,
- 'edition' => $product->edition,
- 'activation_limit' => $product->activation_limit,
- 'max_major_version' => $product->max_major_version,
- 'payment_provider' => $provider,
- 'external_order_id' => $orderID,
- 'external_customer_id' => $customerID,
- ]);
- if ($queueDelivery) {
- LicenseDeliveryOutbox::updateOrCreate(
- ['license_id' => $license->id],
- ['encrypted_license_key' => Crypt::encryptString($plain), 'recipient_email' => $license->customer_email, 'delivered_at' => null]
- );
- }
-
- return [$license, $plain];
- });
- }
-}
diff --git a/Licensing/Services/LicenseKey.php b/Licensing/Services/LicenseKey.php
deleted file mode 100755
index 4e2501f..0000000
--- a/Licensing/Services/LicenseKey.php
+++ /dev/null
@@ -1,27 +0,0 @@
-privateSeed();
- $pair = sodium_crypto_sign_seed_keypair($seed);
- $secret = sodium_crypto_sign_secretkey($pair);
-
- $payload = [
- 'customerEmail' => $license->customer_email,
- 'customerName' => $license->customer_name,
- 'edition' => $license->edition,
- 'expiresAt' => $license->expires_at?->utc()->format('Y-m-d\\TH:i:s\\Z'),
- 'installationID' => strtolower($installationID),
- 'issuedAt' => ($issuedAt ?? now())->utc()->format('Y-m-d\\TH:i:s\\Z'),
- 'licenseID' => strtolower((string) $license->id),
- 'maxMajorVersion' => $license->max_major_version,
- ];
- ksort($payload, SORT_STRING);
- $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
- $sig = sodium_crypto_sign_detached($json, $secret);
-
- return 'RS3.'.self::b64url($json).'.'.self::b64url($sig);
- }
-
- public function publicKeyBase64(): string
- {
- $pair = sodium_crypto_sign_seed_keypair($this->privateSeed());
-
- return base64_encode(sodium_crypto_sign_publickey($pair));
- }
-
- private function privateSeed(): string
- {
- $path = config('sonardesk.private_key_path');
- if (! $path || ! is_file($path)) {
- throw new RuntimeException('SONARDESK_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.');
- }
- $decoded = base64_decode(trim((string) file_get_contents($path)), true);
- if ($decoded === false || strlen($decoded) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) {
- throw new RuntimeException('The SonarDesk private key must be a Base64-encoded 32-byte Ed25519 seed.');
- }
-
- return $decoded;
- }
-
- private static function b64url(string $value): string
- {
- return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
- }
-}
diff --git a/Licensing/Services/LicenseTokenVerifier.php b/Licensing/Services/LicenseTokenVerifier.php
deleted file mode 100755
index 052f633..0000000
--- a/Licensing/Services/LicenseTokenVerifier.php
+++ /dev/null
@@ -1,58 +0,0 @@
-decode($parts[1]);
- $signature = $this->decode($parts[2]);
- // Derive the verification key from the same private seed used by LicenseSigner.
- // Keeping a separate SONARDESK_LICENSE_PUBLIC_KEY allowed the two keys to drift:
- // activation would sign a perfectly valid token with the new private key, then
- // /validate would reject that same token using a stale public key.
- $public = $this->publicKeyFromPrivateSeed();
- if (! sodium_crypto_sign_verify_detached($signature, $payload, $public)) {
- throw new RuntimeException('Licence token signature is invalid.');
- }
- $decoded = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
-
- return is_array($decoded) ? $decoded : throw new RuntimeException('Licence token payload is invalid.');
- }
-
- private function publicKeyFromPrivateSeed(): string
- {
- $path = config('sonardesk.private_key_path');
- if (! $path || ! is_file($path)) {
- throw new RuntimeException('SONARDESK_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.');
- }
-
- $seed = base64_decode(trim((string) file_get_contents($path)), true);
- if ($seed === false || strlen($seed) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) {
- throw new RuntimeException('The SonarDesk private key must be a Base64-encoded 32-byte Ed25519 seed.');
- }
-
- $pair = sodium_crypto_sign_seed_keypair($seed);
-
- return sodium_crypto_sign_publickey($pair);
- }
-
- private function decode(string $value): string
- {
- $value = strtr($value, '-_', '+/');
- $value .= str_repeat('=', (4 - strlen($value) % 4) % 4);
- $out = base64_decode($value, true);
- if ($out === false) {
- throw new RuntimeException('Invalid Base64URL value.');
- }
-
- return $out;
- }
-}
diff --git a/Licensing/Support/.gitkeep b/Licensing/Support/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/Licensing/Support/ModuleInfo.php b/Licensing/Support/ModuleInfo.php
deleted file mode 100644
index 28b9983..0000000
--- a/Licensing/Support/ModuleInfo.php
+++ /dev/null
@@ -1,26 +0,0 @@
-morphMany(LocalizedContent::class, 'translatable');
- }
-
- public function translated(string $field, ?string $locale = null, mixed $fallback = null): mixed
- {
- $locale ??= app()->getLocale();
- $translation = $this->translations->first(fn (LocalizedContent $item): bool => $item->field === $field && $item->locale?->code === $locale);
- if ($translation && filled($translation->value)) {
- return $translation->value;
- }
-
- return $fallback ?? $this->getAttribute($field);
- }
-
- public function setTranslation(string $field, string $localeCode, ?string $value, ?int $userId = null): LocalizedContent
- {
- $locale = Locale::query()->where('code', $localeCode)->firstOrFail();
-
- return $this->translations()->updateOrCreate(
- ['locale_id' => $locale->id, 'field' => $field],
- ['value' => $value, 'updated_by' => $userId]
- );
- }
-}
diff --git a/Localization/Config/config.php b/Localization/Config/config.php
deleted file mode 100644
index da2392d..0000000
--- a/Localization/Config/config.php
+++ /dev/null
@@ -1,16 +0,0 @@
- env('APP_LOCALE', 'en'),
- 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
- 'session_key' => 'platform_locale',
- 'supported_subjects' => [
- 'pages' => ['model' => Page::class, 'label' => 'Pages', 'fields' => ['title', 'summary', 'content', 'meta_title', 'meta_description']],
- 'blog' => ['model' => Blog::class, 'label' => 'Blog', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']],
- 'news' => ['model' => News::class, 'label' => 'News', 'fields' => ['title', 'excerpt', 'content', 'meta_title', 'meta_description']],
- ],
-];
diff --git a/Localization/Database/Migrations/2026_07_24_06000_create_localization_tables.php b/Localization/Database/Migrations/2026_07_24_06000_create_localization_tables.php
deleted file mode 100644
index 24f8aba..0000000
--- a/Localization/Database/Migrations/2026_07_24_06000_create_localization_tables.php
+++ /dev/null
@@ -1,46 +0,0 @@
-id();
- $table->string('code', 20)->unique();
- $table->string('name', 100);
- $table->string('native_name', 100);
- $table->string('regional_code', 20)->nullable();
- $table->string('direction', 3)->default('ltr');
- $table->string('date_format', 40)->default('d/m/Y');
- $table->string('time_format', 40)->default('H:i');
- $table->string('timezone', 80)->default('UTC');
- $table->string('currency_code', 3)->nullable();
- $table->string('currency_symbol', 10)->nullable();
- $table->unsignedInteger('position')->default(0);
- $table->boolean('is_default')->default(false);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- Schema::create('localized_contents', function (Blueprint $table): void {
- $table->id();
- $table->morphs('translatable');
- $table->foreignId('locale_id')->constrained()->cascadeOnDelete();
- $table->string('field', 100);
- $table->longText('value')->nullable();
- $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- $table->unique(['translatable_type', 'translatable_id', 'locale_id', 'field'], 'localized_content_unique');
- $table->index(['locale_id', 'field']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('localized_contents');
- Schema::dropIfExists('locales');
- }
-};
diff --git a/Localization/Database/Seeders/LocaleSeeder.php b/Localization/Database/Seeders/LocaleSeeder.php
deleted file mode 100644
index 1bf793d..0000000
--- a/Localization/Database/Seeders/LocaleSeeder.php
+++ /dev/null
@@ -1,19 +0,0 @@
-updateOrCreate(['code' => 'en'], [
- 'name' => 'English', 'native_name' => 'English', 'regional_code' => 'en-GB',
- 'direction' => 'ltr', 'date_format' => 'd/m/Y', 'time_format' => 'H:i',
- 'timezone' => config('app.timezone', 'UTC'), 'currency_code' => 'GBP',
- 'currency_symbol' => '£', 'position' => 0, 'is_default' => true, 'is_active' => true,
- ]);
- }
-}
diff --git a/Localization/Http/Controllers/LocaleController.php b/Localization/Http/Controllers/LocaleController.php
deleted file mode 100644
index 8e3a54c..0000000
--- a/Localization/Http/Controllers/LocaleController.php
+++ /dev/null
@@ -1,78 +0,0 @@
-user()->can('localization.manage'), 403);
- $subjects = collect(config('localization.supported_subjects'))->map(function (array $config): array {
- $model = $config['model'];
-
- return $config + ['records' => $model::query()->latest()->limit(8)->get()];
- });
-
- return view('localization::admin.index', ['locales' => Locale::query()->orderBy('position')->get(), 'subjects' => $subjects]);
- }
-
- public function switch(string $locale, LocaleManager $manager): RedirectResponse
- {
- $manager->activate($locale);
-
- return back();
- }
-
- public function store(Request $request): RedirectResponse
- {
- abort_unless($request->user()->can('localization.manage'), 403);
- $data = $request->validate($this->rules());
- if (($data['is_default'] ?? false) === true) {
- Locale::query()->update(['is_default' => false]);
- }
- Locale::query()->create($data);
-
- return back()->with('success', 'Locale created.');
- }
-
- public function update(Request $request, Locale $locale): RedirectResponse
- {
- abort_unless($request->user()->can('localization.manage'), 403);
- $data = $request->validate($this->rules($locale));
- if (($data['is_default'] ?? false) === true) {
- Locale::query()->whereKeyNot($locale->id)->update(['is_default' => false]);
- }
- $locale->update($data);
-
- return back()->with('success', 'Locale updated.');
- }
-
- public function destroy(Request $request, Locale $locale): RedirectResponse
- {
- abort_unless($request->user()->can('localization.manage'), 403);
- abort_if($locale->is_default, 422, 'The default locale cannot be deleted.');
- $locale->delete();
-
- return back()->with('success', 'Locale deleted.');
- }
-
- private function rules(?Locale $locale = null): array
- {
- return [
- 'code' => ['required', 'string', 'max:20', 'unique:locales,code,'.($locale?->id ?? 'NULL')],
- 'name' => ['required', 'string', 'max:100'], 'native_name' => ['required', 'string', 'max:100'],
- 'regional_code' => ['nullable', 'string', 'max:20'], 'direction' => ['required', 'in:ltr,rtl'],
- 'date_format' => ['required', 'string', 'max:40'], 'time_format' => ['required', 'string', 'max:40'],
- 'timezone' => ['required', 'timezone'], 'currency_code' => ['nullable', 'string', 'size:3'],
- 'currency_symbol' => ['nullable', 'string', 'max:10'], 'position' => ['nullable', 'integer', 'min:0'],
- 'is_default' => ['sometimes', 'boolean'], 'is_active' => ['sometimes', 'boolean'],
- ];
- }
-}
diff --git a/Localization/Http/Controllers/TranslationController.php b/Localization/Http/Controllers/TranslationController.php
deleted file mode 100644
index 69fd8b5..0000000
--- a/Localization/Http/Controllers/TranslationController.php
+++ /dev/null
@@ -1,44 +0,0 @@
-user()->can('localization.translate'), 403);
- [$record, $config] = $this->resolve($subject, $id);
- $record->load('translations.locale');
-
- return view('localization::admin.translate', ['record' => $record, 'subject' => $subject, 'config' => $config, 'locales' => Locale::query()->active()->get()]);
- }
-
- public function update(Request $request, string $subject, int $id): RedirectResponse
- {
- abort_unless($request->user()->can('localization.translate'), 403);
- [$record, $config] = $this->resolve($subject, $id);
- $data = $request->validate(['translations' => ['array'], 'translations.*' => ['array']]);
- foreach ($data['translations'] ?? [] as $localeCode => $fields) {
- foreach ($config['fields'] as $field) {
- $record->setTranslation($field, $localeCode, $fields[$field] ?? null, $request->user()->id);
- }
- }
-
- return back()->with('success', 'Translations saved.');
- }
-
- private function resolve(string $subject, int $id): array
- {
- $config = config("localization.supported_subjects.$subject");
- abort_unless(is_array($config), 404);
- $record = $config['model']::query()->findOrFail($id);
-
- return [$record, $config];
- }
-}
diff --git a/Localization/Http/Middleware/SetLocale.php b/Localization/Http/Middleware/SetLocale.php
deleted file mode 100644
index 1d93a23..0000000
--- a/Localization/Http/Middleware/SetLocale.php
+++ /dev/null
@@ -1,53 +0,0 @@
-route('locale');
-
- if (! $code && $request->hasSession()) {
- $code = $request->session()->get($sessionKey);
- }
-
- if (! Schema::hasTable('locales')) {
- app()->setLocale(is_string($code) && $code !== '' ? $code : $fallback);
-
- return $next($request);
- }
-
- if (! $code) {
- $code = Locale::query()
- ->where('is_default', true)
- ->value('code') ?: $fallback;
- }
-
- $isActive = Locale::query()
- ->where('code', $code)
- ->where('is_active', true)
- ->exists();
-
- if ($isActive) {
- app()->setLocale((string) $code);
-
- if ($request->hasSession()) {
- $request->session()->put($sessionKey, $code);
- }
- } else {
- app()->setLocale($fallback);
- }
-
- return $next($request);
- }
-}
diff --git a/Localization/Models/Locale.php b/Localization/Models/Locale.php
deleted file mode 100644
index 97fa973..0000000
--- a/Localization/Models/Locale.php
+++ /dev/null
@@ -1,27 +0,0 @@
- 'boolean', 'is_active' => 'boolean'];
- }
-
- public function translations(): HasMany
- {
- return $this->hasMany(LocalizedContent::class);
- }
-
- public function scopeActive(Builder $query): Builder
- {
- return $query->where('is_active', true)->orderBy('position')->orderBy('name');
- }
-}
diff --git a/Localization/Models/LocalizedContent.php b/Localization/Models/LocalizedContent.php
deleted file mode 100644
index 2a10675..0000000
--- a/Localization/Models/LocalizedContent.php
+++ /dev/null
@@ -1,28 +0,0 @@
-morphTo();
- }
-
- public function locale(): BelongsTo
- {
- return $this->belongsTo(Locale::class);
- }
-
- public function editor(): BelongsTo
- {
- return $this->belongsTo(User::class, 'updated_by');
- }
-}
diff --git a/Localization/Providers/LocalizationServiceProvider.php b/Localization/Providers/LocalizationServiceProvider.php
deleted file mode 100644
index 6383fa8..0000000
--- a/Localization/Providers/LocalizationServiceProvider.php
+++ /dev/null
@@ -1,22 +0,0 @@
-mergeConfigFrom(module_path('Localization', 'Config/config.php'), 'localization');
- $this->app->singleton(LocaleManager::class);
- $this->app->register(RouteServiceProvider::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Localization', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Localization', 'Resources/views'), 'localization');
- }
-}
diff --git a/Localization/Providers/RouteServiceProvider.php b/Localization/Providers/RouteServiceProvider.php
deleted file mode 100644
index 39dae19..0000000
--- a/Localization/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Localization', 'Routes/web.php'));
- }
-}
diff --git a/Localization/Resources/views/admin/index.blade.php b/Localization/Resources/views/admin/index.blade.php
deleted file mode 100644
index 4dca7c8..0000000
--- a/Localization/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Localisation Languages, regional formats and translated content.
Add locale @csrfLTR RTL Add locale Translate content @foreach($subjects as $key=>$subject)
@endforeach
diff --git a/Localization/Resources/views/admin/translate.blade.php b/Localization/Resources/views/admin/translate.blade.php
deleted file mode 100644
index c71edbc..0000000
--- a/Localization/Resources/views/admin/translate.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Translate: {{ $record->title ?? ('#'.$record->id) }} {{ $config['label'] }} · source language values are shown for reference.
@csrf @method('PUT')@foreach($locales as $locale){{ $locale->native_name }} {{ $locale->code }}
@foreach($config['fields'] as $field)@php($existing=$record->translations->first(fn($item)=>$item->locale_id===$locale->id&&$item->field===$field)?->value){{ str($field)->headline() }}
Source: {{ str(strip_tags((string)$record->getAttribute($field)))->limit(180) }}
@if(in_array($field,['content','summary','excerpt','meta_description']))
{{ $existing }} @else
@endif
@endforeach @endforeachSave translations
diff --git a/Localization/Routes/web.php b/Localization/Routes/web.php
deleted file mode 100644
index 119020e..0000000
--- a/Localization/Routes/web.php
+++ /dev/null
@@ -1,15 +0,0 @@
-name('localization.switch');
-Route::middleware(['auth', 'verified'])->prefix('administration/localization')->name('administration.localization.')->group(function (): void {
- Route::get('/', [LocaleController::class, 'index'])->name('index');
- Route::post('/locales', [LocaleController::class, 'store'])->name('locales.store');
- Route::put('/locales/{locale}', [LocaleController::class, 'update'])->name('locales.update');
- Route::delete('/locales/{locale}', [LocaleController::class, 'destroy'])->name('locales.destroy');
- Route::get('/translate/{subject}/{id}', [TranslationController::class, 'edit'])->name('translations.edit');
- Route::put('/translate/{subject}/{id}', [TranslationController::class, 'update'])->name('translations.update');
-});
diff --git a/Localization/Services/LocaleManager.php b/Localization/Services/LocaleManager.php
deleted file mode 100644
index 0454a58..0000000
--- a/Localization/Services/LocaleManager.php
+++ /dev/null
@@ -1,28 +0,0 @@
-active()->get();
- }
-
- public function default(): ?Locale
- {
- return Locale::query()->where('is_default', true)->first() ?? Locale::query()->active()->first();
- }
-
- public function activate(string $code): Locale
- {
- $locale = Locale::query()->active()->where('code', $code)->firstOrFail();
- app()->setLocale($locale->code);
- session()->put(config('localization.session_key'), $locale->code);
-
- return $locale;
- }
-}
diff --git a/Localization/Tests/Feature/ModuleBootTest.php b/Localization/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index c4aea49..0000000
--- a/Localization/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,14 +0,0 @@
-assertTrue(app()->providerIsLoaded(LocalizationServiceProvider::class));
- }
-}
diff --git a/Localization/module.json b/Localization/module.json
deleted file mode 100644
index a5e28f7..0000000
--- a/Localization/module.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "Localization",
- "alias": "localization",
- "description": "Locales, translated content, regional settings and language switching.",
- "keywords": ["localization", "translation", "locale", "language"],
- "priority": 36,
- "providers": ["Modules\\Localization\\Providers\\LocalizationServiceProvider"],
- "files": []
-}
diff --git a/Locations/Config/config.php b/Locations/Config/config.php
deleted file mode 100644
index ba035af..0000000
--- a/Locations/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Locations'];
diff --git a/Locations/Database/Migrations/2026_07_24_04400_create_locations_table.php b/Locations/Database/Migrations/2026_07_24_04400_create_locations_table.php
deleted file mode 100644
index 8ef54fb..0000000
--- a/Locations/Database/Migrations/2026_07_24_04400_create_locations_table.php
+++ /dev/null
@@ -1,34 +0,0 @@
-id();
- $t->string('name');
- $t->string('slug')->unique();
- $t->string('type')->default('office');
- $t->text('address')->nullable();
- $t->string('postcode')->nullable();
- $t->string('country')->nullable();
- $t->string('phone')->nullable();
- $t->string('email')->nullable();
- $t->decimal('latitude', 10, 7)->nullable();
- $t->decimal('longitude', 10, 7)->nullable();
- $t->json('opening_hours')->nullable();
- $t->boolean('is_active')->default(true);
- $t->unsignedInteger('sort_order')->default(0);
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('locations');
- }
-};
diff --git a/Locations/Http/Controllers/LocationController.php b/Locations/Http/Controllers/LocationController.php
deleted file mode 100644
index a1b2606..0000000
--- a/Locations/Http/Controllers/LocationController.php
+++ /dev/null
@@ -1,31 +0,0 @@
- Location::orderBy('sort_order')->orderBy('name')->paginate(25)]);
- }
-
- public function store(Request $r): RedirectResponse
- {
- Location::create($r->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:locations,slug'], 'type' => ['required', 'in:office,store,warehouse,venue,service'], 'address' => ['nullable', 'string'], 'postcode' => ['nullable', 'string', 'max:30'], 'country' => ['nullable', 'string', 'max:120'], 'phone' => ['nullable', 'string', 'max:80'], 'email' => ['nullable', 'email', 'max:255'], 'latitude' => ['nullable', 'numeric', 'between:-90,90'], 'longitude' => ['nullable', 'numeric', 'between:-180,180'], 'sort_order' => ['nullable', 'integer', 'min:0']]) + ['is_active' => $r->boolean('is_active', true)]);
-
- return back()->with('status', 'Location created.');
- }
-
- public function destroy(Location $location): RedirectResponse
- {
- $location->delete();
-
- return back()->with('status', 'Location deleted.');
- }
-}
diff --git a/Locations/Models/Location.php b/Locations/Models/Location.php
deleted file mode 100644
index 913fd40..0000000
--- a/Locations/Models/Location.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'decimal:7', 'longitude' => 'decimal:7', 'opening_hours' => 'array', 'is_active' => 'boolean'];
- }
-}
diff --git a/Locations/Providers/LocationsServiceProvider.php b/Locations/Providers/LocationsServiceProvider.php
deleted file mode 100644
index de2419b..0000000
--- a/Locations/Providers/LocationsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Locations', 'Config/config.php'), 'locations');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Locations', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Locations', 'Resources/views'), 'locations');
- }
-}
diff --git a/Locations/Providers/RouteServiceProvider.php b/Locations/Providers/RouteServiceProvider.php
deleted file mode 100644
index cb2ee3e..0000000
--- a/Locations/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Locations', 'Routes/web.php'));
- }
-}
diff --git a/Locations/Resources/views/admin/index.blade.php b/Locations/Resources/views/admin/index.blade.php
deleted file mode 100644
index 3b1f2c1..0000000
--- a/Locations/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Locations Manage offices, stores, venues and service locations.
@if(session('status'))
{{ session('status') }}
@endif
@csrfAdd location Office Store Warehouse Venue Service
Create location Location Type Contact Status @forelse($locations as $location){{ $location->name }}
{{ $location->address }}
{{ ucfirst($location->type) }} {{ $location->phone ?: $location->email ?: '—' }} {{ $location->is_active ? 'Active' : 'Inactive' }} @csrf @method('DELETE')Delete @emptyNo locations yet. @endforelse
{{ $locations->links() }}
diff --git a/Locations/Routes/web.php b/Locations/Routes/web.php
deleted file mode 100644
index 2e5d437..0000000
--- a/Locations/Routes/web.php
+++ /dev/null
@@ -1,10 +0,0 @@
-prefix('administration/locations')->name('administration.locations.')->group(function (): void {
- Route::get('/', [LocationController::class, 'index'])->name('index');
- Route::post('/', [LocationController::class, 'store'])->name('store');
- Route::delete('/{location}', [LocationController::class, 'destroy'])->name('destroy');
-});
diff --git a/Locations/Tests/Feature/ModuleBootTest.php b/Locations/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index c8dec12..0000000
--- a/Locations/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Locations', config('locations.name'));
- }
-}
diff --git a/Locations/module.json b/Locations/module.json
deleted file mode 100644
index 86865e1..0000000
--- a/Locations/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Locations",
- "alias": "locations",
- "version": "1.0.0",
- "description": "Office and service location management.",
- "keywords": ["business", "locations"],
- "priority": 44,
- "providers": ["Modules\\Locations\\Providers\\LocationsServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Marketplace/Config/config.php b/Marketplace/Config/config.php
deleted file mode 100644
index 2274c18..0000000
--- a/Marketplace/Config/config.php
+++ /dev/null
@@ -1,14 +0,0 @@
- (bool) env('MARKETPLACE_ENABLED', true),
- 'catalogue_url' => env('MARKETPLACE_CATALOGUE_URL'),
- 'allow_remote_install' => (bool) env('MARKETPLACE_ALLOW_REMOTE_INSTALL', false),
- 'require_checksum' => (bool) env('MARKETPLACE_REQUIRE_CHECKSUM', true),
- 'require_signature' => (bool) env('MARKETPLACE_REQUIRE_SIGNATURE', false),
- 'trusted_publishers' => array_values(array_filter(array_map('trim', explode(',', (string) env('MARKETPLACE_TRUSTED_PUBLISHERS', ''))))),
- 'storage_disk' => env('MARKETPLACE_STORAGE_DISK', 'local'),
- 'package_path' => env('MARKETPLACE_PACKAGE_PATH', 'marketplace/packages'),
- 'snapshot_path' => env('MARKETPLACE_SNAPSHOT_PATH', 'marketplace/snapshots'),
- 'supported_types' => ['module', 'theme', 'extension'],
-];
diff --git a/Marketplace/Console/Commands/MarketplaceAuditCommand.php b/Marketplace/Console/Commands/MarketplaceAuditCommand.php
deleted file mode 100644
index b8a36b0..0000000
--- a/Marketplace/Console/Commands/MarketplaceAuditCommand.php
+++ /dev/null
@@ -1,24 +0,0 @@
-with('package')->get();
- foreach ($packages as $package) {
- $this->line(sprintf('%s %s [%s]%s', $package->package_name, $package->version, $package->status, $package->hasUpdate() ? ' update available' : ''));
- }
- $this->info($packages->count().' installed package(s) audited.');
-
- return self::SUCCESS;
- }
-}
diff --git a/Marketplace/Console/Commands/MarketplaceSyncCommand.php b/Marketplace/Console/Commands/MarketplaceSyncCommand.php
deleted file mode 100644
index 060aa58..0000000
--- a/Marketplace/Console/Commands/MarketplaceSyncCommand.php
+++ /dev/null
@@ -1,24 +0,0 @@
-warn('MARKETPLACE_CATALOGUE_URL is not configured.');
-
- return self::SUCCESS;
- }
- $this->info('Catalogue synchronisation is configured. Remote installs remain disabled unless explicitly enabled.');
-
- return self::SUCCESS;
- }
-}
diff --git a/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php b/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php
deleted file mode 100644
index eb2a3b9..0000000
--- a/Marketplace/Database/Migrations/2026_07_24_170000_create_marketplace_tables.php
+++ /dev/null
@@ -1,96 +0,0 @@
-id();
- $table->string('name');
- $table->string('slug')->unique();
- $table->string('email')->nullable();
- $table->string('website')->nullable();
- $table->text('public_key')->nullable();
- $table->boolean('is_verified')->default(false)->index();
- $table->json('metadata')->nullable();
- $table->timestamps();
- });
- Schema::create('marketplace_packages', function (Blueprint $table) {
- $table->id();
- $table->foreignId('publisher_id')->nullable()->constrained('marketplace_publishers')->nullOnDelete();
- $table->string('type', 30)->index();
- $table->string('name');
- $table->string('slug')->unique();
- $table->string('package_name')->unique();
- $table->text('description')->nullable();
- $table->string('latest_version')->nullable();
- $table->string('homepage')->nullable();
- $table->string('repository_url')->nullable();
- $table->string('download_url')->nullable();
- $table->string('checksum', 128)->nullable();
- $table->text('signature')->nullable();
- $table->json('manifest')->nullable();
- $table->boolean('is_featured')->default(false)->index();
- $table->boolean('is_active')->default(true)->index();
- $table->timestamp('catalogue_updated_at')->nullable();
- $table->timestamps();
- });
- Schema::create('marketplace_installed_packages', function (Blueprint $table) {
- $table->id();
- $table->foreignId('marketplace_package_id')->nullable()->constrained('marketplace_packages')->nullOnDelete();
- $table->string('package_name')->unique();
- $table->string('type', 30)->index();
- $table->string('version');
- $table->string('status', 30)->default('installed')->index();
- $table->json('manifest')->nullable();
- $table->string('checksum', 128)->nullable();
- $table->string('install_path')->nullable();
- $table->boolean('is_enabled')->default(true)->index();
- $table->foreignId('installed_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamp('installed_at')->nullable();
- $table->timestamp('updated_at_marketplace')->nullable();
- $table->timestamps();
- });
- Schema::create('marketplace_installations', function (Blueprint $table) {
- $table->id();
- $table->foreignId('installed_package_id')->nullable()->constrained('marketplace_installed_packages')->nullOnDelete();
- $table->string('package_name')->index();
- $table->string('operation', 30)->index();
- $table->string('from_version')->nullable();
- $table->string('to_version')->nullable();
- $table->string('status', 30)->default('pending')->index();
- $table->string('snapshot_path')->nullable();
- $table->text('message')->nullable();
- $table->json('context')->nullable();
- $table->foreignId('performed_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamp('started_at')->nullable();
- $table->timestamp('completed_at')->nullable();
- $table->timestamps();
- });
- Schema::create('marketplace_licences', function (Blueprint $table) {
- $table->id();
- $table->foreignId('marketplace_package_id')->nullable()->constrained('marketplace_packages')->nullOnDelete();
- $table->string('licence_key')->nullable();
- $table->string('status', 30)->default('inactive')->index();
- $table->string('plan')->nullable();
- $table->string('licensed_to')->nullable();
- $table->timestamp('activated_at')->nullable();
- $table->timestamp('expires_at')->nullable();
- $table->json('metadata')->nullable();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('marketplace_licences');
- Schema::dropIfExists('marketplace_installations');
- Schema::dropIfExists('marketplace_installed_packages');
- Schema::dropIfExists('marketplace_packages');
- Schema::dropIfExists('marketplace_publishers');
- }
-};
diff --git a/Marketplace/Http/Controllers/MarketplaceApiController.php b/Marketplace/Http/Controllers/MarketplaceApiController.php
deleted file mode 100644
index 958d8f3..0000000
--- a/Marketplace/Http/Controllers/MarketplaceApiController.php
+++ /dev/null
@@ -1,40 +0,0 @@
-json(MarketplacePackage::query()->with('publisher')->where('is_active', true)->when($request->filled('type'), fn ($q) => $q->where('type', $request->string('type')))->paginate(min(100, max(1, $request->integer('per_page', 25)))));
- }
-
- public function installed(): JsonResponse
- {
- return response()->json(InstalledPackage::query()->with('package')->paginate(50));
- }
-
- public function install(MarketplacePackage $package, MarketplaceManager $manager, Request $request): JsonResponse
- {
- return response()->json($manager->install($package, $request->user()?->id)->load('package'), 201);
- }
-
- public function toggle(InstalledPackage $package, MarketplaceManager $manager, Request $request): JsonResponse
- {
- return response()->json($manager->setEnabled($package, ! $package->is_enabled, $request->user()?->id));
- }
-
- public function remove(InstalledPackage $package, MarketplaceManager $manager, Request $request): JsonResponse
- {
- $manager->remove($package, $request->user()?->id);
-
- return response()->json(['status' => 'removed']);
- }
-}
diff --git a/Marketplace/Http/Controllers/MarketplaceController.php b/Marketplace/Http/Controllers/MarketplaceController.php
deleted file mode 100644
index 45744a0..0000000
--- a/Marketplace/Http/Controllers/MarketplaceController.php
+++ /dev/null
@@ -1,64 +0,0 @@
-with('publisher')->where('is_active', true)->when($request->filled('type'), fn ($q) => $q->where('type', $request->string('type')))->when($request->filled('search'), fn ($q) => $q->where(fn ($q) => $q->where('name', 'like', '%'.$request->string('search').'%')->orWhere('description', 'like', '%'.$request->string('search').'%')))->latest('is_featured')->paginate(20)->withQueryString();
-
- return view('marketplace::index', compact('packages'));
- }
-
- public function installed(): View
- {
- $packages = InstalledPackage::query()->with('package')->latest()->paginate(25);
-
- return view('marketplace::installed', compact('packages'));
- }
-
- public function install(MarketplacePackage $package, MarketplaceManager $manager, Request $request): RedirectResponse
- {
- $manager->install($package, $request->user()?->id);
-
- return back()->with('status', 'Package installed successfully.');
- }
-
- public function update(MarketplacePackage $package, MarketplaceManager $manager, Request $request): RedirectResponse
- {
- $manager->install($package, $request->user()?->id);
-
- return back()->with('status', 'Package updated successfully.');
- }
-
- public function toggle(InstalledPackage $package, MarketplaceManager $manager, Request $request): RedirectResponse
- {
- $manager->setEnabled($package, ! $package->is_enabled, $request->user()?->id);
-
- return back()->with('status', 'Package state updated.');
- }
-
- public function remove(InstalledPackage $package, MarketplaceManager $manager, Request $request): RedirectResponse
- {
- $manager->remove($package, $request->user()?->id);
-
- return back()->with('status', 'Package removed.');
- }
-
- public function rollback(InstalledPackage $package, Installation $installation, MarketplaceManager $manager, Request $request): RedirectResponse
- {
- $manager->rollback($package, $installation, $request->user()?->id);
-
- return back()->with('status','Package rolled back.');
- }
-}
diff --git a/Marketplace/Models/Installation.php b/Marketplace/Models/Installation.php
deleted file mode 100644
index c152b8c..0000000
--- a/Marketplace/Models/Installation.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'array', 'started_at' => 'datetime', 'completed_at' => 'datetime'];
-}
diff --git a/Marketplace/Models/InstalledPackage.php b/Marketplace/Models/InstalledPackage.php
deleted file mode 100644
index bb7eda5..0000000
--- a/Marketplace/Models/InstalledPackage.php
+++ /dev/null
@@ -1,31 +0,0 @@
- 'array', 'is_enabled' => 'boolean', 'installed_at' => 'datetime', 'updated_at_marketplace' => 'datetime'];
-
- public function package(): BelongsTo
- {
- return $this->belongsTo(MarketplacePackage::class, 'marketplace_package_id');
- }
-
- public function installations(): HasMany
- {
- return $this->hasMany(Installation::class, 'installed_package_id');
- }
-
- public function hasUpdate(): bool
- {
- return $this->package?->latest_version && version_compare($this->package->latest_version, $this->version, '>');
- }
-}
diff --git a/Marketplace/Models/MarketplaceLicence.php b/Marketplace/Models/MarketplaceLicence.php
deleted file mode 100644
index 22038e8..0000000
--- a/Marketplace/Models/MarketplaceLicence.php
+++ /dev/null
@@ -1,16 +0,0 @@
- 'datetime', 'expires_at' => 'datetime', 'metadata' => 'array'];
-}
diff --git a/Marketplace/Models/MarketplacePackage.php b/Marketplace/Models/MarketplacePackage.php
deleted file mode 100644
index ad4a0c2..0000000
--- a/Marketplace/Models/MarketplacePackage.php
+++ /dev/null
@@ -1,26 +0,0 @@
- 'array', 'is_featured' => 'boolean', 'is_active' => 'boolean', 'catalogue_updated_at' => 'datetime'];
-
- public function publisher(): BelongsTo
- {
- return $this->belongsTo(Publisher::class);
- }
-
- public function licences(): HasMany
- {
- return $this->hasMany(MarketplaceLicence::class, 'marketplace_package_id');
- }
-}
diff --git a/Marketplace/Models/Publisher.php b/Marketplace/Models/Publisher.php
deleted file mode 100644
index 16558ef..0000000
--- a/Marketplace/Models/Publisher.php
+++ /dev/null
@@ -1,20 +0,0 @@
- 'boolean', 'metadata' => 'array'];
-
- public function packages(): HasMany
- {
- return $this->hasMany(MarketplacePackage::class, 'publisher_id');
- }
-}
diff --git a/Marketplace/Providers/MarketplaceServiceProvider.php b/Marketplace/Providers/MarketplaceServiceProvider.php
deleted file mode 100644
index 0edd17f..0000000
--- a/Marketplace/Providers/MarketplaceServiceProvider.php
+++ /dev/null
@@ -1,36 +0,0 @@
-mergeConfigFrom(module_path('Marketplace', 'Config/config.php'), 'marketplace');
- $this->app->singleton(PackageManifest::class);
- $this->app->singleton(CompatibilityChecker::class);
- $this->app->singleton(DependencyResolver::class);
- $this->app->singleton(PackageVerifier::class);
- $this->app->singleton(MarketplaceManager::class);
- $this->app->register(RouteServiceProvider::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Marketplace', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Marketplace', 'Resources/views'), 'marketplace');
- $this->loadTranslationsFrom(module_path('Marketplace', 'Resources/lang'), 'marketplace');
- if ($this->app->runningInConsole()) {
- $this->commands([MarketplaceSyncCommand::class, MarketplaceAuditCommand::class]);
- }
- }
-}
diff --git a/Marketplace/Providers/RouteServiceProvider.php b/Marketplace/Providers/RouteServiceProvider.php
deleted file mode 100644
index 6bf0725..0000000
--- a/Marketplace/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Marketplace', 'Routes/web.php'));
- Route::middleware('api')->prefix('api/v1')->group(module_path('Marketplace', 'Routes/api.php'));
- }
-}
diff --git a/Marketplace/Resources/views/index.blade.php b/Marketplace/Resources/views/index.blade.php
deleted file mode 100644
index 6399b57..0000000
--- a/Marketplace/Resources/views/index.blade.php
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-
-
- Marketplace
-
- Discover verified modules, themes and extensions.
-
-
-
-
- Installed packages
-
-
-
-
-
-
-
- All types
-
- @foreach (config('marketplace.supported_types', []) as $type)
-
- {{ \Illuminate\Support\Str::headline($type) }}
-
- @endforeach
-
-
- Filter
-
-
-
- @forelse ($packages as $package)
-
-
-
-
-
{{ $package->name }}
-
- @if ($package->is_featured)
- Featured
- @endif
-
-
-
- {{ $package->package_name }}
- · {{ \Illuminate\Support\Str::headline($package->type) }}
- · {{ $package->latest_version ?: 'No release' }}
-
-
-
- @can('marketplace.install')
-
- @csrf
-
-
- Install
-
-
- @endcan
-
-
- {{ $package->description }}
-
- @if ($package->publisher)
-
- Published by {{ $package->publisher->name }}
-
- @if ($package->publisher->is_verified)
- · Verified
- @endif
-
- @endif
-
- @empty
-
- No marketplace packages found.
-
- @endforelse
-
-
- {{ $packages->links() }}
-
-
diff --git a/Marketplace/Resources/views/installed.blade.php b/Marketplace/Resources/views/installed.blade.php
deleted file mode 100644
index 7609352..0000000
--- a/Marketplace/Resources/views/installed.blade.php
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
-
- Installed Packages
-
- Manage package state, updates and removals.
-
-
-
-
- Browse marketplace
-
-
-
-
-
-
-
- Package
- Version
- Status
- Update
- Actions
-
-
-
-
- @forelse ($packages as $package)
-
-
- {{ $package->package_name }}
-
- {{ \Illuminate\Support\Str::headline($package->type) }}
-
-
-
- {{ $package->version }}
- {{ ucfirst($package->status) }}
-
- {{ $package->hasUpdate() ? $package->package->latest_version : 'Current' }}
-
-
-
-
- @can('marketplace.update')
-
- @csrf
-
-
- {{ $package->is_enabled ? 'Disable' : 'Enable' }}
-
-
-
- @if ($package->hasUpdate())
-
- @csrf
-
-
- Update
-
-
- @endif
- @endcan
-
- @can('marketplace.remove')
-
- @csrf
- @method('DELETE')
-
-
- Remove
-
-
- @endcan
-
-
-
- @empty
-
-
- No packages installed through the marketplace.
-
-
- @endforelse
-
-
-
-
- {{ $packages->links() }}
-
-
diff --git a/Marketplace/Routes/api.php b/Marketplace/Routes/api.php
deleted file mode 100644
index 5c04c0e..0000000
--- a/Marketplace/Routes/api.php
+++ /dev/null
@@ -1,12 +0,0 @@
-prefix('marketplace')->name('api.marketplace.')->group(function () {
- Route::get('/packages', [MarketplaceApiController::class, 'packages'])->middleware('permission:marketplace.view')->name('packages');
- Route::get('/installed', [MarketplaceApiController::class, 'installed'])->middleware('permission:marketplace.view')->name('installed');
- Route::post('/packages/{package}/install', [MarketplaceApiController::class, 'install'])->middleware('permission:marketplace.install')->name('install');
- Route::post('/installed/{package}/toggle', [MarketplaceApiController::class, 'toggle'])->middleware('permission:marketplace.update')->name('toggle');
- Route::delete('/installed/{package}', [MarketplaceApiController::class, 'remove'])->middleware('permission:marketplace.remove')->name('remove');
-});
diff --git a/Marketplace/Routes/web.php b/Marketplace/Routes/web.php
deleted file mode 100644
index 2ac0b30..0000000
--- a/Marketplace/Routes/web.php
+++ /dev/null
@@ -1,14 +0,0 @@
-prefix('administration/marketplace')->name('administration.marketplace.')->group(function () {
- Route::get('/', [MarketplaceController::class, 'index'])->middleware('permission:marketplace.view')->name('index');
- Route::get('/installed', [MarketplaceController::class, 'installed'])->middleware('permission:marketplace.view')->name('installed');
- Route::post('/packages/{package}/install', [MarketplaceController::class, 'install'])->middleware('permission:marketplace.install')->name('install');
- Route::post('/packages/{package}/update', [MarketplaceController::class, 'update'])->middleware('permission:marketplace.update')->name('update');
- Route::post('/installed/{package}/toggle', [MarketplaceController::class, 'toggle'])->middleware('permission:marketplace.update')->name('toggle');
- Route::delete('/installed/{package}', [MarketplaceController::class, 'remove'])->middleware('permission:marketplace.remove')->name('remove');
- Route::post('/installed/{package}/rollback/{installation}', [MarketplaceController::class, 'rollback'])->middleware('permission:marketplace.update')->name('rollback');
-});
diff --git a/Marketplace/Services/CompatibilityChecker.php b/Marketplace/Services/CompatibilityChecker.php
deleted file mode 100644
index 4ef2e12..0000000
--- a/Marketplace/Services/CompatibilityChecker.php
+++ /dev/null
@@ -1,30 +0,0 @@
-constraints->matches(PHP_VERSION, $php)) {
- $errors[] = "Requires PHP $php; running ".PHP_VERSION.'.';
- }
- if (! $this->constraints->matches(App::version(), $laravel)) {
- $errors[] = "Requires Laravel $laravel; running ".App::version().'.';
- }
- if (! $this->constraints->matches($platformVersion, $platform)) {
- $errors[] = "Requires Content Manager $platform; running $platformVersion.";
- }
-
- return ['compatible' => $errors === [], 'errors' => $errors];
- }
-}
diff --git a/Marketplace/Services/DependencyResolver.php b/Marketplace/Services/DependencyResolver.php
deleted file mode 100644
index 02c711a..0000000
--- a/Marketplace/Services/DependencyResolver.php
+++ /dev/null
@@ -1,29 +0,0 @@
- $constraint) {
- $installed = InstalledPackage::query()->where('package_name', $package)->where('status', 'installed')->first();
- if (! $installed) {
- $missing[$package] = $constraint;
-
- continue;
- }
- if (! $this->constraints->matches($installed->version, (string) $constraint)) {
- $incompatible[$package] = ['required' => $constraint, 'installed' => $installed->version];
- }
- }
-
- return ['satisfied' => $missing === [] && $incompatible === [], 'missing' => $missing, 'incompatible' => $incompatible];
- }
-}
diff --git a/Marketplace/Services/MarketplaceManager.php b/Marketplace/Services/MarketplaceManager.php
deleted file mode 100644
index a457800..0000000
--- a/Marketplace/Services/MarketplaceManager.php
+++ /dev/null
@@ -1,72 +0,0 @@
-manifests->parse($package->manifest ?: ['name' => $package->package_name, 'type' => $package->type, 'version' => $package->latest_version]);
- $compatibility = $this->compatibility->check($manifest);
- if (! $compatibility['compatible']) {
- throw new RuntimeException(implode(' ', $compatibility['errors']));
- }
- $dependencies = $this->dependencies->resolve($manifest);
- if (! $dependencies['satisfied']) {
- throw new RuntimeException('Package dependencies are not satisfied.');
- }
-
- return DB::transaction(function () use ($package, $manifest, $userId) {
- $installed = InstalledPackage::query()->firstOrNew(['package_name' => $package->package_name]);
- $from = $installed->exists ? $installed->version : null;
- $installed->fill(['marketplace_package_id' => $package->id, 'type' => $package->type, 'version' => $manifest['version'], 'status' => 'installed', 'manifest' => $manifest, 'checksum' => $package->checksum, 'is_enabled' => true, 'installed_by' => $userId, 'installed_at' => $installed->installed_at ?: now(), 'updated_at_marketplace' => now()])->save();
- $this->history($installed, $from ? 'update' : 'install', $from, $manifest['version'], $userId, 'completed');
-
- return $installed->refresh();
- });
- }
-
- public function setEnabled(InstalledPackage $package, bool $enabled, ?int $userId = null): InstalledPackage
- {
- $package->update(['is_enabled' => $enabled, 'status' => $enabled ? 'installed' : 'disabled']);
- $this->history($package, $enabled ? 'enable' : 'disable', $package->version, $package->version, $userId, 'completed');
-
- return $package->refresh();
- }
-
- public function remove(InstalledPackage $package, ?int $userId = null): void
- {
- DB::transaction(function () use ($package, $userId) {
- $this->history($package, 'remove', $package->version, null, $userId, 'completed');
- $package->update(['status' => 'removed', 'is_enabled' => false]);
- });
- }
-
- public function rollback(InstalledPackage $package, Installation $installation, ?int $userId = null): InstalledPackage
- {
- if ($installation->package_name !== $package->package_name || blank($installation->from_version)) {
- throw new RuntimeException('This installation cannot be rolled back.');
- }
- $current = $package->version;
- $package->update(['version' => $installation->from_version, 'status' => 'installed', 'is_enabled' => true]);
- $this->history($package, 'rollback', $current, $installation->from_version, $userId, 'completed');
-
- return $package->refresh();
- }
-
- private function history(InstalledPackage $package, string $operation, ?string $from, ?string $to, ?int $userId, string $status): Installation
- {
- return Installation::query()->create(['installed_package_id' => $package->id, 'package_name' => $package->package_name, 'operation' => $operation, 'from_version' => $from, 'to_version' => $to, 'status' => $status, 'performed_by' => $userId, 'started_at' => now(), 'completed_at' => now()]);
- }
-}
diff --git a/Marketplace/Services/PackageManifest.php b/Marketplace/Services/PackageManifest.php
deleted file mode 100644
index 2fecb73..0000000
--- a/Marketplace/Services/PackageManifest.php
+++ /dev/null
@@ -1,38 +0,0 @@
- 'The package manifest is not valid JSON.']);
- }
- $manifest = $decoded;
- }
- foreach (['name', 'type', 'version'] as $field) {
- if (! filled(Arr::get($manifest, $field))) {
- throw ValidationException::withMessages(['manifest' => "Manifest field [$field] is required."]);
- }
- }
- if (! in_array($manifest['type'], config('marketplace.supported_types', []), true)) {
- throw ValidationException::withMessages(['manifest' => 'Unsupported marketplace package type.']);
- }
- $manifest['requires'] = $manifest['requires'] ?? [];
- $manifest['content_manager'] = $manifest['content_manager'] ?? '*';
- $manifest['laravel'] = $manifest['laravel'] ?? '*';
-
- return $manifest;
- }
-
- public function fromFile(string $path): array
- {
- return $this->parse((string) file_get_contents($path));
- }
-}
diff --git a/Marketplace/Services/PackageVerifier.php b/Marketplace/Services/PackageVerifier.php
deleted file mode 100644
index 1f909f7..0000000
--- a/Marketplace/Services/PackageVerifier.php
+++ /dev/null
@@ -1,28 +0,0 @@
- true, 'checksum' => $actual, 'signature_present' => filled($manifest['signature'] ?? null)];
- }
-}
diff --git a/Marketplace/Services/VersionConstraint.php b/Marketplace/Services/VersionConstraint.php
deleted file mode 100644
index 14e206c..0000000
--- a/Marketplace/Services/VersionConstraint.php
+++ /dev/null
@@ -1,47 +0,0 @@
-=') && version_compare($version, ($major + 1).'.0.0', '<');
-
- continue;
- }
- if (str_starts_with($part, '~')) {
- $base = substr($part, 1);
- $bits = array_pad(explode('.', $base), 3, '0');
- $upper = $bits[0].'.'.((int) $bits[1] + 1).'.0';
- $ok = $ok && version_compare($version, $base, '>=') && version_compare($version, $upper, '<');
-
- continue;
- }
- if (preg_match('/^(>=|<=|>|<|=)?\s*([0-9][0-9A-Za-z.\-+]*)$/', $part, $m)) {
- $ok = $ok && version_compare($version, $m[2], $m[1] ?: '=');
- } else {
- $ok = $ok && $version === $part;
- }
- }
- if ($ok) {
- return true;
- }
- }
-
- return false;
- }
-}
diff --git a/Marketplace/Tests/Feature/MarketplaceModuleTest.php b/Marketplace/Tests/Feature/MarketplaceModuleTest.php
deleted file mode 100644
index 663809b..0000000
--- a/Marketplace/Tests/Feature/MarketplaceModuleTest.php
+++ /dev/null
@@ -1,23 +0,0 @@
-parse(['name' => 'vendor/example', 'type' => 'module', 'version' => '1.0.0']);
- $this->assertSame('module', $manifest['type']);
- $this->assertSame([], $manifest['requires']);
- }
-
- public function test_current_platform_is_compatible_with_wildcard_manifest(): void
- {
- $result = app(CompatibilityChecker::class)->check(['php' => '*', 'laravel' => '*', 'content_manager' => '*']);
- $this->assertTrue($result['compatible']);
- }
-}
diff --git a/Marketplace/module.json b/Marketplace/module.json
deleted file mode 100644
index 936c435..0000000
--- a/Marketplace/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Marketplace","alias":"marketplace","version":"1.0.0","description":"Secure package catalogue, installation, updates, licences, publishers and rollback management.","keywords":["marketplace","modules","themes","extensions","packages"],"priority":170,"providers":["Modules\\Marketplace\\Providers\\MarketplaceServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit","Developer","Security"]}
diff --git a/Media/Config/config.php b/Media/Config/config.php
deleted file mode 100644
index ba65b69..0000000
--- a/Media/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Media',
- 'enabled' => true,
-];
diff --git a/Media/Database/Migrations/2026_07_23_000300_create_media_table.php b/Media/Database/Migrations/2026_07_23_000300_create_media_table.php
deleted file mode 100644
index 581799e..0000000
--- a/Media/Database/Migrations/2026_07_23_000300_create_media_table.php
+++ /dev/null
@@ -1,37 +0,0 @@
-id();
- $table->uuid('uuid')->unique();
- $table->nullableMorphs('attachable');
- $table->string('collection')->default('default')->index();
- $table->string('disk')->default('public');
- $table->string('path');
- $table->string('original_name');
- $table->string('mime_type')->nullable();
- $table->unsignedBigInteger('size')->default(0);
- $table->string('title')->nullable();
- $table->string('alt_text')->nullable();
- $table->text('caption')->nullable();
- $table->json('metadata')->nullable();
- $table->unsignedInteger('sort_order')->default(0);
- $table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- $table->softDeletes();
- $table->index(['attachable_type', 'attachable_id', 'collection', 'sort_order'], 'media_attachable_collection_index');
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('media');
- }
-};
diff --git a/Media/Http/Controllers/MediaController.php b/Media/Http/Controllers/MediaController.php
deleted file mode 100644
index ee5c15e..0000000
--- a/Media/Http/Controllers/MediaController.php
+++ /dev/null
@@ -1,34 +0,0 @@
- Media::query()->latest()->paginate(30)]);
- }
-
- public function store(Request $request)
- {
- $request->validate(['file' => ['required', 'file', 'max:20480'], 'title' => ['nullable', 'string', 'max:255'], 'alt_text' => ['nullable', 'string', 'max:255']]);
- $file = $request->file('file');
- $path = $file->store('media', 'public');
- Media::create(['disk' => 'public', 'path' => $path, 'original_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'size' => $file->getSize(), 'title' => $request->string('title')->toString() ?: pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME), 'alt_text' => $request->string('alt_text')->toString() ?: null, 'uploaded_by' => $request->user()->id]);
-
- return back()->with('status', 'Media uploaded.');
- }
-
- public function destroy(Media $medium)
- {
- Storage::disk($medium->disk)->delete($medium->path);
- $medium->delete();
-
- return back()->with('status', 'Media deleted.');
- }
-}
diff --git a/Media/Models/Media.php b/Media/Models/Media.php
deleted file mode 100644
index 56af7ee..0000000
--- a/Media/Models/Media.php
+++ /dev/null
@@ -1,26 +0,0 @@
- $m->uuid ??= (string) Str::uuid());
- }
-
- protected function casts(): array
- {
- return ['metadata' => 'array', 'size' => 'integer', 'sort_order' => 'integer'];
- }
-}
diff --git a/Media/Providers/MediaServiceProvider.php b/Media/Providers/MediaServiceProvider.php
deleted file mode 100644
index eee1f9a..0000000
--- a/Media/Providers/MediaServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Media/Providers/RouteServiceProvider.php b/Media/Providers/RouteServiceProvider.php
deleted file mode 100644
index ec522c8..0000000
--- a/Media/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Media', 'Routes/web.php'));
- });
- }
-}
diff --git a/Media/Resources/views/index.blade.php b/Media/Resources/views/index.blade.php
deleted file mode 100644
index e574feb..0000000
--- a/Media/Resources/views/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Media library Upload and manage reusable files.
@if(session('status'))
{{ session('status') }}
@endif
@csrfUpload
@forelse($media as $item)
@if(str_starts_with((string)$item->mime_type,'image/')) @else
@endif{{ $item->title ?: $item->original_name }}
{{ Number::fileSize($item->size) }}
@csrf @method('DELETE')Delete @empty
No media uploaded.
@endforelse
{{ $media->links() }}
diff --git a/Media/Routes/web.php b/Media/Routes/web.php
deleted file mode 100644
index 172cab2..0000000
--- a/Media/Routes/web.php
+++ /dev/null
@@ -1,10 +0,0 @@
-prefix('administration/media')->name('administration.media.')->group(function (): void {
- Route::get('/', [MediaController::class, 'index'])->name('index');
- Route::post('/', [MediaController::class, 'store'])->name('store');
- Route::delete('/{medium}', [MediaController::class, 'destroy'])->name('destroy');
-});
diff --git a/Media/Support/ModuleInfo.php b/Media/Support/ModuleInfo.php
deleted file mode 100644
index 91bfbd1..0000000
--- a/Media/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Media');
-});
diff --git a/Media/composer.json b/Media/composer.json
deleted file mode 100644
index 3d22a93..0000000
--- a/Media/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/media-module",
- "description": "Media foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Media\\": ""
- }
- }
-}
diff --git a/Media/module.json b/Media/module.json
deleted file mode 100644
index 23a97df..0000000
--- a/Media/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Media",
- "alias": "media",
- "version": "1.0.0",
- "description": "Media foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "media"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Media\\Providers\\MediaServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Menus/Config/config.php b/Menus/Config/config.php
deleted file mode 100644
index 634ea78..0000000
--- a/Menus/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Menus'];
diff --git a/Menus/Database/Migrations/2026_07_24_000200_create_menus_table.php b/Menus/Database/Migrations/2026_07_24_000200_create_menus_table.php
deleted file mode 100644
index 6ab61c5..0000000
--- a/Menus/Database/Migrations/2026_07_24_000200_create_menus_table.php
+++ /dev/null
@@ -1,26 +0,0 @@
-id();
- $table->string('name');
- $table->string('location');
- $table->json('items')->nullable();
- $table->boolean('enabled')->default(true);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('menus');
- }
-};
diff --git a/Menus/Http/Controllers/MenuController.php b/Menus/Http/Controllers/MenuController.php
deleted file mode 100644
index 3154304..0000000
--- a/Menus/Http/Controllers/MenuController.php
+++ /dev/null
@@ -1,90 +0,0 @@
- Menu::query()->orderBy('location')->paginate(30)]);
- }
-
- public function create(): View
- {
- return view('menus::admin.form', ['locations' => $this->locations()]);
- }
-
- public function store(Request $request): RedirectResponse
- {
- Menu::create($this->validated($request));
-
- return redirect()->route('administration.menus.index')->with('status', 'Menu created. Every header or footer using this location has been updated.');
- }
-
- public function edit(Menu $menu): View
- {
- return view('menus::admin.form', ['record' => $menu, 'locations' => $this->locations()]);
- }
-
- public function update(Request $request, Menu $menu): RedirectResponse
- {
- $menu->update($this->validated($request));
-
- return redirect()->route('administration.menus.index')->with('status', 'Menu updated everywhere it is used.');
- }
-
- public function destroy(Menu $menu): RedirectResponse
- {
- $menu->delete();
-
- return back()->with('status', 'Menu deleted. Components using its location will use their safe fallback.');
- }
-
- /** @return array */
- private function validated(Request $request): array
- {
- $data = $request->validate([
- 'name' => ['required', 'string', 'max:255'],
- 'location' => ['required', 'string', 'max:100', 'regex:/^[a-z0-9_-]+$/'],
- 'items_json' => ['nullable', 'string'],
- 'enabled' => ['nullable', 'boolean'],
- ]);
-
- try {
- $items = json_decode($data['items_json'] ?: '[]', true, 512, JSON_THROW_ON_ERROR);
- } catch (JsonException) {
- throw ValidationException::withMessages(['items_json' => 'The menu items could not be saved. Please reload the editor and try again.']);
- }
-
- if (! is_array($items)) {
- throw ValidationException::withMessages(['items_json' => 'Menu items must be a list.']);
- }
-
- return [
- 'name' => $data['name'],
- 'location' => $data['location'],
- 'items' => array_values($items),
- 'enabled' => $request->boolean('enabled'),
- ];
- }
-
- /** @return array */
- private function locations(): array
- {
- return [
- 'primary' => 'Primary navigation',
- 'footer' => 'Footer — Explore',
- 'footer_account' => 'Footer — Account',
- 'footer_legal' => 'Footer — Legal',
- 'mobile' => 'Mobile navigation override',
- ];
- }
-}
diff --git a/Menus/Models/Menu.php b/Menus/Models/Menu.php
deleted file mode 100644
index 0f957c6..0000000
--- a/Menus/Models/Menu.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'array',
- 'enabled' => 'boolean',
- ];
- }
-}
diff --git a/Menus/Providers/MenusServiceProvider.php b/Menus/Providers/MenusServiceProvider.php
deleted file mode 100644
index 9572a1a..0000000
--- a/Menus/Providers/MenusServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Menus', 'Config/config.php'), 'menus');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Menus', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Menus', 'Resources/views'), 'menus');
- }
-}
diff --git a/Menus/Providers/RouteServiceProvider.php b/Menus/Providers/RouteServiceProvider.php
deleted file mode 100644
index 2e19213..0000000
--- a/Menus/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Menus', 'Routes/web.php')));
- }
-}
diff --git a/Menus/Resources/views/admin/form.blade.php b/Menus/Resources/views/admin/form.blade.php
deleted file mode 100644
index 73a9d39..0000000
--- a/Menus/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,57 +0,0 @@
-
- @php($initialItems = old('items_json', json_encode($record->items ?? [], JSON_UNESCAPED_SLASHES)))
-
-
-
Reusable navigation
-
{{ isset($record) ? 'Edit' : 'Create' }} menu
-
Edit links once. Every header and footer assigned to this menu location updates automatically.
-
-
-
- @csrf
- @isset($record) @method('PUT') @endisset
-
-
-
-
- Menu items Relative paths such as /blog, anchors and full external URLs are supported.
Add link
-
- @error('items_json'){{ $message }}
@enderror
-
-
-
-
-
-
-
-
- @push('scripts')
-
- @endpush
-
diff --git a/Menus/Resources/views/admin/index.blade.php b/Menus/Resources/views/admin/index.blade.php
deleted file mode 100644
index db7b2a4..0000000
--- a/Menus/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
Single source of navigation
Menus Reusable menus update every header and footer that references their location.
- @if(session('status'))
{{ session('status') }}
@endif
-
- @forelse($records as $record)
-
{{ $record->location }} {{ $record->name }} {{ count($record->items ?? []) }} links · {{ $record->enabled ? 'Enabled' : 'Disabled' }}
- @empty
-
No menus yet Run the Saasland installer or create your first reusable menu.
- @endforelse
-
- {{ $records->links() }}
-
-
diff --git a/Menus/Routes/web.php b/Menus/Routes/web.php
deleted file mode 100644
index f76c43f..0000000
--- a/Menus/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/menus')->name('administration.menus.')->group(function (): void {
- Route::get('/', [MenuController::class, 'index'])->name('index');
- Route::get('/create', [MenuController::class, 'create'])->name('create');
- Route::post('/', [MenuController::class, 'store'])->name('store');
- Route::get('/{menu}/edit', [MenuController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{menu}', [MenuController::class, 'update'])->name('update');
- Route::delete('/{menu}', [MenuController::class, 'destroy'])->name('destroy');
-});
diff --git a/Menus/Support/ModuleInfo.php b/Menus/Support/ModuleInfo.php
deleted file mode 100644
index e96b94b..0000000
--- a/Menus/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-create(['name' => 'Primary', 'location' => 'primary', 'enabled' => true, 'items' => [['enabled' => true, 'label' => 'Home', 'url' => '/']]]);
- expect(app(\Modules\Stylist\Support\FrontendNavigation::class)->primary())->toHaveCount(1)->and(app(\Modules\Stylist\Support\FrontendNavigation::class)->primary()[0]['label'])->toBe('Home');
-});
diff --git a/Menus/Tests/Feature/ModuleBootTest.php b/Menus/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 516d963..0000000
--- a/Menus/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,7 +0,0 @@
-toBeTrue();
-});
diff --git a/Menus/module.json b/Menus/module.json
deleted file mode 100644
index 6474504..0000000
--- a/Menus/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Menus",
- "alias": "menus",
- "version": "1.0.0",
- "description": "Menus CMS module.",
- "keywords": [
- "cms",
- "menus"
- ],
- "priority": 22,
- "providers": [
- "Modules\\Menus\\Providers\\MenusServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/News/Config/config.php b/News/Config/config.php
deleted file mode 100644
index 3909f94..0000000
--- a/News/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'News'];
diff --git a/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php b/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php
deleted file mode 100644
index d939ffc..0000000
--- a/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php
+++ /dev/null
@@ -1,40 +0,0 @@
-id();
- $table->foreignId('author_id')->nullable()->constrained('authors')->nullOnDelete();
- $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
- $table->string('title');
- $table->string('slug')->unique();
- $table->text('excerpt')->nullable();
- $table->longText('content')->nullable();
- $table->string('featured_image')->nullable();
- $table->string('status')->default('draft')->index();
- $table->boolean('is_featured')->default(false);
- $table->timestamp('published_at')->nullable()->index();
- $table->timestamp('expires_at')->nullable();
- $table->string('meta_title')->nullable();
- $table->text('meta_description')->nullable();
- $table->timestamps();
- });
- Schema::create('news_article_tag', function (Blueprint $table): void {
- $table->foreignId('news_article_id')->constrained('news_articles')->cascadeOnDelete();
- $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete();
- $table->primary(['news_article_id', 'tag_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('news_article_tag');
- Schema::dropIfExists('news_articles');
- }
-};
diff --git a/News/Http/Controllers/NewsController.php b/News/Http/Controllers/NewsController.php
deleted file mode 100644
index cc69cc2..0000000
--- a/News/Http/Controllers/NewsController.php
+++ /dev/null
@@ -1,66 +0,0 @@
- News::with(['author', 'category'])->latest()->paginate(25)]);
- }
-
- public function create(): View
- {
- return view('news::admin.form', $this->lookups());
- }
-
- public function store(Request $r): RedirectResponse
- {
- $record = News::create($this->data($r));
- $record->tags()->sync($r->input('tag_ids', []));
-
- return redirect()->route('administration.news.index')->with('status', 'News created.');
- }
-
- public function edit(News $news_article): View
- {
- return view('news::admin.form', array_merge($this->lookups(), ['record' => $news_article]));
- }
-
- public function update(Request $r, News $news_article): RedirectResponse
- {
- $news_article->update($this->data($r));
- $news_article->tags()->sync($r->input('tag_ids', []));
-
- return redirect()->route('administration.news.index')->with('status', 'News updated.');
- }
-
- public function destroy(News $news_article): RedirectResponse
- {
- $news_article->delete();
-
- return back()->with('status', 'News deleted.');
- }
-
- private function lookups(): array
- {
- return ['authors' => Author::where('is_active', true)->orderBy('name')->get(), 'categories' => Category::where('is_active', true)->orderBy('name')->get(), 'tags' => Tag::orderBy('name')->get()];
- }
-
- private function data(Request $r): array
- {
- $d = $r->validate(['author_id' => ['nullable', 'exists:authors,id'], 'category_id' => ['nullable', 'exists:categories,id'], 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'excerpt' => ['nullable', 'string'], 'content' => ['nullable', 'string'], 'featured_image' => ['nullable', 'string', 'max:255'], 'status' => ['required', 'in:draft,review,published,archived'], 'is_featured' => ['sometimes', 'boolean'], 'published_at' => ['nullable', 'date'], 'expires_at' => ['nullable', 'date', 'after:published_at'], 'meta_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string']]);
- $d['is_featured'] = $r->boolean('is_featured');
-
- return $d;
- }
-}
diff --git a/News/Http/Controllers/PublicNewsController.php b/News/Http/Controllers/PublicNewsController.php
deleted file mode 100644
index 5783072..0000000
--- a/News/Http/Controllers/PublicNewsController.php
+++ /dev/null
@@ -1,20 +0,0 @@
- News::published()->with(['author', 'category'])->latest('published_at')->paginate(12)]);
- }
-
- public function show(string $slug): View
- {
- return view('news::public.show', ['record' => News::published()->with(['author', 'category', 'tags'])->where('slug', $slug)->firstOrFail()]);
- }
-}
diff --git a/News/Models/News.php b/News/Models/News.php
deleted file mode 100644
index 586c952..0000000
--- a/News/Models/News.php
+++ /dev/null
@@ -1,48 +0,0 @@
- 'boolean', 'published_at' => 'datetime', 'expires_at' => 'datetime'];
- }
-
- public function author(): BelongsTo
- {
- return $this->belongsTo(Author::class);
- }
-
- public function category(): BelongsTo
- {
- return $this->belongsTo(Category::class);
- }
-
- public function tags(): BelongsToMany
- {
- return $this->belongsToMany(Tag::class, 'news_article_tag');
- }
-
- public function scopePublished(Builder $query): Builder
- {
- return $query->where('status', 'published')->where(fn (Builder $q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->where(fn (Builder $q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()));
- }
-}
diff --git a/News/Providers/NewsServiceProvider.php b/News/Providers/NewsServiceProvider.php
deleted file mode 100644
index 959998d..0000000
--- a/News/Providers/NewsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('News', 'Config/config.php'), 'news');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('News', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('News', 'Resources/views'), 'news');
- }
-}
diff --git a/News/Providers/RouteServiceProvider.php b/News/Providers/RouteServiceProvider.php
deleted file mode 100644
index cfa0850..0000000
--- a/News/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('News', 'Routes/web.php'));
- }
-}
diff --git a/News/Resources/views/admin/form.blade.php b/News/Resources/views/admin/form.blade.php
deleted file mode 100644
index 2e122f0..0000000
--- a/News/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-@csrf @isset($record) @method('PUT') @endisset None @foreach($authors as $author)author_id ?? null)==$author->id)>{{ $author->name }} @endforeachNone @foreach($categories as $category)category_id ?? null)==$category->id)>{{ $category->name }} @endforeach@foreach(['draft','review','published','archived'] as $status)status ?? 'draft')===$status)>{{ $status }} @endforeach
{{ old('excerpt',$record->excerpt ?? '') }} {{ old('content',$record->content ?? '') }} {{ old('meta_description',$record->meta_description ?? '') }} Save
diff --git a/News/Resources/views/admin/index.blade.php b/News/Resources/views/admin/index.blade.php
deleted file mode 100644
index f8860b7..0000000
--- a/News/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-News Manage publishing news.
Create @if(session('status')){{ session('status') }}
@endifName / title Status Actions @forelse($records as $record){{ $record->title ?? $record->name }} {{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
diff --git a/News/Resources/views/public/index.blade.php b/News/Resources/views/public/index.blade.php
deleted file mode 100644
index 5413c83..0000000
--- a/News/Resources/views/public/index.blade.php
+++ /dev/null
@@ -1,31 +0,0 @@
-@extends('stylist-theme-saasland::layouts.app')
-
-@php
- $title = 'News';
- $description = 'The latest announcements, releases and company updates.';
- $eyebrow = 'Updates';
- $breadcrumbs = ['News' => null];
-@endphp
-
-@section('content')
-
-
-
- @forelse($records as $record)
-
-
-
-
{{ optional($record->published_at)->format('j F Y') ?: 'Latest update' }}
-
- @if($record->excerpt)
{{ $record->excerpt }}
@endif
-
Read update →
-
-
- @empty
-
No news has been published yet.
- @endforelse
-
-
-
-
-@endsection
diff --git a/News/Resources/views/public/show.blade.php b/News/Resources/views/public/show.blade.php
deleted file mode 100644
index ddb3795..0000000
--- a/News/Resources/views/public/show.blade.php
+++ /dev/null
@@ -1,23 +0,0 @@
-@extends('stylist-theme-saasland::layouts.app')
-
-@php
- $title = $record->title;
- $description = $record->excerpt;
- $eyebrow = 'News';
- $breadcrumbs = ['News' => route('news.public.index'), $record->title => null];
-@endphp
-
-@section('content')
-
-
-
-
- @if($record->author){{ $record->author->name }} @endif
- @if($record->published_at){{ $record->published_at->format('j F Y') }} @endif
-
- @if($record->excerpt){{ $record->excerpt }}
@endif
- {!! nl2br(e($record->content)) !!}
-
-
-
-@endsection
diff --git a/News/Routes/web.php b/News/Routes/web.php
deleted file mode 100644
index 672eba1..0000000
--- a/News/Routes/web.php
+++ /dev/null
@@ -1,16 +0,0 @@
-prefix('administration/news')->name('administration.news.')->group(function (): void {
- Route::get('/', [NewsController::class, 'index'])->name('index');
- Route::get('/create', [NewsController::class, 'create'])->name('create');
- Route::post('/', [NewsController::class, 'store'])->name('store');
- Route::get('/{news_article}/edit', [NewsController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{news_article}', [NewsController::class, 'update'])->name('update');
- Route::delete('/{news_article}', [NewsController::class, 'destroy'])->name('destroy');
-});
-Route::get('/news', [PublicNewsController::class, 'index'])->name('news.public.index');
-Route::get('/news/{slug}', [PublicNewsController::class, 'show'])->name('news.public.show');
diff --git a/News/Tests/Feature/ModuleBootTest.php b/News/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index dbe1c8a..0000000
--- a/News/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('News', config('news.name'));
- }
-}
diff --git a/News/module.json b/News/module.json
deleted file mode 100644
index dcb3490..0000000
--- a/News/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "News",
- "alias": "news",
- "version": "1.0.0",
- "description": "News publishing module.",
- "keywords": ["publishing", "news"],
- "priority": 30,
- "providers": ["Modules\\News\\Providers\\NewsServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Newsletter/Config/config.php b/Newsletter/Config/config.php
deleted file mode 100644
index 293dea2..0000000
--- a/Newsletter/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Newsletter'];
diff --git a/Newsletter/Database/Migrations/2026_07_24_04100_create_newsletter_tables.php b/Newsletter/Database/Migrations/2026_07_24_04100_create_newsletter_tables.php
deleted file mode 100644
index 613856a..0000000
--- a/Newsletter/Database/Migrations/2026_07_24_04100_create_newsletter_tables.php
+++ /dev/null
@@ -1,57 +0,0 @@
-id();
- $t->string('name');
- $t->text('description')->nullable();
- $t->boolean('is_active')->default(true);
- $t->timestamps();
- });
- Schema::create('newsletter_subscribers', function (Blueprint $t): void {
- $t->id();
- $t->string('email')->unique();
- $t->string('name')->nullable();
- $t->string('status')->default('pending')->index();
- $t->string('confirmation_token')->nullable()->unique();
- $t->timestamp('confirmed_at')->nullable();
- $t->timestamp('unsubscribed_at')->nullable();
- $t->json('metadata')->nullable();
- $t->timestamps();
- });
- Schema::create('newsletter_list_subscriber', function (Blueprint $t): void {
- $t->foreignId('list_id')->constrained('newsletter_lists')->cascadeOnDelete();
- $t->foreignId('subscriber_id')->constrained('newsletter_subscribers')->cascadeOnDelete();
- $t->primary(['list_id', 'subscriber_id']);
- });
- Schema::create('newsletter_campaigns', function (Blueprint $t): void {
- $t->id();
- $t->foreignId('list_id')->nullable()->constrained('newsletter_lists')->nullOnDelete();
- $t->string('name');
- $t->string('subject');
- $t->longText('content')->nullable();
- $t->string('status')->default('draft')->index();
- $t->timestamp('scheduled_at')->nullable();
- $t->timestamp('sent_at')->nullable();
- $t->unsignedInteger('sent_count')->default(0);
- $t->unsignedInteger('open_count')->default(0);
- $t->unsignedInteger('click_count')->default(0);
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('newsletter_campaigns');
- Schema::dropIfExists('newsletter_list_subscriber');
- Schema::dropIfExists('newsletter_subscribers');
- Schema::dropIfExists('newsletter_lists');
- }
-};
diff --git a/Newsletter/Http/Controllers/NewsletterController.php b/Newsletter/Http/Controllers/NewsletterController.php
deleted file mode 100644
index 8e37e2c..0000000
--- a/Newsletter/Http/Controllers/NewsletterController.php
+++ /dev/null
@@ -1,52 +0,0 @@
- Subscriber::latest()->paginate(20), 'campaigns' => Campaign::latest()->limit(10)->get(), 'lists' => MailingList::orderBy('name')->get()]);
- }
-
- public function storeSubscriber(Request $r): RedirectResponse
- {
- $d = $r->validate(['email' => ['required', 'email', 'unique:newsletter_subscribers,email'], 'name' => ['nullable', 'string', 'max:255']]);
- Subscriber::create($d + ['status' => 'confirmed', 'confirmation_token' => Str::random(48), 'confirmed_at' => now()]);
-
- return back()->with('status', 'Subscriber added.');
- }
-
- public function storeCampaign(Request $r): RedirectResponse
- {
- Campaign::create($r->validate(['list_id' => ['nullable', 'exists:newsletter_lists,id'], 'name' => ['required', 'string', 'max:255'], 'subject' => ['required', 'string', 'max:255'], 'content' => ['nullable', 'string'], 'status' => ['required', 'in:draft,scheduled,sent'], 'scheduled_at' => ['nullable', 'date']]));
-
- return back()->with('status', 'Campaign created.');
- }
-
- public function publicSubscribe(Request $r): RedirectResponse
- {
- $data = $r->validate(['email' => ['required', 'email', 'max:255'], 'name' => ['nullable', 'string', 'max:255']]);
- Subscriber::query()->updateOrCreate(
- ['email' => strtolower($data['email'])],
- ['name' => $data['name'] ?? null, 'status' => 'confirmed', 'confirmation_token' => Str::random(48), 'confirmed_at' => now()]
- );
-
- return back()->with('status', 'Thank you for subscribing.');
- }
-
- public function destroySubscriber(Subscriber $subscriber): RedirectResponse
- {
- $subscriber->delete();
-
- return back()->with('status', 'Subscriber deleted.');
- }
-}
diff --git a/Newsletter/Models/Campaign.php b/Newsletter/Models/Campaign.php
deleted file mode 100644
index 924a6a8..0000000
--- a/Newsletter/Models/Campaign.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'datetime', 'sent_at' => 'datetime'];
- }
-}
diff --git a/Newsletter/Models/MailingList.php b/Newsletter/Models/MailingList.php
deleted file mode 100644
index 786b233..0000000
--- a/Newsletter/Models/MailingList.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'boolean'];
- }
-}
diff --git a/Newsletter/Models/Subscriber.php b/Newsletter/Models/Subscriber.php
deleted file mode 100644
index 9d0bd30..0000000
--- a/Newsletter/Models/Subscriber.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'datetime', 'unsubscribed_at' => 'datetime', 'metadata' => 'array'];
- }
-}
diff --git a/Newsletter/Providers/NewsletterServiceProvider.php b/Newsletter/Providers/NewsletterServiceProvider.php
deleted file mode 100644
index d10726f..0000000
--- a/Newsletter/Providers/NewsletterServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Newsletter', 'Config/config.php'), 'newsletter');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Newsletter', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Newsletter', 'Resources/views'), 'newsletter');
- }
-}
diff --git a/Newsletter/Providers/RouteServiceProvider.php b/Newsletter/Providers/RouteServiceProvider.php
deleted file mode 100644
index 47472fc..0000000
--- a/Newsletter/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Newsletter', 'Routes/web.php'));
- }
-}
diff --git a/Newsletter/Resources/views/admin/index.blade.php b/Newsletter/Resources/views/admin/index.blade.php
deleted file mode 100644
index 93c0489..0000000
--- a/Newsletter/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Newsletter Manage subscribers and email campaigns.
@if(session('status'))
{{ session('status') }}
@endif
@csrfAdd subscriber Add subscriber @csrfCreate campaign Draft Scheduled Create campaign Subscriber Status Confirmed @forelse($subscribers as $subscriber){{ $subscriber->email }}
{{ $subscriber->name }}
{{ ucfirst($subscriber->status) }} {{ optional($subscriber->confirmed_at)->format('j M Y') ?: '—' }} @csrf @method('DELETE')Delete @emptyNo subscribers yet. @endforelse
{{ $subscribers->links() }}
diff --git a/Newsletter/Routes/web.php b/Newsletter/Routes/web.php
deleted file mode 100644
index 045e8f9..0000000
--- a/Newsletter/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/newsletter')->name('administration.newsletter.')->group(function (): void {
- Route::get('/', [NewsletterController::class, 'index'])->name('index');
- Route::post('/subscribers', [NewsletterController::class, 'storeSubscriber'])->name('subscribers.store');
- Route::delete('/subscribers/{subscriber}', [NewsletterController::class, 'destroySubscriber'])->name('subscribers.destroy');
- Route::post('/campaigns', [NewsletterController::class, 'storeCampaign'])->name('campaigns.store');
-});
-
-Route::post('/newsletter/subscribe', [NewsletterController::class, 'publicSubscribe'])->middleware('throttle:10,1')->name('newsletter.public.subscribe');
diff --git a/Newsletter/Tests/Feature/ModuleBootTest.php b/Newsletter/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index d76ba2b..0000000
--- a/Newsletter/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Newsletter', config('newsletter.name'));
- }
-}
diff --git a/Newsletter/module.json b/Newsletter/module.json
deleted file mode 100644
index 620f465..0000000
--- a/Newsletter/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Newsletter",
- "alias": "newsletter",
- "version": "1.0.0",
- "description": "Subscriber lists and email campaign management.",
- "keywords": ["business", "newsletter"],
- "priority": 41,
- "providers": ["Modules\\Newsletter\\Providers\\NewsletterServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Pages/Config/config.php b/Pages/Config/config.php
deleted file mode 100644
index 68aabbf..0000000
--- a/Pages/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Pages'];
diff --git a/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php b/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php
deleted file mode 100644
index d4cb696..0000000
--- a/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php
+++ /dev/null
@@ -1,29 +0,0 @@
-id();
- $table->string('title');
- $table->string('slug');
- $table->text('summary')->nullable();
- $table->longText('content')->nullable();
- $table->string('status')->default('draft');
- $table->timestamp('published_at')->nullable();
- $table->boolean('is_home')->default(false);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('pages');
- }
-};
diff --git a/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php b/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php
deleted file mode 100644
index ac8810e..0000000
--- a/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php
+++ /dev/null
@@ -1,37 +0,0 @@
-string('template')->default('default');
- $table->timestamp('expires_at')->nullable();
- $table->string('preview_token', 64)->nullable()->unique();
- $table->foreignId('locked_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamp('locked_at')->nullable();
- });
- Schema::create('page_revisions', function (Blueprint $table): void {
- $table->id();
- $table->foreignId('page_id')->constrained('pages')->cascadeOnDelete();
- $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
- $table->string('title');
- $table->longText('content')->nullable();
- $table->json('snapshot');
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('page_revisions');
- Schema::table('pages', function (Blueprint $table): void {
- $table->dropConstrainedForeignId('locked_by');
- $table->dropColumn(['template', 'expires_at', 'preview_token', 'locked_at']);
- });
- }
-};
diff --git a/Pages/Http/Controllers/PageController.php b/Pages/Http/Controllers/PageController.php
deleted file mode 100644
index f9f2e02..0000000
--- a/Pages/Http/Controllers/PageController.php
+++ /dev/null
@@ -1,151 +0,0 @@
-withCount('contentPlacements')
- ->when($request->string('search')->isNotEmpty(), function ($query) use ($request): void {
- $search = $request->string('search')->toString();
- $query->where(fn ($page) => $page
- ->where('title', 'like', "%{$search}%")
- ->orWhere('slug', 'like', "%{$search}%")
- ->orWhere('summary', 'like', "%{$search}%"));
- })
- ->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')->toString()))
- ->orderByDesc('is_home')
- ->orderBy('title')
- ->paginate(24)
- ->withQueryString();
-
- return view('pages::admin.index', compact('records'));
- }
-
- public function create(): View
- {
- return view('pages::admin.form', ['record' => new Page(['status' => 'draft', 'template' => 'default'])]);
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $this->validated($request);
- $data['slug'] = $this->uniqueSlug($data['slug'] ?: $data['title']);
- $data['is_home'] = $request->boolean('is_home');
- $data['published_at'] = $data['status'] === 'published' ? ($data['published_at'] ?? now()) : null;
-
- $page = Page::query()->create($data);
- $this->enforceSingleHomepage($page);
-
- return redirect()->route('administration.pages.edit', $page)->with('status', 'Page created. Add content sections when you are ready.');
- }
-
- public function edit(Page $page): View
- {
- $page->loadCount('contentPlacements');
-
- return view('pages::admin.form', ['record' => $page]);
- }
-
- public function update(Request $request, Page $page): RedirectResponse
- {
- $oldSlug = $page->slug;
- $data = $this->validated($request, $page);
- $data['slug'] = $this->uniqueSlug($data['slug'] ?: $data['title'], $page);
- $data['is_home'] = $request->boolean('is_home');
- $data['published_at'] = $data['status'] === 'published' ? ($data['published_at'] ?? $page->published_at ?? now()) : null;
-
- $page->update($data);
- $this->enforceSingleHomepage($page);
-
- if ($oldSlug !== $page->slug) {
- PageContent::query()->where('page_id', $page->id)->orWhere('page_key', $oldSlug)->update(['page_key' => $page->slug, 'page_id' => $page->id]);
- }
-
- return back()->with('status', 'Page saved.');
- }
-
- public function duplicate(Request $request, Page $page): RedirectResponse
- {
- $copy = $page->replicate(['slug', 'status', 'published_at', 'preview_token', 'locked_by', 'locked_at', 'is_home']);
- $copy->title = $page->title.' copy';
- $copy->slug = $this->uniqueSlug($copy->title);
- $copy->status = 'draft';
- $copy->published_at = null;
- $copy->is_home = false;
- $copy->save();
-
- foreach ($page->contentPlacements()->orderBy('region')->orderBy('sort_order')->get() as $placement) {
- $copy->contentPlacements()->create([
- 'page_key' => $copy->slug,
- 'content_document_id' => $placement->content_document_id,
- 'region' => $placement->region,
- 'sort_order' => $placement->sort_order,
- 'is_enabled' => $placement->is_enabled,
- 'visibility_rules' => $placement->visibility_rules,
- 'presentation' => $placement->presentation,
- 'starts_at' => $placement->starts_at,
- 'ends_at' => $placement->ends_at,
- ]);
- }
-
- return redirect()->route('administration.pages.edit', $copy)->with('status', 'Draft copy created with the same content structure.');
- }
-
- public function destroy(Page $page): RedirectResponse
- {
- if ($page->is_home) {
- return back()->withErrors(['page' => 'Choose another homepage before deleting the current homepage.']);
- }
-
- $page->delete();
-
- return redirect()->route('administration.pages.index')->with('status', 'Page moved to the bin.');
- }
-
- private function validated(Request $request, ?Page $page = null): array
- {
- return $request->validate([
- 'title' => ['required', 'string', 'max:255'],
- 'slug' => ['nullable', 'string', 'max:255', Rule::unique('pages', 'slug')->ignore($page?->id)->whereNull('deleted_at')],
- 'summary' => ['nullable', 'string', 'max:1000'],
- 'content' => ['nullable', 'string'],
- 'status' => ['required', Rule::in(['draft', 'published'])],
- 'template' => ['required', Rule::in(['default', 'landing', 'full-width'])],
- 'published_at' => ['nullable', 'date'],
- 'expires_at' => ['nullable', 'date', 'after:published_at'],
- 'is_home' => ['sometimes', 'boolean'],
- ]);
- }
-
- private function uniqueSlug(string $value, ?Page $except = null): string
- {
- $base = Str::slug($value) ?: 'page';
- $slug = $base;
- $suffix = 2;
-
- while (Page::withTrashed()->where('slug', $slug)->when($except, fn ($query) => $query->whereKeyNot($except->getKey()))->exists()) {
- $slug = $base.'-'.$suffix++;
- }
-
- return $slug;
- }
-
- private function enforceSingleHomepage(Page $page): void
- {
- if ($page->is_home) {
- Page::query()->whereKeyNot($page->id)->where('is_home', true)->update(['is_home' => false]);
- }
- }
-}
diff --git a/Pages/Http/Controllers/PublicPageController.php b/Pages/Http/Controllers/PublicPageController.php
deleted file mode 100644
index c9e531f..0000000
--- a/Pages/Http/Controllers/PublicPageController.php
+++ /dev/null
@@ -1,17 +0,0 @@
-where('slug', $slug)->where('status', 'published')->where(fn ($q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->firstOrFail();
-
- return view('pages::public.show', compact('page'));
- }
-}
diff --git a/Pages/Models/Page.php b/Pages/Models/Page.php
deleted file mode 100644
index 22e2d3f..0000000
--- a/Pages/Models/Page.php
+++ /dev/null
@@ -1,35 +0,0 @@
- 'datetime',
- 'expires_at' => 'datetime',
- 'is_home' => 'boolean',
- ];
- }
-
- public function contentPlacements(): HasMany
- {
- return $this->hasMany(PageContent::class)->orderBy('region')->orderBy('sort_order');
- }
-}
diff --git a/Pages/Providers/PagesServiceProvider.php b/Pages/Providers/PagesServiceProvider.php
deleted file mode 100644
index 0a1d101..0000000
--- a/Pages/Providers/PagesServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Pages', 'Config/config.php'), 'pages');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Pages', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Pages', 'Resources/views'), 'pages');
- }
-}
diff --git a/Pages/Providers/RouteServiceProvider.php b/Pages/Providers/RouteServiceProvider.php
deleted file mode 100644
index 7984a9e..0000000
--- a/Pages/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Pages', 'Routes/web.php')));
- }
-}
diff --git a/Pages/Resources/views/admin/form.blade.php b/Pages/Resources/views/admin/form.blade.php
deleted file mode 100644
index 3a513d2..0000000
--- a/Pages/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
-
← Pages
-
-
{{ $record->exists ? $record->title : 'New page' }}
- @if($record->exists && $record->is_home)Homepage @endif
-
-
Page details, search metadata, publishing and content structure.
-
- @if($record->exists)
-
- @if($record->status === 'published')Open live page @endif
- Edit content
-
@csrfDuplicate
-
- @endif
-
-
- @if(session('status'))
{{ session('status') }}
@endif
- @if($errors->any())
@foreach($errors->all() as $error){{ $error }} @endforeach @endif
-
-
-
- Page details
- SEO & URL
- Publishing
-
-
-
-
- @csrf @if($record->exists) @method('PUT') @endif
-
-
-
- Page identity Use a clear title editors and visitors can recognise.
-
- {{ old('summary', $record->summary) }}
-
-
-
- Content structure Build the page with ordered, reusable sections in the visual composer.
@if($record->exists)
{{ $record->content_placements_count ?? 0 }} sections @endif
- @if($record->exists)
- Page composer Add hero areas, text, feeds, calls to action and shared site sections, then drag them into order.
Open content composer
- @else
- Save the page first, then add and arrange its content sections.
- @endif
- Legacy body content Kept for compatibility with older templates. New pages should use the content composer.
{{ old('content', $record->content) }}
-
-
-
-
-
- URL and presentation Keep URLs short, readable and stable.
-
- template ?: 'default') === 'default')>Default template) === 'landing')>Landing page template) === 'full-width')>Full width
-
- Search preview {{ url('/') }}/
{{ old('title', $record->title ?: 'Page title') }}
{{ Str::limit(old('summary', $record->summary ?: 'Add a concise summary to improve how this page appears in search and content listings.'), 160) }}
-
-
-
-
-
-
-
Page status {{ ucfirst($record->status ?: 'draft') }} @if($record->exists)
Sections {{ $record->content_placements_count ?? 0 }}
Updated {{ $record->updated_at?->diffForHumans() }}
Template {{ ucfirst($record->template ?? 'default') }} @endif{{ $record->exists ? 'Save page' : 'Create page' }}
- @if($record->exists && !$record->is_home)Danger zone Deleting a page also removes its page placements, but reusable content documents remain available.
@csrf @method('DELETE')Delete page @endif
-
-
-
-
-
-
diff --git a/Pages/Resources/views/admin/index.blade.php b/Pages/Resources/views/admin/index.blade.php
deleted file mode 100644
index 1858e3a..0000000
--- a/Pages/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
Website
-
Pages
-
Create, organise, compose and publish every page from one place.
-
-
- Content library
- New page
-
-
-
- @if(session('status'))
-
{{ session('status') }}
- @endif
- @if($errors->any())
-
{{ $errors->first() }}
- @endif
-
-
-
-
- All pages
- Draft
- Published
-
- Filter Reset
-
-
-
- @forelse($records as $record)
- @php
- $isLive = $record->status === 'published' && (!$record->published_at || $record->published_at->isPast()) && (!$record->expires_at || $record->expires_at->isFuture());
- @endphp
-
-
-
-
-
{{ $record->title }}
- @if($record->is_home)Homepage @endif
- {{ $isLive ? 'Live' : ucfirst($record->status) }}
-
-
/{{ $record->slug }}
-
-
-
{{ $record->content_placements_count }}
-
sections
-
-
-
- {{ Str::limit($record->summary ?: 'No page summary has been added yet.', 150) }}
-
-
-
Updated {{ $record->updated_at?->diffForHumans() }} · {{ ucfirst($record->template ?? 'default') }}
-
- @if($record->status === 'published')View @endif
- Content
- Edit page
-
-
-
- @empty
-
-
No pages found
-
Create your first page or clear the current filters.
-
Create page
-
- @endforelse
-
-
- {{ $records->links() }}
-
-
diff --git a/Pages/Resources/views/public/show.blade.php b/Pages/Resources/views/public/show.blade.php
deleted file mode 100644
index f066034..0000000
--- a/Pages/Resources/views/public/show.blade.php
+++ /dev/null
@@ -1,22 +0,0 @@
-@extends('stylist-theme-saasland::layouts.app')
-
-@php
- $title = $page->title;
- $description = $page->summary;
- $eyebrow = 'Page';
- $breadcrumbs = [$page->title => null];
-@endphp
-
-@section('content')
-
-
-
- @if ($page->summary)
- {{ $page->summary }}
- @endif
-
- {!! $page->content !!}
-
-
-
-@endsection
diff --git a/Pages/Routes/web.php b/Pages/Routes/web.php
deleted file mode 100644
index dadc7f4..0000000
--- a/Pages/Routes/web.php
+++ /dev/null
@@ -1,17 +0,0 @@
-prefix('administration/pages')->name('administration.pages.')->group(function (): void {
- Route::get('/', [PageController::class, 'index'])->name('index');
- Route::get('/create', [PageController::class, 'create'])->name('create');
- Route::post('/', [PageController::class, 'store'])->name('store');
- Route::get('/{page}/edit', [PageController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{page}', [PageController::class, 'update'])->name('update');
- Route::post('/{page}/duplicate', [PageController::class, 'duplicate'])->name('duplicate');
- Route::delete('/{page}', [PageController::class, 'destroy'])->name('destroy');
-});
-
-Route::get('/pages/{slug}', [PublicPageController::class, 'show'])->name('pages.public.show');
diff --git a/Pages/Tests/Feature/ModuleBootTest.php b/Pages/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 2a22cc4..0000000
--- a/Pages/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,7 +0,0 @@
-toBeTrue();
-});
diff --git a/Pages/module.json b/Pages/module.json
deleted file mode 100644
index d891304..0000000
--- a/Pages/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Pages",
- "alias": "pages",
- "version": "1.0.0",
- "description": "Pages CMS module.",
- "keywords": [
- "cms",
- "pages"
- ],
- "priority": 21,
- "providers": [
- "Modules\\Pages\\Providers\\PagesServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Products/Config/config.php b/Products/Config/config.php
deleted file mode 100644
index 3bfe783..0000000
--- a/Products/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Products'];
diff --git a/Products/Database/Migrations/2026_07_24_105054_create_products_table.php b/Products/Database/Migrations/2026_07_24_105054_create_products_table.php
deleted file mode 100644
index a009d1a..0000000
--- a/Products/Database/Migrations/2026_07_24_105054_create_products_table.php
+++ /dev/null
@@ -1,23 +0,0 @@
-id();
- $table->string('name');
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('products');
- }
-};
diff --git a/Products/Database/Migrations/2026_07_24_150000_add_pricing_fields_to_products_table.php b/Products/Database/Migrations/2026_07_24_150000_add_pricing_fields_to_products_table.php
deleted file mode 100644
index f6b9a80..0000000
--- a/Products/Database/Migrations/2026_07_24_150000_add_pricing_fields_to_products_table.php
+++ /dev/null
@@ -1,34 +0,0 @@
-string('product_type')->default('product')->after('name');
- $table->text('description')->nullable()->after('product_type');
- $table->decimal('price', 12, 2)->nullable()->after('description');
- $table->string('currency', 3)->default('GBP')->after('price');
- $table->string('billing_period')->nullable()->after('currency');
- $table->json('features')->nullable()->after('billing_period');
- $table->string('cta_label')->nullable()->after('features');
- $table->string('cta_url')->nullable()->after('cta_label');
- $table->boolean('is_featured')->default(false)->after('cta_url');
- $table->unsignedInteger('sort_order')->default(0)->after('is_featured');
- });
- }
-
- public function down(): void
- {
- Schema::table('products', function (Blueprint $table): void {
- $table->dropColumn([
- 'product_type', 'description', 'price', 'currency', 'billing_period',
- 'features', 'cta_label', 'cta_url', 'is_featured', 'sort_order',
- ]);
- });
- }
-};
diff --git a/Products/Http/Controllers/ProductController.php b/Products/Http/Controllers/ProductController.php
deleted file mode 100644
index c3ccf6f..0000000
--- a/Products/Http/Controllers/ProductController.php
+++ /dev/null
@@ -1,23 +0,0 @@
- Product::query()->latest()->paginate()]);
- }
-
- public function store(StoreProductRequest $request): RedirectResponse
- {
- Product::query()->create($request->validated());
-
- return back()->with('status', 'Product created.');
- }
-}
diff --git a/Products/Http/Requests/StoreProductRequest.php b/Products/Http/Requests/StoreProductRequest.php
deleted file mode 100644
index ae953bc..0000000
--- a/Products/Http/Requests/StoreProductRequest.php
+++ /dev/null
@@ -1,45 +0,0 @@
-user() !== null;
- }
-
- protected function prepareForValidation(): void
- {
- $this->merge([
- 'is_active' => $this->boolean('is_active'),
- 'is_featured' => $this->boolean('is_featured'),
- 'features' => collect(preg_split('/\r\n|\r|\n/', (string) $this->input('features')))
- ->map(fn (string $feature) => trim($feature))
- ->filter()
- ->values()
- ->all(),
- ]);
- }
-
- public function rules(): array
- {
- return [
- 'name' => ['required', 'string', 'max:255'],
- 'product_type' => ['required', 'in:product,plan'],
- 'description' => ['nullable', 'string'],
- 'price' => ['nullable', 'numeric', 'min:0'],
- 'currency' => ['required', 'string', 'size:3'],
- 'billing_period' => ['nullable', 'string', 'max:40'],
- 'features' => ['array'],
- 'features.*' => ['string', 'max:255'],
- 'cta_label' => ['nullable', 'string', 'max:80'],
- 'cta_url' => ['nullable', 'string', 'max:2048'],
- 'is_featured' => ['boolean'],
- 'sort_order' => ['integer', 'min:0'],
- 'is_active' => ['boolean'],
- ];
- }
-}
diff --git a/Products/Models/Product.php b/Products/Models/Product.php
deleted file mode 100644
index 30dbf31..0000000
--- a/Products/Models/Product.php
+++ /dev/null
@@ -1,21 +0,0 @@
- 'decimal:2',
- 'features' => 'array',
- 'is_active' => 'boolean',
- 'is_featured' => 'boolean',
- 'sort_order' => 'integer',
- ];
- }
-}
diff --git a/Products/Providers/ProductsServiceProvider.php b/Products/Providers/ProductsServiceProvider.php
deleted file mode 100644
index 98d5e46..0000000
--- a/Products/Providers/ProductsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Products', 'Config/config.php'), 'products');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Products', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Products', 'Resources/views'), 'products');
- }
-}
diff --git a/Products/Providers/RouteServiceProvider.php b/Products/Providers/RouteServiceProvider.php
deleted file mode 100644
index 337a787..0000000
--- a/Products/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,14 +0,0 @@
-group(module_path('Products', 'Routes/web.php'));
- }
-}
diff --git a/Products/Resources/views/admin/products/form.blade.php b/Products/Resources/views/admin/products/form.blade.php
deleted file mode 100644
index a98563c..0000000
--- a/Products/Resources/views/admin/products/form.blade.php
+++ /dev/null
@@ -1,25 +0,0 @@
-
- @csrf
-
-
-
- Product
- Pricing plan
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ __('Save Product') }}
-
diff --git a/Products/Resources/views/admin/products/index.blade.php b/Products/Resources/views/admin/products/index.blade.php
deleted file mode 100644
index 757785c..0000000
--- a/Products/Resources/views/admin/products/index.blade.php
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
Products & Pricing
-
Manage catalogue products and public-facing pricing plans from one place.
-
-
- @include('products::admin.products.form')
-
-
- @forelse ($records as $record)
-
-
-
{{ $record->name }} {{ ucfirst($record->product_type ?? 'product') }}
- @if($record->is_featured)
Featured @endif
-
- @if($record->price !== null){{ $record->currency }} {{ $record->price }} /{{ $record->billing_period }}
@elseCustom
@endif
- {{ $record->description }}
-
- @empty
-
No products or plans have been created.
- @endforelse
-
- {{ $records->links() }}
-
-
diff --git a/Products/Routes/web.php b/Products/Routes/web.php
deleted file mode 100644
index eecbc02..0000000
--- a/Products/Routes/web.php
+++ /dev/null
@@ -1,7 +0,0 @@
-prefix('administration/products')->name('administration.products.')->group(function (): void {
- // Register module routes here.
-});
diff --git a/Products/Tests/Feature/ProductCrudTest.php b/Products/Tests/Feature/ProductCrudTest.php
deleted file mode 100644
index f596dc9..0000000
--- a/Products/Tests/Feature/ProductCrudTest.php
+++ /dev/null
@@ -1,7 +0,0 @@
-create(['name' => 'Example']))->toBeInstanceOf(Product::class);
-});
diff --git a/Products/Tests/Feature/ProductsModuleBootTest.php b/Products/Tests/Feature/ProductsModuleBootTest.php
deleted file mode 100644
index f89049e..0000000
--- a/Products/Tests/Feature/ProductsModuleBootTest.php
+++ /dev/null
@@ -1,7 +0,0 @@
-providerIsLoaded(ProductsServiceProvider::class))->toBeTrue();
-});
diff --git a/Products/module.json b/Products/module.json
deleted file mode 100644
index 5473559..0000000
--- a/Products/module.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "Products",
- "alias": "products",
- "version": "1.0.0",
- "description": "Products module.",
- "keywords": [
- "products"
- ],
- "priority": 100,
- "providers": [
- "Modules\\Products\\Providers\\ProductsServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Redirects/Config/config.php b/Redirects/Config/config.php
deleted file mode 100644
index e5170bd..0000000
--- a/Redirects/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Redirects'];
diff --git a/Redirects/Database/Migrations/2026_07_24_000300_create_redirects_table.php b/Redirects/Database/Migrations/2026_07_24_000300_create_redirects_table.php
deleted file mode 100644
index 5635594..0000000
--- a/Redirects/Database/Migrations/2026_07_24_000300_create_redirects_table.php
+++ /dev/null
@@ -1,26 +0,0 @@
-id();
- $table->string('source');
- $table->string('destination');
- $table->unsignedInteger('status_code')->default(301);
- $table->boolean('enabled')->default(true);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('redirects');
- }
-};
diff --git a/Redirects/Http/Controllers/RedirectController.php b/Redirects/Http/Controllers/RedirectController.php
deleted file mode 100644
index efb0a93..0000000
--- a/Redirects/Http/Controllers/RedirectController.php
+++ /dev/null
@@ -1,58 +0,0 @@
- Redirect::query()->latest()->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('redirects::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $request->validate(['source' => ['required', 'string', 'max:255'],
- 'destination' => ['required', 'string', 'max:255'],
- 'status_code' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- Redirect::create($data);
-
- return redirect()->route('administration.redirects.index')->with('status', 'Redirect created.');
- }
-
- public function edit(Redirect $redirect): View
- {
- return view('redirects::admin.form', ['record' => $redirect]);
- }
-
- public function update(Request $request, Redirect $redirect): RedirectResponse
- {
- $data = $request->validate(['source' => ['required', 'string', 'max:255'],
- 'destination' => ['required', 'string', 'max:255'],
- 'status_code' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- $redirect->update($data);
-
- return redirect()->route('administration.redirects.index')->with('status', 'Redirect updated.');
- }
-
- public function destroy(Redirect $redirect): RedirectResponse
- {
- $redirect->delete();
-
- return back()->with('status', 'Redirect deleted.');
- }
-}
diff --git a/Redirects/Models/Redirect.php b/Redirects/Models/Redirect.php
deleted file mode 100644
index ba4db4e..0000000
--- a/Redirects/Models/Redirect.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'integer',
- 'enabled' => 'boolean',
- ];
- }
-}
diff --git a/Redirects/Providers/RedirectsServiceProvider.php b/Redirects/Providers/RedirectsServiceProvider.php
deleted file mode 100644
index d32dbda..0000000
--- a/Redirects/Providers/RedirectsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Redirects', 'Config/config.php'), 'redirects');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Redirects', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Redirects', 'Resources/views'), 'redirects');
- }
-}
diff --git a/Redirects/Providers/RouteServiceProvider.php b/Redirects/Providers/RouteServiceProvider.php
deleted file mode 100644
index ba5a142..0000000
--- a/Redirects/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Redirects', 'Routes/web.php')));
- }
-}
diff --git a/Redirects/Resources/views/admin/form.blade.php b/Redirects/Resources/views/admin/form.blade.php
deleted file mode 100644
index 18dccf9..0000000
--- a/Redirects/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
diff --git a/Redirects/Resources/views/admin/index.blade.php b/Redirects/Resources/views/admin/index.blade.php
deleted file mode 100644
index cf7590b..0000000
--- a/Redirects/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-
-Redirects Manage redirects content.
Add new
-@if(session('status'))
{{ session('status') }}
@endif
-
Source Destination Status Code Actions @forelse($records as $record){{ is_array($record->source) ? json_encode($record->source) : Str::limit((string) $record->source, 70) }} {{ is_array($record->destination) ? json_encode($record->destination) : Str::limit((string) $record->destination, 70) }} {{ is_array($record->status_code) ? json_encode($record->status_code) : Str::limit((string) $record->status_code, 70) }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
-
diff --git a/Redirects/Routes/web.php b/Redirects/Routes/web.php
deleted file mode 100644
index 9faf228..0000000
--- a/Redirects/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/redirects')->name('administration.redirects.')->group(function (): void {
- Route::get('/', [RedirectController::class, 'index'])->name('index');
- Route::get('/create', [RedirectController::class, 'create'])->name('create');
- Route::post('/', [RedirectController::class, 'store'])->name('store');
- Route::get('/{redirect}/edit', [RedirectController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{redirect}', [RedirectController::class, 'update'])->name('update');
- Route::delete('/{redirect}', [RedirectController::class, 'destroy'])->name('destroy');
-});
diff --git a/Redirects/Support/ModuleInfo.php b/Redirects/Support/ModuleInfo.php
deleted file mode 100644
index a226dac..0000000
--- a/Redirects/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBeTrue();
-});
diff --git a/Redirects/module.json b/Redirects/module.json
deleted file mode 100644
index cebfd49..0000000
--- a/Redirects/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Redirects",
- "alias": "redirects",
- "version": "1.0.0",
- "description": "Redirects CMS module.",
- "keywords": [
- "cms",
- "redirects"
- ],
- "priority": 23,
- "providers": [
- "Modules\\Redirects\\Providers\\RedirectsServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Roles/Config/config.php b/Roles/Config/config.php
deleted file mode 100644
index 60fa263..0000000
--- a/Roles/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Roles',
- 'enabled' => true,
-];
diff --git a/Roles/Database/Migrations/2026_07_23_000050_create_permission_tables.php b/Roles/Database/Migrations/2026_07_23_000050_create_permission_tables.php
deleted file mode 100644
index d74934a..0000000
--- a/Roles/Database/Migrations/2026_07_23_000050_create_permission_tables.php
+++ /dev/null
@@ -1,119 +0,0 @@
-id();
- $table->string('name');
- $table->string('guard_name');
- $table->timestamps();
-
- $table->unique(['name', 'guard_name']);
- });
-
- Schema::create($tableNames['roles'], function (Blueprint $table): void {
- $table->id();
- $table->string('name');
- $table->string('guard_name');
- $table->timestamps();
-
- $table->unique(['name', 'guard_name']);
- });
-
- Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use (
- $tableNames,
- $pivotPermission,
- $modelMorphKey
- ): void {
- $table->unsignedBigInteger($pivotPermission);
- $table->string('model_type');
- $table->unsignedBigInteger($modelMorphKey);
-
- $table->index([$modelMorphKey, 'model_type'], 'model_has_permissions_model_id_model_type_index');
- $table->foreign($pivotPermission)
- ->references('id')
- ->on($tableNames['permissions'])
- ->cascadeOnDelete();
-
- $table->primary(
- [$pivotPermission, $modelMorphKey, 'model_type'],
- 'model_has_permissions_permission_model_type_primary'
- );
- });
-
- Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use (
- $tableNames,
- $pivotRole,
- $modelMorphKey
- ): void {
- $table->unsignedBigInteger($pivotRole);
- $table->string('model_type');
- $table->unsignedBigInteger($modelMorphKey);
-
- $table->index([$modelMorphKey, 'model_type'], 'model_has_roles_model_id_model_type_index');
- $table->foreign($pivotRole)
- ->references('id')
- ->on($tableNames['roles'])
- ->cascadeOnDelete();
-
- $table->primary(
- [$pivotRole, $modelMorphKey, 'model_type'],
- 'model_has_roles_role_model_type_primary'
- );
- });
-
- Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use (
- $tableNames,
- $pivotRole,
- $pivotPermission
- ): void {
- $table->unsignedBigInteger($pivotPermission);
- $table->unsignedBigInteger($pivotRole);
-
- $table->foreign($pivotPermission)
- ->references('id')
- ->on($tableNames['permissions'])
- ->cascadeOnDelete();
- $table->foreign($pivotRole)
- ->references('id')
- ->on($tableNames['roles'])
- ->cascadeOnDelete();
-
- $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
- });
-
- app('cache')->store(config('permission.cache.store') === 'default'
- ? null
- : config('permission.cache.store'))
- ->forget(config('permission.cache.key'));
- }
-
- public function down(): void
- {
- $tableNames = config('permission.table_names');
-
- Schema::dropIfExists($tableNames['role_has_permissions']);
- Schema::dropIfExists($tableNames['model_has_roles']);
- Schema::dropIfExists($tableNames['model_has_permissions']);
- Schema::dropIfExists($tableNames['roles']);
- Schema::dropIfExists($tableNames['permissions']);
- }
-};
diff --git a/Roles/Database/Seeders/RolePermissionSeeder.php b/Roles/Database/Seeders/RolePermissionSeeder.php
deleted file mode 100644
index 9322669..0000000
--- a/Roles/Database/Seeders/RolePermissionSeeder.php
+++ /dev/null
@@ -1,96 +0,0 @@
-forgetCachedPermissions();
-
- $permissions = [
- 'administration.access',
- 'users.view',
- 'users.create',
- 'users.update',
- 'users.delete',
- 'roles.manage',
- 'settings.manage',
- 'audit.view',
- 'media.view',
- 'media.upload',
- 'media.update',
- 'media.delete',
- 'seo.manage',
- 'search.manage',
- 'themes.manage',
- 'pages.manage',
- 'menus.manage',
- 'redirects.manage',
- 'forms.manage',
- 'documents.manage',
- 'banners.manage',
- 'faq.manage',
- 'team.manage',
- 'testimonials.manage',
- 'blog.manage',
- 'news.manage',
- 'categories.manage',
- 'tags.manage',
- 'authors.manage',
- 'comments.moderate',
- 'crm.manage',
- 'newsletter.manage',
- 'events.manage',
- 'careers.manage',
- 'locations.manage',
- 'workflow.view',
- 'workflow.manage',
- 'workflow.review',
- 'localization.manage',
- 'localization.translate',
- 'sites.view',
- 'sites.manage',
- 'sites.assign',
- 'developer.access',
- 'api.view',
- 'api.manage',
- 'enterprise.view',
- 'enterprise.manage',
- 'commerce.view',
- 'commerce.manage',
- 'commerce.orders.manage',
- 'commerce.products.manage',
- 'security.view',
- 'security.manage',
- 'security.audit',
- 'automation.view',
- 'automation.manage',
- 'automation.execute',
- 'ai.view',
- 'ai.manage',
- 'ai.execute',
- 'analytics.view',
- 'analytics.manage',
- '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) {
- Permission::findOrCreate($permission, 'web');
- }
-
- $administrator = Role::findOrCreate('admin', 'web');
- $administrator->syncPermissions($permissions);
-
- $registrar->forgetCachedPermissions();
- }
-}
diff --git a/Roles/Http/Controllers/RoleController.php b/Roles/Http/Controllers/RoleController.php
deleted file mode 100644
index d5b0e16..0000000
--- a/Roles/Http/Controllers/RoleController.php
+++ /dev/null
@@ -1,58 +0,0 @@
- Role::withCount(['users', 'permissions'])->orderBy('name')->get()]);
- }
-
- public function create()
- {
- return view('roles::form', ['role' => new Role(['guard_name' => 'web']), 'permissions' => Permission::orderBy('name')->get()]);
- }
-
- public function store(Request $request)
- {
- $data = $this->validated($request);
- $role = Role::create(['name' => $data['name'], 'guard_name' => 'web']);
- $role->syncPermissions($data['permissions'] ?? []);
-
- return redirect()->route('administration.roles.index')->with('status', 'Role created.');
- }
-
- public function edit(Role $role)
- {
- return view('roles::form', ['role' => $role->load('permissions'), 'permissions' => Permission::orderBy('name')->get()]);
- }
-
- public function update(Request $request, Role $role)
- {
- $data = $this->validated($request, $role);
- $role->update(['name' => $data['name']]);
- $role->syncPermissions($data['permissions'] ?? []);
-
- return redirect()->route('administration.roles.index')->with('status', 'Role updated.');
- }
-
- public function destroy(Role $role)
- {
- abort_if($role->name === 'admin', 422, 'The administrator role cannot be deleted.');
- $role->delete();
-
- return back()->with('status', 'Role deleted.');
- }
-
- private function validated(Request $request, ?Role $role = null): array
- {
- return $request->validate(['name' => ['required', 'string', 'max:100', Rule::unique('roles', 'name')->ignore($role)], 'permissions' => ['array'], 'permissions.*' => ['string', 'exists:permissions,name']]);
- }
-}
diff --git a/Roles/Providers/RolesServiceProvider.php b/Roles/Providers/RolesServiceProvider.php
deleted file mode 100644
index 120c1c0..0000000
--- a/Roles/Providers/RolesServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Roles/Providers/RouteServiceProvider.php b/Roles/Providers/RouteServiceProvider.php
deleted file mode 100644
index a8104fd..0000000
--- a/Roles/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Roles', 'Routes/web.php'));
- });
- }
-}
diff --git a/Roles/Resources/views/form.blade.php b/Roles/Resources/views/form.blade.php
deleted file mode 100644
index 202fec9..0000000
--- a/Roles/Resources/views/form.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-
{{ $role->exists ? 'Edit role' : 'Create role' }} @csrf @if($role->exists)@method('PUT')@endifCancel Save
diff --git a/Roles/Resources/views/index.blade.php b/Roles/Resources/views/index.blade.php
deleted file mode 100644
index 6ccb021..0000000
--- a/Roles/Resources/views/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Roles & permissions Control administrative access.
Create role @if(session('status'))
{{ session('status') }}
@endif
@foreach($roles as $role)
{{ $role->name }}
{{ $role->users_count }} users · {{ $role->permissions_count }} permissions
Edit @if($role->name!=='admin')
@csrf @method('DELETE')Delete @endif@endforeach
diff --git a/Roles/Routes/web.php b/Roles/Routes/web.php
deleted file mode 100644
index f611599..0000000
--- a/Roles/Routes/web.php
+++ /dev/null
@@ -1,8 +0,0 @@
-prefix('administration/roles')->name('administration.roles.')->group(function (): void {
- Route::resource('', RoleController::class)->parameters(['' => 'role'])->except(['show']);
-});
diff --git a/Roles/Support/ModuleInfo.php b/Roles/Support/ModuleInfo.php
deleted file mode 100644
index 339da0a..0000000
--- a/Roles/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Roles');
-});
diff --git a/Roles/composer.json b/Roles/composer.json
deleted file mode 100644
index 0bdaa9f..0000000
--- a/Roles/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/roles-module",
- "description": "Roles foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Roles\\": ""
- }
- }
-}
diff --git a/Roles/module.json b/Roles/module.json
deleted file mode 100644
index b3f52b7..0000000
--- a/Roles/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Roles",
- "alias": "roles",
- "version": "1.0.0",
- "description": "Roles foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "roles"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Roles\\Providers\\RolesServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Search/Config/config.php b/Search/Config/config.php
deleted file mode 100644
index 9b3b517..0000000
--- a/Search/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Search',
- 'enabled' => true,
-];
diff --git a/Search/Database/Migrations/2026_07_23_000500_create_search_index_table.php b/Search/Database/Migrations/2026_07_23_000500_create_search_index_table.php
deleted file mode 100644
index ca700a7..0000000
--- a/Search/Database/Migrations/2026_07_23_000500_create_search_index_table.php
+++ /dev/null
@@ -1,30 +0,0 @@
-id();
- $table->morphs('searchable');
- $table->string('title')->index();
- $table->text('summary')->nullable();
- $table->longText('content')->nullable();
- $table->string('url')->nullable();
- $table->json('metadata')->nullable();
- $table->boolean('is_public')->default(true)->index();
- $table->timestamp('published_at')->nullable()->index();
- $table->timestamps();
- $table->unique(['searchable_type', 'searchable_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('search_index');
- }
-};
diff --git a/Search/Http/Controllers/SearchController.php b/Search/Http/Controllers/SearchController.php
deleted file mode 100644
index 9c591c6..0000000
--- a/Search/Http/Controllers/SearchController.php
+++ /dev/null
@@ -1,18 +0,0 @@
-query('q'));
- $results = DB::table('search_index')->when($q !== '', fn ($query) => $query->where(fn ($w) => $w->where('title', 'like', "%{$q}%")->orWhere('summary', 'like', "%{$q}%")->orWhere('content', 'like', "%{$q}%")))->latest('updated_at')->paginate(25)->withQueryString();
-
- return view('search::index', compact('results', 'q'));
- }
-}
diff --git a/Search/Providers/RouteServiceProvider.php b/Search/Providers/RouteServiceProvider.php
deleted file mode 100644
index d94acd2..0000000
--- a/Search/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Search', 'Routes/web.php'));
- });
- }
-}
diff --git a/Search/Providers/SearchServiceProvider.php b/Search/Providers/SearchServiceProvider.php
deleted file mode 100644
index 12ad9c9..0000000
--- a/Search/Providers/SearchServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Search/Resources/views/index.blade.php b/Search/Resources/views/index.blade.php
deleted file mode 100644
index 07ea084..0000000
--- a/Search/Resources/views/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Search index Inspect content currently available to platform search.
Search @forelse($results as $result)
{{ $result->title }}
{{ str($result->summary ?: $result->content)->limit(180) }}
{{ $result->searchable_type }} #{{ $result->searchable_id }}
@empty
No indexed content found.
@endforelse
{{ $results->links() }}
diff --git a/Search/Routes/web.php b/Search/Routes/web.php
deleted file mode 100644
index ad6c1f7..0000000
--- a/Search/Routes/web.php
+++ /dev/null
@@ -1,6 +0,0 @@
-get('administration/search', [SearchController::class, 'index'])->name('administration.search.index');
diff --git a/Search/Support/ModuleInfo.php b/Search/Support/ModuleInfo.php
deleted file mode 100644
index 98f93c7..0000000
--- a/Search/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Search');
-});
diff --git a/Search/composer.json b/Search/composer.json
deleted file mode 100644
index 62e3b1a..0000000
--- a/Search/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/search-module",
- "description": "Search foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Search\\": ""
- }
- }
-}
diff --git a/Search/module.json b/Search/module.json
deleted file mode 100644
index 6eda4a2..0000000
--- a/Search/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Search",
- "alias": "search",
- "version": "1.0.0",
- "description": "Search foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "search"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Search\\Providers\\SearchServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Security/Config/config.php b/Security/Config/config.php
deleted file mode 100644
index b4e3156..0000000
--- a/Security/Config/config.php
+++ /dev/null
@@ -1,9 +0,0 @@
- (bool) env('SECURITY_RECORD_AUTH_EVENTS', true),
- 'trusted_device_days' => (int) env('SECURITY_TRUSTED_DEVICE_DAYS', 30),
- 'session_idle_minutes' => (int) env('SECURITY_SESSION_IDLE_MINUTES', 120),
- 'max_failed_logins' => (int) env('SECURITY_MAX_FAILED_LOGINS', 10),
- 'lockout_minutes' => (int) env('SECURITY_LOCKOUT_MINUTES', 15),
-];
diff --git a/Security/Database/Migrations/2026_07_24_120000_create_security_tables.php b/Security/Database/Migrations/2026_07_24_120000_create_security_tables.php
deleted file mode 100644
index a393e34..0000000
--- a/Security/Database/Migrations/2026_07_24_120000_create_security_tables.php
+++ /dev/null
@@ -1,76 +0,0 @@
-id();
- $table->string('name')->default('Default security policy');
- $table->unsignedSmallInteger('minimum_password_length')->default(12);
- $table->boolean('require_mixed_case')->default(true);
- $table->boolean('require_numbers')->default(true);
- $table->boolean('require_symbols')->default(true);
- $table->boolean('require_mfa')->default(false);
- $table->unsignedInteger('password_expiry_days')->nullable();
- $table->unsignedSmallInteger('password_history')->default(5);
- $table->unsignedSmallInteger('max_failed_logins')->default(10);
- $table->unsignedSmallInteger('lockout_minutes')->default(15);
- $table->unsignedSmallInteger('session_idle_minutes')->default(120);
- $table->boolean('single_session')->default(false);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- Schema::create('security_login_events', function (Blueprint $table) {
- $table->id();
- $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
- $table->string('email')->nullable()->index();
- $table->string('event', 30)->index();
- $table->string('ip_address', 45)->nullable()->index();
- $table->text('user_agent')->nullable();
- $table->string('session_id')->nullable()->index();
- $table->string('reason')->nullable();
- $table->json('metadata')->nullable();
- $table->timestamp('occurred_at')->useCurrent();
- $table->timestamps();
- });
- Schema::create('security_devices', function (Blueprint $table) {
- $table->id();
- $table->foreignId('user_id')->constrained()->cascadeOnDelete();
- $table->string('name');
- $table->string('fingerprint', 64)->index();
- $table->string('ip_address', 45)->nullable();
- $table->text('user_agent')->nullable();
- $table->timestamp('trusted_until')->nullable();
- $table->timestamp('last_seen_at')->nullable();
- $table->timestamp('revoked_at')->nullable();
- $table->timestamps();
- $table->unique(['user_id', 'fingerprint']);
- });
- Schema::create('security_ip_rules', function (Blueprint $table) {
- $table->id();
- $table->string('name');
- $table->string('type', 10)->default('deny');
- $table->string('address', 64);
- $table->text('description')->nullable();
- $table->boolean('applies_to_admin')->default(true);
- $table->boolean('applies_to_login')->default(true);
- $table->boolean('is_active')->default(true);
- $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
- $table->timestamps();
- $table->index(['type', 'is_active']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('security_ip_rules');
- Schema::dropIfExists('security_devices');
- Schema::dropIfExists('security_login_events');
- Schema::dropIfExists('security_policies');
- }
-};
diff --git a/Security/Http/Controllers/SecurityController.php b/Security/Http/Controllers/SecurityController.php
deleted file mode 100644
index 77e2633..0000000
--- a/Security/Http/Controllers/SecurityController.php
+++ /dev/null
@@ -1,52 +0,0 @@
- SecurityPolicy::first(), 'events' => LoginEvent::latest('occurred_at')->paginate(25), 'devices' => TrustedDevice::whereNull('revoked_at')->latest('last_seen_at')->limit(20)->get(), 'rules' => IpRule::latest()->get(), 'failed24h' => LoginEvent::where('event', 'failed')->where('occurred_at', '>=', now()->subDay())->count()]);
- }
-
- public function savePolicy(Request $request): RedirectResponse
- {
- $data = $request->validate(['minimum_password_length' => ['required', 'integer', 'min:8', 'max:128'], 'password_expiry_days' => ['nullable', 'integer', 'min:1'], 'password_history' => ['required', 'integer', 'min:0', 'max:24'], 'max_failed_logins' => ['required', 'integer', 'min:1', 'max:100'], 'lockout_minutes' => ['required', 'integer', 'min:1', 'max:1440'], 'session_idle_minutes' => ['required', 'integer', 'min:5', 'max:10080']]);
- foreach (['require_mixed_case', 'require_numbers', 'require_symbols', 'require_mfa', 'single_session'] as $key) {
- $data[$key] = $request->boolean($key);
- }
- SecurityPolicy::updateOrCreate(['id' => SecurityPolicy::value('id')], $data + ['name' => 'Default security policy', 'is_active' => true]);
-
- return back()->with('status', 'Security policy updated.');
- }
-
- public function storeRule(Request $request): RedirectResponse
- {
- IpRule::create($request->validate(['name' => ['required', 'string', 'max:255'], 'type' => ['required', 'in:allow,deny'], 'address' => ['required', 'string', 'max:64'], 'description' => ['nullable', 'string']]) + ['applies_to_admin' => $request->boolean('applies_to_admin'), 'applies_to_login' => $request->boolean('applies_to_login'), 'is_active' => true, 'created_by' => $request->user()->id]);
-
- return back()->with('status', 'IP rule created.');
- }
-
- public function destroyRule(IpRule $rule): RedirectResponse
- {
- $rule->delete();
-
- return back()->with('status', 'IP rule deleted.');
- }
-
- public function revokeDevice(TrustedDevice $device): RedirectResponse
- {
- $device->update(['revoked_at' => now()]);
-
- return back()->with('status', 'Device trust revoked.');
- }
-}
diff --git a/Security/Http/Middleware/EnforceIpRestrictions.php b/Security/Http/Middleware/EnforceIpRestrictions.php
deleted file mode 100644
index e009e1e..0000000
--- a/Security/Http/Middleware/EnforceIpRestrictions.php
+++ /dev/null
@@ -1,20 +0,0 @@
-rules->isBlocked((string) $request->ip(), $scope), 403, 'Access is not permitted from this IP address.');
-
- return $next($request);
- }
-}
diff --git a/Security/Listeners/RecordFailedLogin.php b/Security/Listeners/RecordFailedLogin.php
deleted file mode 100644
index 6bcd118..0000000
--- a/Security/Listeners/RecordFailedLogin.php
+++ /dev/null
@@ -1,19 +0,0 @@
-user : null;
- LoginEvent::create(['user_id' => $user?->getAuthIdentifier(), 'email' => $user?->email ?? (property_exists($event, 'credentials') ? ($event->credentials['email'] ?? null) : null), 'event' => 'failed', 'ip_address' => $request?->ip(), 'user_agent' => $request?->userAgent(), 'session_id' => $request?->hasSession() ? $request->session()->getId() : null, 'occurred_at' => now()]);
- }
-}
diff --git a/Security/Listeners/RecordLogin.php b/Security/Listeners/RecordLogin.php
deleted file mode 100644
index 83face4..0000000
--- a/Security/Listeners/RecordLogin.php
+++ /dev/null
@@ -1,19 +0,0 @@
-user : null;
- LoginEvent::create(['user_id' => $user?->getAuthIdentifier(), 'email' => $user?->email ?? (property_exists($event, 'credentials') ? ($event->credentials['email'] ?? null) : null), 'event' => 'login', 'ip_address' => $request?->ip(), 'user_agent' => $request?->userAgent(), 'session_id' => $request?->hasSession() ? $request->session()->getId() : null, 'occurred_at' => now()]);
- }
-}
diff --git a/Security/Listeners/RecordLogout.php b/Security/Listeners/RecordLogout.php
deleted file mode 100644
index f29feed..0000000
--- a/Security/Listeners/RecordLogout.php
+++ /dev/null
@@ -1,19 +0,0 @@
-user : null;
- LoginEvent::create(['user_id' => $user?->getAuthIdentifier(), 'email' => $user?->email ?? (property_exists($event, 'credentials') ? ($event->credentials['email'] ?? null) : null), 'event' => 'logout', 'ip_address' => $request?->ip(), 'user_agent' => $request?->userAgent(), 'session_id' => $request?->hasSession() ? $request->session()->getId() : null, 'occurred_at' => now()]);
- }
-}
diff --git a/Security/Models/IpRule.php b/Security/Models/IpRule.php
deleted file mode 100644
index 2996dfe..0000000
--- a/Security/Models/IpRule.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'boolean', 'applies_to_login' => 'boolean', 'is_active' => 'boolean'];
-}
diff --git a/Security/Models/LoginEvent.php b/Security/Models/LoginEvent.php
deleted file mode 100644
index 1fce0ab..0000000
--- a/Security/Models/LoginEvent.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'array', 'occurred_at' => 'datetime'];
-}
diff --git a/Security/Models/SecurityPolicy.php b/Security/Models/SecurityPolicy.php
deleted file mode 100644
index e790c60..0000000
--- a/Security/Models/SecurityPolicy.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'boolean', 'require_numbers' => 'boolean', 'require_symbols' => 'boolean', 'require_mfa' => 'boolean', 'single_session' => 'boolean', 'is_active' => 'boolean'];
-}
diff --git a/Security/Models/TrustedDevice.php b/Security/Models/TrustedDevice.php
deleted file mode 100644
index 6777067..0000000
--- a/Security/Models/TrustedDevice.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'datetime', 'last_seen_at' => 'datetime', 'revoked_at' => 'datetime'];
-}
diff --git a/Security/Providers/RouteServiceProvider.php b/Security/Providers/RouteServiceProvider.php
deleted file mode 100644
index ed3f4c3..0000000
--- a/Security/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,14 +0,0 @@
-group(module_path('Security', 'Routes/web.php'));
- }
-}
diff --git a/Security/Providers/SecurityServiceProvider.php b/Security/Providers/SecurityServiceProvider.php
deleted file mode 100644
index cf9e892..0000000
--- a/Security/Providers/SecurityServiceProvider.php
+++ /dev/null
@@ -1,33 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Security', 'Config/config.php'), 'security');
- }
-
- public function boot(Router $router): void
- {
- $this->loadMigrationsFrom(module_path('Security', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Security', 'Resources/views'), 'security');
- $router->aliasMiddleware('security.ip', EnforceIpRestrictions::class);
- Event::listen(Login::class, RecordLogin::class);
- Event::listen(Failed::class, RecordFailedLogin::class);
- Event::listen(Logout::class, RecordLogout::class);
- }
-}
diff --git a/Security/Resources/views/admin/index.blade.php b/Security/Resources/views/admin/index.blade.php
deleted file mode 100644
index b07de13..0000000
--- a/Security/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-Security Centre Identity policy, authentication history, trusted devices and network access controls.
@if(session('status'))
{{ session('status') }}
@endif
-
Failed logins (24h)
{{ $failed24h }}
Active trusted devices
{{ $devices->count() }}
IP rules
{{ $rules->count() }}
MFA required
{{ $policy?->require_mfa ? 'Yes' : 'No' }}
-
@csrf @method('PUT')Security policy
Save policy
-
@csrfAdd IP rule Deny Allow
Create rule @foreach($rules as $rule)
{{ $rule->name }} {{ $rule->type }} {{ $rule->address }}
@csrf @method('DELETE')Delete @endforeach
-
Authentication history
When Event User IP address Client @forelse($events as $event){{ $event->occurred_at?->format('d M Y H:i:s') }} {{ $event->event }} {{ $event->email ?: 'Unknown' }} {{ $event->ip_address ?: '—' }} {{ $event->user_agent ?: '—' }} @emptyNo authentication events recorded. @endforelse
{{ $events->links() }}
diff --git a/Security/Routes/web.php b/Security/Routes/web.php
deleted file mode 100644
index b92281e..0000000
--- a/Security/Routes/web.php
+++ /dev/null
@@ -1,12 +0,0 @@
-name('administration.security.')->middleware(['auth', 'permission:security.view', 'security.ip:admin'])->group(function () {
- Route::get('/', [SecurityController::class, 'index'])->name('index');
- Route::put('/policy', [SecurityController::class, 'savePolicy'])->middleware('permission:security.manage')->name('policy.update');
- Route::post('/ip-rules', [SecurityController::class, 'storeRule'])->middleware('permission:security.manage')->name('rules.store');
- Route::delete('/ip-rules/{rule}', [SecurityController::class, 'destroyRule'])->middleware('permission:security.manage')->name('rules.destroy');
- Route::delete('/devices/{device}', [SecurityController::class, 'revokeDevice'])->middleware('permission:security.manage')->name('devices.revoke');
-});
diff --git a/Security/Services/IpRuleService.php b/Security/Services/IpRuleService.php
deleted file mode 100644
index 490f000..0000000
--- a/Security/Services/IpRuleService.php
+++ /dev/null
@@ -1,39 +0,0 @@
-where('is_active', true)->where($column, true)->get();
- $allow = $rules->where('type', 'allow');
- if ($allow->isNotEmpty() && ! $allow->contains(fn (IpRule $rule) => $this->matches($ip, $rule->address))) {
- return true;
- }
-
- return $rules->where('type', 'deny')->contains(fn (IpRule $rule) => $this->matches($ip, $rule->address));
- }
-
- private function matches(string $ip, string $rule): bool
- {
- if ($ip === $rule) {
- return true;
- }
- if (! str_contains($rule, '/')) {
- return false;
- }
- [$subnet,$bits] = explode('/', $rule, 2);
- $bits = (int) $bits;
- if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
- $mask = -1 << (32 - $bits);
-
- return (ip2long($ip) & $mask) === (ip2long($subnet) & $mask);
- }
-
- return false;
- }
-}
diff --git a/Security/Tests/Feature/ModuleBootTest.php b/Security/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 7f2e420..0000000
--- a/Security/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
-assertTrue(app('router')->has('administration.security.index'));
- }
-
- public function test_security_middleware_alias_is_registered(): void
- {
- $this->assertArrayHasKey('security.ip', app('router')->getMiddleware());
- }
-}
diff --git a/Security/module.json b/Security/module.json
deleted file mode 100644
index 515918b..0000000
--- a/Security/module.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Security","alias":"security","version":"1.0.0","description":"Identity policies, login history, trusted devices, IP restrictions and session security.","keywords":["identity","security","sessions","devices","authentication"],"priority":120,"providers":["Modules\\Security\\Providers\\SecurityServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Users","Roles","Audit"]}
diff --git a/Seo/Config/config.php b/Seo/Config/config.php
deleted file mode 100644
index b2892d1..0000000
--- a/Seo/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Seo',
- 'enabled' => true,
-];
diff --git a/Seo/Database/Migrations/2026_07_23_000400_create_seo_metadata_table.php b/Seo/Database/Migrations/2026_07_23_000400_create_seo_metadata_table.php
deleted file mode 100644
index 1867c1e..0000000
--- a/Seo/Database/Migrations/2026_07_23_000400_create_seo_metadata_table.php
+++ /dev/null
@@ -1,36 +0,0 @@
-id();
- $table->morphs('seoable');
- $table->string('title')->nullable();
- $table->text('description')->nullable();
- $table->string('canonical_url')->nullable();
- $table->string('robots')->default('index,follow');
- $table->string('og_title')->nullable();
- $table->text('og_description')->nullable();
- $table->string('og_image')->nullable();
- $table->string('twitter_card')->default('summary_large_image');
- $table->string('schema_type')->nullable();
- $table->json('schema_data')->nullable();
- $table->boolean('include_in_sitemap')->default(true);
- $table->decimal('sitemap_priority', 2, 1)->default(0.5);
- $table->string('sitemap_change_frequency')->default('monthly');
- $table->timestamps();
- $table->unique(['seoable_type', 'seoable_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('seo_metadata');
- }
-};
diff --git a/Seo/Http/Controllers/SeoController.php b/Seo/Http/Controllers/SeoController.php
deleted file mode 100644
index 99349c0..0000000
--- a/Seo/Http/Controllers/SeoController.php
+++ /dev/null
@@ -1,27 +0,0 @@
-pluck('value', 'key');
-
- return view('seo::index', compact('values'));
- }
-
- public function update(Request $request)
- {
- $data = $request->validate(['site_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string', 'max:500'], 'robots' => ['required', 'string', 'max:100'], 'canonical_base' => ['nullable', 'url', 'max:255']]);
- foreach ($data as $key => $value) {
- Setting::updateOrCreate(['group' => 'seo', 'key' => $key], ['value' => $value, 'type' => 'string', 'is_public' => true]);
- }
-
-return back()->with('status', 'SEO defaults saved.');
- }
-}
diff --git a/Seo/Models/SeoMetadata.php b/Seo/Models/SeoMetadata.php
deleted file mode 100644
index fa17538..0000000
--- a/Seo/Models/SeoMetadata.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'array', 'include_in_sitemap' => 'boolean', 'sitemap_priority' => 'decimal:1'];
- }
-}
diff --git a/Seo/Providers/RouteServiceProvider.php b/Seo/Providers/RouteServiceProvider.php
deleted file mode 100644
index d2a362d..0000000
--- a/Seo/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Seo', 'Routes/web.php'));
- });
- }
-}
diff --git a/Seo/Providers/SeoServiceProvider.php b/Seo/Providers/SeoServiceProvider.php
deleted file mode 100644
index 1a134fc..0000000
--- a/Seo/Providers/SeoServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Seo/Resources/views/index.blade.php b/Seo/Resources/views/index.blade.php
deleted file mode 100644
index 9939a86..0000000
--- a/Seo/Resources/views/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-SEO defaults Site-wide metadata used when content does not provide its own values.
@if(session('status'))
{{ session('status') }}
@endif
@csrf @method('PUT'){{ old('meta_description',$values['meta_description']??'') }} Save SEO defaults
diff --git a/Seo/Routes/web.php b/Seo/Routes/web.php
deleted file mode 100644
index fa3652f..0000000
--- a/Seo/Routes/web.php
+++ /dev/null
@@ -1,9 +0,0 @@
-prefix('administration/seo')->name('administration.seo.')->group(function (): void {
- Route::get('/', [SeoController::class, 'index'])->name('index');
- Route::put('/', [SeoController::class, 'update'])->name('update');
-});
diff --git a/Seo/Support/ModuleInfo.php b/Seo/Support/ModuleInfo.php
deleted file mode 100644
index ac5145e..0000000
--- a/Seo/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Seo');
-});
diff --git a/Seo/composer.json b/Seo/composer.json
deleted file mode 100644
index a7eae0a..0000000
--- a/Seo/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/seo-module",
- "description": "Seo foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Seo\\": ""
- }
- }
-}
diff --git a/Seo/module.json b/Seo/module.json
deleted file mode 100644
index 4d886f8..0000000
--- a/Seo/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Seo",
- "alias": "seo",
- "version": "1.0.0",
- "description": "Seo foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "seo"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Seo\\Providers\\SeoServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Settings/Config/config.php b/Settings/Config/config.php
deleted file mode 100644
index c9342ba..0000000
--- a/Settings/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Settings',
- 'enabled' => true,
-];
diff --git a/Settings/Database/Migrations/2026_07_23_000100_create_settings_table.php b/Settings/Database/Migrations/2026_07_23_000100_create_settings_table.php
deleted file mode 100644
index 62b7134..0000000
--- a/Settings/Database/Migrations/2026_07_23_000100_create_settings_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-id();
- $table->string('group')->default('general')->index();
- $table->string('key');
- $table->longText('value')->nullable();
- $table->string('type')->default('string');
- $table->boolean('is_public')->default(false);
- $table->timestamps();
- $table->unique(['group', 'key']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('settings');
- }
-};
diff --git a/Settings/Http/Controllers/SettingController.php b/Settings/Http/Controllers/SettingController.php
deleted file mode 100644
index ceb3f78..0000000
--- a/Settings/Http/Controllers/SettingController.php
+++ /dev/null
@@ -1,109 +0,0 @@
- $settings->site(),
- 'sections' => $this->sections($themes),
- ]);
- }
-
- public function update(Request $request, SettingsRepository $settings, Loader $themes): RedirectResponse
- {
- $rules = [];
- foreach ($this->sections($themes) as $fields) {
- foreach ($fields as $key => $definition) {
- $rules["settings.$key"] = $definition['rules'];
- }
- }
-
- $validated = $request->validate($rules);
- foreach ($validated['settings'] ?? [] as $key => $value) {
- $settings->put($key, $value, 'site', 'string', true);
- }
-
- return back()->with('status', 'Site settings saved. The active frontend and backend themes have been updated.');
- }
-
- /** @return array>> */
- private function sections(Loader $themes): array
- {
- $text = ['type' => 'text', 'rules' => ['nullable', 'string', 'max:255']];
- $url = ['type' => 'text', 'rules' => ['nullable', 'string', 'max:2048'], 'placeholder' => '/path or https://example.com'];
- $textarea = ['type' => 'textarea', 'rules' => ['nullable', 'string', 'max:5000']];
-
- $backendThemes = $themes->all()
- ->filter(fn ($theme): bool => $theme->type() === 'backend')
- ->mapWithKeys(fn ($theme): array => [$theme->key() => $theme->name()])
- ->all();
-
- $frontendThemes = $themes->all()
- ->filter(fn ($theme): bool => $theme->type() === 'frontend')
- ->mapWithKeys(fn ($theme): array => [$theme->key() => $theme->name()])
- ->all();
-
- return [
- 'Themes' => [
- 'backend_theme' => [
- 'type' => 'select',
- 'rules' => ['required', 'string', Rule::in(array_keys($backendThemes))],
- 'label' => 'Backend theme',
- 'help' => 'Used for the authenticated administration area.',
- 'options' => $backendThemes,
- ],
- 'frontend_theme' => [
- 'type' => 'select',
- 'rules' => ['required', 'string', Rule::in(array_keys($frontendThemes))],
- 'label' => 'Frontend theme',
- 'help' => 'Used for the public website.',
- 'options' => $frontendThemes,
- ],
- ],
- 'General' => [
- 'site_name' => $text + ['label' => 'Site name', 'help' => 'Used in the header, footer, browser titles and metadata.'],
- 'tagline' => $text + ['label' => 'Tagline'],
- 'site_description' => $textarea + ['label' => 'Site description'],
- 'company_name' => $text + ['label' => 'Legal company name'],
- 'site_url' => $url + ['label' => 'Canonical site URL'],
- 'copyright' => $text + ['label' => 'Copyright text', 'help' => 'Leave blank to generate it automatically.'],
- ],
- 'Branding' => [
- 'logo_url' => $url + ['label' => 'Primary logo'],
- 'logo_dark_url' => $url + ['label' => 'Dark-mode logo'],
- 'favicon_url' => $url + ['label' => 'Favicon'],
- ],
- 'Contact' => [
- 'contact_email' => ['type' => 'email', 'rules' => ['nullable', 'email', 'max:255'], 'label' => 'Public email'],
- 'contact_phone' => $text + ['label' => 'Public telephone'],
- 'address' => $textarea + ['label' => 'Address'],
- ],
- 'Social' => [
- 'facebook_url' => $url + ['label' => 'Facebook'],
- 'x_url' => $url + ['label' => 'X / Twitter'],
- 'linkedin_url' => $url + ['label' => 'LinkedIn'],
- 'instagram_url' => $url + ['label' => 'Instagram'],
- ],
- 'Account navigation' => [
- 'dashboard_label' => $text + ['label' => 'Dashboard label'],
- 'login_label' => $text + ['label' => 'Login label'],
- 'registration_label' => $text + ['label' => 'Registration label'],
- ],
- 'SEO defaults' => [
- 'default_meta_title' => $text + ['label' => 'Default meta title', 'help' => 'Leave blank to use the site name.'],
- 'default_meta_description' => $textarea + ['label' => 'Default meta description', 'help' => 'Leave blank to use the site description.'],
- ],
- ];
- }
-}
diff --git a/Settings/Models/Setting.php b/Settings/Models/Setting.php
deleted file mode 100644
index 79c0ed6..0000000
--- a/Settings/Models/Setting.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'boolean'];
- }
-}
diff --git a/Settings/Providers/RouteServiceProvider.php b/Settings/Providers/RouteServiceProvider.php
deleted file mode 100644
index a5a3776..0000000
--- a/Settings/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Settings', 'Routes/web.php'));
- });
- }
-}
diff --git a/Settings/Providers/SettingsServiceProvider.php b/Settings/Providers/SettingsServiceProvider.php
deleted file mode 100644
index 6a5ea24..0000000
--- a/Settings/Providers/SettingsServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Settings/Resources/views/index.blade.php b/Settings/Resources/views/index.blade.php
deleted file mode 100644
index fcf669e..0000000
--- a/Settings/Resources/views/index.blade.php
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
Single source of truth
-
Site settings
-
- Global identity, branding, contact details and SEO are entered once here and reused by every theme, page, header and footer.
-
-
-
Manage menus
-
-
- @if (session('status'))
-
{{ session('status') }}
- @endif
-
-
- @csrf
- @method('PUT')
-
- @foreach ($sections as $section => $fields)
-
- @endforeach
-
-
- Save site settings
-
-
-
-
diff --git a/Settings/Routes/web.php b/Settings/Routes/web.php
deleted file mode 100644
index 3f28a23..0000000
--- a/Settings/Routes/web.php
+++ /dev/null
@@ -1,9 +0,0 @@
-prefix('administration/settings')->name('administration.settings.')->group(function (): void {
- Route::get('/', [SettingController::class, 'index'])->name('index');
- Route::put('/', [SettingController::class, 'update'])->name('update');
-});
diff --git a/Settings/Services/SettingsRepository.php b/Settings/Services/SettingsRepository.php
deleted file mode 100644
index 4e80ad7..0000000
--- a/Settings/Services/SettingsRepository.php
+++ /dev/null
@@ -1,137 +0,0 @@
-tableExists()) {
- return $default;
- }
-
- /** @var array{value: ?string, type: string}|null $setting */
- $setting = Cache::rememberForever(
- "settings.value.v2.$group.$key",
- fn (): ?array => Setting::query()
- ->where([
- 'group' => $group,
- 'key' => $key,
- ])
- ->first(['value', 'type'])
- ?->only(['value', 'type'])
- );
-
- return $setting !== null
- ? $this->decode($setting['value'], $setting['type'])
- : $default;
- }
-
- /** @return array */
- public function group(string $group): array
- {
- if (! $this->tableExists()) {
- return [];
- }
-
- return Cache::rememberForever("settings.group.$group", function () use ($group): array {
- return Setting::query()
- ->where('group', $group)
- ->get()
- ->mapWithKeys(fn (Setting $setting): array => [
- $setting->key => $this->decode($setting->value, $setting->type),
- ])
- ->all();
- });
- }
-
- /** @return array */
- public function site(): array
- {
- $defaults = [
- 'site_name' => config('app.name', 'Content Manager'),
- 'tagline' => '',
- 'site_description' => 'Modern software for modern companies.',
- 'company_name' => config('app.name', 'Content Manager'),
- 'site_url' => url('/'),
- 'logo_url' => '',
- 'logo_dark_url' => '',
- 'favicon_url' => '/favicon.ico',
- 'contact_email' => '',
- 'contact_phone' => '',
- 'address' => '',
- 'facebook_url' => '',
- 'x_url' => '',
- 'linkedin_url' => '',
- 'instagram_url' => '',
- 'copyright' => '',
- 'dashboard_label' => 'Dashboard',
- 'login_label' => 'Log in',
- 'registration_label' => 'Get started',
- 'default_meta_title' => '',
- 'default_meta_description' => '',
- 'backend_theme' => config('stylist.default_backend_theme', 'admin'),
- 'frontend_theme' => config('stylist.default_frontend_theme', 'saasland'),
- ];
-
- return array_replace($defaults, $this->group('site'));
- }
-
- public function put(string $key, mixed $value, string $group = 'general', string $type = 'string', bool $public = false): Setting
- {
- $setting = Setting::query()->updateOrCreate(
- compact('group', 'key'),
- [
- 'value' => $this->encode($value, $type),
- 'type' => $type,
- 'is_public' => $public,
- ]
- );
-
- // Remove both the legacy model cache and the scalar-only cache.
- Cache::forget("settings.$group.$key");
- Cache::forget("settings.value.v2.$group.$key");
- Cache::forget("settings.group.$group");
-
- return $setting;
- }
-
- private function tableExists(): bool
- {
- try {
- return Schema::hasTable((new Setting())->getTable());
- } catch (\Throwable) {
- return false;
- }
- }
-
- private function encode(mixed $value, string $type): ?string
- {
- if ($value === null) {
- return null;
- }
-
- return match ($type) {
- 'boolean' => filter_var($value, FILTER_VALIDATE_BOOL) ? '1' : '0',
- 'integer' => (string) (int) $value,
- 'float' => (string) (float) $value,
- 'json', 'array' => json_encode($value, JSON_THROW_ON_ERROR),
- default => (string) $value,
- };
- }
-
- private function decode(?string $value, string $type): mixed
- {
- return match ($type) {
- 'boolean' => filter_var($value, FILTER_VALIDATE_BOOL),
- 'integer' => (int) $value,
- 'float' => (float) $value,
- 'json', 'array' => filled($value) ? json_decode($value, true, 512, JSON_THROW_ON_ERROR) : [],
- default => $value,
- };
- }
-}
diff --git a/Settings/Support/ModuleInfo.php b/Settings/Support/ModuleInfo.php
deleted file mode 100644
index 6c5d3ee..0000000
--- a/Settings/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Settings');
-});
diff --git a/Settings/Tests/Feature/SiteSettingsSingleSourceTest.php b/Settings/Tests/Feature/SiteSettingsSingleSourceTest.php
deleted file mode 100644
index 41b4f99..0000000
--- a/Settings/Tests/Feature/SiteSettingsSingleSourceTest.php
+++ /dev/null
@@ -1,9 +0,0 @@
-put('site_name', 'Unified CMS', 'site', 'string', true);
- expect($repository->site()['site_name'])->toBe('Unified CMS');
-});
diff --git a/Settings/Tests/Feature/ThemeSelectionSettingsTest.php b/Settings/Tests/Feature/ThemeSelectionSettingsTest.php
deleted file mode 100644
index cae583a..0000000
--- a/Settings/Tests/Feature/ThemeSelectionSettingsTest.php
+++ /dev/null
@@ -1,27 +0,0 @@
-put('frontend_theme', 'swiss', 'site', 'string', true);
- $settings->put('backend_theme', 'admin', 'site', 'string', true);
-
- expect($settings->site())
- ->frontend_theme->toBe('swiss')
- ->backend_theme->toBe('admin');
-
- expect(app(Stylist::class)->frontend()->key())->toBe('swiss')
- ->and(app(Stylist::class)->backend()->key())->toBe('admin');
-});
-
-it('falls back to configured themes when stored theme keys are unavailable', function (): void {
- $settings = app(SettingsRepository::class);
- $settings->put('frontend_theme', 'missing-theme', 'site', 'string', true);
- $settings->put('backend_theme', 'missing-theme', 'site', 'string', true);
-
- expect(app(Stylist::class)->frontend()->key())->toBe(config('stylist.default_frontend_theme'))
- ->and(app(Stylist::class)->backend()->key())->toBe(config('stylist.default_backend_theme'));
-});
diff --git a/Settings/composer.json b/Settings/composer.json
deleted file mode 100644
index 2701db4..0000000
--- a/Settings/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/settings-module",
- "description": "Settings foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Settings\\": ""
- }
- }
-}
diff --git a/Settings/module.json b/Settings/module.json
deleted file mode 100644
index a65856b..0000000
--- a/Settings/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Settings",
- "alias": "settings",
- "version": "1.0.0",
- "description": "Settings foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "settings"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Settings\\Providers\\SettingsServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Sitemap/Config/config.php b/Sitemap/Config/config.php
deleted file mode 100644
index 02edfd4..0000000
--- a/Sitemap/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Sitemap'];
diff --git a/Sitemap/Http/Controllers/SitemapController.php b/Sitemap/Http/Controllers/SitemapController.php
deleted file mode 100644
index 3486a49..0000000
--- a/Sitemap/Http/Controllers/SitemapController.php
+++ /dev/null
@@ -1,23 +0,0 @@
-push(['loc' => url('/'), 'updated_at' => now()]);
- Page::query()->where('status', 'published')->get()->each(fn ($r) => $urls->push(['loc' => route('pages.public.show', $r->slug), 'updated_at' => $r->updated_at]));
- Blog::published()->get()->each(fn ($r) => $urls->push(['loc' => route('blog.public.show', $r->slug), 'updated_at' => $r->updated_at]));
- News::published()->get()->each(fn ($r) => $urls->push(['loc' => route('news.public.show', $r->slug), 'updated_at' => $r->updated_at]));
-
- return response()->view('sitemap::index', compact('urls'))->header('Content-Type', 'application/xml; charset=UTF-8');
- }
-}
diff --git a/Sitemap/Providers/RouteServiceProvider.php b/Sitemap/Providers/RouteServiceProvider.php
deleted file mode 100644
index d3894cc..0000000
--- a/Sitemap/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Sitemap', 'Routes/web.php'));
- }
-}
diff --git a/Sitemap/Providers/SitemapServiceProvider.php b/Sitemap/Providers/SitemapServiceProvider.php
deleted file mode 100644
index 4549e61..0000000
--- a/Sitemap/Providers/SitemapServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Sitemap', 'Config/config.php'), 'sitemap');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Sitemap', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Sitemap', 'Resources/views'), 'sitemap');
- }
-}
diff --git a/Sitemap/Resources/views/index.blade.php b/Sitemap/Resources/views/index.blade.php
deleted file mode 100644
index 0464fe2..0000000
--- a/Sitemap/Resources/views/index.blade.php
+++ /dev/null
@@ -1,7 +0,0 @@
-{!! '' !!}
-@foreach($urls as $url)
-
- {{ $url['loc'] }}
- {{ $url['updated_at']->toAtomString() }}
-
- @endforeach
diff --git a/Sitemap/Routes/web.php b/Sitemap/Routes/web.php
deleted file mode 100644
index e6ebc68..0000000
--- a/Sitemap/Routes/web.php
+++ /dev/null
@@ -1,6 +0,0 @@
-name('sitemap.index');
diff --git a/Sitemap/Tests/Feature/ModuleBootTest.php b/Sitemap/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 4b1361f..0000000
--- a/Sitemap/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Sitemap', config('sitemap.name'));
- }
-}
diff --git a/Sitemap/module.json b/Sitemap/module.json
deleted file mode 100644
index ecf9761..0000000
--- a/Sitemap/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Sitemap",
- "alias": "sitemap",
- "version": "1.0.0",
- "description": "Sitemap publishing module.",
- "keywords": ["publishing", "sitemap"],
- "priority": 30,
- "providers": ["Modules\\Sitemap\\Providers\\SitemapServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Sites/Concerns/BelongsToSites.php b/Sites/Concerns/BelongsToSites.php
deleted file mode 100644
index 7634d6e..0000000
--- a/Sites/Concerns/BelongsToSites.php
+++ /dev/null
@@ -1,32 +0,0 @@
-morphToMany(Site::class, 'content', 'site_content', 'content_id', 'site_id')->withPivot('is_primary')->withTimestamps();
- }
-
- public function scopeForCurrentSite(Builder $query): Builder
- {
- $site = app(SiteManager::class)->current();
-
- return $site ? $query->whereHas('sites', fn (Builder $sites) => $sites->whereKey($site->id)) : $query;
- }
-
- public function assignToSite(Site|int $site, bool $primary = false): void
- {
- $id = $site instanceof Site ? $site->getKey() : $site;
- if ($primary) {
- $this->sites()->updateExistingPivot($this->sites()->pluck('sites.id'), ['is_primary' => false]);
- }
- $this->sites()->syncWithoutDetaching([$id => ['is_primary' => $primary]]);
- }
-}
diff --git a/Sites/Config/config.php b/Sites/Config/config.php
deleted file mode 100644
index 4941723..0000000
--- a/Sites/Config/config.php
+++ /dev/null
@@ -1,16 +0,0 @@
- true,
- 'session_key' => 'platform_site_id',
- 'central_domains' => array_values(array_filter(explode(',', (string) env('CENTRAL_DOMAINS', '')))),
- 'content_types' => [
- 'pages' => Page::class,
- 'blog' => Blog::class,
- 'news' => News::class,
- ],
-];
diff --git a/Sites/Contracts/SiteAwareModule.php b/Sites/Contracts/SiteAwareModule.php
deleted file mode 100644
index 61f13f5..0000000
--- a/Sites/Contracts/SiteAwareModule.php
+++ /dev/null
@@ -1,10 +0,0 @@
-id();
- $table->string('name');
- $table->string('key')->unique();
- $table->string('status')->default('active');
- $table->string('theme')->nullable();
- $table->string('default_locale', 20)->default('en');
- $table->string('timezone')->default('UTC');
- $table->boolean('is_default')->default(false);
- $table->timestamps();
- });
- Schema::create('site_domains', function (Blueprint $table): void {
- $table->id();
- $table->foreignId('site_id')->constrained()->cascadeOnDelete();
- $table->string('domain')->unique();
- $table->boolean('is_primary')->default(false);
- $table->boolean('redirect_to_primary')->default(false);
- $table->boolean('force_https')->default(true);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
- Schema::create('site_settings', function (Blueprint $table): void {
- $table->id();
- $table->foreignId('site_id')->constrained()->cascadeOnDelete();
- $table->string('group')->default('general');
- $table->string('key');
- $table->longText('value')->nullable();
- $table->string('type')->default('string');
- $table->timestamps();
- $table->unique(['site_id', 'key']);
- });
- Schema::create('site_content', function (Blueprint $table): void {
- $table->id();
- $table->foreignId('site_id')->constrained()->cascadeOnDelete();
- $table->string('content_type');
- $table->unsignedBigInteger('content_id');
- $table->boolean('is_primary')->default(false);
- $table->timestamps();
- $table->unique(['site_id', 'content_type', 'content_id']);
- $table->index(['content_type', 'content_id']);
- });
- Schema::create('site_user', function (Blueprint $table): void {
- $table->foreignId('site_id')->constrained()->cascadeOnDelete();
- $table->foreignId('user_id')->constrained()->cascadeOnDelete();
- $table->string('role')->default('editor');
- $table->timestamps();
- $table->primary(['site_id', 'user_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('site_user');
- Schema::dropIfExists('site_content');
- Schema::dropIfExists('site_settings');
- Schema::dropIfExists('site_domains');
- Schema::dropIfExists('sites');
- }
-};
diff --git a/Sites/Database/Seeders/SiteSeeder.php b/Sites/Database/Seeders/SiteSeeder.php
deleted file mode 100644
index 905f213..0000000
--- a/Sites/Database/Seeders/SiteSeeder.php
+++ /dev/null
@@ -1,16 +0,0 @@
-updateOrCreate(['key' => 'default'], ['name' => 'Default Site', 'status' => 'active', 'default_locale' => config('app.locale', 'en'), 'timezone' => config('app.timezone', 'UTC'), 'is_default' => true]);
- $site->domains()->updateOrCreate(['domain' => $host], ['is_primary' => true, 'redirect_to_primary' => false, 'force_https' => str_starts_with((string) config('app.url'), 'https://'), 'is_active' => true]);
- }
-}
diff --git a/Sites/Http/Controllers/DomainController.php b/Sites/Http/Controllers/DomainController.php
deleted file mode 100644
index 90e2dca..0000000
--- a/Sites/Http/Controllers/DomainController.php
+++ /dev/null
@@ -1,33 +0,0 @@
-validate(['domain' => ['required', 'string', 'max:255', 'regex:/^[a-z0-9.-]+$/i', Rule::unique('site_domains', 'domain')], 'is_primary' => ['sometimes', 'boolean'], 'redirect_to_primary' => ['sometimes', 'boolean'], 'force_https' => ['sometimes', 'boolean'], 'is_active' => ['sometimes', 'boolean']]);
- if ($request->boolean('is_primary')) {
- $site->domains()->update(['is_primary' => false]);
- }
- $site->domains()->create($data + ['is_primary' => $request->boolean('is_primary'), 'redirect_to_primary' => $request->boolean('redirect_to_primary'), 'force_https' => $request->boolean('force_https'), 'is_active' => $request->boolean('is_active', true)]);
-
- return back()->with('success', 'Domain added.');
- }
-
- public function destroy(Site $site, SiteDomain $domain): RedirectResponse
- {
- abort_unless($domain->site_id === $site->id, 404);
- abort_if($domain->is_primary && $site->domains()->count() === 1, 422, 'A site must retain a domain.');
- $domain->delete();
-
- return back()->with('success', 'Domain removed.');
- }
-}
diff --git a/Sites/Http/Controllers/SiteController.php b/Sites/Http/Controllers/SiteController.php
deleted file mode 100644
index 3e719f9..0000000
--- a/Sites/Http/Controllers/SiteController.php
+++ /dev/null
@@ -1,68 +0,0 @@
- Site::query()->with('domains')->orderByDesc('is_default')->orderBy('name')->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('sites::admin.sites.form', ['site' => new Site]);
- }
-
- public function store(Request $request): RedirectResponse
- {
- $site = Site::query()->create($this->validated($request));
-
- return redirect()->route('administration.sites.edit', $site)->with('success', 'Site created.');
- }
-
- public function edit(Site $site): View
- {
- return view('sites::admin.sites.form', ['site' => $site->load(['domains', 'settings'])]);
- }
-
- public function update(Request $request, Site $site): RedirectResponse
- {
- DB::transaction(function () use ($request, $site): void {
- $data = $this->validated($request, $site);
- if ($data['is_default']) {
- Site::query()->whereKeyNot($site->id)->update(['is_default' => false]);
- } $site->update($data);
- });
-
- return back()->with('success', 'Site updated.');
- }
-
- public function destroy(Site $site): RedirectResponse
- {
- abort_if($site->is_default, 422, 'The default site cannot be deleted.');
- $site->delete();
-
- return redirect()->route('administration.sites.index')->with('success', 'Site deleted.');
- }
-
- public function switch(Request $request, Site $site): RedirectResponse
- {
- $request->session()->put(config('sites.session_key'), $site->id);
-
- return back()->with('success', "Site context changed to {$site->name}.");
- }
-
- private function validated(Request $request, ?Site $site = null): array
- {
- return $request->validate(['name' => ['required', 'string', 'max:150'], 'key' => ['required', 'alpha_dash', 'max:100', Rule::unique('sites', 'key')->ignore($site)], 'status' => ['required', Rule::in(['active', 'inactive', 'maintenance'])], 'theme' => ['nullable', 'string', 'max:100'], 'default_locale' => ['required', 'string', 'max:20'], 'timezone' => ['required', 'timezone'], 'is_default' => ['sometimes', 'boolean']]) + ['is_default' => $request->boolean('is_default')];
- }
-}
diff --git a/Sites/Http/Middleware/ResolveSite.php b/Sites/Http/Middleware/ResolveSite.php
deleted file mode 100644
index fc3207d..0000000
--- a/Sites/Http/Middleware/ResolveSite.php
+++ /dev/null
@@ -1,24 +0,0 @@
-sites->resolve($request);
- if ($site) {
- config(['app.url' => $request->getSchemeAndHttpHost(), 'app.timezone' => $site->timezone ?: config('app.timezone')]);
- date_default_timezone_set((string) config('app.timezone'));
- }
-
- return $next($request);
- }
-}
diff --git a/Sites/Models/Site.php b/Sites/Models/Site.php
deleted file mode 100644
index a9c2689..0000000
--- a/Sites/Models/Site.php
+++ /dev/null
@@ -1,44 +0,0 @@
- 'boolean'];
- }
-
- public function domains(): HasMany
- {
- return $this->hasMany(SiteDomain::class);
- }
-
- public function settings(): HasMany
- {
- return $this->hasMany(SiteSetting::class);
- }
-
- public function assignments(): HasMany
- {
- return $this->hasMany(SiteContent::class);
- }
-
- public function scopeActive(Builder $query): Builder
- {
- return $query->where('status', 'active');
- }
-
- public function setting(string $key, mixed $default = null): mixed
- {
- $setting = $this->relationLoaded('settings') ? $this->settings->firstWhere('key', $key) : $this->settings()->where('key', $key)->first();
-
- return $setting?->typedValue() ?? $default;
- }
-}
diff --git a/Sites/Models/SiteContent.php b/Sites/Models/SiteContent.php
deleted file mode 100644
index c359fbb..0000000
--- a/Sites/Models/SiteContent.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'boolean'];
- }
-
- public function site(): BelongsTo
- {
- return $this->belongsTo(Site::class);
- }
-
- public function content(): MorphTo
- {
- return $this->morphTo(__FUNCTION__, 'content_type', 'content_id');
- }
-}
diff --git a/Sites/Models/SiteDomain.php b/Sites/Models/SiteDomain.php
deleted file mode 100644
index b92e2cf..0000000
--- a/Sites/Models/SiteDomain.php
+++ /dev/null
@@ -1,21 +0,0 @@
- 'boolean', 'redirect_to_primary' => 'boolean', 'force_https' => 'boolean', 'is_active' => 'boolean'];
- }
-
- public function site(): BelongsTo
- {
- return $this->belongsTo(Site::class);
- }
-}
diff --git a/Sites/Models/SiteSetting.php b/Sites/Models/SiteSetting.php
deleted file mode 100644
index 249314a..0000000
--- a/Sites/Models/SiteSetting.php
+++ /dev/null
@@ -1,26 +0,0 @@
-belongsTo(Site::class);
- }
-
- public function typedValue(): mixed
- {
- return match ($this->type) {
- 'boolean' => filter_var($this->value, FILTER_VALIDATE_BOOLEAN),
- 'integer' => (int) $this->value,
- 'json' => json_decode((string) $this->value, true),
- default => $this->value,
- };
- }
-}
diff --git a/Sites/Providers/RouteServiceProvider.php b/Sites/Providers/RouteServiceProvider.php
deleted file mode 100644
index f4820fa..0000000
--- a/Sites/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Sites', 'Routes/web.php'));
- }
-}
diff --git a/Sites/Providers/SitesServiceProvider.php b/Sites/Providers/SitesServiceProvider.php
deleted file mode 100644
index fbfd18f..0000000
--- a/Sites/Providers/SitesServiceProvider.php
+++ /dev/null
@@ -1,22 +0,0 @@
-mergeConfigFrom(module_path('Sites', 'Config/config.php'), 'sites');
- $this->app->singleton(SiteManager::class);
- $this->app->register(RouteServiceProvider::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Sites', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Sites', 'Resources/views'), 'sites');
- }
-}
diff --git a/Sites/Resources/views/admin/sites/form.blade.php b/Sites/Resources/views/admin/sites/form.blade.php
deleted file mode 100644
index 987ee09..0000000
--- a/Sites/Resources/views/admin/sites/form.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-{{ $site->exists ? 'Edit site' : 'New site' }} Configure the site identity, theme and regional defaults.
@csrf @if($site->exists) @method('PUT') @endifstatus)==='active')>Active status)==='inactive')>Inactive status)==='maintenance')>Maintenance
Default site Save site Cancel
@if($site->exists)
Domains @foreach($site->domains as $domain)
{{ $domain->domain }} @if($domain->is_primary)Primary @endif
@csrf @method('DELETE')Remove @endforeach
@csrfPrimary Force HTTPS Active
Add domain
@endif
diff --git a/Sites/Resources/views/admin/sites/index.blade.php b/Sites/Resources/views/admin/sites/index.blade.php
deleted file mode 100644
index dc7414e..0000000
--- a/Sites/Resources/views/admin/sites/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Sites Manage websites, domains, themes and regional defaults.
New site Site Domains Theme Status @foreach($sites as $site){{ $site->name }} @if($site->is_default)Default @endif {{ $site->domains->pluck('domain')->join(', ') ?: 'No domains' }} {{ $site->theme ?: 'Inherited' }} {{ ucfirst($site->status) }} Edit @endforeach
{{ $sites->links() }}
diff --git a/Sites/Routes/web.php b/Sites/Routes/web.php
deleted file mode 100644
index 78ebd55..0000000
--- a/Sites/Routes/web.php
+++ /dev/null
@@ -1,12 +0,0 @@
-prefix('administration')->name('administration.')->group(function (): void {
- Route::post('sites/{site}/switch', [SiteController::class, 'switch'])->name('sites.switch');
- Route::resource('sites', SiteController::class)->except('show');
- Route::post('sites/{site}/domains', [DomainController::class, 'store'])->name('sites.domains.store');
- Route::delete('sites/{site}/domains/{domain}', [DomainController::class, 'destroy'])->name('sites.domains.destroy');
-});
diff --git a/Sites/Services/SiteManager.php b/Sites/Services/SiteManager.php
deleted file mode 100644
index c9e1306..0000000
--- a/Sites/Services/SiteManager.php
+++ /dev/null
@@ -1,76 +0,0 @@
-current;
- }
-
- public function setCurrent(?Site $site): void
- {
- $this->current = $site;
-
- if ($site) {
- app()->instance(Site::class, $site);
- app()->instance('currentSite', $site);
- }
- }
-
- public function all(): Collection
- {
- if (! $this->tablesExist()) {
- return collect();
- }
-
- return Site::query()
- ->active()
- ->orderByDesc('is_default')
- ->orderBy('name')
- ->get();
- }
-
- public function resolve(Request $request): ?Site
- {
- if (! $this->tablesExist()) {
- $this->setCurrent(null);
-
- return null;
- }
-
- $host = strtolower(preg_replace('/:\d+$/', '', $request->getHost()));
-
- $site = Site::query()
- ->with(['domains', 'settings'])
- ->whereHas('domains', fn ($query) => $query
- ->where('domain', $host)
- ->where('is_active', true))
- ->first();
-
- if (! $site && config('sites.fallback_to_default')) {
- $site = Site::query()
- ->with(['domains', 'settings'])
- ->active()
- ->where('is_default', true)
- ->first();
- }
-
- $this->setCurrent($site);
-
- return $site;
- }
-
- private function tablesExist(): bool
- {
- return Schema::hasTable('sites') && Schema::hasTable('site_domains');
- }
-}
diff --git a/Sites/Tests/Feature/SitesModuleBootTest.php b/Sites/Tests/Feature/SitesModuleBootTest.php
deleted file mode 100644
index 94916ed..0000000
--- a/Sites/Tests/Feature/SitesModuleBootTest.php
+++ /dev/null
@@ -1,14 +0,0 @@
-assertTrue(app('router')->has('administration.sites.index'));
- $this->assertTrue(app('router')->has('administration.sites.domains.store'));
- }
-}
diff --git a/Sites/module.json b/Sites/module.json
deleted file mode 100644
index a8886dd..0000000
--- a/Sites/module.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "Sites",
- "alias": "sites",
- "description": "Multi-site domains, context, settings, content assignments and administration.",
- "keywords": ["multisite", "sites", "domains", "tenancy"],
- "priority": 35,
- "providers": ["Modules\\Sites\\Providers\\SitesServiceProvider"],
- "files": []
-}
diff --git a/Stylist/Config/config.php b/Stylist/Config/config.php
deleted file mode 100644
index 71b15d3..0000000
--- a/Stylist/Config/config.php
+++ /dev/null
@@ -1,29 +0,0 @@
- env('STYLIST_DEFAULT_THEME', 'admin'),
- 'default_backend_theme' => env('STYLIST_BACKEND_THEME', 'admin'),
- 'default_frontend_theme' => env('STYLIST_FRONTEND_THEME', 'saasland'),
-
- /*
- | Themes are application resources, never module resources.
- | Every theme is self-contained and owns its views and assets.
- */
- 'themes_path' => env('STYLIST_THEMES_PATH', resource_path('themes')),
-
- 'tenant_theme_column' => 'theme_key',
- 'tenant_brand_columns' => [
- 'brand_primary' => 'brand-primary',
- 'brand_secondary' => 'brand-secondary',
- 'brand_accent' => 'brand-accent',
- ],
-
- 'fallback_variables' => [
- 'brand-primary' => '#2563eb',
- 'brand-secondary' => '#0f172a',
- 'brand-accent' => '#22c55e',
- 'surface' => '#ffffff',
- 'surface-muted' => '#f8fafc',
- 'text-primary' => '#0f172a',
- ],
-];
diff --git a/Stylist/Console/CreateThemeCommand.php b/Stylist/Console/CreateThemeCommand.php
deleted file mode 100644
index c7e1dff..0000000
--- a/Stylist/Console/CreateThemeCommand.php
+++ /dev/null
@@ -1,64 +0,0 @@
-argument('key')));
- if (! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $key)) {
- $this->error('Theme keys may contain lowercase letters, numbers, dashes, and underscores only.');
-
- return self::FAILURE;
- }
-
- $path = rtrim((string) config('stylist.themes_path'), '/\\').DIRECTORY_SEPARATOR.$key;
- if ($this->files->exists($path)) {
- $this->error("Theme [{$key}] already exists.");
-
- return self::FAILURE;
- }
-
- $parent = trim((string) $this->option('parent'));
- $name = trim((string) ($this->option('name') ?: str($key)->headline()));
- $this->files->ensureDirectoryExists($path.'/views');
-
- $manifest = [
- 'manifest_version' => 1,
- 'name' => $name,
- 'slug' => $key,
- 'version' => '1.0.0',
- 'description' => "Child theme for {$name}.",
- 'extends' => $parent !== '' ? $parent : null,
- 'entry_view' => 'home',
- 'views' => true,
- 'variables' => [],
- 'settings' => [],
- ];
-
- if ($manifest['extends'] === null) {
- unset($manifest['extends']);
- }
-
- $this->files->put($path.'/theme.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);
- $this->files->put($path.'/views/.gitkeep', '');
-
- $this->info("Theme [{$key}] created at {$path}.");
- $this->line('Shared views will resolve through its parent; only add genuine overrides.');
-
- return self::SUCCESS;
- }
-}
diff --git a/Stylist/Console/FindDuplicateViewsCommand.php b/Stylist/Console/FindDuplicateViewsCommand.php
deleted file mode 100644
index 2304ca1..0000000
--- a/Stylist/Console/FindDuplicateViewsCommand.php
+++ /dev/null
@@ -1,40 +0,0 @@
-inspector->duplicateViews();
- if ($duplicates === []) {
- $this->info('No duplicate theme views found.');
-
- return self::SUCCESS;
- }
-
- $rows = [];
- foreach ($duplicates as $view => $owners) {
- foreach ($owners as $owner) {
- $rows[] = [$view, $owner['theme'], $owner['path']];
- }
- }
-
- $this->table(['View', 'Theme', 'Path'], $rows);
- $this->error(count($duplicates).' duplicate view task(s) found.');
-
- return self::FAILURE;
- }
-}
diff --git a/Stylist/Console/InspectThemesCommand.php b/Stylist/Console/InspectThemesCommand.php
deleted file mode 100644
index 0764a1f..0000000
--- a/Stylist/Console/InspectThemesCommand.php
+++ /dev/null
@@ -1,28 +0,0 @@
-inspector->themes();
- $this->table(['Key', 'Name', 'Parent', 'Entry view', 'Views', 'Path'], array_map(fn (array $theme): array => [
- $theme['key'], $theme['name'], $theme['parent'] ?? '—', $theme['entry_view'], $theme['views'], $theme['path'],
- ], $themes));
-
- return self::SUCCESS;
- }
-}
diff --git a/Stylist/Console/PublishAssetsCommand.php b/Stylist/Console/PublishAssetsCommand.php
deleted file mode 100644
index ab661fa..0000000
--- a/Stylist/Console/PublishAssetsCommand.php
+++ /dev/null
@@ -1,34 +0,0 @@
-option('force');
-
- $this->call('vendor:publish', [
- '--tag' => 'stylist-config',
- '--force' => $force,
- ]);
-
- $themesPath = (string) config('stylist.themes_path', resource_path('themes'));
-
- if (! is_dir($themesPath) && ! mkdir($themesPath, 0775, true) && ! is_dir($themesPath)) {
- $this->components->error("Unable to create theme directory: {$themesPath}");
-
- return self::FAILURE;
- }
-
- $this->components->info("Stylist config published. Themes remain in: {$themesPath}");
-
- return self::SUCCESS;
- }
-}
diff --git a/Stylist/Console/ThemeDoctorCommand.php b/Stylist/Console/ThemeDoctorCommand.php
deleted file mode 100644
index d59cb3d..0000000
--- a/Stylist/Console/ThemeDoctorCommand.php
+++ /dev/null
@@ -1,38 +0,0 @@
-inspector->issues();
- if ($issues === []) {
- $this->info('Stylist is healthy. No theme issues found.');
-
- return self::SUCCESS;
- }
-
- $this->table(['Severity', 'Theme', 'Message'], array_map(fn (array $issue): array => [
- strtoupper($issue['severity']), $issue['theme'], $issue['message'],
- ], $issues));
-
- $errors = count(array_filter($issues, fn (array $issue): bool => $issue['severity'] === 'error'));
- $warnings = count($issues) - $errors;
- $this->line("{$errors} error(s), {$warnings} warning(s).");
-
- return $errors > 0 ? self::FAILURE : self::SUCCESS;
- }
-}
diff --git a/Stylist/Console/ValidateThemeCommand.php b/Stylist/Console/ValidateThemeCommand.php
deleted file mode 100644
index 5e21f93..0000000
--- a/Stylist/Console/ValidateThemeCommand.php
+++ /dev/null
@@ -1,108 +0,0 @@
-resolveThemePath((string) $this->argument('theme'));
- $theme = $this->loader->fromPath($path);
- $issues = $this->validator->validate($theme);
-
- $this->info("Theme: {$theme->name()} ({$theme->key()})");
- $this->line('Manifest schema: '.$theme->manifestVersion());
-
- if ($issues === []) {
- $this->info('Theme validation passed with no issues.');
-
- return self::SUCCESS;
- }
-
- $this->table(
- ['Severity', 'File', 'Message'],
- array_map(fn (array $issue) => [
- strtoupper($issue['severity']),
- $issue['file'],
- $issue['message'],
- ], $issues),
- );
-
- $errors = count(array_filter($issues, fn (array $issue) => $issue['severity'] === 'error'));
- $warnings = count($issues) - $errors;
- $this->line("{$errors} error(s), {$warnings} warning(s).");
-
- return $errors > 0 ? self::FAILURE : self::SUCCESS;
- } catch (\Throwable $exception) {
- $this->error($exception->getMessage());
-
- return self::FAILURE;
- } finally {
- if ($temporary !== null) {
- $this->files->deleteDirectory($temporary);
- }
- }
- }
-
- /** @return array{0:string,1:?string} */
- private function resolveThemePath(string $input): array
- {
- if ($this->files->isDirectory($input)) {
- return [realpath($input) ?: $input, null];
- }
-
- if ($this->files->isFile($input) && strtolower(pathinfo($input, PATHINFO_EXTENSION)) === 'zip') {
- $temporary = storage_path('app/stylist/validate-'.bin2hex(random_bytes(8)));
- $this->files->ensureDirectoryExists($temporary);
-
- $zip = new ZipArchive;
- if ($zip->open($input) !== true) {
- throw new RuntimeException('The theme ZIP could not be opened.');
- }
-
- for ($index = 0; $index < $zip->numFiles; $index++) {
- $name = (string) $zip->getNameIndex($index);
- if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) {
- $zip->close();
- throw new RuntimeException('The ZIP contains an unsafe path.');
- }
- }
-
- $zip->extractTo($temporary);
- $zip->close();
-
- $manifest = collect($this->files->allFiles($temporary))
- ->first(fn ($file) => $file->getFilename() === 'theme.json');
-
- if (! $manifest) {
- throw new RuntimeException('The package does not contain theme.json.');
- }
-
- return [$manifest->getPath(), $temporary];
- }
-
- return [$this->loader->find($input)->path(), null];
- }
-}
diff --git a/Stylist/Database/Migrations/2026_07_08_000000_add_stylist_fields_to_companies_table.php b/Stylist/Database/Migrations/2026_07_08_000000_add_stylist_fields_to_companies_table.php
deleted file mode 100644
index 2396070..0000000
--- a/Stylist/Database/Migrations/2026_07_08_000000_add_stylist_fields_to_companies_table.php
+++ /dev/null
@@ -1,56 +0,0 @@
-string('theme_key')->default('default')->after('timezone');
- }
-
- if (! Schema::hasColumn('companies', 'brand_primary')) {
- $table->string('brand_primary')->nullable()->after('theme_key');
- }
-
- if (! Schema::hasColumn('companies', 'brand_secondary')) {
- $table->string('brand_secondary')->nullable()->after('brand_primary');
- }
-
- if (! Schema::hasColumn('companies', 'brand_accent')) {
- $table->string('brand_accent')->nullable()->after('brand_secondary');
- }
-
- if (! Schema::hasColumn('companies', 'logo_path')) {
- $table->string('logo_path')->nullable()->after('brand_accent');
- }
-
- if (! Schema::hasColumn('companies', 'theme_settings')) {
- $table->json('theme_settings')->nullable()->after('logo_path');
- }
- });
- }
-
- public function down(): void
- {
- if (! Schema::hasTable('companies')) {
- return;
- }
-
- Schema::table('companies', function (Blueprint $table): void {
- foreach (['theme_settings', 'logo_path', 'brand_accent', 'brand_secondary', 'brand_primary', 'theme_key'] as $column) {
- if (Schema::hasColumn('companies', $column)) {
- $table->dropColumn($column);
- }
- }
- });
- }
-};
diff --git a/Stylist/Database/Migrations/2026_07_21_000001_create_stylist_theme_settings_table.php b/Stylist/Database/Migrations/2026_07_21_000001_create_stylist_theme_settings_table.php
deleted file mode 100644
index 57b6955..0000000
--- a/Stylist/Database/Migrations/2026_07_21_000001_create_stylist_theme_settings_table.php
+++ /dev/null
@@ -1,24 +0,0 @@
-id();
- $table->string('theme_key')->unique();
- $table->string('variant')->default('light');
- $table->json('settings')->nullable();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('stylist_theme_settings');
- }
-};
diff --git a/Stylist/Diagnostics/ThemeInspector.php b/Stylist/Diagnostics/ThemeInspector.php
deleted file mode 100644
index 371d543..0000000
--- a/Stylist/Diagnostics/ThemeInspector.php
+++ /dev/null
@@ -1,147 +0,0 @@
-> */
- public function themes(): array
- {
- return $this->loader->all()->map(function (Theme $theme): array {
- return [
- 'key' => $theme->key(),
- 'name' => $theme->name(),
- 'parent' => $theme->parent(),
- 'path' => $theme->path(),
- 'views' => count($this->viewFiles($theme)),
- 'entry_view' => $theme->entryView(),
- ];
- })->values()->all();
- }
-
- /** @return array> */
- public function duplicateViews(): array
- {
- $ownership = [];
-
- foreach ($this->loader->all() as $theme) {
- foreach ($this->viewFiles($theme) as $relative => $absolute) {
- $ownership[$relative][] = [
- 'theme' => $theme->key(),
- 'path' => $absolute,
- ];
- }
- }
-
- return array_filter($ownership, fn (array $owners): bool => count($owners) > 1);
- }
-
- /** @return array */
- public function issues(): array
- {
- $issues = [];
- $themes = $this->loader->all();
-
- foreach ($themes as $theme) {
- $parent = $theme->parent();
-
- if ($parent !== null && ! $themes->has($parent)) {
- $issues[] = [
- 'severity' => 'error',
- 'theme' => $theme->key(),
- 'message' => "Parent theme [{$parent}] is not installed.",
- ];
- }
-
- if ($this->hasInheritanceCycle($theme, $themes->all())) {
- $issues[] = [
- 'severity' => 'error',
- 'theme' => $theme->key(),
- 'message' => 'Theme inheritance contains a cycle.',
- ];
- }
-
- $entry = $theme->path().'/views/'.str_replace('.', '/', $theme->entryView()).'.blade.php';
- if (! $this->files->exists($entry) && $parent === null) {
- $issues[] = [
- 'severity' => 'warning',
- 'theme' => $theme->key(),
- 'message' => "Entry view [{$theme->entryView()}] does not exist in this theme.",
- ];
- }
- }
-
- foreach ($this->duplicateViews() as $view => $owners) {
- $issues[] = [
- 'severity' => 'error',
- 'theme' => implode(', ', array_column($owners, 'theme')),
- 'message' => "View [{$view}] has multiple owners.",
- ];
- }
-
- $default = (string) config('stylist.default_theme');
- if ($default !== '' && ! $themes->has($default)) {
- $issues[] = [
- 'severity' => 'warning',
- 'theme' => $default,
- 'message' => 'Configured default theme is not installed.',
- ];
- }
-
- return $issues;
- }
-
- /** @return array */
- private function viewFiles(Theme $theme): array
- {
- $root = $theme->path().'/views';
- if (! $this->files->isDirectory($root)) {
- return [];
- }
-
- $views = [];
- foreach ($this->files->allFiles($root) as $file) {
- if (! str_ends_with($file->getFilename(), '.blade.php')) {
- continue;
- }
-
- $absolute = str_replace('\\', '/', $file->getPathname());
- $relative = ltrim(str_replace(str_replace('\\', '/', $root), '', $absolute), '/');
- $views[$relative] = $absolute;
- }
-
- ksort($views);
-
- return $views;
- }
-
- /** @param array $themes */
- private function hasInheritanceCycle(Theme $theme, array $themes): bool
- {
- $seen = [];
- $key = $theme->key();
-
- while (isset($themes[$key])) {
- if (isset($seen[$key])) {
- return true;
- }
-
- $seen[$key] = true;
- $key = $themes[$key]->parent() ?? '';
- if ($key === '') {
- return false;
- }
- }
-
- return false;
- }
-}
diff --git a/Stylist/Facades/StylistFacade.php b/Stylist/Facades/StylistFacade.php
deleted file mode 100644
index 77d173e..0000000
--- a/Stylist/Facades/StylistFacade.php
+++ /dev/null
@@ -1,13 +0,0 @@
-make($view, $data, $mergeData);
- }
-}
-
-if (! function_exists('stylist_theme_view')) {
- function stylist_theme_view(string $theme, string $view, array $data = [], array $mergeData = []): View
- {
- return app(ThemeViewResolver::class)->makeForTheme($theme, $view, $data, $mergeData);
- }
-}
-
-if (! function_exists('stylist_frontend_view')) {
- function stylist_frontend_view(string $view, array $data = [], array $mergeData = []): View
- {
- return app(ThemeViewResolver::class)->makeForTheme(stylist()->frontend()->key(), $view, $data, $mergeData);
- }
-}
-
-if (! function_exists('stylist_backend_view')) {
- function stylist_backend_view(string $view, array $data = [], array $mergeData = []): View
- {
- return app(ThemeViewResolver::class)->makeForTheme(stylist()->backend()->key(), $view, $data, $mergeData);
- }
-}
-
-if (! function_exists('theme_asset')) {
- function theme_asset(string $path): string
- {
- return app(UrlGenerator::class)->asset($path);
- }
-}
-
-if (! function_exists('theme_switcher')) {
- function theme_switcher(): HtmlString
- {
- return stylist_html()->switcher();
- }
-}
diff --git a/Stylist/Html/ThemeHtmlBuilder.php b/Stylist/Html/ThemeHtmlBuilder.php
deleted file mode 100644
index b7b3c16..0000000
--- a/Stylist/Html/ThemeHtmlBuilder.php
+++ /dev/null
@@ -1,99 +0,0 @@
-stylist->current()->stylesheet();
- if (! $this->stylist->current()->has($path)) {
- return new HtmlString('');
- }
-
- return new HtmlString(' ');
- }
-
- public function script(?string $path = null): HtmlString
- {
- $path ??= $this->stylist->current()->script();
- if (! $this->stylist->current()->has($path)) {
- return new HtmlString('');
- }
-
- return new HtmlString('');
- }
-
- public function variables(): HtmlString
- {
- $theme = $this->stylist->current();
- $default = $this->declarations($this->stylist->variables());
- $rules = [':root{'.$default.'}'];
-
- foreach ($theme->appearances() as $name => $definition) {
- $vars = (array) data_get($definition, 'variables', []);
- if ($vars) {
- $rules[] = ':root[data-theme="'.$this->selector($name).'"]{'.$this->declarations($vars).'}';
- }
- }
- foreach ($theme->accents() as $name => $definition) {
- $vars = (array) data_get($definition, 'variables', []);
- if ($vars) {
- $rules[] = ':root[data-accent="'.$this->selector($name).'"]{'.$this->declarations($vars).'}';
- }
- }
-
- if ($theme->appearances() === [] && $theme->accents() === []) {
- return new HtmlString('');
- }
-
- $appearance = json_encode($this->stylist->appearance(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
- $accent = json_encode($this->stylist->accent(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
- $themeKey = json_encode($theme->key(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
- $hasAppearances = $theme->appearances() !== [] ? 'true' : 'false';
- $hasAccents = $theme->accents() !== [] ? 'true' : 'false';
-
- $script = <<(function(){var d=document.documentElement,k={$themeKey},ha={$hasAppearances},hc={$hasAccents},a=ha?(localStorage.getItem('stylist:'+k+':appearance')||{$appearance}):'',c=hc?(localStorage.getItem('stylist:'+k+':accent')||{$accent}):'';function apply(){if(ha){var dark=a==='dark'||((a==='auto'||a==='system')&&matchMedia('(prefers-color-scheme: dark)').matches);d.dataset.theme=dark?'dark':'light';d.dataset.appearance=a;d.classList.toggle('dark',dark)}if(hc){d.dataset.accent=c}}apply();window.StylistTheme={setAppearance:function(v){if(!ha)return;a=v;localStorage.setItem('stylist:'+k+':appearance',v);apply()},setAccent:function(v){if(!hc)return;c=v;localStorage.setItem('stylist:'+k+':accent',v);apply()},appearance:function(){return a},accent:function(){return c}};if(ha){matchMedia('(prefers-color-scheme: dark)').addEventListener?.('change',function(){if(a==='auto'||a==='system')apply()})}document.dispatchEvent(new CustomEvent('stylist:ready',{detail:window.StylistTheme}))})();
-JS;
-
- return new HtmlString(''.$script);
- }
-
- public function switcher(): HtmlString
- {
- $theme = $this->stylist->current();
- $appearances = array_keys($theme->appearances());
- $accents = array_keys($theme->accents());
-
- if ($appearances === [] && $accents === []) {
- return new HtmlString('');
- }
-
- $appearanceOptions = collect($appearances)->map(fn ($name) => ''.e(ucfirst($name)).' ')->implode('');
- $accentOptions = collect($accents)->map(fn ($name) => ''.e(ucfirst($name)).' ')->implode('');
- $appearanceSelect = $appearances ? 'Appearance '.$appearanceOptions.' ' : '';
- $accentSelect = $accents ? 'Accent '.$accentOptions.' ' : '';
-
- return new HtmlString(''.$appearanceSelect.$accentSelect.'
');
- }
-
- private function declarations(array $variables): string
- {
- return collect($variables)
- ->filter(fn ($value, $key) => is_string($key) && (is_string($value) || is_numeric($value)))
- ->map(fn ($value, string $key) => '--'.ltrim($key, '-').': '.e((string) $value).';')
- ->implode('');
- }
-
- private function selector(string $value): string
- {
- return preg_replace('/[^a-z0-9_-]/i', '', $value) ?: 'default';
- }
-}
diff --git a/Stylist/Html/ThemeSeoBuilder.php b/Stylist/Html/ThemeSeoBuilder.php
deleted file mode 100644
index 28d5840..0000000
--- a/Stylist/Html/ThemeSeoBuilder.php
+++ /dev/null
@@ -1,44 +0,0 @@
-stylist->current();
- $seo = array_replace($theme->seo(), $overrides);
- $settings = $this->stylist->settings();
- $title = (string) ($seo['title'] ?? $settings['site_title'] ?? $theme->name());
- $description = (string) ($seo['description'] ?? $settings['site_description'] ?? $theme->description());
- $image = $seo['image'] ?? $theme->preview();
- if ($image && $theme->has($image)) {
- $image = $this->urls->asset($image);
- }
- $tags = [''.e($title).' '];
- if ($description !== '') {
- $tags[] = ' ';
- }
- foreach (['og:title' => $title, 'og:description' => $description, 'twitter:card' => 'summary_large_image', 'twitter:title' => $title, 'twitter:description' => $description] as $property => $content) {
- if ($content === '') {
- continue;
- }$attribute = str_starts_with($property, 'og:') ? 'property' : 'name';
- $tags[] = ' ';
- }
- if ($image) {
- $tags[] = ' ';
- $tags[] = ' ';
- }
- if (isset($seo['json_ld']) && is_array($seo['json_ld'])) {
- $tags[] = '';
- }
-
- return new HtmlString(implode("\n", $tags));
- }
-}
diff --git a/Stylist/Http/Controllers/StylistController.php b/Stylist/Http/Controllers/StylistController.php
deleted file mode 100644
index 219359e..0000000
--- a/Stylist/Http/Controllers/StylistController.php
+++ /dev/null
@@ -1,23 +0,0 @@
- $stylist->current(), 'themes' => $stylist->all(), 'variables' => $stylist->variables(),
- 'settingsService' => $settings, 'accessibility' => $accessibility, 'urls' => $urls,
- 'activeFrontendTheme' => $siteSettings->get('frontend_theme', config('stylist.default_frontend_theme', 'saasland'), 'site'),
- 'activeBackendTheme' => $siteSettings->get('backend_theme', config('stylist.default_backend_theme', 'admin'), 'site'),
- ]);
- }
-}
diff --git a/Stylist/Http/Controllers/ThemeAssetController.php b/Stylist/Http/Controllers/ThemeAssetController.php
deleted file mode 100644
index e7695d1..0000000
--- a/Stylist/Http/Controllers/ThemeAssetController.php
+++ /dev/null
@@ -1,64 +0,0 @@
-find($theme);
- $themeRoot = realpath($loadedTheme->path());
- abort_unless($themeRoot !== false, 404);
-
- $asset = realpath($themeRoot.DIRECTORY_SEPARATOR.$normalised);
-
- abort_unless(
- $asset !== false
- && str_starts_with($asset, $themeRoot.DIRECTORY_SEPARATOR)
- && is_file($asset),
- 404
- );
-
- $mime = match ($extension) {
- 'css' => 'text/css; charset=UTF-8',
- 'js', 'mjs' => 'application/javascript; charset=UTF-8',
- 'svg' => 'image/svg+xml',
- 'json', 'map', 'webmanifest' => 'application/json',
- 'woff' => 'font/woff',
- 'woff2' => 'font/woff2',
- 'ttf' => 'font/ttf',
- 'otf' => 'font/otf',
- default => mime_content_type($asset) ?: 'application/octet-stream',
- };
-
- return response()->file($asset, [
- 'Content-Type' => $mime,
- 'Cache-Control' => app()->isProduction()
- ? 'public, max-age=31536000, immutable'
- : 'no-cache, private',
- 'X-Content-Type-Options' => 'nosniff',
- 'Content-Security-Policy' => "default-src 'none'; style-src 'unsafe-inline'",
- ]);
- }
-}
diff --git a/Stylist/Http/Controllers/ThemeManagementController.php b/Stylist/Http/Controllers/ThemeManagementController.php
deleted file mode 100644
index 3c4f12f..0000000
--- a/Stylist/Http/Controllers/ThemeManagementController.php
+++ /dev/null
@@ -1,87 +0,0 @@
-find($theme);
- abort_unless(in_array($item->type(), ['frontend', 'backend'], true), 422, 'Only frontend and backend themes can be activated.');
-
- $settings->put($item->type().'_theme', $item->key(), 'site', 'string', true);
-
- return back()->with('status', $item->name().' is now the active '.$item->type().' theme.');
- }
-
- public function preview(string $theme, Loader $loader, ThemeViewResolver $views)
- {
- $item = $loader->find($theme);
-
- return $views->makeForTheme($item->key(), $item->entryView(), [
- 'themePreview' => true,
- 'previewTheme' => $item,
- ]);
- }
-
- public function settings(Request $request, string $theme, Loader $loader, ThemeSettings $settings)
- {
- $item = $loader->find($theme);
- $settings->save(
- $item,
- (array) $request->input('settings', []),
- (string) $request->input('variant', 'light'),
- $request->string('appearance')->toString() ?: null,
- $request->string('accent')->toString() ?: null,
- );
-
- return back()->with('status', 'Theme settings saved.');
- }
-
- public function install(Request $request, ThemePackageManager $packages)
- {
- $request->validate(['theme' => ['required', 'file', 'mimes:zip', 'max:51200']]);
- $theme = $packages->install($request->file('theme'));
-
- return back()->with('status', $theme->name().' installed.');
- }
-
- public function uninstall(string $theme, Loader $loader, ThemePackageManager $packages)
- {
- $item = $loader->find($theme);
- $packages->uninstall($item);
-
- return back()->with('status', $item->name().' uninstalled.');
- }
-
- public function export(string $theme, Loader $loader, ThemePackageManager $packages)
- {
- $item = $loader->find($theme);
- $path = $packages->export($item);
-
- return response()->download($path)->deleteFileAfterSend();
- }
-
- public function optimize(string $theme, Loader $loader, ThemeOptimizer $optimizer)
- {
- $item = $loader->find($theme);
- $result = $optimizer->optimize($item);
-
- return back()->with('status', "Optimised {$result['css']} CSS, {$result['js']} JS and {$result['images']} images.");
- }
-
- public function validateTheme(string $theme, Loader $loader, ThemeAccessibilityValidator $validator)
- {
- return response()->json(['issues' => $validator->validate($loader->find($theme))]);
- }
-}
diff --git a/Stylist/Livewire/ThemePreview.php b/Stylist/Livewire/ThemePreview.php
deleted file mode 100644
index 5e8560d..0000000
--- a/Stylist/Livewire/ThemePreview.php
+++ /dev/null
@@ -1,21 +0,0 @@
- 12,
- 'warehouses' => 34,
- 'users' => 156,
- ];
-
- public function render()
- {
- return stylist_view('livewire.theme-preview', [
- 'stats' => $this->stats,
- ]);
- }
-}
diff --git a/Stylist/Models/ThemeSetting.php b/Stylist/Models/ThemeSetting.php
deleted file mode 100644
index fb01e6f..0000000
--- a/Stylist/Models/ThemeSetting.php
+++ /dev/null
@@ -1,14 +0,0 @@
- 'array'];
-}
diff --git a/Stylist/Providers/RouteServiceProvider.php b/Stylist/Providers/RouteServiceProvider.php
deleted file mode 100644
index 68adb78..0000000
--- a/Stylist/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,22 +0,0 @@
-group(module_path('Stylist', 'Routes/assets.php'));
-
- Route::middleware('web')
- ->prefix('stylist')
- ->name('stylist.')
- ->group(module_path('Stylist', 'Routes/web.php'));
- }
-}
diff --git a/Stylist/Providers/StylistServiceProvider.php b/Stylist/Providers/StylistServiceProvider.php
deleted file mode 100644
index 31ee054..0000000
--- a/Stylist/Providers/StylistServiceProvider.php
+++ /dev/null
@@ -1,215 +0,0 @@
-app->register(RouteServiceProvider::class);
-
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
-
- $this->app->singleton('stylist', fn ($app) => $app->make(Stylist::class));
- $this->app->singleton('stylist.html', fn ($app) => $app->make(ThemeHtmlBuilder::class));
- $this->app->singleton('stylist.seo', fn ($app) => $app->make(ThemeSeoBuilder::class));
- }
-
- public function boot(): void
- {
- $this->registerViews();
- $this->registerThemeViews();
- $this->registerTranslations();
- $this->registerPublishing();
- $this->registerHelpers();
- $this->registerBlade();
-
- if ($this->app->runningInConsole()) {
- $this->commands([
- PublishAssetsCommand::class,
- ValidateThemeCommand::class,
- InspectThemesCommand::class,
- FindDuplicateViewsCommand::class,
- ThemeDoctorCommand::class,
- CreateThemeCommand::class,
- ]);
- }
-
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
-
- View::composer('*', function ($view): void {
- if (! $this->app->bound(Stylist::class)) {
- return;
- }
-
- $view->with('activeTheme', $this->app->make(Stylist::class)->current());
- });
-
-
- View::composer('stylist-theme-saasland::*', function ($view): void {
- $theme = $this->app->make(Loader::class)->find('saasland');
- $settings = $this->app->make(ThemeSettings::class)->get($theme);
- $siteSettings = $this->app->make(SettingsRepository::class)->site();
- $navigation = $this->app->make(FrontendNavigation::class);
- $homepage = $this->app->make(FrontendContent::class)->homepage($settings);
-
- $view->with([
- 'siteSettings' => $siteSettings,
- 'siteTitle' => $siteSettings['site_name'],
- 'siteDescription' => $siteSettings['site_description'],
- 'logoUrl' => $siteSettings['logo_url'] ?: null,
- 'facebookUrl' => $siteSettings['facebook_url'] ?: null,
- 'xUrl' => $siteSettings['x_url'] ?: null,
- 'linkedinUrl' => $siteSettings['linkedin_url'] ?: null,
- 'primaryNavigation' => $navigation->primary(),
- 'footerNavigation' => $navigation->footer(),
- 'themeSettings' => $settings,
- ...$homepage,
- ]);
- });
- }
-
- private function registerViews(): void
- {
- $sourcePath = module_path(ModuleInfo::name(), 'Resources/views');
- $viewPath = resource_path('views/modules/'.ModuleInfo::nameLower());
-
- $this->publishes([$sourcePath => $viewPath], ['views', 'stylist-module-views']);
- $this->loadViewsFrom([$viewPath, $sourcePath], ModuleInfo::nameLower());
- }
-
- private function registerThemeViews(): void
- {
- foreach ($this->themeRoots() as $root) {
- if (! is_dir($root)) {
- logger()->warning('Stylist theme root does not exist', [
- 'root' => $root,
- ]);
-
- continue;
- }
-
- $themeDirectories = glob($root.'/*', GLOB_ONLYDIR) ?: [];
-
- foreach ($themeDirectories as $themeDirectory) {
- $themeViewsPath = $themeDirectory.'/views';
-
- if (! is_dir($themeViewsPath)) {
- continue;
- }
-
- $themeKey = strtolower(basename($themeDirectory));
-
- // Themes always live outside the module, normally in resources/themes.
- // Register the canonical namespace and the legacy namespace as an alias
- // to the same application-owned directory. The alias keeps existing
- // routes and Blade templates working without reintroducing module themes.
- $namespaces = [
- "stylist-theme-{$themeKey}",
- "stylist-module-theme-{$themeKey}",
- ];
-
- $componentsPath = $themeViewsPath.'/components';
-
- // The common theme is the application-wide owner of layouts,
- // authentication, settings and shared components. Livewire 4
- // resolves its default full-page layout through layouts::app,
- // so point that compatibility namespace at the common theme.
- if ($themeKey === 'common') {
- $layoutsPath = $componentsPath.'/layouts';
-
- if (is_dir($layoutsPath)) {
- View::addNamespace('layouts', $layoutsPath);
- Blade::anonymousComponentPath($layoutsPath, 'layouts');
- }
-
- View::addNamespace('common', $themeViewsPath);
-
- if (is_dir($componentsPath)) {
- Blade::anonymousComponentPath($componentsPath, 'common');
- }
- }
-
- foreach ($namespaces as $namespace) {
- View::addNamespace($namespace, $themeViewsPath);
-
- if (is_dir($componentsPath)) {
- Blade::anonymousComponentPath($componentsPath, $namespace);
- }
- }
- }
- }
- }
-
- private function normalisePath(string $path): string
- {
- return rtrim(
- str_replace('\\', '/', realpath($path) ?: $path),
- '/'
- );
- }
-
- /** @return array */
- private function themeRoots(): array
- {
- return array_values(array_unique(array_filter([
- (string) config('stylist.themes_path'),
- ])));
- }
-
- private function registerTranslations(): void
- {
- $langPath = resource_path('lang/modules/'.ModuleInfo::nameLower());
- $sourcePath = module_path(ModuleInfo::name(), 'Resources/lang');
-
- $this->loadTranslationsFrom(is_dir($langPath) ? $langPath : $sourcePath, ModuleInfo::nameLower());
- }
-
- private function registerPublishing(): void
- {
- $this->publishes([
- module_path(ModuleInfo::name(), 'Config/config.php') => config_path('stylist.php'),
- ], ['config', 'stylist-config']);
-
- }
-
- private function registerHelpers(): void
- {
- foreach (glob(module_path(ModuleInfo::name(), 'Helpers/*.php')) ?: [] as $filename) {
- require_once $filename;
- }
- }
-
- private function registerBlade(): void
- {
- Blade::directive('themeVariables', fn () => 'variables(); ?>');
- Blade::directive('themeStylesheet', fn ($expression) => 'stylesheet('.($expression ?: 'null').'); ?>');
- Blade::directive('themeScript', fn ($expression) => 'script('.($expression ?: 'null').'); ?>');
- Blade::directive('themeSwitcher', fn () => 'switcher(); ?>');
- Blade::directive('themeSeo', fn ($expression) => 'render('.($expression ?: '[]').'); ?>');
- }
-}
diff --git a/Stylist/README.md b/Stylist/README.md
deleted file mode 100644
index 810bf53..0000000
--- a/Stylist/README.md
+++ /dev/null
@@ -1,183 +0,0 @@
-# Stylist Module
-
-Tenant-aware styling module for Laravel 13 / nwidart Laravel Modules.
-
-## Theme layout
-
-Each theme is self-contained:
-
-```text
-resources/themes/{theme}/
-├── theme.json
-├── css/theme.css
-└── views/
- ├── layouts/app.blade.php
- ├── components/page-hero.blade.php
- └── livewire/theme-preview.blade.php
-```
-
-The active tenant/company resolves to a theme key such as `default`, `mpba`, or `abbott`.
-
-## Rendering themed views
-
-Use the helper from controllers, Blade components, and Livewire components:
-
-```php
-return stylist_view('layouts.app');
-```
-
-It checks views in this order:
-
-```text
-stylist-theme-{active-theme}::{view}
-stylist-theme-{default-theme}::{view}
-stylist::{view}
-```
-
-## Livewire
-
-Keep the PHP component class shared and put the template in each theme:
-
-```text
-Livewire/ThemePreview.php
-resources/themes/default/views/livewire/theme-preview.blade.php
-resources/themes/mpba/views/livewire/theme-preview.blade.php
-```
-
-The component renders with:
-
-```php
-return stylist_view('livewire.theme-preview', ['stats' => $this->stats]);
-```
-
-## Tenant assignment
-
-Add or use these fields on `companies`:
-
-```text
-theme_key
-brand_primary
-brand_secondary
-brand_accent
-logo_path
-favicon_path
-theme_settings
-```
-
-Then assign:
-
-```php
-$company->update(['theme_key' => 'mpba']);
-```
-
-## Publishing
-
-```bash
-php artisan vendor:publish --tag=stylist-config
-# or
-php artisan stylist:publish-assets
-
-Themes are not published from or stored inside the module. Install them directly under `resources/themes`.
-```
-
-## Blade helpers
-
-```blade
-
- @themeVariables
- @themeStylesheet
-
-```
-
-## Self-contained application themes
-
-Application themes live in `resources/themes/{theme}` and own their runtime assets:
-
-```text
-resources/themes/example/
-├── theme.json
-├── css/theme.css
-├── js/app.js
-└── views/home.blade.php
-```
-
-Use `@themeStylesheet('css/theme.css')`, `@themeScript('js/app.js')`, or `theme_asset('images/example.svg')`. Assets are served from the selected theme directory by the `stylist.theme.asset` route; they do not need to be copied into `public/themes` or added to the global Vite inputs.
-
-
-## Appearance and colour variants
-
-Themes may declare `appearances` (`light`, `dark`, `auto`) and named `accents` in `theme.json`. Include `@themeVariables` in the document `` before the theme stylesheet to prevent a flash of the wrong appearance. Add `@themeSwitcher` anywhere in a Blade layout for a ready-made runtime selector. User choices are stored in localStorage and are applied without rebuilding assets or reloading the page.
-
-## Theme manifest compatibility
-
-Stylist accepts versioned manifests while remaining compatible with older themes. New manifests should declare:
-
-```json
-{
- "manifest_version": 1,
- "key": "design-gallery",
- "name": "Design Gallery"
-}
-```
-
-`manifest_version` defaults to `1` when omitted. Unknown manifest fields are preserved and ignored by Stylist unless the current version explicitly uses them, allowing Theme Builder to add future metadata without preventing theme discovery.
-
-Appearance and accent features are optional. Stylist derives the internal capability flags only when the corresponding definitions exist:
-
-```json
-{
- "appearances": ["light", "dark", "system"],
- "accents": ["blue", "neutral"],
- "default_appearance": "system",
- "default_accent": "neutral"
-}
-```
-
-Legacy themes with none of these fields continue to load unchanged and do not receive appearance or accent controls.
-
-Additional boolean capabilities may be declared explicitly:
-
-```json
-{
- "capabilities": {
- "marketplace": true
- }
-}
-```
-
-## Validate a theme
-
-Validate an installed theme key, unpacked theme directory, or ZIP package:
-
-```bash
-php artisan stylist:validate design-gallery
-php artisan stylist:validate resources/themes/design-gallery
-php artisan stylist:validate ~/Downloads/design-gallery.zip
-```
-
-The command checks the manifest, required entry view, stylesheet, script, declared assets, preview and accessibility issues. It exits with a failure status when errors are found.
-
-## Theme inheritance and diagnostics
-
-Shared application views belong to the `common` theme. Child themes declare inheritance without copying those views:
-
-```json
-{
- "name": "Dashboard",
- "slug": "admin",
- "extends": "common"
-}
-```
-
-Resolution order is the active theme, each declared parent, the configured fallback, the common theme, module defaults, then Laravel's normal views.
-
-Use these maintenance commands:
-
-```bash
-php artisan stylist:inspect
-php artisan stylist:duplicates
-php artisan stylist:doctor
-php artisan stylist:create-theme customer --parent=common --name="Customer Portal"
-```
-
-`stylist:duplicates` fails when the same relative Blade task path exists in more than one theme. Add a child view only when it is a deliberate replacement; this application enforces one authoritative view per task.
diff --git a/Stylist/Resources/assets/css/app.css b/Stylist/Resources/assets/css/app.css
deleted file mode 100644
index 32b4fbc..0000000
--- a/Stylist/Resources/assets/css/app.css
+++ /dev/null
@@ -1,5 +0,0 @@
-[data-theme] {
- --brand-primary: var(--brand-primary, #2563eb);
- --brand-secondary: var(--brand-secondary, #0f172a);
- --brand-accent: var(--brand-accent, #22c55e);
-}
diff --git a/Stylist/Resources/assets/js/app.js b/Stylist/Resources/assets/js/app.js
deleted file mode 100644
index f65cc9b..0000000
--- a/Stylist/Resources/assets/js/app.js
+++ /dev/null
@@ -1 +0,0 @@
-import '../css/app.css';
diff --git a/Stylist/Resources/lang/en/messages.php b/Stylist/Resources/lang/en/messages.php
deleted file mode 100644
index 56494b1..0000000
--- a/Stylist/Resources/lang/en/messages.php
+++ /dev/null
@@ -1,5 +0,0 @@
- 'Stylist',
-];
diff --git a/Stylist/Resources/views/index.blade.php b/Stylist/Resources/views/index.blade.php
deleted file mode 100644
index 19ffd23..0000000
--- a/Stylist/Resources/views/index.blade.php
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
- @if (session('status'))
- {{ session('status') }}
- @endif
- @if ($errors->any())
- {{ $errors->first() }}
- @endif
-
-
- @csrf
- Install theme ZIP
- Install theme
-
-
-
- @foreach ($themes as $item)
- @php($saved = $settingsService->get($item))
- @php($variant = $settingsService->variant($item))
- @php($issues = $accessibility->validate($item))
- @php($appearance = $settingsService->appearance($item))
- @php($accent = $settingsService->accent($item))
-
-
- @if ($item->preview() && $item->has($item->preview()))
-
- @else
-
Add preview.webp to this theme
- @endif
-
-
-
{{ $item->name() }} v{{ $item->version() }}@if($item->author()) · {{ $item->author() }}@endif
@if(($item->type() === 'frontend' && $item->key() === $activeFrontendTheme) || ($item->type() === 'backend' && $item->key() === $activeBackendTheme))
Active {{ ucfirst($item->type()) }} @endif
- @if($item->description())
{{ $item->description() }}
@endif
- @if($item->parent())
Child of {{ $item->parent() }}
@endif
-
- @if(in_array($item->type(), ['frontend', 'backend'], true))
-
@csrfActivate {{ ucfirst($item->type()) }}
-
Preview
- @endif
-
Export ZIP
-
@csrfOptimise assets
- @if(str_starts_with(realpath($item->path()) ?: '', realpath(config('stylist.themes_path')) ?: '/__never__'))
-
@csrf @method('DELETE')Uninstall
- @endif
-
-
- Theme settings
- @csrf @method('PUT')
- @if($item->variants())
- Variant @foreach(array_keys($item->variants()) as $name){{ ucfirst($name) }} @endforeach
- @endif
- @if($item->supports('dark_mode') || $item->appearances())
- Appearance @foreach(array_keys($item->appearances()) ?: ['light', 'dark', 'auto'] as $name){{ ucfirst($name) }} @endforeach
- @endif
- @if($item->accents())
-
- @endif
- @foreach($item->settingsSchema() as $key => $definition)
- @php($definition = is_array($definition) ? $definition : ['default' => $definition])
- {{ $definition['label'] ?? str($key)->headline() }}
- @switch($definition['type'] ?? 'text')
- @case('color') @break
- @case('textarea'){{ $saved[$key] ?? ($definition['default'] ?? '') }} @break
- @case('checkbox')Enabled @break
- @default
- @endswitch
-
- @endforeach
- Save settings
-
-
-
Accessibility: {{ count($issues) ? count($issues).' issue(s)' : 'passed static checks' }}
-
-
- @endforeach
-
-
diff --git a/Stylist/Routes/assets.php b/Stylist/Routes/assets.php
deleted file mode 100644
index 9662db6..0000000
--- a/Stylist/Routes/assets.php
+++ /dev/null
@@ -1,16 +0,0 @@
-where('theme', '[A-Za-z0-9_-]+')
- ->where('path', '.*')
- ->name('stylist.theme.asset');
diff --git a/Stylist/Routes/web.php b/Stylist/Routes/web.php
deleted file mode 100644
index c39ec85..0000000
--- a/Stylist/Routes/web.php
+++ /dev/null
@@ -1,17 +0,0 @@
-group(function (): void {
- Route::get('/', StylistController::class)->name('index');
- Route::post('/install', [ThemeManagementController::class, 'install'])->name('install');
- Route::put('/{theme}/settings', [ThemeManagementController::class, 'settings'])->name('settings');
- Route::post('/{theme}/activate', [ThemeManagementController::class, 'activate'])->name('activate');
- Route::get('/{theme}/preview', [ThemeManagementController::class, 'preview'])->name('preview');
- Route::delete('/{theme}', [ThemeManagementController::class, 'uninstall'])->name('uninstall');
- Route::get('/{theme}/export', [ThemeManagementController::class, 'export'])->name('export');
- Route::post('/{theme}/optimize', [ThemeManagementController::class, 'optimize'])->name('optimize');
- Route::get('/{theme}/accessibility', [ThemeManagementController::class, 'validateTheme'])->name('accessibility');
-});
diff --git a/Stylist/Services/FrontendContent.php b/Stylist/Services/FrontendContent.php
deleted file mode 100644
index e57aac5..0000000
--- a/Stylist/Services/FrontendContent.php
+++ /dev/null
@@ -1,102 +0,0 @@
- */
- public function homepage(array $settings): array
- {
- // Always provide source collections. Visibility is owned by page placements
- // in the composable content system; the legacy theme still applies its
- // existing show_* settings while no composition has been installed.
- return [
- 'pricingPlans' => $this->pricingPlans(),
- 'testimonials' => $this->testimonials(),
- 'faqs' => $this->faqs(),
- 'teamMembers' => $this->team(),
- 'latestBlogPosts' => $this->blogPosts(),
- 'latestNewsArticles' => $this->newsArticles(),
- ];
- }
-
- private function pricingPlans(): Collection
- {
- return $this->query('products', fn () => Product::query()
- ->where('is_active', true)
- ->where('product_type', 'plan')
- ->orderBy('sort_order')
- ->orderBy('id')
- ->limit(6)
- ->get());
- }
-
- private function testimonials(): Collection
- {
- return $this->query('testimonials', fn () => Testimonial::query()
- ->where('enabled', true)
- ->latest('id')
- ->limit(6)
- ->get());
- }
-
- private function faqs(): Collection
- {
- return $this->query('faqs', fn () => Faq::query()
- ->where('enabled', true)
- ->orderBy('sort_order')
- ->limit(12)
- ->get());
- }
-
- private function team(): Collection
- {
- return $this->query('team_members', fn () => TeamMember::query()
- ->where('enabled', true)
- ->orderBy('sort_order')
- ->limit(8)
- ->get());
- }
-
- private function blogPosts(): Collection
- {
- return $this->query('blog_posts', fn () => Blog::query()
- ->published()
- ->latest('published_at')
- ->limit(3)
- ->get());
- }
-
- private function newsArticles(): Collection
- {
- return $this->query('news_articles', fn () => News::query()
- ->published()
- ->latest('published_at')
- ->limit(3)
- ->get());
- }
-
- private function query(string $table, callable $callback): Collection
- {
- try {
- return Schema::hasTable($table) ? $callback() : collect();
- } catch (Throwable) {
- return collect();
- }
- }
-
- private function enabled(array $settings, string $key, bool $default): bool
- {
- return filter_var($settings[$key] ?? $default, FILTER_VALIDATE_BOOL);
- }
-}
diff --git a/Stylist/Services/TenantThemeResolver.php b/Stylist/Services/TenantThemeResolver.php
deleted file mode 100644
index 58d4920..0000000
--- a/Stylist/Services/TenantThemeResolver.php
+++ /dev/null
@@ -1,65 +0,0 @@
-context();
- $column = config('stylist.tenant_theme_column', 'theme_key');
- $user = Auth::user();
- $company = $user?->company ?? null;
-
- if ($company && filled($company->{$column} ?? null)) {
- return (string) $company->{$column};
- }
-
- if (function_exists('tenant')) {
- $tenant = tenant();
- if ($tenant && filled($tenant->{$column} ?? null)) {
- return (string) $tenant->{$column};
- }
- }
-
- $settingKey = $context === 'frontend' ? 'frontend_theme' : 'backend_theme';
- $default = $context === 'frontend'
- ? config('stylist.default_frontend_theme', 'saasland')
- : config('stylist.default_backend_theme', config('stylist.default_theme', 'admin'));
-
- return (string) $this->settings->get($settingKey, $default, 'site');
- }
-
- public function context(): string
- {
- if (app()->runningInConsole()) {
- return 'backend';
- }
-
- $request = request();
-
- return $request->routeIs('home', 'blog.public.*', 'news.public.*', 'pages.public.*')
- || ! $request->user()
- ? 'frontend'
- : 'backend';
- }
-
- public function variables(): array
- {
- $variables = config('stylist.fallback_variables', []);
- $company = Auth::user()?->company ?? null;
-
- foreach (config('stylist.tenant_brand_columns', []) as $column => $variable) {
- if ($company && filled($company->{$column} ?? null)) {
- $variables[$variable] = (string) $company->{$column};
- }
- }
-
- return $variables;
- }
-}
diff --git a/Stylist/Services/ThemeAccessibilityValidator.php b/Stylist/Services/ThemeAccessibilityValidator.php
deleted file mode 100644
index 70b68bb..0000000
--- a/Stylist/Services/ThemeAccessibilityValidator.php
+++ /dev/null
@@ -1,32 +0,0 @@
- */
- public function validate(Theme $theme): array
- {
- $issues = [];
- foreach ($this->files->allFiles($theme->path().'/views') as $file) {
- $content = $file->getContents();
- $relative = $file->getRelativePathname();
- if (preg_match_all('/ ]*\balt=)[^>]*>/i', $content, $matches)) {
- $issues[] = ['severity' => 'error', 'message' => count($matches[0]).' image(s) missing alt text.', 'file' => $relative];
- }
- if (preg_match('/]*\blang=)/i', $content)) {
- $issues[] = ['severity' => 'warning', 'message' => 'HTML element is missing a lang attribute.', 'file' => $relative];
- }
- if (preg_match_all('/]*>\s*(?:<[^>]+>\s*)*<\/button>/is', $content, $matches)) {
- $issues[] = ['severity' => 'warning', 'message' => count($matches[0]).' button(s) may lack an accessible name.', 'file' => $relative];
- }
- }
-
- return $issues;
- }
-}
diff --git a/Stylist/Services/ThemeOptimizer.php b/Stylist/Services/ThemeOptimizer.php
deleted file mode 100644
index def6927..0000000
--- a/Stylist/Services/ThemeOptimizer.php
+++ /dev/null
@@ -1,57 +0,0 @@
- 0, 'js' => 0, 'images' => 0];
- foreach (['css' => 'css', 'js' => 'js'] as $type => $folder) {
- $directory = $theme->path().'/'.$folder;
- if (! $this->files->isDirectory($directory)) {
- continue;
- }
- foreach ($this->files->files($directory) as $file) {
- if ($file->getExtension() !== $type || str_ends_with($file->getFilename(), '.min.'.$type)) {
- continue;
- }
- $content = $file->getContents();
- $content = preg_replace('!/\*.*?\*/!s', '', $content) ?? $content;
- $content = preg_replace('/\s+/', ' ', $content) ?? $content;
- $content = preg_replace('/\s*([{}:;,>])\s*/', '$1', $content) ?? $content;
- $target = $file->getPath().'/'.$file->getBasename('.'.$type).'.min.'.$type;
- $this->files->put($target, trim($content));
- $result[$type]++;
- }
- }
- if (function_exists('imagewebp')) {
- foreach (['images', 'icons'] as $folder) {
- $directory = $theme->path().'/'.$folder;
- if (! $this->files->isDirectory($directory)) {
- continue;
- }
- foreach ($this->files->files($directory) as $file) {
- $ext = strtolower($file->getExtension());
- if (! in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
- continue;
- }
- $image = $ext === 'png' ? @imagecreatefrompng($file->getPathname()) : @imagecreatefromjpeg($file->getPathname());
- if ($image) {
- imagepalettetotruecolor($image);
- imagewebp($image, $file->getPath().'/'.$file->getBasename('.'.$ext).'.webp', 82);
- imagedestroy($image);
- $result['images']++;
- }
- }
- }
- }
-
- return $result;
- }
-}
diff --git a/Stylist/Services/ThemePackageManager.php b/Stylist/Services/ThemePackageManager.php
deleted file mode 100644
index 56d1a4a..0000000
--- a/Stylist/Services/ThemePackageManager.php
+++ /dev/null
@@ -1,98 +0,0 @@
-getClientOriginalExtension()) !== 'zip') {
- throw new RuntimeException('Only ZIP theme packages may be installed.');
- }
- $temporary = storage_path('app/stylist/install-'.bin2hex(random_bytes(8)));
- $this->files->ensureDirectoryExists($temporary);
- $zip = new ZipArchive;
- if ($zip->open($upload->getRealPath()) !== true) {
- throw new RuntimeException('The uploaded ZIP could not be opened.');
- }
- for ($i = 0; $i < $zip->numFiles; $i++) {
- $name = $zip->getNameIndex($i);
- if (str_contains($name, '..') || str_starts_with($name, '/') || str_contains($name, "\0")) {
- $zip->close();
- throw new RuntimeException('The ZIP contains an unsafe path.');
- }
- }
- $zip->extractTo($temporary);
- $zip->close();
- $manifest = collect($this->files->allFiles($temporary))->first(fn ($file) => $file->getFilename() === 'theme.json');
- if (! $manifest) {
- $this->files->deleteDirectory($temporary);
- throw new RuntimeException('The package does not contain theme.json.');
- }
- $source = $manifest->getPath();
- $theme = $this->loader->fromPath($source);
- if ($theme->parent() && ! $this->loader->all()->has($theme->parent())) {
- throw new RuntimeException('Parent theme '.$theme->parent().' is not installed.');
- }
- $issues = $this->validator->validate($theme);
- if (collect($issues)->contains('severity', 'error')) {
- throw new RuntimeException('Accessibility validation failed: '.collect($issues)->where('severity', 'error')->pluck('message')->implode(' '));
- }
- $target = rtrim((string) config('stylist.themes_path'), '/').'/'.$theme->key();
- if ($this->files->isDirectory($target)) {
- $this->files->deleteDirectory($target);
- }
- $this->files->copyDirectory($source, $target);
- $this->files->deleteDirectory($temporary);
- $this->loader->flush();
-
- return $this->loader->find($theme->key());
- }
-
- public function uninstall(Theme $theme): void
- {
- $active = $theme->type() === 'frontend'
- ? $this->settings->get('frontend_theme', config('stylist.default_frontend_theme'), 'site')
- : $this->settings->get('backend_theme', config('stylist.default_backend_theme'), 'site');
- if ($active === $theme->key()) {
- throw new RuntimeException('The active theme cannot be uninstalled. Activate another theme first.');
- }
- $projectRoot = realpath((string) config('stylist.themes_path'));
- $themePath = realpath($theme->path());
- if (! $projectRoot || ! $themePath || ! str_starts_with($themePath, $projectRoot.DIRECTORY_SEPARATOR)) {
- throw new RuntimeException('Bundled themes cannot be uninstalled.');
- }
- if (collect($this->loader->all())->contains(fn (Theme $candidate) => $candidate->parent() === $theme->key())) {
- throw new RuntimeException('This theme has installed child themes.');
- }
- $this->files->deleteDirectory($themePath);
- $this->loader->flush();
- }
-
- public function export(Theme $theme): string
- {
- $directory = storage_path('app/stylist/exports');
- $this->files->ensureDirectoryExists($directory);
- $path = $directory.'/'.$theme->key().'-'.$theme->version().'.zip';
- $zip = new ZipArchive;
- if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
- throw new RuntimeException('Unable to create theme archive.');
- }
- foreach ($this->files->allFiles($theme->path()) as $file) {
- $zip->addFile($file->getPathname(), $theme->key().'/'.$file->getRelativePathname());
- }
- $zip->close();
-
- return $path;
- }
-}
diff --git a/Stylist/Services/ThemeSettings.php b/Stylist/Services/ThemeSettings.php
deleted file mode 100644
index 0a1f253..0000000
--- a/Stylist/Services/ThemeSettings.php
+++ /dev/null
@@ -1,97 +0,0 @@
-stored($theme);
- $defaults = collect($theme->settingsSchema())
- ->mapWithKeys(fn ($definition, $key) => [$key => is_array($definition) ? ($definition['default'] ?? null) : $definition])
- ->all();
-
- return array_replace($defaults, array_diff_key((array) ($stored?->settings ?? []), array_flip([self::APPEARANCE_KEY, self::ACCENT_KEY])));
- }
-
- public function variant(Theme $theme): string
- {
- $variant = (string) ($this->stored($theme)?->variant ?? array_key_first($theme->variants()) ?? 'light');
-
- return array_key_exists($variant, $theme->variants()) ? $variant : (array_key_first($theme->variants()) ?? 'light');
- }
-
- public function appearance(Theme $theme): string
- {
- $stored = (array) ($this->stored($theme)?->settings ?? []);
- $allowed = array_keys($theme->appearances());
- if ($allowed === []) {
- return '';
- }
- $appearance = (string) ($stored[self::APPEARANCE_KEY] ?? $theme->defaultAppearance());
-
- return in_array($appearance, $allowed, true) ? $appearance : (string) array_key_first($theme->appearances());
- }
-
- public function accent(Theme $theme): string
- {
- $stored = (array) ($this->stored($theme)?->settings ?? []);
- $allowed = array_keys($theme->accents());
- if ($allowed === []) {
- return '';
- }
- $accent = (string) ($stored[self::ACCENT_KEY] ?? $theme->defaultAccent());
-
- return in_array($accent, $allowed, true) ? $accent : (string) array_key_first($theme->accents());
- }
-
- public function save(Theme $theme, array $settings, string $variant, ?string $appearance = null, ?string $accent = null): ThemeSetting
- {
- $settings = array_intersect_key($settings, array_flip(array_keys($theme->settingsSchema())));
- $existing = (array) ($this->stored($theme)?->settings ?? []);
-
- $allowedAppearances = array_keys($theme->appearances());
- if ($allowedAppearances !== []) {
- $appearance ??= (string) ($existing[self::APPEARANCE_KEY] ?? $theme->defaultAppearance());
- if (! in_array($appearance, $allowedAppearances, true)) {
- $appearance = (string) array_key_first($theme->appearances());
- }
- $settings[self::APPEARANCE_KEY] = $appearance;
- }
-
- $allowedAccents = array_keys($theme->accents());
- if ($allowedAccents !== []) {
- $accent ??= (string) ($existing[self::ACCENT_KEY] ?? $theme->defaultAccent());
- if (! in_array($accent, $allowedAccents, true)) {
- $accent = (string) array_key_first($theme->accents());
- }
- $settings[self::ACCENT_KEY] = $accent;
- }
-
- if ($theme->variants() && ! array_key_exists($variant, $theme->variants())) {
- $variant = array_key_first($theme->variants());
- }
-
- return ThemeSetting::query()->updateOrCreate(
- ['theme_key' => $theme->key()],
- ['settings' => $settings, 'variant' => $variant]
- );
- }
-
- private function stored(Theme $theme): ?ThemeSetting
- {
- try {
- return ThemeSetting::query()->where('theme_key', $theme->key())->first();
- } catch (QueryException) {
- return null;
- }
- }
-}
diff --git a/Stylist/Services/ThemeValidator.php b/Stylist/Services/ThemeValidator.php
deleted file mode 100644
index 472397c..0000000
--- a/Stylist/Services/ThemeValidator.php
+++ /dev/null
@@ -1,74 +0,0 @@
- */
- public function validatePath(string $path): array
- {
- $theme = $this->loader->fromPath($path);
-
- return $this->validate($theme);
- }
-
- /** @return array */
- public function validate(Theme $theme): array
- {
- $issues = [];
- $entryView = 'views/'.str_replace('.', '/', $theme->entryView()).'.blade.php';
-
- if (! $theme->has($entryView)) {
- $issues[] = [
- 'severity' => 'error',
- 'message' => "Entry view [{$theme->entryView()}] was not found.",
- 'file' => $entryView,
- ];
- }
-
- foreach (['stylesheet' => $theme->stylesheet(), 'script' => $theme->script()] as $label => $relativePath) {
- if ($relativePath !== '' && ! $theme->has($relativePath)) {
- $issues[] = [
- 'severity' => 'error',
- 'message' => ucfirst($label)." file [{$relativePath}] was not found.",
- 'file' => $relativePath,
- ];
- }
- }
-
- $preview = $theme->preview();
- if ($preview !== null && ! $theme->has($preview)) {
- $issues[] = [
- 'severity' => 'warning',
- 'message' => "Preview file [{$preview}] was not found.",
- 'file' => $preview,
- ];
- }
-
- foreach ((array) ($theme->meta()['assets'] ?? []) as $asset) {
- if (! is_string($asset) || $asset === '') {
- continue;
- }
-
- if (! $theme->has($asset)) {
- $issues[] = [
- 'severity' => 'warning',
- 'message' => "Declared asset [{$asset}] was not found.",
- 'file' => $asset,
- ];
- }
- }
-
- return array_merge($issues, $this->accessibility->validate($theme));
- }
-}
diff --git a/Stylist/Support/FrontendNavigation.php b/Stylist/Support/FrontendNavigation.php
deleted file mode 100644
index 711f089..0000000
--- a/Stylist/Support/FrontendNavigation.php
+++ /dev/null
@@ -1,76 +0,0 @@
-> */
- public function primary(): array
- {
- return $this->location('primary', $this->fallbackPrimary());
- }
-
- /** @return array> */
- public function footer(): array
- {
- return $this->location('footer', $this->fallbackPrimary());
- }
-
- /** @param array> $fallback
- * @return array>
- */
- public function location(string $location, array $fallback = []): array
- {
- try {
- if (! Schema::hasTable('menus')) {
- return $fallback;
- }
-
- $menu = Menu::query()->where('enabled', true)->where('location', $location)->first();
- if (! $menu || ! is_array($menu->items)) {
- return $fallback;
- }
-
- $items = $this->normaliseItems($menu->items);
-
- return $items === [] ? $fallback : $items;
- } catch (Throwable) {
- return $fallback;
- }
- }
-
- /** @param array $items
- * @return array>
- */
- private function normaliseItems(array $items): array
- {
- return collect($items)
- ->filter(fn (mixed $item): bool => is_array($item) && filter_var($item['enabled'] ?? true, FILTER_VALIDATE_BOOL))
- ->map(function (array $item): ?array {
- $label = trim((string) ($item['label'] ?? ''));
- $url = trim((string) ($item['url'] ?? ''));
- if ($label === '' || $url === '') return null;
-
- return [
- 'label' => $label,
- 'url' => $url,
- 'target' => ($item['target'] ?? '') === '_blank' ? '_blank' : null,
- 'children' => $this->normaliseItems((array) ($item['children'] ?? [])),
- ];
- })->filter()->values()->all();
- }
-
- /** @return array> */
- private function fallbackPrimary(): array
- {
- $items = [['label' => 'Home', 'url' => url('/'), 'target' => null, 'children' => []]];
- if (Route::has('blog.public.index')) $items[] = ['label' => 'Blog', 'url' => route('blog.public.index'), 'target' => null, 'children' => []];
- if (Route::has('news.public.index')) $items[] = ['label' => 'News', 'url' => route('news.public.index'), 'target' => null, 'children' => []];
- return $items;
- }
-}
diff --git a/Stylist/Support/ModuleInfo.php b/Stylist/Support/ModuleInfo.php
deleted file mode 100644
index 6c2f429..0000000
--- a/Stylist/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-assertTrue(view()->exists($view), "Expected common view [{$view}] to exist.");
- }
- }
-
- public function test_authentication_and_settings_views_are_not_duplicated(): void
- {
- $this->assertDirectoryDoesNotExist(resource_path('views'));
- $this->assertDirectoryDoesNotExist(resource_path('themes/admin/views/livewire'));
- $this->assertDirectoryDoesNotExist(resource_path('themes/welcome/views/livewire'));
-
- $commonViews = resource_path('themes/common/views');
-
- foreach ([
- 'livewire/auth/login.blade.php',
- 'livewire/auth/register.blade.php',
- 'livewire/auth/forgot-password.blade.php',
- 'livewire/auth/reset-password.blade.php',
- 'livewire/settings/profile.blade.php',
- 'livewire/settings/appearance.blade.php',
- 'livewire/settings/security.blade.php',
- ] as $relativePath) {
- $matches = collect(File::allFiles(resource_path('themes')))
- ->filter(fn ($file) => str_ends_with(
- str_replace('\\', '/', $file->getPathname()),
- '/views/'.$relativePath
- ));
-
- $this->assertCount(1, $matches, "Expected one authoritative view for [{$relativePath}].");
- $this->assertStringStartsWith(
- str_replace('\\', '/', $commonViews),
- str_replace('\\', '/', $matches->first()->getPathname())
- );
- }
- }
-}
diff --git a/Stylist/Tests/Feature/LegacyThemeNamespaceAliasTest.php b/Stylist/Tests/Feature/LegacyThemeNamespaceAliasTest.php
deleted file mode 100644
index 2d0fd45..0000000
--- a/Stylist/Tests/Feature/LegacyThemeNamespaceAliasTest.php
+++ /dev/null
@@ -1,14 +0,0 @@
-assertTrue(view()->exists('stylist-module-theme-admin::dashboard'));
- $this->assertTrue(view()->exists('stylist-theme-admin::dashboard'));
- }
-}
diff --git a/Stylist/Tests/Feature/StylistModuleTest.php b/Stylist/Tests/Feature/StylistModuleTest.php
deleted file mode 100644
index 386ac03..0000000
--- a/Stylist/Tests/Feature/StylistModuleTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-assertInstanceOf(Stylist::class, app(Stylist::class));
- }
-
- public function test_default_theme_resolves(): void
- {
- config(['stylist.default_theme' => 'admin']);
-
- $this->assertSame('admin', app(Stylist::class)->current()->key());
- }
-}
diff --git a/Stylist/Tests/Feature/ThemeAssetControllerTest.php b/Stylist/Tests/Feature/ThemeAssetControllerTest.php
deleted file mode 100644
index 876ee02..0000000
--- a/Stylist/Tests/Feature/ThemeAssetControllerTest.php
+++ /dev/null
@@ -1,41 +0,0 @@
- resource_path('themes')]);
- app(Loader::class)->flush();
-
- $this->get(route('stylist.theme.asset', [
- 'theme' => 'saasland',
- 'path' => 'css/theme.css',
- ]))->assertOk()->assertHeader('X-Content-Type-Options', 'nosniff');
- }
-
- public function test_asset_url_uses_public_themes_path(): void
- {
- $url = route('stylist.theme.asset', [
- 'theme' => 'saasland',
- 'path' => 'images/example.jpg',
- ], false);
-
- $this->assertSame('/themes/saasland/images/example.jpg', $url);
- }
-
- public function test_non_asset_theme_files_are_not_served(): void
- {
- config(['stylist.themes_path' => resource_path('themes')]);
- app(Loader::class)->flush();
-
- $this->get(route('stylist.theme.asset', [
- 'theme' => 'saasland',
- 'path' => 'views/home.blade.php',
- ]))->assertNotFound();
- }
-}
diff --git a/Stylist/Tests/Feature/ThemeHtmlBuilderTest.php b/Stylist/Tests/Feature/ThemeHtmlBuilderTest.php
deleted file mode 100644
index 65b03aa..0000000
--- a/Stylist/Tests/Feature/ThemeHtmlBuilderTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
- '#111827']);
-
- $html = app(ThemeHtmlBuilder::class)->variables()->toHtml();
-
- $this->assertStringContainsString('--brand-primary: #111827;', $html);
- }
-}
diff --git a/Stylist/Tests/Feature/ThemeInheritanceTest.php b/Stylist/Tests/Feature/ThemeInheritanceTest.php
deleted file mode 100644
index 495f581..0000000
--- a/Stylist/Tests/Feature/ThemeInheritanceTest.php
+++ /dev/null
@@ -1,95 +0,0 @@
-themesPath = storage_path('framework/testing/stylist-inheritance-'.uniqid());
- mkdir($this->themesPath, 0777, true);
- config([
- 'stylist.themes_path' => $this->themesPath,
- 'stylist.default_theme' => 'common',
- ]);
- app(Loader::class)->flush();
- }
-
- protected function tearDown(): void
- {
- app(Filesystem::class)->deleteDirectory($this->themesPath);
- parent::tearDown();
- }
-
- public function test_child_theme_resolves_views_through_parent_without_copying_them(): void
- {
- $this->theme('common', null, ['components/layouts/app.blade.php' => '{{ $slot }} ']);
- $this->theme('admin', 'common', ['dashboard.blade.php' => 'Dashboard ']);
- app(Loader::class)->flush();
-
- $candidates = app(ThemeViewResolver::class)->candidatesForTheme('admin', 'components.layouts.app');
-
- $this->assertSame('stylist-theme-admin::components.layouts.app', $candidates[0]);
- $this->assertContains('stylist-theme-common::components.layouts.app', $candidates);
- $this->assertLessThan(
- array_search('stylist-theme-common::components.layouts.app', $candidates, true),
- array_search('stylist-theme-admin::components.layouts.app', $candidates, true),
- );
- }
-
- public function test_parent_alias_is_normalised_to_extends(): void
- {
- $path = $this->themesPath.'/child';
- mkdir($path, 0777, true);
- file_put_contents($path.'/theme.json', json_encode([
- 'name' => 'Child',
- 'parent' => 'common',
- ]));
-
- $theme = app(Loader::class)->fromPath($path);
-
- $this->assertSame('common', $theme->parent());
- }
-
- public function test_duplicate_view_ownership_is_reported(): void
- {
- $this->theme('common', null, ['livewire/auth/login.blade.php' => 'common']);
- $this->theme('admin', 'common', ['livewire/auth/login.blade.php' => 'admin']);
- app(Loader::class)->flush();
-
- $duplicates = app(ThemeInspector::class)->duplicateViews();
-
- $this->assertArrayHasKey('livewire/auth/login.blade.php', $duplicates);
- $this->assertEqualsCanonicalizing(['common', 'admin'], array_column($duplicates['livewire/auth/login.blade.php'], 'theme'));
- }
-
- /** @param array $views */
- private function theme(string $key, ?string $parent, array $views): void
- {
- $path = $this->themesPath.'/'.$key;
- mkdir($path.'/views', 0777, true);
- file_put_contents($path.'/theme.json', json_encode(array_filter([
- 'name' => ucfirst($key),
- 'slug' => $key,
- 'extends' => $parent,
- ], fn ($value) => $value !== null)));
-
- foreach ($views as $relative => $contents) {
- $file = $path.'/views/'.$relative;
- if (! is_dir(dirname($file))) {
- mkdir(dirname($file), 0777, true);
- }
- file_put_contents($file, $contents);
- }
- }
-}
diff --git a/Stylist/Tests/Feature/ThemeManifestTest.php b/Stylist/Tests/Feature/ThemeManifestTest.php
deleted file mode 100644
index 11d681e..0000000
--- a/Stylist/Tests/Feature/ThemeManifestTest.php
+++ /dev/null
@@ -1,157 +0,0 @@
-themesPath = storage_path('framework/testing/stylist-themes-'.uniqid());
- mkdir($this->themesPath, 0777, true);
- config(['stylist.themes_path' => $this->themesPath]);
- app(Loader::class)->flush();
- }
-
- protected function tearDown(): void
- {
- app(Filesystem::class)->deleteDirectory($this->themesPath);
- parent::tearDown();
- }
-
- public function test_manifest_is_normalised(): void
- {
- $path = $this->themesPath.'/sample';
- mkdir($path.'/css', 0777, true);
- file_put_contents($path.'/theme.json', json_encode(['name' => 'Sample']));
- file_put_contents($path.'/css/theme.css', 'body{}');
-
- $theme = app(Loader::class)->fromPath($path);
-
- $this->assertSame('sample', $theme->key());
- $this->assertSame('css/theme.css', $theme->stylesheet());
- $this->assertSame('js/app.js', $theme->script());
- $this->assertSame('home', $theme->entryView());
- }
-
- public function test_unsafe_manifest_path_is_rejected(): void
- {
- $path = $this->themesPath.'/unsafe';
- mkdir($path, 0777, true);
- file_put_contents($path.'/theme.json', json_encode([
- 'name' => 'Unsafe',
- 'stylesheet' => '../secret.css',
- ]));
-
- $this->expectException(InvalidThemeManifestException::class);
- app(Loader::class)->fromPath($path);
- }
-
- public function test_phase_one_manifest_optional_capabilities_are_normalised(): void
- {
- $path = $this->themesPath.'/design-gallery';
- mkdir($path.'/css', 0777, true);
- mkdir($path.'/js', 0777, true);
- file_put_contents($path.'/theme.json', json_encode([
- 'key' => 'design-gallery',
- 'name' => 'Design Gallery',
- 'stylesheet' => 'css/app.css',
- 'script' => 'js/app.js',
- 'appearances' => ['light', 'dark', 'system'],
- 'accents' => ['blue', 'emerald', 'neutral'],
- 'default_appearance' => 'system',
- 'default_accent' => 'neutral',
- // Phase 1 incorrectly duplicated accents here; Stylist must not
- // reject the whole theme because of that compact legacy list.
- 'variants' => ['blue', 'emerald', 'neutral'],
- ]));
- file_put_contents($path.'/css/app.css', 'body{}');
- file_put_contents($path.'/js/app.js', '');
-
- $theme = app(Loader::class)->fromPath($path);
-
- $this->assertSame(['light', 'dark', 'system'], array_keys($theme->appearances()));
- $this->assertSame(['blue', 'emerald', 'neutral'], array_keys($theme->accents()));
- $this->assertSame([], $theme->variants());
- $this->assertSame('system', $theme->defaultAppearance());
- $this->assertSame('neutral', $theme->defaultAccent());
- }
-
- public function test_legacy_manifest_does_not_gain_appearance_or_accent_capabilities(): void
- {
- $path = $this->themesPath.'/legacy';
- mkdir($path.'/css', 0777, true);
- file_put_contents($path.'/theme.json', json_encode(['name' => 'Legacy']));
- file_put_contents($path.'/css/theme.css', 'body{}');
-
- $theme = app(Loader::class)->fromPath($path);
-
- $this->assertSame([], $theme->appearances());
- $this->assertSame([], $theme->accents());
- $this->assertSame('', $theme->defaultAppearance());
- $this->assertSame('', $theme->defaultAccent());
- }
-
- public function test_defaults_must_reference_declared_optional_capabilities(): void
- {
- $path = $this->themesPath.'/invalid-default';
- mkdir($path, 0777, true);
- file_put_contents($path.'/theme.json', json_encode([
- 'name' => 'Invalid Default',
- 'appearances' => ['light', 'dark'],
- 'default_appearance' => 'system',
- ]));
-
- $this->expectException(InvalidThemeManifestException::class);
- app(Loader::class)->fromPath($path);
- }
-
- public function test_manifest_version_and_capabilities_are_normalised(): void
- {
- $path = $this->themesPath.'/capable';
- mkdir($path.'/css', 0777, true);
- mkdir($path.'/js', 0777, true);
- mkdir($path.'/views', 0777, true);
- file_put_contents($path.'/theme.json', json_encode([
- 'manifest_version' => 1,
- 'name' => 'Capable',
- 'appearances' => ['light', 'dark'],
- 'accents' => ['blue'],
- 'capabilities' => ['marketplace' => true],
- ]));
- file_put_contents($path.'/css/theme.css', 'body{}');
- file_put_contents($path.'/js/app.js', '');
- file_put_contents($path.'/views/home.blade.php', 'Capable
');
-
- $theme = app(Loader::class)->fromPath($path);
-
- $this->assertSame(1, $theme->manifestVersion());
- $this->assertTrue($theme->hasCapability('appearances'));
- $this->assertTrue($theme->hasCapability('accents'));
- $this->assertTrue($theme->hasCapability('marketplace'));
- $this->assertFalse($theme->hasCapability('preview'));
- }
-
- public function test_unknown_manifest_fields_are_preserved_without_preventing_loading(): void
- {
- $path = $this->themesPath.'/future';
- mkdir($path.'/css', 0777, true);
- file_put_contents($path.'/theme.json', json_encode([
- 'name' => 'Future',
- 'future_builder_metadata' => ['anything' => true],
- ]));
- file_put_contents($path.'/css/theme.css', 'body{}');
-
- $theme = app(Loader::class)->fromPath($path);
-
- $this->assertSame(['anything' => true], $theme->meta()['future_builder_metadata']);
- }
-}
diff --git a/Stylist/Tests/Feature/ThemePackageRoundTripTest.php b/Stylist/Tests/Feature/ThemePackageRoundTripTest.php
deleted file mode 100644
index 633b3dc..0000000
--- a/Stylist/Tests/Feature/ThemePackageRoundTripTest.php
+++ /dev/null
@@ -1,79 +0,0 @@
-themesPath = storage_path('framework/testing/stylist-installed-'.uniqid());
- $this->workspace = storage_path('framework/testing/stylist-package-'.uniqid());
- mkdir($this->themesPath, 0777, true);
- mkdir($this->workspace, 0777, true);
- config(['stylist.themes_path' => $this->themesPath]);
- app(Loader::class)->flush();
- }
-
- protected function tearDown(): void
- {
- app(Filesystem::class)->deleteDirectory($this->themesPath);
- app(Filesystem::class)->deleteDirectory($this->workspace);
- app(Loader::class)->flush();
- parent::tearDown();
- }
-
- public function test_generated_theme_zip_installs_discovers_and_renders(): void
- {
- $source = $this->workspace.'/design-gallery';
- mkdir($source.'/css', 0777, true);
- mkdir($source.'/js', 0777, true);
- mkdir($source.'/views', 0777, true);
-
- file_put_contents($source.'/theme.json', json_encode([
- 'manifest_version' => 1,
- 'key' => 'design-gallery',
- 'name' => 'Design Gallery',
- 'stylesheet' => 'css/app.css',
- 'script' => 'js/app.js',
- 'entry_view' => 'home',
- 'appearances' => ['light', 'dark', 'system'],
- 'accents' => ['blue', 'neutral'],
- 'default_appearance' => 'system',
- 'default_accent' => 'neutral',
- ]));
- file_put_contents($source.'/css/app.css', 'body{}');
- file_put_contents($source.'/js/app.js', '');
- file_put_contents($source.'/views/home.blade.php', 'Design Gallery works ');
-
- $zipPath = $this->workspace.'/design-gallery.zip';
- $zip = new ZipArchive;
- $this->assertTrue($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE));
- foreach (app(Filesystem::class)->allFiles($source) as $file) {
- $zip->addFile($file->getPathname(), 'design-gallery/'.$file->getRelativePathname());
- }
- $zip->close();
-
- $upload = new UploadedFile($zipPath, 'design-gallery.zip', 'application/zip', null, true);
- $theme = app(ThemePackageManager::class)->install($upload);
-
- $this->assertSame('design-gallery', $theme->key());
- $this->assertTrue(app(Loader::class)->all()->has('design-gallery'));
- $this->assertStringContainsString(
- 'Design Gallery works',
- stylist_theme_view('design-gallery', 'home')->render(),
- );
- }
-}
diff --git a/Stylist/Tests/Feature/ThemeViewResolverTest.php b/Stylist/Tests/Feature/ThemeViewResolverTest.php
deleted file mode 100644
index 0ae83e8..0000000
--- a/Stylist/Tests/Feature/ThemeViewResolverTest.php
+++ /dev/null
@@ -1,22 +0,0 @@
- 'admin',
- 'stylist.themes_path' => resource_path('themes'),
- ]);
-
- $candidates = app(ThemeViewResolver::class)->candidates('components.page-hero');
-
- $this->assertContains('stylist-theme-admin::components.page-hero', $candidates);
- $this->assertContains('stylist::components.page-hero', $candidates);
- }
-}
diff --git a/Stylist/Tests/Feature/WelcomeThemeViewTest.php b/Stylist/Tests/Feature/WelcomeThemeViewTest.php
deleted file mode 100644
index ec7624b..0000000
--- a/Stylist/Tests/Feature/WelcomeThemeViewTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-assertTrue(view()->exists('stylist-theme-saasland::home'));
- }
-
- public function test_home_route_renders_the_saasland_theme(): void
- {
- $this->get('/')
- ->assertOk()
- ->assertViewIs('stylist-theme-saasland::home')
- ->assertSee('Transform Your Business With Our SaaS Solution');
- }
-}
diff --git a/Stylist/Theme/Exceptions/InvalidThemeManifestException.php b/Stylist/Theme/Exceptions/InvalidThemeManifestException.php
deleted file mode 100644
index a548f9f..0000000
--- a/Stylist/Theme/Exceptions/InvalidThemeManifestException.php
+++ /dev/null
@@ -1,18 +0,0 @@
- $errors */
- public static function forPath(string $path, array $errors): self
- {
- return new self(sprintf(
- 'Invalid theme manifest at [%s]: %s',
- rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'theme.json',
- implode(' ', $errors),
- ));
- }
-}
diff --git a/Stylist/Theme/Exceptions/ThemeJsonNotFoundException.php b/Stylist/Theme/Exceptions/ThemeJsonNotFoundException.php
deleted file mode 100644
index 746d52e..0000000
--- a/Stylist/Theme/Exceptions/ThemeJsonNotFoundException.php
+++ /dev/null
@@ -1,13 +0,0 @@
-files->exists($manifest)) {
- throw ThemeJsonNotFoundException::forPath($path);
- }
-
- return json_decode($this->files->get($manifest), true, flags: JSON_THROW_ON_ERROR);
- }
-}
diff --git a/Stylist/Theme/Loader.php b/Stylist/Theme/Loader.php
deleted file mode 100644
index e93708d..0000000
--- a/Stylist/Theme/Loader.php
+++ /dev/null
@@ -1,83 +0,0 @@
-themes !== null) {
- return $this->themes;
- }
-
- $themes = collect();
-
- foreach ($this->roots() as $root) {
- if (! $this->files->isDirectory($root)) {
- continue;
- }
-
- foreach ($this->files->directories($root) as $themePath) {
- try {
- $theme = $this->fromPath($themePath);
- $themes->put($theme->key(), $theme);
- } catch (\Throwable $exception) {
- report($exception);
- }
- }
- }
-
- return $this->themes = $themes->sortKeys();
- }
-
- public function find(string $key): Theme
- {
- $theme = $this->all()->get($key);
-
- if (! $theme) {
- throw ThemeNotFoundException::forKey($key);
- }
-
- return $theme;
- }
-
- public function fromPath(string $path): Theme
- {
- $data = $this->validator->validate($this->json->read($path), $path);
-
- return new Theme(
- key: $data['key'],
- name: $data['name'],
- path: rtrim($path, DIRECTORY_SEPARATOR),
- stylesheet: $data['stylesheet'],
- script: $data['script'],
- variables: $data['variables'],
- meta: $data,
- );
- }
-
- public function flush(): void
- {
- $this->themes = null;
- }
-
- /** @return array */
- public function roots(): array
- {
- return array_values(array_unique(array_filter([
- (string) config('stylist.themes_path'),
- ])));
- }
-}
diff --git a/Stylist/Theme/ManifestValidator.php b/Stylist/Theme/ManifestValidator.php
deleted file mode 100644
index cc75998..0000000
--- a/Stylist/Theme/ManifestValidator.php
+++ /dev/null
@@ -1,179 +0,0 @@
- */
- public function validate(array $manifest, string $path): array
- {
- $errors = [];
- $manifestVersion = $manifest['manifest_version'] ?? $manifest['schema_version'] ?? 1;
- if ((is_string($manifestVersion) && ctype_digit($manifestVersion))) {
- $manifestVersion = (int) $manifestVersion;
- }
- if (! is_int($manifestVersion) || $manifestVersion < 1) {
- $errors[] = 'The manifest_version must be a positive integer.';
- }
-
- $key = $manifest['key'] ?? $manifest['slug'] ?? basename($path);
-
- if (! is_string($key) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $key)) {
- $errors[] = 'The key/slug must contain only lowercase letters, numbers, dashes, and underscores.';
- }
- if (isset($manifest['name']) && (! is_string($manifest['name']) || trim($manifest['name']) === '')) {
- $errors[] = 'The name must be a non-empty string.';
- }
- if (isset($manifest['version']) && (! is_string($manifest['version']) || ! preg_match('/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/', $manifest['version']))) {
- $errors[] = 'The version must use semantic versioning, for example 1.2.0.';
- }
- if (isset($manifest['type']) && ! in_array($manifest['type'], ['frontend', 'backend', 'common'], true)) {
- $errors[] = 'The type must be frontend, backend, or common.';
- }
- $parent = $manifest['extends'] ?? $manifest['parent'] ?? null;
- if ($parent !== null && (! is_string($parent) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $parent))) {
- $errors[] = 'The extends/parent value must be a valid theme key.';
- }
-
- foreach (['stylesheet', 'script', 'preview'] as $field) {
- if (isset($manifest[$field]) && ! $this->isSafeRelativePath($manifest[$field])) {
- $errors[] = "The {$field} value must be a safe relative path.";
- }
- }
-
- foreach (['assets', 'variables', 'supports', 'capabilities', 'variants', 'appearances', 'accents', 'settings', 'seo'] as $field) {
- if (isset($manifest[$field]) && ! is_array($manifest[$field])) {
- $errors[] = "The {$field} value must be an object or array.";
- }
- }
-
- $variants = $this->normaliseDefinitions($manifest['variants'] ?? [], 'variants', $errors, false);
- $appearances = $this->normaliseDefinitions($manifest['appearances'] ?? [], 'appearances', $errors);
- $accents = $this->normaliseDefinitions($manifest['accents'] ?? [], 'accents', $errors);
- $capabilities = $this->normaliseCapabilities($manifest['capabilities'] ?? [], $errors);
- $capabilities['appearances'] = $appearances !== [];
- $capabilities['accents'] = $accents !== [];
- $capabilities['preview'] = isset($manifest['preview']) || is_file($path.'/preview.webp');
-
- $defaultAppearance = $manifest['default_appearance'] ?? null;
- if ($defaultAppearance !== null) {
- if (! is_string($defaultAppearance) || $defaultAppearance === '') {
- $errors[] = 'The default_appearance value must be a non-empty string.';
- } elseif ($appearances === [] || ! array_key_exists($defaultAppearance, $appearances)) {
- $errors[] = 'The default_appearance value must be declared in appearances.';
- }
- }
-
- $defaultAccent = $manifest['default_accent'] ?? null;
- if ($defaultAccent !== null) {
- if (! is_string($defaultAccent) || $defaultAccent === '') {
- $errors[] = 'The default_accent value must be a non-empty string.';
- } elseif ($accents === [] || ! array_key_exists($defaultAccent, $accents)) {
- $errors[] = 'The default_accent value must be declared in accents.';
- }
- }
-
- if ($errors !== []) {
- throw InvalidThemeManifestException::forPath($path, $errors);
- }
-
- $manifest['manifest_version'] = $manifestVersion;
- unset($manifest['schema_version']);
- $manifest['key'] = $key;
- $manifest['slug'] = $key;
- $manifest['name'] = $manifest['name'] ?? str($key)->headline()->toString();
- $manifest['extends'] = $parent;
- $manifest['version'] = (string) ($manifest['version'] ?? '1.0.0');
- $manifest['entry_view'] = (string) ($manifest['entry_view'] ?? 'home');
- $manifest['stylesheet'] = $manifest['stylesheet'] ?? $manifest['css'] ?? 'css/theme.css';
- $manifest['script'] = $manifest['script'] ?? 'js/app.js';
- $manifest['assets'] = is_array($manifest['assets'] ?? null) ? $manifest['assets'] : [];
- $manifest['variables'] = is_array($manifest['variables'] ?? null) ? $manifest['variables'] : [];
- $manifest['supports'] = is_array($manifest['supports'] ?? null) ? $manifest['supports'] : [];
- $manifest['capabilities'] = $capabilities;
- $manifest['variants'] = $variants;
- $manifest['appearances'] = $appearances;
- $manifest['accents'] = $accents;
- $manifest['settings'] = is_array($manifest['settings'] ?? null) ? $manifest['settings'] : [];
- $manifest['seo'] = is_array($manifest['seo'] ?? null) ? $manifest['seo'] : [];
- $manifest['preview'] = $manifest['preview'] ?? (is_file($path.'/preview.webp') ? 'preview.webp' : null);
-
- return $manifest;
- }
-
- /**
- * Compact lists such as ["light", "dark"] are accepted and converted to
- * keyed empty definitions. Object definitions remain unchanged.
- *
- * @param array $value
- * @param array $errors
- * @return array>
- */
- private function normaliseDefinitions(array $value, string $field, array &$errors, bool $allowCompact = true): array
- {
- if ($value === []) {
- return [];
- }
-
- if (array_is_list($value)) {
- if (! $allowCompact && $field === 'variants') {
- // Older Theme Builder Phase 1 builds incorrectly wrote accent names
- // to variants. Ignore that list while preserving object-style variants.
- return [];
- }
-
- $normalised = [];
- foreach ($value as $name) {
- if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name)) {
- $errors[] = "Every {$field} entry must be a valid lowercase key.";
-
- continue;
- }
- $normalised[$name] = [];
- }
-
- return $normalised;
- }
-
- $normalised = [];
- foreach ($value as $name => $definition) {
- if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name) || ! is_array($definition)) {
- $errors[] = "Every {$field} definition must use a valid lowercase key and an object value.";
-
- continue;
- }
- $normalised[$name] = $definition;
- }
-
- return $normalised;
- }
-
- /**
- * @param array $value
- * @param array $errors
- * @return array
- */
- private function normaliseCapabilities(array $value, array &$errors): array
- {
- $normalised = [];
-
- foreach ($value as $name => $enabled) {
- if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name) || ! is_bool($enabled)) {
- $errors[] = 'Every capability must use a valid lowercase key and a boolean value.';
-
- continue;
- }
-
- $normalised[$name] = $enabled;
- }
-
- return $normalised;
- }
-
- private function isSafeRelativePath(mixed $path): bool
- {
- return is_string($path) && $path !== '' && ! str_starts_with($path, '/') && ! str_contains($path, '..') && ! str_contains($path, "\0");
- }
-}
diff --git a/Stylist/Theme/Stylist.php b/Stylist/Theme/Stylist.php
deleted file mode 100644
index 82793ef..0000000
--- a/Stylist/Theme/Stylist.php
+++ /dev/null
@@ -1,103 +0,0 @@
-resolver->resolve($context);
- try {
- return $this->loader->find($requested);
- } catch (ThemeNotFoundException) {
- $fallback = $context === 'frontend'
- ? (string) config('stylist.default_frontend_theme', 'saasland')
- : (string) config('stylist.default_backend_theme', config('stylist.default_theme', 'admin'));
- if ($requested !== $fallback) {
- return $this->loader->find($fallback);
- }
- throw ThemeNotFoundException::forKey($requested);
- }
- }
-
- public function frontend(): Theme
- {
- return $this->current('frontend');
- }
-
- public function backend(): Theme
- {
- return $this->current('backend');
- }
-
- public function variables(): array
- {
- $theme = $this->current();
- $variant = $this->settings->variant($theme);
- $variantVars = (array) data_get($theme->variants(), $variant.'.variables', []);
- $appearanceVars = (array) data_get($theme->appearances(), $this->appearance().'.variables', []);
- $accentVars = (array) data_get($theme->accents(), $this->accent().'.variables', []);
- $settingVars = collect($this->settings->get($theme))
- ->filter(fn ($value, $key) => is_string($key) && str_starts_with($key, '--'))
- ->mapWithKeys(fn ($value, $key) => [ltrim($key, '-') => $value])
- ->all();
-
- return array_replace(
- (array) config('stylist.fallback_variables', []),
- $this->inheritedVariables($theme),
- $variantVars,
- $appearanceVars,
- $accentVars,
- $settingVars,
- $this->resolver->variables(),
- );
- }
-
- public function settings(): array
- {
- return $this->settings->get($this->current());
- }
-
- public function variant(): string
- {
- return $this->settings->variant($this->current());
- }
-
- public function appearance(): string
- {
- return $this->settings->appearance($this->current());
- }
-
- public function accent(): string
- {
- return $this->settings->accent($this->current());
- }
-
- public function all()
- {
- return $this->loader->all();
- }
-
- private function inheritedVariables(Theme $theme, array $seen = []): array
- {
- if (in_array($theme->key(), $seen, true)) {
- return [];
- }
- $seen[] = $theme->key();
- $parent = [];
- if ($theme->parent()) {
- try {
- $parent = $this->inheritedVariables($this->loader->find($theme->parent()), $seen);
- } catch (ThemeNotFoundException) {
- }
- }
-
- return array_replace($parent, $theme->variables());
- }
-}
diff --git a/Stylist/Theme/Theme.php b/Stylist/Theme/Theme.php
deleted file mode 100644
index 2010173..0000000
--- a/Stylist/Theme/Theme.php
+++ /dev/null
@@ -1,161 +0,0 @@
-key;
- }
-
- public function name(): string
- {
- return $this->name;
- }
-
- public function path(): string
- {
- return $this->path;
- }
-
- public function stylesheet(): string
- {
- return $this->stylesheet;
- }
-
- public function script(): string
- {
- return $this->script;
- }
-
- public function variables(): array
- {
- return $this->variables;
- }
-
- public function meta(): array
- {
- return $this->meta;
- }
-
- public function manifestVersion(): int
- {
- return (int) ($this->meta['manifest_version'] ?? 1);
- }
-
- public function version(): string
- {
- return (string) ($this->meta['version'] ?? '1.0.0');
- }
-
- public function description(): string
- {
- return (string) ($this->meta['description'] ?? '');
- }
-
- public function type(): string
- {
- return (string) ($this->meta['type'] ?? 'frontend');
- }
-
- public function author(): string
- {
- return (string) ($this->meta['author'] ?? '');
- }
-
- public function parent(): ?string
- {
- $parent = $this->meta['extends'] ?? null;
-
- return is_string($parent) && $parent !== '' ? $parent : null;
- }
-
- public function preview(): ?string
- {
- $preview = $this->meta['preview'] ?? null;
-
- return is_string($preview) && $preview !== '' ? $preview : null;
- }
-
- public function variants(): array
- {
- return (array) ($this->meta['variants'] ?? []);
- }
-
- public function appearances(): array
- {
- return (array) ($this->meta['appearances'] ?? []);
- }
-
- public function accents(): array
- {
- return (array) ($this->meta['accents'] ?? []);
- }
-
- public function defaultAppearance(): string
- {
- return (string) ($this->meta['default_appearance'] ?? array_key_first($this->appearances()) ?? '');
- }
-
- public function defaultAccent(): string
- {
- return (string) ($this->meta['default_accent'] ?? array_key_first($this->accents()) ?? '');
- }
-
- public function settingsSchema(): array
- {
- return (array) ($this->meta['settings'] ?? []);
- }
-
- public function seo(): array
- {
- return (array) ($this->meta['seo'] ?? []);
- }
-
- public function entryView(): string
- {
- return (string) ($this->meta['entry_view'] ?? 'home');
- }
-
- public function capabilities(): array
- {
- return (array) ($this->meta['capabilities'] ?? []);
- }
-
- public function hasCapability(string $feature): bool
- {
- return (bool) ($this->capabilities()[$feature] ?? false);
- }
-
- public function supports(string $feature): bool
- {
- return (bool) data_get($this->meta, 'supports.'.$feature, $this->hasCapability($feature));
- }
-
- public function has(string $relativePath): bool
- {
- return is_file($this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/'));
- }
-
- public function absolutePath(string $relativePath): string
- {
- return $this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/');
- }
-
- /** @deprecated Theme assets are served through UrlGenerator. */
- public function assetsPath(): string
- {
- return 'stylist/theme-assets/'.$this->key;
- }
-}
diff --git a/Stylist/Theme/UrlGenerator.php b/Stylist/Theme/UrlGenerator.php
deleted file mode 100644
index 77d08f8..0000000
--- a/Stylist/Theme/UrlGenerator.php
+++ /dev/null
@@ -1,20 +0,0 @@
-forTheme($this->stylist->current(), $path);
- }
-
- public function forTheme(Theme|string $theme, string $path): string
- {
- $key = $theme instanceof Theme ? $theme->key() : $theme;
-
- return route('stylist.theme.asset', ['theme' => $key, 'path' => ltrim($path, '/')]);
- }
-}
diff --git a/Stylist/View/ThemeViewResolver.php b/Stylist/View/ThemeViewResolver.php
deleted file mode 100644
index 91d263d..0000000
--- a/Stylist/View/ThemeViewResolver.php
+++ /dev/null
@@ -1,130 +0,0 @@
-> */
- private array $candidateCache = [];
-
- public function __construct(
- private readonly Stylist $stylist,
- private readonly ViewFactory $views,
- private readonly Loader $loader,
- ) {}
-
- public function make(string $view, array $data = [], array $mergeData = []): View
- {
- return $this->views->first(
- $this->candidates($view),
- array_replace([
- 'themeSettings' => $this->stylist->settings(),
- 'themeVariant' => $this->stylist->variant(),
- ], $data),
- $mergeData,
- );
- }
-
- public function makeForTheme(string $theme, string $view, array $data = [], array $mergeData = []): View
- {
- return $this->views->first($this->candidatesForTheme($theme, $view), $data, $mergeData);
- }
-
- /** @return array */
- public function candidates(string $view): array
- {
- return $this->candidatesForTheme($this->stylist->current()->key(), $view);
- }
-
- /** @return array */
- public function candidatesForTheme(string $theme, string $view): array
- {
- $view = ltrim($view, '.');
- $cacheKey = $theme.'|'.$view;
-
- if (isset($this->candidateCache[$cacheKey])) {
- return $this->candidateCache[$cacheKey];
- }
-
- $keys = $this->inheritanceChain($theme);
- $fallback = (string) config('stylist.default_theme', 'common');
-
- if ($fallback !== '' && ! in_array($fallback, $keys, true)) {
- try {
- $keys = array_merge($keys, $this->inheritanceChain($fallback));
- } catch (ThemeNotFoundException) {
- // A missing configured fallback is diagnosed by stylist:doctor.
- }
- }
-
- if (! in_array('common', $keys, true)) {
- try {
- $keys = array_merge($keys, $this->inheritanceChain('common'));
- } catch (ThemeNotFoundException) {
- // The module fallback below remains available.
- }
- }
-
- $candidates = [];
- foreach (array_values(array_unique($keys)) as $key) {
- $this->registerNamespace($key);
- $candidates[] = "stylist-theme-{$key}::{$view}";
- $candidates[] = "stylist-module-theme-{$key}::{$view}";
- }
-
- $candidates[] = "stylist::{$view}";
- $candidates[] = $view;
-
- return $this->candidateCache[$cacheKey] = array_values(array_unique($candidates));
- }
-
- public function flush(): void
- {
- $this->candidateCache = [];
- }
-
- /** @return array */
- private function inheritanceChain(string $theme): array
- {
- $keys = [];
- $seen = [];
- $key = $theme;
-
- while ($key !== '') {
- if (isset($seen[$key])) {
- throw new RuntimeException('Theme inheritance cycle detected at ['.$key.'].');
- }
-
- $seen[$key] = true;
- $loaded = $this->loader->find($key);
- $keys[] = $loaded->key();
- $key = $loaded->parent() ?? '';
- }
-
- return $keys;
- }
-
- private function registerNamespace(string $theme): void
- {
- try {
- $loaded = $this->loader->find($theme);
- } catch (ThemeNotFoundException) {
- return;
- }
-
- $viewsPath = $loaded->path().DIRECTORY_SEPARATOR.'views';
- if (! is_dir($viewsPath)) {
- return;
- }
-
- $this->views->addNamespace("stylist-theme-{$theme}", $viewsPath);
- $this->views->addNamespace("stylist-module-theme-{$theme}", $viewsPath);
- }
-}
diff --git a/Stylist/changelog.yaml b/Stylist/changelog.yaml
deleted file mode 100644
index 7a8e4e0..0000000
--- a/Stylist/changelog.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-latest: 1.1.0
-releases:
- - version: 1.1.0
- date: 2026-07-08
- changes:
- - Added self-contained theme folders with theme.json, CSS, Blade views, and Livewire views.
- - Added ThemeViewResolver and stylist_view() fallback helper.
- - Added default and MPBA example themes.
- - Added theme publishing support.
- - Updated Vite inputs to include theme CSS files.
- - version: 1.0.0
- date: 2026-07-08
- changes:
- - Initial Laravel 13 Stylist module.
-
- - version: 1.1.0
- date: 2026-07-23
- changes:
- - Added optional manifest_version/schema_version normalisation.
- - Added capability normalisation for appearances, accents, previews and future features.
- - Added stylist:validate for installed themes, directories and ZIP packages.
- - Added complete package validation during installation.
- - Added dynamic view namespace registration after installing a theme.
- - Added manifest compatibility and install-render regression coverage.
diff --git a/Stylist/composer.json b/Stylist/composer.json
deleted file mode 100644
index d537e3a..0000000
--- a/Stylist/composer.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "mpba/stylist-module",
- "description": "Tenant-aware theme and asset switching module for Laravel 13.",
- "type": "laravel-module",
- "require": {
- "php": "^8.3",
- "illuminate/support": "^13.0",
- "illuminate/view": "^13.0",
- "illuminate/filesystem": "^13.0",
- "illuminate/console": "^13.0"
- },
- "autoload": {
- "psr-4": {
- "Modules\\Stylist\\": ""
- }
- },
- "autoload-dev": {
- "psr-4": {
- "Modules\\Stylist\\Tests\\": "Tests/"
- }
- }
-}
diff --git a/Stylist/module.json b/Stylist/module.json
deleted file mode 100644
index 214c2d2..0000000
--- a/Stylist/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Stylist",
- "alias": "stylist",
- "version": "1.1.0",
- "description": "Tenant-aware theme and asset switching module for Laravel 13.",
- "keywords": [
- "laravel",
- "modules",
- "themes",
- "tenancy",
- "styling"
- ],
- "priority": 0,
- "providers": [
- "Modules\\Stylist\\Providers\\StylistServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": []
-}
diff --git a/Stylist/package.json b/Stylist/package.json
deleted file mode 100644
index e0b33af..0000000
--- a/Stylist/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "scripts": {
- "dev": "vite",
- "build": "vite build",
-
- "stylist:dev": "vite --config Modules/Stylist/vite.config.js",
- "stylist:build": "vite build --config Modules/Stylist/vite.config.js",
-
- "matches:dev": "vite --config Modules/Matches/vite.config.js",
- "matches:build": "vite build --config Modules/Matches/vite.config.js",
-
- "modules:build": "npm run stylist:build && npm run matches:build",
- "build:all": "npm run build && npm run modules:build"
- }
-}
diff --git a/Stylist/vite.config.js b/Stylist/vite.config.js
deleted file mode 100644
index de55470..0000000
--- a/Stylist/vite.config.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { defineConfig } from 'vite';
-import laravel from 'laravel-vite-plugin';
-import tailwindcss from '@tailwindcss/vite';
-
-export default defineConfig({
- plugins: [
- laravel({
- input: [
- 'Modules/Stylist/Resources/assets/css/app.css',
- 'Modules/Stylist/Resources/assets/js/app.js',
- ],
-
- refresh: [
- 'Modules/Stylist/Resources/views/**',
- 'resources/themes/**/views/**',
- 'Modules/Stylist/App/**',
- 'Modules/Stylist/routes/**',
- ],
-
- buildDirectory: 'build/stylist',
- }),
-
- tailwindcss(),
- ],
-});
diff --git a/Tags/Config/config.php b/Tags/Config/config.php
deleted file mode 100644
index 4f63319..0000000
--- a/Tags/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Tags'];
diff --git a/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php b/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php
deleted file mode 100644
index 8a969e7..0000000
--- a/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php
+++ /dev/null
@@ -1,25 +0,0 @@
-id();
- $table->string('name');
- $table->string('slug')->unique();
- $table->text('description')->nullable();
- $table->unsignedInteger('usage_count')->default(0);
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('tags');
- }
-};
diff --git a/Tags/Http/Controllers/TagController.php b/Tags/Http/Controllers/TagController.php
deleted file mode 100644
index 08e0a08..0000000
--- a/Tags/Http/Controllers/TagController.php
+++ /dev/null
@@ -1,48 +0,0 @@
- Tag::orderByDesc('usage_count')->paginate(30)]);
- }
-
- public function create(): View
- {
- return view('tags::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- Tag::create($request->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:tags,slug'], 'description' => ['nullable', 'string']]));
-
- return redirect()->route('administration.tags.index')->with('status', 'Tag created.');
- }
-
- public function edit(Tag $tag): View
- {
- return view('tags::admin.form', ['record' => $tag]);
- }
-
- public function update(Request $request, Tag $tag): RedirectResponse
- {
- $tag->update($request->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255', 'unique:tags,slug,'.$tag->id], 'description' => ['nullable', 'string']]));
-
- return redirect()->route('administration.tags.index')->with('status', 'Tag updated.');
- }
-
- public function destroy(Tag $tag): RedirectResponse
- {
- $tag->delete();
-
- return back()->with('status', 'Tag deleted.');
- }
-}
diff --git a/Tags/Models/Tag.php b/Tags/Models/Tag.php
deleted file mode 100644
index da7b39c..0000000
--- a/Tags/Models/Tag.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'integer'];
- }
-}
diff --git a/Tags/Providers/RouteServiceProvider.php b/Tags/Providers/RouteServiceProvider.php
deleted file mode 100644
index 3605636..0000000
--- a/Tags/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Tags', 'Routes/web.php'));
- }
-}
diff --git a/Tags/Providers/TagsServiceProvider.php b/Tags/Providers/TagsServiceProvider.php
deleted file mode 100644
index 51407ff..0000000
--- a/Tags/Providers/TagsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Tags', 'Config/config.php'), 'tags');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Tags', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Tags', 'Resources/views'), 'tags');
- }
-}
diff --git a/Tags/Resources/views/admin/form.blade.php b/Tags/Resources/views/admin/form.blade.php
deleted file mode 100644
index 9dfe384..0000000
--- a/Tags/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-@csrf @isset($record) @method('PUT') @endisset {{ old('description',$record->description ?? '') }} Save
diff --git a/Tags/Resources/views/admin/index.blade.php b/Tags/Resources/views/admin/index.blade.php
deleted file mode 100644
index 537b5d9..0000000
--- a/Tags/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Tags Manage publishing tags.
Create @if(session('status')){{ session('status') }}
@endifName / title Status Actions @forelse($records as $record){{ $record->title ?? $record->name }} {{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
diff --git a/Tags/Routes/web.php b/Tags/Routes/web.php
deleted file mode 100644
index 2b425f6..0000000
--- a/Tags/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/tags')->name('administration.tags.')->group(function (): void {
- Route::get('/', [TagController::class, 'index'])->name('index');
- Route::get('/create', [TagController::class, 'create'])->name('create');
- Route::post('/', [TagController::class, 'store'])->name('store');
- Route::get('/{tag}/edit', [TagController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{tag}', [TagController::class, 'update'])->name('update');
- Route::delete('/{tag}', [TagController::class, 'destroy'])->name('destroy');
-});
diff --git a/Tags/Tests/Feature/ModuleBootTest.php b/Tags/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index 1bc2dd9..0000000
--- a/Tags/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,13 +0,0 @@
-assertSame('Tags', config('tags.name'));
- }
-}
diff --git a/Tags/module.json b/Tags/module.json
deleted file mode 100644
index 53001bb..0000000
--- a/Tags/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "Tags",
- "alias": "tags",
- "version": "1.0.0",
- "description": "Tags publishing module.",
- "keywords": ["publishing", "tags"],
- "priority": 30,
- "providers": ["Modules\\Tags\\Providers\\TagsServiceProvider"],
- "aliases": {},
- "files": [],
- "requires": ["Core", "Administration"]
-}
diff --git a/Team/Config/config.php b/Team/Config/config.php
deleted file mode 100644
index 3342295..0000000
--- a/Team/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Team'];
diff --git a/Team/Database/Migrations/2026_07_24_000800_create_team_members_table.php b/Team/Database/Migrations/2026_07_24_000800_create_team_members_table.php
deleted file mode 100644
index 67bfdcc..0000000
--- a/Team/Database/Migrations/2026_07_24_000800_create_team_members_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-id();
- $table->string('name');
- $table->string('job_title')->nullable();
- $table->text('biography')->nullable();
- $table->string('email')->nullable();
- $table->unsignedInteger('sort_order')->default(0);
- $table->boolean('enabled')->default(true);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('team_members');
- }
-};
diff --git a/Team/Http/Controllers/TeamMemberController.php b/Team/Http/Controllers/TeamMemberController.php
deleted file mode 100644
index 270e4ad..0000000
--- a/Team/Http/Controllers/TeamMemberController.php
+++ /dev/null
@@ -1,62 +0,0 @@
- TeamMember::query()->latest()->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('team::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'],
- 'job_title' => ['nullable', 'string'],
- 'biography' => ['nullable', 'string'],
- 'email' => ['nullable', 'string'],
- 'sort_order' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- TeamMember::create($data);
-
- return redirect()->route('administration.team.index')->with('status', 'TeamMember created.');
- }
-
- public function edit(TeamMember $teamMember): View
- {
- return view('team::admin.form', ['record' => $teamMember]);
- }
-
- public function update(Request $request, TeamMember $teamMember): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'],
- 'job_title' => ['nullable', 'string'],
- 'biography' => ['nullable', 'string'],
- 'email' => ['nullable', 'string'],
- 'sort_order' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- $teamMember->update($data);
-
- return redirect()->route('administration.team.index')->with('status', 'TeamMember updated.');
- }
-
- public function destroy(TeamMember $teamMember): RedirectResponse
- {
- $teamMember->delete();
-
- return back()->with('status', 'TeamMember deleted.');
- }
-}
diff --git a/Team/Models/TeamMember.php b/Team/Models/TeamMember.php
deleted file mode 100644
index 1ada6eb..0000000
--- a/Team/Models/TeamMember.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'integer',
- 'enabled' => 'boolean',
- ];
- }
-}
diff --git a/Team/Providers/RouteServiceProvider.php b/Team/Providers/RouteServiceProvider.php
deleted file mode 100644
index a50f441..0000000
--- a/Team/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Team', 'Routes/web.php')));
- }
-}
diff --git a/Team/Providers/TeamServiceProvider.php b/Team/Providers/TeamServiceProvider.php
deleted file mode 100644
index 6cdcac1..0000000
--- a/Team/Providers/TeamServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Team', 'Config/config.php'), 'team');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Team', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Team', 'Resources/views'), 'team');
- }
-}
diff --git a/Team/Resources/views/admin/form.blade.php b/Team/Resources/views/admin/form.blade.php
deleted file mode 100644
index 621f780..0000000
--- a/Team/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
{{ isset($record) ? 'Edit' : 'Create' }} Team member @csrf @isset($record) @method('PUT') @endisset
-Name @error('name')
{{ $message }}
@enderror
-Job Title @error('job_title')
{{ $message }}
@enderror
-Biography {{ old('biography', $record->biography ?? '') }} @error('biography')
{{ $message }}
@enderror
-Email @error('email')
{{ $message }}
@enderror
-Sort Order @error('sort_order')
{{ $message }}
@enderror
- enabled ?? true))>Enabled
-
-
diff --git a/Team/Resources/views/admin/index.blade.php b/Team/Resources/views/admin/index.blade.php
deleted file mode 100644
index 6345c12..0000000
--- a/Team/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-@if(session('status'))
{{ session('status') }}
@endif
-
Name Job Title Biography Actions @forelse($records as $record){{ is_array($record->name) ? json_encode($record->name) : Str::limit((string) $record->name, 70) }} {{ is_array($record->job_title) ? json_encode($record->job_title) : Str::limit((string) $record->job_title, 70) }} {{ is_array($record->biography) ? json_encode($record->biography) : Str::limit((string) $record->biography, 70) }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
-
diff --git a/Team/Routes/web.php b/Team/Routes/web.php
deleted file mode 100644
index 0c6678d..0000000
--- a/Team/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/team')->name('administration.team.')->group(function (): void {
- Route::get('/', [TeamMemberController::class, 'index'])->name('index');
- Route::get('/create', [TeamMemberController::class, 'create'])->name('create');
- Route::post('/', [TeamMemberController::class, 'store'])->name('store');
- Route::get('/{teamMember}/edit', [TeamMemberController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{teamMember}', [TeamMemberController::class, 'update'])->name('update');
- Route::delete('/{teamMember}', [TeamMemberController::class, 'destroy'])->name('destroy');
-});
diff --git a/Team/Support/ModuleInfo.php b/Team/Support/ModuleInfo.php
deleted file mode 100644
index 4fcd387..0000000
--- a/Team/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBeTrue();
-});
diff --git a/Team/module.json b/Team/module.json
deleted file mode 100644
index 7cbacd8..0000000
--- a/Team/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Team",
- "alias": "team",
- "version": "1.0.0",
- "description": "Team CMS module.",
- "keywords": [
- "cms",
- "team"
- ],
- "priority": 28,
- "providers": [
- "Modules\\Team\\Providers\\TeamServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Testimonials/Config/config.php b/Testimonials/Config/config.php
deleted file mode 100644
index ab940e8..0000000
--- a/Testimonials/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Testimonials'];
diff --git a/Testimonials/Database/Migrations/2026_07_24_000900_create_testimonials_table.php b/Testimonials/Database/Migrations/2026_07_24_000900_create_testimonials_table.php
deleted file mode 100644
index bebe283..0000000
--- a/Testimonials/Database/Migrations/2026_07_24_000900_create_testimonials_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-id();
- $table->string('name');
- $table->string('company')->nullable();
-
- $table->unsignedInteger('rating')->default(5);
- $table->boolean('enabled')->default(true);
- $table->timestamps();
- $table->softDeletes();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('testimonials');
- }
-};
diff --git a/Testimonials/Database/Migrations/2026_07_27_070000_add_content_to_testimonials_table.php b/Testimonials/Database/Migrations/2026_07_27_070000_add_content_to_testimonials_table.php
deleted file mode 100644
index e81f8a9..0000000
--- a/Testimonials/Database/Migrations/2026_07_27_070000_add_content_to_testimonials_table.php
+++ /dev/null
@@ -1,26 +0,0 @@
-text('content')->nullable()->after('company');
- });
- }
- }
-
- public function down(): void
- {
- if (Schema::hasTable('testimonials') && Schema::hasColumn('testimonials', 'content')) {
- Schema::table('testimonials', function (Blueprint $table): void {
- $table->dropColumn('content');
- });
- }
- }
-};
diff --git a/Testimonials/Http/Controllers/TestimonialController.php b/Testimonials/Http/Controllers/TestimonialController.php
deleted file mode 100644
index 908e974..0000000
--- a/Testimonials/Http/Controllers/TestimonialController.php
+++ /dev/null
@@ -1,60 +0,0 @@
- Testimonial::query()->latest()->paginate(20)]);
- }
-
- public function create(): View
- {
- return view('testimonials::admin.form');
- }
-
- public function store(Request $request): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'],
- 'company' => ['nullable', 'string'],
- 'content' => ['required', 'string'],
- 'rating' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- Testimonial::create($data);
-
- return redirect()->route('administration.testimonials.index')->with('status', 'Testimonial created.');
- }
-
- public function edit(Testimonial $testimonial): View
- {
- return view('testimonials::admin.form', ['record' => $testimonial]);
- }
-
- public function update(Request $request, Testimonial $testimonial): RedirectResponse
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'],
- 'company' => ['nullable', 'string'],
- 'content' => ['required', 'string'],
- 'rating' => ['nullable', 'integer', 'min:0'],
- 'enabled' => ['sometimes', 'boolean']]);
- $data['enabled'] = $request->boolean('enabled');
- $testimonial->update($data);
-
- return redirect()->route('administration.testimonials.index')->with('status', 'Testimonial updated.');
- }
-
- public function destroy(Testimonial $testimonial): RedirectResponse
- {
- $testimonial->delete();
-
- return back()->with('status', 'Testimonial deleted.');
- }
-}
diff --git a/Testimonials/Models/Testimonial.php b/Testimonials/Models/Testimonial.php
deleted file mode 100644
index 590160f..0000000
--- a/Testimonials/Models/Testimonial.php
+++ /dev/null
@@ -1,24 +0,0 @@
- 'integer',
- 'enabled' => 'boolean',
- ];
- }
-}
diff --git a/Testimonials/Providers/RouteServiceProvider.php b/Testimonials/Providers/RouteServiceProvider.php
deleted file mode 100644
index 98aac75..0000000
--- a/Testimonials/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-routes(fn () => Route::middleware('web')->group(module_path('Testimonials', 'Routes/web.php')));
- }
-}
diff --git a/Testimonials/Providers/TestimonialsServiceProvider.php b/Testimonials/Providers/TestimonialsServiceProvider.php
deleted file mode 100644
index fde762a..0000000
--- a/Testimonials/Providers/TestimonialsServiceProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path('Testimonials', 'Config/config.php'), 'testimonials');
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Testimonials', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Testimonials', 'Resources/views'), 'testimonials');
- }
-}
diff --git a/Testimonials/Resources/views/admin/form.blade.php b/Testimonials/Resources/views/admin/form.blade.php
deleted file mode 100644
index c6f4e86..0000000
--- a/Testimonials/Resources/views/admin/form.blade.php
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
{{ isset($record) ? 'Edit' : 'Create' }} Testimonial @csrf @isset($record) @method('PUT') @endisset
-Name @error('name')
{{ $message }}
@enderror
-Company @error('company')
{{ $message }}
@enderror
-Content {{ old('content', $record->content ?? '') }} @error('content')
{{ $message }}
@enderror
-Rating @error('rating')
{{ $message }}
@enderror
- enabled ?? true))>Enabled
-
-
diff --git a/Testimonials/Resources/views/admin/index.blade.php b/Testimonials/Resources/views/admin/index.blade.php
deleted file mode 100644
index ab55427..0000000
--- a/Testimonials/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1,5 +0,0 @@
-
-Testimonials Manage testimonials content.
Add new
-@if(session('status'))
{{ session('status') }}
@endif
-
Name Company Content Actions @forelse($records as $record){{ is_array($record->name) ? json_encode($record->name) : Str::limit((string) $record->name, 70) }} {{ is_array($record->company) ? json_encode($record->company) : Str::limit((string) $record->company, 70) }} {{ is_array($record->content) ? json_encode($record->content) : Str::limit((string) $record->content, 70) }} Edit @csrf @method('DELETE')Delete @emptyNo records yet. @endforelse
{{ $records->links() }}
-
diff --git a/Testimonials/Routes/web.php b/Testimonials/Routes/web.php
deleted file mode 100644
index 3527aff..0000000
--- a/Testimonials/Routes/web.php
+++ /dev/null
@@ -1,13 +0,0 @@
-prefix('administration/testimonials')->name('administration.testimonials.')->group(function (): void {
- Route::get('/', [TestimonialController::class, 'index'])->name('index');
- Route::get('/create', [TestimonialController::class, 'create'])->name('create');
- Route::post('/', [TestimonialController::class, 'store'])->name('store');
- Route::get('/{testimonial}/edit', [TestimonialController::class, 'edit'])->name('edit');
- Route::match(['put', 'patch'], '/{testimonial}', [TestimonialController::class, 'update'])->name('update');
- Route::delete('/{testimonial}', [TestimonialController::class, 'destroy'])->name('destroy');
-});
diff --git a/Testimonials/Support/ModuleInfo.php b/Testimonials/Support/ModuleInfo.php
deleted file mode 100644
index afc0468..0000000
--- a/Testimonials/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBeTrue();
-});
diff --git a/Testimonials/module.json b/Testimonials/module.json
deleted file mode 100644
index 9ee5cfa..0000000
--- a/Testimonials/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Testimonials",
- "alias": "testimonials",
- "version": "1.0.0",
- "description": "Testimonials CMS module.",
- "keywords": [
- "cms",
- "testimonials"
- ],
- "priority": 29,
- "providers": [
- "Modules\\Testimonials\\Providers\\TestimonialsServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core",
- "Administration"
- ]
-}
diff --git a/Users/Config/config.php b/Users/Config/config.php
deleted file mode 100644
index d0471c5..0000000
--- a/Users/Config/config.php
+++ /dev/null
@@ -1,6 +0,0 @@
- 'Users',
- 'enabled' => true,
-];
diff --git a/Users/Http/Controllers/UserController.php b/Users/Http/Controllers/UserController.php
deleted file mode 100644
index 79191a5..0000000
--- a/Users/Http/Controllers/UserController.php
+++ /dev/null
@@ -1,56 +0,0 @@
- User::with('roles')->latest()->paginate(20)]);
- }
-
- public function create()
- {
- return view('stylist-theme-admin::administration.users.create', ['roles' => Role::orderBy('name')->pluck('name')]);
- }
-
- public function store(Request $request)
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', 'unique:users,email'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'roles' => ['array'], 'roles.*' => ['string', 'exists:roles,name']]);
- $user = User::create($data);
- $user->syncRoles($data['roles'] ?? []);
-
- return redirect()->route('administration.users.index')->with('status', 'User created.');
- }
-
- public function edit(User $user)
- {
- return view('stylist-theme-admin::administration.users.edit', ['user' => $user->load('roles'), 'roles' => Role::orderBy('name')->pluck('name')]);
- }
-
- public function update(Request $request, User $user)
- {
- $data = $request->validate(['name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user)], 'password' => ['nullable', 'string', 'min:8', 'confirmed'], 'roles' => ['array'], 'roles.*' => ['string', 'exists:roles,name']]);
- if (blank($data['password'] ?? null)) {
- unset($data['password']);
- }
- $user->update($data);
- $user->syncRoles($data['roles'] ?? []);
-
- return redirect()->route('administration.users.index')->with('status', 'User updated.');
- }
-
- public function destroy(User $user)
- {
- abort_if($user->is(auth()->user()), 422, 'You cannot delete your own account.');
- $user->delete();
-
- return redirect()->route('administration.users.index')->with('status', 'User deleted.');
- }
-}
diff --git a/Users/Providers/RouteServiceProvider.php b/Users/Providers/RouteServiceProvider.php
deleted file mode 100644
index 89f776a..0000000
--- a/Users/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,17 +0,0 @@
-routes(function (): void {
- Route::middleware('web')->group(module_path('Users', 'Routes/web.php'));
- });
- }
-}
diff --git a/Users/Providers/UsersServiceProvider.php b/Users/Providers/UsersServiceProvider.php
deleted file mode 100644
index 96446ad..0000000
--- a/Users/Providers/UsersServiceProvider.php
+++ /dev/null
@@ -1,21 +0,0 @@
-app->register(RouteServiceProvider::class);
- $this->mergeConfigFrom(module_path(ModuleInfo::name(), 'Config/config.php'), ModuleInfo::nameLower());
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path(ModuleInfo::name(), 'Database/Migrations'));
- $this->loadViewsFrom(module_path(ModuleInfo::name(), 'Resources/views'), ModuleInfo::nameLower());
- }
-}
diff --git a/Users/Routes/web.php b/Users/Routes/web.php
deleted file mode 100644
index d4d0dc8..0000000
--- a/Users/Routes/web.php
+++ /dev/null
@@ -1,8 +0,0 @@
-prefix('administration/users')->name('administration.users.')->group(function (): void {
- Route::resource('', UserController::class)->parameters(['' => 'user'])->except(['show']);
-});
diff --git a/Users/Support/ModuleInfo.php b/Users/Support/ModuleInfo.php
deleted file mode 100644
index 6122d1c..0000000
--- a/Users/Support/ModuleInfo.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toBe('Users');
-});
diff --git a/Users/composer.json b/Users/composer.json
deleted file mode 100644
index cbcc957..0000000
--- a/Users/composer.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "absdev/users-module",
- "description": "Users foundation module for the ABSDEV Laravel application platform.",
- "type": "laravel-module",
- "autoload": {
- "psr-4": {
- "Modules\\Users\\": ""
- }
- }
-}
diff --git a/Users/module.json b/Users/module.json
deleted file mode 100644
index fb2c657..0000000
--- a/Users/module.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "Users",
- "alias": "users",
- "version": "1.0.0",
- "description": "Users foundation module for the ABSDEV Laravel application platform.",
- "keywords": [
- "laravel",
- "modules",
- "users"
- ],
- "priority": 10,
- "providers": [
- "Modules\\Users\\Providers\\UsersServiceProvider"
- ],
- "aliases": {},
- "files": [],
- "requires": [
- "Core"
- ]
-}
diff --git a/Workflow/Concerns/HasWorkflow.php b/Workflow/Concerns/HasWorkflow.php
deleted file mode 100644
index d018a87..0000000
--- a/Workflow/Concerns/HasWorkflow.php
+++ /dev/null
@@ -1,19 +0,0 @@
-morphOne(WorkflowInstance::class, 'subject');
- }
-
- public function workflowLabel(): string
- {
- return (string) ($this->title ?? $this->name ?? class_basename($this).' #'.$this->getKey());
- }
-}
diff --git a/Workflow/Config/config.php b/Workflow/Config/config.php
deleted file mode 100644
index c61a662..0000000
--- a/Workflow/Config/config.php
+++ /dev/null
@@ -1,3 +0,0 @@
- 'Workflow', 'default_due_days' => 3];
diff --git a/Workflow/Contracts/Workflowable.php b/Workflow/Contracts/Workflowable.php
deleted file mode 100644
index 9d76383..0000000
--- a/Workflow/Contracts/Workflowable.php
+++ /dev/null
@@ -1,12 +0,0 @@
-id();
- $t->string('name');
- $t->string('slug')->unique();
- $t->text('description')->nullable();
- $t->string('subject_type')->nullable()->index();
- $t->boolean('is_active')->default(true);
- $t->timestamps();
- });
- Schema::create('workflow_stages', function (Blueprint $t) {
- $t->id();
- $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete();
- $t->string('name');
- $t->string('slug');
- $t->unsignedInteger('position')->default(0);
- $t->string('required_permission')->nullable();
- $t->boolean('is_initial')->default(false);
- $t->boolean('is_terminal')->default(false);
- $t->boolean('allows_rejection')->default(true);
- $t->unsignedInteger('due_days')->nullable();
- $t->timestamps();
- $t->unique(['workflow_definition_id', 'slug']);
- });
- Schema::create('workflow_transitions', function (Blueprint $t) {
- $t->id();
- $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete();
- $t->foreignId('from_stage_id')->constrained('workflow_stages')->cascadeOnDelete();
- $t->foreignId('to_stage_id')->constrained('workflow_stages')->cascadeOnDelete();
- $t->string('name');
- $t->string('action');
- $t->string('required_permission')->nullable();
- $t->boolean('requires_comment')->default(false);
- $t->timestamps();
- });
- Schema::create('workflow_instances', function (Blueprint $t) {
- $t->id();
- $t->foreignId('workflow_definition_id')->constrained()->cascadeOnDelete();
- $t->foreignId('workflow_stage_id')->constrained('workflow_stages');
- $t->string('subject_type');
- $t->unsignedBigInteger('subject_id');
- $t->string('status')->default('active');
- $t->foreignId('started_by')->nullable()->constrained('users')->nullOnDelete();
- $t->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
- $t->timestamp('due_at')->nullable();
- $t->timestamp('completed_at')->nullable();
- $t->timestamps();
- $t->unique(['subject_type', 'subject_id']);
- $t->index(['status', 'workflow_stage_id']);
- });
- Schema::create('workflow_history', function (Blueprint $t) {
- $t->id();
- $t->foreignId('workflow_instance_id')->constrained()->cascadeOnDelete();
- $t->foreignId('from_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete();
- $t->foreignId('to_stage_id')->nullable()->constrained('workflow_stages')->nullOnDelete();
- $t->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
- $t->string('action');
- $t->text('comment')->nullable();
- $t->json('metadata')->nullable();
- $t->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('workflow_history');
- Schema::dropIfExists('workflow_instances');
- Schema::dropIfExists('workflow_transitions');
- Schema::dropIfExists('workflow_stages');
- Schema::dropIfExists('workflow_definitions');
- }
-};
diff --git a/Workflow/Http/Controllers/WorkflowController.php b/Workflow/Http/Controllers/WorkflowController.php
deleted file mode 100644
index 62479b1..0000000
--- a/Workflow/Http/Controllers/WorkflowController.php
+++ /dev/null
@@ -1,44 +0,0 @@
-user()->can('workflow.view'), 403);
-
- return view('workflow::admin.index', ['definitions' => WorkflowDefinition::with('stages')->latest()->get(), 'queue' => WorkflowInstance::with(['definition', 'stage', 'subject'])->where('status', 'active')->latest()->paginate(20), 'completed' => WorkflowInstance::where('status', 'completed')->count()]);
- }
-
- public function store(Request $request): RedirectResponse
- {
- abort_unless($request->user()->can('workflow.manage'), 403);
- $data = $request->validate(['name' => 'required|string|max:120', 'description' => 'nullable|string', 'subject_type' => 'nullable|string|max:255']);
- $definition = WorkflowDefinition::create($data + ['slug' => Str::slug($data['name']), 'is_active' => true]);
- $draft = $definition->stages()->create(['name' => 'Draft', 'slug' => 'draft', 'position' => 0, 'is_initial' => true, 'due_days' => null]);
- $review = $definition->stages()->create(['name' => 'Review', 'slug' => 'review', 'position' => 10, 'required_permission' => 'workflow.review', 'due_days' => 3]);
- $published = $definition->stages()->create(['name' => 'Approved', 'slug' => 'approved', 'position' => 20, 'is_terminal' => true, 'allows_rejection' => false]);
- $definition->transitions()->createMany([['from_stage_id' => $draft->id, 'to_stage_id' => $review->id, 'name' => 'Submit for review', 'action' => 'submit'], ['from_stage_id' => $review->id, 'to_stage_id' => $published->id, 'name' => 'Approve', 'action' => 'approve', 'required_permission' => 'workflow.review'], ['from_stage_id' => $review->id, 'to_stage_id' => $draft->id, 'name' => 'Request changes', 'action' => 'reject', 'required_permission' => 'workflow.review', 'requires_comment' => true]]);
-
- return back()->with('success', 'Workflow created.');
- }
-
- public function transition(Request $request, WorkflowInstance $instance, WorkflowManager $manager): RedirectResponse
- {
- abort_unless($request->user()->can('workflow.review'), 403);
- $data = $request->validate(['action' => 'required|string', 'comment' => 'nullable|string|max:2000']);
- $manager->transition($instance, $data['action'], $request->user(), $data['comment'] ?? null);
-
- return back()->with('success', 'Workflow updated.');
- }
-}
diff --git a/Workflow/Models/WorkflowDefinition.php b/Workflow/Models/WorkflowDefinition.php
deleted file mode 100644
index 2316842..0000000
--- a/Workflow/Models/WorkflowDefinition.php
+++ /dev/null
@@ -1,26 +0,0 @@
- 'boolean'];
- }
-
- public function stages(): HasMany
- {
- return $this->hasMany(WorkflowStage::class)->orderBy('position');
- }
-
- public function transitions(): HasMany
- {
- return $this->hasMany(WorkflowTransition::class);
- }
-}
diff --git a/Workflow/Models/WorkflowHistory.php b/Workflow/Models/WorkflowHistory.php
deleted file mode 100644
index 2b8786f..0000000
--- a/Workflow/Models/WorkflowHistory.php
+++ /dev/null
@@ -1,17 +0,0 @@
- 'array'];
- }
-}
diff --git a/Workflow/Models/WorkflowInstance.php b/Workflow/Models/WorkflowInstance.php
deleted file mode 100644
index b3d1af4..0000000
--- a/Workflow/Models/WorkflowInstance.php
+++ /dev/null
@@ -1,38 +0,0 @@
- 'datetime', 'completed_at' => 'datetime'];
- }
-
- public function definition(): BelongsTo
- {
- return $this->belongsTo(WorkflowDefinition::class, 'workflow_definition_id');
- }
-
- public function stage(): BelongsTo
- {
- return $this->belongsTo(WorkflowStage::class, 'workflow_stage_id');
- }
-
- public function subject(): MorphTo
- {
- return $this->morphTo();
- }
-
- public function history(): HasMany
- {
- return $this->hasMany(WorkflowHistory::class)->latest();
- }
-}
diff --git a/Workflow/Models/WorkflowStage.php b/Workflow/Models/WorkflowStage.php
deleted file mode 100644
index 3d8a8ad..0000000
--- a/Workflow/Models/WorkflowStage.php
+++ /dev/null
@@ -1,21 +0,0 @@
- 'boolean', 'is_terminal' => 'boolean', 'allows_rejection' => 'boolean'];
- }
-
- public function definition(): BelongsTo
- {
- return $this->belongsTo(WorkflowDefinition::class, 'workflow_definition_id');
- }
-}
diff --git a/Workflow/Models/WorkflowTransition.php b/Workflow/Models/WorkflowTransition.php
deleted file mode 100644
index 66bb87f..0000000
--- a/Workflow/Models/WorkflowTransition.php
+++ /dev/null
@@ -1,15 +0,0 @@
- 'boolean'];
- }
-}
diff --git a/Workflow/Providers/RouteServiceProvider.php b/Workflow/Providers/RouteServiceProvider.php
deleted file mode 100644
index 4197f68..0000000
--- a/Workflow/Providers/RouteServiceProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-group(module_path('Workflow', 'Routes/web.php'));
- }
-}
diff --git a/Workflow/Providers/WorkflowServiceProvider.php b/Workflow/Providers/WorkflowServiceProvider.php
deleted file mode 100644
index ab10b81..0000000
--- a/Workflow/Providers/WorkflowServiceProvider.php
+++ /dev/null
@@ -1,22 +0,0 @@
-mergeConfigFrom(module_path('Workflow', 'Config/config.php'), 'workflow');
- $this->app->singleton(WorkflowManager::class);
- $this->app->register(RouteServiceProvider::class);
- }
-
- public function boot(): void
- {
- $this->loadMigrationsFrom(module_path('Workflow', 'Database/Migrations'));
- $this->loadViewsFrom(module_path('Workflow', 'Resources/views'), 'workflow');
- }
-}
diff --git a/Workflow/Resources/views/admin/index.blade.php b/Workflow/Resources/views/admin/index.blade.php
deleted file mode 100644
index 673cde3..0000000
--- a/Workflow/Resources/views/admin/index.blade.php
+++ /dev/null
@@ -1 +0,0 @@
-Workflow Approval definitions, active reviews and completed work.
Definitions {{ $definitions->count() }}
Awaiting action {{ $queue->total() }}
Completed {{ $completed }}
Review queue @forelse($queue as $item)
{{ method_exists($item->subject,'workflowLabel') ? $item->subject->workflowLabel() : class_basename($item->subject_type).' #'.$item->subject_id }}
{{ $item->definition->name }} · {{ $item->stage->name }} @if($item->due_at) · due {{ $item->due_at->diffForHumans() }} @endif
@csrfApprove @csrfChanges @empty
No items are awaiting review. @endforelse
{{ $queue->links() }}
Create workflow @csrfCreate @foreach($definitions as $definition)
{{ $definition->name }}
{{ $definition->stages->pluck('name')->join(' → ') }}
@endforeach
diff --git a/Workflow/Routes/web.php b/Workflow/Routes/web.php
deleted file mode 100644
index 6653370..0000000
--- a/Workflow/Routes/web.php
+++ /dev/null
@@ -1,10 +0,0 @@
-prefix('administration/workflows')->name('administration.workflows.')->group(function () {
- Route::get('/', [WorkflowController::class, 'index'])->name('index');
- Route::post('/', [WorkflowController::class, 'store'])->name('store');
- Route::post('/{instance}/transition', [WorkflowController::class, 'transition'])->name('transition');
-});
diff --git a/Workflow/Services/WorkflowManager.php b/Workflow/Services/WorkflowManager.php
deleted file mode 100644
index 7b8dc1b..0000000
--- a/Workflow/Services/WorkflowManager.php
+++ /dev/null
@@ -1,46 +0,0 @@
-stages()->where('is_initial', true)->first() ?? $definition->stages()->firstOrFail();
- $instance = WorkflowInstance::updateOrCreate(['subject_type' => $subject->getMorphClass(), 'subject_id' => $subject->getKey()], ['workflow_definition_id' => $definition->getKey(), 'workflow_stage_id' => $stage->getKey(), 'status' => $stage->is_terminal ? 'completed' : 'active', 'started_by' => $user?->getAuthIdentifier(), 'due_at' => $stage->due_days ? now()->addDays($stage->due_days) : null, 'completed_at' => $stage->is_terminal ? now() : null]);
- WorkflowHistory::create(['workflow_instance_id' => $instance->id, 'to_stage_id' => $stage->id, 'user_id' => $user?->getAuthIdentifier(), 'action' => 'started']);
-
- return $instance->fresh(['definition', 'stage']);
- });
- }
-
- public function transition(WorkflowInstance $instance, string $action, ?Authenticatable $user = null, ?string $comment = null): WorkflowInstance
- {
- return DB::transaction(function () use ($instance, $action, $user, $comment) {
- $transition = WorkflowTransition::where('workflow_definition_id', $instance->workflow_definition_id)->where('from_stage_id', $instance->workflow_stage_id)->where('action', $action)->first();
- if (! $transition) {
- throw ValidationException::withMessages(['action' => 'This transition is not available.']);
- } if ($transition->requires_comment && blank($comment)) {
- throw ValidationException::withMessages(['comment' => 'A comment is required.']);
- } if ($transition->required_permission && (! $user || ! method_exists($user, 'can') || ! $user->can($transition->required_permission))) {
- abort(403);
- } $target = WorkflowStage::findOrFail($transition->to_stage_id);
- $from = $instance->workflow_stage_id;
- $instance->update(['workflow_stage_id' => $target->id, 'status' => $target->is_terminal ? 'completed' : 'active', 'due_at' => $target->due_days ? now()->addDays($target->due_days) : null, 'completed_at' => $target->is_terminal ? now() : null]);
- WorkflowHistory::create(['workflow_instance_id' => $instance->id, 'from_stage_id' => $from, 'to_stage_id' => $target->id, 'user_id' => $user?->getAuthIdentifier(), 'action' => $action, 'comment' => $comment]);
-
- return $instance->fresh(['definition', 'stage', 'subject']);
- });
- }
-}
diff --git a/Workflow/Tests/Feature/ModuleBootTest.php b/Workflow/Tests/Feature/ModuleBootTest.php
deleted file mode 100644
index e489747..0000000
--- a/Workflow/Tests/Feature/ModuleBootTest.php
+++ /dev/null
@@ -1,14 +0,0 @@
-assertTrue(app()->providerIsLoaded(WorkflowServiceProvider::class));
- }
-}
diff --git a/Workflow/module.json b/Workflow/module.json
deleted file mode 100644
index 2a022f0..0000000
--- a/Workflow/module.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "Workflow",
- "alias": "workflow",
- "description": "Reusable approval workflows, review queues, transitions and audit history.",
- "keywords": ["workflow", "approval", "review"],
- "priority": 35,
- "providers": [
- "Modules\\Workflow\\Providers\\WorkflowServiceProvider"
- ],
- "files": []
-}