- Delete `.claude/rules/livewire.md` (Livewire is vestigial – only starter-kit auth screens remain) - Add `.claude/rules/frontend.md` documenting Vue 3 SPA stack, router, composables, search-URL mirroring, and Leaflet integration - Add `docs/ops/nginx-log-masking.md` runbook for redacting lat/lng from Nginx access/error logs and PHP-FPM on IONOS VPS - Update `CLAUDE.md` and `architecture.md` to reflect Vue SPA as primary frontend, REST API as backend contract, and Livewire's reduced role - Note stale service names in legacy domain docs (`scoring.md`, `prediction.md`, `notifications.md`) now refactored into `Services/Forecasting/`, `Services/StationSearch/`, and `PlanFeatures`
119 lines
6.1 KiB
Markdown
119 lines
6.1 KiB
Markdown
# Architecture
|
|
|
|
## Shape: Vue SPA ⇄ REST API ⇄ fat Laravel Services
|
|
|
|
The frontend is a **Vue 3 single-page app** (`resources/js/`). It talks to the
|
|
backend exclusively over a **REST API** (`routes/api.php` →
|
|
`app/Http/Controllers/Api/`). Controllers stay thin; all business logic lives in
|
|
**Service classes** (`app/Services/`). Blade is reduced to a one-line SPA shell,
|
|
server-rendered legal pages, transactional emails, and the Filament admin panel.
|
|
|
|
```
|
|
Browser
|
|
└── Vue SPA (resources/js, mounted on #app via app.blade.php)
|
|
└── axios (resources/js/axios.js — baseURL '/api', cookie + XSRF auth)
|
|
└── REST API (routes/api.php → Http/Controllers/Api/*)
|
|
└── Services (app/Services/*) ← all business logic
|
|
└── Models / Jobs / Events / Notifications
|
|
```
|
|
|
|
## Core principle: fat Services, thin everything else
|
|
|
|
All business logic lives in Service classes. API controllers, console commands,
|
|
jobs, listeners, and Filament resources are thin orchestrators — they call
|
|
Services and return results. Never put domain logic in a controller or a Vue
|
|
component.
|
|
|
|
## Directory structure (actual)
|
|
|
|
```
|
|
app/
|
|
├── Console/Commands/ # Scheduler: PollFuelPrices, FetchOilPrices, BackfillOilPrices,
|
|
│ # RunLlmOverlay, EvaluateVolatilityRegime, ResolveForecastOutcomes,
|
|
│ # ArchiveOldPricesCommand, ImportBeisFuelPrices, ImportPostcodes
|
|
├── Http/
|
|
│ ├── Controllers/Api/ # AuthController, StationController, StatsController, UserController
|
|
│ ├── Controllers/ # BillingController (Stripe Checkout/Portal redirects)
|
|
│ ├── Middleware/ # VerifyApiKey (API-key gate), RequiresFeature (tier gate)
|
|
│ ├── Requests/ # Form requests
|
|
│ └── Resources/Api/ # StationResource (API output shaping)
|
|
├── Actions/Fortify/ # Fortify auth actions (CreateNewUser, …)
|
|
├── Services/ # ALL business logic — see below
|
|
├── Models/ # Eloquent models
|
|
├── Notifications/ # FuelPriceAlert + custom Channels (OneSignal, Vonage WA/SMS)
|
|
├── Jobs/ # DispatchUserNotificationJob, PollFuelPricesJob,
|
|
│ # SendScheduledWhatsAppJob, SendPaymentFailedReminderJob
|
|
├── Events/ # PricesUpdatedEvent
|
|
├── Listeners/ # HandleStripeWebhook (Cashier WebhookReceived)
|
|
├── Filament/ # Admin panel (Resources, Widgets) + Providers/Filament
|
|
├── Enums/ # FuelType, PlanTier, …
|
|
└── Livewire/Actions/ # Logout only — Livewire is otherwise vestigial
|
|
|
|
app/Services/
|
|
├── FuelPriceService.php # Fuel Finder API polling + storage
|
|
├── StationTaggingService.php # Supermarket brand detection
|
|
├── PostcodeService.php # postcode/outcode/place → lat/lng (+ LocationResult DTO)
|
|
├── PlanFeatures.php # Single source of tier-entitlement truth (see tiers.md)
|
|
├── HaversineQuery.php # Distance SQL helper
|
|
├── ApiLogger.php # Wraps outbound HTTP, logs to api_logs
|
|
├── StationSearch/ # StationSearchService (+ SearchCriteria, SearchResult DTOs)
|
|
├── BrentPriceSources/ # FRED + EIA Brent sources (+ BrentPriceFetcher)
|
|
└── Forecasting/ # Weekly forecast subsystem — WeeklyForecastService,
|
|
# LlmOverlayService, BacktestRunner, OutcomeResolver,
|
|
# VolatilityRegimeService, feature/leak tooling, …
|
|
|
|
resources/
|
|
├── js/ # Vue 3 SPA — see .claude/rules/frontend.md
|
|
└── views/
|
|
├── app.blade.php # SPA shell (mounts #app)
|
|
├── legal/ # Server-rendered legal pages
|
|
├── emails/ # Mailable templates
|
|
└── livewire/auth/ # Starter-kit Volt auth screens (left untouched)
|
|
|
|
routes/
|
|
├── web.php # SPA shell + catch-all, legal pages, billing redirects, server logout
|
|
└── api.php # REST API consumed by the SPA (see below)
|
|
```
|
|
|
|
> **Stale service names elsewhere.** Domain rule files (`scoring.md`,
|
|
> `prediction.md`, `notifications.md`) still name services that have since been
|
|
> refactored away — e.g. `AlertScoringService`, `OilPriceService`,
|
|
> `NationalFuelPredictionService`, `NotificationDispatchService`,
|
|
> `SubscriptionService`. Those concerns now live under `Services/Forecasting/`,
|
|
> `Services/StationSearch/`, and `PlanFeatures`. Always verify a service name
|
|
> against `app/Services/` before trusting it from those files.
|
|
|
|
## Service class conventions
|
|
|
|
- Constructor injection only — no facade usage inside Services
|
|
- Each Service has one responsibility — do not merge concerns
|
|
- Return typed DTOs or Eloquent collections — never raw arrays from Services
|
|
- Services never dispatch jobs directly — that's the controller/command/listener's job
|
|
|
|
## The REST API
|
|
|
|
`routes/api.php` is the SPA's backend. Three access tiers:
|
|
|
|
- **Public** (no auth): `POST /api/auth/register`, `POST /api/auth/login`,
|
|
`GET /api/auth/me`, `GET /api/fuel-types`, `GET /api/stats/live`
|
|
- **API-key** (`VerifyApiKey` + `throttle:60,1`): `GET /api/stations`,
|
|
`GET /api/stats/searches`
|
|
- **Sanctum** (`auth:sanctum`): `POST /api/auth/logout`, `/api/user/*`
|
|
(preferences, saved stations, profile, password, delete account)
|
|
|
|
Conventions:
|
|
|
|
- Shape responses with API Resources (`app/Http/Resources/Api/`), never raw arrays.
|
|
- The fuel **prediction is embedded in the `/api/stations` response** under the
|
|
`prediction` key — there is no standalone prediction endpoint (see `prediction.md`).
|
|
- Tier-gate routes with the `feature` middleware (`RequiresFeature`); never
|
|
inline entitlement checks (see `tiers.md`).
|
|
- Auth is cookie/session via Sanctum SPA mode (axios sends XSRF + credentials);
|
|
the API-key gate protects the expensive station search from scraping.
|
|
|
|
## Frontend
|
|
|
|
The Vue SPA is documented in `.claude/rules/frontend.md`. Do **not** build new
|
|
Livewire components — Livewire/Flux remain only for the starter-kit auth screens
|
|
and the `Logout` action.
|