# 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)
