ford_qc/checks/html_error_reporter.py
2025-09-03 07:03:21 -05:00

569 lines
No EOL
26 KiB
Python

import os
import json
from datetime import datetime
import logging
import traceback
from typing import Dict, Any, Optional
class HTMLErrorReporter:
"""
Generates user-friendly HTML error reports when QC processing fails.
Provides actionable error messages and fix instructions for users.
"""
@staticmethod
def generate_error_report(
error_type: str,
error_message: str,
filename: str,
reports_dir: str,
error_details: Optional[Dict[str, Any]] = None,
exception_info: Optional[str] = None
) -> str:
"""
Generate an HTML error report.
:param error_type: Type of error (e.g., 'zip_extraction', 'json_parsing', 'file_access')
:param error_message: Human-readable error message
:param filename: Name of the file that caused the error
:param reports_dir: Directory to save the error report
:param error_details: Additional error details dictionary
:param exception_info: Exception traceback information
:return: Path to the generated error report
"""
try:
os.makedirs(reports_dir, exist_ok=True)
timestamp = datetime.now()
date_str = timestamp.isoformat(timespec='seconds').replace(":", "-")
safe_name = os.path.basename(filename).replace('.zip', '').replace('.', '_')
output_path = os.path.join(reports_dir, f"{safe_name}_{date_str}_ERROR.html")
html_content = HTMLErrorReporter._build_error_html(
error_type=error_type,
error_message=error_message,
filename=filename,
timestamp=timestamp,
error_details=error_details or {},
exception_info=exception_info
)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
logging.info(f"Generated HTML error report: {output_path}")
return output_path
except Exception as e:
logging.exception("Critical error generating HTML error report")
# Fallback to text error report if HTML generation fails
fallback_path = os.path.join(reports_dir, f"{safe_name}_{date_str}_ERROR.txt")
try:
with open(fallback_path, 'w', encoding='utf-8') as f:
f.write(f"Error Type: {error_type}\n")
f.write(f"Error Message: {error_message}\n")
f.write(f"Filename: {filename}\n")
f.write(f"Timestamp: {timestamp}\n")
if exception_info:
f.write(f"Exception Info:\n{exception_info}\n")
return fallback_path
except Exception:
logging.exception("Failed to create fallback text error report")
raise RuntimeError(f"Critical failure: Unable to generate any error report for {filename}")
@staticmethod
def _build_error_html(
error_type: str,
error_message: str,
filename: str,
timestamp: datetime,
error_details: Dict[str, Any],
exception_info: Optional[str]
) -> str:
"""Build complete HTML error report."""
# Get error-specific content
error_info = HTMLErrorReporter._get_error_info(error_type, error_message, error_details)
return f'''
<!DOCTYPE html>
<html lang="en">
{HTMLErrorReporter._build_head(filename)}
<body>
<div class="container py-4">
{HTMLErrorReporter._build_error_header(filename, timestamp, error_type)}
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-danger">
<h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> Processing Failed</h4>
<p class="mb-0">{error_message}</p>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="fas fa-tools"></i> How to Fix This Issue</h5>
</div>
<div class="card-body">
{error_info['fix_instructions']}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-info">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fas fa-upload"></i> Re-upload Instructions</h5>
</div>
<div class="card-body">
{error_info['reupload_instructions']}
</div>
</div>
</div>
</div>
<div class="accordion mb-4" id="errorDetails">
<div class="accordion-item">
<h2 class="accordion-header" id="headingDetails">
<button class="accordion-button collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseDetails"
aria-expanded="false" aria-controls="collapseDetails">
<i class="fas fa-info-circle me-2"></i> Technical Details
</button>
</h2>
<div id="collapseDetails" class="accordion-collapse collapse"
aria-labelledby="headingDetails" data-bs-parent="#errorDetails">
<div class="accordion-body">
<div class="mb-3">
<h6>Error Type:</h6>
<span class="badge bg-danger">{error_type.replace('_', ' ').title()}</span>
</div>
<div class="mb-3">
<h6>File Information:</h6>
<ul class="list-unstyled">
<li><strong>Filename:</strong> {filename}</li>
<li><strong>Error Time:</strong> {timestamp.strftime('%Y-%m-%d %H:%M:%S')}</li>
</ul>
</div>
{HTMLErrorReporter._format_error_details(error_details)}
{HTMLErrorReporter._format_exception_info(exception_info)}
</div>
</div>
</div>
</div>
<div class="alert alert-info">
<h6><i class="fas fa-question-circle"></i> Need Help?</h6>
<p class="mb-0">
If you continue to experience issues after following the fix instructions,
please contact the technical support team and include this error report.
</p>
</div>
</div>
{HTMLErrorReporter._build_scripts()}
</body>
</html>
'''
@staticmethod
def _build_head(filename: str) -> str:
"""Build HTML head section."""
return f'''
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QC Error Report - {filename}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
{HTMLErrorReporter._get_css_styles()}
</style>
</head>
'''
@staticmethod
def _get_css_styles() -> str:
"""Get CSS styles for error reports."""
return '''
.error-header {
background: linear-gradient(135deg, #dc3545, #c82333);
color: white;
padding: 1.5rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
}
.error-icon {
font-size: 3rem;
opacity: 0.8;
}
.fix-instructions ol, .fix-instructions ul {
padding-left: 1.5rem;
}
.fix-instructions li {
margin-bottom: 0.5rem;
}
pre {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 4px;
font-size: 0.875rem;
max-height: 300px;
overflow-y: auto;
}
.card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.alert {
border: none;
border-radius: 0.5rem;
}
'''
@staticmethod
def _build_error_header(filename: str, timestamp: datetime, error_type: str) -> str:
"""Build error report header."""
return f'''
<div class="error-header">
<div class="row align-items-center">
<div class="col-auto">
<i class="fas fa-exclamation-triangle error-icon"></i>
</div>
<div class="col">
<h1 class="h2 mb-1">QC Processing Error</h1>
<h4 class="mb-2 text-light">{filename}</h4>
<div class="text-light opacity-75">
<div>Error Type: {error_type.replace('_', ' ').title()}</div>
<div>Error Time: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}</div>
</div>
</div>
</div>
</div>
'''
@staticmethod
def _build_scripts() -> str:
"""Build JavaScript section."""
return '''
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
'''
@staticmethod
def _get_error_info(error_type: str, error_message: str, error_details: Dict[str, Any]) -> Dict[str, str]:
"""Get error-specific fix instructions and re-upload guidance."""
error_templates = {
'zip_extraction': {
'fix_instructions': '''
<div class="fix-instructions">
<p>The ZIP file could not be extracted properly. Common causes and solutions:</p>
<ol>
<li><strong>Corrupted ZIP file:</strong> Re-create the ZIP file using a reliable archiver</li>
<li><strong>Unsupported compression:</strong> Use standard ZIP compression (not RAR, 7z, etc.)</li>
<li><strong>Password-protected ZIP:</strong> Remove password protection</li>
<li><strong>Large file size:</strong> Ensure ZIP is under size limits</li>
<li><strong>Invalid characters:</strong> Avoid special characters in filenames inside ZIP</li>
</ol>
<p class="mb-0"><strong>Test your ZIP:</strong> Before uploading, extract it locally to verify it works properly.</p>
</div>
''',
'reupload_instructions': '''
<div>
<ol>
<li>Fix the ZIP file using the instructions provided</li>
<li>Test the ZIP file by extracting it locally</li>
<li>Ensure all required files are present (especially linkingrecord.json)</li>
<li>Re-upload the corrected ZIP file to the input folder</li>
<li>Monitor for a new QC report within a few minutes</li>
</ol>
</div>
'''
},
'json_parsing': {
'fix_instructions': '''
<div class="fix-instructions">
<p>The JSON file (likely linkingrecord.json) contains syntax errors:</p>
<ol>
<li><strong>Use a JSON validator:</strong> Copy your JSON content to <a href="https://jsonlint.com/" target="_blank">jsonlint.com</a></li>
<li><strong>Common issues to check:</strong>
<ul>
<li>Missing or extra commas</li>
<li>Unmatched brackets { } or [ ]</li>
<li>Use double quotes (") not single quotes (')</li>
<li>Remove tab characters and control characters</li>
<li>Ensure proper UTF-8 encoding</li>
</ul>
</li>
<li><strong>Re-export JSON:</strong> If possible, regenerate the JSON from your source system</li>
</ol>
</div>
''',
'reupload_instructions': '''
<div>
<ol>
<li>Validate and fix the JSON file syntax</li>
<li>Ensure the JSON file is UTF-8 encoded</li>
<li>Test the JSON in a validator tool</li>
<li>Re-create the ZIP file with the corrected JSON</li>
<li>Upload the new ZIP file</li>
</ol>
</div>
'''
},
'file_access': {
'fix_instructions': '''
<div class="fix-instructions">
<p>The file could not be accessed or read:</p>
<ol>
<li><strong>File not found:</strong> Verify the file exists and path is correct</li>
<li><strong>Permission issues:</strong> Check file permissions and access rights</li>
<li><strong>File in use:</strong> Ensure the file is not locked by another process</li>
<li><strong>Network issues:</strong> For remote files, check network connectivity</li>
<li><strong>Disk space:</strong> Ensure sufficient disk space for processing</li>
</ol>
</div>
''',
'reupload_instructions': '''
<div>
<ol>
<li>Verify the file is accessible and not corrupted</li>
<li>Check file permissions and ensure it's not locked</li>
<li>Try uploading the file again</li>
<li>If using a network location, verify connectivity</li>
<li>Contact support if the issue persists</li>
</ol>
</div>
'''
},
'qc_profile': {
'fix_instructions': '''
<div class="fix-instructions">
<p>There was an issue with the QC configuration profile:</p>
<ol>
<li><strong>Profile not found:</strong> Verify the QC profile file exists</li>
<li><strong>Invalid JSON:</strong> Check profile JSON syntax</li>
<li><strong>Missing checks:</strong> Ensure all required check modules are available</li>
<li><strong>Configuration error:</strong> Verify check configurations are valid</li>
</ol>
<p class="mb-0"><strong>Note:</strong> This is typically a system configuration issue. Contact technical support.</p>
</div>
''',
'reupload_instructions': '''
<div>
<ol>
<li>This appears to be a system configuration issue</li>
<li>Please contact technical support</li>
<li>Include this error report when contacting support</li>
<li>Do not modify system configuration files</li>
</ol>
</div>
'''
},
'check_execution': {
'fix_instructions': '''
<div class="fix-instructions">
<p>An error occurred while running QC checks on your asset pack:</p>
<ol>
<li><strong>Missing required files:</strong> Ensure all required files are in the ZIP</li>
<li><strong>Invalid file formats:</strong> Check image formats and specifications</li>
<li><strong>Corrupted files:</strong> Verify all files in the pack are not corrupted</li>
<li><strong>Naming conventions:</strong> Check file and folder naming follows requirements</li>
<li><strong>File structure:</strong> Ensure directory structure matches requirements</li>
</ol>
</div>
''',
'reupload_instructions': '''
<div>
<ol>
<li>Review your asset pack contents</li>
<li>Fix any identified issues with files or structure</li>
<li>Re-create the ZIP file</li>
<li>Upload the corrected asset pack</li>
<li>If errors persist, check technical details below</li>
</ol>
</div>
'''
},
'network_upload': {
'fix_instructions': '''
<div class="fix-instructions">
<p>Failed to upload results due to network or connectivity issues:</p>
<ol>
<li><strong>Network connectivity:</strong> Check internet connection</li>
<li><strong>Service availability:</strong> Verify upload service is accessible</li>
<li><strong>File size limits:</strong> Ensure files are within upload limits</li>
<li><strong>Permissions:</strong> Check upload permissions and authentication</li>
</ol>
<p class="mb-0"><strong>Note:</strong> Your QC processing may have completed successfully, but results couldn't be delivered.</p>
</div>
''',
'reupload_instructions': '''
<div>
<ol>
<li>Check your network connection</li>
<li>Try uploading your file again</li>
<li>If the issue persists, contact technical support</li>
<li>The processing may be working, but results delivery failed</li>
</ol>
</div>
'''
},
'unknown': {
'fix_instructions': '''
<div class="fix-instructions">
<p>An unexpected error occurred during processing:</p>
<ol>
<li><strong>Try again:</strong> Sometimes temporary issues resolve themselves</li>
<li><strong>Check file integrity:</strong> Ensure your ZIP file is not corrupted</li>
<li><strong>Verify contents:</strong> Make sure all required files are present</li>
<li><strong>Contact support:</strong> If the issue persists, technical support can help</li>
</ol>
</div>
''',
'reupload_instructions': '''
<div>
<ol>
<li>Wait a few minutes and try uploading again</li>
<li>If the error persists, contact technical support</li>
<li>Include this error report when contacting support</li>
<li>Provide details about your asset pack contents</li>
</ol>
</div>
'''
}
}
return error_templates.get(error_type, error_templates['unknown'])
@staticmethod
def _format_error_details(error_details: Dict[str, Any]) -> str:
"""Format additional error details."""
if not error_details:
return ""
formatted_details = []
for key, value in error_details.items():
if isinstance(value, (dict, list)):
formatted_details.append(f"<li><strong>{key}:</strong><pre>{json.dumps(value, indent=2)}</pre></li>")
else:
formatted_details.append(f"<li><strong>{key}:</strong> {value}</li>")
if formatted_details:
return f'''
<div class="mb-3">
<h6>Additional Details:</h6>
<ul class="list-unstyled">
{''.join(formatted_details)}
</ul>
</div>
'''
return ""
@staticmethod
def _format_exception_info(exception_info: Optional[str]) -> str:
"""Format exception traceback information."""
if not exception_info:
return ""
return f'''
<div class="mb-3">
<h6>Exception Information:</h6>
<pre>{exception_info}</pre>
</div>
'''
# Convenience functions for common error types
def generate_zip_error_report(filename: str, reports_dir: str, error_message: str, exception: Exception = None) -> str:
"""Generate error report for ZIP extraction failures."""
exception_info = traceback.format_exc() if exception else None
return HTMLErrorReporter.generate_error_report(
error_type='zip_extraction',
error_message=error_message,
filename=filename,
reports_dir=reports_dir,
exception_info=exception_info
)
def generate_json_error_report(filename: str, reports_dir: str, error_message: str, json_file: str = None, exception: Exception = None) -> str:
"""Generate error report for JSON parsing failures."""
error_details = {'json_file': json_file} if json_file else {}
exception_info = traceback.format_exc() if exception else None
return HTMLErrorReporter.generate_error_report(
error_type='json_parsing',
error_message=error_message,
filename=filename,
reports_dir=reports_dir,
error_details=error_details,
exception_info=exception_info
)
def generate_file_access_error_report(filename: str, reports_dir: str, error_message: str, file_path: str = None, exception: Exception = None) -> str:
"""Generate error report for file access failures."""
error_details = {'attempted_path': file_path} if file_path else {}
exception_info = traceback.format_exc() if exception else None
return HTMLErrorReporter.generate_error_report(
error_type='file_access',
error_message=error_message,
filename=filename,
reports_dir=reports_dir,
error_details=error_details,
exception_info=exception_info
)
def generate_qc_check_error_report(filename: str, reports_dir: str, error_message: str, failed_check: str = None, exception: Exception = None) -> str:
"""Generate error report for QC check execution failures."""
error_details = {'failed_check': failed_check} if failed_check else {}
exception_info = traceback.format_exc() if exception else None
return HTMLErrorReporter.generate_error_report(
error_type='check_execution',
error_message=error_message,
filename=filename,
reports_dir=reports_dir,
error_details=error_details,
exception_info=exception_info
)
def generate_network_error_report(filename: str, reports_dir: str, error_message: str, operation: str = None, exception: Exception = None) -> str:
"""Generate error report for network/upload failures."""
error_details = {'failed_operation': operation} if operation else {}
exception_info = traceback.format_exc() if exception else None
return HTMLErrorReporter.generate_error_report(
error_type='network_upload',
error_message=error_message,
filename=filename,
reports_dir=reports_dir,
error_details=error_details,
exception_info=exception_info
)
if __name__ == "__main__":
# Test the error reporter
import sys
if len(sys.argv) != 4:
print("Usage: python html_error_reporter.py <error_type> <filename> <output_dir>")
print("Error types: zip_extraction, json_parsing, file_access, qc_profile, check_execution, network_upload")
sys.exit(1)
error_type = sys.argv[1]
filename = sys.argv[2]
output_dir = sys.argv[3]
try:
report_path = HTMLErrorReporter.generate_error_report(
error_type=error_type,
error_message=f"Test error message for {error_type}",
filename=filename,
reports_dir=output_dir,
error_details={'test_detail': 'This is a test error report'},
exception_info="Test exception traceback information"
)
print(f"Generated test error report: {report_path}")
except Exception as e:
print(f"Error generating test report: {e}")
sys.exit(1)