Impact environnemental de votre session
++ Chargement en cours… +
+diff --git a/.gitignore b/.gitignore
index e381d37..22272a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ __pycache__/
.cache/
*.log
*.tmp
+*.old
# Ignorer config locale
.ropeproject/
diff --git a/README.md b/README.md
index a7a62b2..86ae648 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,6 @@ Pour l'environnement de pré-production, (https://fabnum-dev.peccini.fr)[https:/
ENV=dev
PORT=8502
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
->
GITEA_TOKEN = "LE_TOKEN_POUR_ACCEDER_A_GITEA"
ORGANISATION = "fabnum"
DEPOT_FICHES = "fiches"
diff --git a/assets/impact_co2.js b/assets/impact_co2.js
new file mode 100644
index 0000000..66a317b
--- /dev/null
+++ b/assets/impact_co2.js
@@ -0,0 +1,14 @@
+import tgwf from "https://cdn.skypack.dev/@tgwf/co2";
+
+export function calculerImpactCO2(totalBytes) {
+ const emissions = new tgwf.co2();
+ const greenHost = true;
+
+ let estimatedCO2 = emissions.perByte(totalBytes, greenHost).toFixed(1);
+ let totalMB = (totalBytes / (1024 * 1024)).toFixed(1);
+
+ const target = document.getElementById("network-usage");
+ if (target) {
+ target.innerHTML = `Transfert : ${totalMB} Mo
CO₂eq estimé : ${estimatedCO2} g`;
+ }
+}
diff --git a/assets/styles.css b/assets/styles.css
index bdcab37..48c25cf 100644
--- a/assets/styles.css
+++ b/assets/styles.css
@@ -2,126 +2,217 @@
body,
html {
- font-family:
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
- sans-serif;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
+ sans-serif;
}
.stAppHeader {
- visibility: hidden;
+ visibility: hidden;
}
/* Conteneur principal */
.block-container {
- max-width: 1024px !important;
- padding-left: 2rem;
- padding-right: 2rem;
- padding: 0rem 1rem 10rem;
+ max-width: 1024px !important;
+ padding-left: 2rem;
+ padding-right: 2rem;
+ padding: 0rem 1rem 10rem;
}
.stVerticalBlock {
- gap: 0.5rem !important;
+ gap: 0.5rem !important;
}
/* Lien normal (non visité) */
a {
- color: #1b5e20; /* vert foncé */
- text-decoration: none;
+ color: #1b5e20; /* vert foncé */
+ text-decoration: none;
}
/* Lien visité */
a:visited {
- color: #388e3c; /* vert moyen */
+ color: #388e3c; /* vert moyen */
}
/* Lien au survol */
a:hover {
- color: #145a1a; /* vert encore plus foncé */
- text-decoration: underline;
+ color: #145a1a; /* vert encore plus foncé */
+ text-decoration: underline;
}
/* Lien actif */
a:active {
- color: #2e7d32; /* action en cours - nuance */
+ color: #2e7d32; /* action en cours - nuance */
}
/* Couleur des boutons primaires et sliders */
.stButton > button,
.stSlider > div > div {
- background-color: darkgreen !important;
- color: white !important;
- border: 1px solid grey;
+ background-color: darkgreen !important;
+ color: white !important;
+ border: 1px solid grey;
}
/* Style pour impression */
@media print {
- body {
- font-size: 12pt;
- color: black;
- background: white;
- }
- nav,
- footer,
- .stSidebar {
- display: none !important;
- }
+ body {
+ font-size: 12pt;
+ color: black;
+ background: white;
+ }
+ nav,
+ footer,
+ .stSidebar {
+ display: none !important;
+ }
}
/* En-tête large */
.wide-header {
- width: 100vw;
- margin-left: calc(-50vw + 50%);
- background-color: #f9f9f9;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- border-bottom: 1px solid #ddd;
- text-align: center;
- padding-top: 1rem;
+ width: 100vw;
+ margin-left: calc(-50vw + 50%);
+ background-color: #f9f9f9;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid #ddd;
+ text-align: center;
+ padding-top: 1rem;
}
.titre-header {
- font-size: 2rem !important;
- font-weight: bolder !important;
- color: #555;
+ font-size: 2rem !important;
+ font-weight: bolder !important;
+ color: #555;
}
/* Accessibilité RGAA pour les onglets */
div[role="radiogroup"] > label {
- background-color: #eee;
- color: #333;
- padding: 0.5em 1em;
- border-radius: 0.4em;
- margin-right: 0.5em;
- cursor: pointer;
- border: 1px solid #ccc;
+ background-color: #eee;
+ color: #333;
+ padding: 0.5em 1em;
+ border-radius: 0.4em;
+ margin-right: 0.5em;
+ cursor: pointer;
+ border: 1px solid #ccc;
}
div[role="radiogroup"] > label[data-selected="true"] {
- background-color: #1b5e20 !important;
- color: white !important;
- font-weight: bold;
- border: 2px solid #145a1a;
+ background-color: #1b5e20 !important;
+ color: white !important;
+ font-weight: bold;
+ border: 2px solid #145a1a;
}
/* Style du graphique Plotly */
.stPlotlyChart text {
- font-family: Verdana !important;
- fill: black !important;
- font-size: 14px !important;
+ font-family: Verdana !important;
+ fill: black !important;
+ font-size: 14px !important;
}
/* Pied de page */
+/* Footer général */
.wide-footer {
- width: 100vw;
- margin-left: calc(-50vw + 50%);
- margin-top: 2rem;
- background-color: #f9f9f9;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- border-bottom: 1px solid #ddd;
- text-align: center;
- padding-top: 1rem;
+ width: 100vw;
+ margin-left: calc(-50vw + 50%);
+ margin-top: 3rem; /* changé pour matcher */
+ background-color: #f9f9f9;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ border-top: 1px solid #ddd;
+ text-align: center;
+ padding-top: 1rem;
}
+/* Texte à l'intérieur du footer */
.info-footer {
- font-size: 1rem !important;
- color: #555;
- font-weight: 800;
+ font-size: 1rem !important;
+ color: #333; /* au lieu de #555 */
+ font-weight: 800;
+}
+/* Petit paragraphe sous le footer */
+.footer-note {
+ margin-top: 0.5rem;
+ font-size: small;
+}
+
+/* Bloc impact environnemental dans sidebar */
+.impact-environnement {
+ margin-top: 1rem;
+ font-size: medium;
+}
+
+/* Div réseau pour impact CO₂ */
+#network-usage {
+ font-size: small;
+ margin-top: 1rem;
+}
+
+.decorative-heading {
+ font-size: 1.25rem;
+ font-weight: bold;
+ margin-bottom: 0.5rem;
+ color: #145a1a; /* même couleur que hover, bon contraste */
+}
+
+/* Override Streamlit file uploader limit text to 100 Ko */
+div[data-testid="stFileUploaderDropzoneInstructions"] small {
+ visibility: hidden;
+}
+div[data-testid="stFileUploaderDropzoneInstructions"] small::after {
+ content: "Limite 100 Ko par fichier • JSON";
+ visibility: visible;
+ display: block;
+ font-size: inherit;
+ color: inherit;
+ margin-top: 0.25em;
+}
+
+/* Override Streamlit file uploader limit text to 100 Ko */
+div[data-testid="stFileUploaderDropzoneInstructions"] small {
+ visibility: hidden;
+}
+div[data-testid="stFileUploaderDropzoneInstructions"] small::after {
+ content: "Limite 100 Ko par fichier • JSON";
+ visibility: visible;
+ display: block;
+ font-size: inherit;
+ color: inherit;
+ margin-top: 0.25em;
+}
+
+/* Translate Drag and drop and Browse files */
+/* Hide original "Drag and drop file here" text */
+div[data-testid="stFileUploaderDropzoneInstructions"]
+ .st-emotion-cache-j7qwjs
+ > span:nth-of-type(1) {
+ visibility: hidden;
+}
+/* Insert French translation */
+div[data-testid="stFileUploaderDropzoneInstructions"]
+ .st-emotion-cache-j7qwjs
+ > span:nth-of-type(1)::after {
+ content: "Glissez-déposez votre fichier ici";
+ visibility: visible;
+ display: block;
+ font-size: inherit;
+ color: inherit;
+}
+
+/* Hide original "Browse files" button text */
+/* Target the button within the dropzone container for uploader */
+div[data-testid="stFileUploaderDropzone"]
+ button[data-testid="stBaseButton-secondary"] {
+ color: transparent !important;
+ position: relative;
+}
+/* Insert French translation for button */
+div[data-testid="stFileUploaderDropzone"]
+ button[data-testid="stBaseButton-secondary"]::after {
+ content: "Parcourir les fichiers";
+ visibility: visible !important;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ display: block;
+ font-size: inherit;
+ color: inherit !important;
}
diff --git a/connexion.py b/connexion.py
new file mode 100644
index 0000000..65e1fb5
--- /dev/null
+++ b/connexion.py
@@ -0,0 +1,86 @@
+import streamlit as st
+import requests
+import logging
+import os
+
+def initialiser_logger():
+ LOG_FILE_PATH = "/var/log/fabnum-auth.log"
+ if not os.path.exists(os.path.dirname(LOG_FILE_PATH)):
+ os.makedirs(os.path.dirname(LOG_FILE_PATH), exist_ok=True)
+
+ logger = logging.getLogger("auth_logger")
+ logger.setLevel(logging.INFO)
+ if not logger.hasHandlers():
+ fh = logging.FileHandler(LOG_FILE_PATH)
+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+ fh.setFormatter(formatter)
+ logger.addHandler(fh)
+ return logger
+
+def connexion():
+ if not st.session_state.get("logged_in", False):
+ st.title("Authentification")
+
+ GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
+ ORGANISATION = "FabNum"
+ EQUIPE_CIBLE = "Administrateurs"
+
+ logger = initialiser_logger()
+
+ if "logged_in" not in st.session_state:
+ st.session_state.logged_in = False
+ st.session_state.username = ""
+ st.session_state.token = ""
+
+ if not st.session_state.logged_in:
+ with st.form("auth_form"):
+ token = st.text_input("Token d'accès personnel Gitea", type="password")
+ submitted = st.form_submit_button("Se connecter")
+
+ if submitted and token:
+ erreur = True
+ headers = {"Authorization": f"token {token}"}
+ ip = os.environ.get("REMOTE_ADDR", "inconnu")
+ username = "inconnu"
+
+ try:
+ user_response = requests.get(f"{GITEA_URL}/user", headers=headers, timeout=5)
+ user_response.raise_for_status()
+ utilisateur = user_response.json()
+ username = utilisateur.get("login", "inconnu")
+ logger.info(f"Tentative par {username} depuis IP {ip}")
+
+ teams_url = f"{GITEA_URL}/orgs/{ORGANISATION}/teams"
+ teams_response = requests.get(teams_url, headers=headers, timeout=5)
+ teams_response.raise_for_status()
+ equipes = teams_response.json()
+ equipe_admin = next((e for e in equipes if e["name"] == EQUIPE_CIBLE), None)
+
+ if equipe_admin:
+ team_id = equipe_admin["id"]
+ check_url = f"{GITEA_URL}/teams/{team_id}/members/{username}"
+ check_response = requests.get(check_url, headers=headers, timeout=5)
+ if check_response.status_code == 200:
+ st.session_state.logged_in = True
+ st.session_state.username = username
+ st.session_state.token = token
+ erreur = False
+ logger.info(f"Connexion réussie pour {username} depuis IP {ip}")
+ st.rerun()
+
+ except requests.RequestException:
+ st.error("❌ Impossible de vérifier l'utilisateur auprès de Gitea.")
+
+ if erreur:
+ logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
+ st.error("❌ Accès refusé.")
+
+def bouton_deconnexion():
+ if st.session_state.get("logged_in", False):
+ st.sidebar.markdown(f"Connecté en tant que `{st.session_state.username}`")
+ if st.sidebar.button("Se déconnecter"):
+ st.session_state.logged_in = False
+ st.session_state.username = ""
+ st.session_state.token = ""
+ st.success("Déconnecté avec succès.")
+ st.rerun()
diff --git a/fabnum.py b/fabnum.py
index 5663b50..852b2a8 100644
--- a/fabnum.py
+++ b/fabnum.py
@@ -1,5 +1,5 @@
import streamlit as st
-from networkx.drawing.nx_agraph import read_dot
+from networkx.drawing.nx_agraph import read_dot, write_dot
import pandas as pd
import plotly.graph_objects as go
import networkx as nx
@@ -15,7 +15,17 @@ from tickets_fiche import gerer_tickets_fiche
import base64
from dateutil import parser
from datetime import datetime, timezone
-import copy
+import streamlit.components.v1 as components
+from connexion import connexion, bouton_deconnexion
+import tempfile
+import json
+
+st.set_page_config(
+ page_title="Fabnum – Analyse de chaîne",
+ page_icon="assets/weakness.png"
+)
+
+session_id = st.context.headers.get("x-session-id")
# Configuration Gitea
load_dotenv()
@@ -26,17 +36,40 @@ ORGANISATION = os.getenv("ORGANISATION", "fabnum")
DEPOT_FICHES = os.getenv("DEPOT_FICHES", "fiches")
ENV = os.getenv("ENV")
-st.set_page_config(
- page_title="Fabnum – Analyse de chaîne",
- page_icon="assets/weakness.png"
-)
+DOT_FILE = "schema.txt"
+
+niveau_labels = {
+ 0: "Produit final",
+ 1: "Composant",
+ 2: "Minerai",
+ 10: "Opération",
+ 11: "Pays d'opération",
+ 12: "Acteur d'opération",
+ 99: "Pays géographique"
+}
+inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
+
+def get_total_bytes_for_session(session_id):
+ total_bytes = 0
+ try:
+ with open("/var/log/nginx/fabnum-dev.access.log", "r") as f:
+ for line in f:
+ if session_id in line:
+ match = re.search(r'"GET.*?" \d+ (\d+)', line)
+ if match:
+ bytes_sent = int(match.group(1))
+ total_bytes += bytes_sent
+ except Exception as e:
+ st.error(f"Erreur lecture log: {e}")
+ return total_bytes
# Intégration du fichier CSS externe
with open("assets/styles.css") as f:
st.markdown(f"", unsafe_allow_html=True)
header ="""
-
FabNum - Chaîne de fabrication du numérique
""" if ENV == "dev": @@ -46,10 +79,51 @@ else: header+="""Impact environnemental de votre session
++ Chargement en cours… +
+