feat: add Filament admin panel with migrations and design spec
- 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:
@@ -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
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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 — 0–100 (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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user