diff --git a/src/gui/components/_legacy.py b/src/gui/components/_legacy.py index 5bf5707..8234517 100644 --- a/src/gui/components/_legacy.py +++ b/src/gui/components/_legacy.py @@ -1336,33 +1336,81 @@ def upload_and_analyze_section() -> None: def _run_analysis_on_upload(uploaded): - """Read the uploaded file with pre-parse repair, then analyze.""" - from src.core.analyze import analyze + """Read the uploaded file with pre-parse repair, then analyze. + + Errors are caught and surfaced as a single synthetic ``Finding`` + instead of bubbling a traceback up into the page chrome. A bad + file (empty bytes, unreadable encoding, pandas parse failure on + one of several uploaded files) should yield a clean red banner for + that file, not kill the whole multi-file analysis run. + """ + from src.core.analyze import Finding, analyze + from src.core.errors import format_for_user from src.core.io import repair_bytes name = uploaded.name data = uploaded.getvalue() suffix = name.rsplit(".", 1)[-1].lower() if "." in name else "" - if suffix in ("xlsx", "xls"): - df = pd.read_excel(io.BytesIO(data), dtype=str, keep_default_na=False) - return analyze(df) + def _error_finding(description: str, fid: str = "analysis_failed") -> list[Finding]: + return [Finding( + id=fid, + severity="error", + tool="", + count=1, + description=description, + confidence="high", + fix_action="", + )] - # CSV / TSV: run repair_bytes so the user sees csv_* findings. - text_head = data[:4096].decode("utf-8", errors="replace") - delim = "\t" if suffix == "tsv" else "," - if delim == ",": - for cand in ("\t", ";", "|"): - if text_head.count(cand) > text_head.count(",") * 1.5: - delim = cand - break - repair = repair_bytes(data, encoding="utf-8", delimiter=delim) - df = pd.read_csv( - io.BytesIO(repair.repaired_bytes), - encoding="utf-8", delimiter=delim, - dtype=str, keep_default_na=False, on_bad_lines="warn", - ) - return analyze(df, repair_result=repair) + if not data: + return _error_finding( + f"`{name}` is empty (0 bytes). Please re-upload — the bytes " + f"may not have transferred correctly from your browser.", + fid="empty_upload", + ) + + try: + if suffix in ("xlsx", "xls"): + df = pd.read_excel(io.BytesIO(data), dtype=str, keep_default_na=False) + return analyze(df) + + # CSV / TSV: run repair_bytes so the user sees csv_* findings. + text_head = data[:4096].decode("utf-8", errors="replace") + delim = "\t" if suffix == "tsv" else "," + if delim == ",": + for cand in ("\t", ";", "|"): + if text_head.count(cand) > text_head.count(",") * 1.5: + delim = cand + break + repair = repair_bytes(data, encoding="utf-8", delimiter=delim) + if not repair.repaired_bytes: + return _error_finding( + f"`{name}` is empty after pre-parse repair " + f"(original was {len(data)} bytes — likely all NUL " + f"bytes or stripped during a BOM/line-ending pass). " + f"Open the file in a text editor to confirm it has " + f"content.", + fid="empty_after_repair", + ) + df = pd.read_csv( + io.BytesIO(repair.repaired_bytes), + encoding="utf-8", delimiter=delim, + dtype=str, keep_default_na=False, on_bad_lines="warn", + ) + return analyze(df, repair_result=repair) + except pd.errors.EmptyDataError: + return _error_finding( + f"`{name}` could not be parsed — pandas reports no columns " + f"in the file. Original size was {len(data)} bytes. Open " + f"the file in a text editor to confirm the header row is " + f"present and uses the same delimiter as the data rows.", + fid="empty_after_repair", + ) + except Exception as e: + return _error_finding( + f"`{name}` could not be analyzed: {format_for_user(e)}", + ) def findings_count_for_tool(tool_id: str) -> int: