# Filament Admin Panel — Design Spec **Date:** 2026-04-04 **Stack:** Laravel 11, Filament v5, Livewire v4 **Scope:** Admin panel for FuelAlert — internal tooling only, no user-facing exposure --- ## 1. Access & Auth - Filament panel mounted at `/admin` - Protected by `is_admin` boolean on `users` table - Migration adds `is_admin TINYINT(1) DEFAULT 0` to `users` - `AdminPanelProvider` uses `->authGuard('web')` (same guard as the main app — no separate admin users table) - `canAccessPanel()` check: `$user->is_admin === true` - `uovidiu@sent.com` seeded as admin via a dedicated `AdminSeeder` (idempotent — safe to re-run) - No self-registration on the admin panel — access is granted only via the `is_admin` flag --- ## 2. Dashboard Page Default landing page. Four `StatsOverviewWidget` cards: | Stat | Source | Alert | |---|---|---| | Total users | `users` count | — | | Stations in DB | `stations` count + `max(last_seen_at)` | — | | Latest oil prediction | Most recent `price_predictions` row — direction, confidence, source | Yellow if > 24h old | | API errors (24h) | `api_logs` where `status_code >= 400` or `error IS NOT NULL`, last 24h | Red if > 0 | No charts on the dashboard — keep it scannable. Charts live inside individual resources. --- ## 3. Resources ### 3.1 Users (`UserResource`) **Purpose:** View all registered users, manage admin flag, correct postcodes. **Table columns:** name, email, postcode, is_admin (badge), created_at **Filters:** is_admin toggle **Actions:** - Edit: `is_admin` toggle, `postcode` text field (no other fields editable from admin) - Delete: allowed (hard delete, with confirmation modal) **Notes:** - No subscription tier column yet — add when Cashier is integrated - No impersonation in v1 — add later when user dashboard exists --- ### 3.2 API Logs (`ApiLogResource`) **Purpose:** Primary debugging tool — see every outbound HTTP call made by the scheduler. **Table columns:** service (badge), method, url (truncated), status_code (colour-coded), duration_ms, error (truncated), created_at **Filters:** service (select: `fuel_finder`, `fred`, `anthropic`), errors only (toggle: where error IS NOT NULL or status >= 400), date range **Default sort:** created_at DESC **Actions:** View (modal showing full url, full error, request/response if available) — no edit, no delete **Colour coding for status_code:** - 2xx → success (green) - 4xx → warning (yellow) - 5xx / null → danger (red) --- ### 3.3 Oil Predictions (`OilPredictionResource`) **Purpose:** Verify the daily `oil:predict` job is running and producing sensible output. **Table columns:** predicted_for (date), source (badge: LLM / EWMA), direction (badge: rising/falling/flat with colour), confidence (progress bar 0–100), reasoning (truncated), generated_at **Filters:** source, direction, date range **Default sort:** predicted_for DESC **Actions:** - View (modal showing full reasoning text) - **Run prediction now** (page-level action): executes `php artisan oil:predict` via `Artisan::call()`, shows success/failure notification **No edit/delete** — predictions are immutable audit records. --- ### 3.4 Brent Prices (`BrentPriceResource`) **Purpose:** Verify FRED fetch is populating data correctly. **Table columns:** date, price_usd **Default sort:** date DESC **Actions:** none Simple read-only table. No filters needed — data is always chronological. Include a line chart widget (`BrentPriceChartWidget`) showing the last 30 days using Filament's built-in chart widget. --- ### 3.5 Stations (`StationResource`) **Purpose:** Browse the ~14,500 UK stations; verify supermarket tagging and closure flags. **Table columns:** trading_name, brand_name, postcode, city, is_supermarket (badge), is_motorway_service_station (badge), temporary_closure (badge), last_seen_at **Filters:** is_supermarket, is_motorway_service_station, temporary_closure, permanent_closure, postcode prefix (text search) **Search:** trading_name, brand_name, postcode **Default sort:** last_seen_at DESC **Actions:** - View (modal with full address, amenities, opening_times, fuel_types JSON) - **Trigger full poll** (page-level action): dispatches a queued `PollFuelPricesJob` (wraps `fuel:poll --full`) rather than calling Artisan synchronously — full station refresh on 14,500 records would exceed HTTP timeout. Shows "Poll dispatched to queue" notification immediately; result visible in API Logs once complete. **No edit** — station data is owned by Fuel Finder API, overwritten on each poll. --- ## 4. Resources Planned But Not Built Yet These will be added once the underlying tables/services exist: | Resource | Depends on | |---|---| | Alerts Log | `alerts` table + `NotificationDispatchService` | | Scoring Results | `scoring_results` table + `AlertScoringService` | | Subscriptions | Cashier integration + `subscriptions` table | --- ## 5. Navigation Groups ``` Dashboard (no group) ├── Users Data ├── Stations ├── Brent Prices ├── Oil Predictions System ├── API Logs ``` --- ## 6. Build Order 1. Migration — add `is_admin` to `users` 2. `AdminSeeder` — seed `uovidiu@sent.com` as admin 3. `AdminPanelProvider` — mount panel, configure auth 4. `ApiLogResource` — highest immediate value 5. `UserResource` — manage admin flag 6. `OilPredictionResource` + run-prediction action 7. `BrentPriceResource` + chart widget 8. `StationResource` + full-poll action 9. Dashboard widgets (last — depends on all resources being stable) --- ## 7. Out of Scope (v1) - Role-based permissions beyond `is_admin` — single admin user is sufficient - Activity log / audit trail of admin actions - Dark mode customisation - Custom Filament theme — use default - Two-factor auth on admin panel — covered by app-level 2FA (Fortify)