Vortex-Flux / src /streamlit_app.py
klydekushy's picture
Update src/streamlit_app.py
fa028d1 verified
import streamlit as st
import pandas as pd
import gspread
from google.oauth2.service_account import Credentials
import os
import json
import sys
# Ajout du chemin pour trouver les modules
sys.path.append(os.path.join(os.path.dirname(__file__), 'modules'))
# IMPORT DU NOUVEAU MODULE
try:
from modules import kyc_form, map_dashboard, loans_engine, ml_dashboard, notifications, ontology_graph, repayments
except ImportError:
# Fallback si l'import direct échoue (structure de dossier simple)
import kyc_form
import map_dashboard
import loans_engine
import ml_dashboard
import notifications
import ontology_graph
import repayments
# ==============================================================================
# 1. CONFIGURATION DU "DIRECT DRIVER" & DESIGN
# ==============================================================================
st.set_page_config(
page_title="Vortex-Flux | Ontology",
page_icon="🔺",
layout="wide",
initial_sidebar_state="expanded"
)
# Nom exact de votre fichier Google Sheets (Doit être partagé avec l'email du bot)
SHEET_NAME = "Vortex-Flux"
# Scopes requis pour l'accès Google Drive/Sheets
SCOPES = [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive"
]
# ==============================================================================
# 2. INJECTION CSS GOTHAM SURVEILLANCE - STYLE GLOBAL
# ==============================================================================
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
/* === FOND GLOBAL - TONS SURVEILLANCE === */
.stApp {
background: linear-gradient(135deg, #0d1117 0%, #161b22 50%, #1c2128 100%);
color: #c9d1d9;
font-family: 'Space Grotesk', sans-serif;
}
/* === SIDEBAR STYLE === */
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #161b22 0%, #1c2128 100%);
border-right: 2px solid rgba(88, 166, 255, 0.3);
}
[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p {
color: #8b949e !important;
font-family: 'Space Grotesk', sans-serif !important;
}
[data-testid="stSidebar"] .stRadio label {
color: #8b949e !important;
font-family: 'Space Grotesk', sans-serif !important;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
}
[data-testid="stSidebar"] .stRadio label:hover {
color: #58a6ff !important;
}
/* Titre sidebar */
[data-testid="stSidebar"] h1 {
color: #58a6ff !important;
font-family: 'Space Grotesk', sans-serif !important;
font-weight: 700 !important;
letter-spacing: 1px;
text-transform: uppercase;
}
/* Caption sidebar */
[data-testid="stSidebar"] .caption {
color: #6e7681 !important;
font-size: 0.75rem;
letter-spacing: 0.5px;
}
/* === APPLICATION SPACE GROTESK AUX CONTENUS TEXTUELS === */
.stApp h1, .stApp h2, .stApp h3, .stApp h4, .stApp h5, .stApp h6,
.stApp p:not([data-testid]),
.stMarkdown,
.stText {
font-family: 'Space Grotesk', sans-serif !important;
}
/* === HEADERS STYLE SURVEILLANCE === */
.stApp h1, .stApp h2, .stApp h3 {
color: #58a6ff !important;
font-family: 'Space Grotesk', sans-serif !important;
font-weight: 500 !important;
letter-spacing: 0.5px;
text-shadow: none;
}
.stApp h1 {
font-size: 1.8rem !important;
border-bottom: 1px solid rgba(88, 166, 255, 0.2);
padding-bottom: 12px;
margin-bottom: 24px;
}
.stApp h2 {
font-size: 1.3rem !important;
color: #8b949e !important;
}
.stApp h3 {
font-size: 1.1rem !important;
}
/* === METRICS CARDS - STYLE OPS CENTER === */
[data-testid="stMetric"] {
background: rgba(22, 27, 34, 0.6);
border: 1px solid rgba(48, 54, 61, 0.8);
border-radius: 6px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
}
[data-testid="stMetric"] label {
color: #8b949e !important;
font-size: 0.75rem !important;
font-weight: 500 !important;
text-transform: uppercase;
letter-spacing: 0.8px;
font-family: 'Space Grotesk', sans-serif !important;
}
[data-testid="stMetric"] [data-testid="stMetricValue"] {
color: #c9d1d9 !important;
font-size: 1.6rem !important;
font-weight: 600 !important;
text-shadow: none;
font-family: 'Space Grotesk', sans-serif !important;
}
[data-testid="stMetric"] [data-testid="stMetricDelta"] {
color: #58a6ff !important;
font-size: 0.85rem !important;
font-weight: 400;
font-family: 'Space Grotesk', sans-serif !important;
}
/* === BOUTONS STYLE OPS === */
.stButton > button {
background: rgba(22, 27, 34, 0.8);
border: 1px solid rgba(88, 166, 255, 0.6);
color: #58a6ff !important;
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.5px;
border-radius: 4px;
padding: 10px 20px;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
text-transform: uppercase;
font-family: 'Space Grotesk', sans-serif !important;
}
.stButton > button:hover {
background: rgba(88, 166, 255, 0.15);
border-color: rgba(88, 166, 255, 0.8);
box-shadow: 0 0 12px rgba(88, 166, 255, 0.3);
transform: translateY(-1px);
}
.stButton > button:active {
transform: translateY(0);
}
/* === BOUTON RESET SPÉCIAL (ROUGE) === */
.reset-button > button {
background: rgba(231, 76, 60, 0.1) !important;
border: 1px solid rgba(231, 76, 60, 0.6) !important;
color: #e74c3c !important;
}
.reset-button > button:hover {
background: rgba(231, 76, 60, 0.2) !important;
border-color: rgba(231, 76, 60, 0.8) !important;
box-shadow: 0 0 12px rgba(231, 76, 60, 0.3) !important;
}
/* === DOWNLOAD BUTTON === */
.stDownloadButton > button {
background: rgba(88, 166, 255, 0.1);
border: 1px solid rgba(88, 166, 255, 0.4);
color: #58a6ff !important;
font-weight: 600;
}
.stDownloadButton > button:hover {
background: rgba(88, 166, 255, 0.2);
border-color: rgba(88, 166, 255, 0.6);
box-shadow: 0 0 15px rgba(88, 166, 255, 0.3);
}
/* === EXPANDERS STYLE SURVEILLANCE === */
.streamlit-expanderHeader {
background: rgba(22, 27, 34, 0.4);
border-left: 2px solid rgba(88, 166, 255, 0.5);
color: #8b949e !important;
font-weight: 500;
font-size: 0.9rem;
letter-spacing: 0.3px;
padding: 12px 16px;
border-radius: 3px;
font-family: 'Space Grotesk', sans-serif !important;
}
.streamlit-expanderHeader:hover {
background: rgba(22, 27, 34, 0.6);
border-left-color: rgba(88, 166, 255, 0.8);
}
/* === DATAFRAME STYLE === */
.stDataFrame {
border: 1px solid rgba(48, 54, 61, 0.6);
border-radius: 4px;
overflow: hidden;
font-size: 0.85rem;
}
.stDataFrame [data-testid="stTable"] {
background: rgba(22, 27, 34, 0.6);
}
.stDataFrame thead tr th {
background: rgba(48, 54, 61, 0.8) !important;
color: #8b949e !important;
font-weight: 600 !important;
font-size: 0.8rem !important;
text-transform: uppercase;
letter-spacing: 0.5px;
font-family: 'Space Grotesk', sans-serif !important;
}
.stDataFrame tbody tr:hover {
background: rgba(88, 166, 255, 0.05) !important;
}
/* === INFO/SUCCESS/WARNING/ERROR BOXES === */
.stAlert {
background: rgba(22, 27, 34, 0.6);
border: 1px solid rgba(48, 54, 61, 0.8);
border-radius: 4px;
color: #8b949e;
font-size: 0.9rem;
font-family: 'Space Grotesk', sans-serif !important;
}
.stAlert[data-baseweb="notification"] {
border-left-width: 3px;
}
/* Info - Bleu */
.stAlert[kind="info"] {
border-left-color: rgba(88, 166, 255, 0.6);
background: rgba(88, 166, 255, 0.05);
}
/* Success - Vert */
.stAlert[kind="success"] {
border-left-color: rgba(84, 189, 75, 0.6);
background: rgba(84, 189, 75, 0.05);
}
/* Warning - Orange */
.stAlert[kind="warning"] {
border-left-color: rgba(243, 156, 18, 0.6);
background: rgba(243, 156, 18, 0.05);
}
/* Error - Rouge */
.stAlert[kind="error"] {
border-left-color: rgba(231, 76, 60, 0.6);
background: rgba(231, 76, 60, 0.05);
}
/* === TEXT INPUT & SELECT BOX === */
.stTextInput > div > div > input,
.stSelectbox > div > div > div,
.stTextArea textarea {
background: rgba(22, 27, 34, 0.8) !important;
border: 1px solid rgba(48, 54, 61, 0.8) !important;
color: #c9d1d9 !important;
font-family: 'Space Grotesk', sans-serif !important;
border-radius: 4px;
}
.stTextInput > div > div > input:focus,
.stSelectbox > div > div > div:focus,
.stTextArea textarea:focus {
border-color: rgba(88, 166, 255, 0.6) !important;
box-shadow: 0 0 0 1px rgba(88, 166, 255, 0.3) !important;
}
/* === CHECKBOX === */
.stCheckbox label {
color: #8b949e !important;
font-weight: 500;
font-size: 0.85rem;
font-family: 'Space Grotesk', sans-serif !important;
}
.stCheckbox input:checked + span {
background-color: #58a6ff !important;
border-color: #58a6ff !important;
}
/* === RADIO BUTTONS MODIFIÉS EN PETITS CARRÉS === */
.stRadio label {
color: #8b949e !important;
font-family: 'Space Grotesk', sans-serif !important;
}
/* Transformer les boutons radio en carrés */
.stRadio [data-baseweb="radio"] > div:first-child {
border-radius: 3px !important; /* Carré au lieu de cercle */
width: 16px !important;
height: 16px !important;
}
/* Point intérieur des boutons radio (quand sélectionné) */
.stRadio [data-baseweb="radio"] > div:first-child > div {
border-radius: 2px !important; /* Petit carré intérieur */
width: 8px !important;
height: 8px !important;
top: 4px !important;
left: 4px !important;
}
/* Couleur du carré sélectionné */
.stRadio input:checked + div > div:first-child {
background-color: #58a6ff !important;
border-color: #58a6ff !important;
}
/* =========================================== */
/* RÈGLES SPÉCIFIQUES POUR SUPPRIMER LE SURLIGNAGE */
/* =========================================== */
/* Désactiver complètement la sélection de texte sur les éléments interactifs */
[data-testid="stSidebar"] * {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
/* Réactiver la sélection uniquement pour les champs de saisie */
[data-testid="stSidebar"] input,
[data-testid="stSidebar"] textarea {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
}
/* Supprimer l'effet de sélection de texte (comme dans Word) */
[data-testid="stSidebar"] *::selection,
[data-testid="stSidebar"] *::-moz-selection {
background: transparent !important;
color: inherit !important;
}
/* Règles spécifiques pour les boutons radio dans la sidebar */
[data-testid="stSidebar"] .stRadio > div > div[role="radiogroup"] {
outline: none !important;
box-shadow: none !important;
}
/* Supprimer les états de focus sur les boutons radio */
[data-testid="stSidebar"] .stRadio [role="radiogroup"]:focus,
[data-testid="stSidebar"] .stRadio [role="radiogroup"]:focus-visible,
[data-testid="stSidebar"] .stRadio [role="radiogroup"]:active {
outline: none !important;
box-shadow: none !important;
border: none !important;
}
/* Supprimer le surlignage sur les labels radio */
[data-testid="stSidebar"] .stRadio label:focus,
[data-testid="stSidebar"] .stRadio label:active,
[data-testid="stSidebar"] .stRadio label:focus-visible {
outline: none !important;
box-shadow: none !important;
background: transparent !important;
}
/* Cibler les éléments BaseWeb spécifiques des boutons radio */
[data-testid="stSidebar"] .stRadio [data-baseweb="radio"]:focus,
[data-testid="stSidebar"] .stRadio [data-baseweb="radio"]:active,
[data-testid="stSidebar"] .stRadio [data-baseweb="radio"]:focus-visible {
outline: none !important;
box-shadow: none !important;
border: none !important;
}
/* Supprimer le tap highlight sur mobile */
[data-testid="stSidebar"] .stRadio > div,
[data-testid="stSidebar"] .stRadio label {
-webkit-tap-highlight-color: transparent !important;
tap-highlight-color: transparent !important;
}
/* Supprimer l'effet de focus sur les éléments div des boutons radio */
[data-testid="stSidebar"] .stRadio div:focus,
[data-testid="stSidebar"] .stRadio div:focus-visible,
[data-testid="stSidebar"] .stRadio div:active {
outline: none !important;
box-shadow: none !important;
}
/* Règles globales pour supprimer les outlines */
*:focus,
*:focus-visible,
*:focus-within {
outline: none !important;
box-shadow: none !important;
}
/* Supprimer la sélection de texte globale */
::selection {
background: transparent !important;
color: inherit !important;
}
::-moz-selection {
background: transparent !important;
color: inherit !important;
}
/* Empêcher la sélection sur les éléments UI */
.stButton,
.stRadio,
.stCheckbox,
[data-testid="stSidebar"],
[data-testid="stSidebar"] *,
.stTabs,
h1, h2, h3, h4, h5, h6 {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
/* Autoriser la sélection dans les zones de texte */
.stTextInput input,
.stTextArea textarea,
.stDataFrame,
p, span, li {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
}
/* Supprimer les bordures focus Streamlit */
[data-testid="stButton"] button:focus,
[data-testid="stRadio"] *:focus,
[data-testid="stCheckbox"] *:focus {
outline: none !important;
box-shadow: none !important;
border: none !important;
}
/* Inputs gardent leur bordure normale au focus */
.stTextInput input:focus,
.stSelectbox select:focus,
.stTextArea textarea:focus {
outline: none !important;
box-shadow: none !important;
border-color: rgba(48, 54, 61, 0.8) !important;
}
/* Supprimer le highlight au clic (active state) */
*:active {
outline: none !important;
-webkit-tap-highlight-color: transparent !important;
}
/* Pour mobile/tactile */
* {
-webkit-tap-highlight-color: transparent !important;
-webkit-touch-callout: none !important;
}
/* === DIVIDER === */
hr {
border: none;
height: 1px;
background: rgba(48, 54, 61, 0.6);
margin: 2rem 0;
}
/* === TABS === */
.stTabs [data-baseweb="tab-list"] {
background: rgba(22, 27, 34, 0.4);
border-bottom: 1px solid rgba(48, 54, 61, 0.8);
}
.stTabs [data-baseweb="tab"] {
color: #8b949e !important;
font-family: 'Space Grotesk', sans-serif !important;
font-weight: 500;
}
.stTabs [data-baseweb="tab"]:hover {
color: #58a6ff !important;
background: rgba(88, 166, 255, 0.05);
}
.stTabs [aria-selected="true"] {
color: #58a6ff !important;
border-bottom: 2px solid #58a6ff !important;
}
/* === SCROLLBAR CUSTOMIZATION === */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(13, 17, 23, 0.4);
}
::-webkit-scrollbar-thumb {
background: rgba(48, 54, 61, 0.8);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(88, 166, 255, 0.3);
}
/* === SPINNER === */
.stSpinner > div {
border-top-color: #58a6ff !important;
}
/* === PROGRESS BAR === */
.stProgress > div > div > div {
background-color: #58a6ff !important;
}
/* === LIENS === */
a {
color: #58a6ff !important;
text-decoration: none;
}
a:hover {
color: #1f78b4 !important;
text-decoration: underline;
}
/* === CODE BLOCKS === */
code {
background: rgba(22, 27, 34, 0.8) !important;
color: #c9d1d9 !important;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Fira Code', monospace !important;
}
pre {
background: rgba(22, 27, 34, 0.8) !important;
border: 1px solid rgba(48, 54, 61, 0.6);
border-radius: 4px;
padding: 12px;
}
/* === COLONNES === */
[data-testid="column"] {
background: transparent;
padding: 8px;
}
</style>
""", unsafe_allow_html=True)
# ==============================================================================
# 3. MOTEUR DE CONNEXION (GSPREAD PUR)
# ==============================================================================
@st.cache_resource
def get_gspread_client():
"""Authentification robuste via gspread sans passer par st.connection"""
raw_json = os.environ.get("GSHEETS_JSON")
if not raw_json:
st.error("🛑 SECRET MANQUANT : 'GSHEETS_JSON' est introuvable.")
return None
try:
# Création du fichier temporaire sécurisé
creds_dict = json.loads(raw_json)
with open("temp_creds.json", "w") as f:
json.dump(creds_dict, f)
# Authentification Google
credentials = Credentials.from_service_account_file("temp_creds.json", scopes=SCOPES)
client = gspread.authorize(credentials)
return client
except Exception as e:
st.error(f"⚠️ Erreur d'authentification GSpread : {e}")
return None
def get_data_from_sheet(worksheet_name):
"""Récupère les données d'un onglet sous forme de DataFrame"""
client = get_gspread_client()
if not client: return pd.DataFrame()
try:
# Ouverture du classeur par son nom
sh = client.open(SHEET_NAME)
# Sélection de l'onglet
worksheet = sh.worksheet(worksheet_name)
# Lecture des données
data = worksheet.get_all_records()
return pd.DataFrame(data)
except gspread.WorksheetNotFound:
# Si l'onglet n'existe pas encore, on renvoie vide sans planter
return pd.DataFrame()
except gspread.SpreadsheetNotFound:
st.error(f"⚠️ Fichier Google Sheet '{SHEET_NAME}' introuvable. Vérifiez le nom et le partage.")
return pd.DataFrame()
except Exception as e:
st.error(f"Erreur de lecture : {e}")
return pd.DataFrame()
# ==============================================================================
# 4. LOGIQUE ONTOLOGIQUE (IDs)
# ==============================================================================
def generate_ontology_id(prefix, sheet_tab):
df = get_data_from_sheet(sheet_tab)
# Calcul ID : Nombre de lignes + 1
next_id = len(df) + 1
return f"{prefix}-2026-{next_id:04d}" #2026 a changé pour 2027 plus tard
# ==============================================================================
# 5. FONCTION DE RÉINITIALISATION COMPLÈTE
# ==============================================================================
def reset_application():
"""
Réinitialise complètement l'application :
- Vide le session_state
- Vide tous les caches
- Force un rechargement complet
"""
# 1. Sauvegarder les clés essentielles (si nécessaire)
# Par exemple, si vous voulez garder certaines variables
# keys_to_keep = []
# 2. Vider complètement le session_state
for key in list(st.session_state.keys()):
del st.session_state[key]
# 3. Vider tous les caches de Streamlit
st.cache_data.clear()
st.cache_resource.clear()
# 4. Force un rechargement complet
st.rerun()
# ==============================================================================
# 6. INTERFACE UTILISATEUR
# ==============================================================================
client = get_gspread_client()
if client:
# --- BARRE LATÉRALE ---
st.sidebar.title("🔺 VORTEX-FLUX")
st.sidebar.caption("Jumeau Numérique & Ontologie")
st.sidebar.divider()
st.sidebar.markdown("### System")
menu = st.sidebar.radio("Module",
["Dashboard", "Ontology Model", "Tracking Metrics","Actor Onboarding", "Init Loan", "Repayments"]
)
st.sidebar.divider()
# --- BOUTON DE RÉINITIALISATION ---
st.sidebar.markdown("### Rerunning")
col1, col2 = st.sidebar.columns([3, 1])
with col1:
st.markdown("<small style='color: #6e7681;'>Réinitialiser l'application</small>", unsafe_allow_html=True)
with col2:
# Utilisation d'une clé unique et d'une classe CSS personnalisée
if st.button("reset", key="reset_app_btn", help="Réinitialiser l'application à son état initial"):
reset_application()
# Version alternative avec un bouton plus visible :
# st.sidebar.markdown("---")
# if st.sidebar.button("🔄 RESET APPLICATION", key="reset_full", help="Réinitialiser complètement l'application"):
# reset_application()
# --- A. TABLEAU DE BORD ---
if menu == "Dashboard":
# --- AJOUTS VORTEX-FLUX (ML & NOTIFICATIONS) ---
# 1.Appel direct du Dashboard ML (Le "Nouveau" Tableau de Bord)
ml_dashboard.show_ml_features(client, SHEET_NAME)
st.divider()
# 2. Module de Notification (Bouton de vérification)
notifications.verifier_et_notifier_echeances(client, SHEET_NAME)
# --- B. CODE DU MODULE ONTOLOGY ---
elif menu == "Ontology Model":
ontology_graph.show_ontology_graph(client, SHEET_NAME)
# --- C. CODE DU MODULE CARTE TACTIQUE ---
elif menu == "Tracking Metrics":
# Appel du nouveau module
map_dashboard.show_map_dashboard(client, SHEET_NAME)
# --- D. CODE DU MODULE KYC ---
elif menu == "Actor Onboarding":
# APPEL DU MODULE EXTERNE
kyc_form.show_kyc_form(client, SHEET_NAME, generate_ontology_id)
# --- E. CODE DU MODULE LOANS ---
elif menu == "Init Loan":
loans_engine.show_loans_engine(client, SHEET_NAME)
# --- F. CODE DU MODULE Repayment ---
elif menu == "Repayments":
repayments.show_repayments_module(client, SHEET_NAME)
else:
st.error("🛑 Impossible de se connecter à Google Sheets. Vérifiez vos credentials.")