Files
fuel-alert/.claude/rules/architecture.md
Ovidiu U df4ebdb7c6
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
Remove Livewire docs, add Vue SPA frontend docs, document log-masking runbook
- 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`
2026-06-10 10:55:34 +01:00

6.1 KiB

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.phpapp/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.