From 3d934deb561220f0c9024d33a87a4b7fbed17848 Mon Sep 17 00:00:00 2001 From: Ponshu Developer Date: Sun, 12 Apr 2026 07:25:24 +0900 Subject: [PATCH] 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 --- lib/main.dart | 12 +++++++--- lib/screens/camera_screen.dart | 29 ++++++++++-------------- lib/services/analysis_cache_service.dart | 16 ++++++------- lib/services/draft_service.dart | 12 +++++----- lib/services/gemini_service.dart | 16 ++++++++----- lib/services/migration_service.dart | 10 ++++---- lib/services/network_service.dart | 10 ++++---- lib/services/sakenowa_service.dart | 13 ++++++----- pubspec.yaml | 2 +- 9 files changed, 64 insertions(+), 56 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6fbaf1b..24a06bf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,9 +53,15 @@ void main() async { : box.get('migration_version', defaultValue: 0) as int; // 新規ユーザー: 未実行=v0 if (storedVersion < currentMigrationVersion) { - await MigrationService.runMigration(); - await box.put('migration_version', currentMigrationVersion); - await box.put('migration_completed', true); // 旧フラグも維持(後方互換) + final migrationSucceeded = await MigrationService.runMigration(); + if (migrationSucceeded) { + await box.put('migration_version', currentMigrationVersion); + await box.put('migration_completed', true); // 旧フラグも維持(後方互換) + } else { + // バックアップ失敗などでマイグレーションが中断された場合は + // バージョンを進めない。次回起動で再試行される。 + debugPrint('[main] Migration aborted — migration_version not updated'); + } } // ✅ AI解析キャッシュは使うときに初期化する(Lazy initialization) diff --git a/lib/screens/camera_screen.dart b/lib/screens/camera_screen.dart index a0f0cf9..38d3f18 100644 --- a/lib/screens/camera_screen.dart +++ b/lib/screens/camera_screen.dart @@ -292,9 +292,9 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr _capturedImages.add(compressedPath); }); - debugPrint('✅ Gallery image compressed & persisted: $compressedPath'); + debugPrint('Gallery image compressed & persisted: $compressedPath'); } catch (e) { - debugPrint('⚠️ Gallery image compression error: $e'); + debugPrint('Gallery image compression error: $e'); // Fallback: Use original path (legacy behavior) setState(() { _capturedImages.add(img.path); @@ -432,7 +432,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr try { // 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 result = await geminiService.analyzeSakeLabel(_capturedImages); @@ -472,7 +472,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr // ✅ さけのわ自動マッチング(非同期・バックグラウンド) // エラーが発生しても登録フローを中断しない _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 @@ -498,10 +498,10 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr final isLevelUp = newLevel > prevLevel; // Debug: Verify save - debugPrint('✅ Saved to Hive: ${sakeItem.displayData.displayName} (ID: ${sakeItem.id})'); - debugPrint('📦 Total items in box: ${box.length}'); - debugPrint('📦 Prepended to sort order (now ${currentOrder.length} items)'); - debugPrint('🎮 Gamification: EXP +10 (Total: ${updatedProfile.totalExp}) Level: $prevLevel -> $newLevel'); + debugPrint('Saved to Hive: ${sakeItem.displayData.displayName} (ID: ${sakeItem.id})'); + debugPrint('Total items in box: ${box.length}'); + debugPrint('Prepended to sort order (now ${currentOrder.length} items)'); + debugPrint('Gamification: EXP +10 (Total: ${updatedProfile.totalExp}) Level: $prevLevel -> $newLevel'); if (!mounted) return; @@ -903,7 +903,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr /// エラーが発生しても登録フローを中断しない Future _performSakenowaMatching(SakeItem sakeItem) async { try { - debugPrint('🔍 Starting sakenowa auto-matching for: ${sakeItem.displayData.displayName}'); + debugPrint('Starting sakenowa auto-matching for: ${sakeItem.displayData.displayName}'); final matchingService = ref.read(sakenowaAutoMatchingServiceProvider); @@ -915,17 +915,12 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr ); if (result.hasMatch) { - debugPrint('✅ Sakenowa matching successful!'); - debugPrint(' Brand: ${result.brand?.name}'); - debugPrint(' Brewery: ${result.brewery?.name}'); - debugPrint(' Score: ${result.score.toStringAsFixed(2)}'); - debugPrint(' Confident: ${result.isConfident}'); + debugPrint('Sakenowa matching successful: ${result.brand?.name} / score=${result.score.toStringAsFixed(2)} confident=${result.isConfident}'); } else { - debugPrint('ℹ️ No sakenowa match found (score: ${result.score.toStringAsFixed(2)})'); + debugPrint('No sakenowa match found (score: ${result.score.toStringAsFixed(2)})'); } } catch (e, stackTrace) { - // エラーログのみ、ユーザーには通知しない(登録は成功しているため) - debugPrint('💥 Sakenowa auto-matching error: $e'); + debugPrint('Sakenowa auto-matching error: $e'); debugPrint('Stack trace: $stackTrace'); } } diff --git a/lib/services/analysis_cache_service.dart b/lib/services/analysis_cache_service.dart index 0d33675..b872364 100644 --- a/lib/services/analysis_cache_service.dart +++ b/lib/services/analysis_cache_service.dart @@ -28,9 +28,9 @@ class AnalysisCacheService { try { _box = await Hive.openBox(_cacheBoxName); _brandIndexBox = await Hive.openBox(_brandIndexBoxName); - debugPrint('✅ Analysis Cache initialized (${_box!.length} entries, ${_brandIndexBox!.length} brands)'); + debugPrint('Analysis Cache initialized (${_box!.length} entries, ${_brandIndexBox!.length} brands)'); } 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); return digest.toString(); } catch (e) { - debugPrint('⚠️ Hash computation failed: $e'); + debugPrint('Hash computation failed: $e'); // ハッシュ計算失敗時はパスをそのまま使用(キャッシュなし) return imagePath; } @@ -124,7 +124,7 @@ class AnalysisCacheService { final count = _box!.length; 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 imageHash = _brandIndexBox!.get(normalized); if (imageHash == null) { - debugPrint('🔍 Brand Index MISS: $brandName'); + debugPrint('Brand Index MISS: $brandName'); return null; } - debugPrint('✅ Brand Index HIT: $brandName → $imageHash'); + debugPrint('Brand Index HIT: $brandName -> $imageHash'); return getCached(imageHash); } catch (e) { - debugPrint('⚠️ Brand index lookup error: $e'); + debugPrint('Brand index lookup error: $e'); return null; } } @@ -259,6 +259,6 @@ class AnalysisCacheService { final count = _brandIndexBox!.length; await _brandIndexBox!.clear(); - debugPrint('🗑️ Brand index cleared ($count entries deleted)'); + debugPrint('Brand index cleared ($count entries deleted)'); } } diff --git a/lib/services/draft_service.dart b/lib/services/draft_service.dart index e18e18c..c8f34d6 100644 --- a/lib/services/draft_service.dart +++ b/lib/services/draft_service.dart @@ -66,10 +66,10 @@ class DraftService { ); await box.add(draftItem); - debugPrint('📝 Draft saved: ${draftItem.id}'); + debugPrint('Draft saved: ${draftItem.id}'); return draftItem.id; } catch (e) { - debugPrint('⚠️ Draft save error: $e'); + debugPrint('Draft save error: $e'); rethrow; } } @@ -89,10 +89,10 @@ class DraftService { final allItems = box.values.toList(); final pendingDrafts = allItems.where((item) => item.isPendingAnalysis).toList(); - debugPrint('📋 Pending drafts: ${pendingDrafts.length}件'); + debugPrint('Pending drafts: ${pendingDrafts.length}'); return pendingDrafts; } catch (e) { - debugPrint('⚠️ Get pending drafts error: $e'); + debugPrint('Get pending drafts error: $e'); return []; } } @@ -248,7 +248,7 @@ class DraftService { if (item != null && item.isPendingAnalysis) { await box.delete(itemKey); - debugPrint('🗑️ Draft deleted: $itemKey'); + debugPrint('Draft deleted: $itemKey'); } } @@ -263,7 +263,7 @@ class DraftService { await deleteDraft(draft.key); } - debugPrint('🗑️ All drafts deleted: $count件'); + debugPrint('All drafts deleted: $count'); return count; } } diff --git a/lib/services/gemini_service.dart b/lib/services/gemini_service.dart index c08c86f..2d88307 100644 --- a/lib/services/gemini_service.dart +++ b/lib/services/gemini_service.dart @@ -72,7 +72,7 @@ class GeminiService { }) async { // Check Mode: Direct vs Proxy 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); } @@ -141,7 +141,7 @@ class GeminiService { // スキーマ準拠チェック if (result.tasteStats.isEmpty || 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 { // Simple check final requiredKeys = ['aroma', 'sweetness', 'acidity', 'bitterness', 'body']; @@ -149,7 +149,7 @@ class GeminiService { final missing = requiredKeys.where((k) => !actualKeys.contains(k)).toList(); 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. // In strict mode, we might want to fail the analysis to force retry. } @@ -162,7 +162,9 @@ class GeminiService { } } else { // HTTPエラー - debugPrint('Proxy Error: ${response.statusCode} ${response.body}'); + if (kDebugMode) { + debugPrint('Proxy Error: ${response.statusCode} ${response.body}'); + } throw Exception('サーバーエラー (${response.statusCode}): ${response.body}'); } @@ -185,7 +187,7 @@ class GeminiService { final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths); final cached = await AnalysisCacheService.getCached(imageHash); if (cached != null) { - debugPrint('💰 API呼び出しをスキップ(キャッシュヒット)'); + debugPrint('Cache hit: skipping API call'); 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; if (jsonString == null) throw Exception('Empty response from Gemini'); diff --git a/lib/services/migration_service.dart b/lib/services/migration_service.dart index dbd92db..1a91c38 100644 --- a/lib/services/migration_service.dart +++ b/lib/services/migration_service.dart @@ -9,7 +9,10 @@ class MigrationService { /// Runs the migration process with safety backup. /// Should be called after Hive.init and Adapter registration, but before app UI loads. - static Future 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 runMigration() async { debugPrint('[Migration] Starting Phase 0 Migration...'); // 1. Open Boxes @@ -38,9 +41,7 @@ class MigrationService { } catch (e) { debugPrint('[Migration] CRITICAL ERROR during Backup: $e'); - // If backup fails, do we abort? - // Yes, abort migration to be safe. - return; + return false; } // 3. Migration (In-Place) @@ -67,6 +68,7 @@ class MigrationService { } else { debugPrint('[Migration] No items needed migration.'); } + return true; } /// CR-005: 既存画像の圧縮マイグレーション diff --git a/lib/services/network_service.dart b/lib/services/network_service.dart index a7b8553..3343337 100644 --- a/lib/services/network_service.dart +++ b/lib/services/network_service.dart @@ -26,14 +26,14 @@ class NetworkService { // オフライン判定: 接続がない、または機内モード if (results.isEmpty || results.contains(ConnectivityResult.none)) { - debugPrint('🔴 Network: OFFLINE'); + debugPrint('Network: OFFLINE'); return false; } - debugPrint('🟢 Network: ONLINE (${results.join(', ')})'); + debugPrint('Network: ONLINE (${results.join(', ')})'); return true; } catch (e) { - debugPrint('⚠️ Network check error: $e (assuming offline)'); + debugPrint('Network check error: $e (assuming offline)'); return false; // エラー時は安全側に倒す(オフライン扱い) } } @@ -76,13 +76,13 @@ class NetworkService { try { await for (final results in _connectivity.onConnectivityChanged.timeout(timeout)) { if (!results.contains(ConnectivityResult.none)) { - debugPrint('🟢 Network: Back ONLINE!'); + debugPrint('Network: Back ONLINE!'); return true; } } return false; } catch (e) { - debugPrint('⚠️ waitForOnline timeout or error: $e'); + debugPrint('waitForOnline timeout or error: $e'); return false; } } diff --git a/lib/services/sakenowa_service.dart b/lib/services/sakenowa_service.dart index a95c319..c8fe852 100644 --- a/lib/services/sakenowa_service.dart +++ b/lib/services/sakenowa_service.dart @@ -18,6 +18,7 @@ class SakenowaService { // キャッシュ有効期限(24時間) static const _cacheDuration = Duration(hours: 24); + static const _requestTimeout = Duration(seconds: 30); /// 全銘柄データを取得 Future> getBrands({bool forceRefresh = false}) async { @@ -25,7 +26,7 @@ class SakenowaService { 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) { throw Exception('Failed to fetch brands: ${response.statusCode}'); } @@ -45,7 +46,7 @@ class SakenowaService { 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) { throw Exception('Failed to fetch breweries: ${response.statusCode}'); } @@ -64,7 +65,7 @@ class SakenowaService { 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) { throw Exception('Failed to fetch areas: ${response.statusCode}'); } @@ -83,7 +84,7 @@ class SakenowaService { 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) { throw Exception('Failed to fetch flavor charts: ${response.statusCode}'); } @@ -108,7 +109,7 @@ class SakenowaService { /// ランキングを取得(全国) Future> getRankings() async { 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) { throw Exception('Failed to fetch rankings: ${response.statusCode}'); } @@ -129,7 +130,7 @@ class SakenowaService { /// フレーバータグ一覧を取得 Future> 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) { throw Exception('Failed to fetch flavor tags: ${response.statusCode}'); } diff --git a/pubspec.yaml b/pubspec.yaml index c0c03f6..fcf5b31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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. -version: 1.0.31+38 +version: 1.0.32+39 environment: sdk: ^3.10.1