fix(gui): keep sidebar reopenable + add clean Quit button

The chrome-hiding CSS was removing the Streamlit header wholesale,
which also took the sidebar's expand chevron with it — a collapsed
sidebar became unreopenable. Make the header transparent instead and
explicitly preserve the sidebar collapsed-control.

Also add a Quit button in the app footer that signals the Streamlit
server (SIGTERM, falling back to SIGINT) so closing the GUI returns
the shell prompt cleanly instead of leaving Python hung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 13:30:10 +00:00
parent e1f364f010
commit 0c25d80146
3 changed files with 54 additions and 15 deletions

View File

@@ -24,6 +24,7 @@ if str(_project_root) not in sys.path:
from src.gui.components import ( from src.gui.components import (
findings_count_for_tool, findings_count_for_tool,
hide_streamlit_chrome, hide_streamlit_chrome,
quit_button,
upload_and_analyze_section, upload_and_analyze_section,
) )
@@ -87,7 +88,11 @@ for row_start in range(0, len(TOOLS), 3):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
st.divider() st.divider()
st.caption( footer_left, footer_right = st.columns([4, 1])
with footer_left:
st.caption(
"Runs locally. Your data never leaves this computer. " "Runs locally. Your data never leaves this computer. "
"| DataTools v3.0" "| DataTools v3.0"
) )
with footer_right:
quit_button()

View File

@@ -41,6 +41,7 @@ from . import _legacy as _legacy # noqa: F401 (keep for direct access)
__all__ = [ __all__ = [
# Shared chrome / pickup / gate # Shared chrome / pickup / gate
"hide_streamlit_chrome", "hide_streamlit_chrome",
"quit_button",
"pickup_or_upload", "pickup_or_upload",
"require_normalization_gate", "require_normalization_gate",
# Dedup widgets # Dedup widgets

View File

@@ -3,6 +3,8 @@
from __future__ import annotations from __future__ import annotations
import io import io
import os
import signal
from typing import Optional from typing import Optional
import pandas as pd import pandas as pd
@@ -30,25 +32,31 @@ from src.core.normalizers import NormalizerType
_HIDE_CHROME_CSS = """ _HIDE_CHROME_CSS = """
<style> <style>
/* Hide Streamlit header bar */ /* Make the Streamlit header transparent and out of the way, but DO NOT
`display: none` it — the sidebar's collapsed-state expand button is
anchored in the header region, and removing the header makes a
collapsed sidebar impossible to reopen. */
header[data-testid="stHeader"] { header[data-testid="stHeader"] {
background: transparent !important;
height: 0 !important;
}
/* Hide main hamburger menu and deploy button explicitly (don't rely on
hiding the whole header). */
#MainMenu,
[data-testid="stMainMenu"],
[data-testid="stAppDeployButton"] {
display: none !important; display: none !important;
} }
/* Hide hamburger menu */ /* Keep the sidebar expand control visible and clickable above page content. */
button[kind="header"] { [data-testid="stSidebarCollapsedControl"] {
display: none !important; display: flex !important;
} visibility: visible !important;
#MainMenu { z-index: 999 !important;
display: none !important;
} }
/* Hide footer */ /* Hide footer */
footer { footer {
display: none !important; display: none !important;
} }
/* Hide deploy button */
[data-testid="stAppDeployButton"] {
display: none !important;
}
/* Reclaim top padding lost from hidden header */ /* Reclaim top padding lost from hidden header */
.stAppViewBlockContainer, .stAppViewBlockContainer,
[data-testid="stAppViewBlockContainer"] { [data-testid="stAppViewBlockContainer"] {
@@ -67,6 +75,31 @@ def hide_streamlit_chrome() -> None:
st.markdown(_HIDE_CHROME_CSS, unsafe_allow_html=True) st.markdown(_HIDE_CHROME_CSS, unsafe_allow_html=True)
# ---------------------------------------------------------------------------
# Clean shutdown
# ---------------------------------------------------------------------------
def quit_button(label: str = "Quit app", *, key: str = "quit_app_button") -> None:
"""Render a Quit button that terminates the Streamlit server.
Streamlit has no first-class shutdown hook, so closing the browser tab
leaves the server (and the python process running it) alive — the user
has to Ctrl+C in the shell. This helper signals the Streamlit process
so the shell returns cleanly. SIGTERM lets Streamlit run its own
shutdown handlers; if that's unavailable on the platform, fall back to
SIGINT (the same signal Ctrl+C delivers).
"""
if st.session_state.get("_app_shutting_down"):
st.success("App shut down. You can close this browser tab.")
st.stop()
if st.button(label, key=key, type="secondary"):
st.session_state["_app_shutting_down"] = True
sig = getattr(signal, "SIGTERM", signal.SIGINT)
os.kill(os.getpid(), sig)
st.rerun()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Config panel (advanced options) # Config panel (advanced options)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------