Reporting updated.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nickviljoen 2026-01-14 09:14:00 +02:00
parent 42f654f78b
commit 96d0bf95e1
5 changed files with 196 additions and 14 deletions

View file

@ -13,6 +13,10 @@ A Flask-based web application for viewing and analyzing QC reports stored in Box
- Parsed Data View: Structured display of check results with filtering
- Embedded Reports: View original HTML reports inline
- **Aggregated Summary**: Overview of all checks across multiple files
- **Quick Navigation**: Click "View Details" link next to error files to jump directly to that report
- **HTML Export Options**:
- Export Combined Report: All reports in a single HTML file
- Export Error Reports Only: Filter to only files with errors
- **PDF Export**: Export combined reports as a single PDF document (requires WeasyPrint setup)
- **Error Highlighting**: Quickly identify files with errors
- **User Session Management**: httpOnly cookies with automatic logout
@ -206,7 +210,8 @@ web_hm_ai_qc_report/
The dashboard shows:
- **Summary**: Overall statistics (files checked, total checks, passed/errors/warnings)
- **Files with Errors**: Quick list of problematic files
- **Files with Errors**: Quick list of problematic files with "View Details" links
- Click the "View Details" link to jump directly to that report and see which checks failed
- **Parsed Data View**: Structured check results with filtering options
- **Embedded Reports**: Original HTML reports in iframes
@ -217,14 +222,28 @@ Use the filter buttons to view:
- Only files with errors
- Only files that passed all checks
### 5. Export to PDF
### 5. Export Reports
Click "Export Combined Report as PDF" to download a PDF containing:
- Overall summary
- All check results
- Detailed findings for each file
Two export options are available:
**Note**: PDF export requires WeasyPrint system dependencies (see Troubleshooting).
**Export Combined Report as HTML** (Green button):
- Downloads all reports in a single HTML file
- Includes all files (passed and failed)
- Can be converted to PDF using browser's Print function
**Export Error Reports Only** (Red button):
- Only appears when there are files with errors
- Downloads only files that have errors
- Excludes files that passed all checks
- Filename includes "_ERRORS_ONLY_" suffix
- Shows warning banner indicating filtered content
To convert HTML export to PDF:
1. Open the downloaded HTML file in your browser
2. Press Ctrl/Cmd + P to print
3. Select "Save as PDF" as the destination
**Note**: Native PDF export requires WeasyPrint system dependencies (see Troubleshooting).
### 6. Sign Out
@ -243,7 +262,9 @@ Click the "Logout" button in the navbar to sign out.
- `GET /dashboard/<job_number>` - Dashboard for specific job
- `GET /api/report/<file_id>` - Get parsed report data (JSON)
- `GET /api/report/<file_id>/raw` - Get raw HTML report
- `GET /export/pdf/<job_number>` - Export combined PDF
- `GET /export/html/<job_number>` - Export combined HTML report (all files)
- `GET /export/html/<job_number>/errors` - Export HTML report (error files only)
- `GET /export/pdf/<job_number>` - Export combined PDF (requires WeasyPrint)
## Box Folder Structure
@ -474,6 +495,17 @@ For issues or questions, contact the development team.
## Version History
### v2.2.0 - Enhanced Navigation and Export (January 2026)
- **Added Quick Navigation**: "View Details" links next to error files for direct navigation
- Automatically switches to Parsed Data View tab
- Scrolls to and expands the specific report
- Highlights the report briefly for easy identification
- **Added Error-Only Export**: New export option to download only files with errors
- Separate red button appears when errors are present
- Filename includes "_ERRORS_ONLY_" suffix
- Export includes warning banner to indicate filtered content
- **Improved User Experience**: Streamlined workflow for reviewing and sharing error reports
### v2.1.0 - CAMPAIGNS Folder Integration (December 2025)
- **Updated folder structure** to support CAMPAIGNS/{CampaignNumber}/QC/ hierarchy
- **Added real-time progress indicator** with status updates during search

75
app.py
View file

@ -287,6 +287,81 @@ def export_html(job_number):
return jsonify({'error': str(e)}), 500
@app.route('/export/html/<job_number>/errors')
@auth.require_auth
def export_html_errors_only(job_number):
"""
Export error reports only as HTML.
Args:
job_number: Job/reference number
Returns:
HTML file download containing only reports with errors
"""
try:
logger.info(f"Exporting error reports only for job number: {job_number}")
# Search for reports
box_client = get_box_client()
reports = box_client.search_by_job_number(job_number)
if not reports:
return jsonify({'error': f'No reports found for job number: {job_number}'}), 404
# Download and parse reports
parsed_reports = []
for report_info in reports:
try:
content = box_client.download_file(report_info['id'])
html_content = content.decode('utf-8')
parser = QCReportParser(html_content)
parsed_data = parser.parse()
parsed_data['html_content'] = html_content
# Only include reports with errors
if parsed_data.get('summary', {}).get('error', 0) > 0:
parsed_reports.append(parsed_data)
except Exception as e:
logger.error(f"Error parsing report for error export: {e}")
# Check if any error reports were found
if not parsed_reports:
return jsonify({'error': f'No error reports found for job number: {job_number}'}), 404
# Generate aggregate summary (only for error reports)
aggregated = aggregate_reports(parsed_reports)
# Render HTML template
html_string = render_template(
'pdf_export.html',
job_number=job_number,
reports=parsed_reports,
aggregated=aggregated,
generated_at=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
errors_only=True
)
# Generate filename
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'QC_Report_{job_number}_ERRORS_ONLY_{timestamp}.html'
# Return HTML as downloadable file
return send_file(
BytesIO(html_string.encode('utf-8')),
mimetype='text/html',
as_attachment=True,
download_name=filename
)
except Exception as e:
logger.error(f"Error exporting error reports: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/health')
def health():
"""Health check endpoint."""

4
run.sh
View file

@ -22,6 +22,6 @@ fi
# Run Flask app
echo "Starting Flask development server..."
echo "App will be available at: http://localhost:5000"
echo "App will be available at: http://localhost:7183"
echo ""
python app.py
./venv/bin/python app.py

View file

@ -105,7 +105,12 @@
<ul class="list-group">
{% for file in aggregated.files_with_errors %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ file.filename }}
<div class="flex-grow-1">
{{ file.filename }}
<a href="#" class="ms-2 text-primary" onclick="scrollToReport('{{ file.filename }}'); return false;" title="View error details">
<i class="bi bi-box-arrow-down-right"></i> View Details
</a>
</div>
<span class="badge bg-danger rounded-pill">{{ file.error_count }} error(s)</span>
</li>
{% endfor %}
@ -117,12 +122,17 @@
</div>
</div>
<!-- Export Button -->
<!-- Export Buttons -->
<div class="row mb-3">
<div class="col-12">
<a href="/export/html/{{ job_number }}" class="btn btn-success" download>
<a href="/export/html/{{ job_number }}" class="btn btn-success me-2" download>
<i class="bi bi-file-earmark-code"></i> Export Combined Report as HTML
</a>
{% if aggregated.files_with_errors %}
<a href="/export/html/{{ job_number }}/errors" class="btn btn-danger" download>
<i class="bi bi-exclamation-triangle"></i> Export Error Reports Only
</a>
{% endif %}
</div>
</div>
@ -163,7 +173,7 @@
<div class="accordion file-accordion" id="parsedAccordion">
{% for report in reports %}
<div class="accordion-item mb-3" data-has-errors="{{ 'true' if report.summary.error > 0 else 'false' }}" data-all-passed="{{ 'true' if report.summary.error == 0 and report.summary.warning == 0 else 'false' }}">
<div class="accordion-item mb-3" id="report-{{ loop.index }}" data-has-errors="{{ 'true' if report.summary.error > 0 else 'false' }}" data-all-passed="{{ 'true' if report.summary.error == 0 and report.summary.warning == 0 else 'false' }}" data-filename="{{ report.filename }}">
<h2 class="accordion-header" id="heading-{{ loop.index }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ loop.index }}">
<div class="d-flex w-100 align-items-center">
@ -311,6 +321,52 @@
});
});
});
// Scroll to report functionality
function scrollToReport(filename) {
// First, ensure we're on the Parsed Data View tab
const parsedTab = document.getElementById('parsed-tab');
const parsedView = document.getElementById('parsed-view');
if (!parsedView.classList.contains('show')) {
parsedTab.click();
}
// Find the accordion item with matching filename
const accordionItems = document.querySelectorAll('#parsedAccordion .accordion-item');
let targetItem = null;
accordionItems.forEach(item => {
if (item.getAttribute('data-filename') === filename) {
targetItem = item;
}
});
if (targetItem) {
// Clear any active filters to ensure the item is visible
const allFilterBtn = document.querySelector('.filter-btn[data-filter="all"]');
if (allFilterBtn && !allFilterBtn.classList.contains('active')) {
allFilterBtn.click();
}
// Scroll to the item
setTimeout(() => {
targetItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Expand the accordion
const collapseButton = targetItem.querySelector('.accordion-button');
if (collapseButton && collapseButton.classList.contains('collapsed')) {
collapseButton.click();
}
// Highlight the item briefly
targetItem.style.transition = 'background-color 0.5s';
targetItem.style.backgroundColor = '#fff3cd';
setTimeout(() => {
targetItem.style.backgroundColor = '';
}, 2000);
}, 300);
}
}
</script>
</body>
</html>

View file

@ -132,6 +132,18 @@
color: #666;
text-align: center;
}
.errors-only-banner {
background-color: #fce4e4;
border: 2px solid #e74c3c;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
text-align: center;
}
.errors-only-banner h3 {
color: #e74c3c;
margin: 0 0 5px 0;
}
table {
width: 100%;
border-collapse: collapse;
@ -158,6 +170,13 @@
<p><strong>Generated:</strong> {{ generated_at }}</p>
</div>
{% if errors_only %}
<div class="errors-only-banner">
<h3>⚠️ ERROR REPORTS ONLY</h3>
<p>This export contains only files with errors. Files that passed all checks are excluded.</p>
</div>
{% endif %}
<div class="summary">
<h3>Overall Summary</h3>
<div class="summary-grid">