feat(gui): port journey-level nav + local-first pill to the live app

Brings the live Streamlit app in line with the finalized layout-review
mockups (structural/low-risk changes; verified by compile + registry
sanity, still pending a streamlit-run visual check):

- tools_registry: Data Cleaners now in pipeline order (Clean Text ->
  Standardize -> Fix Missing -> Find Duplicates); new "finance" section
  (Reconcile, PDF to CSV) and "coming_soon" section (Find Unusual,
  Quality Check, Combine Files). Adds those to the Section type +
  SECTION_LABELS.
- app.py: Home becomes the "Start here" front door — a standalone,
  unlabeled top entry (play_circle icon) ahead of the hidden
  Activate/Logs/Close pages; nav groups reordered cleaners ->
  transformations -> automations -> finance -> coming soon.
- _legacy.py: render_tool_header now shows the "Runs 100% locally"
  privacy pill (right-aligned, Ready tools only — omitted on Coming
  Soon stubs); accent emphasis CSS for the Start-here nav link.
- i18n: add nav.start_here_title, nav.section_finance,
  nav.section_coming_soon to en + es packs.
- DECISIONS.md: log the PDF/Reconcile in-bundle (Finance group) call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 17:01:57 +00:00
parent 48251b625f
commit 09ec01e98b
6 changed files with 172 additions and 85 deletions

33
DECISIONS.md Normal file
View File

@@ -0,0 +1,33 @@
# Product & architecture decisions
A running log of decisions that aren't obvious from the code and would
otherwise be re-litigated. Newest first.
## 2026-06-08 — PDF to CSV and Reconcile stay in the bundle, under a "Finance" group
**Decision:** `10_pdf_extractor` (PDF to CSV) and `11_reconciler` (Reconcile
Two Files) remain part of the DataTools suite. In the sidebar they are
segregated into their own **Finance** section, distinct from the
file-cleaning tools.
**Context / why this needed deciding:**
- Both tools sit outside the documented 9-script cleaning architecture
(TECHNICAL.md / USER-GUIDE.md stop at the orchestrator).
- They occupy the "reconciliation / manual data-entry" territory the
product's honest-positioning note explicitly placed outside a
file-cleaning tool's scope.
- A journey-level UX review flagged that every extra tool in the main
sidebar raises the "which tool do I need?" load for a non-technical
buyer, so tools serving a different job should live in a clearly
different place.
**Resolution:** Keep them in-bundle (they're built, useful, and ship
today) but group them under "Finance" so the cleaning flow stays
uncluttered. Revisit only if a separate finance-focused product emerges.
**Implications:**
- `tools_registry.py`: Reconcile + PDF to CSV carry a `finance` section.
- Sidebar order: Start here → Data Cleaners → Transformations →
Automations → Finance → Coming soon.
- This is the source-of-truth realization of the `layout-review/`
mockups (see `layout-review/shell.js`).

View File

@@ -82,6 +82,8 @@ def _build_navigation() -> dict[str, list]:
"cleaners": [], "cleaners": [],
"transformations": [], "transformations": [],
"automations": [], "automations": [],
"finance": [],
"coming_soon": [],
} }
# Resolve the tool name through ``tool_name`` (i18n lookup) instead # Resolve the tool name through ``tool_name`` (i18n lookup) instead
# of using the registry's English ``tool.name`` field, otherwise the # of using the registry's English ``tool.name`` field, otherwise the
@@ -96,16 +98,16 @@ def _build_navigation() -> dict[str, list]:
) )
) )
# Home is now surfaced under the new "Analysis" section as # Home is the product's front door: "Start here". It's surfaced as a
# "File Analysis" — the home page's content (importing files, # standalone, unlabeled top entry (in the "" section, ahead of the
# running the analyzer, browsing findings) is itself a data-analysis # hidden Activate/Logs/Close pages) so it reads as the obvious
# workflow, so grouping it next to Reconcile keeps the sidebar's # starting point above the tool groups rather than one item among
# mental model coherent. ``default=True`` still points at this # equals. The companion CSS in ``hide_streamlit_chrome`` gives its
# page so first-visit lands here regardless of section placement. # nav link accent emphasis. ``default=True`` lands first-visit here.
home = st.Page( home = st.Page(
_home_page, _home_page,
title=_t("nav.file_analysis_title") or "File Analysis", title=_t("nav.start_here_title") or "Start here",
icon=":material/insert_chart_outlined:", icon=":material/play_circle:",
default=True, default=True,
url_path="home", url_path="home",
) )
@@ -136,17 +138,20 @@ def _build_navigation() -> dict[str, list]:
url_path="close", url_path="close",
) )
# Activate / Logs / Close stay in the unlabeled section (key ``""``) # Home leads the unlabeled section (key ``""``) so "Start here" sits
# so the CSS in ``hide_streamlit_chrome`` keeps hiding them by # at the very top with no section header above it. Activate / Logs /
# ``href``. Home moved out of that bucket into "Analysis" — the # Close follow in the same unlabeled bucket and stay hidden by their
# unlabeled section now contains ONLY hidden pages, so no orphan # ``href`` via the CSS in ``hide_streamlit_chrome``. Section order
# entry appears above the "Analysis" header in the sidebar. # below is the journey order: cleaners (pipeline order) →
# transformations → automations → finance → coming soon (last, so
# not-yet-shipped tools never interleave with working ones).
return { return {
"": [activate, logs, close], "": [home, activate, logs, close],
section_label("analysis"): [home, *by_section["analysis"]],
section_label("cleaners"): by_section["cleaners"], section_label("cleaners"): by_section["cleaners"],
section_label("transformations"): by_section["transformations"], section_label("transformations"): by_section["transformations"],
section_label("automations"): by_section["automations"], section_label("automations"): by_section["automations"],
section_label("finance"): by_section["finance"],
section_label("coming_soon"): by_section["coming_soon"],
} }

View File

@@ -95,6 +95,18 @@ footer {
[data-testid="stSidebarNav"] a[href$="/close/"] { [data-testid="stSidebarNav"] a[href$="/close/"] {
display: none !important; display: none !important;
} }
/* "Start here" front-door nav item — accent emphasis so the obvious
entry point reads at a glance above the tool groups. Targets the Home
link by href; accent values mirror theme.py (§3 color scale). */
[data-testid="stSidebarNav"] a[href$="/home"],
[data-testid="stSidebarNav"] a[href$="/home/"] {
background: #fef4ed !important;
font-weight: 600 !important;
}
[data-testid="stSidebarNav"] a[href$="/home"]:hover,
[data-testid="stSidebarNav"] a[href$="/home/"]:hover {
background: #fde4d3 !important;
}
/* Reclaim top padding lost from hidden header. Streamlit's default /* Reclaim top padding lost from hidden header. Streamlit's default
block-container padding-top is ~6rem (room for the header it ships). block-container padding-top is ~6rem (room for the header it ships).
We hide the header so reclaim that space — the page title should sit We hide the header so reclaim that space — the page title should sit
@@ -2168,14 +2180,37 @@ def render_tool_header(tool_id: str) -> None:
button as a defense-in-depth so the label can never wrap, no button as a defense-in-depth so the label can never wrap, no
matter how the column ends up sized. matter how the column ends up sized.
""" """
col_title, col_help = st.columns([8, 2]) col_title, col_help = st.columns([7, 3])
with col_title: with col_title:
st.title(_t(f"tools.{tool_id}.page_title")) st.title(_t(f"tools.{tool_id}.page_title"))
with col_help: with col_help:
# Spacer pushes the popover button down so it sits closer to # Local-first reassurance + Help, right-aligned opposite the
# the title's baseline than to its top — without the spacer the # title. The "Runs 100% locally" privacy pill is shown on every
# button floats above the big title text. # working tool page (where the user is actively feeding in a
st.write("") # customer list) and omitted on not-yet-shipped "Coming Soon"
# tools, which process nothing. When the pill is shown it also
# serves as the spacer that nudges the popover down toward the
# title baseline; without it we keep the explicit spacer.
from src.gui.tools_registry import tool_by_id as _tool_by_id
_tool = _tool_by_id(tool_id)
if _tool is None or _tool.status == "Ready":
import html as _html
st.markdown(
'<div style="display:flex;justify-content:flex-end">'
'<span class="dt-privacy-pill">'
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">'
'<rect x="4" y="11" width="16" height="10" rx="2"/>'
'<path d="M8 11V7a4 4 0 018 0v4"/>'
'</svg>'
f'{_html.escape(_t("home.privacy_pill"))}'
'</span>'
'</div>',
unsafe_allow_html=True,
)
else:
# Spacer pushes the popover button down so it sits closer to
# the title's baseline than to its top.
st.write("")
body = _t(f"tools.{tool_id}.help_md") body = _t(f"tools.{tool_id}.help_md")
# ``src.i18n.t`` falls back to returning the lookup key itself # ``src.i18n.t`` falls back to returning the lookup key itself
# on miss (see ``_resolve`` → key-as-string fallback). That's # on miss (see ``_resolve`` → key-as-string fallback). That's

View File

@@ -24,7 +24,10 @@ Tier = Literal["core", "pro", "enterprise"]
Status = Literal["Ready", "Coming Soon"] Status = Literal["Ready", "Coming Soon"]
# Sidebar grouping. Tools are bucketed by what the user is trying to # Sidebar grouping. Tools are bucketed by what the user is trying to
# accomplish rather than by implementation detail. # accomplish rather than by implementation detail.
Section = Literal["analysis", "cleaners", "transformations", "automations"] Section = Literal[
"analysis", "cleaners", "transformations", "automations",
"finance", "coming_soon",
]
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -42,35 +45,14 @@ class Tool:
# Order in this list IS the order shown in each sidebar section, so # Order in this list IS the order shown in each sidebar section, so
# arranging it carefully matters: within "cleaners" we lead with the # arranging it carefully matters. Within "cleaners" the order is the
# operations a non-technical user is most likely to need (filling # recommended PIPELINE order (Clean Text → Standardize → Fix Missing
# blanks, flagging outliers) before progressing to format cleanup, # Find Duplicates) so a user running tools by hand follows the sequence
# dedup, and the final quality report. # the orchestrator would. "Coming Soon" tools are grouped at the end in
# their own section so they never interleave with working tools, and the
# finance-oriented tools (Reconcile, PDF to CSV) live in their own group
# (see DECISIONS.md 2026-06-08).
TOOLS: list[Tool] = [ TOOLS: list[Tool] = [
Tool(
tool_id="04_missing_handler",
icon=":material/help_outline:",
name="Fix Missing Values",
description=(
"Find blank cells (even ones written as 'N/A' or '?') and fill "
"them in or remove them."
),
page_slug="4_Missing_Values",
status="Ready",
section="cleaners",
),
Tool(
tool_id="06_outlier_detector",
icon=":material/insights:",
name="Find Unusual Values",
description=(
"Spot values that look wrong — way too high, way too low, or "
"breaking your rules."
),
page_slug="6_Outlier_Detector",
status="Coming Soon",
section="cleaners",
),
Tool( Tool(
tool_id="02_text_cleaner", tool_id="02_text_cleaner",
icon=":material/text_format:", icon=":material/text_format:",
@@ -95,6 +77,18 @@ TOOLS: list[Tool] = [
status="Ready", status="Ready",
section="cleaners", section="cleaners",
), ),
Tool(
tool_id="04_missing_handler",
icon=":material/help_outline:",
name="Fix Missing Values",
description=(
"Find blank cells (even ones written as 'N/A' or '?') and fill "
"them in or remove them."
),
page_slug="4_Missing_Values",
status="Ready",
section="cleaners",
),
Tool( Tool(
tool_id="01_deduplicator", tool_id="01_deduplicator",
icon=":material/search:", icon=":material/search:",
@@ -106,18 +100,6 @@ TOOLS: list[Tool] = [
status="Ready", status="Ready",
section="cleaners", section="cleaners",
), ),
Tool(
tool_id="08_validator_reporter",
icon=":material/check_circle:",
name="Quality Check",
description=(
"Check your file against rules you set, and export a PDF or "
"Excel report."
),
page_slug="8_Validator_Reporter",
status="Coming Soon",
section="cleaners",
),
Tool( Tool(
tool_id="05_column_mapper", tool_id="05_column_mapper",
icon=":material/view_column:", icon=":material/view_column:",
@@ -130,18 +112,6 @@ TOOLS: list[Tool] = [
status="Ready", status="Ready",
section="transformations", section="transformations",
), ),
Tool(
tool_id="07_multi_file_merger",
icon=":material/account_tree:",
name="Combine Files",
description=(
"Combine several CSV or Excel files into one — even if their "
"columns don't match."
),
page_slug="7_Multi_File_Merger",
status="Coming Soon",
section="transformations",
),
Tool( Tool(
tool_id="09_pipeline_runner", tool_id="09_pipeline_runner",
icon=":material/auto_awesome:", icon=":material/auto_awesome:",
@@ -154,17 +124,6 @@ TOOLS: list[Tool] = [
status="Ready", status="Ready",
section="automations", section="automations",
), ),
Tool(
tool_id="10_pdf_extractor",
icon=":material/picture_as_pdf:",
name="PDF to CSV",
description=(
"Pull transactions out of bank-statement PDFs into a clean CSV file."
),
page_slug="10_PDF_Extractor",
status="Ready",
section="transformations",
),
Tool( Tool(
tool_id="11_reconciler", tool_id="11_reconciler",
icon=":material/compare_arrows:", icon=":material/compare_arrows:",
@@ -175,7 +134,54 @@ TOOLS: list[Tool] = [
), ),
page_slug="11_Reconciler", page_slug="11_Reconciler",
status="Ready", status="Ready",
section="analysis", section="finance",
),
Tool(
tool_id="10_pdf_extractor",
icon=":material/picture_as_pdf:",
name="PDF to CSV",
description=(
"Pull transactions out of bank-statement PDFs into a clean CSV file."
),
page_slug="10_PDF_Extractor",
status="Ready",
section="finance",
),
Tool(
tool_id="06_outlier_detector",
icon=":material/insights:",
name="Find Unusual Values",
description=(
"Spot values that look wrong — way too high, way too low, or "
"breaking your rules."
),
page_slug="6_Outlier_Detector",
status="Coming Soon",
section="coming_soon",
),
Tool(
tool_id="08_validator_reporter",
icon=":material/check_circle:",
name="Quality Check",
description=(
"Check your file against rules you set, and export a PDF or "
"Excel report."
),
page_slug="8_Validator_Reporter",
status="Coming Soon",
section="coming_soon",
),
Tool(
tool_id="07_multi_file_merger",
icon=":material/account_tree:",
name="Combine Files",
description=(
"Combine several CSV or Excel files into one — even if their "
"columns don't match."
),
page_slug="7_Multi_File_Merger",
status="Coming Soon",
section="coming_soon",
), ),
] ]
@@ -187,6 +193,8 @@ SECTION_LABELS: dict[Section, str] = {
"cleaners": "Data Cleaners", "cleaners": "Data Cleaners",
"transformations": "Transformations", "transformations": "Transformations",
"automations": "Automations", "automations": "Automations",
"finance": "Finance",
"coming_soon": "Coming soon",
} }

View File

@@ -193,9 +193,12 @@
"section_cleaners": "Data Cleaners", "section_cleaners": "Data Cleaners",
"section_transformations": "Transformations", "section_transformations": "Transformations",
"section_automations": "Automations", "section_automations": "Automations",
"section_finance": "Finance",
"section_coming_soon": "Coming soon",
"review_page_title": "Review", "review_page_title": "Review",
"home_page_title": "Home", "home_page_title": "Home",
"file_analysis_title": "File Analysis", "file_analysis_title": "File Analysis",
"start_here_title": "Start here",
"section_account": "Account", "section_account": "Account",
"activate_title": "Activate", "activate_title": "Activate",
"close_title": "Close", "close_title": "Close",

View File

@@ -193,9 +193,12 @@
"section_cleaners": "Limpiadores de datos", "section_cleaners": "Limpiadores de datos",
"section_transformations": "Transformaciones", "section_transformations": "Transformaciones",
"section_automations": "Automatizaciones", "section_automations": "Automatizaciones",
"section_finance": "Finanzas",
"section_coming_soon": "Próximamente",
"review_page_title": "Revisión", "review_page_title": "Revisión",
"home_page_title": "Inicio", "home_page_title": "Inicio",
"file_analysis_title": "Análisis de archivo", "file_analysis_title": "Análisis de archivo",
"start_here_title": "Empezar aquí",
"section_account": "Cuenta", "section_account": "Cuenta",
"activate_title": "Activar", "activate_title": "Activar",
"close_title": "Cerrar", "close_title": "Cerrar",