Files
datatools-dev/src/gui/app.py
Michael ea99e292d2 feat(nav): group Home + Reconcile under a new "Analysis" section
Home now appears in the sidebar as "File Analysis" under a labeled
"Analysis" section together with Reconcile Two Files — both pages
are data-analysis workflows (importing/profiling files vs. matching
across files), so grouping them clarifies the sidebar's mental model.

- tools_registry: new ``analysis`` Section; reconcile moves out of
  automations into it.
- i18n: ``nav.section_analysis`` + ``nav.file_analysis_title`` added
  to en.json and es.json.
- app.py: home dropped from the unlabeled section and surfaced at the
  top of the Analysis group; ``default=True`` preserved so first-visit
  routing is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:11:06 +00:00

155 lines
5.9 KiB
Python

"""DataTools — Data Cleaning Mastery Suite.
Launch:
streamlit run src/gui/app.py
This module is the navigation manager for the full GUI: it registers
every tool page with ``st.navigation`` so the sidebar can render
section headers ("Data Review", "Data Cleaners", "Transformations",
"Automations") instead of the flat numbered list Streamlit's
auto-page-discovery would produce. The Home page itself is registered
as a callable defined below so the entry script remains the single
file users invoke.
"""
from __future__ import annotations
import sys
from pathlib import Path
import streamlit as st
# Ensure project root is on sys.path so `src.core` imports work
_project_root = Path(__file__).resolve().parent.parent.parent
if str(_project_root) not in sys.path:
sys.path.insert(0, str(_project_root))
# Quiet the loguru stderr sink. The CLIs (``src/cli*.py``) reconfigure
# it per-script to ``WARNING``; the GUI's ``python -m src.gui``
# entrypoint never did, so internal ``logger.debug`` /
# ``logger.warning`` breadcrumbs bled into the terminal where users
# launched the app from. Drop the default ``DEBUG``-level stderr sink
# and add a clean ``ERROR``-only one so only genuinely actionable
# problems surface — diagnostic warnings still land in the audit log
# via its own sink. Runs once, before any module-level ``logger`` use
# in the GUI tree.
from loguru import logger as _logger # noqa: E402
_logger.remove()
_logger.add(sys.stderr, level="ERROR", format="{level: <8} | {message}")
# ---------------------------------------------------------------------------
# Home page (rendered when the user selects the default nav entry)
# ---------------------------------------------------------------------------
#
# The renderer lives in ``src/gui/_home.py`` so the ``back_to_home_link``
# helper on tool pages can ``import _home_page`` and pass it into
# ``st.switch_page`` without re-executing this entry script's
# navigation setup (which would crash because tool pages have a
# different "main script" context and the relative ``pages/…`` paths
# below would no longer resolve).
from src.gui._home import _home_page # noqa: E402
# ---------------------------------------------------------------------------
# Navigation registration
# ---------------------------------------------------------------------------
#
# ``st.navigation`` overrides Streamlit's auto-discovery of the
# ``pages/`` directory, so every page we want in the sidebar must be
# listed here. The dict key becomes the section header rendered above
# the entries; an empty-string key suppresses the header so the Home
# entry sits at the top without a label above it.
from src.gui.tools_registry import TOOLS, section_label, tool_name # noqa: E402
from src.i18n import t as _t # noqa: E402
def _page_for(tool_id: str, *, page_slug: str, icon: str, title: str) -> "st.Page":
return st.Page(
f"pages/{page_slug}.py",
title=title,
icon=icon,
url_path=tool_id,
)
def _build_navigation() -> dict[str, list]:
by_section: dict[str, list] = {
"analysis": [],
"cleaners": [],
"transformations": [],
"automations": [],
}
# Resolve the tool name through ``tool_name`` (i18n lookup) instead
# of using the registry's English ``tool.name`` field, otherwise the
# sidebar stays in English when the user switches to Spanish.
for tool in TOOLS:
by_section[tool.section].append(
_page_for(
tool.tool_id,
page_slug=tool.page_slug,
icon=tool.icon,
title=tool_name(tool.tool_id),
)
)
# Home is now surfaced under the new "Analysis" section as
# "File Analysis" — the home page's content (importing files,
# running the analyzer, browsing findings) is itself a data-analysis
# workflow, so grouping it next to Reconcile keeps the sidebar's
# mental model coherent. ``default=True`` still points at this
# page so first-visit lands here regardless of section placement.
home = st.Page(
_home_page,
title=_t("nav.file_analysis_title") or "File Analysis",
icon=":material/insert_chart_outlined:",
default=True,
url_path="home",
)
# Activate + Close are both reached from the sticky-footer Help
# popover (Activate / Manage-license link and Close button). They
# are registered with ``st.navigation`` so ``/activate`` and
# ``/close`` remain URL-routable — pages not listed in the dict
# 404 even by direct hit — but their sidebar entries are hidden
# via CSS in ``hide_streamlit_chrome``. We group them under a
# single section header that is also hidden by that same CSS rule
# so no orphan "Account" / "Close" label is left in the sidebar.
activate = st.Page(
"pages/_Activate.py",
title=_t("nav.activate_title") or "Activate",
icon=":material/key:",
url_path="activate",
)
logs = st.Page(
"pages/_Logs.py",
title="Logs",
icon=":material/description:",
url_path="logs",
)
close = st.Page(
"pages/99_Close.py",
title=_t("nav.close_title") or "Close",
icon=":material/close:",
url_path="close",
)
# Activate / Logs / Close stay in the unlabeled section (key ``""``)
# so the CSS in ``hide_streamlit_chrome`` keeps hiding them by
# ``href``. Home moved out of that bucket into "Analysis" — the
# unlabeled section now contains ONLY hidden pages, so no orphan
# entry appears above the "Analysis" header in the sidebar.
return {
"": [activate, logs, close],
section_label("analysis"): [home, *by_section["analysis"]],
section_label("cleaners"): by_section["cleaners"],
section_label("transformations"): by_section["transformations"],
section_label("automations"): by_section["automations"],
}
pg = st.navigation(_build_navigation())
pg.run()