Add collapsible ZIP contents and skipped files sections

- Make ZIP contents collapsible with toggle functionality
- Make skipped files section collapsible with warning styling
- Add JavaScript functions for collapsible sections
- Create dedicated /completed-jobs route for archive purposes
- Add navigation links between jobs pages
- Include job creation timestamps for archive display
- Improve job status display with file counts and statistics
- Add datetime import for timestamp functionality
This commit is contained in:
nmemmert committed 2025-11-06 12:35:38 -05:00
1 parent d28cccc957
commit 36f1722fbe
1 file changed
+206 -21
+206 -21
View File
@@ -3,6 +3,7 @@ import tempfile
import zipfile import zipfile
import threading import threading
import uuid import uuid
from datetime import datetime
from functools import wraps from functools import wraps
from flask import Flask, request, render_template_string, redirect, url_for, session, send_from_directory, flash, get_flashed_messages, jsonify from flask import Flask, request, render_template_string, redirect, url_for, session, send_from_directory, flash, get_flashed_messages, jsonify
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -666,7 +667,8 @@ def upload_files():
'zip_path': None, 'zip_path': None,
'zip_name': None, 'zip_name': None,
'processed_files': [], 'processed_files': [],
'skipped': [] 'skipped': [],
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
} }
file_list = [] file_list = []
@@ -756,7 +758,17 @@ def jobs():
.flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.logout { position: absolute; top: 20px; right: 20px; background-color: #dc3545; color: white; text-decoration: none; padding: 10px 20px; border-radius: 5px; } .logout { position: absolute; top: 20px; right: 20px; background-color: #dc3545; color: white; text-decoration: none; padding: 10px 20px; border-radius: 5px; }
.logout:hover { background-color: #c82333; } .logout:hover { background-color: #c82333; }
.nav-links { margin-bottom: 20px; }
.nav-links a { color: #007bff; text-decoration: none; padding: 10px 20px; background-color: #f8f9fa; border-radius: 5px; margin: 0 10px; }
.nav-links a:hover { background-color: #e9ecef; }
.zip-content { background-color: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #007bff; } .zip-content { background-color: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #007bff; }
.zip-toggle { cursor: pointer; background-color: #e9ecef; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #dee2e6; }
.zip-toggle:hover { background-color: #dee2e6; }
.zip-details { display: none; margin-top: 10px; }
.skipped-section { background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; padding: 10px; margin: 10px 0; }
.skipped-toggle { cursor: pointer; background-color: #ffeaa7; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #ffc107; font-weight: bold; color: #856404; }
.skipped-toggle:hover { background-color: #ffc107; }
.skipped-details { display: none; margin-top: 10px; }
ul { list-style-type: none; padding: 0; } ul { list-style-type: none; padding: 0; }
li { background-color: #ffffff; margin: 2px 0; padding: 5px; border-radius: 3px; } li { background-color: #ffffff; margin: 2px 0; padding: 5px; border-radius: 3px; }
</style> </style>
@@ -765,6 +777,10 @@ def jobs():
<a href="/logout" class="logout">Logout</a> <a href="/logout" class="logout">Logout</a>
<img src="/static/trbc-logo-full.png" alt="Logo" width="200"> <img src="/static/trbc-logo-full.png" alt="Logo" width="200">
<h1>Processing Jobs</h1> <h1>Processing Jobs</h1>
<div class="nav-links" style="margin-bottom: 20px;">
<a href="/">← Upload Files</a>
<a href="/completed-jobs">View Completed Jobs Archive →</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
@@ -784,12 +800,16 @@ def jobs():
<p><strong>ZIP Contents:</strong></p> <p><strong>ZIP Contents:</strong></p>
{% for zip_name, pdf_files in job.zip_contents.items() %} {% for zip_name, pdf_files in job.zip_contents.items() %}
<div class="zip-content"> <div class="zip-content">
<strong>{{ zip_name }}:</strong> <div class="zip-toggle" onclick="toggleZipDetails(this)">
<ul> <strong>{{ zip_name }}</strong> ({{ pdf_files|length }} files) ▶
{% for pdf_file in pdf_files %} </div>
<li>{{ pdf_file }}</li> <div class="zip-details">
{% endfor %} <ul>
</ul> {% for pdf_file in pdf_files %}
<li>{{ pdf_file }}</li>
{% endfor %}
</ul>
</div>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@@ -806,12 +826,18 @@ def jobs():
</form></p> </form></p>
{% endif %} {% endif %}
{% if job.skipped %} {% if job.skipped %}
<p><strong>Skipped Files:</strong></p> <div class="skipped-section">
<ul> <div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
{% for skip_info in job.skipped %} ⚠️ Skipped Files ({{ job.skipped|length }}) ▶
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li> </div>
{% endfor %} <div class="skipped-details">
</ul> <ul>
{% for skip_info in job.skipped %}
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@@ -853,10 +879,14 @@ def jobs():
<p><strong>ZIP Contents:</strong></p> <p><strong>ZIP Contents:</strong></p>
${Object.entries(job.zip_contents).map(([zipName, pdfFiles]) => ` ${Object.entries(job.zip_contents).map(([zipName, pdfFiles]) => `
<div class="zip-content"> <div class="zip-content">
<strong>${zipName}:</strong> <div class="zip-toggle" onclick="toggleZipDetails(this)">
<ul> <strong>${zipName}</strong> (${pdfFiles.length} files) ▶
${pdfFiles.map(pdfFile => `<li>${pdfFile}</li>`).join('')} </div>
</ul> <div class="zip-details">
<ul>
${pdfFiles.map(pdfFile => `<li>${pdfFile}</li>`).join('')}
</ul>
</div>
</div> </div>
`).join('')} `).join('')}
` : ''} ` : ''}
@@ -873,12 +903,19 @@ def jobs():
</form></p> </form></p>
`} `}
${job.skipped ? ` ${job.skipped ? `
<p><strong>Skipped Files:</strong></p> <div class="skipped-section">
<ul> <div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
${job.skipped.map(skipInfo => `<li><strong>${skipInfo[0]}</strong>: ${skipInfo[1]}</li>`).join('')} ⚠️ Skipped Files (${job.skipped.length}) ▶
</ul> </div>
<div class="skipped-details">
<ul>
${job.skipped.map(skipInfo => `<li><strong>${skipInfo[0]}</strong>: ${skipInfo[1]}</li>`).join('')}
</ul>
</div>
</div>
` : ''} ` : ''}
` : ''} ` : ''}
` : ''}
`; `;
jobsContainer.insertBefore(jobDiv, jobsContainer.querySelector('script') || jobsContainer.lastElementChild); jobsContainer.insertBefore(jobDiv, jobsContainer.querySelector('script') || jobsContainer.lastElementChild);
}); });
@@ -893,6 +930,21 @@ def jobs():
// Update jobs every 2 seconds // Update jobs every 2 seconds
setInterval(updateJobs, 2000); setInterval(updateJobs, 2000);
// Collapsible section functions
function toggleZipDetails(element) {
const details = element.nextElementSibling;
const isVisible = details.style.display === 'block';
details.style.display = isVisible ? 'none' : 'block';
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
}
function toggleSkippedDetails(element) {
const details = element.nextElementSibling;
const isVisible = details.style.display === 'block';
details.style.display = isVisible ? 'none' : 'block';
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
}
</script> </script>
</body> </body>
</html> </html>
@@ -935,5 +987,138 @@ def delete_job(job_id):
flash('Job not found.', 'error') flash('Job not found.', 'error')
return redirect('/jobs') return redirect('/jobs')
@app.route('/completed-jobs')
@login_required
def completed_jobs():
# Filter only completed jobs
completed_jobs = {job_id: job for job_id, job in job_queue.items() if job.get('status') == 'completed'}
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>Completed Jobs Archive</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; text-align: center; }
img { margin-bottom: 20px; }
h1 { color: #333; }
.job { background-color: #fff; margin: 10px auto; padding: 15px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 1000px; text-align: left; }
.status { padding: 5px 10px; border-radius: 5px; color: white; font-weight: bold; background-color: #28a745; }
.download-btn { background-color: #007bff; color: white; text-decoration: none; padding: 8px 16px; border-radius: 5px; margin: 5px; display: inline-block; }
.download-btn:hover { background-color: #0056b3; }
.delete-btn { background-color: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 5px; margin: 5px; cursor: pointer; }
.delete-btn:hover { background-color: #c82333; }
.flash { padding: 10px; margin: 10px 0; border-radius: 5px; text-align: center; }
.flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.logout { position: absolute; top: 20px; right: 20px; background-color: #dc3545; color: white; text-decoration: none; padding: 10px 20px; border-radius: 5px; }
.logout:hover { background-color: #c82333; }
.nav-links { margin-bottom: 20px; }
.nav-links a { color: #007bff; text-decoration: none; padding: 10px 20px; background-color: #f8f9fa; border-radius: 5px; margin: 0 10px; }
.nav-links a:hover { background-color: #e9ecef; }
.job-stats { background-color: #e9ecef; padding: 10px; border-radius: 5px; margin-bottom: 15px; }
.file-count { color: #17a2b8; font-weight: bold; }
.zip-content { background-color: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #007bff; }
.zip-toggle { cursor: pointer; background-color: #e9ecef; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #dee2e6; }
.zip-toggle:hover { background-color: #dee2e6; }
.zip-details { display: none; margin-top: 10px; }
.skipped-section { background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; padding: 10px; margin: 10px 0; }
.skipped-toggle { cursor: pointer; background-color: #ffeaa7; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #ffc107; font-weight: bold; color: #856404; }
.skipped-toggle:hover { background-color: #ffc107; }
.skipped-details { display: none; margin-top: 10px; }
ul { list-style-type: none; padding: 0; }
li { background-color: #ffffff; margin: 2px 0; padding: 5px; border-radius: 3px; }
</style>
</head>
<body>
<a href="/logout" class="logout">Logout</a>
<img src="/static/trbc-logo-full.png" alt="Logo" width="200">
<h1>Completed Jobs Archive</h1>
<div class="nav-links">
<a href="/">← Upload Files</a>
<a href="/jobs">View All Jobs →</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% for job_id, job in jobs.items() %}
<div class="job">
<h3>Job {{ job_id[:8] }} - {{ job.created_at if job.created_at else 'Unknown Date' }}</h3>
<div class="job-stats">
<span class="status">Completed</span>
<span class="file-count">{{ job.processed_files|selectattr('0', 'equalto', 'success')|list|length }} files processed</span>
{% if job.skipped %}
<span style="color: #856404; font-weight: bold;">{{ job.skipped|length }} files skipped</span>
{% endif %}
</div>
{% if job.zip_contents %}
<p><strong>Original ZIP Contents:</strong></p>
{% for zip_name, pdf_files in job.zip_contents.items() %}
<div class="zip-content">
<div class="zip-toggle" onclick="toggleZipDetails(this)">
<strong>{{ zip_name }}</strong> ({{ pdf_files|length }} files) ▶
</div>
<div class="zip-details">
<ul>
{% for pdf_file in pdf_files %}
<li>{{ pdf_file }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endfor %}
{% endif %}
{% if job.zip_name %}
<p><strong>Download:</strong> <a href="/download/{{ job.zip_name }}" class="download-btn">📁 Download Organized ZIP ({{ job.zip_name }})</a></p>
{% endif %}
{% if job.skipped %}
<div class="skipped-section">
<div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
⚠️ Skipped Files ({{ job.skipped|length }}) ▶
</div>
<div class="skipped-details">
<ul>
{% for skip_info in job.skipped %}
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<form method="post" action="/delete/{{ job_id }}" style="display: inline;">
<button type="submit" class="delete-btn" onclick="return confirm('Are you sure you want to permanently delete this completed job and its files?')">🗑️ Delete Job</button>
</form>
</div>
{% endfor %}
{% if not jobs %}
<p>No completed jobs yet. <a href="/">Upload some files</a> to get started.</p>
{% endif %}
<script>
// Collapsible section functions
function toggleZipDetails(element) {
const details = element.nextElementSibling;
const isVisible = details.style.display === 'block';
details.style.display = isVisible ? 'none' : 'block';
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
}
function toggleSkippedDetails(element) {
const details = element.nextElementSibling;
const isVisible = details.style.display === 'block';
details.style.display = isVisible ? 'none' : 'block';
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
}
</script>
</body>
</html>
''', jobs=completed_jobs)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, port=3000) app.run(debug=True, port=3000)