pomera-ai-commander 0.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,747 +1,747 @@
1
- """
2
- Comprehensive Error Handler for Settings Database Migration
3
-
4
- This module provides comprehensive error handling, graceful degradation,
5
- and fallback mechanisms for the database settings system. It ensures
6
- the application remains stable even when database operations fail.
7
-
8
- Features:
9
- - Graceful degradation for database connection failures
10
- - Fallback to JSON file system if database fails
11
- - Error logging and notification systems
12
- - Automatic recovery procedures for common failure modes
13
- - Data validation and corruption detection
14
- """
15
-
16
- import json
17
- import sqlite3
18
- import logging
19
- import os
20
- import shutil
21
- import threading
22
- import time
23
- from typing import Dict, List, Tuple, Any, Optional, Union, Callable
24
- from datetime import datetime, timedelta
25
- from pathlib import Path
26
- from enum import Enum
27
- from dataclasses import dataclass
28
-
29
-
30
- class ErrorSeverity(Enum):
31
- """Error severity levels for categorizing issues."""
32
- LOW = "low"
33
- MEDIUM = "medium"
34
- HIGH = "high"
35
- CRITICAL = "critical"
36
-
37
-
38
- class ErrorCategory(Enum):
39
- """Categories of errors that can occur."""
40
- DATABASE_CONNECTION = "database_connection"
41
- DATABASE_CORRUPTION = "database_corruption"
42
- DISK_SPACE = "disk_space"
43
- PERMISSION = "permission"
44
- DATA_VALIDATION = "data_validation"
45
- MIGRATION = "migration"
46
- BACKUP = "backup"
47
- RECOVERY = "recovery"
48
- UNKNOWN = "unknown"
49
-
50
-
51
- @dataclass
52
- class ErrorInfo:
53
- """Information about an error that occurred."""
54
- timestamp: datetime
55
- category: ErrorCategory
56
- severity: ErrorSeverity
57
- message: str
58
- exception: Optional[Exception] = None
59
- context: Optional[Dict[str, Any]] = None
60
- recovery_attempted: bool = False
61
- recovery_successful: bool = False
62
-
63
-
64
- class SettingsErrorHandler:
65
- """
66
- Comprehensive error handler for the settings database system.
67
-
68
- Provides graceful degradation, fallback mechanisms, and automatic
69
- recovery procedures to ensure application stability.
70
- """
71
-
72
- def __init__(self, json_fallback_path: str = "settings.json",
73
- backup_dir: str = "backups",
74
- max_error_history: int = 1000):
75
- """
76
- Initialize the error handler.
77
-
78
- Args:
79
- json_fallback_path: Path to JSON fallback file
80
- backup_dir: Directory for backup files
81
- max_error_history: Maximum number of errors to keep in history
82
- """
83
- self.json_fallback_path = json_fallback_path
84
- self.backup_dir = Path(backup_dir)
85
- self.max_error_history = max_error_history
86
-
87
- # Ensure backup directory exists
88
- self.backup_dir.mkdir(parents=True, exist_ok=True)
89
-
90
- # Error tracking
91
- self._error_history: List[ErrorInfo] = []
92
- self._error_counts: Dict[ErrorCategory, int] = {}
93
- self._last_error_time: Dict[ErrorCategory, datetime] = {}
94
- self._lock = threading.RLock()
95
-
96
- # Recovery state
97
- self._fallback_mode = False
98
- self._fallback_settings: Optional[Dict[str, Any]] = None
99
- self._recovery_in_progress = False
100
-
101
- # Configuration
102
- self._auto_recovery_enabled = True
103
- self._fallback_enabled = True
104
- self._notification_enabled = True
105
-
106
- # Logger
107
- self.logger = logging.getLogger(__name__)
108
-
109
- # Error handlers for different categories
110
- self._error_handlers = {
111
- ErrorCategory.DATABASE_CONNECTION: self._handle_database_connection_error,
112
- ErrorCategory.DATABASE_CORRUPTION: self._handle_database_corruption_error,
113
- ErrorCategory.DISK_SPACE: self._handle_disk_space_error,
114
- ErrorCategory.PERMISSION: self._handle_permission_error,
115
- ErrorCategory.DATA_VALIDATION: self._handle_data_validation_error,
116
- ErrorCategory.MIGRATION: self._handle_migration_error,
117
- ErrorCategory.BACKUP: self._handle_backup_error,
118
- ErrorCategory.RECOVERY: self._handle_recovery_error,
119
- }
120
-
121
- # Recovery procedures
122
- self._recovery_procedures = {
123
- ErrorCategory.DATABASE_CONNECTION: self._recover_database_connection,
124
- ErrorCategory.DATABASE_CORRUPTION: self._recover_database_corruption,
125
- ErrorCategory.DISK_SPACE: self._recover_disk_space,
126
- ErrorCategory.PERMISSION: self._recover_permission,
127
- ErrorCategory.DATA_VALIDATION: self._recover_data_validation,
128
- ErrorCategory.MIGRATION: self._recover_migration,
129
- ErrorCategory.BACKUP: self._recover_backup,
130
- }
131
-
132
- def handle_error(self, category: ErrorCategory, message: str,
133
- exception: Optional[Exception] = None,
134
- context: Optional[Dict[str, Any]] = None,
135
- severity: Optional[ErrorSeverity] = None) -> bool:
136
- """
137
- Handle an error with appropriate recovery procedures.
138
-
139
- Args:
140
- category: Category of the error
141
- message: Error message
142
- exception: Exception that caused the error (if any)
143
- context: Additional context information
144
- severity: Error severity (auto-determined if None)
145
-
146
- Returns:
147
- True if error was handled successfully, False otherwise
148
- """
149
- try:
150
- # Determine severity if not provided
151
- if severity is None:
152
- severity = self._determine_severity(category, exception)
153
-
154
- # Create error info
155
- error_info = ErrorInfo(
156
- timestamp=datetime.now(),
157
- category=category,
158
- severity=severity,
159
- message=message,
160
- exception=exception,
161
- context=context or {}
162
- )
163
-
164
- # Record the error
165
- self._record_error(error_info)
166
-
167
- # Log the error
168
- self._log_error(error_info)
169
-
170
- # Handle the error based on category
171
- handler = self._error_handlers.get(category, self._handle_generic_error)
172
- recovery_successful = handler(error_info)
173
-
174
- # Update recovery status
175
- error_info.recovery_attempted = True
176
- error_info.recovery_successful = recovery_successful
177
-
178
- # Send notification if enabled
179
- if self._notification_enabled:
180
- self._send_error_notification(error_info)
181
-
182
- return recovery_successful
183
-
184
- except Exception as e:
185
- self.logger.critical(f"Error handler itself failed: {e}")
186
- return False
187
-
188
- def enable_fallback_mode(self, settings_data: Optional[Dict[str, Any]] = None) -> bool:
189
- """
190
- Enable fallback mode using JSON file system.
191
-
192
- Args:
193
- settings_data: Settings data to use in fallback mode
194
-
195
- Returns:
196
- True if fallback mode enabled successfully
197
- """
198
- try:
199
- with self._lock:
200
- self._fallback_mode = True
201
-
202
- if settings_data:
203
- self._fallback_settings = settings_data.copy()
204
- else:
205
- # Try to load from JSON file
206
- self._fallback_settings = self._load_json_fallback()
207
-
208
- if self._fallback_settings is None:
209
- # Use minimal default settings
210
- self._fallback_settings = self._get_minimal_default_settings()
211
-
212
- self.logger.warning("Fallback mode enabled - using JSON file system")
213
- return True
214
-
215
- except Exception as e:
216
- self.logger.error(f"Failed to enable fallback mode: {e}")
217
- return False
218
-
219
- def disable_fallback_mode(self) -> bool:
220
- """
221
- Disable fallback mode and return to database system.
222
-
223
- Returns:
224
- True if fallback mode disabled successfully
225
- """
226
- try:
227
- with self._lock:
228
- self._fallback_mode = False
229
- self._fallback_settings = None
230
-
231
- self.logger.info("Fallback mode disabled - returning to database system")
232
- return True
233
-
234
- except Exception as e:
235
- self.logger.error(f"Failed to disable fallback mode: {e}")
236
- return False
237
-
238
- def is_fallback_mode(self) -> bool:
239
- """Check if currently in fallback mode."""
240
- return self._fallback_mode
241
-
242
- def get_fallback_settings(self) -> Optional[Dict[str, Any]]:
243
- """Get current fallback settings."""
244
- return self._fallback_settings.copy() if self._fallback_settings else None
245
-
246
- def save_fallback_settings(self, settings: Dict[str, Any]) -> bool:
247
- """
248
- Save settings in fallback mode.
249
-
250
- Args:
251
- settings: Settings to save
252
-
253
- Returns:
254
- True if saved successfully
255
- """
256
- try:
257
- if not self._fallback_mode:
258
- return False
259
-
260
- with self._lock:
261
- # Update in-memory fallback settings
262
- self._fallback_settings = settings.copy()
263
-
264
- # Save to JSON file
265
- return self._save_json_fallback(settings)
266
-
267
- except Exception as e:
268
- self.logger.error(f"Failed to save fallback settings: {e}")
269
- return False
270
-
271
- def attempt_recovery(self, category: ErrorCategory) -> bool:
272
- """
273
- Attempt recovery for a specific error category.
274
-
275
- Args:
276
- category: Error category to recover from
277
-
278
- Returns:
279
- True if recovery successful
280
- """
281
- if self._recovery_in_progress:
282
- self.logger.warning("Recovery already in progress")
283
- return False
284
-
285
- try:
286
- self._recovery_in_progress = True
287
-
288
- recovery_proc = self._recovery_procedures.get(category)
289
- if recovery_proc:
290
- success = recovery_proc()
291
- if success:
292
- self.logger.info(f"Recovery successful for {category.value}")
293
- else:
294
- self.logger.error(f"Recovery failed for {category.value}")
295
- return success
296
- else:
297
- self.logger.warning(f"No recovery procedure for {category.value}")
298
- return False
299
-
300
- except Exception as e:
301
- self.logger.error(f"Recovery attempt failed: {e}")
302
- return False
303
- finally:
304
- self._recovery_in_progress = False
305
-
306
- def get_error_statistics(self) -> Dict[str, Any]:
307
- """
308
- Get error statistics and health information.
309
-
310
- Returns:
311
- Dictionary with error statistics
312
- """
313
- with self._lock:
314
- total_errors = len(self._error_history)
315
- recent_errors = [e for e in self._error_history
316
- if e.timestamp > datetime.now() - timedelta(hours=24)]
317
-
318
- critical_errors = [e for e in self._error_history
319
- if e.severity == ErrorSeverity.CRITICAL]
320
-
321
- stats = {
322
- 'total_errors': total_errors,
323
- 'recent_errors_24h': len(recent_errors),
324
- 'critical_errors': len(critical_errors),
325
- 'fallback_mode': self._fallback_mode,
326
- 'recovery_in_progress': self._recovery_in_progress,
327
- 'error_counts_by_category': dict(self._error_counts),
328
- 'last_error_times': {
329
- cat.value: time.isoformat()
330
- for cat, time in self._last_error_time.items()
331
- },
332
- 'most_common_errors': self._get_most_common_errors(10)
333
- }
334
-
335
- return stats
336
-
337
- def clear_error_history(self) -> None:
338
- """Clear error history."""
339
- with self._lock:
340
- self._error_history.clear()
341
- self._error_counts.clear()
342
- self._last_error_time.clear()
343
- self.logger.info("Error history cleared")
344
-
345
- def validate_database_integrity(self, connection_manager) -> List[str]:
346
- """
347
- Validate database integrity and detect corruption.
348
-
349
- Args:
350
- connection_manager: Database connection manager
351
-
352
- Returns:
353
- List of integrity issues found
354
- """
355
- issues = []
356
-
357
- try:
358
- conn = connection_manager.get_connection()
359
-
360
- # Check database integrity
361
- cursor = conn.execute("PRAGMA integrity_check")
362
- integrity_result = cursor.fetchone()[0]
363
-
364
- if integrity_result != "ok":
365
- issues.append(f"Database integrity check failed: {integrity_result}")
366
-
367
- # Check foreign key constraints
368
- cursor = conn.execute("PRAGMA foreign_key_check")
369
- fk_violations = cursor.fetchall()
370
-
371
- if fk_violations:
372
- issues.append(f"Foreign key violations found: {len(fk_violations)}")
373
-
374
- # Check table existence
375
- required_tables = [
376
- 'core_settings', 'tool_settings', 'tab_content',
377
- 'performance_settings', 'font_settings', 'dialog_settings',
378
- 'settings_metadata'
379
- ]
380
-
381
- cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
382
- existing_tables = {row[0] for row in cursor.fetchall()}
383
-
384
- missing_tables = set(required_tables) - existing_tables
385
- if missing_tables:
386
- issues.append(f"Missing tables: {', '.join(missing_tables)}")
387
-
388
- # Check for empty critical tables
389
- for table in ['core_settings']:
390
- cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
391
- count = cursor.fetchone()[0]
392
- if count == 0:
393
- issues.append(f"Critical table {table} is empty")
394
-
395
- except Exception as e:
396
- issues.append(f"Database validation failed: {e}")
397
-
398
- return issues
399
-
400
- # Private methods for error handling
401
-
402
- def _record_error(self, error_info: ErrorInfo) -> None:
403
- """Record error in history."""
404
- with self._lock:
405
- self._error_history.append(error_info)
406
-
407
- # Update counts
408
- category = error_info.category
409
- self._error_counts[category] = self._error_counts.get(category, 0) + 1
410
- self._last_error_time[category] = error_info.timestamp
411
-
412
- # Trim history if too large
413
- if len(self._error_history) > self.max_error_history:
414
- self._error_history = self._error_history[-self.max_error_history:]
415
-
416
- def _log_error(self, error_info: ErrorInfo) -> None:
417
- """Log error with appropriate level."""
418
- log_message = f"[{error_info.category.value}] {error_info.message}"
419
-
420
- if error_info.exception:
421
- log_message += f" - Exception: {error_info.exception}"
422
-
423
- if error_info.context:
424
- log_message += f" - Context: {error_info.context}"
425
-
426
- if error_info.severity == ErrorSeverity.CRITICAL:
427
- self.logger.critical(log_message)
428
- elif error_info.severity == ErrorSeverity.HIGH:
429
- self.logger.error(log_message)
430
- elif error_info.severity == ErrorSeverity.MEDIUM:
431
- self.logger.warning(log_message)
432
- else:
433
- self.logger.info(log_message)
434
-
435
- def _determine_severity(self, category: ErrorCategory,
436
- exception: Optional[Exception]) -> ErrorSeverity:
437
- """Determine error severity based on category and exception."""
438
- # Critical errors that prevent core functionality
439
- if category in [ErrorCategory.DATABASE_CORRUPTION, ErrorCategory.MIGRATION]:
440
- return ErrorSeverity.CRITICAL
441
-
442
- # High severity errors that significantly impact functionality
443
- if category in [ErrorCategory.DATABASE_CONNECTION, ErrorCategory.PERMISSION]:
444
- return ErrorSeverity.HIGH
445
-
446
- # Medium severity errors that cause inconvenience
447
- if category in [ErrorCategory.DISK_SPACE, ErrorCategory.BACKUP]:
448
- return ErrorSeverity.MEDIUM
449
-
450
- # Check exception type for additional context
451
- if isinstance(exception, (sqlite3.DatabaseError, sqlite3.CorruptError)):
452
- return ErrorSeverity.CRITICAL
453
- elif isinstance(exception, (PermissionError, OSError)):
454
- return ErrorSeverity.HIGH
455
-
456
- return ErrorSeverity.LOW
457
-
458
- def _send_error_notification(self, error_info: ErrorInfo) -> None:
459
- """Send error notification (placeholder for future implementation)."""
460
- # This could be extended to send notifications via:
461
- # - System notifications
462
- # - Email alerts
463
- # - Logging to external systems
464
- # - UI notifications
465
- pass
466
-
467
- # Error handlers for specific categories
468
-
469
- def _handle_database_connection_error(self, error_info: ErrorInfo) -> bool:
470
- """Handle database connection errors."""
471
- self.logger.warning("Database connection error - attempting fallback")
472
-
473
- # Enable fallback mode
474
- if self._fallback_enabled:
475
- return self.enable_fallback_mode()
476
-
477
- return False
478
-
479
- def _handle_database_corruption_error(self, error_info: ErrorInfo) -> bool:
480
- """Handle database corruption errors."""
481
- self.logger.error("Database corruption detected - attempting recovery")
482
-
483
- # Try to recover from backup
484
- if self._auto_recovery_enabled:
485
- return self.attempt_recovery(ErrorCategory.DATABASE_CORRUPTION)
486
-
487
- # Fall back to JSON if recovery not enabled
488
- if self._fallback_enabled:
489
- return self.enable_fallback_mode()
490
-
491
- return False
492
-
493
- def _handle_disk_space_error(self, error_info: ErrorInfo) -> bool:
494
- """Handle disk space errors."""
495
- self.logger.warning("Disk space error - continuing with in-memory operations")
496
-
497
- # Continue with in-memory operations, disable backups temporarily
498
- return True
499
-
500
- def _handle_permission_error(self, error_info: ErrorInfo) -> bool:
501
- """Handle permission errors."""
502
- self.logger.error("Permission error - attempting fallback")
503
-
504
- # Try fallback mode
505
- if self._fallback_enabled:
506
- return self.enable_fallback_mode()
507
-
508
- return False
509
-
510
- def _handle_data_validation_error(self, error_info: ErrorInfo) -> bool:
511
- """Handle data validation errors."""
512
- self.logger.warning("Data validation error - using default values")
513
-
514
- # Continue with default values for invalid data
515
- return True
516
-
517
- def _handle_migration_error(self, error_info: ErrorInfo) -> bool:
518
- """Handle migration errors."""
519
- self.logger.error("Migration error - attempting rollback")
520
-
521
- # Try to rollback migration
522
- if self._auto_recovery_enabled:
523
- return self.attempt_recovery(ErrorCategory.MIGRATION)
524
-
525
- return False
526
-
527
- def _handle_backup_error(self, error_info: ErrorInfo) -> bool:
528
- """Handle backup errors."""
529
- self.logger.warning("Backup error - continuing without backup")
530
-
531
- # Continue operations without backup
532
- return True
533
-
534
- def _handle_recovery_error(self, error_info: ErrorInfo) -> bool:
535
- """Handle recovery errors."""
536
- self.logger.error("Recovery error - enabling fallback mode")
537
-
538
- # Enable fallback as last resort
539
- if self._fallback_enabled:
540
- return self.enable_fallback_mode()
541
-
542
- return False
543
-
544
- def _handle_generic_error(self, error_info: ErrorInfo) -> bool:
545
- """Handle generic/unknown errors."""
546
- self.logger.warning(f"Generic error: {error_info.message}")
547
-
548
- # Try fallback for unknown errors
549
- if error_info.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
550
- if self._fallback_enabled:
551
- return self.enable_fallback_mode()
552
-
553
- return True
554
-
555
- # Recovery procedures
556
-
557
- def _recover_database_connection(self) -> bool:
558
- """Recover from database connection issues."""
559
- try:
560
- # Wait a moment and retry connection
561
- time.sleep(1)
562
-
563
- # This would be implemented by the connection manager
564
- # For now, just return success to indicate recovery attempt
565
- self.logger.info("Database connection recovery attempted")
566
- return True
567
-
568
- except Exception as e:
569
- self.logger.error(f"Database connection recovery failed: {e}")
570
- return False
571
-
572
- def _recover_database_corruption(self) -> bool:
573
- """Recover from database corruption."""
574
- try:
575
- # Try to restore from most recent backup
576
- backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
577
- if backup_files:
578
- # Sort by modification time, get most recent
579
- latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
580
-
581
- self.logger.info(f"Attempting to restore from backup: {latest_backup}")
582
- # This would be implemented by the connection manager
583
- return True
584
- else:
585
- self.logger.error("No backup files found for recovery")
586
- return False
587
-
588
- except Exception as e:
589
- self.logger.error(f"Database corruption recovery failed: {e}")
590
- return False
591
-
592
- def _recover_disk_space(self) -> bool:
593
- """Recover from disk space issues."""
594
- try:
595
- # Clean up old backup files
596
- backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
597
- if len(backup_files) > 5: # Keep only 5 most recent
598
- old_backups = sorted(backup_files, key=lambda p: p.stat().st_mtime)[:-5]
599
- for backup in old_backups:
600
- backup.unlink()
601
- self.logger.info(f"Cleaned up old backup: {backup}")
602
-
603
- return True
604
-
605
- except Exception as e:
606
- self.logger.error(f"Disk space recovery failed: {e}")
607
- return False
608
-
609
- def _recover_permission(self) -> bool:
610
- """Recover from permission issues."""
611
- try:
612
- # Try to change file permissions if possible
613
- # This is a placeholder - actual implementation would depend on OS
614
- self.logger.info("Permission recovery attempted")
615
- return False # Usually requires manual intervention
616
-
617
- except Exception as e:
618
- self.logger.error(f"Permission recovery failed: {e}")
619
- return False
620
-
621
- def _recover_data_validation(self) -> bool:
622
- """Recover from data validation issues."""
623
- try:
624
- # Reset to default values for invalid data
625
- self.logger.info("Data validation recovery - using defaults")
626
- return True
627
-
628
- except Exception as e:
629
- self.logger.error(f"Data validation recovery failed: {e}")
630
- return False
631
-
632
- def _recover_migration(self) -> bool:
633
- """Recover from migration issues."""
634
- try:
635
- # Try to rollback to original JSON file
636
- backup_files = list(Path(".").glob("settings.json.backup_*"))
637
- if backup_files:
638
- latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
639
- shutil.copy2(latest_backup, self.json_fallback_path)
640
- self.logger.info(f"Migration rollback completed: {latest_backup}")
641
- return True
642
- else:
643
- self.logger.error("No JSON backup found for migration rollback")
644
- return False
645
-
646
- except Exception as e:
647
- self.logger.error(f"Migration recovery failed: {e}")
648
- return False
649
-
650
- def _recover_backup(self) -> bool:
651
- """Recover from backup issues."""
652
- try:
653
- # Try alternative backup location or method
654
- self.logger.info("Backup recovery attempted")
655
- return True
656
-
657
- except Exception as e:
658
- self.logger.error(f"Backup recovery failed: {e}")
659
- return False
660
-
661
- # Fallback JSON operations
662
-
663
- def _load_json_fallback(self) -> Optional[Dict[str, Any]]:
664
- """Load settings from JSON fallback file."""
665
- try:
666
- if os.path.exists(self.json_fallback_path):
667
- with open(self.json_fallback_path, 'r', encoding='utf-8') as f:
668
- return json.load(f)
669
- else:
670
- self.logger.warning(f"JSON fallback file not found: {self.json_fallback_path}")
671
- return None
672
-
673
- except Exception as e:
674
- self.logger.error(f"Failed to load JSON fallback: {e}")
675
- return None
676
-
677
- def _save_json_fallback(self, settings: Dict[str, Any]) -> bool:
678
- """Save settings to JSON fallback file."""
679
- try:
680
- with open(self.json_fallback_path, 'w', encoding='utf-8') as f:
681
- json.dump(settings, f, indent=2, ensure_ascii=False)
682
- return True
683
-
684
- except Exception as e:
685
- self.logger.error(f"Failed to save JSON fallback: {e}")
686
- return False
687
-
688
- def _get_minimal_default_settings(self) -> Dict[str, Any]:
689
- """Get minimal default settings for emergency fallback."""
690
- return {
691
- "export_path": str(Path.home() / "Downloads"),
692
- "debug_level": "INFO",
693
- "selected_tool": "Case Tool",
694
- "active_input_tab": 0,
695
- "active_output_tab": 0,
696
- "input_tabs": [""] * 7,
697
- "output_tabs": [""] * 7,
698
- "tool_settings": {},
699
- "performance_settings": {"mode": "automatic"},
700
- "font_settings": {"text_font": {"family": "Consolas", "size": 11}},
701
- "dialog_settings": {"error": {"enabled": True, "locked": True}}
702
- }
703
-
704
- def _get_most_common_errors(self, limit: int) -> List[Dict[str, Any]]:
705
- """Get most common error categories."""
706
- error_counts = {}
707
-
708
- for error in self._error_history:
709
- key = f"{error.category.value}: {error.message[:50]}"
710
- error_counts[key] = error_counts.get(key, 0) + 1
711
-
712
- # Sort by count and return top N
713
- sorted_errors = sorted(error_counts.items(), key=lambda x: x[1], reverse=True)
714
-
715
- return [
716
- {"error": error, "count": count}
717
- for error, count in sorted_errors[:limit]
718
- ]
719
-
720
-
721
- # Global error handler instance
722
- _global_error_handler: Optional[SettingsErrorHandler] = None
723
-
724
-
725
- def get_error_handler() -> SettingsErrorHandler:
726
- """Get the global error handler instance."""
727
- global _global_error_handler
728
- if _global_error_handler is None:
729
- _global_error_handler = SettingsErrorHandler()
730
- return _global_error_handler
731
-
732
-
733
- def initialize_error_handler(json_fallback_path: str = "settings.json",
734
- backup_dir: str = "backups") -> SettingsErrorHandler:
735
- """
736
- Initialize the global error handler.
737
-
738
- Args:
739
- json_fallback_path: Path to JSON fallback file
740
- backup_dir: Directory for backup files
741
-
742
- Returns:
743
- Initialized error handler instance
744
- """
745
- global _global_error_handler
746
- _global_error_handler = SettingsErrorHandler(json_fallback_path, backup_dir)
1
+ """
2
+ Comprehensive Error Handler for Settings Database Migration
3
+
4
+ This module provides comprehensive error handling, graceful degradation,
5
+ and fallback mechanisms for the database settings system. It ensures
6
+ the application remains stable even when database operations fail.
7
+
8
+ Features:
9
+ - Graceful degradation for database connection failures
10
+ - Fallback to JSON file system if database fails
11
+ - Error logging and notification systems
12
+ - Automatic recovery procedures for common failure modes
13
+ - Data validation and corruption detection
14
+ """
15
+
16
+ import json
17
+ import sqlite3
18
+ import logging
19
+ import os
20
+ import shutil
21
+ import threading
22
+ import time
23
+ from typing import Dict, List, Tuple, Any, Optional, Union, Callable
24
+ from datetime import datetime, timedelta
25
+ from pathlib import Path
26
+ from enum import Enum
27
+ from dataclasses import dataclass
28
+
29
+
30
+ class ErrorSeverity(Enum):
31
+ """Error severity levels for categorizing issues."""
32
+ LOW = "low"
33
+ MEDIUM = "medium"
34
+ HIGH = "high"
35
+ CRITICAL = "critical"
36
+
37
+
38
+ class ErrorCategory(Enum):
39
+ """Categories of errors that can occur."""
40
+ DATABASE_CONNECTION = "database_connection"
41
+ DATABASE_CORRUPTION = "database_corruption"
42
+ DISK_SPACE = "disk_space"
43
+ PERMISSION = "permission"
44
+ DATA_VALIDATION = "data_validation"
45
+ MIGRATION = "migration"
46
+ BACKUP = "backup"
47
+ RECOVERY = "recovery"
48
+ UNKNOWN = "unknown"
49
+
50
+
51
+ @dataclass
52
+ class ErrorInfo:
53
+ """Information about an error that occurred."""
54
+ timestamp: datetime
55
+ category: ErrorCategory
56
+ severity: ErrorSeverity
57
+ message: str
58
+ exception: Optional[Exception] = None
59
+ context: Optional[Dict[str, Any]] = None
60
+ recovery_attempted: bool = False
61
+ recovery_successful: bool = False
62
+
63
+
64
+ class SettingsErrorHandler:
65
+ """
66
+ Comprehensive error handler for the settings database system.
67
+
68
+ Provides graceful degradation, fallback mechanisms, and automatic
69
+ recovery procedures to ensure application stability.
70
+ """
71
+
72
+ def __init__(self, json_fallback_path: str = "settings.json",
73
+ backup_dir: str = "backups",
74
+ max_error_history: int = 1000):
75
+ """
76
+ Initialize the error handler.
77
+
78
+ Args:
79
+ json_fallback_path: Path to JSON fallback file
80
+ backup_dir: Directory for backup files
81
+ max_error_history: Maximum number of errors to keep in history
82
+ """
83
+ self.json_fallback_path = json_fallback_path
84
+ self.backup_dir = Path(backup_dir)
85
+ self.max_error_history = max_error_history
86
+
87
+ # Ensure backup directory exists
88
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
89
+
90
+ # Error tracking
91
+ self._error_history: List[ErrorInfo] = []
92
+ self._error_counts: Dict[ErrorCategory, int] = {}
93
+ self._last_error_time: Dict[ErrorCategory, datetime] = {}
94
+ self._lock = threading.RLock()
95
+
96
+ # Recovery state
97
+ self._fallback_mode = False
98
+ self._fallback_settings: Optional[Dict[str, Any]] = None
99
+ self._recovery_in_progress = False
100
+
101
+ # Configuration
102
+ self._auto_recovery_enabled = True
103
+ self._fallback_enabled = True
104
+ self._notification_enabled = True
105
+
106
+ # Logger
107
+ self.logger = logging.getLogger(__name__)
108
+
109
+ # Error handlers for different categories
110
+ self._error_handlers = {
111
+ ErrorCategory.DATABASE_CONNECTION: self._handle_database_connection_error,
112
+ ErrorCategory.DATABASE_CORRUPTION: self._handle_database_corruption_error,
113
+ ErrorCategory.DISK_SPACE: self._handle_disk_space_error,
114
+ ErrorCategory.PERMISSION: self._handle_permission_error,
115
+ ErrorCategory.DATA_VALIDATION: self._handle_data_validation_error,
116
+ ErrorCategory.MIGRATION: self._handle_migration_error,
117
+ ErrorCategory.BACKUP: self._handle_backup_error,
118
+ ErrorCategory.RECOVERY: self._handle_recovery_error,
119
+ }
120
+
121
+ # Recovery procedures
122
+ self._recovery_procedures = {
123
+ ErrorCategory.DATABASE_CONNECTION: self._recover_database_connection,
124
+ ErrorCategory.DATABASE_CORRUPTION: self._recover_database_corruption,
125
+ ErrorCategory.DISK_SPACE: self._recover_disk_space,
126
+ ErrorCategory.PERMISSION: self._recover_permission,
127
+ ErrorCategory.DATA_VALIDATION: self._recover_data_validation,
128
+ ErrorCategory.MIGRATION: self._recover_migration,
129
+ ErrorCategory.BACKUP: self._recover_backup,
130
+ }
131
+
132
+ def handle_error(self, category: ErrorCategory, message: str,
133
+ exception: Optional[Exception] = None,
134
+ context: Optional[Dict[str, Any]] = None,
135
+ severity: Optional[ErrorSeverity] = None) -> bool:
136
+ """
137
+ Handle an error with appropriate recovery procedures.
138
+
139
+ Args:
140
+ category: Category of the error
141
+ message: Error message
142
+ exception: Exception that caused the error (if any)
143
+ context: Additional context information
144
+ severity: Error severity (auto-determined if None)
145
+
146
+ Returns:
147
+ True if error was handled successfully, False otherwise
148
+ """
149
+ try:
150
+ # Determine severity if not provided
151
+ if severity is None:
152
+ severity = self._determine_severity(category, exception)
153
+
154
+ # Create error info
155
+ error_info = ErrorInfo(
156
+ timestamp=datetime.now(),
157
+ category=category,
158
+ severity=severity,
159
+ message=message,
160
+ exception=exception,
161
+ context=context or {}
162
+ )
163
+
164
+ # Record the error
165
+ self._record_error(error_info)
166
+
167
+ # Log the error
168
+ self._log_error(error_info)
169
+
170
+ # Handle the error based on category
171
+ handler = self._error_handlers.get(category, self._handle_generic_error)
172
+ recovery_successful = handler(error_info)
173
+
174
+ # Update recovery status
175
+ error_info.recovery_attempted = True
176
+ error_info.recovery_successful = recovery_successful
177
+
178
+ # Send notification if enabled
179
+ if self._notification_enabled:
180
+ self._send_error_notification(error_info)
181
+
182
+ return recovery_successful
183
+
184
+ except Exception as e:
185
+ self.logger.critical(f"Error handler itself failed: {e}")
186
+ return False
187
+
188
+ def enable_fallback_mode(self, settings_data: Optional[Dict[str, Any]] = None) -> bool:
189
+ """
190
+ Enable fallback mode using JSON file system.
191
+
192
+ Args:
193
+ settings_data: Settings data to use in fallback mode
194
+
195
+ Returns:
196
+ True if fallback mode enabled successfully
197
+ """
198
+ try:
199
+ with self._lock:
200
+ self._fallback_mode = True
201
+
202
+ if settings_data:
203
+ self._fallback_settings = settings_data.copy()
204
+ else:
205
+ # Try to load from JSON file
206
+ self._fallback_settings = self._load_json_fallback()
207
+
208
+ if self._fallback_settings is None:
209
+ # Use minimal default settings
210
+ self._fallback_settings = self._get_minimal_default_settings()
211
+
212
+ self.logger.warning("Fallback mode enabled - using JSON file system")
213
+ return True
214
+
215
+ except Exception as e:
216
+ self.logger.error(f"Failed to enable fallback mode: {e}")
217
+ return False
218
+
219
+ def disable_fallback_mode(self) -> bool:
220
+ """
221
+ Disable fallback mode and return to database system.
222
+
223
+ Returns:
224
+ True if fallback mode disabled successfully
225
+ """
226
+ try:
227
+ with self._lock:
228
+ self._fallback_mode = False
229
+ self._fallback_settings = None
230
+
231
+ self.logger.info("Fallback mode disabled - returning to database system")
232
+ return True
233
+
234
+ except Exception as e:
235
+ self.logger.error(f"Failed to disable fallback mode: {e}")
236
+ return False
237
+
238
+ def is_fallback_mode(self) -> bool:
239
+ """Check if currently in fallback mode."""
240
+ return self._fallback_mode
241
+
242
+ def get_fallback_settings(self) -> Optional[Dict[str, Any]]:
243
+ """Get current fallback settings."""
244
+ return self._fallback_settings.copy() if self._fallback_settings else None
245
+
246
+ def save_fallback_settings(self, settings: Dict[str, Any]) -> bool:
247
+ """
248
+ Save settings in fallback mode.
249
+
250
+ Args:
251
+ settings: Settings to save
252
+
253
+ Returns:
254
+ True if saved successfully
255
+ """
256
+ try:
257
+ if not self._fallback_mode:
258
+ return False
259
+
260
+ with self._lock:
261
+ # Update in-memory fallback settings
262
+ self._fallback_settings = settings.copy()
263
+
264
+ # Save to JSON file
265
+ return self._save_json_fallback(settings)
266
+
267
+ except Exception as e:
268
+ self.logger.error(f"Failed to save fallback settings: {e}")
269
+ return False
270
+
271
+ def attempt_recovery(self, category: ErrorCategory) -> bool:
272
+ """
273
+ Attempt recovery for a specific error category.
274
+
275
+ Args:
276
+ category: Error category to recover from
277
+
278
+ Returns:
279
+ True if recovery successful
280
+ """
281
+ if self._recovery_in_progress:
282
+ self.logger.warning("Recovery already in progress")
283
+ return False
284
+
285
+ try:
286
+ self._recovery_in_progress = True
287
+
288
+ recovery_proc = self._recovery_procedures.get(category)
289
+ if recovery_proc:
290
+ success = recovery_proc()
291
+ if success:
292
+ self.logger.info(f"Recovery successful for {category.value}")
293
+ else:
294
+ self.logger.error(f"Recovery failed for {category.value}")
295
+ return success
296
+ else:
297
+ self.logger.warning(f"No recovery procedure for {category.value}")
298
+ return False
299
+
300
+ except Exception as e:
301
+ self.logger.error(f"Recovery attempt failed: {e}")
302
+ return False
303
+ finally:
304
+ self._recovery_in_progress = False
305
+
306
+ def get_error_statistics(self) -> Dict[str, Any]:
307
+ """
308
+ Get error statistics and health information.
309
+
310
+ Returns:
311
+ Dictionary with error statistics
312
+ """
313
+ with self._lock:
314
+ total_errors = len(self._error_history)
315
+ recent_errors = [e for e in self._error_history
316
+ if e.timestamp > datetime.now() - timedelta(hours=24)]
317
+
318
+ critical_errors = [e for e in self._error_history
319
+ if e.severity == ErrorSeverity.CRITICAL]
320
+
321
+ stats = {
322
+ 'total_errors': total_errors,
323
+ 'recent_errors_24h': len(recent_errors),
324
+ 'critical_errors': len(critical_errors),
325
+ 'fallback_mode': self._fallback_mode,
326
+ 'recovery_in_progress': self._recovery_in_progress,
327
+ 'error_counts_by_category': dict(self._error_counts),
328
+ 'last_error_times': {
329
+ cat.value: time.isoformat()
330
+ for cat, time in self._last_error_time.items()
331
+ },
332
+ 'most_common_errors': self._get_most_common_errors(10)
333
+ }
334
+
335
+ return stats
336
+
337
+ def clear_error_history(self) -> None:
338
+ """Clear error history."""
339
+ with self._lock:
340
+ self._error_history.clear()
341
+ self._error_counts.clear()
342
+ self._last_error_time.clear()
343
+ self.logger.info("Error history cleared")
344
+
345
+ def validate_database_integrity(self, connection_manager) -> List[str]:
346
+ """
347
+ Validate database integrity and detect corruption.
348
+
349
+ Args:
350
+ connection_manager: Database connection manager
351
+
352
+ Returns:
353
+ List of integrity issues found
354
+ """
355
+ issues = []
356
+
357
+ try:
358
+ conn = connection_manager.get_connection()
359
+
360
+ # Check database integrity
361
+ cursor = conn.execute("PRAGMA integrity_check")
362
+ integrity_result = cursor.fetchone()[0]
363
+
364
+ if integrity_result != "ok":
365
+ issues.append(f"Database integrity check failed: {integrity_result}")
366
+
367
+ # Check foreign key constraints
368
+ cursor = conn.execute("PRAGMA foreign_key_check")
369
+ fk_violations = cursor.fetchall()
370
+
371
+ if fk_violations:
372
+ issues.append(f"Foreign key violations found: {len(fk_violations)}")
373
+
374
+ # Check table existence
375
+ required_tables = [
376
+ 'core_settings', 'tool_settings', 'tab_content',
377
+ 'performance_settings', 'font_settings', 'dialog_settings',
378
+ 'settings_metadata'
379
+ ]
380
+
381
+ cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
382
+ existing_tables = {row[0] for row in cursor.fetchall()}
383
+
384
+ missing_tables = set(required_tables) - existing_tables
385
+ if missing_tables:
386
+ issues.append(f"Missing tables: {', '.join(missing_tables)}")
387
+
388
+ # Check for empty critical tables
389
+ for table in ['core_settings']:
390
+ cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
391
+ count = cursor.fetchone()[0]
392
+ if count == 0:
393
+ issues.append(f"Critical table {table} is empty")
394
+
395
+ except Exception as e:
396
+ issues.append(f"Database validation failed: {e}")
397
+
398
+ return issues
399
+
400
+ # Private methods for error handling
401
+
402
+ def _record_error(self, error_info: ErrorInfo) -> None:
403
+ """Record error in history."""
404
+ with self._lock:
405
+ self._error_history.append(error_info)
406
+
407
+ # Update counts
408
+ category = error_info.category
409
+ self._error_counts[category] = self._error_counts.get(category, 0) + 1
410
+ self._last_error_time[category] = error_info.timestamp
411
+
412
+ # Trim history if too large
413
+ if len(self._error_history) > self.max_error_history:
414
+ self._error_history = self._error_history[-self.max_error_history:]
415
+
416
+ def _log_error(self, error_info: ErrorInfo) -> None:
417
+ """Log error with appropriate level."""
418
+ log_message = f"[{error_info.category.value}] {error_info.message}"
419
+
420
+ if error_info.exception:
421
+ log_message += f" - Exception: {error_info.exception}"
422
+
423
+ if error_info.context:
424
+ log_message += f" - Context: {error_info.context}"
425
+
426
+ if error_info.severity == ErrorSeverity.CRITICAL:
427
+ self.logger.critical(log_message)
428
+ elif error_info.severity == ErrorSeverity.HIGH:
429
+ self.logger.error(log_message)
430
+ elif error_info.severity == ErrorSeverity.MEDIUM:
431
+ self.logger.warning(log_message)
432
+ else:
433
+ self.logger.info(log_message)
434
+
435
+ def _determine_severity(self, category: ErrorCategory,
436
+ exception: Optional[Exception]) -> ErrorSeverity:
437
+ """Determine error severity based on category and exception."""
438
+ # Critical errors that prevent core functionality
439
+ if category in [ErrorCategory.DATABASE_CORRUPTION, ErrorCategory.MIGRATION]:
440
+ return ErrorSeverity.CRITICAL
441
+
442
+ # High severity errors that significantly impact functionality
443
+ if category in [ErrorCategory.DATABASE_CONNECTION, ErrorCategory.PERMISSION]:
444
+ return ErrorSeverity.HIGH
445
+
446
+ # Medium severity errors that cause inconvenience
447
+ if category in [ErrorCategory.DISK_SPACE, ErrorCategory.BACKUP]:
448
+ return ErrorSeverity.MEDIUM
449
+
450
+ # Check exception type for additional context
451
+ if isinstance(exception, (sqlite3.DatabaseError, sqlite3.CorruptError)):
452
+ return ErrorSeverity.CRITICAL
453
+ elif isinstance(exception, (PermissionError, OSError)):
454
+ return ErrorSeverity.HIGH
455
+
456
+ return ErrorSeverity.LOW
457
+
458
+ def _send_error_notification(self, error_info: ErrorInfo) -> None:
459
+ """Send error notification (placeholder for future implementation)."""
460
+ # This could be extended to send notifications via:
461
+ # - System notifications
462
+ # - Email alerts
463
+ # - Logging to external systems
464
+ # - UI notifications
465
+ pass
466
+
467
+ # Error handlers for specific categories
468
+
469
+ def _handle_database_connection_error(self, error_info: ErrorInfo) -> bool:
470
+ """Handle database connection errors."""
471
+ self.logger.warning("Database connection error - attempting fallback")
472
+
473
+ # Enable fallback mode
474
+ if self._fallback_enabled:
475
+ return self.enable_fallback_mode()
476
+
477
+ return False
478
+
479
+ def _handle_database_corruption_error(self, error_info: ErrorInfo) -> bool:
480
+ """Handle database corruption errors."""
481
+ self.logger.error("Database corruption detected - attempting recovery")
482
+
483
+ # Try to recover from backup
484
+ if self._auto_recovery_enabled:
485
+ return self.attempt_recovery(ErrorCategory.DATABASE_CORRUPTION)
486
+
487
+ # Fall back to JSON if recovery not enabled
488
+ if self._fallback_enabled:
489
+ return self.enable_fallback_mode()
490
+
491
+ return False
492
+
493
+ def _handle_disk_space_error(self, error_info: ErrorInfo) -> bool:
494
+ """Handle disk space errors."""
495
+ self.logger.warning("Disk space error - continuing with in-memory operations")
496
+
497
+ # Continue with in-memory operations, disable backups temporarily
498
+ return True
499
+
500
+ def _handle_permission_error(self, error_info: ErrorInfo) -> bool:
501
+ """Handle permission errors."""
502
+ self.logger.error("Permission error - attempting fallback")
503
+
504
+ # Try fallback mode
505
+ if self._fallback_enabled:
506
+ return self.enable_fallback_mode()
507
+
508
+ return False
509
+
510
+ def _handle_data_validation_error(self, error_info: ErrorInfo) -> bool:
511
+ """Handle data validation errors."""
512
+ self.logger.warning("Data validation error - using default values")
513
+
514
+ # Continue with default values for invalid data
515
+ return True
516
+
517
+ def _handle_migration_error(self, error_info: ErrorInfo) -> bool:
518
+ """Handle migration errors."""
519
+ self.logger.error("Migration error - attempting rollback")
520
+
521
+ # Try to rollback migration
522
+ if self._auto_recovery_enabled:
523
+ return self.attempt_recovery(ErrorCategory.MIGRATION)
524
+
525
+ return False
526
+
527
+ def _handle_backup_error(self, error_info: ErrorInfo) -> bool:
528
+ """Handle backup errors."""
529
+ self.logger.warning("Backup error - continuing without backup")
530
+
531
+ # Continue operations without backup
532
+ return True
533
+
534
+ def _handle_recovery_error(self, error_info: ErrorInfo) -> bool:
535
+ """Handle recovery errors."""
536
+ self.logger.error("Recovery error - enabling fallback mode")
537
+
538
+ # Enable fallback as last resort
539
+ if self._fallback_enabled:
540
+ return self.enable_fallback_mode()
541
+
542
+ return False
543
+
544
+ def _handle_generic_error(self, error_info: ErrorInfo) -> bool:
545
+ """Handle generic/unknown errors."""
546
+ self.logger.warning(f"Generic error: {error_info.message}")
547
+
548
+ # Try fallback for unknown errors
549
+ if error_info.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
550
+ if self._fallback_enabled:
551
+ return self.enable_fallback_mode()
552
+
553
+ return True
554
+
555
+ # Recovery procedures
556
+
557
+ def _recover_database_connection(self) -> bool:
558
+ """Recover from database connection issues."""
559
+ try:
560
+ # Wait a moment and retry connection
561
+ time.sleep(1)
562
+
563
+ # This would be implemented by the connection manager
564
+ # For now, just return success to indicate recovery attempt
565
+ self.logger.info("Database connection recovery attempted")
566
+ return True
567
+
568
+ except Exception as e:
569
+ self.logger.error(f"Database connection recovery failed: {e}")
570
+ return False
571
+
572
+ def _recover_database_corruption(self) -> bool:
573
+ """Recover from database corruption."""
574
+ try:
575
+ # Try to restore from most recent backup
576
+ backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
577
+ if backup_files:
578
+ # Sort by modification time, get most recent
579
+ latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
580
+
581
+ self.logger.info(f"Attempting to restore from backup: {latest_backup}")
582
+ # This would be implemented by the connection manager
583
+ return True
584
+ else:
585
+ self.logger.error("No backup files found for recovery")
586
+ return False
587
+
588
+ except Exception as e:
589
+ self.logger.error(f"Database corruption recovery failed: {e}")
590
+ return False
591
+
592
+ def _recover_disk_space(self) -> bool:
593
+ """Recover from disk space issues."""
594
+ try:
595
+ # Clean up old backup files
596
+ backup_files = list(self.backup_dir.glob("settings_backup_*.db"))
597
+ if len(backup_files) > 5: # Keep only 5 most recent
598
+ old_backups = sorted(backup_files, key=lambda p: p.stat().st_mtime)[:-5]
599
+ for backup in old_backups:
600
+ backup.unlink()
601
+ self.logger.info(f"Cleaned up old backup: {backup}")
602
+
603
+ return True
604
+
605
+ except Exception as e:
606
+ self.logger.error(f"Disk space recovery failed: {e}")
607
+ return False
608
+
609
+ def _recover_permission(self) -> bool:
610
+ """Recover from permission issues."""
611
+ try:
612
+ # Try to change file permissions if possible
613
+ # This is a placeholder - actual implementation would depend on OS
614
+ self.logger.info("Permission recovery attempted")
615
+ return False # Usually requires manual intervention
616
+
617
+ except Exception as e:
618
+ self.logger.error(f"Permission recovery failed: {e}")
619
+ return False
620
+
621
+ def _recover_data_validation(self) -> bool:
622
+ """Recover from data validation issues."""
623
+ try:
624
+ # Reset to default values for invalid data
625
+ self.logger.info("Data validation recovery - using defaults")
626
+ return True
627
+
628
+ except Exception as e:
629
+ self.logger.error(f"Data validation recovery failed: {e}")
630
+ return False
631
+
632
+ def _recover_migration(self) -> bool:
633
+ """Recover from migration issues."""
634
+ try:
635
+ # Try to rollback to original JSON file
636
+ backup_files = list(Path(".").glob("settings.json.backup_*"))
637
+ if backup_files:
638
+ latest_backup = max(backup_files, key=lambda p: p.stat().st_mtime)
639
+ shutil.copy2(latest_backup, self.json_fallback_path)
640
+ self.logger.info(f"Migration rollback completed: {latest_backup}")
641
+ return True
642
+ else:
643
+ self.logger.error("No JSON backup found for migration rollback")
644
+ return False
645
+
646
+ except Exception as e:
647
+ self.logger.error(f"Migration recovery failed: {e}")
648
+ return False
649
+
650
+ def _recover_backup(self) -> bool:
651
+ """Recover from backup issues."""
652
+ try:
653
+ # Try alternative backup location or method
654
+ self.logger.info("Backup recovery attempted")
655
+ return True
656
+
657
+ except Exception as e:
658
+ self.logger.error(f"Backup recovery failed: {e}")
659
+ return False
660
+
661
+ # Fallback JSON operations
662
+
663
+ def _load_json_fallback(self) -> Optional[Dict[str, Any]]:
664
+ """Load settings from JSON fallback file."""
665
+ try:
666
+ if os.path.exists(self.json_fallback_path):
667
+ with open(self.json_fallback_path, 'r', encoding='utf-8') as f:
668
+ return json.load(f)
669
+ else:
670
+ self.logger.warning(f"JSON fallback file not found: {self.json_fallback_path}")
671
+ return None
672
+
673
+ except Exception as e:
674
+ self.logger.error(f"Failed to load JSON fallback: {e}")
675
+ return None
676
+
677
+ def _save_json_fallback(self, settings: Dict[str, Any]) -> bool:
678
+ """Save settings to JSON fallback file."""
679
+ try:
680
+ with open(self.json_fallback_path, 'w', encoding='utf-8') as f:
681
+ json.dump(settings, f, indent=2, ensure_ascii=False)
682
+ return True
683
+
684
+ except Exception as e:
685
+ self.logger.error(f"Failed to save JSON fallback: {e}")
686
+ return False
687
+
688
+ def _get_minimal_default_settings(self) -> Dict[str, Any]:
689
+ """Get minimal default settings for emergency fallback."""
690
+ return {
691
+ "export_path": str(Path.home() / "Downloads"),
692
+ "debug_level": "INFO",
693
+ "selected_tool": "Case Tool",
694
+ "active_input_tab": 0,
695
+ "active_output_tab": 0,
696
+ "input_tabs": [""] * 7,
697
+ "output_tabs": [""] * 7,
698
+ "tool_settings": {},
699
+ "performance_settings": {"mode": "automatic"},
700
+ "font_settings": {"text_font": {"family": "Consolas", "size": 11}},
701
+ "dialog_settings": {"error": {"enabled": True, "locked": True}}
702
+ }
703
+
704
+ def _get_most_common_errors(self, limit: int) -> List[Dict[str, Any]]:
705
+ """Get most common error categories."""
706
+ error_counts = {}
707
+
708
+ for error in self._error_history:
709
+ key = f"{error.category.value}: {error.message[:50]}"
710
+ error_counts[key] = error_counts.get(key, 0) + 1
711
+
712
+ # Sort by count and return top N
713
+ sorted_errors = sorted(error_counts.items(), key=lambda x: x[1], reverse=True)
714
+
715
+ return [
716
+ {"error": error, "count": count}
717
+ for error, count in sorted_errors[:limit]
718
+ ]
719
+
720
+
721
+ # Global error handler instance
722
+ _global_error_handler: Optional[SettingsErrorHandler] = None
723
+
724
+
725
+ def get_error_handler() -> SettingsErrorHandler:
726
+ """Get the global error handler instance."""
727
+ global _global_error_handler
728
+ if _global_error_handler is None:
729
+ _global_error_handler = SettingsErrorHandler()
730
+ return _global_error_handler
731
+
732
+
733
+ def initialize_error_handler(json_fallback_path: str = "settings.json",
734
+ backup_dir: str = "backups") -> SettingsErrorHandler:
735
+ """
736
+ Initialize the global error handler.
737
+
738
+ Args:
739
+ json_fallback_path: Path to JSON fallback file
740
+ backup_dir: Directory for backup files
741
+
742
+ Returns:
743
+ Initialized error handler instance
744
+ """
745
+ global _global_error_handler
746
+ _global_error_handler = SettingsErrorHandler(json_fallback_path, backup_dir)
747
747
  return _global_error_handler