Feb 27 2026: fix upload size

This commit is contained in:
2026-02-27 11:35:58 -05:00
parent 0ff27f2b06
commit 64445eb793
3 changed files with 119 additions and 1 deletions
+20
View File
@@ -44,6 +44,26 @@ def create_app(config_name='default'):
app.register_blueprint(facilities.bp)
app.register_blueprint(issues.bp)
# ── Error handler: 413 Request Entity Too Large ───────────────────────
# Nginx can return 413 before Flask sees the request; this handler covers
# the Flask-side rejection and gives users a clear, actionable message
# with a redirect back into the inspection workflow.
from werkzeug.exceptions import RequestEntityTooLarge
@app.errorhandler(RequestEntityTooLarge)
@app.errorhandler(413)
def handle_413(e):
from flask import request as flask_request, flash as flask_flash, redirect, url_for
flask_flash(
f'The uploaded file(s) are too large. '
f'Please reduce the photo size or upload fewer photos at once '
f'(maximum {app.config["MAX_CONTENT_LENGTH"] // (1024 * 1024)}MB per submission).',
'danger'
)
# Redirect back to the referring page if available, otherwise dashboard
referrer = flask_request.referrer
return redirect(referrer or url_for('dashboard.index')), 302
with app.app_context():
db.create_all()
+98
View File
@@ -496,5 +496,103 @@ function collectSignatures() {
function confirmSubmit() {
return confirm('Submit this inspection? This action cannot be undone.');
}
// ── Client-side image compression ─────────────────────────────────────────────
// Compresses photos via Canvas before upload to prevent 413 errors.
// Target: ≤ 1MB per image at 85% JPEG quality, max 1920px on longest side.
const IMG_MAX_PX = 1920; // max dimension in pixels
const IMG_QUALITY = 0.85; // JPEG quality (01)
const IMG_MAX_BYTES = 1 * 1024 * 1024; // 1MB per compressed image
function compressImageFile(file) {
return new Promise((resolve) => {
// Only compress images that exceed the size threshold
if (!file.type.startsWith('image/') || file.size <= IMG_MAX_BYTES) {
return resolve(file);
}
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
// Calculate scaled dimensions
let { width, height } = img;
if (width > IMG_MAX_PX || height > IMG_MAX_PX) {
if (width >= height) {
height = Math.round(height * IMG_MAX_PX / width);
width = IMG_MAX_PX;
} else {
width = Math.round(width * IMG_MAX_PX / height);
height = IMG_MAX_PX;
}
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {
// Create a new File from the compressed blob, preserving the name
const compressed = new File([blob], file.name, { type: 'image/jpeg', lastModified: Date.now() });
resolve(compressed);
}, 'image/jpeg', IMG_QUALITY);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
// Intercept form submission — compress all image fields before sending
document.getElementById('inspectionForm').addEventListener('submit', async function(e) {
const fileInputs = this.querySelectorAll('input[type="file"]');
if (!fileInputs.length) return; // nothing to compress
// Only intercept if there are files to compress
const hasFiles = Array.from(fileInputs).some(inp => inp.files && inp.files.length > 0);
if (!hasFiles) return;
e.preventDefault();
const btn = this.querySelector('button[type="submit"]');
const origText = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Compressing photos…';
}
try {
// Build a FormData object with compressed images
const formData = new FormData(this);
for (const input of fileInputs) {
if (!input.files || !input.files.length) continue;
const compressed = await compressImageFile(input.files[0]);
formData.set(input.name, compressed, compressed.name);
}
// Submit via fetch with the compressed FormData
const response = await fetch(this.action || window.location.href, {
method: 'POST',
body: formData,
credentials: 'same-origin',
});
if (response.redirected) {
window.location.href = response.url;
} else if (response.ok) {
window.location.reload();
} else {
// Surface server error (e.g. still-too-large after compression)
if (btn) { btn.disabled = false; btn.innerHTML = origText; }
alert('Submission failed (HTTP ' + response.status + '). Please try again or remove some photos.');
}
} catch (err) {
if (btn) { btn.disabled = false; btn.innerHTML = origText; }
console.error('Photo compression error:', err);
alert('An error occurred while processing photos. Please try again.');
}
});
</script>
{% endblock %}