feat(home): multi-file upload + per-file analysis, drop tool grid

Home is now upload + analysis only. The page accepts multiple files in
one go, analyzes each independently, and renders findings grouped by
filename in bordered containers. The 3-section tool-card grid is gone —
discovery happens via the sidebar now.

Mechanics:
- file_uploader uses accept_multiple_files=True. Each file's findings
  cache in session_state["home_findings_by_file"] keyed by filename so
  removing a file via Streamlit's "x" button drops its findings too,
  and re-clicking Run only re-analyzes pending files.
- The first uploaded file is mirrored into the singular
  home_uploaded_{name,bytes,size} keys so tool pages continue to pick
  up an "active" upload through pickup_or_upload — no tool-page changes.
- New i18n keys: upload.intro_multi, upload.uploader_label_multi,
  upload.clear_results, upload.empty_state. upload.heading text is
  updated to "Upload one or more files to start" (EN + ES).

Dropped tests pinning the tool grid:
- TestHomeToolGridLocalization (test_chrome.py)
- test_home_tool_card_uses_es_name (test_smoke.py)
- TestLiteHomeGridBadges (test_lite_tier.py — locked-card lock-badge
  assertions; locking is still enforced per-tool-page via
  require_feature_or_render_upgrade)

2009 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 20:12:48 +00:00
parent dad744f17f
commit ff2eaeb6c4
6 changed files with 99 additions and 140 deletions

View File

@@ -30,20 +30,10 @@ if str(_project_root) not in sys.path:
# ---------------------------------------------------------------------------
def _home_page() -> None:
"""Render the home page — upload section + tool grid + footer."""
from src.gui.components import (
findings_count_for_tool,
hide_streamlit_chrome,
upload_and_analyze_section,
)
from src.gui.tools_registry import (
TOOLS,
section_label,
tool_description,
tool_name,
)
"""Render the home page — multi-file upload + per-file analysis."""
from src.gui.components import hide_streamlit_chrome, render_findings_panel
from src.gui.components._legacy import _run_analysis_on_upload
from src.i18n import t
from src.license import get_manager
st.set_page_config(
page_title=t("home.page_title"),
@@ -54,73 +44,95 @@ def _home_page() -> None:
st.title(t("home.title"))
st.caption(t("home.caption"))
st.divider()
upload_and_analyze_section()
st.markdown(f"### {t('upload.heading')}")
st.caption(t("upload.intro_multi"))
st.divider()
# Per-tier feature set for badging — Ready tools that the user's
# tier doesn't unlock get a red "🔒 Locked" pill instead of the green
# "Ready" so they see at a glance which tools an upgrade would
# unlock. Dev-mode skips the lock check so the home grid renders the
# full SKU during development.
_license_state = get_manager().current_state()
_unlocked_features = (
set(_license_state.features) if _license_state.valid else set()
uploaded_files = st.file_uploader(
t("upload.uploader_label_multi"),
type=["csv", "tsv", "xlsx", "xls"],
accept_multiple_files=True,
key="home_upload",
help=t("upload.uploader_help"),
)
_dev_mode = get_manager().dev_mode
# Group tool cards by sidebar section so the home grid mirrors the
# left-nav layout — same vocabulary, same ordering.
sections: list[tuple[str, list]] = []
for section in ("cleaners", "transformations", "automations"):
tools = [tool for tool in TOOLS if tool.section == section]
if not tools:
continue
sections.append((section_label(section), tools))
if not uploaded_files:
st.info(t("upload.empty_state"))
return
for header, tools in sections:
st.subheader(header)
for row_start in range(0, len(tools), 3):
cols = st.columns(3)
for i, col in enumerate(cols):
idx = row_start + i
if idx >= len(tools):
break
tool = tools[idx]
with col:
tool_locked = (
tool.status == "Ready"
and not _dev_mode
and tool.tool_id not in _unlocked_features
)
if tool_locked:
status_key = "license.status_locked"
status_color = "red"
elif tool.status == "Ready":
status_key = "status.ready"
status_color = "green"
else:
status_key = "status.coming_soon"
status_color = "orange"
# Keep tool pages working: they consume a single ``home_uploaded_*``
# set via ``pickup_or_upload``. Expose the first uploaded file as
# the "active" upload for that contract; the rest live alongside
# for per-file analysis on this page.
first = uploaded_files[0]
if (
st.session_state.get("home_uploaded_name") != first.name
or st.session_state.get("home_uploaded_size") != first.size
):
st.session_state["home_uploaded_name"] = first.name
st.session_state["home_uploaded_size"] = first.size
st.session_state["home_uploaded_bytes"] = first.getvalue()
badge = ""
n = findings_count_for_tool(tool.tool_id)
if n:
badge_key = (
"home.findings_badge_one"
if n == 1
else "home.findings_badge_other"
)
badge = f" :red-background[**{t(badge_key, n=n)}**]"
lock_glyph = "🔒 " if tool_locked else ""
st.markdown(
f"### {tool.icon} {tool_name(tool.tool_id)}{badge}\n\n"
f"{tool_description(tool.tool_id)}\n\n"
f":{status_color}[**{lock_glyph}{t(status_key)}**]"
)
# Per-file findings live in a dict so removing a file from the
# uploader (Streamlit's "x" button) drops its results too. We only
# re-analyze files we haven't already analyzed in this session.
findings_by_file: dict = st.session_state.setdefault(
"home_findings_by_file", {}
)
current_names = {f.name for f in uploaded_files}
findings_by_file = {
name: result for name, result in findings_by_file.items()
if name in current_names
}
st.session_state["home_findings_by_file"] = findings_by_file
pending = [f for f in uploaded_files if f.name not in findings_by_file]
col_run, col_clear, _ = st.columns([1, 1, 4])
with col_run:
run_clicked = st.button(
t("upload.run_button"),
type="primary",
key="home_run_analysis",
disabled=not pending,
use_container_width=True,
)
with col_clear:
clear_clicked = st.button(
t("upload.clear_results"),
key="home_clear_results",
disabled=not findings_by_file,
use_container_width=True,
)
if clear_clicked:
st.session_state["home_findings_by_file"] = {}
st.rerun()
if run_clicked:
progress = st.progress(0.0, text=t("upload.scanning"))
for i, f in enumerate(pending, start=1):
findings_by_file[f.name] = _run_analysis_on_upload(f)
progress.progress(i / len(pending), text=f"{f.name}")
st.session_state["home_findings_by_file"] = findings_by_file
progress.empty()
st.rerun()
if findings_by_file:
st.divider()
# Preserve uploader order so the user sees results in the same
# order they appear in the file list above.
for f in uploaded_files:
if f.name not in findings_by_file:
continue
findings = findings_by_file[f.name]
with st.container(border=True):
if not findings:
st.markdown(f"### 📄 {f.name}")
st.success(t("findings.none"))
else:
render_findings_panel(findings, header=f"📄 {f.name}")
st.divider()
st.caption(t("chrome.footer"))

View File

@@ -15,7 +15,7 @@
"coming_soon": "Coming Soon"
},
"upload": {
"heading": "📤 Upload a file to start",
"heading": "📤 Upload one or more files to start",
"intro": "Optional: scan an uploaded file for data quality issues and see which tools can fix each one. Skip if you already know what you need.",
"limits": "**Up to 1.5 GB.** Formats: CSV, TSV, XLSX, XLS. Delimiters auto-detected: comma, tab, semicolon, pipe. Encodings auto-detected: UTF-8 (with/without BOM), UTF-16, cp1252, Latin-1/9, cp1250, ISO-8859-2, cp1251, KOI8-R, Mac Roman, Shift_JIS, GB18030, Big5, EUC-KR — and override on the Review page.",
"uploader_label": "Upload CSV or Excel",
@@ -27,7 +27,11 @@
"using_session_file": "Using **{name}** from the upload screen.",
"use_different_file": "Use a different file",
"switch_back": "Switch back to upload-screen file",
"pickup_caption": "Up to 1.5 GB. Delimiters auto-detected: comma, tab, semicolon, pipe. Encoding auto-detected (UTF-8 / UTF-16 / cp1252 / Latin-1 family / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), with override on the Review page."
"pickup_caption": "Up to 1.5 GB. Delimiters auto-detected: comma, tab, semicolon, pipe. Encoding auto-detected (UTF-8 / UTF-16 / cp1252 / Latin-1 family / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), with override on the Review page.",
"intro_multi": "Drop files below. Each one is analyzed locally — no upload anywhere.",
"uploader_label_multi": "Upload CSV, TSV, or Excel files",
"clear_results": "Clear results",
"empty_state": "Upload one or more files to begin. Your data never leaves this computer."
},
"findings": {
"header": "Detected issues",

View File

@@ -15,7 +15,7 @@
"coming_soon": "Próximamente"
},
"upload": {
"heading": "📤 Sube un archivo para empezar",
"heading": "📤 Sube uno o más archivos para empezar",
"intro": "Opcional: analiza un archivo para detectar problemas de calidad de datos y ver qué herramientas pueden corregir cada uno. Sáltalo si ya sabes lo que necesitas.",
"limits": "**Hasta 1,5 GB.** Formatos: CSV, TSV, XLSX, XLS. Delimitadores detectados automáticamente: coma, tabulador, punto y coma, barra vertical. Codificaciones detectadas automáticamente: UTF-8 (con/sin BOM), UTF-16, cp1252, Latin-1/9, cp1250, ISO-8859-2, cp1251, KOI8-R, Mac Roman, Shift_JIS, GB18030, Big5, EUC-KR — y se pueden sustituir desde la página Revisar.",
"uploader_label": "Sube un archivo CSV o Excel",
@@ -27,7 +27,11 @@
"using_session_file": "Usando **{name}** de la pantalla de carga.",
"use_different_file": "Usar otro archivo",
"switch_back": "Volver al archivo de la pantalla de carga",
"pickup_caption": "Hasta 1,5 GB. Delimitadores detectados automáticamente: coma, tabulador, punto y coma, barra vertical. Codificación detectada automáticamente (UTF-8 / UTF-16 / cp1252 / familia Latin-1 / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), con opción de sustituirla en la página Revisar."
"pickup_caption": "Hasta 1,5 GB. Delimitadores detectados automáticamente: coma, tabulador, punto y coma, barra vertical. Codificación detectada automáticamente (UTF-8 / UTF-16 / cp1252 / familia Latin-1 / cp1250 / cp1251 / KOI8-R / Mac Roman / Shift_JIS / GB18030 / Big5 / EUC-KR), con opción de sustituirla en la página Revisar.",
"intro_multi": "Suelta archivos abajo. Cada uno se analiza localmente — no se sube a ningún lado.",
"uploader_label_multi": "Sube archivos CSV, TSV o Excel",
"clear_results": "Borrar resultados",
"empty_state": "Sube uno o más archivos para empezar. Tus datos nunca salen de este equipo."
},
"findings": {
"header": "Problemas detectados",