feat: add Filament admin panel with migrations and design spec
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

- Add AdminPanelProvider mounting panel at `/admin` with `is_admin` auth guard
- Add `is_admin` boolean column to users table
- Add brent_prices and price_predictions tables with appropriate indexes
- Add comprehensive admin design spec covering resources, dashboard, navigation, and build order
- Configure default panel with amber primary color and standard middleware stack
- Add compiled Filament assets (actions.js, app.css)
This commit is contained in:
Ovidiu U
2026-04-04 13:40:56 +01:00
parent e532cc1208
commit d5fb7f85bd
59 changed files with 3422 additions and 28 deletions

View File

@@ -392,22 +392,55 @@ FUEL_FINDER_CLIENT_SECRET=
FUEL_FINDER_BASE_URL=https://api.fuel-finder.service.gov.uk
```
## Postcodes.io — postcode → lat/lng
## Postcodes.io — location resolution
- URL: `https://api.postcodes.io/postcodes/{postcode}`
- Free, no API key required
- Called once on user registration / when postcode changes
- Store resolved `lat` + `lng` on `users` table
- Cache postcode lookups for 30 days (postcodes rarely change coordinates)
- Handled by `PostcodeService::resolve(string $query): ?LocationResult`
- Returns `LocationResult` DTO with `query`, `displayName`, `lat`, `lng`
- Results cached for 30 days — cache key `postcode:{normalised_input}`
- Failed lookups are NOT cached — retried on next request
- Input is auto-detected:
## FRED API (St. Louis Fed) — Brent crude direction
| Input type | Example | Endpoint |
|---|---|---|
| Full postcode | `SW1A 1AA` | `GET /postcodes/{postcode}` |
| Outcode (district) | `PE7` | `GET /outcodes/{outcode}` |
| Place / city name | `Manchester` | `GET /places?q={query}&limit=1` |
- Series: `DCOILBRENTEU` (daily Brent spot price)
- URL: `https://api.stlouisfed.org/fred/series/observations?series_id=DCOILBRENTEU&api_key={key}&sort_order=desc&limit=10&file_type=json`
- Free API key required — stored as `FRED_API_KEY` in .env
- Fetched once daily via scheduler at 7am
- Stored in `brent_prices` table: `(date DATE, price_usd DECIMAL(8,2))`
- Only the 5-day trend direction is used by the scoring engine
**Anonymous search flow:** user types a postcode/city → `PostcodeService::resolve()` → lat/lng stored in a JSON cookie (30 days) alongside the query string. On return visits, cookie lat/lng is used directly — postcodes.io is only called when the search term changes.
**Registered users:** postcode resolved once on registration, lat/lng stored on `users` table — not re-resolved unless postcode changes.
## FRED API (St. Louis Fed) — Brent crude prices
- Series: `DCOILBRENTEU` (daily Brent spot price, USD/barrel)
- Endpoint: `GET https://api.stlouisfed.org/fred/series/observations`
- Params: `series_id=DCOILBRENTEU`, `sort_order=desc`, `limit=30`, `file_type=json`
- Free API key required — stored as `FRED_API_KEY` in `.env`
- Handled by `OilPriceService::fetchBrentPrices()`
- Fetched daily at 7am via `oil:predict --fetch` scheduler command
- FRED uses `"."` as a placeholder for non-trading days (weekends/holidays) — filtered out before insert
- Stored in `brent_prices` table, upserted on `date` primary key
## Anthropic API — oil price direction prediction
- Endpoint: `POST https://api.anthropic.com/v1/messages`
- Model: `claude-haiku-4-5-20251001` (configurable via `ANTHROPIC_MODEL` in `.env`)
- Key stored as `ANTHROPIC_API_KEY` in `.env`
- Handled by `OilPriceService::generateLlmPrediction()`
- Called once daily after FRED fetch — sends last 30 days of Brent prices + pre-computed EWMA context
- Response must be JSON: `{"direction": "rising|falling|flat", "confidence": 0-85, "reasoning": "..."}`
- Model sometimes wraps JSON in markdown code fences — these are stripped before `json_decode`
- Confidence is capped at 85 regardless of what the model returns
- On any failure (API error, malformed JSON, invalid direction) → falls back to EWMA silently
- Result stored in `price_predictions` table with `source = 'llm'`
**EWMA fallback (`OilPriceService::generateEwmaPrediction()`):**
- Compares 3-day EWMA vs 7-day EWMA on chronological Brent price data
- Threshold: ±1.5% change → rising/falling; below → flat
- Confidence capped at 65 (simpler model)
- Used when: no `ANTHROPIC_API_KEY` set, or LLM call fails
- Result stored in `price_predictions` table with `source = 'ewma'`
## OneSignal — push notifications

View File

@@ -10,7 +10,7 @@ This keeps the app API-extractable later without a rewrite.
```
app/
├── Console/Commands/ # Scheduler commands (PollFuelPrices, RunScoringEngine)
├── Console/Commands/ # Scheduler commands (PollFuelPrices, PredictOilPrices, RunScoringEngine)
├── Http/Controllers/ # Minimal — auth + Stripe webhook only
├── Livewire/ # Classic two-file Livewire components
├── Models/ # Eloquent models
@@ -20,7 +20,11 @@ app/
│ ├── AlertScoringService.php # Fill-up timing recommendation engine
│ ├── StationTaggingService.php # Supermarket brand detection
│ ├── NotificationDispatchService.php # Tier-aware notification routing
── SubscriptionService.php # Cashier/tier helpers
── SubscriptionService.php # Cashier/tier helpers
│ ├── PostcodeService.php # Resolves postcodes/outcodes/place names → lat/lng
│ ├── OilPriceService.php # FRED fetch + EWMA/LLM Brent crude prediction
│ ├── LocationResult.php # DTO returned by PostcodeService
│ └── ApiLogger.php # Wraps all outbound HTTP calls, logs to api_logs
└── Jobs/ # Queued jobs (dispatch notifications per user)
resources/views/

View File

@@ -91,6 +91,31 @@ INDEX (user_id, expires_at)
```
OTP codes expire after 10 minutes. Mark `used_at` on success — never delete rows.
### brent_prices (daily Brent crude from FRED)
```
date DATE PRIMARY KEY — trading day (no weekends/holidays)
price_usd DECIMAL(8,2) — spot price USD per barrel
```
Populated daily by `OilPriceService::fetchBrentPrices()` via FRED API.
FRED returns `"."` for non-trading days — these are filtered out before insert.
Upserted on refetch so duplicate dates never occur.
### price_predictions (oil price direction forecast)
```
id BIGINT UNSIGNED AUTO_INCREMENT
predicted_for DATE — the date this prediction covers
source ENUM('llm','ewma') — which method generated it
direction ENUM('rising','falling','flat')
confidence TINYINT — 0100 (LLM max 85, EWMA max 65)
reasoning TEXT NULLABLE — plain-English explanation
generated_at DATETIME
INDEX (predicted_for, source)
```
Generated daily by `OilPriceService::generatePrediction()`.
LLM (Anthropic) is tried first; EWMA is used as fallback if LLM fails or key not set.
Signal 4 in AlertScoringService reads from this table — never from brent_prices directly.
## Supermarket brands (StationTaggingService)
Match station `name` (case-insensitive) against:

View File

@@ -32,8 +32,11 @@ Never guess — stay silent (no_signal) when signals conflict or data is insuffi
- Weight: 15 points max
### Signal 4 — Brent crude direction (LOW WEIGHT)
- Fetched daily from FRED API, stored in a simple `brent_prices` table
- 5-day trend: rising ≥ 3% → mild fill_up pressure; falling ≥ 3% → mild wait
- Read from `price_predictions` table — never query `brent_prices` directly in scoring
- `OilPriceService::generatePrediction()` runs daily at 7am and writes the prediction
- LLM (`source = 'llm'`) is preferred; EWMA (`source = 'ewma'`) is the fallback
- Direction `rising` → mild fill_up pressure; `falling` → mild wait; `flat` → no signal
- Points awarded proportionally to confidence: `(confidence / 100) * 10`
- Weight: 10 points max
## Confidence thresholds