From 07f35ea33a93755970976f68611507cb17cc869d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 19:33:25 +0000 Subject: [PATCH 1/5] Add Cambridge Beer Festival 2026 to festival data feed Adds cbf2026 (May 18-23, 2026, Jesus Green) as the new default active festival, deactivates cbf2025, and updates the DefaultFestivals fallback and associated model tests. https://claude.ai/code/session_01CCeyxeuivbX5jWR4YJSxV9 --- data/festivals.json | 41 ++++++++++++++++++++++++++++++++++++---- lib/models/festival.dart | 27 ++++++++++++++++++++++++-- test/models_test.dart | 5 +++-- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/data/festivals.json b/data/festivals.json index 5b01a7c4..cbd7c4c3 100644 --- a/data/festivals.json +++ b/data/festivals.json @@ -1,5 +1,38 @@ { "festivals": [ + { + "id": "cbf2026", + "name": "Cambridge Beer Festival 2026", + "hashtag": "#cbf2026", + "start_date": "2026-05-18", + "end_date": "2026-05-23", + "location": "Jesus Green, Cambridge", + "address": "Jesus Green, Cambridge CB5 8BA", + "latitude": 52.2119, + "longitude": 0.1220, + "description": "The Cambridge Beer Festival 2026, featuring 200+ real ales from 100+ breweries, 80+ ciders and perries, international beer, mead, and wine.", + "website_url": "https://www.cambridgebeerfestival.com", + "hours": { + "Monday": "17:00 - 23:00", + "Tuesday": "12:00 - 23:00", + "Wednesday": "12:00 - 23:00", + "Thursday": "12:00 - 23:00", + "Friday": "12:00 - 23:00", + "Saturday": "12:00 - 22:00" + }, + "available_beverage_types": [ + "beer", + "international-beer", + "cider", + "perry", + "mead", + "wine", + "apple-juice", + "low-no" + ], + "data_base_url": "/cbf2026", + "is_active": true + }, { "id": "cbf2025", "name": "Cambridge Beer Festival 2025", @@ -31,7 +64,7 @@ "low-no" ], "data_base_url": "/cbf2025", - "is_active": true + "is_active": false }, { "id": "cbfw2025", @@ -91,7 +124,7 @@ "is_active": false } ], - "default_festival_id": "cbf2025", - "version": "1.0.0", - "last_updated": "2025-11-29T00:00:00Z" + "default_festival_id": "cbf2026", + "version": "1.1.0", + "last_updated": "2026-05-03T00:00:00Z" } diff --git a/lib/models/festival.dart b/lib/models/festival.dart index c9492d66..554870b9 100644 --- a/lib/models/festival.dart +++ b/lib/models/festival.dart @@ -246,6 +246,29 @@ class Festival { /// Predefined festival configurations class DefaultFestivals { + static final cambridge2026 = Festival( + id: 'cbf2026', + name: 'Cambridge Beer Festival 2026', + hashtag: '#cbf2026', + startDate: DateTime(2026, 5, 18), + endDate: DateTime(2026, 5, 23), + location: 'Jesus Green, Cambridge', + description: 'The Cambridge Beer Festival 2026', + websiteUrl: 'https://www.cambridgebeerfestival.com', + availableBeverageTypes: [ + 'beer', + 'international-beer', + 'cider', + 'perry', + 'mead', + 'wine', + 'apple-juice', + 'low-no', + ], + dataBaseUrl: 'https://data.cambeerfestival.app/cbf2026', + isActive: true, + ); + static final cambridge2025 = Festival( id: 'cbf2025', name: 'Cambridge Beer Festival 2025', @@ -265,7 +288,7 @@ class DefaultFestivals { 'low-no', ], dataBaseUrl: 'https://data.cambeerfestival.app/cbf2025', - isActive: true, + isActive: false, ); static final cambridgeWinter2025 = Festival( @@ -302,5 +325,5 @@ class DefaultFestivals { isActive: false, ); - static List get all => [cambridge2025, cambridgeWinter2025, cambridge2024]; + static List get all => [cambridge2026, cambridge2025, cambridgeWinter2025, cambridge2024]; } diff --git a/test/models_test.dart b/test/models_test.dart index d0e3acac..c7f3902e 100644 --- a/test/models_test.dart +++ b/test/models_test.dart @@ -967,7 +967,7 @@ void main() { expect(festival.id, 'cbf2025'); expect(festival.name, 'Cambridge Beer Festival 2025'); - expect(festival.isActive, isTrue); + expect(festival.isActive, isFalse); expect(festival.availableBeverageTypes, contains('beer')); expect(festival.availableBeverageTypes, contains('cider')); expect(festival.availableBeverageTypes, contains('mead')); @@ -996,7 +996,8 @@ void main() { test('all returns list of festivals', () { final festivals = DefaultFestivals.all; - expect(festivals.length, 3); + expect(festivals.length, 4); + expect(festivals.map((f) => f.id), contains('cbf2026')); expect(festivals.map((f) => f.id), contains('cbf2025')); expect(festivals.map((f) => f.id), contains('cbfw2025')); expect(festivals.map((f) => f.id), contains('cbf2024')); From 73c4d657512b61eacddb2b7063cf5d2c821886c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 19:37:41 +0000 Subject: [PATCH 2/5] Fix validate-festivals.js schema path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schema lives at docs/code/api/ not docs/api/ — caused CI to fail with ENOENT. https://claude.ai/code/session_01CCeyxeuivbX5jWR4YJSxV9 --- scripts/validate-festivals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/validate-festivals.js b/scripts/validate-festivals.js index fac9b771..34ee157a 100644 --- a/scripts/validate-festivals.js +++ b/scripts/validate-festivals.js @@ -13,7 +13,7 @@ const fs = require('fs'); const path = require('path'); // Paths relative to repo root -const schemaPath = path.join(__dirname, '..', 'docs', 'api', 'festival-registry-schema.json'); +const schemaPath = path.join(__dirname, '..', 'docs', 'code', 'api', 'festival-registry-schema.json'); const festivalsPath = path.join(__dirname, '..', 'data', 'festivals.json'); // Load files From cc50d9825fb3d2c11cb9e99eeeb8aa22fdf5ef9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 19:44:40 +0000 Subject: [PATCH 3/5] Add validate:festivals mise task Runs the same festivals.json schema check as CI so it can be run locally before pushing: ./bin/mise run validate:festivals https://claude.ai/code/session_01CCeyxeuivbX5jWR4YJSxV9 --- mise.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mise.toml b/mise.toml index c21eb30e..6ba610c0 100644 --- a/mise.toml +++ b/mise.toml @@ -44,6 +44,12 @@ description = "Run Flutter code analysis (no-fatal-infos mode)" depends = ['generate'] run = 'flutter analyze --no-fatal-infos' +[tasks."validate:festivals"] +description = "Validate festivals.json against schema (mirrors CI)" +dir = "scripts" +sources = ['data/festivals.json', 'docs/code/api/festival-registry-schema.json'] +run = 'npm ci && node ../scripts/validate-festivals.js' + [tasks."test:worker"] description = "Run Cloudflare Worker tests (Vitest + workerd)" dir = "cloudflare-worker" From e940a9bfb68b5d4ac61b55158cb7053f1ae5b11a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 19:54:29 +0000 Subject: [PATCH 4/5] Address PR review comments for cbf2026 - Update BeerProvider hardcoded fallback from cambridge2025 to cambridge2026 - Add dedicated configuration test for DefaultFestivals.cambridge2026 - Update router and provider tests to expect cbf2026 as the fallback festival https://claude.ai/code/session_01CCeyxeuivbX5jWR4YJSxV9 --- lib/providers/beer_provider.dart | 4 ++-- test/beer_provider_test.dart | 2 +- test/models_test.dart | 13 +++++++++++++ test/router_test.dart | 4 ++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/providers/beer_provider.dart b/lib/providers/beer_provider.dart index dc7c221e..88237e9a 100644 --- a/lib/providers/beer_provider.dart +++ b/lib/providers/beer_provider.dart @@ -59,7 +59,7 @@ class BeerProvider extends ChangeNotifier { List get festivals => _festivals; /// Get festivals sorted by date (live/upcoming first, then past in reverse chronological order) List get sortedFestivals => Festival.sortByDate(_festivals); - Festival get currentFestival => _currentFestival ?? DefaultFestivals.cambridge2025; + Festival get currentFestival => _currentFestival ?? DefaultFestivals.cambridge2026; bool get isLoading => _isLoading; bool get isFestivalsLoading => _isFestivalsLoading; bool get isInitialized => _isInitialized; @@ -243,7 +243,7 @@ class BeerProvider extends ChangeNotifier { if (_festivals.isEmpty) { await loadFestivals(); } - _currentFestival ??= DefaultFestivals.cambridge2025; + _currentFestival ??= DefaultFestivals.cambridge2026; } _isLoading = true; diff --git a/test/beer_provider_test.dart b/test/beer_provider_test.dart index f76a18a3..7a3b1e2f 100644 --- a/test/beer_provider_test.dart +++ b/test/beer_provider_test.dart @@ -128,7 +128,7 @@ void main() { analyticsService: mockAnalyticsService, ); - expect(provider.currentFestival.id, 'cbf2025'); + expect(provider.currentFestival.id, DefaultFestivals.cambridge2026.id); }); test('isLoading is false initially', () { diff --git a/test/models_test.dart b/test/models_test.dart index c7f3902e..197963be 100644 --- a/test/models_test.dart +++ b/test/models_test.dart @@ -962,6 +962,19 @@ void main() { }); group('DefaultFestivals', () { + test('cambridge2026 is configured correctly', () { + final festival = DefaultFestivals.cambridge2026; + + expect(festival.id, 'cbf2026'); + expect(festival.name, 'Cambridge Beer Festival 2026'); + expect(festival.isActive, isTrue); + expect(festival.startDate, DateTime(2026, 5, 18)); + expect(festival.endDate, DateTime(2026, 5, 23)); + expect(festival.availableBeverageTypes, contains('beer')); + expect(festival.availableBeverageTypes, contains('cider')); + expect(festival.availableBeverageTypes, contains('mead')); + }); + test('cambridge2025 is configured correctly', () { final festival = DefaultFestivals.cambridge2025; diff --git a/test/router_test.dart b/test/router_test.dart index c37a7ef0..736c2b99 100644 --- a/test/router_test.dart +++ b/test/router_test.dart @@ -306,7 +306,7 @@ void main() { // Should fall back to default festival despite API failure final currentUri = Uri.parse(appRouter.routerDelegate.currentConfiguration.uri.toString()); expect(currentUri.pathSegments.isNotEmpty, true); - expect(currentUri.pathSegments.first, 'cbf2025', reason: 'Should use default festival when API fails'); + expect(currentUri.pathSegments.first, 'cbf2026', reason: 'Should use default festival when API fails'); }); testWidgets('redirect handles empty festivals list', (tester) async { @@ -335,7 +335,7 @@ void main() { // Should use hardcoded default festival final currentUri = Uri.parse(appRouter.routerDelegate.currentConfiguration.uri.toString()); expect(currentUri.pathSegments.isNotEmpty, true); - expect(currentUri.pathSegments.first, 'cbf2025', reason: 'Should use DefaultFestivals.cambridge2025'); + expect(currentUri.pathSegments.first, 'cbf2026', reason: 'Should use DefaultFestivals.cambridge2026'); }); testWidgets('multiple rapid navigations before init completes', (tester) async { From 5f59d48d6ceec8d0c17c6c616a39e6e235372b91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 19:56:45 +0000 Subject: [PATCH 5/5] Remove hardcoded festival ID from fallback logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BeerProvider now falls back to the first active festival in DefaultFestivals.all rather than a specific named instance, so next year's update only needs to change DefaultFestivals — not the provider or tests. https://claude.ai/code/session_01CCeyxeuivbX5jWR4YJSxV9 --- lib/providers/beer_provider.dart | 4 ++-- test/beer_provider_test.dart | 2 +- test/router_test.dart | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/providers/beer_provider.dart b/lib/providers/beer_provider.dart index 88237e9a..b121cb25 100644 --- a/lib/providers/beer_provider.dart +++ b/lib/providers/beer_provider.dart @@ -59,7 +59,7 @@ class BeerProvider extends ChangeNotifier { List get festivals => _festivals; /// Get festivals sorted by date (live/upcoming first, then past in reverse chronological order) List get sortedFestivals => Festival.sortByDate(_festivals); - Festival get currentFestival => _currentFestival ?? DefaultFestivals.cambridge2026; + Festival get currentFestival => _currentFestival ?? DefaultFestivals.all.firstWhere((f) => f.isActive, orElse: () => DefaultFestivals.all.first); bool get isLoading => _isLoading; bool get isFestivalsLoading => _isFestivalsLoading; bool get isInitialized => _isInitialized; @@ -243,7 +243,7 @@ class BeerProvider extends ChangeNotifier { if (_festivals.isEmpty) { await loadFestivals(); } - _currentFestival ??= DefaultFestivals.cambridge2026; + _currentFestival ??= DefaultFestivals.all.firstWhere((f) => f.isActive, orElse: () => DefaultFestivals.all.first); } _isLoading = true; diff --git a/test/beer_provider_test.dart b/test/beer_provider_test.dart index 7a3b1e2f..c0e0fcfe 100644 --- a/test/beer_provider_test.dart +++ b/test/beer_provider_test.dart @@ -128,7 +128,7 @@ void main() { analyticsService: mockAnalyticsService, ); - expect(provider.currentFestival.id, DefaultFestivals.cambridge2026.id); + expect(provider.currentFestival.id, DefaultFestivals.all.firstWhere((f) => f.isActive).id); }); test('isLoading is false initially', () { diff --git a/test/router_test.dart b/test/router_test.dart index 736c2b99..2e8b2387 100644 --- a/test/router_test.dart +++ b/test/router_test.dart @@ -306,7 +306,7 @@ void main() { // Should fall back to default festival despite API failure final currentUri = Uri.parse(appRouter.routerDelegate.currentConfiguration.uri.toString()); expect(currentUri.pathSegments.isNotEmpty, true); - expect(currentUri.pathSegments.first, 'cbf2026', reason: 'Should use default festival when API fails'); + expect(currentUri.pathSegments.first, DefaultFestivals.all.firstWhere((f) => f.isActive).id, reason: 'Should use active hardcoded festival when API fails'); }); testWidgets('redirect handles empty festivals list', (tester) async { @@ -335,7 +335,7 @@ void main() { // Should use hardcoded default festival final currentUri = Uri.parse(appRouter.routerDelegate.currentConfiguration.uri.toString()); expect(currentUri.pathSegments.isNotEmpty, true); - expect(currentUri.pathSegments.first, 'cbf2026', reason: 'Should use DefaultFestivals.cambridge2026'); + expect(currentUri.pathSegments.first, DefaultFestivals.all.firstWhere((f) => f.isActive).id, reason: 'Should use active hardcoded festival when registry is empty'); }); testWidgets('multiple rapid navigations before init completes', (tester) async {