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:
parent
cb71ab91de
commit
3d934deb56
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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: 既存画像の圧縮マイグレーション
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue