Admin Menü neu geordnet.
Introduced a two-tier media pipeline with dynamic disks, asset tracking, admin controls, and alerting around
upload/archival workflows.
- Added storage metadata + asset tables and models so every photo/variant knows where it lives
(database/migrations/2025_10_20_090000_create_media_storage_targets_table.php, database/ migrations/2025_10_20_090200_create_event_media_assets_table.php, app/Models/MediaStorageTarget.php:1, app/
Models/EventMediaAsset.php:1, app/Models/EventStorageAssignment.php:1, app/Models/Event.php:27).
- Rewired guest and tenant uploads to pick the event’s hot disk, persist EventMediaAsset records, compute
checksums, and clean up on delete (app/Http/Controllers/Api/EventPublicController.php:243, app/Http/
Controllers/Api/Tenant/PhotoController.php:25, app/Models/Photo.php:25).
- Implemented storage services, archival job scaffolding, monitoring config, and queue-failure notifications for upload issues (app/Services/Storage/EventStorageManager.php:16, app/Services/Storage/
StorageHealthService.php:9, app/Jobs/ArchiveEventMediaAssets.php:16, app/Providers/AppServiceProvider.php:39, app/Notifications/UploadPipelineFailed.php:8, config/storage-monitor.php:1).
- Seeded default hot/cold targets and exposed super-admin tooling via a Filament resource and capacity widget (database/seeders/MediaStorageTargetSeeder.php:13, database/seeders/DatabaseSeeder.php:17, app/Filament/Resources/MediaStorageTargetResource.php:1, app/Filament/Widgets/StorageCapacityWidget.php:12, app/Providers/Filament/SuperAdminPanelProvider.php:47).
- Dropped cron skeletons and artisan placeholders to schedule storage monitoring, archival dispatch, and upload queue health checks (cron/storage_monitor.sh, cron/archive_dispatcher.sh, cron/upload_queue_health.sh, routes/console.php:9).
This commit is contained in:
@@ -44,6 +44,11 @@ class CategoryResource extends Resource
|
||||
|
||||
protected static ?string $modelLabel = 'Kategorie';
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Content & Bibliothek';
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
|
||||
@@ -51,6 +51,11 @@ class PostResource extends Resource
|
||||
|
||||
protected static ?string $modelLabel = 'Beitrag';
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Content & Bibliothek';
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
|
||||
@@ -28,7 +28,7 @@ class EmotionResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.library');
|
||||
return __('admin.nav.tasks_emotions');
|
||||
}
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ class EventPurchaseResource extends Resource
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Billing & Finanzen';
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
|
||||
@@ -34,7 +34,7 @@ class EventResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.platform');
|
||||
return __('admin.nav.event_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
|
||||
@@ -25,7 +25,7 @@ class EventTypeResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.library');
|
||||
return __('admin.nav.event_management');
|
||||
}
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class LegalPageResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.platform');
|
||||
return __('admin.nav.content_library');
|
||||
}
|
||||
protected static ?int $navigationSort = 40;
|
||||
|
||||
|
||||
130
app/Filament/Resources/MediaStorageTargetResource.php
Normal file
130
app/Filament/Resources/MediaStorageTargetResource.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\MediaStorageTargetResource\Pages;
|
||||
use App\Models\MediaStorageTarget;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
class MediaStorageTargetResource extends Resource
|
||||
{
|
||||
protected static ?string $model = MediaStorageTarget::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-server';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = null;
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.platform_management');
|
||||
}
|
||||
|
||||
protected static ?int $navigationSort = 60;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->schema([
|
||||
TextInput::make('key')
|
||||
->label('Schlüssel')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(64),
|
||||
TextInput::make('name')
|
||||
->label('Bezeichnung')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Select::make('driver')
|
||||
->label('Treiber')
|
||||
->required()
|
||||
->options([
|
||||
'local' => 'Local',
|
||||
'sftp' => 'SFTP',
|
||||
's3' => 'S3 Compatible',
|
||||
]),
|
||||
TextInput::make('priority')
|
||||
->label('Priorität')
|
||||
->numeric()
|
||||
->default(0),
|
||||
Toggle::make('is_hot')
|
||||
->label('Hot Storage')
|
||||
->helperText('Markiert Speicher als primär für aktive Uploads.')
|
||||
->default(false),
|
||||
Toggle::make('is_default')
|
||||
->label('Standard')
|
||||
->helperText('Wird automatisch für neue Events verwendet.')
|
||||
->default(false),
|
||||
Toggle::make('is_active')
|
||||
->label('Aktiv')
|
||||
->default(true),
|
||||
KeyValue::make('config')
|
||||
->label('Konfiguration')
|
||||
->keyLabel('Option')
|
||||
->valueLabel('Wert')
|
||||
->columnSpanFull()
|
||||
->helperText('Treiber-spezifische Einstellungen wie Pfade, Hosts oder Zugangsdaten.'),
|
||||
])->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('key')
|
||||
->label('Key')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('Name')
|
||||
->searchable(),
|
||||
Tables\Columns\BadgeColumn::make('driver')
|
||||
->label('Driver')
|
||||
->colors([
|
||||
'gray' => 'local',
|
||||
'info' => 'sftp',
|
||||
'success' => 's3',
|
||||
]),
|
||||
Tables\Columns\IconColumn::make('is_hot')
|
||||
->label('Hot')
|
||||
->boolean(),
|
||||
Tables\Columns\IconColumn::make('is_default')
|
||||
->label('Default')
|
||||
->boolean(),
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->label('Active')
|
||||
->boolean(),
|
||||
Tables\Columns\TextColumn::make('priority')
|
||||
->label('Priority')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->label('Aktualisiert')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([])
|
||||
->actions([
|
||||
Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Actions\DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListMediaStorageTargets::route('/'),
|
||||
'create' => Pages\CreateMediaStorageTarget::route('/create'),
|
||||
'edit' => Pages\EditMediaStorageTarget::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\MediaStorageTargetResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MediaStorageTargetResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateMediaStorageTarget extends CreateRecord
|
||||
{
|
||||
protected static string $resource = MediaStorageTargetResource::class;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\MediaStorageTargetResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MediaStorageTargetResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditMediaStorageTarget extends EditRecord
|
||||
{
|
||||
protected static string $resource = MediaStorageTargetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\MediaStorageTargetResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MediaStorageTargetResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListMediaStorageTargets extends ListRecords
|
||||
{
|
||||
protected static string $resource = MediaStorageTargetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ class PackageResource extends Resource
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = null;
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.platform_management');
|
||||
}
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -243,10 +248,12 @@ class PackageResource extends Resource
|
||||
{
|
||||
return [
|
||||
'basic_uploads' => 'Basis-Uploads',
|
||||
'limited_sharing' => 'Begrenztes Teilen',
|
||||
'unlimited_sharing' => 'Unbegrenztes Teilen',
|
||||
'no_watermark' => 'Kein Wasserzeichen',
|
||||
'custom_branding' => 'Eigenes Branding',
|
||||
'custom_tasks' => 'Eigene Aufgaben',
|
||||
'reseller_dashboard' => 'Reseller-Dashboard',
|
||||
'advanced_analytics' => 'Erweiterte Analytics',
|
||||
'advanced_reporting' => 'Erweiterte Reports',
|
||||
'live_slideshow' => 'Live-Slideshow',
|
||||
|
||||
@@ -30,7 +30,7 @@ class PhotoResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.content');
|
||||
return __('admin.nav.event_management');
|
||||
}
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
|
||||
@@ -36,7 +36,7 @@ class PurchaseResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Billing';
|
||||
return 'Billing & Finanzen';
|
||||
}
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
@@ -28,7 +28,7 @@ class TaskResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.library');
|
||||
return __('admin.nav.tasks_emotions');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
|
||||
@@ -32,6 +32,11 @@ class TenantPackageResource extends Resource
|
||||
|
||||
protected static ?string $slug = 'tenant-packages';
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Billing & Finanzen';
|
||||
}
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
|
||||
@@ -32,7 +32,7 @@ class TenantResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.platform');
|
||||
return __('admin.nav.platform_management');
|
||||
}
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ class UserResource extends Resource
|
||||
|
||||
protected static ?string $slug = 'users';
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Plattform-Verwaltung';
|
||||
}
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
|
||||
76
app/Filament/Widgets/StorageCapacityWidget.php
Normal file
76
app/Filament/Widgets/StorageCapacityWidget.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\MediaStorageTarget;
|
||||
use App\Services\Storage\StorageHealthService;
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Card;
|
||||
|
||||
class StorageCapacityWidget extends StatsOverviewWidget
|
||||
{
|
||||
protected static ?int $sort = 1;
|
||||
|
||||
protected function getCards(): array
|
||||
{
|
||||
$health = app(StorageHealthService::class);
|
||||
|
||||
return MediaStorageTarget::all()
|
||||
->map(function (MediaStorageTarget $target) use ($health) {
|
||||
$stats = $health->getCapacity($target);
|
||||
|
||||
if ($stats['status'] !== 'ok') {
|
||||
return Card::make($target->name, 'Kapazität unbekannt')
|
||||
->description(match ($stats['status']) {
|
||||
'unavailable' => 'Monitoring nicht verfügbar',
|
||||
'unknown' => 'Monitor-Pfad nicht gesetzt',
|
||||
'error' => $stats['message'] ?? 'Fehler beim Auslesen',
|
||||
default => 'Status unbekannt',
|
||||
})
|
||||
->descriptionIcon('heroicon-m-question-mark-circle')
|
||||
->color('warning');
|
||||
}
|
||||
|
||||
$used = $this->formatBytes($stats['used']);
|
||||
$total = $this->formatBytes($stats['total']);
|
||||
$free = $this->formatBytes($stats['free']);
|
||||
$percentageValue = $stats['percentage'];
|
||||
$percent = $percentageValue !== null ? $percentageValue.' %' : '–';
|
||||
|
||||
$color = 'success';
|
||||
if ($percentageValue === null) {
|
||||
$color = 'warning';
|
||||
} elseif ($percentageValue >= 80) {
|
||||
$color = 'danger';
|
||||
} elseif ($percentageValue >= 60) {
|
||||
$color = 'warning';
|
||||
}
|
||||
|
||||
return Card::make($target->name, "$used / $total")
|
||||
->description("Frei: $free · Auslastung: $percent")
|
||||
->color($color)
|
||||
->extraAttributes([
|
||||
'data-storage-disk' => $target->key,
|
||||
]);
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function formatBytes(?int $bytes): string
|
||||
{
|
||||
if ($bytes === null) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
$index = 0;
|
||||
$value = (float) $bytes;
|
||||
|
||||
while ($value >= 1024 && $index < count($units) - 1) {
|
||||
$value /= 1024;
|
||||
$index++;
|
||||
}
|
||||
|
||||
return round($value, 1).' '.$units[$index];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user