Laravel Filament admin dashboard

Laravel Filament Admin Dashboard for AI Applications: Token Costs, Prompt Management, and Agent Audit Trails

You have the AI pipeline. You have the service classes, the middleware, the queued jobs. The backend is instrumented. What you do not have is any way to look at it without writing a SQL query at 2am because costs just spiked.

That is the gap this article closes.

A Laravel Filament admin dashboard is not the glamorous part of AI development. But the teams that skip it are the ones who discover they have been over-spending on tokens for six weeks, or that an agent ran a destructive tool call nobody authorised, or that a prompt change from last Tuesday quietly broke output quality across the board. You cannot govern what you cannot see.

The Laravel Filament admin dashboard built in this guide surfaces four things: real-time token cost monitoring, usage analytics broken down by user and model, a prompt version management panel, and a full agent audit trail. Each section is a Filament v3 Resource or Widget, built on top of the telemetry data your existing middleware and service layer already generate.

Why Filament, and Why Now

There is no serious competition in the Laravel ecosystem for building admin interfaces right now. Filament v3 ships with a full panel system, Livewire-powered tables, stats widgets, chart widgets, custom pages, and a role-based authorization layer. It works inside your existing Laravel application. It does not require a separate process or a Node frontend.

The Livewire-driven rendering keeps things reactive without JavaScript complexity. The Eloquent-backed Resources mean your existing models map directly to admin panels with almost no boilerplate. And because it is all Laravel under the hood, your Service Container, policies, events, and observers all still apply — you are not learning a new paradigm, just adding a UI layer on top of architecture you have already built.

Filament v3 requires PHP 8.1+ and Laravel 10+. On Laravel 11 or 12 you will be fine.

Installation and Panel Setup

Install Filament with the panel scaffolding command:

bash

composer require filament/filament:"^3.0" -W
php artisan filament:install --panels

This generates app/Providers/Filament/AdminPanelProvider.php and registers it via bootstrap/app.php. The panel provider is where you wire discovery, colours, middleware, and authentication. Here is a clean starting configuration:

php

// app/Providers/Filament/AdminPanelProvider.php

namespace App\Providers\Filament;

use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;

class AdminPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->default()
            ->id('admin')
            ->path('admin')
            ->login()
            ->colors(['primary' => Color::Violet])
            ->discoverResources(
                in: app_path('Filament/Resources'),
                for: 'App\\Filament\\Resources'
            )
            ->discoverPages(
                in: app_path('Filament/Pages'),
                for: 'App\\Filament\\Pages'
            )
            ->discoverWidgets(
                in: app_path('Filament/Widgets'),
                for: 'App\\Filament\\Widgets'
            )
            ->middleware([
                EncryptCookies::class,
                AddQueuedCookiesToResponse::class,
                StartSession::class,
                AuthenticateSession::class,
                ShareErrorsFromSession::class,
                VerifyCsrfToken::class,
                SubstituteBindings::class,
                DisableBladeIconComponents::class,
                DispatchServingFilamentEvent::class,
            ])
            ->authMiddleware([
                Authenticate::class,
            ])
            ->authorization(fn () => auth()->user()?->is_admin ?? false);
    }
}

That last ->authorization() call is the line most tutorials omit. Without it, any authenticated user can reach your admin panel. We will come back to access control later — but set that guard from day one.

Modeling the Data Your Dashboard Will Surface

Before you build a single Filament Resource, you need to decide what data you are collecting. On a production AI application, there are two primary log types worth persisting.

AI Usage Logs capture every inference call: which provider, which model, how many tokens, what it cost, which feature triggered it, and whether it succeeded.

Agent Audit Logs capture agentic actions: which agent class ran, what action it performed, which tool it called, what the input and output were, and how long it took.

Both should be Eloquent models backed by indexed tables. Start with the usage log migration:

php

// database/migrations/2026_04_15_000001_create_ai_usage_logs_table.php

Schema::create('ai_usage_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $table->string('provider');           // 'anthropic', 'openai'
    $table->string('model');
    $table->string('feature')->nullable(); // 'chat', 'summarize', 'agent'
    $table->integer('prompt_tokens');
    $table->integer('completion_tokens');
    $table->integer('total_tokens');
    $table->decimal('cost_usd', 10, 6)->nullable();
    $table->string('prompt_version')->nullable();
    $table->json('metadata')->nullable();
    $table->boolean('success')->default(true);
    $table->text('error_message')->nullable();
    $table->timestamps();

    $table->index(['user_id', 'created_at']);
    $table->index(['provider', 'model', 'created_at']);
    $table->index(['feature', 'created_at']);
});

The compound indexes on created_at are non-negotiable. Dashboard widgets run aggregation queries constantly, and without them you will feel the difference under real traffic.

The agent audit log has a slightly different shape:

php

// database/migrations/2026_04_15_000002_create_agent_audit_logs_table.php

Schema::create('agent_audit_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
    $table->string('agent_class');
    $table->string('action');
    $table->string('tool_used')->nullable();
    $table->json('input_payload')->nullable();
    $table->json('output_payload')->nullable();
    $table->integer('tokens_used')->default(0);
    $table->float('duration_ms')->nullable();
    $table->boolean('success')->default(true);
    $table->text('error')->nullable();
    $table->timestamps();

    $table->index(['agent_class', 'created_at']);
    $table->index(['user_id', 'created_at']);
    $table->index(['success', 'created_at']);
});

Both models should cast their JSON columns and use standard Eloquent relationships back to User. Nothing exotic required.

Token Cost Monitoring: The Stats Overview Widget

The first thing an operator needs to see when they open the dashboard is a top-level summary of what the AI layer has cost in the last 24 hours. Filament’s StatsOverviewWidget handles this cleanly.

bash

php artisan make:filament-widget AiStatsOverview --stats-overview

php

// app/Filament/Widgets/AiStatsOverview.php

namespace App\Filament\Widgets;

use App\Models\AiUsageLog;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;

class AiStatsOverview extends BaseWidget
{
    protected static ?int $sort = 1;

    protected function getStats(): array
    {
        $today  = AiUsageLog::whereDate('created_at', today());
        $month  = AiUsageLog::whereMonth('created_at', now()->month)
                            ->whereYear('created_at', now()->year);

        return [
            Stat::make("Today's Tokens", number_format($today->sum('total_tokens')))
                ->description('Across all providers and models')
                ->color('primary'),

            Stat::make("Today's Cost", '$' . number_format($today->sum('cost_usd'), 4))
                ->description('USD — live tally')
                ->color('warning'),

            Stat::make('Monthly Spend', '$' . number_format($month->sum('cost_usd'), 2))
                ->description(now()->format('F Y'))
                ->color('danger'),

            Stat::make('Failures Today', number_format($today->where('success', false)->count()))
                ->description('Inference errors across all features')
                ->color('gray'),
        ];
    }
}

Register this widget on your dashboard in the panel provider’s ->widgets() array. It polls on a configurable interval using Livewire’s polling mechanism — add protected static ?string $pollingInterval = '30s'; if you want it to refresh automatically.

If you have not yet built the middleware layer that feeds these logs, the Laravel AI Middleware guide on token tracking and rate limiting covers the exact pattern for intercepting inference calls and persisting usage data before the response reaches the controller.

Daily Token Usage: The Chart Widget

Raw totals are useful. Trend lines are what actually surface problems — a cost spike on Tuesday, a failure rate that spiked after a deployment. Add a chart widget to give operators a 30-day view:

bash

php artisan make:filament-widget TokenUsageChart --chart

php

// app/Filament/Widgets/TokenUsageChart.php

namespace App\Filament\Widgets;

use App\Models\AiUsageLog;
use Filament\Widgets\ChartWidget;

class TokenUsageChart extends ChartWidget
{
    protected static ?string $heading = 'Daily Token Usage — Last 30 Days';
    protected static ?int $sort = 2;
    public ?string $filter = 'tokens';

    protected function getFilters(): ?array
    {
        return [
            'tokens' => 'Total Tokens',
            'cost'   => 'Cost (USD)',
        ];
    }

    protected function getData(): array
    {
        $column = $this->filter === 'cost' ? 'cost_usd' : 'total_tokens';
        $label  = $this->filter === 'cost' ? 'Cost (USD)' : 'Total Tokens';

        $data = AiUsageLog::selectRaw("DATE(created_at) as date, SUM({$column}) as value")
            ->where('created_at', '>=', now()->subDays(30))
            ->groupBy('date')
            ->orderBy('date')
            ->get();

        return [
            'datasets' => [
                [
                    'label'       => $label,
                    'data'        => $data->pluck('value')->toArray(),
                    'borderColor' => '#8b5cf6',
                    'tension'     => 0.3,
                    'fill'        => false,
                ],
            ],
            'labels' => $data->pluck('date')->toArray(),
        ];
    }

    protected function getType(): string
    {
        return 'line';
    }
}

The $filter toggle lets operators switch between token volume and cost in a single widget, which keeps the dashboard clean without multiplying panels.

The Usage Log Resource: Per-User and Per-Model Breakdown

Stats and charts give you the summary. When you need to investigate — which user ran 400,000 tokens in one session, which model is responsible for the cost spike — you need a sortable, filterable table.

bash

php artisan make:filament-resource AiUsageLog --generate

Then replace the generated table definition with something that has real operator value:

php

// app/Filament/Resources/AiUsageLogResource.php

namespace App\Filament\Resources;

use App\Filament\Resources\AiUsageLogResource\Pages;
use App\Models\AiUsageLog;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;

class AiUsageLogResource extends Resource
{
    protected static ?string $model           = AiUsageLog::class;
    protected static ?string $navigationIcon  = 'heroicon-o-chart-bar';
    protected static ?string $navigationGroup = 'AI Governance';
    protected static ?string $navigationLabel = 'Usage Logs';

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('user.name')
                    ->label('User')
                    ->searchable()
                    ->sortable()
                    ->placeholder('—'),

                TextColumn::make('provider')
                    ->badge()
                    ->sortable(),

                TextColumn::make('model')
                    ->searchable()
                    ->copyable(),

                TextColumn::make('feature')
                    ->badge()
                    ->color('info')
                    ->placeholder('—'),

                TextColumn::make('total_tokens')
                    ->numeric()
                    ->sortable()
                    ->summarize(Tables\Columns\Summarizers\Sum::make()->label('Total')),

                TextColumn::make('cost_usd')
                    ->money('usd')
                    ->sortable()
                    ->summarize(Tables\Columns\Summarizers\Sum::make()->label('Total')),

                TextColumn::make('prompt_version')
                    ->badge()
                    ->color('warning')
                    ->placeholder('—'),

                IconColumn::make('success')
                    ->boolean(),

                TextColumn::make('created_at')
                    ->dateTime()
                    ->sortable()
                    ->since()
                    ->toggleable(),
            ])
            ->filters([
                SelectFilter::make('provider')
                    ->options([
                        'anthropic' => 'Anthropic',
                        'openai'    => 'OpenAI',
                    ]),

                SelectFilter::make('success')
                    ->options([
                        '1' => 'Successful',
                        '0' => 'Failed',
                    ]),

                Filter::make('today')
                    ->label('Today only')
                    ->query(fn (Builder $query) => $query->whereDate('created_at', today())),
            ])
            ->defaultSort('created_at', 'desc')
            ->paginated([25, 50, 100]);
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListAiUsageLogs::route('/'),
        ];
    }

    public static function canCreate(): bool
    {
        return false;
    }
}

Two things to note here. First, ->summarize() on the token and cost columns gives you running totals in the table footer — incredibly useful when you have applied filters to isolate a user or a date range. Second, canCreate(): false disables the “New record” button. Usage logs are system-generated. Nobody should be hand-entering them.

Production-Grade AI Architecture in Laravel: Contracts, Governance & Telemetry details the telemetry layer that populates this data. If your AI services are not already emitting structured usage events to an observable logging interface, that article is the right place to start before wiring up this dashboard.

Prompt Version Management

Operators need to know which prompt is active. They also need to see when it changed, what the previous version said, and whether cost or quality shifted after a change. This is where the dashboard and your version control system connect.

Prompt Migrations: Bringing Determinism to AI in Laravel covers building the underlying prompt migration system. Assuming you have a prompt_versions table from that guide, the Filament Resource surfaces it with minimal effort:

bash

php artisan make:filament-resource PromptVersion --generate

php

// app/Filament/Resources/PromptVersionResource.php

namespace App\Filament\Resources;

use App\Filament\Resources\PromptVersionResource\Pages;
use App\Models\PromptVersion;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;

class PromptVersionResource extends Resource
{
    protected static ?string $model           = PromptVersion::class;
    protected static ?string $navigationIcon  = 'heroicon-o-document-text';
    protected static ?string $navigationGroup = 'AI Governance';
    protected static ?string $navigationLabel = 'Prompt Versions';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('key')->required()->disabled(),
                TextInput::make('version')->required()->disabled(),
                Textarea::make('system_prompt')->rows(8)->columnSpanFull(),
                Textarea::make('description')->rows(3)->columnSpanFull(),
                Toggle::make('is_active')->label('Active')->disabled(),
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('key')->searchable()->sortable(),
                TextColumn::make('version')->badge()->color('primary'),
                TextColumn::make('description')->limit(60)->placeholder('—'),
                IconColumn::make('is_active')->boolean()->label('Active'),
                TextColumn::make('created_at')->dateTime()->sortable(),
            ])
            ->defaultSort('created_at', 'desc');
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListPromptVersions::route('/'),
            'view'  => Pages\ViewPromptVersion::route('/{record}'),
        ];
    }

    public static function canCreate(): bool { return false; }
    public static function canEdit($record): bool { return false; }
    public static function canDelete($record): bool { return false; }
}

All three can* methods return false. Prompt changes belong in migrations under version control. The admin panel gives you read access only. This is intentional. The moment you allow operators to edit prompt content via a UI form, you have created an unversioned, un-reviewable, un-rollbackable change path that will cause you pain.

[Architect’s Note] Keep your admin panel read-only for prompt content. The dashboard is a governance tool, not a content editor. Every prompt mutation should flow through a migration, be committed to version control, and be deployable like code. If your team is asking for a UI to edit prompts live, that is a process problem masquerading as a tooling request. Fix the process.

Agent Audit Trails

This is the section most teams skip. It is also the one that saves them when something goes wrong.

Agentic workflows make decisions. They call tools. They read data and write data. When a user complains that the agent deleted something it should not have, or an integration breaks because the agent started formatting its output differently, you need a record of exactly what happened.

If you are building agentic pipelines, the Hardening Laravel Agentic Workflows guide covers schema validation and output hardening. The audit trail built here is the operational companion to that — it answers “what did it do?” as opposed to “did it do it correctly?”

Emit an audit log from your base agent class on every action:

php

// app/AI/Agents/BaseAgent.php

namespace App\AI\Agents;

use App\Models\AgentAuditLog;

abstract class BaseAgent
{
    protected function logAction(
        string $action,
        array $input,
        array $output,
        ?string $toolUsed = null,
        int $tokensUsed = 0,
        float $durationMs = 0.0,
        bool $success = true,
        ?string $error = null
    ): void {
        AgentAuditLog::create([
            'user_id'        => auth()->id(),
            'agent_class'    => static::class,
            'action'         => $action,
            'tool_used'      => $toolUsed,
            'input_payload'  => $input,
            'output_payload' => $output,
            'tokens_used'    => $tokensUsed,
            'duration_ms'    => $durationMs,
            'success'        => $success,
            'error'          => $error,
        ]);
    }
}

The Filament Resource for the audit log is where the detail view earns its keep. Operators need to be able to click into a single agent run and see the full input/output payload:

php

// app/Filament/Resources/AgentAuditLogResource.php

namespace App\Filament\Resources;

use App\Filament\Resources\AgentAuditLogResource\Pages;
use App\Models\AgentAuditLog;
use Filament\Infolists\Components\KeyValueEntry;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;

class AgentAuditLogResource extends Resource
{
    protected static ?string $model           = AgentAuditLog::class;
    protected static ?string $navigationIcon  = 'heroicon-o-cpu-chip';
    protected static ?string $navigationGroup = 'AI Governance';
    protected static ?string $navigationLabel = 'Agent Audit Trail';

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('user.name')->label('User')->searchable()->placeholder('System'),
                TextColumn::make('agent_class')->badge()->searchable(),
                TextColumn::make('action')->searchable(),
                TextColumn::make('tool_used')->badge()->color('info')->placeholder('—'),
                TextColumn::make('tokens_used')->numeric()->sortable(),
                TextColumn::make('duration_ms')->label('ms')->numeric()->sortable(),
                IconColumn::make('success')->boolean(),
                TextColumn::make('created_at')->dateTime()->sortable()->since(),
            ])
            ->filters([
                SelectFilter::make('success')->options(['1' => 'Successful', '0' => 'Failed']),
                SelectFilter::make('agent_class')->relationship('agentClass', 'agent_class'),
            ])
            ->defaultSort('created_at', 'desc')
            ->recordUrl(fn ($record) => static::getUrl('view', ['record' => $record]));
    }

    public static function infolist(Infolist $infolist): Infolist
    {
        return $infolist
            ->schema([
                Section::make('Run Details')
                    ->columns(2)
                    ->schema([
                        TextEntry::make('agent_class')->badge(),
                        TextEntry::make('action'),
                        TextEntry::make('tool_used')->badge()->color('info')->placeholder('None'),
                        TextEntry::make('tokens_used')->numeric(),
                        TextEntry::make('duration_ms')->label('Duration (ms)')->numeric(),
                        TextEntry::make('success')->badge()->getStateUsing(
                            fn ($record) => $record->success ? 'Success' : 'Failed'
                        )->color(fn ($state) => $state === 'Success' ? 'success' : 'danger'),
                    ]),

                Section::make('Input Payload')
                    ->schema([
                        KeyValueEntry::make('input_payload')->label(''),
                    ]),

                Section::make('Output Payload')
                    ->schema([
                        KeyValueEntry::make('output_payload')->label(''),
                    ]),

                Section::make('Error')
                    ->schema([TextEntry::make('error')->placeholder('None')])
                    ->visible(fn ($record) => !$record->success),
            ]);
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListAgentAuditLogs::route('/'),
            'view'  => Pages\ViewAgentAuditLog::route('/{record}'),
        ];
    }

    public static function canCreate(): bool { return false; }
    public static function canEdit($record): bool { return false; }
}

The Infolist view is the right choice here over a form, because you are displaying data, not editing it. The KeyValueEntry component renders JSON payloads cleanly without any custom Blade work.

Locking Down the Admin Panel

Your Filament admin is a high-value target. It contains cost data, user data, prompt intellectual property, and agent decision history. Basic access control is not optional.

The ->authorization() callback in your panel provider is your first line of defence. The example earlier used is_admin on the User model. In practice, most teams reach for Spatie’s Laravel Permission package for this. Your panel provider call then becomes:

php

->authorization(fn () => auth()->user()?->hasRole('admin') ?? false)

For more granular control over individual Resources, use Laravel Policies. Filament automatically discovers and respects a policy for any model that has one. A AiUsageLogPolicy with viewAny returning $user->hasRole('admin') gives you fine-grained control without putting access logic inside the Resource class itself.

One pattern worth enforcing: scope your AI admin panel to a separate admin guard, and ensure it sits behind your Sanctum token authentication if your application issues API tokens to admin users. The Laravel Sanctum API Authentication guide covers token scoping and ability verification, and the same principles apply when you are protecting admin panel access programmatically.

Do not expose the Filament panel under the default /admin path in production. Change the path in your panel provider:

php

->path('ops-internal') // or any non-obvious string

This is not security through obscurity as a replacement for authentication. It is a minor but free win against automated scanning.

Deploying Filament Alongside Your AI Stack

Filament adds Livewire, Alpine.js, and its own asset pipeline to your application. Running php artisan filament:assets publishes vendor assets to your public/ directory. Make sure your deployment script runs this command after every Filament update — if you skip it, operators will see unstyled panels or JavaScript errors after a version bump.

bash

php artisan filament:assets
php artisan filament:optimize  # Filament v3.1+

filament:optimize caches component discovery. On applications with many Resources and Widgets, it makes a measurable difference to panel load time.

If your application uses Redis for caching, you may want to cache the stats queries powering your overview widgets. Filament’s widgets do not cache their data by default. For a stats widget that runs four SUM() aggregations on every page load, wrapping them in cache()->remember() with a 60-second TTL is worth doing before you go to production.

The complete Laravel production deployment guide covers the full deployment pipeline in detail. When you add Filament, extend the deploy script to include filament:assets and filament:optimize alongside your standard migrate, config:cache, and queue:restart steps. Missing any of these on a Filament upgrade is a common source of post-deploy surprises.


Organising the Navigation: The AI Governance Group

With three or more Filament Resources all related to AI observability, you want them grouped in the sidebar. Every Resource in this article already carries protected static ?string $navigationGroup = 'AI Governance';. That single property is enough to group them under a collapsible sidebar section.

You can control group ordering in the panel provider:

php

->navigationGroups([
    'AI Governance',
    'User Management',
    'System',
])

Groups listed here appear in that order in the sidebar. Groups not listed appear at the bottom. Keep AI Governance at the top — it is why operators are opening this panel.

What to Cache, What to Queue

Two performance considerations you will hit once real traffic arrives.

The stats overview widget runs database queries on every page render. If 10 admins have this dashboard open, that is 10 concurrent aggregation queries on your ai_usage_logs table. Cache the results. Override getStats() to use cache()->remember('ai_stats_today', 60, fn() => ...) for each query group.

Bulk insert your usage logs via a queued job rather than synchronously inside the middleware response cycle. A single queued insert dispatched after the AI response returns adds zero latency to the user-facing request and prevents your ai_usage_logs table from becoming a write bottleneck under load. This is especially relevant if you are logging agent audit trails — agent runs that call multiple tools can generate a burst of log writes in a short window.

[Production Pitfall] If you are writing AiUsageLog::create() synchronously inside an AI middleware or a service class, you will see that call become a bottleneck under sustained load. A single slow database write can add 20–50ms to every AI response. Dispatch a queued job instead. Your usage logs do not need to be written before the response returns — they need to be written eventually and reliably.

Extending the Dashboard Over Time

This guide builds the foundation. Here is what comes next.

Budget alerts. Add an Eloquent Observer on AiUsageLog that triggers a notification when daily spend crosses a configurable threshold. Filament’s database notifications work well here — the panel will show the alert in the notification bell without any additional UI work.

Per-feature cost attribution. The feature column on ai_usage_logs lets you break cost down by product feature. A second chart widget comparing cost by feature over time gives product teams the data they need to make informed decisions about which AI features are worth their current token spend.

Prompt performance correlation. Once you have both usage logs (with prompt_version) and an output quality signal — whether that is a user rating, a downstream validation pass/fail, or a schema validation score from your agentic pipeline — you can join them in a custom Filament page and show quality versus cost per prompt version. That is a genuinely differentiated internal tool that most teams never build.

Model comparison. If you are testing multiple models for the same feature (Claude versus GPT-4o, for example), the model column combined with a quality signal gives you a cost-per-quality-point comparison you can act on. That decision gets much easier when you can see it in a table rather than inferring it from CloudWatch logs.


Refer to the official Filament v3 documentation for the full API surface on widgets, infolists, and panel configuration. The patterns in this guide are production-tested starting points, not exhaustive implementations.


Subscribe
Notify of
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Navigation
Scroll to Top