diff --git a/api/routers/upload.py b/api/routers/upload.py index da9a155..8921483 100644 --- a/api/routers/upload.py +++ b/api/routers/upload.py @@ -962,7 +962,12 @@ def add_in(name: Any) -> None: if not name: return text = str(name) - if text in seen_out or text in {lot_col, ts_col} or text in seen_in: + if ( + text in seen_out + or text in _PLACEHOLDER_TARGET_IDS + or text in {lot_col, ts_col} + or text in seen_in + ): return seen_in.add(text) input_names.append(text) diff --git a/api/tests/test_phase3_security.py b/api/tests/test_phase3_security.py index 4d7cbab..f3d0cc7 100644 --- a/api/tests/test_phase3_security.py +++ b/api/tests/test_phase3_security.py @@ -769,6 +769,49 @@ def test_upload_reports_missing_required_columns(self): any("Missing required columns" in message for message in payload["errors"]) ) + def test_upload_does_not_require_placeholder_target_columns(self): + with patch.object( + upload, + "get_equipment_pg", + return_value={ + "domain_id": "etcher_01", + "owner_id": "owner-1", + "owner_org": "Birck", + "status": "approved", + "columns": [ + {"name": "LOTNAME", "type": "string", "unit": ""}, + {"name": "run_date", "type": "datetime", "unit": ""}, + {"name": "pressure", "type": "float", "unit": "mTorr"}, + {"name": "primary_metric", "type": "float", "unit": ""}, + {"name": "secondary_metric", "type": "float", "unit": ""}, + ], + "config_json": {}, + }, + ), patch.object( + upload, + "create_upload_record_pg", + return_value={ + "upload_id": "upload-placeholder-targets", + "uploaded_at": "2026-04-15T12:00:00+00:00", + }, + ): + response = self.client.post( + "/api/data/upload", + headers=OWNER_HEADERS, + data={"equipment_id": "etcher_01"}, + files={ + "file": ( + "runs.csv", + b"LOTNAME,run_date,pressure\nLOT-001,2026-04-15,55\n", + "text/csv", + ) + }, + ) + + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload["status"], "success") + def test_upload_requires_approved_equipment(self): with patch.object( upload, diff --git a/web/app/data/upload/page.tsx b/web/app/data/upload/page.tsx index c29325c..1351cfe 100644 --- a/web/app/data/upload/page.tsx +++ b/web/app/data/upload/page.tsx @@ -67,6 +67,10 @@ function mapPersistedUpload(upload: PersistedUpload): UploadedFile { }; } +function shouldRestoreUpload(upload: PersistedUpload): boolean { + return upload.status !== "error" && upload.status !== "failed"; +} + export default function DataUploadPage() { const [files, setFiles] = useState([]); const [selectedEquipment, setSelectedEquipment] = useState(""); @@ -87,7 +91,7 @@ export default function DataUploadPage() { ]); setEquipment(equipmentData ?? []); - setFiles((uploadData ?? []).map(mapPersistedUpload)); + setFiles((uploadData ?? []).filter(shouldRestoreUpload).map(mapPersistedUpload)); setProjects(projectData ?? []); setEquipmentLoading(false);