fix: address remaining code audit findings and bump to v1.0.32

- migration_service: runMigration() now returns bool; main.dart only
  advances migration_version when migration succeeds
- sakenowa_service: add 30s timeout to all 6 http.get calls
- gemini_service: add 60s timeout to Direct API generateContent call
- gemini_service: guard response.body error log with kDebugMode
- Remove emoji from debugPrint in core service/screen files
  (gemini_service, analysis_cache_service, network_service,
   draft_service, camera_screen)

Made-with: Cursor
This commit is contained in:
Ponshu Developer 2026-04-12 07:25:24 +09:00
parent cb71ab91de
commit 3d934deb56
9 changed files with 64 additions and 56 deletions

View File

@ -53,9 +53,15 @@ void main() async {
: box.get('migration_version', defaultValue: 0) as int; // : =v0 : box.get('migration_version', defaultValue: 0) as int; // : =v0
if (storedVersion < currentMigrationVersion) { if (storedVersion < currentMigrationVersion) {
await MigrationService.runMigration(); final migrationSucceeded = await MigrationService.runMigration();
await box.put('migration_version', currentMigrationVersion); if (migrationSucceeded) {
await box.put('migration_completed', true); // await box.put('migration_version', currentMigrationVersion);
await box.put('migration_completed', true); //
} else {
//
//
debugPrint('[main] Migration aborted — migration_version not updated');
}
} }
// AI解析キャッシュは使うときに初期化するLazy initialization // AI解析キャッシュは使うときに初期化するLazy initialization

View File

@ -292,9 +292,9 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
_capturedImages.add(compressedPath); _capturedImages.add(compressedPath);
}); });
debugPrint('Gallery image compressed & persisted: $compressedPath'); debugPrint('Gallery image compressed & persisted: $compressedPath');
} catch (e) { } catch (e) {
debugPrint('⚠️ Gallery image compression error: $e'); debugPrint('Gallery image compression error: $e');
// Fallback: Use original path (legacy behavior) // Fallback: Use original path (legacy behavior)
setState(() { setState(() {
_capturedImages.add(img.path); _capturedImages.add(img.path);
@ -432,7 +432,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
try { try {
// Direct Gemini Vision Analysis (OCR removed for app size reduction) // Direct Gemini Vision Analysis (OCR removed for app size reduction)
debugPrint('📸 Starting Gemini Vision Direct Analysis for ${_capturedImages.length} images'); debugPrint('Starting Gemini Vision Direct Analysis for ${_capturedImages.length} images');
final geminiService = ref.read(geminiServiceProvider); final geminiService = ref.read(geminiServiceProvider);
final result = await geminiService.analyzeSakeLabel(_capturedImages); final result = await geminiService.analyzeSakeLabel(_capturedImages);
@ -472,7 +472,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
// //
// //
_performSakenowaMatching(sakeItem).catchError((error) { _performSakenowaMatching(sakeItem).catchError((error) {
debugPrint('⚠️ Sakenowa auto-matching failed (non-critical): $error'); debugPrint('Sakenowa auto-matching failed (non-critical): $error');
}); });
// Prepend new item to sort order so it appears at the top // Prepend new item to sort order so it appears at the top
@ -498,10 +498,10 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
final isLevelUp = newLevel > prevLevel; final isLevelUp = newLevel > prevLevel;
// Debug: Verify save // Debug: Verify save
debugPrint('Saved to Hive: ${sakeItem.displayData.displayName} (ID: ${sakeItem.id})'); debugPrint('Saved to Hive: ${sakeItem.displayData.displayName} (ID: ${sakeItem.id})');
debugPrint('📦 Total items in box: ${box.length}'); debugPrint('Total items in box: ${box.length}');
debugPrint('📦 Prepended to sort order (now ${currentOrder.length} items)'); debugPrint('Prepended to sort order (now ${currentOrder.length} items)');
debugPrint('🎮 Gamification: EXP +10 (Total: ${updatedProfile.totalExp}) Level: $prevLevel -> $newLevel'); debugPrint('Gamification: EXP +10 (Total: ${updatedProfile.totalExp}) Level: $prevLevel -> $newLevel');
if (!mounted) return; if (!mounted) return;
@ -903,7 +903,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
/// ///
Future<void> _performSakenowaMatching(SakeItem sakeItem) async { Future<void> _performSakenowaMatching(SakeItem sakeItem) async {
try { try {
debugPrint('🔍 Starting sakenowa auto-matching for: ${sakeItem.displayData.displayName}'); debugPrint('Starting sakenowa auto-matching for: ${sakeItem.displayData.displayName}');
final matchingService = ref.read(sakenowaAutoMatchingServiceProvider); final matchingService = ref.read(sakenowaAutoMatchingServiceProvider);
@ -915,17 +915,12 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
); );
if (result.hasMatch) { if (result.hasMatch) {
debugPrint('✅ Sakenowa matching successful!'); debugPrint('Sakenowa matching successful: ${result.brand?.name} / score=${result.score.toStringAsFixed(2)} confident=${result.isConfident}');
debugPrint(' Brand: ${result.brand?.name}');
debugPrint(' Brewery: ${result.brewery?.name}');
debugPrint(' Score: ${result.score.toStringAsFixed(2)}');
debugPrint(' Confident: ${result.isConfident}');
} else { } else {
debugPrint(' No sakenowa match found (score: ${result.score.toStringAsFixed(2)})'); debugPrint('No sakenowa match found (score: ${result.score.toStringAsFixed(2)})');
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
// debugPrint('Sakenowa auto-matching error: $e');
debugPrint('💥 Sakenowa auto-matching error: $e');
debugPrint('Stack trace: $stackTrace'); debugPrint('Stack trace: $stackTrace');
} }
} }

View File

@ -28,9 +28,9 @@ class AnalysisCacheService {
try { try {
_box = await Hive.openBox<String>(_cacheBoxName); _box = await Hive.openBox<String>(_cacheBoxName);
_brandIndexBox = await Hive.openBox<String>(_brandIndexBoxName); _brandIndexBox = await Hive.openBox<String>(_brandIndexBoxName);
debugPrint('Analysis Cache initialized (${_box!.length} entries, ${_brandIndexBox!.length} brands)'); debugPrint('Analysis Cache initialized (${_box!.length} entries, ${_brandIndexBox!.length} brands)');
} catch (e) { } catch (e) {
debugPrint('⚠️ Failed to open cache box: $e'); debugPrint('Failed to open cache box: $e');
} }
} }
@ -44,7 +44,7 @@ class AnalysisCacheService {
final digest = sha256.convert(bytes); final digest = sha256.convert(bytes);
return digest.toString(); return digest.toString();
} catch (e) { } catch (e) {
debugPrint('⚠️ Hash computation failed: $e'); debugPrint('Hash computation failed: $e');
// 使 // 使
return imagePath; return imagePath;
} }
@ -124,7 +124,7 @@ class AnalysisCacheService {
final count = _box!.length; final count = _box!.length;
await _box!.clear(); await _box!.clear();
debugPrint('🗑️ Cache cleared ($count entries deleted)'); debugPrint('Cache cleared ($count entries deleted)');
} }
/// ///
@ -200,14 +200,14 @@ class AnalysisCacheService {
final normalized = _normalizeBrandName(brandName); final normalized = _normalizeBrandName(brandName);
final imageHash = _brandIndexBox!.get(normalized); final imageHash = _brandIndexBox!.get(normalized);
if (imageHash == null) { if (imageHash == null) {
debugPrint('🔍 Brand Index MISS: $brandName'); debugPrint('Brand Index MISS: $brandName');
return null; return null;
} }
debugPrint('Brand Index HIT: $brandName $imageHash'); debugPrint('Brand Index HIT: $brandName -> $imageHash');
return getCached(imageHash); return getCached(imageHash);
} catch (e) { } catch (e) {
debugPrint('⚠️ Brand index lookup error: $e'); debugPrint('Brand index lookup error: $e');
return null; return null;
} }
} }
@ -259,6 +259,6 @@ class AnalysisCacheService {
final count = _brandIndexBox!.length; final count = _brandIndexBox!.length;
await _brandIndexBox!.clear(); await _brandIndexBox!.clear();
debugPrint('🗑️ Brand index cleared ($count entries deleted)'); debugPrint('Brand index cleared ($count entries deleted)');
} }
} }

View File

@ -66,10 +66,10 @@ class DraftService {
); );
await box.add(draftItem); await box.add(draftItem);
debugPrint('📝 Draft saved: ${draftItem.id}'); debugPrint('Draft saved: ${draftItem.id}');
return draftItem.id; return draftItem.id;
} catch (e) { } catch (e) {
debugPrint('⚠️ Draft save error: $e'); debugPrint('Draft save error: $e');
rethrow; rethrow;
} }
} }
@ -89,10 +89,10 @@ class DraftService {
final allItems = box.values.toList(); final allItems = box.values.toList();
final pendingDrafts = allItems.where((item) => item.isPendingAnalysis).toList(); final pendingDrafts = allItems.where((item) => item.isPendingAnalysis).toList();
debugPrint('📋 Pending drafts: ${pendingDrafts.length}'); debugPrint('Pending drafts: ${pendingDrafts.length}');
return pendingDrafts; return pendingDrafts;
} catch (e) { } catch (e) {
debugPrint('⚠️ Get pending drafts error: $e'); debugPrint('Get pending drafts error: $e');
return []; return [];
} }
} }
@ -248,7 +248,7 @@ class DraftService {
if (item != null && item.isPendingAnalysis) { if (item != null && item.isPendingAnalysis) {
await box.delete(itemKey); await box.delete(itemKey);
debugPrint('🗑️ Draft deleted: $itemKey'); debugPrint('Draft deleted: $itemKey');
} }
} }
@ -263,7 +263,7 @@ class DraftService {
await deleteDraft(draft.key); await deleteDraft(draft.key);
} }
debugPrint('🗑️ All drafts deleted: $count'); debugPrint('All drafts deleted: $count');
return count; return count;
} }
} }

View File

@ -72,7 +72,7 @@ class GeminiService {
}) async { }) async {
// Check Mode: Direct vs Proxy // Check Mode: Direct vs Proxy
if (!Secrets.useProxy) { if (!Secrets.useProxy) {
debugPrint('🚀 Direct Cloud Mode: Connecting to Gemini API directly...'); debugPrint('Direct Cloud Mode: Connecting to Gemini API directly...');
return _callDirectApi(imagePaths, customPrompt, forceRefresh: forceRefresh); return _callDirectApi(imagePaths, customPrompt, forceRefresh: forceRefresh);
} }
@ -141,7 +141,7 @@ class GeminiService {
// //
if (result.tasteStats.isEmpty || if (result.tasteStats.isEmpty ||
result.tasteStats.values.every((v) => v == 0)) { result.tasteStats.values.every((v) => v == 0)) {
debugPrint('⚠️ WARNING: AI returned empty or zero taste stats. This item will not form a valid chart.'); debugPrint('WARNING: AI returned empty or zero taste stats. This item will not form a valid chart.');
} else { } else {
// Simple check // Simple check
final requiredKeys = ['aroma', 'sweetness', 'acidity', 'bitterness', 'body']; final requiredKeys = ['aroma', 'sweetness', 'acidity', 'bitterness', 'body'];
@ -149,7 +149,7 @@ class GeminiService {
final missing = requiredKeys.where((k) => !actualKeys.contains(k)).toList(); final missing = requiredKeys.where((k) => !actualKeys.contains(k)).toList();
if (missing.isNotEmpty) { if (missing.isNotEmpty) {
debugPrint('⚠️ WARNING: AI response missing keys: $missing. Old schema?'); debugPrint('WARNING: AI response missing keys: $missing. Old schema?');
// We could throw here, but for now let's just log. // We could throw here, but for now let's just log.
// In strict mode, we might want to fail the analysis to force retry. // In strict mode, we might want to fail the analysis to force retry.
} }
@ -162,7 +162,9 @@ class GeminiService {
} }
} else { } else {
// HTTPエラー // HTTPエラー
debugPrint('Proxy Error: ${response.statusCode} ${response.body}'); if (kDebugMode) {
debugPrint('Proxy Error: ${response.statusCode} ${response.body}');
}
throw Exception('サーバーエラー (${response.statusCode}): ${response.body}'); throw Exception('サーバーエラー (${response.statusCode}): ${response.body}');
} }
@ -185,7 +187,7 @@ class GeminiService {
final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths); final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths);
final cached = await AnalysisCacheService.getCached(imageHash); final cached = await AnalysisCacheService.getCached(imageHash);
if (cached != null) { if (cached != null) {
debugPrint('💰 API呼び出しをスキップキャッシュヒット'); debugPrint('Cache hit: skipping API call');
return cached; return cached;
} }
} }
@ -272,7 +274,9 @@ class GeminiService {
), ),
); );
final response = await model.generateContent([Content.multi(contentParts)]); final response = await model
.generateContent([Content.multi(contentParts)])
.timeout(const Duration(seconds: 60));
final jsonString = response.text; final jsonString = response.text;
if (jsonString == null) throw Exception('Empty response from Gemini'); if (jsonString == null) throw Exception('Empty response from Gemini');

View File

@ -9,7 +9,10 @@ class MigrationService {
/// Runs the migration process with safety backup. /// Runs the migration process with safety backup.
/// Should be called after Hive.init and Adapter registration, but before app UI loads. /// Should be called after Hive.init and Adapter registration, but before app UI loads.
static Future<void> runMigration() async { ///
/// Returns true if migration completed successfully, false if it was aborted.
/// The caller must NOT update the stored migration version when false is returned.
static Future<bool> runMigration() async {
debugPrint('[Migration] Starting Phase 0 Migration...'); debugPrint('[Migration] Starting Phase 0 Migration...');
// 1. Open Boxes // 1. Open Boxes
@ -38,9 +41,7 @@ class MigrationService {
} catch (e) { } catch (e) {
debugPrint('[Migration] CRITICAL ERROR during Backup: $e'); debugPrint('[Migration] CRITICAL ERROR during Backup: $e');
// If backup fails, do we abort? return false;
// Yes, abort migration to be safe.
return;
} }
// 3. Migration (In-Place) // 3. Migration (In-Place)
@ -67,6 +68,7 @@ class MigrationService {
} else { } else {
debugPrint('[Migration] No items needed migration.'); debugPrint('[Migration] No items needed migration.');
} }
return true;
} }
/// CR-005: /// CR-005:

View File

@ -26,14 +26,14 @@ class NetworkService {
// : // :
if (results.isEmpty || results.contains(ConnectivityResult.none)) { if (results.isEmpty || results.contains(ConnectivityResult.none)) {
debugPrint('🔴 Network: OFFLINE'); debugPrint('Network: OFFLINE');
return false; return false;
} }
debugPrint('🟢 Network: ONLINE (${results.join(', ')})'); debugPrint('Network: ONLINE (${results.join(', ')})');
return true; return true;
} catch (e) { } catch (e) {
debugPrint('⚠️ Network check error: $e (assuming offline)'); debugPrint('Network check error: $e (assuming offline)');
return false; // return false; //
} }
} }
@ -76,13 +76,13 @@ class NetworkService {
try { try {
await for (final results in _connectivity.onConnectivityChanged.timeout(timeout)) { await for (final results in _connectivity.onConnectivityChanged.timeout(timeout)) {
if (!results.contains(ConnectivityResult.none)) { if (!results.contains(ConnectivityResult.none)) {
debugPrint('🟢 Network: Back ONLINE!'); debugPrint('Network: Back ONLINE!');
return true; return true;
} }
} }
return false; return false;
} catch (e) { } catch (e) {
debugPrint('⚠️ waitForOnline timeout or error: $e'); debugPrint('waitForOnline timeout or error: $e');
return false; return false;
} }
} }

View File

@ -18,6 +18,7 @@ class SakenowaService {
// 24 // 24
static const _cacheDuration = Duration(hours: 24); static const _cacheDuration = Duration(hours: 24);
static const _requestTimeout = Duration(seconds: 30);
/// ///
Future<List<SakenowaBrand>> getBrands({bool forceRefresh = false}) async { Future<List<SakenowaBrand>> getBrands({bool forceRefresh = false}) async {
@ -25,7 +26,7 @@ class SakenowaService {
return _brandsCache!; return _brandsCache!;
} }
final response = await http.get(Uri.parse('$_baseUrl/brands')); final response = await http.get(Uri.parse('$_baseUrl/brands')).timeout(_requestTimeout);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Failed to fetch brands: ${response.statusCode}'); throw Exception('Failed to fetch brands: ${response.statusCode}');
} }
@ -45,7 +46,7 @@ class SakenowaService {
return _breweriesCache!; return _breweriesCache!;
} }
final response = await http.get(Uri.parse('$_baseUrl/breweries')); final response = await http.get(Uri.parse('$_baseUrl/breweries')).timeout(_requestTimeout);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Failed to fetch breweries: ${response.statusCode}'); throw Exception('Failed to fetch breweries: ${response.statusCode}');
} }
@ -64,7 +65,7 @@ class SakenowaService {
return _areasCache!; return _areasCache!;
} }
final response = await http.get(Uri.parse('$_baseUrl/areas')); final response = await http.get(Uri.parse('$_baseUrl/areas')).timeout(_requestTimeout);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Failed to fetch areas: ${response.statusCode}'); throw Exception('Failed to fetch areas: ${response.statusCode}');
} }
@ -83,7 +84,7 @@ class SakenowaService {
return _flavorChartsCache!; return _flavorChartsCache!;
} }
final response = await http.get(Uri.parse('$_baseUrl/flavor-charts')); final response = await http.get(Uri.parse('$_baseUrl/flavor-charts')).timeout(_requestTimeout);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Failed to fetch flavor charts: ${response.statusCode}'); throw Exception('Failed to fetch flavor charts: ${response.statusCode}');
} }
@ -108,7 +109,7 @@ class SakenowaService {
/// ///
Future<List<SakenowaRanking>> getRankings() async { Future<List<SakenowaRanking>> getRankings() async {
try { try {
final response = await http.get(Uri.parse('$_baseUrl/rankings')); final response = await http.get(Uri.parse('$_baseUrl/rankings')).timeout(_requestTimeout);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Failed to fetch rankings: ${response.statusCode}'); throw Exception('Failed to fetch rankings: ${response.statusCode}');
} }
@ -129,7 +130,7 @@ class SakenowaService {
/// ///
Future<List<SakenowaFlavorTag>> getFlavorTags() async { Future<List<SakenowaFlavorTag>> getFlavorTags() async {
final response = await http.get(Uri.parse('$_baseUrl/flavor-tags')); final response = await http.get(Uri.parse('$_baseUrl/flavor-tags')).timeout(_requestTimeout);
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception('Failed to fetch flavor tags: ${response.statusCode}'); throw Exception('Failed to fetch flavor tags: ${response.statusCode}');
} }

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.31+38 version: 1.0.32+39
environment: environment:
sdk: ^3.10.1 sdk: ^3.10.1