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
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

View File

@ -292,9 +292,9 @@ class _CameraScreenState extends ConsumerState<CameraScreen> 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<CameraScreen> 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<CameraScreen> 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<CameraScreen> 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<CameraScreen> with SingleTickerPr
///
Future<void> _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<CameraScreen> 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');
}
}

View File

@ -28,9 +28,9 @@ class AnalysisCacheService {
try {
_box = await Hive.openBox<String>(_cacheBoxName);
_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) {
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)');
}
}

View File

@ -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;
}
}

View File

@ -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');

View File

@ -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<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...');
// 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:

View File

@ -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;
}
}

View File

@ -18,6 +18,7 @@ class SakenowaService {
// 24
static const _cacheDuration = Duration(hours: 24);
static const _requestTimeout = Duration(seconds: 30);
///
Future<List<SakenowaBrand>> 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<List<SakenowaRanking>> 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<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) {
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
# 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