feat(admin): add Filament resources for the forecasting stack

Adds three resources under a new "Forecasting" navigation group: a full-CRUD
WatchedEventResource for the Layer 5 volatility detector, plus read-only
WeeklyForecastResource and BacktestResource so the ridge model output and
its calibration can be inspected without SQL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-05-03 09:13:05 +01:00
parent 1c46667f56
commit 8dad223d06
25 changed files with 1289 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Filament\Resources\WeeklyForecasts\Schemas;
use App\Models\WeeklyForecast;
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class WeeklyForecastInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema->components([
Section::make('Forecast')->columns(3)->schema([
TextEntry::make('forecast_for')->date('d M Y'),
TextEntry::make('direction')
->badge()
->color(fn (string $state) => match ($state) {
'rising' => 'warning',
'falling' => 'success',
default => 'gray',
}),
TextEntry::make('magnitude_pence')
->label('Magnitude')
->state(fn (WeeklyForecast $record): string => self::formatMagnitude($record->magnitude_pence)),
TextEntry::make('ridge_confidence')
->label('Confidence')
->state(fn (WeeklyForecast $record): string => $record->ridge_confidence.'%')
->color(fn (WeeklyForecast $record) => $record->ridge_confidence < 40 ? 'warning' : null),
IconEntry::make('flagged_duty_change')
->label('Duty change adjacent')
->boolean()
->trueColor('warning'),
TextEntry::make('generated_at')->dateTime('d M Y H:i'),
]),
Section::make('Reasoning')->schema([
TextEntry::make('reasoning')
->columnSpanFull()
->placeholder('No reasoning recorded.'),
]),
Section::make('Model')
->description('Calibration table from the matching backtest determines the displayed confidence.')
->schema([
TextEntry::make('model_version')->columnSpanFull(),
]),
]);
}
protected static function formatMagnitude(?int $magnitudePence): string
{
if ($magnitudePence === null) {
return '—';
}
$pence = round($magnitudePence / 100, 1);
$sign = $pence > 0 ? '+' : '';
return $sign.$pence.'p';
}
}