6. Runtime View
This chapter describes the runtime behavior of Tanker24 by documenting the most important usage scenarios as sequence diagrams. Each scenario illustrates the interaction between the user, frontend, backend, database, and external systems.
6.1 Scenario: Search Nearby Gas Stations (UC1, UC7)
The user opens the map page, selects a location, and the system returns nearby gas stations with current prices.
User Frontend Backend PostgreSQL Tankerk.nig API User User Frontend (SvelteKit + Leaflet) Frontend (SvelteKit + Leaflet) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Tankerkönig API Tankerkönig API Opens the map page (/map) Gets user's current position (Geolocation API or map click) GET /api/v0/stations/nearby?latitude=X&longitude=Y (Authorization: Bearer <JWT>) Validates JWT token via fastapi-users Checks rate limit (10/min per user) SlowAPI/memory rate limiter SELECT * FROM stations WHERE cache_lat ≈ X AND cache_lon ≈ Y AND cache_radius = radius AND cached_at > now - 30 min Cached stations (or empty) alt [Cache hit (fresh data)] Data is recent enough [Cache miss (stale or no data)] Acquires token from global rate limiter (100 req/min to Tankerkönig) GET list.php?lat=X&lng=Y&rad=5&type=all&apikey=KEY JSON: { "ok": true, "stations": [...] } UPSERT stations with cache metadata (lat, lon, radius, cached_at timestamp) DELETE stale stations not in new result set JSON: [{tankerkoenig_id, name, brand, diesel, e5, e10, distance, ...}] Renders markers on Leaflet map with fuel prices in popups Map is displayed with station markers User Frontend Backend PostgreSQL Tankerk.nig API User User Frontend (SvelteKit + Leaflet) Frontend (SvelteKit + Leaflet) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Tankerkönig API Tankerkönig API Opens the map page (/map) Gets user's current position (Geolocation API or map click) GET /api/v0/stations/nearby?latitude=X&longitude=Y (Authorization: Bearer <JWT>) Validates JWT token via fastapi-users Checks rate limit (10/min per user) SlowAPI/memory rate limiter SELECT * FROM stations WHERE cache_lat ≈ X AND cache_lon ≈ Y AND cache_radius = radius AND cached_at > now - 30 min Cached stations (or empty) alt [Cache hit (fresh data)] Data is recent enough [Cache miss (stale or no data)] Acquires token from global rate limiter (100 req/min to Tankerkönig) GET list.php?lat=X&lng=Y&rad=5&type=all&apikey=KEY JSON: { "ok": true, "stations": [...] } UPSERT stations with cache metadata (lat, lon, radius, cached_at timestamp) DELETE stale stations not in new result set JSON: [{tankerkoenig_id, name, brand, diesel, e5, e10, distance, ...}] Renders markers on Leaflet map with fuel prices in popups Map is displayed with station markers
Steps:
1. User navigates to /map; the SvelteKit frontend loads the Leaflet map component.
2. The user selects a location (via geolocation or map click).
3. Frontend sends GET /api/v0/stations/nearby?latitude=X&longitude=Y with the JWT token in the Authorization header.
4. Backend validates the JWT and checks the user-based rate limit (10 requests per minute).
5. Backend queries the stations cache table for matching entries within the configured tolerance (0.01 km) and expiry (30 minutes).
6. If cache hit: cached stations are returned directly.
7. If cache miss: the backend acquires a token from the global Tankerkönig rate limiter (100 requests/minute), then calls the Tankerkönig list.php API. Results are upserted into the cache with metadata (search coordinates, radius, timestamp). Stale entries from previous searches are cleaned up.
8. Backend returns the station list as JSON with fuel prices and distances.
9. Frontend renders station markers on the Leaflet map with price information.
Error Handling:
- If the Tankerkönig API is unavailable, the service catches the exception, logs it, and returns an empty list (graceful degradation).
- If the user exceeds the rate limit, a 429 Too Many Requests response is returned.
- If latitude or longitude are out of valid range, a 400 Bad Request is returned.
6.2 Scenario: User Registration (UC5)
A new user registers with an invitation key.
User Frontend Backend PostgreSQL User User Frontend (SvelteKit) Frontend (SvelteKit) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Navigates to /register fills form (email, password, forename, surname, invitation key) Client-side validation (password rules, required fields) POST /api/v0/auth/register {email, password, forename, surname, invitation_key} Validates password policy: - Min 8 chars - Upper + lower + digit + special char Checks if email already exists (UserAlreadyExists → 400) SELECT * FROM invitation_keys WHERE key = <key> InvitationKey (or None) alt [Invalid or missing invitation key] 400 Invalid invitation key [Valid key] Hashes password with passlib INSERT INTO users (... invitation_key_id=...) Logs "User registered: id=X email=Y" 201 Created {id, email, forename, surname, is_active, ...} Stores success message, redirects to /login "Registration successful. Please log in." User Frontend Backend PostgreSQL User User Frontend (SvelteKit) Frontend (SvelteKit) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Navigates to /register fills form (email, password, forename, surname, invitation key) Client-side validation (password rules, required fields) POST /api/v0/auth/register {email, password, forename, surname, invitation_key} Validates password policy: - Min 8 chars - Upper + lower + digit + special char Checks if email already exists (UserAlreadyExists → 400) SELECT * FROM invitation_keys WHERE key = <key> InvitationKey (or None) alt [Invalid or missing invitation key] 400 Invalid invitation key [Valid key] Hashes password with passlib INSERT INTO users (... invitation_key_id=...) Logs "User registered: id=X email=Y" 201 Created {id, email, forename, surname, is_active, ...} Stores success message, redirects to /login "Registration successful. Please log in."
6.3 Scenario: Record Fuel Filling (UC3)
An authenticated user records a fuel filling event for one of their cars.
User Frontend Backend PostgreSQL User User Frontend (SvelteKit) Frontend (SvelteKit) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Opens account page, selects a car, fills form (litres, price/litre, mileage, fuel type) POST /api/v0/fillings/create (Authorization: Bearer <JWT>) Validates JWT, extracts user ID Check if the car already exists or create a new one Car record (or 404) Get FuelTypes from database for foreign key FuelType (diesel/e5/e10) Create history record with reference to car and fuel type 200 OK Updates history list on account page "Filling recorded. Total: €XX.XX" User Frontend Backend PostgreSQL User User Frontend (SvelteKit) Frontend (SvelteKit) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Opens account page, selects a car, fills form (litres, price/litre, mileage, fuel type) POST /api/v0/fillings/create (Authorization: Bearer <JWT>) Validates JWT, extracts user ID Check if the car already exists or create a new one Car record (or 404) Get FuelTypes from database for foreign key FuelType (diesel/e5/e10) Create history record with reference to car and fuel type 200 OK Updates history list on account page "Filling recorded. Total: €XX.XX"
6.4 Scenario: Export User Data (UC4)
The authenticated user exports their fueling history as JSON or CSV.
User Frontend Backend PostgreSQL User User Frontend (SvelteKit) Frontend (SvelteKit) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Clicks "Export as JSON" or "Export as CSV" alt [JSON Export] GET /api/v0/export/json (Authorization: Bearer <JWT>) Validates JWT, extracts user ID SELECT cars WHERE owner_id = user_id List of cars loop [For each car] SELECT history_records WHERE car_id = X JOIN fuel_types List of history records with fuel type names NestedExportDataService builds JSON: [{car, history: [{record, fuel_type}]}] JSONResponse with Content-Disposition attachment [CSV Export] GET /api/v0/export/csv (Authorization: Bearer <JWT>) Validates JWT, extracts user ID SELECT cars, history, fuel_types (same as JSON) Data FlatExportDataService flattens to CSV rows with car_id repeated per row (using semicolon delimiter) StreamingResponse (text/csv) with Content-Disposition attachment Triggers browser download File is downloaded User Frontend Backend PostgreSQL User User Frontend (SvelteKit) Frontend (SvelteKit) Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL Clicks "Export as JSON" or "Export as CSV" alt [JSON Export] GET /api/v0/export/json (Authorization: Bearer <JWT>) Validates JWT, extracts user ID SELECT cars WHERE owner_id = user_id List of cars loop [For each car] SELECT history_records WHERE car_id = X JOIN fuel_types List of history records with fuel type names NestedExportDataService builds JSON: [{car, history: [{record, fuel_type}]}] JSONResponse with Content-Disposition attachment [CSV Export] GET /api/v0/export/csv (Authorization: Bearer <JWT>) Validates JWT, extracts user ID SELECT cars, history, fuel_types (same as JSON) Data FlatExportDataService flattens to CSV rows with car_id repeated per row (using semicolon delimiter) StreamingResponse (text/csv) with Content-Disposition attachment Triggers browser download File is downloaded
6.5 Scenario: Application Startup
Docker Compose Backend .FastAPI. PostgreSQL Docker Compose Docker Compose Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL docker compose up -d (startup) setup_logging() Structured logging to stdout SQLAlchemy: WARNING (INFO in debug) init_db() SQLAlchemy: Base.metadata.create_all (Creates tables if not existing) CREATE TABLE IF NOT EXISTS (users, cars, history_records, stations, invitation_keys, fuel_types) sync_invitation_keys() Reads INVITATION_KEYS from env SELECT * FROM invitation_keys Existing DB keys Key diff: env vs DB DELETE keys not in env (unset invitation_key_id for affected users) INSERT new keys from env "Application startup complete" Ready to serve requests on port 8000 Docker Compose Backend .FastAPI. PostgreSQL Docker Compose Docker Compose Backend (FastAPI) Backend (FastAPI) PostgreSQL PostgreSQL docker compose up -d (startup) setup_logging() Structured logging to stdout SQLAlchemy: WARNING (INFO in debug) init_db() SQLAlchemy: Base.metadata.create_all (Creates tables if not existing) CREATE TABLE IF NOT EXISTS (users, cars, history_records, stations, invitation_keys, fuel_types) sync_invitation_keys() Reads INVITATION_KEYS from env SELECT * FROM invitation_keys Existing DB keys Key diff: env vs DB DELETE keys not in env (unset invitation_key_id for affected users) INSERT new keys from env "Application startup complete" Ready to serve requests on port 8000
Startup Steps:
1. Docker Compose starts the backend container (after PostgreSQL is healthy).
2. lifespan context manager calls setup_logging() to configure structured logging.
3. init_db() runs Base.metadata.create_all to create any missing tables (idempotent).
4. sync_invitation_keys() reads the INVITATION_KEYS environment variable (comma-separated 32-char hex strings), diff against the DB, removes expired keys, and adds new ones. Users with deleted keys have their invitation_key_id set to NULL.
5. The application starts serving requests on port 8000.