Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98102e510d | ||
|
|
1d8a202bdf | ||
| 8f2590477c | |||
|
|
be304e7b1a | ||
|
|
fab9318f64 | ||
|
|
1fbac8f9ab | ||
|
|
208cb93c95 |
295 changed files with 4157 additions and 521 deletions
|
|
@ -71,7 +71,6 @@ src/Core/Lang/ # Translation system with ICU + locale fallback chains
|
|||
src/Core/Media/ # Media handling with thumbnail helpers
|
||||
src/Core/Search/ # Search functionality
|
||||
src/Core/Seo/ # SEO utilities
|
||||
src/Core/Service/ # Service discovery and dependency resolution
|
||||
src/Core/Storage/ # Storage with Redis circuit breaker + fallback
|
||||
src/Core/Webhook/ # Webhook system + CronTrigger scheduled action
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Core\Activity\Models\Activity;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|
|
@ -449,7 +451,7 @@ return [
|
|||
// Custom Activity model class (optional).
|
||||
// Set this to use a custom Activity model with additional scopes.
|
||||
// Default: Core\Activity\Models\Activity::class
|
||||
'activity_model' => env('CORE_ACTIVITY_MODEL', \Core\Activity\Models\Activity::class),
|
||||
'activity_model' => env('CORE_ACTIVITY_MODEL', Activity::class),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
10
go.mod
10
go.mod
|
|
@ -3,17 +3,17 @@ module forge.lthn.ai/core/php
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/cli v0.3.5
|
||||
forge.lthn.ai/core/go-i18n v0.1.5
|
||||
forge.lthn.ai/core/go-io v0.1.5
|
||||
forge.lthn.ai/core/cli v0.3.7
|
||||
forge.lthn.ai/core/go-i18n v0.1.7
|
||||
forge.lthn.ai/core/go-io v0.1.7
|
||||
github.com/dunglas/frankenphp v1.12.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/go v0.3.1 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.4 // indirect
|
||||
forge.lthn.ai/core/go v0.3.3 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.6 // indirect
|
||||
forge.lthn.ai/core/go-log v0.0.4 // indirect
|
||||
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.15.0 // indirect
|
||||
|
|
|
|||
20
go.sum
20
go.sum
|
|
@ -1,13 +1,13 @@
|
|||
forge.lthn.ai/core/cli v0.3.5 h1:P7yK0DmSA1QnUMFuCjJZf/fk/akKPIxopQ6OwD8Sar8=
|
||||
forge.lthn.ai/core/cli v0.3.5/go.mod h1:SeArHx+hbpX5iZqgASCD7Q1EDoc6uaaGiGBotmNzIx4=
|
||||
forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM=
|
||||
forge.lthn.ai/core/go v0.3.1/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
|
||||
forge.lthn.ai/core/go-i18n v0.1.5 h1:B4hV4eTl63akZiplM8lswuttctrcSOCWyFSGBZmu6Nc=
|
||||
forge.lthn.ai/core/go-i18n v0.1.5/go.mod h1:hJsUxmqdPly73i3VkTDxvmbrpjxSd65hQVQqWA3+fnM=
|
||||
forge.lthn.ai/core/go-inference v0.1.4 h1:fuAgWbqsEDajHniqAKyvHYbRcBrkGEiGSqR2pfTMRY0=
|
||||
forge.lthn.ai/core/go-inference v0.1.4/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
||||
forge.lthn.ai/core/go-io v0.1.5 h1:+XJ1YhaGGFLGtcNbPtVlndTjk+pO0Ydi2hRDj5/cHOM=
|
||||
forge.lthn.ai/core/go-io v0.1.5/go.mod h1:FRtXSsi8W+U9vewCU+LBAqqbIj3wjXA4dBdSv3SAtWI=
|
||||
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
|
||||
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
|
||||
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
|
||||
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
|
||||
forge.lthn.ai/core/go-inference v0.1.6 h1:ce42zC0zO8PuISUyAukAN1NACEdWp5wF1mRgnh5+58E=
|
||||
forge.lthn.ai/core/go-inference v0.1.6/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
||||
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
|
||||
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 h1:1yw6O62BReQ+uA1oyk9XaQTvLhcoHWmoQAgXmDFXpIY=
|
||||
|
|
|
|||
57
src/Core/Actions/CLAUDE.md
Normal file
57
src/Core/Actions/CLAUDE.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Actions
|
||||
|
||||
Single-purpose business logic pattern with scheduling support.
|
||||
|
||||
## What It Does
|
||||
|
||||
Provides the `Action` trait for extracting business logic from controllers/components into focused, testable classes. Each action does one thing via a `handle()` method and gets a static `run()` shortcut that resolves dependencies from the container.
|
||||
|
||||
Also provides attribute-driven scheduling: annotate an Action with `#[Scheduled]` and it gets wired into Laravel's scheduler automatically.
|
||||
|
||||
## Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `Action` (trait) | Adds `static run(...$args)` that resolves via container and calls `handle()` |
|
||||
| `Actionable` (interface) | Optional contract for type-hinting actions |
|
||||
| `Scheduled` (attribute) | Marks an Action for scheduled execution with frequency string |
|
||||
| `ScheduledAction` (model) | Eloquent model persisted to `scheduled_actions` table |
|
||||
| `ScheduledActionScanner` | Discovers `#[Scheduled]` attributes by scanning directories |
|
||||
| `ScheduleServiceProvider` | Reads enabled scheduled actions from DB and registers with Laravel scheduler |
|
||||
|
||||
## Public API
|
||||
|
||||
```php
|
||||
// Use the Action pattern
|
||||
class CreateOrder {
|
||||
use Action;
|
||||
public function handle(User $user, array $data): Order { ... }
|
||||
}
|
||||
CreateOrder::run($user, $data); // resolves from container
|
||||
|
||||
// Schedule an action
|
||||
#[Scheduled(frequency: 'dailyAt:09:00', timezone: 'Europe/London')]
|
||||
class PublishDigest {
|
||||
use Action;
|
||||
public function handle(): void { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Frequency String Format
|
||||
|
||||
`method:arg1,arg2` maps directly to Laravel Schedule methods:
|
||||
- `everyMinute` / `hourly` / `daily` / `weekly` / `monthly`
|
||||
- `dailyAt:09:00` / `weeklyOn:1,09:00` / `cron:* * * * *`
|
||||
|
||||
## Integration
|
||||
|
||||
- Scanner skips `Tests/` directories and `*Test.php` files
|
||||
- ScheduleServiceProvider validates namespace (`App\`, `Core\`, `Mod\`) and frequency method against allowlists before executing
|
||||
- Actions are placed in `app/Mod/{Module}/Actions/`
|
||||
|
||||
## Conventions
|
||||
|
||||
- One action per file, named after what it does: `CreatePage`, `SendInvoice`
|
||||
- Dependencies injected via constructor
|
||||
- `handle()` is the single public method
|
||||
- Scheduling state is DB-driven (enable/disable without code changes)
|
||||
|
|
@ -99,7 +99,7 @@ class ScheduleServiceProvider extends ServiceProvider
|
|||
}
|
||||
|
||||
// Verify the class uses the Action trait
|
||||
if (! in_array(\Core\Actions\Action::class, class_uses_recursive($class), true)) {
|
||||
if (! in_array(Action::class, class_uses_recursive($class), true)) {
|
||||
logger()->warning("Scheduled action {$class} does not use the Action trait — skipping");
|
||||
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Core\Actions;
|
|||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* Represents a scheduled action persisted in the database.
|
||||
|
|
@ -24,10 +25,10 @@ use Illuminate\Database\Eloquent\Model;
|
|||
* @property bool $without_overlapping
|
||||
* @property bool $run_in_background
|
||||
* @property bool $is_enabled
|
||||
* @property \Illuminate\Support\Carbon|null $last_run_at
|
||||
* @property \Illuminate\Support\Carbon|null $next_run_at
|
||||
* @property \Illuminate\Support\Carbon $created_at
|
||||
* @property \Illuminate\Support\Carbon $updated_at
|
||||
* @property Carbon|null $last_run_at
|
||||
* @property Carbon|null $next_run_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class ScheduledAction extends Model
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Actions;
|
||||
|
||||
use Core\ModuleScanner;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
|
|
@ -24,7 +25,7 @@ use ReflectionClass;
|
|||
* It uses PHP's native reflection to read attributes — no file parsing.
|
||||
*
|
||||
* @see Scheduled The attribute this scanner discovers
|
||||
* @see \Core\ModuleScanner Similar pattern for Boot.php discovery
|
||||
* @see ModuleScanner Similar pattern for Boot.php discovery
|
||||
*/
|
||||
class ScheduledActionScanner
|
||||
{
|
||||
|
|
@ -32,7 +33,7 @@ class ScheduledActionScanner
|
|||
* Scan directories for classes with #[Scheduled] attribute.
|
||||
*
|
||||
* @param array<string> $paths Directories to scan recursively
|
||||
* @return array<class-string, Scheduled> Map of class name to attribute instance
|
||||
* @return array<class-string, Scheduled> Map of class name to attribute instance
|
||||
*/
|
||||
public function scan(array $paths): array
|
||||
{
|
||||
|
|
|
|||
48
src/Core/Activity/CLAUDE.md
Normal file
48
src/Core/Activity/CLAUDE.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Activity
|
||||
|
||||
Workspace-aware activity logging built on `spatie/laravel-activitylog`.
|
||||
|
||||
## What It Does
|
||||
|
||||
Wraps Spatie's activity log with automatic `workspace_id` tagging, a fluent query service, a Livewire feed component for the admin panel, and a prune command for retention management.
|
||||
|
||||
## Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `Boot` | Registers console commands, Livewire component, and service binding via lifecycle events |
|
||||
| `Activity` (model) | Extends Spatie's model with `ActivityScopes` trait. Adds `workspace_id`, `old_values`, `new_values`, `changes`, `causer_name`, `subject_name` accessors |
|
||||
| `ActivityLogService` | Fluent query builder: `logFor($model)`, `logBy($user)`, `forWorkspace($ws)`, `ofType('updated')`, `search('term')`, `paginate()`, `statistics()`, `timeline()`, `prune()` |
|
||||
| `LogsActivity` (trait) | Drop-in trait for models. Auto-logs dirty attributes, auto-tags `workspace_id` from model or request context, generates human descriptions |
|
||||
| `ActivityScopes` (trait) | 20+ Eloquent scopes: `forWorkspace`, `forSubject`, `byCauser`, `ofType`, `betweenDates`, `today`, `lastDays`, `search`, `withChanges`, `withExistingSubject` |
|
||||
| `ActivityPruneCommand` | `php artisan activity:prune [--days=N] [--dry-run]` |
|
||||
| `ActivityFeed` (Livewire) | `<livewire:core.activity-feed :workspace-id="$id" />` with filters, search, pagination, detail modal |
|
||||
|
||||
## Public API
|
||||
|
||||
```php
|
||||
// Make a model log activity
|
||||
class Post extends Model {
|
||||
use LogsActivity;
|
||||
protected array $activityLogAttributes = ['title', 'status'];
|
||||
}
|
||||
|
||||
// Query activities
|
||||
$service = app(ActivityLogService::class);
|
||||
$service->logFor($post)->lastDays(7)->paginate();
|
||||
$service->forWorkspace($workspace)->ofType('deleted')->recent(10);
|
||||
$service->statistics($workspace); // => [total, by_event, by_subject, by_user]
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
- Listens to `ConsoleBooting` and `AdminPanelBooting` lifecycle events
|
||||
- `LogsActivity` trait auto-detects workspace from model's `workspace_id` attribute, request `workspace_model` attribute, or authenticated user's `defaultHostWorkspace()`
|
||||
- Config: `core.activity.enabled`, `core.activity.retention_days` (default 90), `core.activity.log_name`
|
||||
- Override activity model in `config/activitylog.php`: `'activity_model' => Activity::class`
|
||||
|
||||
## Conventions
|
||||
|
||||
- `LogsActivity::withoutActivityLogging(fn() => ...)` to suppress logging during bulk operations
|
||||
- Models can implement `customizeActivity($activity, $event)` for custom property injection
|
||||
- Config properties on model: `$activityLogAttributes`, `$activityLogName`, `$activityLogEvents`, `$activityLogWorkspace`, `$activityLogOnlyDirty`
|
||||
17
src/Core/Activity/Concerns/CLAUDE.md
Normal file
17
src/Core/Activity/Concerns/CLAUDE.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Activity/Concerns/ — Activity Logging Trait
|
||||
|
||||
## Traits
|
||||
|
||||
| Trait | Purpose |
|
||||
|-------|---------|
|
||||
| `LogsActivity` | Drop-in trait for models that should log changes. Wraps `spatie/laravel-activitylog` with sensible defaults: auto workspace_id tagging, dirty-only logging, empty log suppression. |
|
||||
|
||||
## Configuration via Model Properties
|
||||
|
||||
- `$activityLogAttributes` — array of attributes to log (default: all dirty)
|
||||
- `$activityLogName` — custom log name
|
||||
- `$activityLogEvents` — events to log (default: created, updated, deleted)
|
||||
- `$activityLogWorkspace` — include workspace_id (default: true)
|
||||
- `$activityLogOnlyDirty` — only log changed attributes (default: true)
|
||||
|
||||
Static helpers: `activityLoggingEnabled()`, `withoutActivityLogging(callable)`.
|
||||
|
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Activity\Concerns;
|
||||
|
||||
use Spatie\Activitylog\Contracts\Activity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity as SpatieLogsActivity;
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ trait LogsActivity
|
|||
/**
|
||||
* Tap into the activity before it's saved to add workspace_id.
|
||||
*/
|
||||
public function tapActivity(\Spatie\Activitylog\Contracts\Activity $activity, string $eventName): void
|
||||
public function tapActivity(Activity $activity, string $eventName): void
|
||||
{
|
||||
if ($this->shouldIncludeWorkspace()) {
|
||||
$workspaceId = $this->getActivityWorkspaceId();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Core\Activity\Console;
|
|||
|
||||
use Core\Activity\Services\ActivityLogService;
|
||||
use Illuminate\Console\Command;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
/**
|
||||
* Command to prune old activity logs.
|
||||
|
|
@ -48,7 +49,7 @@ class ActivityPruneCommand extends Command
|
|||
|
||||
if ($this->option('dry-run')) {
|
||||
// Count without deleting
|
||||
$activityModel = config('core.activity.activity_model', \Spatie\Activitylog\Models\Activity::class);
|
||||
$activityModel = config('core.activity.activity_model', Activity::class);
|
||||
$count = $activityModel::where('created_at', '<', $cutoffDate)->count();
|
||||
|
||||
$this->info("Would delete {$count} activity records.");
|
||||
|
|
|
|||
9
src/Core/Activity/Console/CLAUDE.md
Normal file
9
src/Core/Activity/Console/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Activity/Console/ — Activity Log Commands
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Signature | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `ActivityPruneCommand` | `activity:prune` | Prunes old activity logs. Options: `--days=N` (retention period), `--dry-run` (show count without deleting). Uses retention from config when days not specified. |
|
||||
|
||||
Part of the Activity subsystem's maintenance tooling. Should be scheduled in the application's console kernel for regular cleanup.
|
||||
|
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||
namespace Core\Activity\Models;
|
||||
|
||||
use Core\Activity\Scopes\ActivityScopes;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Activitylog\Models\Activity as SpatieActivity;
|
||||
|
||||
/**
|
||||
|
|
@ -81,9 +82,9 @@ class Activity extends SpatieActivity
|
|||
/**
|
||||
* Get the changed attributes.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection<string, array{old: mixed, new: mixed}>
|
||||
* @return Collection<string, array{old: mixed, new: mixed}>
|
||||
*/
|
||||
public function getChangesAttribute(): \Illuminate\Support\Collection
|
||||
public function getChangesAttribute(): Collection
|
||||
{
|
||||
$old = $this->old_values;
|
||||
$new = $this->new_values;
|
||||
|
|
|
|||
14
src/Core/Activity/Models/CLAUDE.md
Normal file
14
src/Core/Activity/Models/CLAUDE.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Activity/Models/ — Activity Log Model
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Extends | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `Activity` | `Spatie\Activitylog\Models\Activity` | Extended activity model with workspace-aware scopes via the `ActivityScopes` trait. Adds query scopes for filtering by workspace, subject, causer, event type, date range, and search. |
|
||||
|
||||
Configure as the activity model in `config/activitylog.php`:
|
||||
```php
|
||||
'activity_model' => \Core\Activity\Models\Activity::class,
|
||||
```
|
||||
|
||||
Requires `spatie/laravel-activitylog`.
|
||||
9
src/Core/Activity/Scopes/CLAUDE.md
Normal file
9
src/Core/Activity/Scopes/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Activity/Scopes/ — Activity Query Scopes
|
||||
|
||||
## Traits
|
||||
|
||||
| Trait | Purpose |
|
||||
|-------|---------|
|
||||
| `ActivityScopes` | Comprehensive query scopes for activity log filtering. Includes: `forWorkspace`, `forSubject`, `forSubjectType`, `byCauser`, `byCauserId`, `ofType`, `createdEvents`, `updatedEvents`, `deletedEvents`, `betweenDates`, `today`, `lastDays`, `lastHours`, `search`, `inLog`, `withChanges`, `withExistingSubject`, `withDeletedSubject`, `newest`, `oldest`. |
|
||||
|
||||
Used by `Core\Activity\Models\Activity`. Workspace scoping checks both `properties->workspace_id` and subject model's `workspace_id`. Requires `spatie/laravel-activitylog`.
|
||||
11
src/Core/Activity/Services/CLAUDE.md
Normal file
11
src/Core/Activity/Services/CLAUDE.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Activity/Services/ — Activity Log Service
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| `ActivityLogService` | Fluent interface for querying and managing activity logs. Methods: `logFor($model)`, `logBy($user)`, `forWorkspace($workspace)`, `recent()`, `search($term)`. Chainable query builder with workspace awareness. |
|
||||
|
||||
Provides the business logic layer over Spatie's activity log. Used by the `ActivityFeed` Livewire component and available for injection throughout the application.
|
||||
|
||||
Requires `spatie/laravel-activitylog`.
|
||||
9
src/Core/Activity/View/Blade/admin/CLAUDE.md
Normal file
9
src/Core/Activity/View/Blade/admin/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Activity/View/Blade/admin/ — Activity Feed Blade Template
|
||||
|
||||
## Templates
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `activity-feed.blade.php` | Admin panel activity log display — paginated list with filters (user, model type, event type, date range), activity detail modal with full diff view, optional polling for real-time updates. |
|
||||
|
||||
Rendered by the `ActivityFeed` Livewire component via the `core.activity::admin.*` view namespace.
|
||||
|
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||
namespace Core\Activity\View\Modal\Admin;
|
||||
|
||||
use Core\Activity\Services\ActivityLogService;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Url;
|
||||
|
|
@ -362,7 +363,7 @@ class ActivityFeed extends Component
|
|||
};
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\View
|
||||
public function render(): View
|
||||
{
|
||||
return view('core.activity::admin.activity-feed');
|
||||
}
|
||||
|
|
|
|||
11
src/Core/Activity/View/Modal/Admin/CLAUDE.md
Normal file
11
src/Core/Activity/View/Modal/Admin/CLAUDE.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Activity/View/Modal/Admin/ — Activity Feed Livewire Component
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `ActivityFeed` | Livewire component for displaying activity logs in the admin panel. Paginated list with URL-bound filters (causer, subject type, event type, date range, search). Supports workspace scoping and optional polling for real-time updates. |
|
||||
|
||||
Usage: `<livewire:core.activity-feed />` or `<livewire:core.activity-feed :workspace-id="$workspace->id" poll="10s" />`
|
||||
|
||||
Requires `spatie/laravel-activitylog`.
|
||||
|
|
@ -14,6 +14,7 @@ namespace Core;
|
|||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
|
||||
/**
|
||||
* Application bootstrap - configures Laravel with Core framework patterns.
|
||||
|
|
@ -36,16 +37,16 @@ class Boot
|
|||
*/
|
||||
public static array $providers = [
|
||||
// Lifecycle events - must load first to wire lazy listeners
|
||||
\Core\LifecycleEventProvider::class,
|
||||
LifecycleEventProvider::class,
|
||||
|
||||
// Websites - domain-scoped, must wire before frontages fire events
|
||||
\Core\Website\Boot::class,
|
||||
Website\Boot::class,
|
||||
|
||||
// Core frontages - fire lifecycle events
|
||||
\Core\Front\Boot::class,
|
||||
Front\Boot::class,
|
||||
|
||||
// Base modules (from core-php package)
|
||||
\Core\Mod\Boot::class,
|
||||
Mod\Boot::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -58,7 +59,7 @@ class Boot
|
|||
->withMiddleware(function (Middleware $middleware): void {
|
||||
// Session middleware priority
|
||||
$middleware->priority([
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
StartSession::class,
|
||||
]);
|
||||
|
||||
$middleware->redirectGuestsTo('/login');
|
||||
|
|
|
|||
61
src/Core/Bouncer/CLAUDE.md
Normal file
61
src/Core/Bouncer/CLAUDE.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Bouncer
|
||||
|
||||
Early-exit security middleware + whitelist-based action authorisation gate.
|
||||
|
||||
## What It Does
|
||||
|
||||
Two subsystems in one:
|
||||
|
||||
1. **Bouncer** (top-level): IP blocklist + SEO redirects, runs before all other middleware
|
||||
2. **Gate** (subdirectory): Whitelist-based controller action authorisation with training mode
|
||||
|
||||
## Bouncer (IP Blocking + Redirects)
|
||||
|
||||
### Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `Boot` | ServiceProvider registering `BlocklistService`, `RedirectService`, and migrations |
|
||||
| `BouncerMiddleware` | Early-exit middleware: sets trusted proxies, checks blocklist (O(1) via cached set), handles SEO redirects, then passes through |
|
||||
| `BlocklistService` | IP blocking with Redis-cached lookup. Statuses: `pending` (honeypot, needs review), `approved` (active block), `rejected` (reviewed, not blocked). Methods: `isBlocked()`, `block()`, `unblock()`, `syncFromHoneypot()`, `approve()`, `reject()`, `getPending()`, `getStats()` |
|
||||
| `RedirectService` | Cached SEO redirects from `seo_redirects` table. Supports exact match and wildcard (`path/*`). Methods: `match()`, `add()`, `remove()` |
|
||||
|
||||
### Hidden Ideas
|
||||
|
||||
- Blocked IPs get `418 I'm a teapot` with `X-Powered-By: Earl Grey`
|
||||
- Honeypot monitors paths from `robots.txt` disallow list; critical paths (`/admin`, `/.env`, `/wp-admin`) trigger auto-block
|
||||
- Rate-limited honeypot logging prevents DoS via log flooding
|
||||
- `TRUSTED_PROXIES` env var: comma-separated IPs or `*` (trust all)
|
||||
|
||||
## Gate (Action Whitelist)
|
||||
|
||||
Philosophy: **"If it wasn't trained, it doesn't exist."**
|
||||
|
||||
### Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `Gate\Boot` | ServiceProvider registering middleware, migrations, route macros, and training routes |
|
||||
| `ActionGateService` | Resolves action name from route (3-level priority), checks against `ActionPermission` table, logs to `ActionRequest`. Methods: `check()`, `allow()`, `deny()`, `resolveAction()` |
|
||||
| `ActionGateMiddleware` | Enforces gate: allowed = pass, denied = 403, training = approval prompt (JSON for API, redirect for web) |
|
||||
| `Action` (attribute) | `#[Action('product.create', scope: 'product')]` on controller methods |
|
||||
| `ActionPermission` (model) | Whitelist record: action + guard + role + scope. Methods: `isAllowed()`, `train()`, `revoke()`, `allowedFor()` |
|
||||
| `ActionRequest` (model) | Audit log of all permission checks. Methods: `log()`, `pending()`, `deniedActionsSummary()`, `prune()` |
|
||||
| `RouteActionMacro` | Adds `->action('name')`, `->bypassGate()`, `->requiresTraining()` to Route |
|
||||
|
||||
### Action Resolution Priority
|
||||
|
||||
1. Route action: `Route::post(...)->action('product.create')`
|
||||
2. Controller attribute: `#[Action('product.create')]`
|
||||
3. Auto-resolved: `ProductController@store` becomes `product.store`
|
||||
|
||||
### Training Mode
|
||||
|
||||
When `core.bouncer.training_mode = true`, unknown actions prompt for approval instead of blocking. Training routes at `/_bouncer/approve` and `/_bouncer/pending`.
|
||||
|
||||
## Integration
|
||||
|
||||
- BouncerMiddleware runs FIRST in the stack (replaces Laravel TrustProxies)
|
||||
- ActionGateMiddleware appends to `web`, `admin`, `api`, `client` groups
|
||||
- Config: `core.bouncer.enabled`, `core.bouncer.training_mode`, `core.bouncer.guarded_middleware`
|
||||
- DB tables: `blocked_ips`, `seo_redirects`, `honeypot_hits`, `core_action_permissions`, `core_action_requests`
|
||||
7
src/Core/Bouncer/Database/Seeders/CLAUDE.md
Normal file
7
src/Core/Bouncer/Database/Seeders/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Bouncer/Database/Seeders/ — Bouncer Seeders
|
||||
|
||||
## Seeders
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `WebsiteRedirectSeeder.php` | Seeds 301 redirects for renamed website URLs. Uses the `RedirectService` to register old-to-new path mappings (e.g., `/services/biohost` -> `/services/bio`). Added during URL simplification (2026-01-16). |
|
||||
14
src/Core/Bouncer/Gate/Attributes/CLAUDE.md
Normal file
14
src/Core/Bouncer/Gate/Attributes/CLAUDE.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Bouncer/Gate/Attributes/ — Action Gate PHP Attributes
|
||||
|
||||
## Attributes
|
||||
|
||||
| Attribute | Target | Purpose |
|
||||
|-----------|--------|---------|
|
||||
| `#[Action(name, scope?)]` | Method, Class | Declares an explicit action name for permission checking, overriding auto-resolution from controller/method names. Optional `scope` for resource-specific permissions. |
|
||||
|
||||
Without this attribute, action names are auto-resolved: `ProductController@store` becomes `product.store`.
|
||||
|
||||
```php
|
||||
#[Action('product.create')]
|
||||
public function store(Request $request) { ... }
|
||||
```
|
||||
18
src/Core/Bouncer/Gate/CLAUDE.md
Normal file
18
src/Core/Bouncer/Gate/CLAUDE.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Bouncer/Gate/ — Action Gate Authorisation
|
||||
|
||||
Whitelist-based request authorisation system. Philosophy: "If it wasn't trained, it doesn't exist."
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Boot.php` | ServiceProvider — registers middleware, configures action gate. |
|
||||
| `ActionGateMiddleware.php` | Intercepts requests, checks if the target action is permitted. Production mode blocks unknown actions (403). Training mode prompts for approval. |
|
||||
| `ActionGateService.php` | Core service — resolves action names from routes/controllers, checks `ActionPermission` records. Supports `#[Action]` attribute, auto-resolution from controller names, and training mode. |
|
||||
| `RouteActionMacro.php` | Adds `->action('name')` and `->bypassGate()` macros to Laravel routes for fluent action naming. |
|
||||
|
||||
## Integration Flow
|
||||
|
||||
```
|
||||
Request -> ActionGateMiddleware -> ActionGateService::check() -> ActionPermission (allowed/denied) -> Controller
|
||||
```
|
||||
7
src/Core/Bouncer/Gate/Migrations/CLAUDE.md
Normal file
7
src/Core/Bouncer/Gate/Migrations/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Bouncer/Gate/Migrations/ — Action Gate Schema
|
||||
|
||||
## Migrations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `0001_01_01_000002_create_action_permission_tables.php` | Creates `core_action_permissions` (whitelisted actions) and `core_action_requests` (audit log) tables. |
|
||||
|
|
@ -11,6 +11,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Bouncer\Gate\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
|
@ -29,9 +31,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
* @property string $source How this was created ('trained', 'seeded', 'manual')
|
||||
* @property string|null $trained_route The route used during training
|
||||
* @property int|null $trained_by User ID who trained this action
|
||||
* @property \Carbon\Carbon|null $trained_at When training occurred
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property Carbon|null $trained_at When training occurred
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class ActionPermission extends Model
|
||||
{
|
||||
|
|
@ -174,9 +176,9 @@ class ActionPermission extends Model
|
|||
/**
|
||||
* Get all actions for a guard.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function forGuard(string $guard): \Illuminate\Database\Eloquent\Collection
|
||||
public static function forGuard(string $guard): Collection
|
||||
{
|
||||
return static::where('guard', $guard)->get();
|
||||
}
|
||||
|
|
@ -184,9 +186,9 @@ class ActionPermission extends Model
|
|||
/**
|
||||
* Get all allowed actions for a guard/role combination.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function allowedFor(string $guard, ?string $role = null): \Illuminate\Database\Eloquent\Collection
|
||||
public static function allowedFor(string $guard, ?string $role = null): Collection
|
||||
{
|
||||
$query = static::where('guard', $guard)
|
||||
->where('allowed', true);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Bouncer\Gate\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
|
@ -30,8 +32,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
* @property string|null $ip_address Client IP
|
||||
* @property string $status Result: 'allowed', 'denied', 'pending'
|
||||
* @property bool $was_trained Whether this request triggered training
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class ActionRequest extends Model
|
||||
{
|
||||
|
|
@ -103,9 +105,9 @@ class ActionRequest extends Model
|
|||
/**
|
||||
* Get pending requests (for training review).
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function pending(): \Illuminate\Database\Eloquent\Collection
|
||||
public static function pending(): Collection
|
||||
{
|
||||
return static::where('status', self::STATUS_PENDING)
|
||||
->orderBy('created_at', 'desc')
|
||||
|
|
@ -115,9 +117,9 @@ class ActionRequest extends Model
|
|||
/**
|
||||
* Get denied requests for an action.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function deniedFor(string $action): \Illuminate\Database\Eloquent\Collection
|
||||
public static function deniedFor(string $action): Collection
|
||||
{
|
||||
return static::where('action', $action)
|
||||
->where('status', self::STATUS_DENIED)
|
||||
|
|
@ -128,9 +130,9 @@ class ActionRequest extends Model
|
|||
/**
|
||||
* Get requests by user.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function forUser(int $userId): \Illuminate\Database\Eloquent\Collection
|
||||
public static function forUser(int $userId): Collection
|
||||
{
|
||||
return static::where('user_id', $userId)
|
||||
->orderBy('created_at', 'desc')
|
||||
|
|
|
|||
8
src/Core/Bouncer/Gate/Models/CLAUDE.md
Normal file
8
src/Core/Bouncer/Gate/Models/CLAUDE.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Bouncer/Gate/Models/ — Action Gate Models
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Table | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `ActionPermission` | `core_action_permissions` | Whitelisted action record. Stores action identifier, scope, guard, role, allowed flag, and training metadata (who trained it, when, from which route). Source: `trained`, `seeded`, or `manual`. |
|
||||
| `ActionRequest` | `core_action_requests` | Audit log entry for all action permission checks. Records HTTP method, route, action, guard, user, IP, status (allowed/denied/pending), and whether training was triggered. |
|
||||
|
|
@ -13,6 +13,7 @@ namespace Core\Bouncer\Gate\Tests\Feature;
|
|||
|
||||
use Core\Bouncer\Gate\ActionGateService;
|
||||
use Core\Bouncer\Gate\Attributes\Action;
|
||||
use Core\Bouncer\Gate\Boot;
|
||||
use Core\Bouncer\Gate\Models\ActionPermission;
|
||||
use Core\Bouncer\Gate\Models\ActionRequest;
|
||||
use Core\Bouncer\Gate\RouteActionMacro;
|
||||
|
|
@ -40,7 +41,7 @@ class ActionGateTest extends TestCase
|
|||
protected function getPackageProviders($app): array
|
||||
{
|
||||
return [
|
||||
\Core\Bouncer\Gate\Boot::class,
|
||||
Boot::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
7
src/Core/Bouncer/Gate/Tests/Feature/CLAUDE.md
Normal file
7
src/Core/Bouncer/Gate/Tests/Feature/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Bouncer/Gate/Tests/Feature/ — Action Gate Feature Tests
|
||||
|
||||
## Test Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ActionGateTest.php` | Integration tests for the full action gate flow — middleware interception, permission enforcement, training mode responses, route macro behaviour. |
|
||||
7
src/Core/Bouncer/Gate/Tests/Unit/CLAUDE.md
Normal file
7
src/Core/Bouncer/Gate/Tests/Unit/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Bouncer/Gate/Tests/Unit/ — Action Gate Unit Tests
|
||||
|
||||
## Test Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ActionGateServiceTest.php` | Unit tests for the `ActionGateService`. Tests action name resolution from routes and controllers, permission checking, training mode behaviour, and `#[Action]` attribute support. |
|
||||
9
src/Core/Bouncer/Migrations/CLAUDE.md
Normal file
9
src/Core/Bouncer/Migrations/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Bouncer/Migrations/ — Bouncer Schema Migrations
|
||||
|
||||
## Migrations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `0001_01_01_000001_create_bouncer_tables.php` | Creates core bouncer tables for IP/domain blocklisting, redirect rules, and rate limiting configuration. |
|
||||
|
||||
Uses early timestamps to run before application migrations.
|
||||
7
src/Core/Bouncer/Tests/Unit/CLAUDE.md
Normal file
7
src/Core/Bouncer/Tests/Unit/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Bouncer/Tests/Unit/ — Bouncer Unit Tests
|
||||
|
||||
## Test Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `BlocklistServiceTest.php` | Unit tests for the IP/domain blocklist service. Tests blocking, allowing, and checking IPs and domains against the blocklist. |
|
||||
83
src/Core/CLAUDE.md
Normal file
83
src/Core/CLAUDE.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Core Orchestration
|
||||
|
||||
Root-level files in `src/Core/` that wire the entire framework together. These are the bootstrap, module discovery, lazy loading, and pro-feature detection systems.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Init.php` | True entry point. `Core\Init::handle()` replaces Laravel's `bootstrap/app.php`. Runs WAF input filtering via `Input::capture()`, then delegates to `Boot::app()`. Prefers `App\Boot` if it exists. |
|
||||
| `Boot.php` | Configures Laravel `Application` with providers, middleware, and exceptions. Provider load order is critical: `LifecycleEventProvider` -> `Website\Boot` -> `Front\Boot` -> `Mod\Boot`. |
|
||||
| `LifecycleEventProvider.php` | The orchestrator. Registers `ModuleScanner` and `ModuleRegistry` as singletons, scans configured paths, wires lazy listeners. Static `fire*()` methods are called by frontage modules to dispatch lifecycle events and process collected requests (views, livewire, routes, middleware). |
|
||||
| `ModuleScanner.php` | Discovers `Boot.php` files in subdirectories of given paths. Reads static `$listens` arrays via reflection without instantiating modules. Maps paths to namespaces (`/Core` -> `Core\`, `/Mod` -> `Mod\`, `/Website` -> `Website\`, `/Plug` -> `Plug\`). |
|
||||
| `ModuleRegistry.php` | Coordinates scanner output into Laravel's event system. Sorts listeners by priority (highest first), creates `LazyModuleListener` instances, supports late-registration via `addPaths()`. |
|
||||
| `LazyModuleListener.php` | The lazy-loading wrapper. Instantiates module on first event fire (cached thereafter). ServiceProviders use `resolveProvider()`, plain classes use `make()`. Records audit logs and profiling data. |
|
||||
| `Pro.php` | Detects Flux Pro and FontAwesome Pro installations. Auto-enables pro features, falls back gracefully to free equivalents. Throws helpful dev-mode exceptions. |
|
||||
| `config.php` | Framework configuration: branding, domains, CDN, organisation, social links, contact, FontAwesome, pro fallback behaviour, icon defaults, debug settings, seeder auto-discovery. |
|
||||
|
||||
## Bootstrap Sequence
|
||||
|
||||
```
|
||||
public/index.php
|
||||
-> Core\Init::handle()
|
||||
-> Input::capture() # WAF layer sanitises $_GET/$_POST
|
||||
-> Boot::app() # Build Laravel Application
|
||||
-> LifecycleEventProvider # register(): scan + wire lazy listeners
|
||||
-> Website\Boot # register(): domain resolution
|
||||
-> Front\Boot # boot(): fires lifecycle events
|
||||
-> Mod\Boot # aggregates feature modules
|
||||
```
|
||||
|
||||
## Module Declaration Pattern
|
||||
|
||||
Modules declare interest in events via static `$listens`:
|
||||
|
||||
```php
|
||||
class Boot
|
||||
{
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => ['onAdmin', 10], // priority 10
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Modules are never instantiated until their event fires.
|
||||
|
||||
## Lifecycle Events (fire* methods)
|
||||
|
||||
| Method | Event | Middleware | Processes |
|
||||
|--------|-------|-----------|-----------|
|
||||
| `fireWebRoutes()` | `WebRoutesRegistering` | `web` | views, livewire, routes |
|
||||
| `fireAdminBooting()` | `AdminPanelBooting` | `admin` | views, translations, livewire, routes |
|
||||
| `fireClientRoutes()` | `ClientRoutesRegistering` | `client` | views, livewire, routes |
|
||||
| `fireApiRoutes()` | `ApiRoutesRegistering` | `api` | routes |
|
||||
| `fireMcpRoutes()` | `McpRoutesRegistering` | `mcp` | routes |
|
||||
| `fireMcpTools()` | `McpToolsRegistering` | -- | returns handler class names |
|
||||
| `fireConsoleBooting()` | `ConsoleBooting` | -- | artisan commands |
|
||||
| `fireQueueWorkerBooting()` | `QueueWorkerBooting` | -- | queue-specific init |
|
||||
|
||||
All route-registering fire methods call `refreshRoutes()` afterward to deduplicate names and refresh lookups.
|
||||
|
||||
## Default Scan Paths
|
||||
|
||||
- `app_path('Core')` -- application-level core modules
|
||||
- `app_path('Mod')` -- feature modules
|
||||
- `app_path('Website')` -- domain-scoped website modules
|
||||
- `src/Core` -- framework's own modules
|
||||
- `src/Mod` -- framework's own feature modules
|
||||
|
||||
Configurable via `config('core.module_paths')`.
|
||||
|
||||
## Priority System
|
||||
|
||||
- Default: `0`
|
||||
- Higher values run first: `['onAdmin', 100]` runs before `['onAdmin', 0]`
|
||||
- Negative values run last: `['onCleanup', -10]`
|
||||
|
||||
## Key Integration Points
|
||||
|
||||
- `Init::boot()` returns `App\Boot` if it exists, allowing apps to customise providers
|
||||
- `Boot::basePath()` auto-detects monorepo vs vendor structure
|
||||
- `LifecycleEventProvider` processes middleware aliases, view namespaces, and Livewire components collected during event dispatch
|
||||
- Route deduplication prevents `route:cache` failures when the same route file serves multiple domains
|
||||
|
|
@ -11,6 +11,10 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Cdn;
|
||||
|
||||
use App\Facades\Cdn;
|
||||
use App\Http\Middleware\RewriteOffloadedUrls;
|
||||
use App\Jobs\PushAssetToCdn;
|
||||
use App\Traits\HasCdnUrls;
|
||||
use Core\Cdn\Console\CdnPurge;
|
||||
use Core\Cdn\Console\OffloadMigrateCommand;
|
||||
use Core\Cdn\Console\PushAssetsToCdn;
|
||||
|
|
@ -21,6 +25,9 @@ use Core\Cdn\Services\BunnyStorageService;
|
|||
use Core\Cdn\Services\FluxCdnService;
|
||||
use Core\Cdn\Services\StorageOffload;
|
||||
use Core\Cdn\Services\StorageUrlResolver;
|
||||
use Core\Crypt\LthnHash;
|
||||
use Core\Plug\Cdn\CdnManager;
|
||||
use Core\Plug\Storage\StorageManager;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
/**
|
||||
|
|
@ -45,11 +52,11 @@ class Boot extends ServiceProvider
|
|||
$this->mergeConfigFrom(__DIR__.'/offload.php', 'offload');
|
||||
|
||||
// Register Plug managers as singletons (when available)
|
||||
if (class_exists(\Core\Plug\Cdn\CdnManager::class)) {
|
||||
$this->app->singleton(\Core\Plug\Cdn\CdnManager::class);
|
||||
if (class_exists(CdnManager::class)) {
|
||||
$this->app->singleton(CdnManager::class);
|
||||
}
|
||||
if (class_exists(\Core\Plug\Storage\StorageManager::class)) {
|
||||
$this->app->singleton(\Core\Plug\Storage\StorageManager::class);
|
||||
if (class_exists(StorageManager::class)) {
|
||||
$this->app->singleton(StorageManager::class);
|
||||
}
|
||||
|
||||
// Register legacy services as singletons (for backward compatibility)
|
||||
|
|
@ -115,32 +122,32 @@ class Boot extends ServiceProvider
|
|||
|
||||
// Crypt
|
||||
if (! class_exists(\App\Services\Crypt\LthnHash::class)) {
|
||||
class_alias(\Core\Crypt\LthnHash::class, \App\Services\Crypt\LthnHash::class);
|
||||
class_alias(LthnHash::class, \App\Services\Crypt\LthnHash::class);
|
||||
}
|
||||
|
||||
// Models
|
||||
if (! class_exists(\App\Models\StorageOffload::class)) {
|
||||
class_alias(\Core\Cdn\Models\StorageOffload::class, \App\Models\StorageOffload::class);
|
||||
class_alias(Models\StorageOffload::class, \App\Models\StorageOffload::class);
|
||||
}
|
||||
|
||||
// Facades
|
||||
if (! class_exists(\App\Facades\Cdn::class)) {
|
||||
class_alias(\Core\Cdn\Facades\Cdn::class, \App\Facades\Cdn::class);
|
||||
if (! class_exists(Cdn::class)) {
|
||||
class_alias(Facades\Cdn::class, Cdn::class);
|
||||
}
|
||||
|
||||
// Traits
|
||||
if (! trait_exists(\App\Traits\HasCdnUrls::class)) {
|
||||
class_alias(\Core\Cdn\Traits\HasCdnUrls::class, \App\Traits\HasCdnUrls::class);
|
||||
if (! trait_exists(HasCdnUrls::class)) {
|
||||
class_alias(Traits\HasCdnUrls::class, HasCdnUrls::class);
|
||||
}
|
||||
|
||||
// Middleware
|
||||
if (! class_exists(\App\Http\Middleware\RewriteOffloadedUrls::class)) {
|
||||
class_alias(\Core\Cdn\Middleware\RewriteOffloadedUrls::class, \App\Http\Middleware\RewriteOffloadedUrls::class);
|
||||
if (! class_exists(RewriteOffloadedUrls::class)) {
|
||||
class_alias(Middleware\RewriteOffloadedUrls::class, RewriteOffloadedUrls::class);
|
||||
}
|
||||
|
||||
// Jobs
|
||||
if (! class_exists(\App\Jobs\PushAssetToCdn::class)) {
|
||||
class_alias(\Core\Cdn\Jobs\PushAssetToCdn::class, \App\Jobs\PushAssetToCdn::class);
|
||||
if (! class_exists(PushAssetToCdn::class)) {
|
||||
class_alias(Jobs\PushAssetToCdn::class, PushAssetToCdn::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
src/Core/Cdn/CLAUDE.md
Normal file
57
src/Core/Cdn/CLAUDE.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Cdn
|
||||
|
||||
BunnyCDN integration with vBucket workspace isolation and storage offloading.
|
||||
|
||||
## What It Does
|
||||
|
||||
Unified CDN and object storage layer providing:
|
||||
- BunnyCDN pull zone operations (purge, stats)
|
||||
- BunnyCDN storage zone operations (upload, download, list, delete)
|
||||
- Context-aware URL building (CDN, origin, private, signed)
|
||||
- vBucket-scoped paths using `LthnHash` for tenant isolation
|
||||
- Asset pipeline for processing and offloading
|
||||
- Flux Pro CDN delivery
|
||||
- Storage offload migration from local to CDN
|
||||
|
||||
## Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `Boot` | ServiceProvider registering all services as singletons + backward-compat aliases to `App\` namespaces |
|
||||
| `BunnyCdnService` | Pull zone API: `purgeUrl()`, `purgeUrls()`, `purgeAll()`, `purgeByTag()`, `purgeWorkspace()`, `getStats()`, `getBandwidth()`, `listStorageFiles()`, `uploadFile()`, `deleteFile()`. Sanitises error messages to redact API keys |
|
||||
| `BunnyStorageService` | Direct storage zone operations (separate from pull zone API) |
|
||||
| `CdnUrlBuilder` | URL construction: `cdn()`, `origin()`, `private()`, `apex()`, `signed()`, `vBucket()`, `vBucketId()`, `vBucketPath()`, `asset()`, `withVersion()`, `urls()`, `allUrls()` |
|
||||
| `StorageUrlResolver` | Context-aware URL resolution |
|
||||
| `FluxCdnService` | Flux Pro component CDN delivery |
|
||||
| `AssetPipeline` | Asset processing pipeline |
|
||||
| `StorageOffload` (service) | Migrates files from local storage to CDN |
|
||||
| `StorageOffload` (model) | Tracks offloaded files in DB |
|
||||
| `Cdn` (facade) | `Cdn::purge(...)` etc. |
|
||||
| `HasCdnUrls` (trait) | Adds CDN URL methods to Eloquent models |
|
||||
|
||||
## Console Commands
|
||||
|
||||
- `cdn:purge` -- Purge CDN cache
|
||||
- `cdn:push-assets` -- Push assets to CDN storage
|
||||
- `cdn:push-flux` -- Push Flux Pro assets to CDN
|
||||
- `cdn:offload-migrate` -- Migrate local files to CDN storage
|
||||
|
||||
## Middleware
|
||||
|
||||
- `RewriteOffloadedUrls` -- Rewrites storage URLs in responses to CDN URLs
|
||||
- `LocalCdnMiddleware` -- Serves CDN assets locally in development
|
||||
|
||||
## vBucket Pattern
|
||||
|
||||
Workspace-isolated CDN paths using `LthnHash::vBucketId()`:
|
||||
```
|
||||
cdn.example.com/{vBucketId}/path/to/asset.js
|
||||
```
|
||||
The vBucketId is a deterministic SHA-256 hash of the domain name, ensuring each workspace's assets are namespaced.
|
||||
|
||||
## Integration
|
||||
|
||||
- Reads credentials from `ConfigService` (DB-backed config), not just `.env`
|
||||
- Signed URLs use HMAC-SHA256 with BunnyCDN token authentication
|
||||
- Config files: `config.php` (CDN settings), `offload.php` (storage offload settings)
|
||||
- Backward-compat aliases registered for all `App\Services\*` and `App\Models\*` namespaces
|
||||
10
src/Core/Cdn/Console/CLAUDE.md
Normal file
10
src/Core/Cdn/Console/CLAUDE.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Cdn/Console/ — CDN Artisan Commands
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Signature | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `CdnPurge` | `cdn:purge` | Purge CDN cache — by URL, tag, workspace, or global. |
|
||||
| `OffloadMigrateCommand` | `cdn:offload-migrate` | Migrate local files to remote storage, creating offload records. |
|
||||
| `PushAssetsToCdn` | `cdn:push` | Push local assets to CDN storage zone. |
|
||||
| `PushFluxToCdn` | `cdn:push-flux` | Push Flux UI framework assets to CDN. |
|
||||
|
|
@ -11,6 +11,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Cdn\Console;
|
||||
|
||||
use Core\Plug\Cdn\Bunny\Purge;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CdnPurge extends Command
|
||||
|
|
@ -43,8 +45,8 @@ class CdnPurge extends Command
|
|||
{
|
||||
parent::__construct();
|
||||
|
||||
if (class_exists(\Core\Plug\Cdn\Bunny\Purge::class)) {
|
||||
$this->purger = new \Core\Plug\Cdn\Bunny\Purge;
|
||||
if (class_exists(Purge::class)) {
|
||||
$this->purger = new Purge;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,8 +98,8 @@ class CdnPurge extends Command
|
|||
// Purge by workspace
|
||||
if (empty($workspaceArg)) {
|
||||
$workspaceOptions = ['all', 'Select specific URLs'];
|
||||
if (class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
$workspaceOptions = array_merge($workspaceOptions, \Core\Tenant\Models\Workspace::pluck('slug')->toArray());
|
||||
if (class_exists(Workspace::class)) {
|
||||
$workspaceOptions = array_merge($workspaceOptions, Workspace::pluck('slug')->toArray());
|
||||
}
|
||||
$workspaceArg = $this->choice(
|
||||
'What would you like to purge?',
|
||||
|
|
@ -218,13 +220,13 @@ class CdnPurge extends Command
|
|||
|
||||
protected function purgeAllWorkspaces(bool $dryRun): int
|
||||
{
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->error('Workspace purge requires Tenant module to be installed.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$workspaces = \Core\Tenant\Models\Workspace::all();
|
||||
$workspaces = Workspace::all();
|
||||
|
||||
if ($workspaces->isEmpty()) {
|
||||
$this->error('No workspaces found');
|
||||
|
|
@ -276,19 +278,19 @@ class CdnPurge extends Command
|
|||
|
||||
protected function purgeWorkspace(string $slug, bool $dryRun): int
|
||||
{
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->error('Workspace purge requires Tenant module to be installed.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$workspace = \Core\Tenant\Models\Workspace::where('slug', $slug)->first();
|
||||
$workspace = Workspace::where('slug', $slug)->first();
|
||||
|
||||
if (! $workspace) {
|
||||
$this->error("Workspace not found: {$slug}");
|
||||
$this->newLine();
|
||||
$this->info('Available workspaces:');
|
||||
\Core\Tenant\Models\Workspace::pluck('slug')->each(fn ($s) => $this->line(" - {$s}"));
|
||||
Workspace::pluck('slug')->each(fn ($s) => $this->line(" - {$s}"));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Core\Cdn\Console;
|
|||
|
||||
use Core\Cdn\Services\FluxCdnService;
|
||||
use Core\Cdn\Services\StorageUrlResolver;
|
||||
use Core\Plug\Storage\Bunny\VBucket;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ class PushAssetsToCdn extends Command
|
|||
|
||||
public function handle(FluxCdnService $flux, StorageUrlResolver $cdn): int
|
||||
{
|
||||
if (! class_exists(\Core\Plug\Storage\Bunny\VBucket::class)) {
|
||||
if (! class_exists(VBucket::class)) {
|
||||
$this->error('Push assets to CDN requires Core\Plug\Storage\Bunny\VBucket class. Plug module not installed.');
|
||||
|
||||
return self::FAILURE;
|
||||
|
|
@ -54,7 +55,7 @@ class PushAssetsToCdn extends Command
|
|||
|
||||
// Create vBucket for workspace isolation
|
||||
$domain = $this->option('domain');
|
||||
$this->vbucket = \Core\Plug\Storage\Bunny\VBucket::public($domain);
|
||||
$this->vbucket = VBucket::public($domain);
|
||||
|
||||
$pushFlux = $this->option('flux');
|
||||
$pushFontawesome = $this->option('fontawesome');
|
||||
|
|
|
|||
9
src/Core/Cdn/Facades/CLAUDE.md
Normal file
9
src/Core/Cdn/Facades/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Cdn/Facades/ — CDN Facade
|
||||
|
||||
## Facades
|
||||
|
||||
| Facade | Resolves To | Purpose |
|
||||
|--------|-------------|---------|
|
||||
| `Cdn` | `StorageUrlResolver` | Static proxy for CDN operations — `cdn()`, `origin()`, `private()`, `signedUrl()`, `asset()`, `pushToCdn()`, `deleteFromCdn()`, `purge()`, `storePublic()`, `storePrivate()`, `vBucketCdn()`, and more. |
|
||||
|
||||
Usage: `Cdn::cdn('images/logo.png')` returns the CDN URL for the asset.
|
||||
|
|
@ -42,7 +42,7 @@ use Illuminate\Support\Facades\Facade;
|
|||
* @method static string vBucketPath(string $domain, string $path)
|
||||
* @method static array vBucketUrls(string $domain, string $path)
|
||||
*
|
||||
* @see \Core\Cdn\Services\StorageUrlResolver
|
||||
* @see StorageUrlResolver
|
||||
*/
|
||||
class Cdn extends Facade
|
||||
{
|
||||
|
|
|
|||
7
src/Core/Cdn/Jobs/CLAUDE.md
Normal file
7
src/Core/Cdn/Jobs/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Cdn/Jobs/ — CDN Background Jobs
|
||||
|
||||
## Jobs
|
||||
|
||||
| Job | Purpose |
|
||||
|-----|---------|
|
||||
| `PushAssetToCdn` | Queued job to push a local asset file to the CDN (BunnyCDN). Handles upload, verification, and StorageOffload record creation. |
|
||||
|
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Cdn\Jobs;
|
||||
|
||||
use Core\Plug\Storage\StorageManager;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
|
@ -60,7 +61,7 @@ class PushAssetToCdn implements ShouldQueue
|
|||
*/
|
||||
public function handle(?object $storage = null): void
|
||||
{
|
||||
if (! class_exists(\Core\Plug\Storage\StorageManager::class)) {
|
||||
if (! class_exists(StorageManager::class)) {
|
||||
Log::warning('PushAssetToCdn: StorageManager not available, Plug module not installed');
|
||||
|
||||
return;
|
||||
|
|
@ -68,7 +69,7 @@ class PushAssetToCdn implements ShouldQueue
|
|||
|
||||
// Resolve from container if not injected
|
||||
if ($storage === null) {
|
||||
$storage = app(\Core\Plug\Storage\StorageManager::class);
|
||||
$storage = app(StorageManager::class);
|
||||
}
|
||||
|
||||
if (! config('cdn.bunny.push_enabled', false)) {
|
||||
|
|
|
|||
8
src/Core/Cdn/Middleware/CLAUDE.md
Normal file
8
src/Core/Cdn/Middleware/CLAUDE.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Cdn/Middleware/ — CDN HTTP Middleware
|
||||
|
||||
## Middleware
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `LocalCdnMiddleware` | Adds aggressive caching headers and compression for requests on the `cdn.*` subdomain. Provides CDN-like behaviour without external services. |
|
||||
| `RewriteOffloadedUrls` | Processes JSON responses and replaces local storage paths with remote equivalents when files have been offloaded to external storage. |
|
||||
7
src/Core/Cdn/Models/CLAUDE.md
Normal file
7
src/Core/Cdn/Models/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Cdn/Models/ — CDN Storage Models
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Purpose |
|
||||
|-------|---------|
|
||||
| `StorageOffload` | Tracks files offloaded to remote storage. Records local path, remote path, disk, SHA-256 hash, file size, MIME type, category, metadata, and offload timestamp. Used by the URL rewriting middleware. |
|
||||
|
|
@ -13,6 +13,7 @@ namespace Core\Cdn\Models;
|
|||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* Tracks files that have been offloaded to remote storage.
|
||||
|
|
@ -26,9 +27,9 @@ use Illuminate\Database\Eloquent\Model;
|
|||
* @property string|null $mime_type MIME type
|
||||
* @property string|null $category Category for path prefixing
|
||||
* @property array|null $metadata Additional metadata
|
||||
* @property \Illuminate\Support\Carbon|null $offloaded_at When file was offloaded
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property Carbon|null $offloaded_at When file was offloaded
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*/
|
||||
class StorageOffload extends Model
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Core\Cdn\Services;
|
|||
|
||||
use Core\Cdn\Jobs\PushAssetToCdn;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
|
|
@ -339,7 +340,7 @@ class AssetPipeline
|
|||
PushAssetToCdn::dispatch($disk, $path, $zone);
|
||||
} elseif ($this->storage !== null) {
|
||||
// Synchronous push if no queue configured (requires StorageManager from Plug module)
|
||||
$diskInstance = \Illuminate\Support\Facades\Storage::disk($disk);
|
||||
$diskInstance = Storage::disk($disk);
|
||||
if ($diskInstance->exists($path)) {
|
||||
$contents = $diskInstance->get($path);
|
||||
$this->storage->zone($zone)->upload()->contents($path, $contents);
|
||||
|
|
|
|||
13
src/Core/Cdn/Services/CLAUDE.md
Normal file
13
src/Core/Cdn/Services/CLAUDE.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Cdn/Services/ — CDN Service Layer
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| `BunnyCdnService` | BunnyCDN pull zone API — cache purging (URL, tag, workspace, global), statistics retrieval, pull zone management. Uses config from `ConfigService`. |
|
||||
| `BunnyStorageService` | BunnyCDN storage zone API — file upload, download, delete, list. Supports public and private storage zones. |
|
||||
| `StorageOffload` (service) | Manages file offloading to remote storage — upload, track, verify. Creates `StorageOffload` model records. |
|
||||
| `StorageUrlResolver` | URL builder for all asset contexts — CDN, origin, private, signed, apex. Supports virtual buckets (vBucket) per domain. Backs the `Cdn` facade. |
|
||||
| `CdnUrlBuilder` | Low-level URL construction for CDN paths with cache-busting and domain resolution. |
|
||||
| `AssetPipeline` | Orchestrates asset processing — push to CDN, cache headers, versioning. |
|
||||
| `FluxCdnService` | Pushes Flux UI assets to CDN for faster component loading. |
|
||||
|
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||
namespace Core\Cdn\Services;
|
||||
|
||||
use Core\Helpers\Cdn;
|
||||
use Flux\AssetManager;
|
||||
use Flux\Flux;
|
||||
|
||||
/**
|
||||
|
|
@ -83,7 +84,7 @@ class FluxCdnService
|
|||
|
||||
// Use CDN when enabled (respects CDN_FORCE_LOCAL for testing)
|
||||
if (! $this->shouldUseCdn()) {
|
||||
return \Flux\AssetManager::editorScripts();
|
||||
return AssetManager::editorScripts();
|
||||
}
|
||||
|
||||
// In production, use CDN URL (no vBucket - shared platform asset)
|
||||
|
|
@ -109,7 +110,7 @@ class FluxCdnService
|
|||
|
||||
// Use CDN when enabled (respects CDN_FORCE_LOCAL for testing)
|
||||
if (! $this->shouldUseCdn()) {
|
||||
return \Flux\AssetManager::editorStyles();
|
||||
return AssetManager::editorStyles();
|
||||
}
|
||||
|
||||
// In production, use CDN URL (no vBucket - shared platform asset)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Core\Cdn\Services;
|
|||
|
||||
use Core\Cdn\Models\StorageOffload as OffloadModel;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -305,7 +306,7 @@ class StorageOffload
|
|||
/**
|
||||
* Get all offloaded files for a category.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<OffloadModel>
|
||||
* @return Collection<OffloadModel>
|
||||
*/
|
||||
public function getByCategory(string $category)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ declare(strict_types=1);
|
|||
namespace Core\Cdn\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Cdn\Jobs\PushAssetToCdn;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
|
@ -372,7 +374,7 @@ class StorageUrlResolver
|
|||
/**
|
||||
* Get the public storage disk.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Filesystem\Filesystem
|
||||
* @return Filesystem
|
||||
*/
|
||||
public function publicDisk()
|
||||
{
|
||||
|
|
@ -382,7 +384,7 @@ class StorageUrlResolver
|
|||
/**
|
||||
* Get the private storage disk.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Filesystem\Filesystem
|
||||
* @return Filesystem
|
||||
*/
|
||||
public function privateDisk()
|
||||
{
|
||||
|
|
@ -403,7 +405,7 @@ class StorageUrlResolver
|
|||
if ($stored && $pushToCdn && config('cdn.pipeline.auto_push', true)) {
|
||||
// Queue the push if configured, otherwise push synchronously
|
||||
if ($queue = config('cdn.pipeline.queue')) {
|
||||
dispatch(new \Core\Cdn\Jobs\PushAssetToCdn('hetzner-public', $path, 'public'))->onQueue($queue);
|
||||
dispatch(new PushAssetToCdn('hetzner-public', $path, 'public'))->onQueue($queue);
|
||||
} else {
|
||||
$this->pushToCdn('hetzner-public', $path, 'public');
|
||||
}
|
||||
|
|
@ -425,7 +427,7 @@ class StorageUrlResolver
|
|||
|
||||
if ($stored && $pushToCdn && config('cdn.pipeline.auto_push', true)) {
|
||||
if ($queue = config('cdn.pipeline.queue')) {
|
||||
dispatch(new \Core\Cdn\Jobs\PushAssetToCdn('hetzner-private', $path, 'private'))->onQueue($queue);
|
||||
dispatch(new PushAssetToCdn('hetzner-private', $path, 'private'))->onQueue($queue);
|
||||
} else {
|
||||
$this->pushToCdn('hetzner-private', $path, 'private');
|
||||
}
|
||||
|
|
|
|||
7
src/Core/Cdn/Traits/CLAUDE.md
Normal file
7
src/Core/Cdn/Traits/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Cdn/Traits/ — CDN Model Traits
|
||||
|
||||
## Traits
|
||||
|
||||
| Trait | Purpose |
|
||||
|-------|---------|
|
||||
| `HasCdnUrls` | For models with asset paths needing CDN URL resolution. Requires `$cdnPathAttribute` (attribute with storage path) and optional `$cdnBucket` (`public` or `private`). Provides `cdnUrl()` accessor. |
|
||||
70
src/Core/Config/CLAUDE.md
Normal file
70
src/Core/Config/CLAUDE.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Config
|
||||
|
||||
Database-backed configuration with scoping, versioning, profiles, and admin UI.
|
||||
|
||||
## What It Does
|
||||
|
||||
Replaces/supplements Laravel's file-based config with a DB-backed system supporting:
|
||||
- Hierarchical scope resolution (global -> workspace -> user)
|
||||
- Configuration profiles (sets of values that can be switched)
|
||||
- Version history with diffs
|
||||
- Sensitive value encryption
|
||||
- Import/export (JSON/YAML)
|
||||
- Livewire admin panels
|
||||
- Event-driven invalidation
|
||||
|
||||
## Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `Boot` | Listens to `AdminPanelBooting` and `ConsoleBooting` for registration |
|
||||
| `ConfigService` | Primary API: `get()`, `set()`, `isConfigured()`, plus scope-aware resolution |
|
||||
| `ConfigResolver` | Resolves values through scope hierarchy: user -> workspace -> global -> default |
|
||||
| `ConfigResult` | DTO wrapping resolved value with metadata (source scope, profile, etc.) |
|
||||
| `ConfigVersioning` | Tracks changes with diffs between versions |
|
||||
| `VersionDiff` | Computes and formats diffs between config versions |
|
||||
| `ConfigExporter` | Export/import config as JSON/YAML |
|
||||
| `ImportResult` | DTO for import operation results |
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Table | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `ConfigKey` | `config_keys` | Key definitions with type, default, validation rules, `is_sensitive` flag |
|
||||
| `ConfigValue` | `config_values` | Actual values scoped by type (global/workspace/user) |
|
||||
| `ConfigProfile` | `config_profiles` | Named sets of config values (soft-deletable) |
|
||||
| `ConfigVersion` | `config_versions` | Version history snapshots |
|
||||
| `ConfigResolved` | -- | Value object for resolved config |
|
||||
| `Channel` | -- | Notification channel config |
|
||||
|
||||
## Enums
|
||||
|
||||
- `ConfigType` -- Value types (string, int, bool, json, etc.)
|
||||
- `ScopeType` -- Resolution scopes (global, workspace, user)
|
||||
|
||||
## Events
|
||||
|
||||
- `ConfigChanged` -- Fired when any config value changes
|
||||
- `ConfigInvalidated` -- Fired when cache needs clearing
|
||||
- `ConfigLocked` -- Fired when a config key is locked
|
||||
|
||||
## Console Commands
|
||||
|
||||
- `config:prime` -- Pre-populate config cache
|
||||
- `config:list` -- List all config keys and values
|
||||
- `config:version` -- Show version history
|
||||
- `config:import` -- Import config from file
|
||||
- `config:export` -- Export config to file
|
||||
|
||||
## Admin UI
|
||||
|
||||
- `ConfigPanel` (Livewire) -- General config editing panel
|
||||
- `WorkspaceConfig` (Livewire) -- Workspace-specific config panel
|
||||
- Routes registered under admin prefix
|
||||
|
||||
## Integration
|
||||
|
||||
- `ConfigService` is used by other subsystems (e.g., `BunnyCdnService` reads CDN credentials via `$this->config->get('cdn.bunny.api_key')`)
|
||||
- Sensitive keys (`is_sensitive = true`) are encrypted at rest
|
||||
- Seeder: `ConfigKeySeeder` populates default keys
|
||||
- 4 migrations covering base tables, soft deletes, versions, and sensitive flag
|
||||
13
src/Core/Config/Console/CLAUDE.md
Normal file
13
src/Core/Config/Console/CLAUDE.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Config/Console/ — Config Artisan Commands
|
||||
|
||||
Artisan commands for managing the hierarchical configuration system.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Signature | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `ConfigListCommand` | `config:list` | List config keys with resolved values. Filters by workspace, category, or configured-only. |
|
||||
| `ConfigPrimeCommand` | `config:prime` | Materialise resolved config into the fast-read table. Primes system and/or specific workspace. |
|
||||
| `ConfigExportCommand` | `config:export` | Export config to JSON or YAML file. Supports workspace scope and sensitive value inclusion. |
|
||||
| `ConfigImportCommand` | `config:import` | Import config from JSON or YAML file. Supports dry-run mode and automatic version snapshots. |
|
||||
| `ConfigVersionCommand` | `config:version` | Manage config versions — list, create snapshots, show, rollback, and compare versions. |
|
||||
|
|
@ -12,8 +12,11 @@ declare(strict_types=1);
|
|||
namespace Core\Config\Console;
|
||||
|
||||
use Core\Config\ConfigExporter;
|
||||
use Core\Config\Models\ConfigKey;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Export config to JSON or YAML file.
|
||||
|
|
@ -45,13 +48,13 @@ class ConfigExportCommand extends Command
|
|||
// Resolve workspace
|
||||
$workspace = null;
|
||||
if ($workspaceSlug) {
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->components->error('Tenant module not installed. Cannot export workspace config.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$workspace = \Core\Tenant\Models\Workspace::where('slug', $workspaceSlug)->first();
|
||||
$workspace = Workspace::where('slug', $workspaceSlug)->first();
|
||||
|
||||
if (! $workspace) {
|
||||
$this->components->error("Workspace not found: {$workspaceSlug}");
|
||||
|
|
@ -96,16 +99,16 @@ class ConfigExportCommand extends Command
|
|||
/**
|
||||
* Get autocompletion suggestions.
|
||||
*/
|
||||
public function complete(CompletionInput $input, \Symfony\Component\Console\Completion\CompletionSuggestions $suggestions): void
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestOptionValuesFor('workspace')) {
|
||||
if (class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
$suggestions->suggestValues(\Core\Tenant\Models\Workspace::pluck('slug')->toArray());
|
||||
if (class_exists(Workspace::class)) {
|
||||
$suggestions->suggestValues(Workspace::pluck('slug')->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('category')) {
|
||||
$suggestions->suggestValues(\Core\Config\Models\ConfigKey::distinct()->pluck('category')->toArray());
|
||||
$suggestions->suggestValues(ConfigKey::distinct()->pluck('category')->toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ namespace Core\Config\Console;
|
|||
|
||||
use Core\Config\ConfigExporter;
|
||||
use Core\Config\ConfigVersioning;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Import config from JSON or YAML file.
|
||||
|
|
@ -53,13 +55,13 @@ class ConfigImportCommand extends Command
|
|||
// Resolve workspace
|
||||
$workspace = null;
|
||||
if ($workspaceSlug) {
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->components->error('Tenant module not installed. Cannot import workspace config.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$workspace = \Core\Tenant\Models\Workspace::where('slug', $workspaceSlug)->first();
|
||||
$workspace = Workspace::where('slug', $workspaceSlug)->first();
|
||||
|
||||
if (! $workspace) {
|
||||
$this->components->error("Workspace not found: {$workspaceSlug}");
|
||||
|
|
@ -174,11 +176,11 @@ class ConfigImportCommand extends Command
|
|||
/**
|
||||
* Get autocompletion suggestions.
|
||||
*/
|
||||
public function complete(CompletionInput $input, \Symfony\Component\Console\Completion\CompletionSuggestions $suggestions): void
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestOptionValuesFor('workspace')) {
|
||||
if (class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
$suggestions->suggestValues(\Core\Tenant\Models\Workspace::pluck('slug')->toArray());
|
||||
if (class_exists(Workspace::class)) {
|
||||
$suggestions->suggestValues(Workspace::pluck('slug')->toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Core\Config\Console;
|
|||
|
||||
use Core\Config\ConfigService;
|
||||
use Core\Config\Models\ConfigKey;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ConfigListCommand extends Command
|
||||
|
|
@ -33,13 +34,13 @@ class ConfigListCommand extends Command
|
|||
$workspace = null;
|
||||
|
||||
if ($workspaceSlug) {
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->error('Tenant module not installed. Cannot filter by workspace.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$workspace = \Core\Tenant\Models\Workspace::where('slug', $workspaceSlug)->first();
|
||||
$workspace = Workspace::where('slug', $workspaceSlug)->first();
|
||||
|
||||
if (! $workspace) {
|
||||
$this->error("Workspace not found: {$workspaceSlug}");
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||
namespace Core\Config\Console;
|
||||
|
||||
use Core\Config\ConfigService;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ConfigPrimeCommand extends Command
|
||||
|
|
@ -36,13 +37,13 @@ class ConfigPrimeCommand extends Command
|
|||
}
|
||||
|
||||
if ($workspaceSlug) {
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->error('Tenant module not installed. Cannot prime workspace config.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$workspace = \Core\Tenant\Models\Workspace::where('slug', $workspaceSlug)->first();
|
||||
$workspace = Workspace::where('slug', $workspaceSlug)->first();
|
||||
|
||||
if (! $workspace) {
|
||||
$this->error("Workspace not found: {$workspaceSlug}");
|
||||
|
|
@ -59,7 +60,7 @@ class ConfigPrimeCommand extends Command
|
|||
|
||||
$this->info('Priming config cache for all workspaces...');
|
||||
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->warn('Tenant module not installed. Only priming system config.');
|
||||
$config->prime(null);
|
||||
$this->info('System config cached.');
|
||||
|
|
@ -67,7 +68,7 @@ class ConfigPrimeCommand extends Command
|
|||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->withProgressBar(\Core\Tenant\Models\Workspace::all(), function ($workspace) use ($config) {
|
||||
$this->withProgressBar(Workspace::all(), function ($workspace) use ($config) {
|
||||
$config->prime($workspace);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ namespace Core\Config\Console;
|
|||
|
||||
use Core\Config\ConfigVersioning;
|
||||
use Core\Config\Models\ConfigVersion;
|
||||
use Core\Config\VersionDiff;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Manage config versions.
|
||||
|
|
@ -50,13 +53,13 @@ class ConfigVersionCommand extends Command
|
|||
// Resolve workspace
|
||||
$workspace = null;
|
||||
if ($workspaceSlug) {
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
if (! class_exists(Workspace::class)) {
|
||||
$this->components->error('Tenant module not installed. Cannot manage workspace versions.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$workspace = \Core\Tenant\Models\Workspace::where('slug', $workspaceSlug)->first();
|
||||
$workspace = Workspace::where('slug', $workspaceSlug)->first();
|
||||
|
||||
if (! $workspace) {
|
||||
$this->components->error("Workspace not found: {$workspaceSlug}");
|
||||
|
|
@ -282,7 +285,7 @@ class ConfigVersionCommand extends Command
|
|||
/**
|
||||
* Display a diff.
|
||||
*/
|
||||
protected function displayDiff(\Core\Config\VersionDiff $diff): void
|
||||
protected function displayDiff(VersionDiff $diff): void
|
||||
{
|
||||
$this->components->info("Summary: {$diff->getSummary()}");
|
||||
$this->newLine();
|
||||
|
|
@ -402,15 +405,15 @@ class ConfigVersionCommand extends Command
|
|||
/**
|
||||
* Get autocompletion suggestions.
|
||||
*/
|
||||
public function complete(CompletionInput $input, \Symfony\Component\Console\Completion\CompletionSuggestions $suggestions): void
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestArgumentValuesFor('action')) {
|
||||
$suggestions->suggestValues(['list', 'create', 'show', 'rollback', 'compare', 'diff', 'delete']);
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('workspace')) {
|
||||
if (class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
$suggestions->suggestValues(\Core\Tenant\Models\Workspace::pluck('slug')->toArray());
|
||||
if (class_exists(Workspace::class)) {
|
||||
$suggestions->suggestValues(Workspace::pluck('slug')->toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
src/Core/Config/Contracts/CLAUDE.md
Normal file
9
src/Core/Config/Contracts/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Config/Contracts/ — Config System Interfaces
|
||||
|
||||
## Interfaces
|
||||
|
||||
| Interface | Purpose |
|
||||
|-----------|---------|
|
||||
| `ConfigProvider` | Virtual configuration provider. Supplies config values at runtime without database storage. Matched against key patterns (e.g., `bio.*`). Registered via `ConfigResolver::registerProvider()`. |
|
||||
|
||||
Providers implement `pattern()` (wildcard key matching) and `resolve()` (returns the value for a given key, workspace, and channel context). Returns `null` to fall through to database resolution.
|
||||
|
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Config\Contracts;
|
||||
|
||||
use Core\Config\ConfigResolver;
|
||||
use Core\Config\Models\Channel;
|
||||
|
||||
/**
|
||||
|
|
@ -72,7 +73,7 @@ use Core\Config\Models\Channel;
|
|||
* ```
|
||||
*
|
||||
*
|
||||
* @see \Core\Config\ConfigResolver::registerProvider()
|
||||
* @see ConfigResolver::registerProvider()
|
||||
*/
|
||||
interface ConfigProvider
|
||||
{
|
||||
|
|
|
|||
9
src/Core/Config/Database/Seeders/CLAUDE.md
Normal file
9
src/Core/Config/Database/Seeders/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Config/Database/Seeders/ — Config Key Seeders
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ConfigKeySeeder.php` | Seeds known configuration keys into the `config_keys` table. Defines CDN (Bunny), storage (Hetzner S3), social, analytics, and bio settings with their types, categories, and defaults. Uses `firstOrCreate` for idempotency. |
|
||||
|
||||
Part of the Config subsystem's M1 layer (key definitions). These keys are then assigned values via `ConfigValue` at system or workspace scope.
|
||||
12
src/Core/Config/Enums/CLAUDE.md
Normal file
12
src/Core/Config/Enums/CLAUDE.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Config/Enums/ — Config Type System
|
||||
|
||||
Backed enums for the configuration system's type safety and scope hierarchy.
|
||||
|
||||
## Enums
|
||||
|
||||
| Enum | Values | Purpose |
|
||||
|------|--------|---------|
|
||||
| `ConfigType` | STRING, BOOL, INT, FLOAT, ARRAY, JSON | Determines how config values are cast and validated. Has `cast()` and `default()` methods. |
|
||||
| `ScopeType` | SYSTEM, ORG, WORKSPACE | Defines the inheritance hierarchy. Resolution order: workspace (priority 20) > org (10) > system (0). |
|
||||
|
||||
`ScopeType::resolutionOrder()` returns scopes from most specific to least specific for cascade resolution.
|
||||
13
src/Core/Config/Events/CLAUDE.md
Normal file
13
src/Core/Config/Events/CLAUDE.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Config/Events/ — Config System Events
|
||||
|
||||
Events dispatched by the configuration system for reactive integration.
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Fired When | Key Properties |
|
||||
|-------|-----------|----------------|
|
||||
| `ConfigChanged` | A config value is set or updated via `ConfigService::set()` | `keyCode`, `value`, `previousValue`, `profile`, `channelId` |
|
||||
| `ConfigInvalidated` | Config cache is manually cleared | `keyCode` (null = all), `workspaceId`, `channelId`. Has `isFull()` and `affectsKey()` helpers. |
|
||||
| `ConfigLocked` | A config value is locked (FINAL) | `keyCode`, `profile`, `channelId` |
|
||||
|
||||
Modules can listen to these events via the standard `$listens` pattern in their Boot class to react to config changes (e.g., refreshing CDN clients, flushing caches).
|
||||
14
src/Core/Config/Migrations/CLAUDE.md
Normal file
14
src/Core/Config/Migrations/CLAUDE.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Config/Migrations/ — Config Schema Migrations
|
||||
|
||||
Database migrations for the hierarchical configuration system.
|
||||
|
||||
## Migrations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `0001_01_01_000001_create_config_tables.php` | Creates core config tables: `config_keys`, `config_profiles`, `config_values`, `config_channels`, `config_resolved`. |
|
||||
| `0001_01_01_000002_add_soft_deletes_to_config_profiles.php` | Adds soft delete support to `config_profiles`. |
|
||||
| `0001_01_01_000003_add_is_sensitive_to_config_keys.php` | Adds `is_sensitive` flag for automatic encryption of values. |
|
||||
| `0001_01_01_000004_create_config_versions_table.php` | Creates `config_versions` table for point-in-time snapshots and rollback. |
|
||||
|
||||
Uses early timestamps (`0001_01_01_*`) to run before application migrations.
|
||||
22
src/Core/Config/Models/CLAUDE.md
Normal file
22
src/Core/Config/Models/CLAUDE.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Config/Models/ — Config Eloquent Models
|
||||
|
||||
Eloquent models implementing the four-layer hierarchical configuration system.
|
||||
|
||||
## Models
|
||||
|
||||
| Model | Table | Purpose |
|
||||
|-------|-------|---------|
|
||||
| `ConfigKey` | `config_keys` | M1 layer — defines what keys exist. Dot-notation codes, typed (`ConfigType`), categorised. Supports sensitive flag for auto-encryption. Hierarchical parent/child grouping. |
|
||||
| `ConfigProfile` | `config_profiles` | M2 layer — groups values at a scope level (system/org/workspace). Inherits from parent profiles. Soft-deletable. |
|
||||
| `ConfigValue` | `config_values` | Junction table linking profiles to keys with actual values. `locked` flag implements FINAL (prevents child override). Auto-encrypts sensitive keys. Invalidates resolver hash on write. |
|
||||
| `ConfigVersion` | `config_versions` | Point-in-time snapshots for version history and rollback. Immutable (no `updated_at`). Stores JSON snapshot of all values. |
|
||||
| `Channel` | `config_channels` | Context dimension (web, api, mobile, instagram, etc.). Hierarchical inheritance chain with cycle detection. System or workspace-scoped. |
|
||||
| `ConfigResolved` | `config_resolved` | Materialised READ table — all lookups hit this directly. No computation at read time. Populated by the `prime` operation. Composite key (workspace_id, channel_id, key_code). |
|
||||
|
||||
## Resolution Flow
|
||||
|
||||
```
|
||||
ConfigService::get() → ConfigResolved (fast lookup)
|
||||
→ miss: ConfigResolver computes from ConfigValue chain
|
||||
→ stores result back to ConfigResolved + in-memory hash
|
||||
```
|
||||
|
|
@ -11,6 +11,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Config\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
|
@ -35,8 +37,8 @@ use Illuminate\Support\Facades\Log;
|
|||
* @property int|null $parent_id
|
||||
* @property int|null $workspace_id
|
||||
* @property array|null $metadata
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class Channel extends Model
|
||||
{
|
||||
|
|
@ -77,8 +79,8 @@ class Channel extends Model
|
|||
*/
|
||||
public function workspace(): BelongsTo
|
||||
{
|
||||
if (class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
return $this->belongsTo(\Core\Tenant\Models\Workspace::class);
|
||||
if (class_exists(Workspace::class)) {
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
// Return a null relationship when Tenant module is not installed
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Config\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Config\Enums\ConfigType;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
|
@ -30,8 +32,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||
* @property string|null $description
|
||||
* @property mixed $default_value
|
||||
* @property bool $is_sensitive
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class ConfigKey extends Model
|
||||
{
|
||||
|
|
@ -108,9 +110,9 @@ class ConfigKey extends Model
|
|||
/**
|
||||
* Get all keys for a category.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function forCategory(string $category): \Illuminate\Database\Eloquent\Collection
|
||||
public static function forCategory(string $category): Collection
|
||||
{
|
||||
return static::where('category', $category)->get();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Config\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Config\Enums\ScopeType;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
|
@ -29,9 +31,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property int|null $scope_id
|
||||
* @property int|null $parent_profile_id
|
||||
* @property int $priority
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property \Carbon\Carbon|null $deleted_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon|null $deleted_at
|
||||
*/
|
||||
class ConfigProfile extends Model
|
||||
{
|
||||
|
|
@ -90,9 +92,9 @@ class ConfigProfile extends Model
|
|||
/**
|
||||
* Get profiles for a scope.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function forScope(ScopeType $type, ?int $scopeId = null): \Illuminate\Database\Eloquent\Collection
|
||||
public static function forScope(ScopeType $type, ?int $scopeId = null): Collection
|
||||
{
|
||||
return static::where('scope_type', $type)
|
||||
->where('scope_id', $scopeId)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Config\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Config\ConfigResult;
|
||||
use Core\Config\Enums\ConfigType;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
|
@ -36,7 +39,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
* @property int|null $source_profile_id
|
||||
* @property int|null $source_channel_id
|
||||
* @property bool $virtual
|
||||
* @property \Carbon\Carbon $computed_at
|
||||
* @property Carbon $computed_at
|
||||
*/
|
||||
class ConfigResolved extends Model
|
||||
{
|
||||
|
|
@ -71,8 +74,8 @@ class ConfigResolved extends Model
|
|||
*/
|
||||
public function workspace(): BelongsTo
|
||||
{
|
||||
if (class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
return $this->belongsTo(\Core\Tenant\Models\Workspace::class);
|
||||
if (class_exists(Workspace::class)) {
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
// Return a null relationship when Tenant module is not installed
|
||||
|
|
@ -155,9 +158,9 @@ class ConfigResolved extends Model
|
|||
/**
|
||||
* Get all resolved config for a scope.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function forScope(?int $workspaceId = null, ?int $channelId = null): \Illuminate\Database\Eloquent\Collection
|
||||
public static function forScope(?int $workspaceId = null, ?int $channelId = null): Collection
|
||||
{
|
||||
return static::where('workspace_id', $workspaceId)
|
||||
->where('channel_id', $channelId)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Config\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Config\ConfigResolver;
|
||||
use Core\Config\Enums\ScopeType;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
|
@ -31,8 +34,8 @@ use Illuminate\Support\Facades\Crypt;
|
|||
* @property mixed $value
|
||||
* @property bool $locked
|
||||
* @property int|null $inherited_from
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class ConfigValue extends Model
|
||||
{
|
||||
|
|
@ -76,7 +79,7 @@ class ConfigValue extends Model
|
|||
$encrypted = substr($decoded, strlen(self::ENCRYPTED_PREFIX));
|
||||
|
||||
return json_decode(Crypt::decryptString($encrypted), true);
|
||||
} catch (\Illuminate\Contracts\Encryption\DecryptException) {
|
||||
} catch (DecryptException) {
|
||||
// Return null if decryption fails (key rotation, corruption, etc.)
|
||||
return null;
|
||||
}
|
||||
|
|
@ -255,9 +258,9 @@ class ConfigValue extends Model
|
|||
*
|
||||
* @param array<int> $profileIds
|
||||
* @param array<int>|null $channelIds Include null for "all channels" values
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function forKeyInProfiles(int $keyId, array $profileIds, ?array $channelIds = null): \Illuminate\Database\Eloquent\Collection
|
||||
public static function forKeyInProfiles(int $keyId, array $profileIds, ?array $channelIds = null): Collection
|
||||
{
|
||||
return static::where('key_id', $keyId)
|
||||
->whereIn('profile_id', $profileIds)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Config\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
|
@ -26,7 +29,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
* @property string $label
|
||||
* @property string $snapshot
|
||||
* @property string|null $author
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property Carbon $created_at
|
||||
*/
|
||||
class ConfigVersion extends Model
|
||||
{
|
||||
|
|
@ -65,8 +68,8 @@ class ConfigVersion extends Model
|
|||
*/
|
||||
public function workspace(): BelongsTo
|
||||
{
|
||||
if (class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
return $this->belongsTo(\Core\Tenant\Models\Workspace::class);
|
||||
if (class_exists(Workspace::class)) {
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
// Return a null relationship when Tenant module is not installed
|
||||
|
|
@ -136,9 +139,9 @@ class ConfigVersion extends Model
|
|||
* Get versions for a scope.
|
||||
*
|
||||
* @param int|null $workspaceId Workspace ID or null for system
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function forScope(?int $workspaceId = null): \Illuminate\Database\Eloquent\Collection
|
||||
public static function forScope(?int $workspaceId = null): Collection
|
||||
{
|
||||
return static::where('workspace_id', $workspaceId)
|
||||
->orderByDesc('created_at')
|
||||
|
|
@ -160,9 +163,9 @@ class ConfigVersion extends Model
|
|||
/**
|
||||
* Get versions created by a specific author.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function byAuthor(string $author): \Illuminate\Database\Eloquent\Collection
|
||||
public static function byAuthor(string $author): Collection
|
||||
{
|
||||
return static::where('author', $author)
|
||||
->orderByDesc('created_at')
|
||||
|
|
@ -172,11 +175,11 @@ class ConfigVersion extends Model
|
|||
/**
|
||||
* Get versions created within a date range.
|
||||
*
|
||||
* @param \Carbon\Carbon $from Start date
|
||||
* @param \Carbon\Carbon $to End date
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
* @param Carbon $from Start date
|
||||
* @param Carbon $to End date
|
||||
* @return Collection<int, self>
|
||||
*/
|
||||
public static function inDateRange(\Carbon\Carbon $from, \Carbon\Carbon $to): \Illuminate\Database\Eloquent\Collection
|
||||
public static function inDateRange(Carbon $from, Carbon $to): Collection
|
||||
{
|
||||
return static::whereBetween('created_at', [$from, $to])
|
||||
->orderByDesc('created_at')
|
||||
|
|
|
|||
7
src/Core/Config/Routes/CLAUDE.md
Normal file
7
src/Core/Config/Routes/CLAUDE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Config/Routes/ — Config Admin Routes
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `admin.php` | Admin route definitions for the configuration panel. Registers routes under the `admin` middleware group for the `ConfigPanel` and `WorkspaceConfig` Livewire components. |
|
||||
11
src/Core/Config/Tests/Feature/CLAUDE.md
Normal file
11
src/Core/Config/Tests/Feature/CLAUDE.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Config/Tests/Feature/ — Config Integration Tests
|
||||
|
||||
Pest feature tests for the hierarchical configuration system.
|
||||
|
||||
## Test Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ConfigServiceTest.php` | Full integration tests covering ConfigKey creation, ConfigProfile inheritance, ConfigResolver scope cascading, FINAL lock enforcement, ConfigService materialised reads/writes, ConfigResolved storage, and the single-hash lazy-load pattern. |
|
||||
|
||||
Tests cover the complete config lifecycle: key definition, profile hierarchy (system/workspace), value resolution with inheritance, lock semantics, cache invalidation, and the prime/materialise flow.
|
||||
|
|
@ -19,8 +19,9 @@ use Core\Config\Models\ConfigProfile;
|
|||
use Core\Config\Models\ConfigResolved;
|
||||
use Core\Config\Models\ConfigValue;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Clear hash for clean test state
|
||||
|
|
|
|||
12
src/Core/Config/View/Blade/admin/CLAUDE.md
Normal file
12
src/Core/Config/View/Blade/admin/CLAUDE.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Config/View/Blade/admin/ — Config Admin Blade Templates
|
||||
|
||||
Blade templates for the admin configuration panel.
|
||||
|
||||
## Templates
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `config-panel.blade.php` | Full config management panel — browse keys by category, edit values, toggle locks, manage system vs workspace scopes. Used by `ConfigPanel` Livewire component. |
|
||||
| `workspace-config.blade.php` | Workspace-specific config panel — hierarchical namespace navigation, tab grouping, value editing with system inheritance display. Used by `WorkspaceConfig` Livewire component. |
|
||||
|
||||
Both templates use the `hub::admin.layouts.app` layout and are rendered via the `core.config::admin.*` view namespace.
|
||||
12
src/Core/Config/View/Modal/Admin/CLAUDE.md
Normal file
12
src/Core/Config/View/Modal/Admin/CLAUDE.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Config/View/Modal/Admin/ — Config Admin Livewire Components
|
||||
|
||||
Livewire components for the admin configuration interface.
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `ConfigPanel` | Hades-only config management. Browse/search keys by category, edit values inline, toggle FINAL locks, manage system and workspace scopes. Respects parent lock enforcement. |
|
||||
| `WorkspaceConfig` | Workspace-scoped settings. Hierarchical namespace navigation (cdn/bunny/storage), tab grouping by second-level prefix, value editing with inherited value display, system lock indicators. |
|
||||
|
||||
Both require the Tenant module for workspace support and fall back gracefully without it. `ConfigPanel` requires Hades (super-admin) access. Values are persisted via `ConfigService`.
|
||||
|
|
@ -15,6 +15,9 @@ use Core\Config\ConfigService;
|
|||
use Core\Config\Models\ConfigKey;
|
||||
use Core\Config\Models\ConfigProfile;
|
||||
use Core\Config\Models\ConfigValue;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
|
|
@ -24,7 +27,7 @@ use Livewire\Component;
|
|||
*
|
||||
* @property-read ConfigProfile $activeProfile
|
||||
* @property-read array<string> $categories
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection $workspaces
|
||||
* @property-read Collection $workspaces
|
||||
*/
|
||||
class ConfigPanel extends Component
|
||||
{
|
||||
|
|
@ -79,17 +82,17 @@ class ConfigPanel extends Component
|
|||
* Get all workspaces (requires Tenant module).
|
||||
*/
|
||||
#[Computed]
|
||||
public function workspaces(): \Illuminate\Database\Eloquent\Collection
|
||||
public function workspaces(): Collection
|
||||
{
|
||||
if (! class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
return new \Illuminate\Database\Eloquent\Collection;
|
||||
if (! class_exists(Workspace::class)) {
|
||||
return new Collection;
|
||||
}
|
||||
|
||||
return \Core\Tenant\Models\Workspace::orderBy('name')->get();
|
||||
return Workspace::orderBy('name')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function keys(): \Illuminate\Database\Eloquent\Collection
|
||||
public function keys(): Collection
|
||||
{
|
||||
return ConfigKey::query()
|
||||
->when($this->category, fn ($q) => $q->where('category', $this->category))
|
||||
|
|
@ -119,8 +122,8 @@ class ConfigPanel extends Component
|
|||
#[Computed]
|
||||
public function selectedWorkspace(): ?object
|
||||
{
|
||||
if ($this->workspaceId && class_exists(\Core\Tenant\Models\Workspace::class)) {
|
||||
return \Core\Tenant\Models\Workspace::find($this->workspaceId);
|
||||
if ($this->workspaceId && class_exists(Workspace::class)) {
|
||||
return Workspace::find($this->workspaceId);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -271,7 +274,7 @@ class ConfigPanel extends Component
|
|||
$this->dispatch('config-cleared');
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\View
|
||||
public function render(): View
|
||||
{
|
||||
return view('core.config::admin.config-panel')
|
||||
->layout('hub::admin.layouts.app', ['title' => 'Configuration']);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ use Core\Config\ConfigService;
|
|||
use Core\Config\Models\ConfigKey;
|
||||
use Core\Config\Models\ConfigProfile;
|
||||
use Core\Config\Models\ConfigValue;
|
||||
use Core\Tenant\Services\WorkspaceService;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
|
@ -28,7 +31,7 @@ use Livewire\Component;
|
|||
* @property-read ConfigProfile $systemProfile
|
||||
* @property-read object|null $workspace
|
||||
* @property-read string $prefix
|
||||
* @property-read array<int, array{namespace: string, label: string, keys: \Illuminate\Support\Collection}> $tabs
|
||||
* @property-read array<int, array{namespace: string, label: string, keys: Collection}> $tabs
|
||||
*/
|
||||
class WorkspaceConfig extends Component
|
||||
{
|
||||
|
|
@ -46,8 +49,8 @@ class WorkspaceConfig extends Component
|
|||
$this->config = $config;
|
||||
|
||||
// Try to resolve WorkspaceService if Tenant module is installed
|
||||
if (class_exists(\Core\Tenant\Services\WorkspaceService::class)) {
|
||||
$this->workspaceService = app(\Core\Tenant\Services\WorkspaceService::class);
|
||||
if (class_exists(WorkspaceService::class)) {
|
||||
$this->workspaceService = app(WorkspaceService::class);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +280,7 @@ class WorkspaceConfig extends Component
|
|||
$this->dispatch('config-cleared');
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\View
|
||||
public function render(): View
|
||||
{
|
||||
return view('core.config::admin.workspace-config')
|
||||
->layout('hub::admin.layouts.app', ['title' => 'Settings']);
|
||||
|
|
|
|||
32
src/Core/Console/CLAUDE.md
Normal file
32
src/Core/Console/CLAUDE.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Core\Console
|
||||
|
||||
Framework artisan commands registered via the `ConsoleBooting` lifecycle event.
|
||||
|
||||
## Boot
|
||||
|
||||
Uses the event-driven module loading pattern:
|
||||
|
||||
```php
|
||||
public static array $listens = [
|
||||
ConsoleBooting::class => 'onConsole',
|
||||
];
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Signature | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `InstallCommand` | `core:install` | Framework setup wizard: env file, app config, migrations, app key, storage link. Supports `--dry-run` and `--force` |
|
||||
| `NewProjectCommand` | `core:new` | Scaffold a new project |
|
||||
| `MakeModCommand` | `make:mod {name}` | Generate a module in the `Mod` namespace with Boot.php. Flags: `--web`, `--admin`, `--api`, `--console`, `--all` |
|
||||
| `MakePlugCommand` | `make:plug` | Generate a plugin scaffold |
|
||||
| `MakeWebsiteCommand` | `make:website` | Generate a Website module scaffold |
|
||||
| `PruneEmailShieldStatsCommand` | prunes `email_shield_stats` | Cleans old EmailShield validation stats |
|
||||
| `ScheduleSyncCommand` | schedule sync | Schedule synchronisation |
|
||||
|
||||
## Conventions
|
||||
|
||||
- All commands use `declare(strict_types=1)` and the `Core\Console\Commands` namespace.
|
||||
- `MakeModCommand` generates a complete module scaffold with optional handler stubs (web routes, admin panel, API, console).
|
||||
- `InstallCommand` tracks progress via named installation steps and supports dry-run mode.
|
||||
- Commands are registered via `$event->command()` on the `ConsoleBooting` event, not via a service provider's `$this->commands()`.
|
||||
15
src/Core/Console/Commands/CLAUDE.md
Normal file
15
src/Core/Console/Commands/CLAUDE.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Console/Commands/ — Core Framework Commands
|
||||
|
||||
Artisan commands for framework scaffolding and maintenance.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Signature | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `InstallCommand` | `core:install` | Framework installation wizard — sets up sensible defaults for new projects. |
|
||||
| `MakeModCommand` | `core:make-mod` | Generates a new module scaffold in the `Mod` namespace with Boot.php event-driven loading pattern. |
|
||||
| `MakePlugCommand` | `core:make-plug` | Generates a new plugin scaffold in the `Plug` namespace. |
|
||||
| `MakeWebsiteCommand` | `core:make-website` | Generates a new website module scaffold in the `Website` namespace. |
|
||||
| `NewProjectCommand` | `core:new` | Creates a complete new project from the Core PHP template. |
|
||||
| `PruneEmailShieldStatsCommand` | `emailshield:prune` | Prunes old EmailShield validation statistics. |
|
||||
| `ScheduleSyncCommand` | `core:schedule-sync` | Synchronises scheduled tasks across the application. |
|
||||
|
|
@ -13,6 +13,8 @@ namespace Core\Console\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Core PHP Framework Installation Command.
|
||||
|
|
@ -484,8 +486,8 @@ class InstallCommand extends Command
|
|||
* but implements the method for consistency with other commands.
|
||||
*/
|
||||
public function complete(
|
||||
\Symfony\Component\Console\Completion\CompletionInput $input,
|
||||
\Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
|
||||
CompletionInput $input,
|
||||
CompletionSuggestions $suggestions
|
||||
): void {
|
||||
// No argument/option values need completion for this command
|
||||
// All options are flags (--force, --no-interaction, --dry-run)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ namespace Core\Console\Commands;
|
|||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Generate a new module scaffold.
|
||||
|
|
@ -508,8 +510,8 @@ BLADE;
|
|||
* Get shell completion suggestions for arguments.
|
||||
*/
|
||||
public function complete(
|
||||
\Symfony\Component\Console\Completion\CompletionInput $input,
|
||||
\Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
|
||||
CompletionInput $input,
|
||||
CompletionSuggestions $suggestions
|
||||
): void {
|
||||
if ($input->mustSuggestArgumentValuesFor('name')) {
|
||||
// Suggest common module naming patterns
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ namespace Core\Console\Commands;
|
|||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Generate a new Plug provider scaffold.
|
||||
|
|
@ -604,8 +606,8 @@ PHP;
|
|||
* Get shell completion suggestions for arguments and options.
|
||||
*/
|
||||
public function complete(
|
||||
\Symfony\Component\Console\Completion\CompletionInput $input,
|
||||
\Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
|
||||
CompletionInput $input,
|
||||
CompletionSuggestions $suggestions
|
||||
): void {
|
||||
if ($input->mustSuggestArgumentValuesFor('name')) {
|
||||
// Suggest common social platform names
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ namespace Core\Console\Commands;
|
|||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Generate a new Website scaffold.
|
||||
|
|
@ -574,8 +576,8 @@ BLADE;
|
|||
* Get shell completion suggestions for arguments and options.
|
||||
*/
|
||||
public function complete(
|
||||
\Symfony\Component\Console\Completion\CompletionInput $input,
|
||||
\Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
|
||||
CompletionInput $input,
|
||||
CompletionSuggestions $suggestions
|
||||
): void {
|
||||
if ($input->mustSuggestArgumentValuesFor('name')) {
|
||||
// Suggest common website naming patterns
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ use Illuminate\Console\Command;
|
|||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Create a new Core PHP Framework project.
|
||||
|
|
@ -343,8 +345,8 @@ class NewProjectCommand extends Command
|
|||
* Get shell completion suggestions.
|
||||
*/
|
||||
public function complete(
|
||||
\Symfony\Component\Console\Completion\CompletionInput $input,
|
||||
\Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
|
||||
CompletionInput $input,
|
||||
CompletionSuggestions $suggestions
|
||||
): void {
|
||||
if ($input->mustSuggestArgumentValuesFor('name')) {
|
||||
// Suggest common project naming patterns
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ namespace Core\Console\Commands;
|
|||
|
||||
use Core\Mail\EmailShieldStat;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
||||
/**
|
||||
* Prune old Email Shield statistics records.
|
||||
|
|
@ -135,8 +137,8 @@ class PruneEmailShieldStatsCommand extends Command
|
|||
* Get shell completion suggestions for options.
|
||||
*/
|
||||
public function complete(
|
||||
\Symfony\Component\Console\Completion\CompletionInput $input,
|
||||
\Symfony\Component\Console\Completion\CompletionSuggestions $suggestions
|
||||
CompletionInput $input,
|
||||
CompletionSuggestions $suggestions
|
||||
): void {
|
||||
if ($input->mustSuggestOptionValuesFor('days')) {
|
||||
// Suggest common retention periods
|
||||
|
|
|
|||
65
src/Core/Crypt/CLAUDE.md
Normal file
65
src/Core/Crypt/CLAUDE.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Crypt
|
||||
|
||||
Encryption utilities: encrypted Eloquent casts and LTHN QuasiHash identifier generator.
|
||||
|
||||
## What It Does
|
||||
|
||||
Two independent tools:
|
||||
|
||||
1. **EncryptArrayObject** -- Eloquent cast that encrypts/decrypts array data transparently using Laravel's `Crypt` facade
|
||||
2. **LthnHash** -- Deterministic identifier generator for workspace scoping, vBucket CDN paths, and consistent sharding
|
||||
|
||||
## Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `EncryptArrayObject` | `CastsAttributes` implementation. Encrypts arrays as JSON+AES on write, decrypts on read. Fails gracefully (returns null + logs warning) |
|
||||
| `LthnHash` | Static utility: `hash()`, `shortHash()`, `fastHash()`, `vBucketId()`, `toInt()`, `verify()`, `benchmark()`. Supports key rotation |
|
||||
|
||||
## EncryptArrayObject Usage
|
||||
|
||||
```php
|
||||
class ApiCredential extends Model {
|
||||
protected $casts = ['secrets' => EncryptArrayObject::class];
|
||||
}
|
||||
$model->secrets['api_key'] = 'sk_live_xxx'; // encrypted in DB
|
||||
```
|
||||
|
||||
## LthnHash API
|
||||
|
||||
| Method | Output | Use Case |
|
||||
|--------|--------|----------|
|
||||
| `hash($input)` | 64 hex chars (SHA-256) | Default, high quality |
|
||||
| `shortHash($input, $len)` | 16-32 hex chars | Space-constrained IDs |
|
||||
| `fastHash($input)` | 8-16 hex chars (xxHash/CRC32) | High-throughput |
|
||||
| `vBucketId($domain)` | 64 hex chars | CDN path isolation |
|
||||
| `toInt($input, $max)` | int (60 bits) | Sharding/partitioning |
|
||||
| `verify($input, $hash)` | bool | Constant-time comparison, tries all key maps |
|
||||
| `benchmark($iterations)` | timing array | Performance measurement |
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. Reverse input, apply character substitution map (key map)
|
||||
2. Concatenate original + substituted string
|
||||
3. Hash with SHA-256 (or xxHash/CRC32 for `fastHash`)
|
||||
|
||||
## Key Rotation
|
||||
|
||||
```php
|
||||
LthnHash::addKeyMap('v2', $newMap, setActive: true);
|
||||
// New hashes use v2, verify() tries v2 first then falls back to older maps
|
||||
LthnHash::removeKeyMap('v1'); // after migration
|
||||
```
|
||||
|
||||
## NOT For
|
||||
|
||||
- Password hashing (use `password_hash()`)
|
||||
- Security tokens (use `random_bytes()`)
|
||||
- Cryptographic signatures
|
||||
|
||||
## Integration
|
||||
|
||||
- `CdnUrlBuilder::vBucketId()` delegates to `LthnHash::vBucketId()`
|
||||
- `verify()` uses `hash_equals()` for timing-attack resistance
|
||||
- `fastHash()` auto-selects xxh64 (PHP 8.1+) or CRC32b+CRC32c fallback
|
||||
- `toInt()` uses GMP for safe large-integer modular arithmetic
|
||||
|
|
@ -12,7 +12,9 @@ declare(strict_types=1);
|
|||
namespace Core\Crypt;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Database\Eloquent\Casts\ArrayObject;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
|
|
@ -27,7 +29,7 @@ class EncryptArrayObject implements CastsAttributes
|
|||
/**
|
||||
* Cast the given value to an ArrayObject.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param Model $model
|
||||
* @param mixed $value
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
|
|
@ -36,7 +38,7 @@ class EncryptArrayObject implements CastsAttributes
|
|||
if (isset($attributes[$key])) {
|
||||
try {
|
||||
$decrypted = Crypt::decryptString($attributes[$key]);
|
||||
} catch (\Illuminate\Contracts\Encryption\DecryptException $e) {
|
||||
} catch (DecryptException $e) {
|
||||
Log::warning('Failed to decrypt array object', ['key' => $key, 'error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
|
|
@ -59,7 +61,7 @@ class EncryptArrayObject implements CastsAttributes
|
|||
/**
|
||||
* Prepare the given value for storage.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param Model $model
|
||||
* @param mixed $value
|
||||
* @param array<string, mixed> $attributes
|
||||
* @return array<string, string>|null
|
||||
|
|
|
|||
27
src/Core/Database/Seeders/Attributes/CLAUDE.md
Normal file
27
src/Core/Database/Seeders/Attributes/CLAUDE.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Database/Seeders/Attributes/ — Seeder Ordering Attributes
|
||||
|
||||
PHP 8 attributes for controlling seeder execution order in the auto-discovery system.
|
||||
|
||||
## Attributes
|
||||
|
||||
| Attribute | Target | Purpose |
|
||||
|-----------|--------|---------|
|
||||
| `#[SeederAfter(...)]` | Class | This seeder must run after the specified seeders. Repeatable. |
|
||||
| `#[SeederBefore(...)]` | Class | This seeder must run before the specified seeders. Repeatable. |
|
||||
| `#[SeederPriority(n)]` | Class | Numeric priority (lower runs first, default 50). |
|
||||
|
||||
## Priority Guidelines
|
||||
|
||||
- 0-20: Foundation (features, configuration)
|
||||
- 20-40: Core data (packages, workspaces)
|
||||
- 40-60: Default (general seeders)
|
||||
- 60-80: Content (pages, posts)
|
||||
- 80-100: Demo/test data
|
||||
|
||||
## Example
|
||||
|
||||
```php
|
||||
#[SeederAfter(FeatureSeeder::class)]
|
||||
#[SeederPriority(30)]
|
||||
class PackageSeeder extends Seeder { ... }
|
||||
```
|
||||
68
src/Core/Database/Seeders/CLAUDE.md
Normal file
68
src/Core/Database/Seeders/CLAUDE.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Database/Seeders
|
||||
|
||||
Auto-discovering, dependency-aware seeder orchestration.
|
||||
|
||||
## What It Does
|
||||
|
||||
Replaces Laravel's manual seeder ordering with automatic discovery and topological sorting. Seeders declare their dependencies via attributes or properties, and the framework figures out the correct execution order using Kahn's algorithm.
|
||||
|
||||
## Key Classes
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `CoreDatabaseSeeder` | Base seeder class. Extends Laravel's `Seeder`. Auto-discovers seeders from configured paths, applies `--exclude` and `--only` filters, runs them in dependency order |
|
||||
| `SeederDiscovery` | Scans directories for `*Seeder.php` files, reads priority/dependency metadata from attributes or properties, produces topologically sorted list |
|
||||
| `SeederRegistry` | Manual registration alternative: `register(Class, priority: 10, after: [...])`. Fluent API with `registerMany()`, `merge()`, `getOrdered()` |
|
||||
|
||||
## Attributes
|
||||
|
||||
| Attribute | Target | Purpose |
|
||||
|-----------|--------|---------|
|
||||
| `#[SeederPriority(10)]` | Class | Lower values run first (default: 50) |
|
||||
| `#[SeederAfter(FeatureSeeder::class)]` | Class | Must run after specified seeders (repeatable) |
|
||||
| `#[SeederBefore(PackageSeeder::class)]` | Class | Must run before specified seeders (repeatable) |
|
||||
|
||||
## Priority Guidelines
|
||||
|
||||
- 0-20: Foundation (features, configuration)
|
||||
- 20-40: Core data (packages, workspaces)
|
||||
- 40-60: Default (general seeders)
|
||||
- 60-80: Content (pages, posts)
|
||||
- 80-100: Demo/test data
|
||||
|
||||
## Ordering Rules
|
||||
|
||||
Dependencies take precedence over priority. Within the same dependency level, lower priority numbers run first. Circular dependencies throw `CircularDependencyException` with the full cycle path.
|
||||
|
||||
## Usage
|
||||
|
||||
```php
|
||||
// Auto-discovery (default)
|
||||
class DatabaseSeeder extends CoreDatabaseSeeder {
|
||||
protected function getSeederPaths(): array {
|
||||
return [app_path('Core'), app_path('Mod')];
|
||||
}
|
||||
}
|
||||
|
||||
// Manual registration
|
||||
class DatabaseSeeder extends CoreDatabaseSeeder {
|
||||
protected bool $autoDiscover = false;
|
||||
protected function registerSeeders(SeederRegistry $registry): void {
|
||||
$registry->register(FeatureSeeder::class, priority: 10)
|
||||
->register(PackageSeeder::class, after: [FeatureSeeder::class]);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI filtering
|
||||
php artisan db:seed --exclude=DemoSeeder --only=FeatureSeeder
|
||||
```
|
||||
|
||||
## Discovery Paths
|
||||
|
||||
Scans `{path}/*/Database/Seeders/*Seeder.php` (module subdirs) and `{path}/Database/Seeders/*Seeder.php` (direct). Configured via `core.seeders.paths` or defaults to `app/Core`, `app/Mod`, `app/Website`.
|
||||
|
||||
## Integration
|
||||
|
||||
- Properties alternative to attributes: `public int $priority = 10;`, `public array $after = [...]`, `public array $before = [...]`
|
||||
- Pattern matching for `--exclude`/`--only`: full class name, short name, or partial match
|
||||
- Config: `core.seeders.auto_discover`, `core.seeders.paths`, `core.seeders.exclude`
|
||||
9
src/Core/Database/Seeders/Exceptions/CLAUDE.md
Normal file
9
src/Core/Database/Seeders/Exceptions/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Database/Seeders/Exceptions/ — Seeder Exception Types
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `CircularDependencyException.php` | Thrown when seeder dependency graph contains a cycle. Includes the `$cycle` array showing the loop path. Has `fromPath()` factory for building from a traversal path. |
|
||||
|
||||
Part of the seeder auto-discovery system. The `CoreDatabaseSeeder` uses topological sorting on `#[SeederAfter]`/`#[SeederBefore]` attributes and throws this when a cycle is detected.
|
||||
84
src/Core/Events/CLAUDE.md
Normal file
84
src/Core/Events/CLAUDE.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Events
|
||||
|
||||
Lifecycle events that drive the module loading system.
|
||||
|
||||
## What It Does
|
||||
|
||||
Defines the event classes that modules listen to via `static $listens` arrays in their Boot classes. Events use a request/collect pattern: modules call methods like `routes()`, `views()`, `livewire()` during event dispatch, and `LifecycleEventProvider` processes the collected requests afterwards.
|
||||
|
||||
This is the **core of the module loading architecture**. Modules are never instantiated until their listened events fire.
|
||||
|
||||
## Lifecycle Events (Mutually Exclusive by Context)
|
||||
|
||||
| Event | Context | Middleware | Purpose |
|
||||
|-------|---------|------------|---------|
|
||||
| `WebRoutesRegistering` | Web requests | `web` | Public-facing routes, views |
|
||||
| `AdminPanelBooting` | Admin requests | `admin` | Admin dashboard resources |
|
||||
| `ApiRoutesRegistering` | API requests | `api` | REST API endpoints |
|
||||
| `ClientRoutesRegistering` | Client dashboard | `client` | Authenticated SaaS user routes |
|
||||
| `ConsoleBooting` | CLI | -- | Artisan commands |
|
||||
| `QueueWorkerBooting` | Queue workers | -- | Job registration, queue init |
|
||||
| `McpToolsRegistering` | MCP server | -- | MCP tool handlers |
|
||||
| `McpRoutesRegistering` | MCP HTTP | `mcp` | MCP HTTP endpoints |
|
||||
| `FrameworkBooted` | All contexts | -- | Late-stage cross-cutting init |
|
||||
|
||||
## Capability Events (On-Demand)
|
||||
|
||||
| Event | Purpose |
|
||||
|-------|---------|
|
||||
| `DomainResolving` | Multi-tenancy by domain. First provider to `register()` wins |
|
||||
| `SearchRequested` | Lazy-load search: `searchable(Model::class)` |
|
||||
| `MediaRequested` | Lazy-load media: `processor('image', ImageProcessor::class)` |
|
||||
| `MailSending` | Lazy-load mail: `mailable(WelcomeEmail::class)` |
|
||||
|
||||
## Base Class: LifecycleEvent
|
||||
|
||||
All lifecycle events extend `LifecycleEvent`, which provides these request methods:
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `routes(callable)` | Register route callback |
|
||||
| `views(namespace, path)` | Register view namespace |
|
||||
| `livewire(alias, class)` | Register Livewire component |
|
||||
| `middleware(alias, class)` | Register middleware alias |
|
||||
| `command(class)` | Register Artisan command |
|
||||
| `translations(namespace, path)` | Register translation namespace |
|
||||
| `bladeComponentPath(path, namespace)` | Register anonymous Blade components |
|
||||
| `policy(model, policy)` | Register model policy |
|
||||
| `navigation(item)` | Register nav item |
|
||||
|
||||
Each has a corresponding `*Requests()` getter for `LifecycleEventProvider` to process.
|
||||
|
||||
## Observability
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `ListenerProfiler` | Measures execution time, memory, call count per listener. `enable()`, `getSlowListeners()`, `getSlowest(10)`, `getSummary()`, `export()` |
|
||||
| `EventAuditLog` | Tracks success/failure of event handlers. `enable()`, `entries()`, `failures()`, `summary()` |
|
||||
|
||||
## Event Versioning
|
||||
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
| `HasEventVersion` (trait) | Modules declare `$eventVersions` for compatibility checking |
|
||||
|
||||
Events carry `VERSION` and `MIN_SUPPORTED_VERSION` constants. Handlers check `$event->version()` or `$event->supportsVersion(2)` for forward compatibility.
|
||||
|
||||
## Integration
|
||||
|
||||
```php
|
||||
// Module Boot class
|
||||
class Boot {
|
||||
public static array $listens = [
|
||||
WebRoutesRegistering::class => 'onWebRoutes',
|
||||
AdminPanelBooting::class => ['onAdmin', 10], // with priority
|
||||
];
|
||||
|
||||
public function onWebRoutes(WebRoutesRegistering $event): void {
|
||||
$event->views('mymod', __DIR__.'/Views');
|
||||
$event->routes(fn() => require __DIR__.'/web.php');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Flow: `ModuleScanner` reads `$listens` -> `ModuleRegistry` registers `LazyModuleListener` with Laravel Events -> Event fires -> Module instantiated via container -> Method called with event -> Requests collected -> `LifecycleEventProvider` processes.
|
||||
9
src/Core/Events/Concerns/CLAUDE.md
Normal file
9
src/Core/Events/Concerns/CLAUDE.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Events/Concerns/ — Event Version Compatibility
|
||||
|
||||
## Traits
|
||||
|
||||
| Trait | Purpose |
|
||||
|-------|---------|
|
||||
| `HasEventVersion` | For Boot classes to declare which event API versions they support. Methods: `getRequiredEventVersion(eventClass)`, `isCompatibleWithEventVersion(eventClass, version)`, `getEventVersionRequirements()`. |
|
||||
|
||||
Enables graceful handling of event API changes. Modules declare minimum versions via `$eventVersions` static property. The framework checks compatibility during bootstrap and logs warnings for version mismatches.
|
||||
|
|
@ -14,6 +14,7 @@ namespace Core\Front\Admin;
|
|||
use Core\Front\Admin\Contracts\AdminMenuProvider;
|
||||
use Core\Front\Admin\Contracts\DynamicMenuProvider;
|
||||
use Core\Front\Admin\Validation\IconValidator;
|
||||
use Core\Tenant\Services\EntitlementService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
|
|
@ -111,8 +112,8 @@ class AdminMenuRegistry
|
|||
|
||||
public function __construct(?object $entitlements = null, ?IconValidator $iconValidator = null)
|
||||
{
|
||||
if ($entitlements === null && class_exists(\Core\Tenant\Services\EntitlementService::class)) {
|
||||
$this->entitlements = app(\Core\Tenant\Services\EntitlementService::class);
|
||||
if ($entitlements === null && class_exists(EntitlementService::class)) {
|
||||
$this->entitlements = app(EntitlementService::class);
|
||||
} else {
|
||||
$this->entitlements = $entitlements;
|
||||
}
|
||||
|
|
|
|||
38
src/Core/Front/Admin/Blade/components/CLAUDE.md
Normal file
38
src/Core/Front/Admin/Blade/components/CLAUDE.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Front/Admin/Blade/components
|
||||
|
||||
Anonymous Blade components for the admin panel. Used via `<admin:xyz>` tag syntax.
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| action-link | Styled link for table row actions |
|
||||
| activity-feed | Template for ActivityFeed class component |
|
||||
| activity-log | Template for ActivityLog class component |
|
||||
| alert | Template for Alert class component |
|
||||
| card-grid | Template for CardGrid class component |
|
||||
| clear-filters | Template for ClearFilters class component |
|
||||
| data-table | Template for DataTable class component |
|
||||
| editable-table | Template for EditableTable class component |
|
||||
| empty-state | Empty state placeholder with icon and message |
|
||||
| entitlement-gate | Conditionally renders content based on workspace entitlements |
|
||||
| filter / filter-bar | Template for Filter/FilterBar class components |
|
||||
| flash | Session flash message display |
|
||||
| header | Page header with breadcrumbs and actions |
|
||||
| link-grid | Template for LinkGrid class component |
|
||||
| manager-table | Template for ManagerTable class component |
|
||||
| metric-card / metrics | Individual metric card and grid template |
|
||||
| module | Module wrapper with loading states |
|
||||
| nav-group / nav-item / nav-link / nav-menu / nav-panel | Sidebar navigation primitives |
|
||||
| page-header | Page title bar with optional subtitle and actions |
|
||||
| panel | Content panel with optional header/footer |
|
||||
| progress-list | Template for ProgressList class component |
|
||||
| search | Template for Search class component |
|
||||
| service-card / service-cards | Service overview cards |
|
||||
| sidebar / sidemenu | Sidebar shell and menu template |
|
||||
| stat-card / stats | Individual stat card and grid template |
|
||||
| status-cards | Template for StatusCards class component |
|
||||
| tabs | Tab navigation wrapper using `<core:tabs>` |
|
||||
| workspace-card | Workspace overview card |
|
||||
|
||||
Most are templates for the class-backed components in `View/Components/`. A few are standalone anonymous components (empty-state, entitlement-gate, flash, nav-*, page-header, panel, workspace-card).
|
||||
22
src/Core/Front/Admin/Blade/components/tabs/CLAUDE.md
Normal file
22
src/Core/Front/Admin/Blade/components/tabs/CLAUDE.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Front/Admin/Blade/components/tabs
|
||||
|
||||
Tab panel sub-component for admin tabs.
|
||||
|
||||
## Files
|
||||
|
||||
- **panel.blade.php** -- Individual tab panel that auto-detects selected state from `TabContext::$selected`. Wraps `<core:tab.panel>` with automatic selection.
|
||||
- Props: `name` (string, required) -- must match the tab key
|
||||
- Reads `\Core\Front\Admin\TabContext::$selected` to determine visibility
|
||||
|
||||
## Usage
|
||||
|
||||
```blade
|
||||
<admin:tabs :tabs="$tabs" :selected="$currentTab">
|
||||
<admin:tabs.panel name="general">
|
||||
General settings content
|
||||
</admin:tabs.panel>
|
||||
<admin:tabs.panel name="advanced">
|
||||
Advanced settings content
|
||||
</admin:tabs.panel>
|
||||
</admin:tabs>
|
||||
```
|
||||
10
src/Core/Front/Admin/Blade/layouts/CLAUDE.md
Normal file
10
src/Core/Front/Admin/Blade/layouts/CLAUDE.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Front/Admin/Blade/layouts
|
||||
|
||||
Layout templates for the admin panel.
|
||||
|
||||
## Files
|
||||
|
||||
- **app.blade.php** -- Full admin HTML shell with sidebar + content area layout. Includes dark mode (localStorage + cookie sync), FontAwesome Pro CSS, Vite assets (admin.css + app.js), Flux appearance/scripts, collapsible sidebar with `sidebarExpanded` Alpine state (persisted to localStorage), and light/dark mode toggle script.
|
||||
- Props: `title` (string, default 'Admin')
|
||||
- Slots: `$sidebar` (sidebar component), `$header` (top header), `$slot` (main content), `$head` (extra head content), `$scripts` (extra scripts)
|
||||
- Responsive: sidebar hidden on mobile, 20px collapsed / 64px expanded on desktop.
|
||||
|
|
@ -31,9 +31,15 @@ use Core\Front\Admin\View\Components\Stats;
|
|||
use Core\Front\Admin\View\Components\StatusCards;
|
||||
use Core\Headers\SecurityHeaders;
|
||||
use Core\LifecycleEventProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
/**
|
||||
* Admin frontage - admin dashboard stage.
|
||||
|
|
@ -49,12 +55,12 @@ class Boot extends ServiceProvider
|
|||
public static function middleware(Middleware $middleware): void
|
||||
{
|
||||
$middleware->group('admin', [
|
||||
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
ValidateCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
SecurityHeaders::class,
|
||||
'auth',
|
||||
]);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue