From ef801609cbc6e70adf423128b9cde827cc7e0fc2 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Sat, 30 Aug 2025 18:20:50 +0200 Subject: [PATCH] add dashboard --- README.md | 18 + dashboard_instructions.md | 65 ++++ fix_csv_execution.py | 48 +++ follow_progress.py | 38 +- generate_dashboard.py | 654 ++++++++++++++++++++++++++++++++++ graphique_gantt_intrigues.png | Bin 28791 -> 16545 bytes 6 files changed, 822 insertions(+), 1 deletion(-) create mode 100644 dashboard_instructions.md create mode 100644 fix_csv_execution.py create mode 100755 generate_dashboard.py diff --git a/README.md b/README.md index 1d26e04..2f7d962 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,24 @@ Ceci alimente un fichier csv de suivi des évolutions et présente les changemen Le CSV contient les décomptes de mots pour livre.org, personnages.org, le nombre de personnages, de chapitres, et de sous chapitres. +## Tableau de bord web + +Un tableau de bord web interactif est disponible pour visualiser les données de votre livre. Ce tableau de bord inclut: + +- Statistiques générales (mots, chapitres, personnages, intrigues) +- Graphique de progression de l'écriture +- Graphique de réseau des personnages (déplaçable à la souris) +- Chronologie des intrigues + +Pour générer le tableau de bord, exécutez: +```bash +./generate_dashboard.py +``` + +Le tableau de bord sera généré dans le dossier `build/` et pourra être visualisé en ouvrant `build/dashboard.html` dans votre navigateur. + +Pour plus de détails sur l'utilisation du tableau de bord, consultez le fichier [dashboard_instructions.md](dashboard_instructions.md). + # Licence AGPLv3+ diff --git a/dashboard_instructions.md b/dashboard_instructions.md new file mode 100644 index 0000000..06ecfba --- /dev/null +++ b/dashboard_instructions.md @@ -0,0 +1,65 @@ +# Tableau de Bord du Livre + +Ce document explique comment utiliser le tableau de bord pour visualiser les données de votre livre. + +## Génération du Tableau de Bord + +Pour générer le tableau de bord, exécutez le script `generate_dashboard.py` : + +```bash +./generate_dashboard.py +``` + +Le script va créer un fichier HTML et les ressources associées dans le dossier `build/`. + +## Visualisation du Tableau de Bord + +Ouvrez le fichier `build/dashboard.html` dans votre navigateur web pour visualiser le tableau de bord. + +## Fonctionnalités du Tableau de Bord + +Le tableau de bord comprend plusieurs visualisations interactives : + +### 1. Statistiques Générales + +Affiche un résumé des statistiques clés de votre livre : +- Nombre total de mots +- Nombre de chapitres +- Nombre de personnages +- Nombre d'intrigues + +### 2. Progression de l'Écriture + +Un graphique interactif montrant l'évolution du nombre de mots et de chapitres au fil du temps. + +### 3. Réseau des Personnages + +Un graphique de réseau interactif montrant les relations entre les personnages de votre livre. +- Les personnages sont représentés par des nœuds +- Les liens entre les nœuds indiquent que les personnages apparaissent ensemble dans un ou plusieurs chapitres +- L'épaisseur des liens représente la fréquence des interactions +- **Interaction** : Vous pouvez cliquer et faire glisser les nœuds pour réorganiser le graphique + +### 4. Chronologie des Intrigues + +Un diagramme de Gantt montrant la chronologie des différentes intrigues de votre livre. +- L'axe horizontal représente la progression temporelle +- Chaque barre représente une intrigue +- **Interaction** : Passez la souris sur une intrigue pour voir plus de détails + +## Mise à Jour des Données + +Le tableau de bord utilise les fichiers de données suivants : +- `suivi_livre.csv` : Données de progression de l'écriture +- `occurrences_personnages.csv` : Données sur les apparitions des personnages +- `intrigues.csv` : Données sur les intrigues + +Pour mettre à jour le tableau de bord avec les dernières données, il suffit de réexécuter le script `generate_dashboard.py`. + +## Personnalisation + +Si vous souhaitez personnaliser l'apparence ou le comportement du tableau de bord, vous pouvez modifier les fichiers suivants : +- `build/static/css/dashboard.css` : Style du tableau de bord +- `build/static/js/network-graph.js` : Comportement du graphique de réseau +- `build/static/js/progress-chart.js` : Comportement du graphique de progression +- `build/static/js/plot-timeline.js` : Comportement du diagramme des intrigues \ No newline at end of file diff --git a/fix_csv_execution.py b/fix_csv_execution.py new file mode 100644 index 0000000..53af166 --- /dev/null +++ b/fix_csv_execution.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +""" +Script pour ajouter un commentaire en début de fichier CSV afin d'éviter +qu'il soit exécuté directement comme un script Python. +""" + +import os +import sys + +def fix_csv_file(csv_file_path): + """ + Ajoute un commentaire en début de fichier CSV pour éviter l'exécution directe. + """ + if not os.path.exists(csv_file_path): + print(f"Erreur: Le fichier {csv_file_path} n'existe pas.") + return False + + # Lire le contenu actuel du fichier + with open(csv_file_path, 'r') as f: + content = f.read() + + # Vérifier si le fichier commence déjà par un commentaire + if content.startswith('#'): + print(f"Le fichier {csv_file_path} est déjà protégé.") + return True + + # Ajouter le commentaire au début du fichier + with open(csv_file_path, 'w') as f: + f.write('# Ce fichier est un CSV et ne doit pas être exécuté directement avec Python.\n') + f.write(content) + + print(f"Le fichier {csv_file_path} a été protégé contre l'exécution directe.") + return True + +def main(): + """ + Fonction principale. + """ + # Utiliser le fichier spécifié en argument ou par défaut 'suivi_livre.csv' + if len(sys.argv) > 1: + csv_file = sys.argv[1] + else: + csv_file = 'suivi_livre.csv' + + fix_csv_file(csv_file) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/follow_progress.py b/follow_progress.py index 50f2bd9..993971c 100755 --- a/follow_progress.py +++ b/follow_progress.py @@ -3,6 +3,7 @@ # colonnes du csv: date; mots; intrigues; personnages; personnages_mots; import csv +import os from datetime import date, timedelta, datetime def mise_a_jour_suivi(fichier_csv, fichier_livre, fichier_personnages): @@ -23,6 +24,32 @@ def mise_a_jour_suivi(fichier_csv, fichier_livre, fichier_personnages): intrigues = 5 # Mettre à jour le fichier de suivi + # Vérifier si le fichier existe et s'il commence par un commentaire + file_exists = os.path.exists(fichier_csv) + needs_comment = False + + if not file_exists: + needs_comment = True + else: + with open(fichier_csv, 'r') as f: + first_line = f.readline().strip() + if not first_line.startswith('#'): + needs_comment = True + + if needs_comment: + # Sauvegarder le contenu existant si le fichier existe + content = "" + if file_exists: + with open(fichier_csv, 'r') as f: + content = f.read() + + # Réécrire le fichier avec un commentaire en tête + with open(fichier_csv, 'w', newline='') as f: + f.write('# Ce fichier est un CSV et ne doit pas être exécuté directement avec Python.\n') + if content: + f.write(content) + + # Ajouter la nouvelle ligne with open(fichier_csv, 'a', newline='') as csvfile: writer = csv.writer(csvfile, delimiter=';') now = datetime.now() @@ -38,7 +65,16 @@ def analyse_csv(fichier_csv): with open(fichier_csv, 'r') as csvfile: reader = csv.reader(csvfile, delimiter=';') - donnees = list(reader) + donnees = [] + for row in reader: + # Ignorer les lignes de commentaire qui commencent par # + if row and not row[0].startswith('#'): + donnees.append(row) + + # Si aucune donnée valide n'a été trouvée, retourner + if not donnees: + print("Aucune donnée valide trouvée dans le fichier CSV.") + return # Récupérer les dates et les nombres de mots dates = [datetime.fromisoformat(donnee[0]).date() for donnee in donnees] diff --git a/generate_dashboard.py b/generate_dashboard.py new file mode 100755 index 0000000..1e7e436 --- /dev/null +++ b/generate_dashboard.py @@ -0,0 +1,654 @@ +#!/usr/bin/env python3 +""" +Script pour générer un tableau de bord web pour le suivi d'un livre. +Ce script crée une page HTML dans le dossier 'build' avec des visualisations interactives: +- Graphique de réseau des personnages (déplaçable à la souris) +- Suivi de rédaction +- Graphique des intrigues +""" + +import os +import csv +import json +import shutil +from datetime import datetime +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +from scipy.interpolate import make_interp_spline + +# Assurez-vous que le dossier build existe +BUILD_DIR = 'build' +if not os.path.exists(BUILD_DIR): + os.makedirs(BUILD_DIR) + +# Créer le dossier pour les ressources statiques s'il n'existe pas +STATIC_DIR = os.path.join(BUILD_DIR, 'static') +if not os.path.exists(STATIC_DIR): + os.makedirs(STATIC_DIR) + +# Créer les sous-dossiers pour CSS, JS et images +CSS_DIR = os.path.join(STATIC_DIR, 'css') +JS_DIR = os.path.join(STATIC_DIR, 'js') +IMG_DIR = os.path.join(STATIC_DIR, 'img') + +for directory in [CSS_DIR, JS_DIR, IMG_DIR]: + if not os.path.exists(directory): + os.makedirs(directory) + +def load_csv_data(file_path, delimiter=','): + """Charge les données d'un fichier CSV.""" + if not os.path.exists(file_path): + print(f"Erreur: Le fichier {file_path} n'existe pas.") + return [] + + with open(file_path, 'r', encoding='utf-8') as f: + reader = csv.reader(f, delimiter=delimiter) + return list(reader) + +def process_character_occurrences(csv_file='occurrences_personnages.csv'): + """ + Traite le fichier d'occurrences des personnages pour générer les données + du graphique de réseau. + """ + data = load_csv_data(csv_file) + if not data: + return None + + # Extraire les en-têtes (noms des personnages) + headers = data[0] + characters = headers[1:] # Ignorer la première colonne (Chapitre) + + # Créer les nœuds pour chaque personnage + nodes = [{"id": char, "name": char, "group": 1} for char in characters] + + # Créer les liens entre personnages qui apparaissent dans les mêmes chapitres + links = [] + for row in data[1:]: # Ignorer la ligne d'en-tête + if len(row) < len(headers): + continue + + chapter = row[0] + # Trouver les personnages présents dans ce chapitre + present_chars = [] + for i, count in enumerate(row[1:], 1): + if count and int(count) > 0: + present_chars.append(headers[i]) + + # Créer des liens entre tous les personnages présents + for i in range(len(present_chars)): + for j in range(i+1, len(present_chars)): + # Vérifier si le lien existe déjà + link_exists = False + for link in links: + if (link["source"] == present_chars[i] and link["target"] == present_chars[j]) or \ + (link["source"] == present_chars[j] and link["target"] == present_chars[i]): + link["value"] += 1 # Incrémenter la force du lien + link_exists = True + break + + # Si le lien n'existe pas, le créer + if not link_exists: + links.append({ + "source": present_chars[i], + "target": present_chars[j], + "value": 1 + }) + + return {"nodes": nodes, "links": links} + +def process_writing_progress(csv_file='suivi_livre.csv'): + """ + Traite le fichier de suivi pour générer les données du graphique de progression. + """ + try: + # Lire le fichier CSV avec pandas pour faciliter le traitement des dates + df = pd.read_csv(csv_file, delimiter=';', + names=['date', 'mots', 'intrigues', 'personnages', 'personnages_mots', 'chapitres', 'sous_chapitres']) + + # Convertir les dates + df['date'] = pd.to_datetime(df['date']) + df['date_str'] = df['date'].dt.strftime('%Y-%m-%d') + + # Grouper par jour et prendre la dernière valeur de chaque jour + df = df.sort_values('date') + df = df.drop_duplicates('date_str', keep='last') + + # Préparer les données pour le graphique + dates = df['date_str'].tolist() + words = df['mots'].tolist() + chapters = df['chapitres'].tolist() + + return { + 'dates': dates, + 'words': words, + 'chapters': chapters + } + except Exception as e: + print(f"Erreur lors du traitement du fichier de suivi: {e}") + return None + +def process_plot_timeline(csv_file='intrigues.csv'): + """ + Traite le fichier d'intrigues pour générer les données du graphique de Gantt. + """ + data = load_csv_data(csv_file) + if not data or len(data) < 2: + return None + + # Extraire les en-têtes + headers = data[0] + if len(headers) < 3: + print("Format de fichier d'intrigues invalide.") + return None + + # Créer la structure de données pour le graphique de Gantt + plots = [] + for row in data[1:]: + if len(row) < 3: + continue + + try: + start = int(row[0]) + end = int(row[1]) + name = row[2] + + plots.append({ + 'name': name, + 'start': start, + 'end': end + }) + except (ValueError, IndexError) as e: + print(f"Erreur lors du traitement d'une ligne d'intrigue: {e}") + continue + + return plots + +def create_css(): + """Crée le fichier CSS pour le tableau de bord.""" + css_content = """ + body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + background-color: #f5f5f5; + color: #333; + } + + .container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + } + + header { + background-color: #2c3e50; + color: white; + padding: 20px; + text-align: center; + margin-bottom: 30px; + } + + h1, h2, h3 { + margin-top: 0; + } + + .dashboard-section { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 20px; + margin-bottom: 30px; + } + + .chart-container { + width: 100%; + height: 400px; + margin: 20px 0; + } + + #network-graph { + border: 1px solid #ddd; + border-radius: 4px; + } + + .stats-container { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + margin-bottom: 20px; + } + + .stat-card { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + padding: 15px; + width: calc(25% - 20px); + margin-bottom: 20px; + text-align: center; + } + + .stat-card h3 { + margin-bottom: 5px; + font-size: 16px; + color: #666; + } + + .stat-card .value { + font-size: 24px; + font-weight: bold; + color: #2c3e50; + } + + @media (max-width: 768px) { + .stat-card { + width: calc(50% - 20px); + } + } + + @media (max-width: 480px) { + .stat-card { + width: 100%; + } + } + + footer { + text-align: center; + padding: 20px; + color: #666; + font-size: 14px; + } + """ + + with open(os.path.join(CSS_DIR, 'dashboard.css'), 'w', encoding='utf-8') as f: + f.write(css_content) + +def create_network_graph_js(network_data): + """Crée le fichier JavaScript pour le graphique de réseau.""" + js_content = f""" + // Données du graphique de réseau + const networkData = {json.dumps(network_data)}; + + // Fonction pour initialiser le graphique de réseau + function initNetworkGraph() {{ + const width = document.getElementById('network-graph').clientWidth; + const height = 400; + + // Créer le SVG + const svg = d3.select("#network-graph") + .append("svg") + .attr("width", width) + .attr("height", height); + + // Créer la simulation de force + const simulation = d3.forceSimulation(networkData.nodes) + .force("link", d3.forceLink(networkData.links).id(d => d.id).distance(100)) + .force("charge", d3.forceManyBody().strength(-200)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide().radius(30)); + + // Créer les liens + const link = svg.append("g") + .attr("class", "links") + .selectAll("line") + .data(networkData.links) + .enter().append("line") + .attr("stroke-width", d => Math.sqrt(d.value)) + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6); + + // Créer les nœuds + const node = svg.append("g") + .attr("class", "nodes") + .selectAll("g") + .data(networkData.nodes) + .enter().append("g") + .call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + // Ajouter des cercles pour les nœuds + node.append("circle") + .attr("r", 10) + .attr("fill", "#69b3a2"); + + // Ajouter des étiquettes pour les nœuds + node.append("text") + .text(d => d.name) + .attr("x", 12) + .attr("y", 3) + .style("font-size", "12px"); + + // Mettre à jour la position des éléments à chaque tick de la simulation + simulation.on("tick", () => {{ + link + .attr("x1", d => d.source.x) + .attr("y1", d => d.source.y) + .attr("x2", d => d.target.x) + .attr("y2", d => d.target.y); + + node + .attr("transform", d => `translate(${{d.x}},${{d.y}})`); + }}); + + // Fonctions pour le drag & drop + function dragstarted(event, d) {{ + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }} + + function dragged(event, d) {{ + d.fx = event.x; + d.fy = event.y; + }} + + function dragended(event, d) {{ + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + }} + }} + + // Initialiser le graphique quand la page est chargée + document.addEventListener('DOMContentLoaded', initNetworkGraph); + """ + + with open(os.path.join(JS_DIR, 'network-graph.js'), 'w', encoding='utf-8') as f: + f.write(js_content) + +def create_progress_chart_js(progress_data): + """Crée le fichier JavaScript pour le graphique de progression.""" + js_content = f""" + // Données du graphique de progression + const progressData = {json.dumps(progress_data)}; + + // Fonction pour initialiser le graphique de progression + function initProgressChart() {{ + const ctx = document.getElementById('progress-chart').getContext('2d'); + + const chart = new Chart(ctx, {{ + type: 'line', + data: {{ + labels: progressData.dates, + datasets: [ + {{ + label: 'Mots', + data: progressData.words, + borderColor: '#3498db', + backgroundColor: 'rgba(52, 152, 219, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4 + }}, + {{ + label: 'Chapitres', + data: progressData.chapters, + borderColor: '#e74c3c', + backgroundColor: 'rgba(231, 76, 60, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + yAxisID: 'y1' + }} + ] + }}, + options: {{ + responsive: true, + maintainAspectRatio: false, + scales: {{ + x: {{ + title: {{ + display: true, + text: 'Date' + }} + }}, + y: {{ + title: {{ + display: true, + text: 'Nombre de mots' + }}, + beginAtZero: true + }}, + y1: {{ + position: 'right', + title: {{ + display: true, + text: 'Nombre de chapitres' + }}, + beginAtZero: true, + grid: {{ + drawOnChartArea: false + }} + }} + }}, + plugins: {{ + tooltip: {{ + mode: 'index', + intersect: false + }}, + legend: {{ + position: 'top' + }} + }} + }} + }}); + }} + + // Initialiser le graphique quand la page est chargée + document.addEventListener('DOMContentLoaded', initProgressChart); + """ + + with open(os.path.join(JS_DIR, 'progress-chart.js'), 'w', encoding='utf-8') as f: + f.write(js_content) + +def create_plot_timeline_js(plot_data): + """Crée le fichier JavaScript pour le graphique des intrigues.""" + js_content = f""" + // Données du graphique des intrigues + const plotData = {json.dumps(plot_data)}; + + // Fonction pour initialiser le graphique des intrigues + function initPlotTimeline() {{ + const width = document.getElementById('plot-timeline').clientWidth; + const height = 400; + const margin = {{ top: 50, right: 50, bottom: 50, left: 150 }}; + + // Créer le SVG + const svg = d3.select("#plot-timeline") + .append("svg") + .attr("width", width) + .attr("height", height); + + // Trouver les valeurs min et max pour l'échelle + const minStart = d3.min(plotData, d => d.start); + const maxEnd = d3.max(plotData, d => d.end); + + // Créer les échelles + const xScale = d3.scaleLinear() + .domain([minStart, maxEnd]) + .range([margin.left, width - margin.right]); + + const yScale = d3.scaleBand() + .domain(plotData.map(d => d.name)) + .range([margin.top, height - margin.bottom]) + .padding(0.2); + + // Créer les axes + const xAxis = d3.axisBottom(xScale); + const yAxis = d3.axisLeft(yScale); + + // Ajouter les axes au SVG + svg.append("g") + .attr("transform", `translate(0,${{height - margin.bottom}})`) + .call(xAxis); + + svg.append("g") + .attr("transform", `translate(${{margin.left}},0)`) + .call(yAxis); + + // Ajouter les barres pour chaque intrigue + svg.selectAll(".plot-bar") + .data(plotData) + .enter() + .append("rect") + .attr("class", "plot-bar") + .attr("x", d => xScale(d.start)) + .attr("y", d => yScale(d.name)) + .attr("width", d => xScale(d.end) - xScale(d.start)) + .attr("height", yScale.bandwidth()) + .attr("fill", "#3498db") + .attr("opacity", 0.7) + .on("mouseover", function(event, d) {{ + d3.select(this).attr("opacity", 1); + + // Afficher une infobulle + svg.append("text") + .attr("class", "tooltip") + .attr("x", xScale(d.start) + (xScale(d.end) - xScale(d.start)) / 2) + .attr("y", yScale(d.name) - 5) + .attr("text-anchor", "middle") + .text(`${{d.name}}: ${{d.start}} - ${{d.end}}`) + .style("font-size", "12px") + .style("font-weight", "bold"); + }}) + .on("mouseout", function() {{ + d3.select(this).attr("opacity", 0.7); + svg.selectAll(".tooltip").remove(); + }}); + + // Ajouter un titre + svg.append("text") + .attr("x", width / 2) + .attr("y", margin.top / 2) + .attr("text-anchor", "middle") + .style("font-size", "16px") + .style("font-weight", "bold") + .text("Chronologie des intrigues"); + }} + + // Initialiser le graphique quand la page est chargée + document.addEventListener('DOMContentLoaded', initPlotTimeline); + """ + + with open(os.path.join(JS_DIR, 'plot-timeline.js'), 'w', encoding='utf-8') as f: + f.write(js_content) + +def create_dashboard_html(network_data, progress_data, plot_data): + """Crée le fichier HTML pour le tableau de bord.""" + # Calculer quelques statistiques pour afficher + total_words = progress_data['words'][-1] if progress_data['words'] else 0 + total_chapters = progress_data['chapters'][-1] if progress_data['chapters'] else 0 + total_characters = len(network_data['nodes']) if network_data else 0 + total_plots = len(plot_data) if plot_data else 0 + + html_content = f""" + + + + + Tableau de Bord du Livre + + + + + +
+

Tableau de Bord du Livre

+

Dernière mise à jour: {datetime.now().strftime('%Y-%m-%d %H:%M')}

+
+ +
+
+

Statistiques Générales

+
+
+

Mots

+
{total_words}
+
+
+

Chapitres

+
{total_chapters}
+
+
+

Personnages

+
{total_characters}
+
+
+

Intrigues

+
{total_plots}
+
+
+
+ +
+

Progression de l'Écriture

+
+ +
+
+ +
+

Réseau des Personnages

+

Cliquez et faites glisser les nœuds pour réorganiser le graphique.

+
+
+ +
+

Chronologie des Intrigues

+
+
+
+ + + + + + + + +""" + + with open(os.path.join(BUILD_DIR, 'dashboard.html'), 'w', encoding='utf-8') as f: + f.write(html_content) + +def main(): + """Fonction principale pour générer le tableau de bord.""" + print("Génération du tableau de bord...") + + # Traiter les données + network_data = process_character_occurrences() + progress_data = process_writing_progress() + plot_data = process_plot_timeline() + + # Vérifier si les données sont disponibles + if not network_data: + network_data = {"nodes": [], "links": []} + print("Avertissement: Données de réseau des personnages non disponibles.") + + if not progress_data: + progress_data = {"dates": [], "words": [], "chapters": []} + print("Avertissement: Données de progression non disponibles.") + + if not plot_data: + plot_data = [] + print("Avertissement: Données d'intrigues non disponibles.") + + # Créer les fichiers CSS et JS + create_css() + create_network_graph_js(network_data) + create_progress_chart_js(progress_data) + create_plot_timeline_js(plot_data) + + # Créer le fichier HTML principal + create_dashboard_html(network_data, progress_data, plot_data) + + print(f"Tableau de bord généré avec succès dans {os.path.join(BUILD_DIR, 'dashboard.html')}") + print("Ouvrez ce fichier dans votre navigateur pour voir le tableau de bord.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/graphique_gantt_intrigues.png b/graphique_gantt_intrigues.png index bbc24fa7df4ccbd4203b3681219cd2fb712bfa25..23bfe6ca9fb85c4bcffd1e72106d9decec2a88d4 100644 GIT binary patch literal 16545 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 literal 28791 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