diff --git a/.claude/mockup/homepage.html b/.claude/mockup/homepage.html new file mode 100644 index 0000000..da37ecc --- /dev/null +++ b/.claude/mockup/homepage.html @@ -0,0 +1,420 @@ + + + + + + FuelAlert | Save Smart on Petrol & Diesel + + + + + + + + +
+ + + + +
+
+
+
+ + Save up to £250/year on fuel +
+

+ Stop Overpaying
for Fuel. +

+

+ Join 50,000+ UK drivers using real-time insights to find the cheapest petrol and time their fill-ups perfectly. +

+ +
+
+ + +
+ +
+ +
+
+ User + User + User +
+ "Saved me £12 on my first tank!" +
+
+ + +
+
+
+
+
+
+ FuelAlert +
+
SW1A 1AA
+
+ +
+

Recommendation

+

Fill up now

+
+
+
+
+ +
+
+ Tesco Superstore + 142.9p +
+
+ Shell V-Power + 148.9p +
+
+
+
+
+
+ + +
+
+
+

Smart Savings in 3 Steps

+

Stop guessing when to fill up. Our engine analyzes thousands of data points daily to save you money.

+
+ +
+
+
+ +
+

1. Search

+

Enter your postcode or location to find every forecourt within a 5-20 mile radius instantly.

+
+
+
+ +
+

2. Get Advice

+

Our AI compares local prices against national wholesale trends to give you a Fill Up/Wait recommendation.

+
+
+
+ +
+

3. Fill Up Smart

+

Navigate to the cheapest station and fill up with confidence knowing you've secured the best price.

+
+
+
+
+ + +
+
+
+
+
+
+ +

Real-Time Prices

+

Verified daily prices from thousands of UK forecourts.

+
+
+ +

Timing Predictions

+

Proprietary 14-day forecasts for petrol and diesel trends.

+
+
+ +

Supermarket Anchors

+

Track local supermarkets to find the absolute lowest base price.

+
+
+ +

Smart Price Alerts

+

Get notified when local prices drop below your set target.

+
+
+
+ +
+

The ultimate fuel companion.

+

Whether you're a daily commuter, a delivery professional, or just planning a weekend road trip, FuelAlert gives you the edge at the pump.

+
    +
  • + + Coverage for 98% of UK Forecourts +
  • +
  • + + Hyper-local Map Visualization +
  • +
  • + + Historic Price Benchmarking +
  • +
+ +
+
+
+
+ + +
+
+
+

Pricing for every driver

+

Save hundreds for less than the cost of a coffee.

+
+ +
+ +
+
+

Free

+
+ £0 + /mo +
+
+
    +
  • Basic Search
  • +
  • Daily Updates
  • +
  • No Alerts
  • +
+ Get Started +
+ + +
+
+

Basic

+
+ £0.99 + /mo +
+
+
    +
  • Ad-free Experience
  • +
  • 14-day Trend Data
  • +
  • 3 Daily Price Alerts
  • +
+ Select Basic +
+ + +
+
Most Popular
+
+

Plus

+
+ £2.49 + /mo +
+
+
    +
  • Supermarket Anchor
  • +
  • Priority Price Alerts
  • +
  • Multi-location tracking
  • +
+ Join Plus +
+ + +
+
+

Pro

+
+ £3.99 + /mo +
+
+
    +
  • AI Price Predictions
  • +
  • Multi-Vehicle Fleet
  • +
  • Exportable Price History
  • +
+ Go Pro +
+
+
+
+ + +
+
+
+
+

Loved by commuters.

+
+ + + + + +
+

Join thousands of UK drivers saving every single month.

+
+
+
+ "I used to just go to the station on my way home. Now I check FuelAlert and realize there's a station 2 miles away that's 5p cheaper! Over a month, it adds up to a free tank per year." +
+ +
+

James R.

+

Daily Commuter

+
+
+
+
+ "The predictions are eerily accurate. I was going to fill up Friday, but FuelAlert said 'Hold on' for Monday. Sure enough, prices dropped at my local Tesco by 3p. Brilliant." +
+ +
+

Sarah M.

+

Delivery Driver

+
+
+
+
+
+
+
+ + +
+
+

Ready to outsmart the pumps?

+

Sign up for free today and never pay over the odds for fuel again.

+ +
+
+ + + +
+ + \ No newline at end of file diff --git a/.claude/rules/api-data.md b/.claude/rules/api-data.md index 53be7b6..b66c58d 100644 --- a/.claude/rules/api-data.md +++ b/.claude/rules/api-data.md @@ -34,344 +34,25 @@ Content-Type: application/json - Use the `refresh_token` to regenerate before expiry if needed - Include token in every API request: `Authorization: Bearer {token}` -Fuel Finder REST API -The Fuel Finder API is a REST API that gives a simple, consistent way to request, create and update data. REST stands for Representational State Transfer which is an architectural software style in which standard HTTP request methods are used to retrieve and modify representations of data. This is identical to the process of retrieving a web page or submitting a web form. - -Representational State Transfer (REST) web services -In a RESTful API, each data resource has a unique URL and is manipulated using standard HTTP verbs such as: - -GET to request a resource -POST to create a resource (not used for read-only endpoints) -PUT to change a resource (not used for read-only endpoints) -DELETE to remove a resource (not used for read-only endpoints) -Example: request a price resource -GET: https://api.fuelfinder.service.gov.uk/v1/prices/GB-12345 HTTP/1.1 -The request uses GET and does not include a request body. - -In a RESTful API, a resource is modified by POSTing a revised resource representation, in this case JSON, to the same resource URL: - -POST: https://api.fuelfinder.service.gov.uk/v1/ -Content-Type: text/json -{ -"CustomerName": "Joe Bloggs", -"Address": "", -"etc": etc -} -REST builds on the features of HTTP. Because each resource has a globally unique URL and can be fetched with GET, REST APIs can benefit from existing network components such as caches and proxies. - -The JSON data format -Responses use JSON (JavaScript Object Notation). JSON is a compact, widely used format for storing and exchanging data. Most programming languages support JSON, which makes it well suited to HTTP-based API services. - #### Endpoints -- Endpoints -- Method Endpoint -- GET Fetch all PFS fuel prices -- GET Fetch incremental PFS fuel prices -- GET Fetch PFS information -- GET Fetch incremental PFS information +- `GET /api/v1/pfs/fuel-prices?batch-number` — all/incremental station prices +- `GET /api/v1/pfs?batch-number` — all/incremental station metadata +**Fuel prices response fields** (array of stations): +- `node_id` — station identifier +- `trading_name` — station name +- `fuel_prices[]` — array of `{fuel_type, price, price_last_updated, price_change_effective_timestamp}` +- Fuel types: `E5`, `E10`, `B7_STANDARD`, `B7_PREMIUM`, `B10`, `HVO` +- Price is a float (e.g. `159.9` = 159.9p) — multiply × 100 and store as integer pence -``` -https://www.fuel-finder.service.gov.uk/api/v1/pfs/fuel-prices?batch-number -[ - { - "node_id": "0028acef5f3afc41c7e7d56fb285a940dfb64d6fea01cb4accd79c148321112d", - "public_phone_number": null, - "trading_name": "Alex Fuel Station", - "fuel_prices": [ - { - "fuel_type": "E5", - "price": 159.9, - "price_last_updated": "2026-02-17T16:03:04.938Z", - "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z" - }, - { - "fuel_type": "E10", - "price": 132.9, - "price_last_updated": "2026-02-17T16:03:04.938Z", - "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z" - }, - { - "fuel_type": "B7_STANDARD", - "price": 141.9, - "price_last_updated": "2026-02-17T16:03:04.938Z", - "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z" - } - ] - }, - { - "node_id": "01da92125c3751767044d06b202f45da5933f0e16e256fa3e98a16af8386308d", - "public_phone_number": "", - "trading_name": "Star Garage", - "fuel_prices": [ - { - "fuel_type": "E5", - "price": 159.9, - "price_last_updated": "2026-02-17T16:03:04.938Z", - "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z" - } - ] - }, - { - "node_id": "020592cd81196efdb61ab2135f837ddf3d2bee4e64346810270f0b088b4c09d8", - "public_phone_number": null, - "trading_name": "Blue Hills Fuel Station", - "fuel_prices": [ - { - "fuel_type": "E5", - "price": 159.9, - "price_last_updated": "2026-02-17T16:03:04.938Z", - "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z" - }, - { - "fuel_type": "B7_STANDARD", - "price": 141.9, - "price_last_updated": "2026-02-17T16:03:04.938Z", - "price_change_effective_timestamp": "2026-02-17T16:00:00.000Z" - } - ] - } -] -``` - -``` -https://www.fuel-finder.service.gov.uk/api/v1/pfs?batch-number=1 -[ - { - "node_id": "9b275ab576eeba3c6677984be15ee22a74e54fdfe8e5ea700e84a03178dc4ac1", - "public_phone_number": null, - "trading_name": "TEST", - "is_same_trading_and_brand_name": true, - "brand_name": "TEST", - "temporary_closure": false, - "permanent_closure": false, - "permanent_closure_date": null, - "is_motorway_service_station": false, - "is_supermarket_service_station": false, - "location": { - "address_line_1": "HALL & WOODHOUSE, TAPLOW BOATYARD, MILL LANE, TAPLOW, MAIDENHEAD, SL6 0AA", - "address_line_2": null, - "city": "MAIDENHEAD", - "country": "England", - "county": null, - "postcode": "SL6 0AA", - "latitude": 51.5268585, - "longitude": -0.700361 - }, - "amenities": [ - "water_filling" - ], - "opening_times": { - "usual_days": { - "monday": { - "open": "00:00:00", - "close": "00:00:00", - "is_24_hours": false - }, - "tuesday": { - "open": "00:00:00", - "close": "00:00:00", - "is_24_hours": false - }, - "wednesday": { - "open": "00:00:00", - "close": "00:00:00", - "is_24_hours": false - }, - "thursday": { - "open": "00:00:00", - "close": "00:00:00", - "is_24_hours": false - }, - "friday": { - "open": "00:00:00", - "close": "00:00:00", - "is_24_hours": false - }, - "saturday": { - "open": "00:00:00", - "close": "00:00:00", - "is_24_hours": false - }, - "sunday": { - "open": "00:00:00", - "close": "23:59:00", - "is_24_hours": true - } - }, - "bank_holiday": { - "type": "bank holiday", - "open_time": "00:00:00", - "close_time": "00:00:00", - "is_24_hours": false - } - }, - "fuel_types": [ - "E10", - "E5", - "HVO", - "B10" - ] - }, - { - "node_id": "4fd9a4c6b48358b9b5c95989fba100fdcbb87c9e909ed4ce1ad96f64ffb8b56a", - "public_phone_number": "+44 7723608248", - "trading_name": "TEST FORECOURT 1", - "is_same_trading_and_brand_name": true, - "brand_name": "TEXACO ONE", - "temporary_closure": false, - "permanent_closure": null, - "permanent_closure_date": null, - "is_motorway_service_station": false, - "is_supermarket_service_station": false, - "location": { - "address_line_1": "NEWPORT", - "address_line_2": "", - "city": "BROUGH", - "country": "ENGLAND", - "county": "EAST YORKSHIRE", - "postcode": "HU15 2RD", - "latitude": 51.258503, - "longitude": -3.417567 - }, - "amenities": [ - "adblue_packaged", - "adblue_pumps", - "car_wash", - "customer_toilets" - ], - "opening_times": { - "usual_days": { - "monday": { - "open": "06:00:01", - "close": "23:00:01", - "is_24_hours": false - }, - "tuesday": { - "open": "06:00:01", - "close": "23:00:01", - "is_24_hours": false - }, - "wednesday": { - "open": "06:00:01", - "close": "23:00:01", - "is_24_hours": false - }, - "thursday": { - "open": "06:00:01", - "close": "23:00:01", - "is_24_hours": false - }, - "friday": { - "open": "06:00:01", - "close": "23:00:01", - "is_24_hours": false - }, - "saturday": { - "open": "06:00:01", - "close": "23:00:01", - "is_24_hours": false - }, - "sunday": { - "open": "06:00:01", - "close": "23:00:01", - "is_24_hours": false - } - }, - "bank_holiday": { - "type": "standard", - "open_time": "06:00:01", - "close_time": "23:00:01", - "is_24_hours": false - } - }, - "fuel_types": [ - "B10" - ] - }, - { - "node_id": "91bdda1c07fa05110a31639cc66932f9ed8bd388d4f6be542a423365bcfd53e1", - "public_phone_number": "+442071930000", - "trading_name": "SUPERFUEL LOUGHBOROUGH 12", - "is_same_trading_and_brand_name": true, - "brand_name": "SUPERFUEL STATION 4", - "temporary_closure": false, - "permanent_closure": null, - "permanent_closure_date": null, - "is_motorway_service_station": false, - "is_supermarket_service_station": false, - "location": { - "address_line_1": "14 LONDON ROAD", - "address_line_2": "FUELVILLE", - "city": "LOUGHBOROUGH", - "country": "ENGLAND", - "county": "LEICESTERSHIRE", - "postcode": "LE11 9AA", - "latitude": 50.503343, - "longitude": -2.12444 - }, - "amenities": [ - "adblue_packaged", - "adblue_pumps", - "car_wash", - "customer_toilets", - "water_filling" - ], - "opening_times": { - "usual_days": { - "monday": { - "open": "06:00:00", - "close": "22:00:00", - "is_24_hours": false - }, - "tuesday": { - "open": "06:00:00", - "close": "22:00:00", - "is_24_hours": false - }, - "wednesday": { - "open": "06:00:00", - "close": "22:00:00", - "is_24_hours": false - }, - "thursday": { - "open": "06:00:00", - "close": "22:00:00", - "is_24_hours": false - }, - "friday": { - "open": "06:00:00", - "close": "22:00:00", - "is_24_hours": false - }, - "saturday": { - "open": "06:00:00", - "close": "22:00:00", - "is_24_hours": false - }, - "sunday": { - "open": "06:00:00", - "close": "22:00:00", - "is_24_hours": false - } - }, - "bank_holiday": { - "type": "standard", - "open_time": "08:00:00", - "close_time": "20:00:00", - "is_24_hours": false - } - }, - "fuel_types": [ - "E5", - "HVO", - "B10", - "B7_PREMIUM", - "B7_STANDARD" - ] - } -] -``` +**Station metadata response fields** (array of stations): +- `node_id`, `trading_name`, `brand_name` +- `is_supermarket_service_station`, `is_motorway_service_station` +- `temporary_closure`, `permanent_closure` +- `location` — `{address_line_1, city, postcode, latitude, longitude}` +- `amenities` — string array (e.g. `car_wash`, `adblue_pumps`) +- `fuel_types` — string array of available fuel types +- `opening_times` — per-day open/close times (not used in scoring) ### FuelPriceService responsibilities 1. Fetch OAuth token (cache it) diff --git a/.claude/rules/scoring.md b/.claude/rules/scoring.md index 0cc1345..9f757f0 100644 --- a/.claude/rules/scoring.md +++ b/.claude/rules/scoring.md @@ -8,52 +8,30 @@ Never guess — stay silent (no_signal) when signals conflict or data is insuffi ## The 5 signals (in priority order) -### Signal 1 — Local price trend (HIGHEST WEIGHT) -- Query `station_prices` for user's nearest 5 stations (within 5km of user lat/lng) -- Use last 14 days of history for `e10` (or user's preferred fuel type) -- **Use linear regression, not rolling averages:** - - Run least-squares regression on `(recorded_at, price_pence)` pairs - - Calculate slope (pence/day) and R² (goodness of fit, 0–1) - - Only use the regression result if R² ≥ 0.5 — below that, data is too noisy - - Use adaptive lookback: try 5 days first (best signal on sharp moves), fall back to 14 days if R² < 0.5 -- **Falling**: slope ≤ -0.3p/day AND R² ≥ 0.5 → wait signal, points scale with slope magnitude -- **Rising**: slope ≥ +0.3p/day AND R² ≥ 0.5 → fill_up signal -- **Flat / noisy**: |slope| < 0.3 OR R² < 0.5 → no signal from this source -- Store slope, R², lookback_days, and data_points in signal output -- Weight: 40 points max +### Signal 1 — Local price trend (40 pts max) +- Nearest 5 stations within 5km; user's preferred fuel type +- Least-squares regression on `(recorded_at, price_pence)`; adaptive lookback: 5 days first, fall back to 14 if R² < 0.5 +- Slope ≤ -0.3p/day AND R² ≥ 0.5 → wait; slope ≥ +0.3p/day AND R² ≥ 0.5 → fill_up; otherwise no signal +- Store: slope, R², lookback_days, data_points -### Signal 2 — Supermarket anchor effect (HIGH WEIGHT) -- Find nearest supermarket station (is_supermarket = 1) within 10km -- Check if supermarket cut price in last 48 hours (> 1p drop) -- Check if nearest non-supermarket stations have NOT yet followed -- If supermarket cut AND independents haven't moved → strong wait signal -- Also check the inverse: if supermarket RAISED and independents haven't → mild fill_up -- Weight: 35 points max +### Signal 2 — Supermarket anchor effect (35 pts max) +- Nearest supermarket (is_supermarket = 1) within 10km +- Supermarket cut > 1p in last 48h AND independents haven't followed → wait +- Inverse (supermarket raised, independents haven't) → mild fill_up -### Signal 3 — Day-of-week pattern (MEDIUM WEIGHT — needs 8+ weeks data) -- Per station: average price by day-of-week over last 90 days -- Only activate if station has 56+ days of history -- If today is statistically 1.5p+ cheaper than weekly average → mild fill_up -- If today is statistically 1.5p+ more expensive → mild wait -- Weight: 15 points max +### Signal 3 — Day-of-week pattern (15 pts max) +- Requires 56+ days of station history; average price by day-of-week over last 90 days +- Today 1.5p+ below weekly average → mild fill_up; 1.5p+ above → mild wait -### Signal 4 — Brent crude direction (LOW WEIGHT) -- 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 +### Signal 4 — Brent crude direction (10 pts max) +- Read from `price_predictions` table only (never query `brent_prices` in scoring) +- LLM (`source='llm'`) preferred; EWMA fallback. Points = `(confidence / 100) * 10` +- `rising` → fill_up; `falling` → wait; `flat` → no signal -### Signal 5 — Price stickiness (CONFIDENCE MODIFIER) -- Per station: calculate average hold duration (days between price changes) from history -- Requires 30+ days of history to activate -- Use as a confidence modifier, not a directional signal: - - avg hold < 2 days → reduce overall confidence by 5 points (volatile, hard to predict) - - avg hold 2–4 days → neutral, no adjustment - - avg hold > 5 days → increase overall confidence by 5 points (predictable, sticky) -- Store avg_hold_days and data_points in signal output -- Applied after all other signals are summed (±5 points) +### Signal 5 — Price stickiness (confidence modifier, ±5 pts) +- Requires 30+ days history. Applied after all signals are summed. +- avg hold < 2 days → -5 pts; 2–4 days → 0; > 5 days → +5 pts +- Store: avg_hold_days, data_points ## Confidence thresholds @@ -99,18 +77,9 @@ Reason strings are stored in `scoring_results.signals` JSON and shown in the UI ## Data quality — anomaly rejection -The Fuel Finder API contains dirty data (live example: 1369.0p/litre in national index). -Reject a price record before storing or scoring if: -- `price_pence > 25000` (over 250p/litre — physically implausible for UK pump prices) -- `price_pence < 10000` (under 100p/litre — almost certainly a decimal entry error) -- Price changed by more than 20p in a single update from the same station - (flag for review, do not use in scoring) - -Log rejected records to an `anomalous_prices` table for monitoring. -Never let a dirty data point skew the regression slope or collapse R². +Reject before storing or scoring: `price_pence > 25000` or `< 10000`, or single-update change > 20p (flag for review). +Log to `anomalous_prices` table. Never let dirty data skew regression slope or collapse R². ## Accuracy self-tracking -After 3 days, check if `wait` recommendation was correct (prices did fall further). -Store outcome in `scoring_results` for future display: -"This signal has been right X% of the time in your area." \ No newline at end of file +After 3 days, check if `wait` was correct (prices fell further). Store outcome in `scoring_results` for display. \ No newline at end of file diff --git a/.claude/skills/fortify-development/SKILL.md b/.claude/skills/fortify-development/SKILL.md new file mode 100644 index 0000000..86322d9 --- /dev/null +++ b/.claude/skills/fortify-development/SKILL.md @@ -0,0 +1,131 @@ +--- +name: fortify-development +description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.' +license: MIT +metadata: + author: laravel +--- + +# Laravel Fortify Development + +Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications. + +## Documentation + +Use `search-docs` for detailed Laravel Fortify patterns and documentation. + +## Usage + +- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints +- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.) +- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field +- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.) +- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc. + +## Available Features + +Enable in `config/fortify.php` features array: + +- `Features::registration()` - User registration +- `Features::resetPasswords()` - Password reset via email +- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail` +- `Features::updateProfileInformation()` - Profile updates +- `Features::updatePasswords()` - Password changes +- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes + +> Use `search-docs` for feature configuration options and customization patterns. + +## Setup Workflows + +### Two-Factor Authentication Setup + +``` +- [ ] Add TwoFactorAuthenticatable trait to User model +- [ ] Enable feature in config/fortify.php +- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate +- [ ] Set up view callbacks in FortifyServiceProvider +- [ ] Create 2FA management UI +- [ ] Test QR code and recovery codes +``` + +> Use `search-docs` for TOTP implementation and recovery code handling patterns. + +### Email Verification Setup + +``` +- [ ] Enable emailVerification feature in config +- [ ] Implement MustVerifyEmail interface on User model +- [ ] Set up verifyEmailView callback +- [ ] Add verified middleware to protected routes +- [ ] Test verification email flow +``` + +> Use `search-docs` for MustVerifyEmail implementation patterns. + +### Password Reset Setup + +``` +- [ ] Enable resetPasswords feature in config +- [ ] Set up requestPasswordResetLinkView callback +- [ ] Set up resetPasswordView callback +- [ ] Define password.reset named route (if views disabled) +- [ ] Test reset email and link flow +``` + +> Use `search-docs` for custom password reset flow patterns. + +### SPA Authentication Setup + +``` +- [ ] Set 'views' => false in config/fortify.php +- [ ] Install and configure Laravel Sanctum for session-based SPA authentication +- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication) +- [ ] Set up CSRF token handling +- [ ] Test XHR authentication flows +``` + +> Use `search-docs` for integration and SPA authentication patterns. + +#### Two-Factor Authentication in SPA Mode + +When `views` is set to `false`, Fortify returns JSON responses instead of redirects. + +If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required: + +```json +{ + "two_factor": true +} +``` + +## Best Practices + +### Custom Authentication Logic + +Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects. + +### Registration Customization + +Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields. + +### Rate Limiting + +Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination. + +## Key Endpoints + +| Feature | Method | Endpoint | +|------------------------|----------|---------------------------------------------| +| Login | POST | `/login` | +| Logout | POST | `/logout` | +| Register | POST | `/register` | +| Password Reset Request | POST | `/forgot-password` | +| Password Reset | POST | `/reset-password` | +| Email Verify Notice | GET | `/email/verify` | +| Resend Verification | POST | `/email/verification-notification` | +| Password Confirm | POST | `/user/confirm-password` | +| Enable 2FA | POST | `/user/two-factor-authentication` | +| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` | +| 2FA Challenge | POST | `/two-factor-challenge` | +| Get QR Code | GET | `/user/two-factor-qr-code` | +| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` | \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 4f42b3c..fac0a15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,9 +51,11 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - php - 8.4 +- filament/filament (FILAMENT) - v5 - laravel/fortify (FORTIFY) - v1 - laravel/framework (LARAVEL) - v13 - laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 - livewire/flux (FLUXUI_FREE) - v2 - livewire/livewire (LIVEWIRE) - v4 - laravel/boost (BOOST) - v2 @@ -74,6 +76,7 @@ This project has domain-specific skills available. You MUST activate the relevan - `livewire-development` — Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, wire:sort, or islands, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, drag-and-drop, loading states, migrating from Livewire 3 to 4, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire. - `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code. - `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS. +- `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features. ## Conventions @@ -148,7 +151,7 @@ This project has domain-specific skills available. You MUST activate the relevan - Always use curly braces for control structures, even for single-line bodies. - Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. - Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` -- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. +- Follow existing application Enum naming conventions. - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. - Use array shape type definitions in PHPDoc blocks. @@ -219,4 +222,154 @@ This project has domain-specific skills available. You MUST activate the relevan - Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. - Do NOT delete tests without approval. +=== filament/filament rules === + +## Filament + +- Filament is used by this application. Follow the existing conventions for how and where it is implemented. +- Filament is a Server-Driven UI (SDUI) framework for Laravel that lets you define user interfaces in PHP using structured configuration objects. Built on Livewire, Alpine.js, and Tailwind CSS. +- Use the `search-docs` tool for official documentation on Artisan commands, code examples, testing, relationships, and idiomatic practices. If `search-docs` is unavailable, refer to https://filamentphp.com/docs. + +### Artisan + +- Always use Filament-specific Artisan commands to create files. Find available commands with the `list-artisan-commands` tool, or run `php artisan --help`. +- Always inspect required options before running a command, and always pass `--no-interaction`. + +### Patterns + +Always use static `make()` methods to initialize components. Most configuration methods accept a `Closure` for dynamic values. + +Use `Get $get` to read other form field values for conditional logic: + + +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TextInput; +use Filament\Schemas\Components\Utilities\Get; + +Select::make('type') + ->options(CompanyType::class) + ->required() + ->live(), + +TextInput::make('company_name') + ->required() + ->visible(fn (Get $get): bool => $get('type') === 'business'), + + + +Use `state()` with a `Closure` to compute derived column values: + + +use Filament\Tables\Columns\TextColumn; + +TextColumn::make('full_name') + ->state(fn (User $record): string => "{$record->first_name} {$record->last_name}"), + + + +Actions encapsulate a button with an optional modal form and logic: + + +use Filament\Actions\Action; +use Filament\Forms\Components\TextInput; + +Action::make('updateEmail') + ->schema([ + TextInput::make('email') + ->email() + ->required(), + ]) + ->action(fn (array $data, User $record) => $record->update($data)) + + + +### Testing + +Always authenticate before testing panel functionality. Filament uses Livewire, so use `Livewire::test()` or `livewire()` (available when `pestphp/pest-plugin-livewire` is in `composer.json`): + + +use function Pest\Livewire\livewire; + +livewire(ListUsers::class) + ->assertCanSeeTableRecords($users) + ->searchTable($users->first()->name) + ->assertCanSeeTableRecords($users->take(1)) + ->assertCanNotSeeTableRecords($users->skip(1)); + + + + +use function Pest\Laravel\assertDatabaseHas; +use function Pest\Livewire\livewire; + +livewire(CreateUser::class) + ->fillForm([ + 'name' => 'Test', + 'email' => 'test@example.com', + ]) + ->call('create') + ->assertNotified() + ->assertRedirect(); + +assertDatabaseHas(User::class, [ + 'name' => 'Test', + 'email' => 'test@example.com', +]); + + + + +use function Pest\Livewire\livewire; + +livewire(CreateUser::class) + ->fillForm([ + 'name' => null, + 'email' => 'invalid-email', + ]) + ->call('create') + ->assertHasFormErrors([ + 'name' => 'required', + 'email' => 'email', + ]) + ->assertNotNotified(); + + + + +use Filament\Actions\DeleteAction; +use function Pest\Livewire\livewire; + +livewire(EditUser::class, ['record' => $user->id]) + ->callAction(DeleteAction::class) + ->assertNotified() + ->assertRedirect(); + + + + +use Filament\Actions\Testing\TestAction; +use function Pest\Livewire\livewire; + +livewire(ListUsers::class) + ->callAction(TestAction::make('promote')->table($user), [ + 'role' => 'admin', + ]) + ->assertNotified(); + + + +### Correct Namespaces + +- Form fields (`TextInput`, `Select`, etc.): `Filament\Forms\Components\` +- Infolist entries (`TextEntry`, `IconEntry`, etc.): `Filament\Infolists\Components\` +- Layout components (`Grid`, `Section`, `Fieldset`, `Tabs`, `Wizard`, etc.): `Filament\Schemas\Components\` +- Schema utilities (`Get`, `Set`, etc.): `Filament\Schemas\Components\Utilities\` +- Actions (`DeleteAction`, `CreateAction`, etc.): `Filament\Actions\`. Never use `Filament\Tables\Actions\`, `Filament\Forms\Actions\`, or any other sub-namespace for actions. +- Icons: `Filament\Support\Icons\Heroicon` enum (e.g., `Heroicon::PencilSquare`) + +### Common Mistakes + +- **Never assume public file visibility.** File visibility is `private` by default. Always use `->visibility('public')` when public access is needed. +- **Never assume full-width layout.** `Grid`, `Section`, and `Fieldset` do not span all columns by default. Explicitly set column spans when needed. + diff --git a/boost.json b/boost.json index 169479f..1f290d3 100644 --- a/boost.json +++ b/boost.json @@ -5,12 +5,17 @@ "guidelines": true, "mcp": true, "nightwatch_mcp": false, + "packages": [ + "filament/filament", + "laravel/fortify" + ], "sail": false, "skills": [ "laravel-best-practices", "fluxui-development", "livewire-development", "pest-testing", - "tailwindcss-development" + "tailwindcss-development", + "fortify-development" ] } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 037dd1b..466b298 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1,5 +1,5 @@ - - + + {{ $slot }} - + \ No newline at end of file