diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 357b8ad7..3d204028 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -119,7 +119,7 @@ flutter build ios The app uses a Cloudflare Worker proxy to access Cambridge Beer Festival data: -- Base URL: `https://cbf-data-proxy.richard-alcock.workers.dev` +- Base URL: `https://data.cambeerfestival.app` - Endpoints: `/{festivalId}/{category}.json` (e.g., `/cbf2025/beer.json`) - Categories: `beer`, `cider`, `perry`, `mead`, `wine`, `international-beer`, `low-no` diff --git a/CLAUDE.md b/CLAUDE.md index dd316922..642ba236 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -489,7 +489,7 @@ Tests go in `test/` mirroring `lib/` structure: ## API Details -**Base URL**: `https://cbf-data-proxy.richard-alcock.workers.dev` +**Base URL**: `https://data.cambeerfestival.app` **Endpoints**: - `/{festivalId}/beer.json` - Beers diff --git a/README.md b/README.md index a8ff9ad5..ccb83409 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,8 @@ Coverage fails if it drops below 70% overall, helping maintain code quality. ## Data API This app uses the Cambridge Beer Festival data API via a Cloudflare Worker proxy: -- Base URL: `https://cbf-data-proxy.richard-alcock.workers.dev` -- Example: `https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025/beer.json` +- Base URL: `https://data.cambeerfestival.app` +- Example: `https://data.cambeerfestival.app/cbf2025/beer.json` ### Documentation diff --git a/cloudflare-worker/README.md b/cloudflare-worker/README.md index b0376f1c..0e7ca18f 100644 --- a/cloudflare-worker/README.md +++ b/cloudflare-worker/README.md @@ -116,7 +116,7 @@ curl https://cbf-data-proxy..workers.dev/festivals.json Once deployed, update `lib/models/festival.dart` to use the proxy URL: ```dart -dataBaseUrl: 'https://cbf-data-proxy..workers.dev/cbf2025', +dataBaseUrl: 'https://data.cambeerfestival.app/cbf2025', ``` Or configure it dynamically based on the platform (see `lib/services/beer_api_service.dart`). diff --git a/data/festivals.json b/data/festivals.json index b4cd7454..5b01a7c4 100644 --- a/data/festivals.json +++ b/data/festivals.json @@ -30,7 +30,7 @@ "apple-juice", "low-no" ], - "data_base_url": "https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025", + "data_base_url": "/cbf2025", "is_active": true }, { @@ -55,7 +55,7 @@ "beer", "low-no" ], - "data_base_url": "https://cbf-data-proxy.richard-alcock.workers.dev/cbfw2025", + "data_base_url": "/cbfw2025", "is_active": false }, { @@ -87,7 +87,7 @@ "wine", "low-no" ], - "data_base_url": "https://cbf-data-proxy.richard-alcock.workers.dev/cbf2024", + "data_base_url": "/cbf2024", "is_active": false } ], diff --git a/docs/CICD.md b/docs/CICD.md index 36c67df0..b416925f 100644 --- a/docs/CICD.md +++ b/docs/CICD.md @@ -217,7 +217,7 @@ Deploys Cloudflare Worker to production. 4. Copy `festivals.json` to worker directory 5. Deploy using `wrangler` via `cloudflare/wrangler-action@v3` -**Worker URL**: `https://cbf-data-proxy.richard-alcock.workers.dev` +**Worker URL**: `https://data.cambeerfestival.app` **Key Features:** - CORS proxy for Cambridge Beer Festival API @@ -343,7 +343,7 @@ See [GITHUB_SECRETS.md](GITHUB_SECRETS.md) for setup instructions. | **Staging** | App CI/CD | Push to `main` | `main.cambeerfestival.pages.dev` | Stable staging environment | | **PR Preview** | App CI/CD | Pull request | `.cambeerfestival.pages.dev` | Test PRs before merge | | **Development** | App CI/CD | Push to `main` | `richardthe3rd.github.io/...` | Alternative dev environment | -| **Worker** | Worker | Push to `main` | `cbf-data-proxy.richard-alcock.workers.dev` | API proxy | +| **Worker** | Worker | Push to `main` | `data.cambeerfestival.app` | API proxy | --- diff --git a/docs/CLOUDFLARE_PAGES_SETUP.md b/docs/CLOUDFLARE_PAGES_SETUP.md index 365dcbe5..61e8bea3 100644 --- a/docs/CLOUDFLARE_PAGES_SETUP.md +++ b/docs/CLOUDFLARE_PAGES_SETUP.md @@ -292,7 +292,7 @@ The app has three main deployment workflows: 3. GitHub Actions automatically: - Validates festivals.json against schema - Deploys worker to production - - Worker is live at `https://cbf-data-proxy.richard-alcock.workers.dev` + - Worker is live at `https://data.cambeerfestival.app` **On Pull Requests**: - Validates festivals.json @@ -364,7 +364,7 @@ After deployment, verify: Open browser console on `https://cambeerfestival.app` and run: ```javascript -fetch('https://cbf-data-proxy.richard-alcock.workers.dev/festivals.json') +fetch('https://data.cambeerfestival.app/festivals.json') .then(r => r.json()) .then(console.log) ``` diff --git a/docs/NETWORK_ALLOWLIST.md b/docs/NETWORK_ALLOWLIST.md index 62fa3185..4504711f 100644 --- a/docs/NETWORK_ALLOWLIST.md +++ b/docs/NETWORK_ALLOWLIST.md @@ -10,7 +10,7 @@ This document lists all the network domains that need to be allowed for GitHub C | `pub.dev` | Dart packages | `flutter pub get` | | `www.gstatic.com` | Flutter CanvasKit engine | Running web app | | `fonts.gstatic.com` | Google Fonts (Roboto) | Running web app | -| `cbf-data-proxy.richard-alcock.workers.dev` | Festival API (via Cloudflare Worker) | App runtime data | +| `data.cambeerfestival.app` | Festival API (via Cloudflare Worker) | App runtime data | | `data.cambridgebeerfestival.com` | Upstream festival data | App runtime data (proxied) | ## Detailed Breakdown @@ -68,14 +68,14 @@ Failed to load font Roboto at https://fonts.gstatic.com/s/roboto/v20/... ### 5. Festival Data API -**Domain:** `cbf-data-proxy.richard-alcock.workers.dev` +**Domain:** `data.cambeerfestival.app` The app's Cloudflare Worker proxy that serves festival data with CORS headers enabled for web access. ``` -https://cbf-data-proxy.richard-alcock.workers.dev/festivals.json -https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025/beer.json -https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025/cider.json +https://data.cambeerfestival.app/festivals.json +https://data.cambeerfestival.app/cbf2025/beer.json +https://data.cambeerfestival.app/cbf2025/cider.json # ... other beverage types ``` @@ -123,4 +123,4 @@ For **running the web app and taking screenshots**: For **full app functionality with live data**: -5. `cbf-data-proxy.richard-alcock.workers.dev` - Festival API +5. `data.cambeerfestival.app` - Festival API diff --git a/docs/PLAY_STORE_METADATA.md b/docs/PLAY_STORE_METADATA.md index af37b962..7db390da 100644 --- a/docs/PLAY_STORE_METADATA.md +++ b/docs/PLAY_STORE_METADATA.md @@ -168,7 +168,7 @@ The Cambridge Beer Festival app is designed with privacy in mind. We do not coll - The app fetches public festival data from our Cloudflare Worker API - These requests contain no personal information -- API endpoint: `https://cbf-data-proxy.richard-alcock.workers.dev` +- API endpoint: `https://data.cambeerfestival.app` ## Permissions diff --git a/docs/api/README.md b/docs/api/README.md index f2df4902..e503eaf9 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -25,27 +25,27 @@ node validate-festivals.js The Cambridge Beer Festival app fetches beverage data from a public API: -- **Base URL:** `https://data.cambridgebeerfestival.com` +- **Base URL:** `https://data.cambeerfestival.app` - **Format:** JSON - **Authentication:** None required (public API) ### URL Pattern ``` -https://data.cambridgebeerfestival.com/{festival_id}/{beverage_type}.json +https://data.cambeerfestival.app/{festival_id}/{beverage_type}.json ``` ### Examples ```bash # Beer data for CBF 2025 -curl https://data.cambridgebeerfestival.com/cbf2025/beer.json +curl https://data.cambeerfestival.app/cbf2025/beer.json # Cider data for CBF 2025 -curl https://data.cambridgebeerfestival.com/cbf2025/cider.json +curl https://data.cambeerfestival.app/cbf2025/cider.json # Winter festival beer data -curl https://data.cambridgebeerfestival.com/cbfw2025/beer.json +curl https://data.cambeerfestival.app/cbfw2025/beer.json ``` ## Schemas @@ -93,7 +93,7 @@ const ajv = new Ajv(); addFormats(ajv); const validate = ajv.compile(schema); -const data = await fetch('https://data.cambridgebeerfestival.com/cbf2025/beer.json') +const data = await fetch('https://data.cambeerfestival.app/cbf2025/beer.json') .then(r => r.json()); if (validate(data)) { @@ -113,7 +113,7 @@ import requests with open('beer-list-schema.json') as f: schema = json.load(f) -data = requests.get('https://data.cambridgebeerfestival.com/cbf2025/beer.json').json() +data = requests.get('https://data.cambeerfestival.app/cbf2025/beer.json').json() jsonschema.validate(data, schema) # Raises on error print('Valid!') diff --git a/docs/api/beer-list-schema.json b/docs/api/beer-list-schema.json index 9d2aa99b..9d775946 100644 --- a/docs/api/beer-list-schema.json +++ b/docs/api/beer-list-schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://data.cambridgebeerfestival.com/beer-list-schema.json", + "$id": "https://data.cambeerfestival.app/beer-list-schema.json", "title": "Beer Festival Data API Schema", "description": "JSON Schema for Cambridge Beer Festival beverage data API responses", "type": "object", diff --git a/docs/api/data-api-reference.md b/docs/api/data-api-reference.md index fd63a898..4f752026 100644 --- a/docs/api/data-api-reference.md +++ b/docs/api/data-api-reference.md @@ -1,7 +1,7 @@ # Cambridge Beer Festival Data API Reference **Last Updated:** 2025-11-29 -**API Base URL:** https://data.cambridgebeerfestival.com +**API Base URL:** https://data.cambeerfestival.app **Purpose:** Documentation for multi-festival and multi-beverage-type support --- @@ -27,14 +27,14 @@ The Cambridge Beer Festival Data API provides structured beverage and producer d ### Pattern ``` -https://data.cambridgebeerfestival.com/{festival_code}/{beverage_type}.json +https://data.cambeerfestival.app/{festival_code}/{beverage_type}.json ``` ### Examples ``` -https://data.cambridgebeerfestival.com/cbf2025/beer.json -https://data.cambridgebeerfestival.com/cbf2025/cider.json -https://data.cambridgebeerfestival.com/cbfw2025/beer.json +https://data.cambeerfestival.app/cbf2025/beer.json +https://data.cambeerfestival.app/cbf2025/cider.json +https://data.cambeerfestival.app/cbfw2025/beer.json ``` --- @@ -223,19 +223,19 @@ Common allergens: ### Fetch Beer Data for CBF 2025 ```bash -curl https://data.cambridgebeerfestival.com/cbf2025/beer.json +curl https://data.cambeerfestival.app/cbf2025/beer.json ``` ### Fetch Cider Data for CBF 2025 ```bash -curl https://data.cambridgebeerfestival.com/cbf2025/cider.json +curl https://data.cambeerfestival.app/cbf2025/cider.json ``` ### Fetch Winter Festival Beer Data ```bash -curl https://data.cambridgebeerfestival.com/cbfw2025/beer.json +curl https://data.cambeerfestival.app/cbfw2025/beer.json ``` --- diff --git a/docs/api/festival-registry-schema.json b/docs/api/festival-registry-schema.json index 2cdc8fb4..4c875cbf 100644 --- a/docs/api/festival-registry-schema.json +++ b/docs/api/festival-registry-schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://data.cambridgebeerfestival.com/festivals-schema.json", + "$id": "https://data.cambeerfestival.app/festivals-schema.json", "title": "Festivals Configuration Schema", "description": "JSON Schema for Cambridge Beer Festival app configuration (data/festivals.json)", "type": "object", @@ -124,9 +124,8 @@ }, "data_base_url": { "type": "string", - "description": "Base URL for fetching beverage data", - "format": "uri", - "examples": ["https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025"] + "description": "Base URL for fetching beverage data. Can be absolute (https://...) or relative (/festivalId). Relative URLs are resolved against the base URL where festivals.json was fetched from.", + "examples": ["/cbf2025", "https://data.cambeerfestival.app/cbf2025"] }, "is_active": { "type": "boolean", diff --git a/lib/models/festival.dart b/lib/models/festival.dart index 2082ddab..c9492d66 100644 --- a/lib/models/festival.dart +++ b/lib/models/festival.dart @@ -264,7 +264,7 @@ class DefaultFestivals { 'wine', 'low-no', ], - dataBaseUrl: 'https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025', + dataBaseUrl: 'https://data.cambeerfestival.app/cbf2025', isActive: true, ); @@ -276,7 +276,7 @@ class DefaultFestivals { endDate: DateTime(2025, 12, 13), location: 'Cambridge Corn Exchange, Cambridge', availableBeverageTypes: ['beer', 'international-beer', 'cider', 'perry', 'low-no'], - dataBaseUrl: 'https://cbf-data-proxy.richard-alcock.workers.dev/cbfw2025', + dataBaseUrl: 'https://data.cambeerfestival.app/cbfw2025', isActive: false, ); @@ -298,7 +298,7 @@ class DefaultFestivals { 'wine', 'low-no', ], - dataBaseUrl: 'https://cbf-data-proxy.richard-alcock.workers.dev/cbf2024', + dataBaseUrl: 'https://data.cambeerfestival.app/cbf2024', isActive: false, ); diff --git a/lib/services/festival_service.dart b/lib/services/festival_service.dart index c6610df8..74399e27 100644 --- a/lib/services/festival_service.dart +++ b/lib/services/festival_service.dart @@ -8,17 +8,29 @@ class FestivalsResponse { final String defaultFestivalId; final String version; final DateTime? lastUpdated; + final String baseUrl; FestivalsResponse({ required this.festivals, required this.defaultFestivalId, required this.version, this.lastUpdated, + required this.baseUrl, }); - factory FestivalsResponse.fromJson(Map json) { + factory FestivalsResponse.fromJson(Map json, String baseUrl) { final festivalsList = (json['festivals'] as List) - .map((f) => Festival.fromJson(f as Map)) + .map((f) { + final festivalJson = Map.from(f as Map); + // Resolve relative URLs to absolute URLs + if (festivalJson['data_base_url'] != null) { + final dataBaseUrl = festivalJson['data_base_url'] as String; + if (dataBaseUrl.startsWith('/')) { + festivalJson['data_base_url'] = baseUrl + dataBaseUrl; + } + } + return Festival.fromJson(festivalJson); + }) .toList(); return FestivalsResponse( @@ -28,6 +40,7 @@ class FestivalsResponse { lastUpdated: json['last_updated'] != null ? DateTime.tryParse(json['last_updated'] as String) : null, + baseUrl: baseUrl, ); } @@ -47,7 +60,7 @@ class FestivalsResponse { /// Service for fetching festival metadata class FestivalService { static const String _festivalsUrl = - 'https://cbf-data-proxy.richard-alcock.workers.dev/festivals.json'; + 'https://data.cambeerfestival.app/festivals.json'; final http.Client _client; final Duration timeout; @@ -64,7 +77,9 @@ class FestivalService { if (response.statusCode == 200) { final data = json.decode(response.body) as Map; - return FestivalsResponse.fromJson(data); + // Extract base URL from the festivals URL (remove /festivals.json) + final baseUrl = _festivalsUrl.replaceAll(RegExp(r'/festivals\.json$'), ''); + return FestivalsResponse.fromJson(data, baseUrl); } else { throw FestivalServiceException( 'Failed to fetch festivals: ${response.statusCode}', diff --git a/test-e2e/screenshots.ts b/test-e2e/screenshots.ts index e18e9906..b49ecb54 100644 --- a/test-e2e/screenshots.ts +++ b/test-e2e/screenshots.ts @@ -43,7 +43,7 @@ const VIEWPORT = { }; // Cambridge Beer Festival API -const API_BASE_URL = 'https://cbf-data-proxy.richard-alcock.workers.dev'; +const API_BASE_URL = 'https://data.cambeerfestival.app'; const FESTIVAL_ID = process.env.FESTIVAL_ID || 'cbf2025'; // Default to current festival const API_TIMEOUT_MS = 10000; // 10 second timeout for API calls diff --git a/test/beer_provider_test.dart b/test/beer_provider_test.dart index 472f586f..6389d030 100644 --- a/test/beer_provider_test.dart +++ b/test/beer_provider_test.dart @@ -894,6 +894,7 @@ void main() { ), ], defaultFestivalId: 'test', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -968,6 +969,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1013,6 +1015,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1045,6 +1048,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1071,6 +1075,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1136,6 +1141,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1162,6 +1168,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1193,6 +1200,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1227,6 +1235,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); @@ -1273,6 +1282,7 @@ void main() { ), ], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ), ); diff --git a/test/brewery_screen_test.dart b/test/brewery_screen_test.dart index d4334569..c8233f55 100644 --- a/test/brewery_screen_test.dart +++ b/test/brewery_screen_test.dart @@ -60,6 +60,7 @@ void main() { final festivalsResponse = FestivalsResponse( festivals: [testFestival], defaultFestivalId: 'cbf2025', + baseUrl: 'https://example.com', version: '1.0.0', ); when(mockFestivalService.fetchFestivals()) diff --git a/test/main_test.dart b/test/main_test.dart index 4792007e..597aa749 100644 --- a/test/main_test.dart +++ b/test/main_test.dart @@ -41,6 +41,7 @@ void main() { ], defaultFestivalId: 'cbf2025', version: '1.0.0', + baseUrl: 'https://example.com', ), ); diff --git a/test/models_test.dart b/test/models_test.dart index 6adf03ee..d0e3acac 100644 --- a/test/models_test.dart +++ b/test/models_test.dart @@ -734,12 +734,12 @@ void main() { const festival = Festival( id: 'cbf2025', name: 'Cambridge Beer Festival 2025', - dataBaseUrl: 'https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025', + dataBaseUrl: 'https://data.cambeerfestival.app/cbf2025', ); expect( festival.getBeverageUrl('beer'), - 'https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025/beer.json', + 'https://data.cambeerfestival.app/cbf2025/beer.json', ); }); diff --git a/test/services_test.dart b/test/services_test.dart index bc673732..a15ca132 100644 --- a/test/services_test.dart +++ b/test/services_test.dart @@ -76,12 +76,38 @@ void main() { 'last_updated': '2025-01-15T12:00:00.000Z', }; - final response = FestivalsResponse.fromJson(json); + final response = FestivalsResponse.fromJson(json, 'https://example.com'); expect(response.festivals.length, 2); expect(response.defaultFestivalId, 'cbf2025'); expect(response.version, '1.0.0'); expect(response.lastUpdated, isNotNull); + expect(response.baseUrl, 'https://example.com'); + }); + + test('fromJson resolves relative URLs to absolute', () { + final json = { + 'festivals': [ + { + 'id': 'cbf2025', + 'name': 'Cambridge Beer Festival 2025', + 'data_base_url': '/cbf2025', + 'is_active': true, + }, + { + 'id': 'cbfw2025', + 'name': 'Cambridge Winter Beer Festival 2025', + 'data_base_url': '/cbfw2025', + 'is_active': false, + }, + ], + 'default_festival_id': 'cbf2025', + }; + + final response = FestivalsResponse.fromJson(json, 'https://data.cambeerfestival.app'); + + expect(response.festivals[0].dataBaseUrl, 'https://data.cambeerfestival.app/cbf2025'); + expect(response.festivals[1].dataBaseUrl, 'https://data.cambeerfestival.app/cbfw2025'); }); test('fromJson handles missing optional fields', () { @@ -96,7 +122,7 @@ void main() { 'default_festival_id': 'test', }; - final response = FestivalsResponse.fromJson(json); + final response = FestivalsResponse.fromJson(json, 'https://example.com'); expect(response.festivals.length, 1); expect(response.version, '1.0.0'); @@ -120,7 +146,7 @@ void main() { 'default_festival_id': 'cbfw2025', }; - final response = FestivalsResponse.fromJson(json); + final response = FestivalsResponse.fromJson(json, 'https://example.com'); expect(response.defaultFestival, isNotNull); expect(response.defaultFestival!.id, 'cbfw2025'); @@ -138,7 +164,7 @@ void main() { 'default_festival_id': 'nonexistent', }; - final response = FestivalsResponse.fromJson(json); + final response = FestivalsResponse.fromJson(json, 'https://example.com'); expect(response.defaultFestival, isNotNull); expect(response.defaultFestival!.id, 'cbf2025'); @@ -150,7 +176,7 @@ void main() { 'default_festival_id': 'cbf2025', }; - final response = FestivalsResponse.fromJson(json); + final response = FestivalsResponse.fromJson(json, 'https://example.com'); expect(response.defaultFestival, isNull); }); @@ -180,7 +206,7 @@ void main() { 'default_festival_id': 'cbf2025', }; - final response = FestivalsResponse.fromJson(json); + final response = FestivalsResponse.fromJson(json, 'https://example.com'); final activeFestivals = response.activeFestivals; expect(activeFestivals.length, 2); @@ -202,7 +228,7 @@ void main() { 'default_festival_id': 'cbf2024', }; - final response = FestivalsResponse.fromJson(json); + final response = FestivalsResponse.fromJson(json, 'https://example.com'); expect(response.activeFestivals, isEmpty); });