Reporting updated.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
42f654f78b
commit
96d0bf95e1
5 changed files with 196 additions and 14 deletions
48
README.md
48
README.md
|
|
@ -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
75
app.py
|
|
@ -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
4
run.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue