Problem
The import form (app/views/import_jobs/_form.html.erb) uses ActiveStorage direct upload via the shared dropzone controller, but its submit button is not gated on upload success. When the browser-to-S3 upload fails, the form still submits, creating an ImportJob with no file attached. The job then fails server-side with:
[{"title":"No records were provided","detail":{"messages":["No records were provided for this import"]}}]
— stored in import_job.error_message, invisible to Scout and opaque to the user, who sees only a failed import job with no hint that the upload itself never happened.
By contrast, the GPX course upload form (app/views/events/_course_gpx_form.html.erb), which uses the same dropzone controller, correctly keeps its Save button disabled until the upload completes — so the same failure surfaces there as a visible client-side error.
Real-world case
2026-08-28: a race timing official setting up Bear 100 on staging hit both forms while the staging S3 bucket's CORS policy still allowed only the defunct Heroku origin (every direct upload failed with Status: 0). The GPX form told him something was wrong with the upload; the CSV form let him queue a job that failed mysteriously, turning a five-minute client-side diagnosis into an email chain and a background-job investigation.
Fix
Gate the import form's submit the same way the GPX form gates Save: disable until the dropzone controller reports the direct upload stored successfully, and keep it disabled (with the error visible) on upload failure. Worth checking whether the difference lives in the form markup or in how each form wires the dropzone controller's targets/actions — the controller evidently supports the correct behavior already.
Belt-and-braces option while in there: Etl::AsyncImporter (or the controller create action) could reject an ImportJob with files.attached? == false up front with a clearer message like "No file was uploaded — the file upload may have failed," rather than the generic "No records were provided."
🤖 Generated with Claude Code
Problem
The import form (
app/views/import_jobs/_form.html.erb) uses ActiveStorage direct upload via the shared dropzone controller, but its submit button is not gated on upload success. When the browser-to-S3 upload fails, the form still submits, creating anImportJobwith no file attached. The job then fails server-side with:— stored in
import_job.error_message, invisible to Scout and opaque to the user, who sees only a failed import job with no hint that the upload itself never happened.By contrast, the GPX course upload form (
app/views/events/_course_gpx_form.html.erb), which uses the same dropzone controller, correctly keeps its Save button disabled until the upload completes — so the same failure surfaces there as a visible client-side error.Real-world case
2026-08-28: a race timing official setting up Bear 100 on staging hit both forms while the staging S3 bucket's CORS policy still allowed only the defunct Heroku origin (every direct upload failed with
Status: 0). The GPX form told him something was wrong with the upload; the CSV form let him queue a job that failed mysteriously, turning a five-minute client-side diagnosis into an email chain and a background-job investigation.Fix
Gate the import form's submit the same way the GPX form gates Save: disable until the dropzone controller reports the direct upload stored successfully, and keep it disabled (with the error visible) on upload failure. Worth checking whether the difference lives in the form markup or in how each form wires the dropzone controller's targets/actions — the controller evidently supports the correct behavior already.
Belt-and-braces option while in there:
Etl::AsyncImporter(or the controllercreateaction) could reject anImportJobwithfiles.attached? == falseup front with a clearer message like "No file was uploaded — the file upload may have failed," rather than the generic "No records were provided."🤖 Generated with Claude Code