docs: aggiorna CLAUDE.md al nuovo workflow e aggiunge ruolo esperto

Riflette lo sviluppo su host + emulatore (comandi, sezione "Sviluppo
locale"), condensa architettura/testing sui contenuti non deducibili
dal codice, e aggiunge il flusso atteso per le richieste di modifica:
analisi critica → implementazione → test suggeriti (non scritti salvo
richiesta esplicita).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 19:30:51 +02:00
parent 0d3943f9f9
commit 45d53c1bd1
+51 -74
View File
@@ -4,113 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
BitePlan è un'app Android per meal planning, conversione crudo/cotto e lista della spesa, scritta in Flutter. Tutta la UI e la UX sono in **italiano**. L'obiettivo finale è un APK Android buildabile via Docker. BitePlan è un'app Android per meal planning, conversione crudo/cotto e lista della spesa, scritta in Flutter. Tutta la UI e la UX sono in **italiano**. Il rilascio avviene come APK Android buildato via Docker.
## Ruolo atteso
Quando viene richiesta una modifica o una nuova funzionalità, segui questo flusso:
1. **Analizza prima di implementare**: valuta se la richiesta ha senso — fattibilità, complessità reale, impatto su architettura/UX/persistenza. Se esiste un modo migliore di implementarla, proponilo; se la richiesta è mal posta o ha un costo sproporzionato al beneficio, dillo esplicitamente con l'alternativa. Motiva i giudizi con riferimenti concreti al codice.
2. **Implementa da esperto**: se la richiesta va bene (o dopo l'eventuale discussione), implementala seguendo le convenzioni del progetto (feature-first, Provider, tutto in italiano).
3. **Suggerisci i test, non scriverli**: al termine indica quali test coprirebbero la modifica (casi e file dove andrebbero), ma implementali solo se richiesto esplicitamente.
## Comandi ## Comandi
```bash ```bash
# Sviluppo (web server con hot reload) # Sviluppo (host, no Docker)
cd docker/dev && docker compose up emulator -avd biteplan & # AVD dedicato, Pixel 6 / Android 14 (google_apis x86_64)
# → http://localhost:5173 flutter run # hot reload con "r", hot restart con "R"
# Con il container attivo: docker compose attach dev → r = hot reload
# Test (dalla root del progetto — richiede immagine biteplan-build) # Analisi statica
flutter analyze # lint: package:flutter_lints/flutter.yaml
# Test rapidi (host)
flutter test # tutta la suite
flutter test test/features/meal_planner/qr_test.dart # singolo file
# Test riproducibili (Docker, immagine biteplan-build — usati come riferimento)
docker run --rm -v "$(pwd):/workspace" -w /workspace biteplan-build \ docker run --rm -v "$(pwd):/workspace" -w /workspace biteplan-build \
bash -c "flutter pub get --enforce-lockfile && flutter test" bash -c "flutter pub get --enforce-lockfile && flutter test"
# Test singolo file # Integration test (richiede emulatore avviato)
docker run --rm -v "$(pwd):/workspace" -w /workspace biteplan-build \ flutter test integration_test/app_test.dart
bash -c "flutter test test/features/meal_planner/qr_test.dart"
# Build APK (headless, da host) # Build APK (headless, via Docker)
bash docker/build/build.sh # debug → dist/biteplan-debug.apk bash docker/build.sh # debug → dist/biteplan-debug.apk
export BITEPLAN_KEYSTORE_PASS=password # richiesto solo per --release export BITEPLAN_KEYSTORE_PASS=password # richiesto solo per --release
bash docker/build/build.sh --release # release → dist/biteplan-release.apk bash docker/build.sh --release # release → dist/biteplan-release.apk
``` ```
## Sviluppo locale
- **Flutter host**: installato in `~/flutter` (tag `3.41.9`, stessa versione pinnata nel Dockerfile), nel `PATH` via `~/.bashrc`. Docker non si usa più per il dev; resta per test di riferimento e build APK (isolamento, `--enforce-lockfile`, firma release).
- **Emulatore**: Android SDK in `/home/davide/android-sdk`; accelerazione hardware via `/dev/kvm` (WSL2 con KVM abilitato).
- **AVD `biteplan`**: dedicato a questo progetto (Pixel 6, Android 14, google_apis/x86_64). Non riutilizzare AVD di altri progetti (es. `palladium_wallet`).
- **Permessi**: le build Docker girano come root nel volume montato — se `.dart_tool/` o `build/` risultano di proprietà root, serve `sudo chown -R davide:davide .dart_tool build` prima di usare flutter da host.
## Build e firma ## Build e firma
- **Flutter pinnato**: versione `3.41.9` nei Dockerfile (`ARG FLUTTER_VERSION`); aggiornare anche `.flutter-version` - **Flutter pinnato**: versione `3.41.9` in `docker/Dockerfile` (`ARG FLUTTER_VERSION`); aggiornare anche `.flutter-version` e l'install host
- **Gradle cache**: named volume Docker `gradle-cache`, isolata dall'host
- **Dipendenze**: `pubspec.lock` committato; la build usa `--enforce-lockfile` e fallisce se il lock non è allineato - **Dipendenze**: `pubspec.lock` committato; la build usa `--enforce-lockfile` e fallisce se il lock non è allineato
- **Keystore**: `docker/biteplan.jks` è in `.gitignore`; da ottenere dall'autore o generare con `keytool` - **Keystore**: `docker/biteplan.jks` è in `.gitignore`; da ottenere dall'autore o generare con `keytool`. Password via `BITEPLAN_KEYSTORE_PASS` (mai hardcoded); alias: `biteplan`
- **Firma release**: password passata via `BITEPLAN_KEYSTORE_PASS` (mai hardcoded); alias keystore: `biteplan`
- **applicationId**: `com.davide.biteplan` — non cambiare, garantisce aggiornamento diretto dalla v1.2.1 - **applicationId**: `com.davide.biteplan` — non cambiare, garantisce aggiornamento diretto dalla v1.2.1
- **versionCode**: letto da `pubspec.yaml` (`version: X.Y.Z+BUILD`); BUILD deve essere sempre crescente - **versionCode**: letto da `pubspec.yaml` (`version: X.Y.Z+BUILD`); BUILD deve essere sempre crescente
## Architettura ## Architettura
Feature-first sotto `lib/features/` (meal_planner, converter, shopping_list, guide), ognuna con `models/`, `providers/`, `presentation/`. Codice trasversale in `lib/shared/` e `lib/core/`.
``` ```
lib/ lib/
├── main.dart ├── app.dart # MaterialApp + NavigationBar; in initState chiama UpdateService.checkUpdate() e mostra showUpdateDialog()
├── app.dart # MaterialApp, NavigationBar; initState chiama UpdateService.checkUpdate() e mostra showUpdateDialog() se c'è aggiornamento ├── core/constants/app_constants.dart # kDayIds, kMealSlots, kStorageKey*, kAppVersion
├── core/
│ ├── constants/app_constants.dart # kDayIds, kMealSlots, kStorageKey*, kAppVersion
│ └── theme/app_theme.dart
├── shared/ ├── shared/
│ ├── services/ │ ├── services/storage_service.dart # wrapper SharedPreferences (load/save)
│ ├── storage_service.dart # wrapper SharedPreferences (load/save) │ ├── services/update_service.dart # checkUpdate() → GitHub API; isNewer(), parseTagName() (@visibleForTesting)
│ ├── update_service.dart # checkUpdate() → GitHub API, isNewer(), parseTagName() (@visibleForTesting) │ ├── services/url_launcher_service.dart # MethodChannel 'com.davide.biteplan/launcher' → Intent.ACTION_VIEW
│ └── url_launcher_service.dart # MethodChannel 'com.davide.biteplan/launcher' → Intent.ACTION_VIEW │ └── widgets/update_dialog.dart # showUpdateDialog() — "Più tardi" / "Scarica"
│ └── widgets/
│ └── update_dialog.dart # showUpdateDialog() — AlertDialog con "Più tardi" / "Scarica"
├── features/ ├── features/
│ ├── meal_planner/ │ ├── meal_planner/ # MealPlan/DayPlan, qr_codec.dart (buildQrPayload, parseMealPlanFromQr), pagine planner + scan QR
│ ├── models/meal_plan.dart # MealPlan, DayPlan │ ├── converter/ # ConversionEntry con rawToCooked/cookedToRaw, dati da assets/data/conversions.json
│ ├── providers/meal_planner_provider.dart │ ├── shopping_list/ # ShoppingItem con quantity per aggregazione duplicati
│ ├── qr_codec.dart # buildQrPayload, parseMealPlanFromQr └── guide/ # guide_page (3 tab) + info_bottom_sheet
│ │ └── presentation/
│ │ ├── pages/meal_planner_page.dart
│ │ ├── pages/qr_scan_page.dart
│ │ └── widgets/meal_card.dart, qr_share_sheet.dart
│ ├── converter/
│ │ ├── models/conversion_entry.dart # rawToCooked, cookedToRaw (metodi sul model)
│ │ ├── providers/converter_provider.dart
│ │ └── presentation/pages/converter_page.dart
│ ├── shopping_list/
│ │ ├── models/shopping_item.dart # quantity per aggregazione duplicati
│ │ ├── providers/shopping_list_provider.dart
│ │ └── presentation/
│ │ ├── pages/shopping_list_page.dart
│ │ └── widgets/shopping_item_tile.dart
│ └── guide/
│ └── presentation/
│ ├── pages/guide_page.dart # 3 tab: Pasti, Converti, Spesa
│ └── widgets/info_bottom_sheet.dart # aperto dal bottone info in AppBar
└── assets/data/conversions.json # 50+ alimenti × metodi cottura └── assets/data/conversions.json # 50+ alimenti × metodi cottura
``` ```
**State management**: Provider (`ChangeNotifier`). **State management**: Provider (`ChangeNotifier`), uno per feature, registrati in `main.dart` con `MultiProvider`.
**Persistenza**: `shared_preferences`, chiavi `meals` e `shopping_list` (JSON serializzato). **Persistenza**: `shared_preferences`, chiavi `meals` e `shopping_list` (JSON serializzato).
**Conversione**: logica in `ConversionEntry``rawToCooked = raw * yieldFactor`, `cookedToRaw = cooked / yieldFactor`. **Conversione**: logica in `ConversionEntry``rawToCooked = raw * yieldFactor`, `cookedToRaw = cooked / yieldFactor`.
**UI**: Material 3, seed color `Color(0xFF2d6a4f)`, tutto in italiano. **UI**: Material 3, seed color `Color(0xFF2d6a4f)`, tutto in italiano.
**QR**: payload JSON `{ "v": 1, "meals": { ... } }`, limite 2953 byte (capacità QR con error correction L). **QR**: payload JSON `{ "v": 1, "meals": { ... } }`, limite 2953 byte (capacità QR con error correction L).
**Update checker**: `UpdateService.checkUpdate()` usa `dart:io` `HttpClient` (nessun package aggiuntivo); disabilitato su web (`kIsWeb`). Il `MethodChannel` `com.davide.biteplan/launcher` è implementato in `android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt`. **Update checker**: `UpdateService.checkUpdate()` usa `dart:io` `HttpClient` (nessun package aggiuntivo); disabilitato su web (`kIsWeb`). Il `MethodChannel` `com.davide.biteplan/launcher` è implementato in `android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt`.
**Widget test con MethodChannel**: usare `TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler` per mockare il canale nei test — vedi `test/shared/widgets/update_dialog_test.dart`.
## Testing ## Testing
130 test (unit + widget). Non richiedono device fisico. 130 test unit + widget in `test/` (rispecchia la struttura di `lib/`), più un integration test in `integration_test/app_test.dart`. I test unit/widget non richiedono device.
``` - Storage isolato con `SharedPreferences.setMockInitialValues({})`
test/ - Widget test: estensione `pumpApp` in `test/helpers/pump_app.dart`
├── helpers/pump_app.dart # estensione pumpApp per widget test - MethodChannel nei test: `TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler` — vedi `test/shared/widgets/update_dialog_test.dart`
├── features/
│ ├── converter/
│ │ ├── models/conversion_entry_test.dart
│ │ └── providers/converter_provider_test.dart
│ ├── meal_planner/
│ │ ├── models/meal_plan_test.dart
│ │ ├── providers/meal_planner_provider_test.dart
│ │ ├── widgets/meal_card_test.dart
│ │ └── qr_test.dart
│ └── shopping_list/
│ ├── models/shopping_item_test.dart
│ ├── providers/shopping_list_provider_test.dart
│ └── widgets/shopping_item_tile_test.dart
└── shared/
├── services/update_service_test.dart
└── widgets/update_dialog_test.dart
```
I test usano `SharedPreferences.setMockInitialValues({})` per isolare lo storage.