Trop de modifications

This commit is contained in:
Fabrication du Numérique 2025-05-08 00:26:02 +02:00
parent 5436ccff5e
commit 967ca4bcf2
29 changed files with 19286 additions and 1203 deletions

10
.env Normal file
View File

@ -0,0 +1,10 @@
ENV = "dev"
ENV_CODE = "dev"
PORT=8502
DOT_FILE = "schema.txt"
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
ORGANISATION = "fabnum"
DEPOT_FICHES = "fiches"
DEPOT_CODE = "code"
ID_PROJET = "3"
INSTRUCTIONS = "Instructions.md"

7
.gitignore vendored
View File

@ -1,6 +1,5 @@
# Ignorer fichiers sensibles
.env
*.env
.env.local
# Ignorer fichiers utilisateurs
*.pyc
@ -17,7 +16,8 @@ __pycache__/
# Ignorer config locale
.ropeproject/
.streamlit/
venv
venv/
.venv/
# Ignorer données Fiches (adapté à ton projet)
Instructions.md
@ -25,4 +25,3 @@ Fiches/
# Autres spécifiques si besoin
.DS_Store

View File

@ -0,0 +1,349 @@
<section role="region" aria-labelledby="fiche-assemblage-iot-wearables">
<h1 id="fiche-assemblage-iot-wearables">Fiche assemblage : IoT/Wearables</h1>
<p>Les objets connectés (IoT) et les appareils électroniques portables (wearables) constituent l'un des segments les plus dynamiques du marché des technologies, avec plus de 1,5 milliard d'unités produites annuellement et une croissance projetée de 15-20% par an. Cette catégorie englobe une grande diversité de produits, des montres connectées aux trackers fitness, en passant par les objets domotiques et les capteurs industriels. Leur assemblage présente des défis uniques liés à la miniaturisation extrême, aux contraintes énergétiques et à la nécessité d'intégrer de multiples fonctionnalités dans des volumes très restreints. Le processus d'assemblage comprend généralement le montage d'une carte électronique miniaturisée, l'intégration de capteurs spécialisés, la connexion de batteries compactes et l'encapsulation dans des boîtiers souvent étanches ou résistants. La production est fortement concentrée en Asie, avec une spécialisation croissante selon les types de produits.</p>
<hr/>
<details><summary>Composants assemblés</summary><h2>Composants assemblés</h2>
<table role="table" summary="Composants assemblés">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Composant</strong></th>
<th scope="col" style="text-align: left;"><strong>Fonction</strong></th>
<th scope="col" style="text-align: left;"><strong>Origine (fiche composant)</strong></th>
<th scope="col" style="text-align: left;"><strong>Part dans le coût total</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Processeur ARM/ASIC</td>
<td style="text-align: left;">Traitement optimisé pour faible consommation</td>
<td style="text-align: left;">Fiche composant processeur</td>
<td style="text-align: left;">12-18%</td>
</tr>
<tr>
<td style="text-align: left;">Capteurs</td>
<td style="text-align: left;">Collecte de données biométriques ou environnementales</td>
<td style="text-align: left;">Fiche composant capteurs</td>
<td style="text-align: left;">15-25%</td>
</tr>
<tr>
<td style="text-align: left;">Batterie</td>
<td style="text-align: left;">Alimentation électrique miniaturisée longue durée</td>
<td style="text-align: left;">Fiche composant batterie</td>
<td style="text-align: left;">10-15%</td>
</tr>
<tr>
<td style="text-align: left;">Écran (pour wearables)</td>
<td style="text-align: left;">Interface visuelle compacte (e-ink, OLED, LCD)</td>
<td style="text-align: left;">Fiche composant écran</td>
<td style="text-align: left;">8-15%</td>
</tr>
<tr>
<td style="text-align: left;">Mémoire RAM</td>
<td style="text-align: left;">Stockage temporaire limité</td>
<td style="text-align: left;">Fiche composant mémoire</td>
<td style="text-align: left;">5-8%</td>
</tr>
<tr>
<td style="text-align: left;">Stockage eMMC</td>
<td style="text-align: left;">Stockage permanent compact</td>
<td style="text-align: left;">Fiche composant stockage</td>
<td style="text-align: left;">4-7%</td>
</tr>
<tr>
<td style="text-align: left;">Connectivité</td>
<td style="text-align: left;">Bluetooth LE, WiFi, NFC, LoRa, Zigbee, Thread</td>
<td style="text-align: left;">Fiche composant connectivité</td>
<td style="text-align: left;">10-15%</td>
</tr>
<tr>
<td style="text-align: left;">Carte mère</td>
<td style="text-align: left;">Intégration miniaturisée des composants</td>
<td style="text-align: left;">Fiche composant carte mère</td>
<td style="text-align: left;">8-12%</td>
</tr>
<tr>
<td style="text-align: left;">Boîtier</td>
<td style="text-align: left;">Protection, étanchéité, esthétique</td>
<td style="text-align: left;">Fiche composant boîtier</td>
<td style="text-align: left;">8-12%</td>
</tr>
<tr>
<td style="text-align: left;">Connecteurs</td>
<td style="text-align: left;">Recharge, transmission de données</td>
<td style="text-align: left;">Fiche composant connecteurs</td>
<td style="text-align: left;">2-4%</td>
</tr>
<tr>
<td style="text-align: left;">Audio (pour certains)</td>
<td style="text-align: left;">Microphones, haut-parleurs miniaturisés</td>
<td style="text-align: left;">Fiche composant audio</td>
<td style="text-align: left;">3-6%</td>
</tr>
</tbody>
<caption>Composants assemblés</caption></table>
<p><em>Note: Chaque composant listé fait l'objet d'une fiche détaillée séparée qui analyse sa propre chaîne d'approvisionnement et ses vulnérabilités spécifiques. La grande diversité des produits IoT/wearables implique des variations significatives dans l'importance relative de ces composants.</em></p>
<hr/></details>
<details><summary>Principaux assembleurs</summary><h2>Principaux assembleurs</h2>
<table role="table" summary="Principaux assembleurs">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Pays d'implantation</strong></th>
<th scope="col" style="text-align: left;"><strong>Entreprise</strong></th>
<th scope="col" style="text-align: left;"><strong>Pays d'origine</strong></th>
<th scope="col" style="text-align: left;"><strong>Part de marché</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Foxconn</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">25 %</td>
</tr>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Luxshare Precision</td>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">18 %</td>
</tr>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Goertek</td>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">13 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Chine</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Chine</strong></td>
<td style="text-align: left;"><strong>56 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Vietnam</td>
<td style="text-align: left;">Compal Electronics</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">9 %</td>
</tr>
<tr>
<td style="text-align: left;">Vietnam</td>
<td style="text-align: left;">Inventec</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">6 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Vietnam</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Vietnam</strong></td>
<td style="text-align: left;"><strong>15 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Malaisie</td>
<td style="text-align: left;">Flextronics</td>
<td style="text-align: left;">États-Unis</td>
<td style="text-align: left;">7 %</td>
</tr>
<tr>
<td style="text-align: left;">Malaisie</td>
<td style="text-align: left;">Jabil Circuit</td>
<td style="text-align: left;">États-Unis</td>
<td style="text-align: left;">5 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Malaisie</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Malaisie</strong></td>
<td style="text-align: left;"><strong>12 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Corée du Sud</td>
<td style="text-align: left;">Samsung Electronics</td>
<td style="text-align: left;">Corée du Sud</td>
<td style="text-align: left;">6 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Corée du Sud</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Corée du Sud</strong></td>
<td style="text-align: left;"><strong>6 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Inde</td>
<td style="text-align: left;">Dixon Technologies</td>
<td style="text-align: left;">Inde</td>
<td style="text-align: left;">4 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Inde</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Inde</strong></td>
<td style="text-align: left;"><strong>4 %</strong></td>
</tr>
</tbody>
<caption>Principaux assembleurs</caption></table>
<p><em>Note: Les capacités indiquées représentent la capacité d'assemblage annuelle en 2024-2025. Une spécialisation s'observe entre la production massive en Chine, les wearables haut de gamme en Corée/Malaisie, et les solutions IoT industrielles dans différentes régions.</em></p>
<hr/></details>
<details><summary>Contraintes spécifiques à l'assemblage</summary><h2>Contraintes spécifiques à l'assemblage</h2>
<table role="table" summary="Contraintes spécifiques à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Contrainte</strong></th>
<th scope="col" style="text-align: left;"><strong>Description</strong></th>
<th scope="col" style="text-align: left;"><strong>Impact sur la production</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Miniaturisation extrême</td>
<td style="text-align: left;">Assemblage de composants à des échelles submillimétriques</td>
<td style="text-align: left;">Équipements spécialisés et précision accrue</td>
</tr>
<tr>
<td style="text-align: left;">Étanchéité</td>
<td style="text-align: left;">Résistance à l'eau/poussière (IP67/IP68) pour wearables</td>
<td style="text-align: left;">Tests sous pression ajoutant 5-10% au temps de production</td>
</tr>
<tr>
<td style="text-align: left;">Efficience énergétique</td>
<td style="text-align: left;">Optimisation pour autonomie maximale</td>
<td style="text-align: left;">Tests de décharge complets pour chaque lot</td>
</tr>
<tr>
<td style="text-align: left;">Variabilité des produits</td>
<td style="text-align: left;">Grande diversité de formes et fonctions</td>
<td style="text-align: left;">Lignes de production flexibles à reconfiguration rapide</td>
</tr>
<tr>
<td style="text-align: left;">Cycles de vie courts</td>
<td style="text-align: left;">Renouvellement rapide des gammes (12-18 mois)</td>
<td style="text-align: left;">Amortissement accéléré des équipements d'assemblage</td>
</tr>
<tr>
<td style="text-align: left;">Défi des matériaux</td>
<td style="text-align: left;">Combinaison de plastiques, métaux, textiles, etc.</td>
<td style="text-align: left;">Processus d'assemblage multi-matériaux complexes</td>
</tr>
<tr>
<td style="text-align: left;">Soudure miniaturisée</td>
<td style="text-align: left;">Connexions fiables sur des surfaces très réduites</td>
<td style="text-align: left;">Taux de défauts 15-20% plus élevé que l'électronique standard</td>
</tr>
<tr>
<td style="text-align: left;">Fiabilité des capteurs</td>
<td style="text-align: left;">Calibration individuelle nécessaire</td>
<td style="text-align: left;">Augmentation du temps de production de 10-15%</td>
</tr>
</tbody>
<caption>Contraintes spécifiques à l'assemblage</caption></table>
<p><em>Note: Ces contraintes concernent spécifiquement l'étape d'assemblage final et non la fabrication des composants individuels qui ont leurs propres contraintes traitées dans les fiches spécifiques.</em></p>
<hr/></details>
<details><summary>Matrice des risques liés à l'assemblage</summary><h2>Matrice des risques liés à l'assemblage</h2>
<table role="table" summary="Matrice des risques liés à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Impact/Probabilité</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Moyen</strong></th>
<th scope="col" style="text-align: left;"><strong>Fort</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Fort</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">R1 (Fiabilité long terme)</td>
<td style="text-align: left;">R2 (Miniaturisation extrême)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Moyen</strong></td>
<td style="text-align: left;">R6 (Standardisation)</td>
<td style="text-align: left;">R3 (Chaîne fragmentée)</td>
<td style="text-align: left;">R4 (Volatilité du marché)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Faible</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
</tr>
</tbody>
<caption>Matrice des risques liés à l'assemblage</caption></table>
<p><strong>Détail des risques principaux:</strong></p>
<ul>
<li><strong>R1</strong>: Difficultés à garantir la fiabilité à long terme d'appareils soumis à des conditions d'utilisation exigeantes (transpiration, chocs, etc.)</li>
<li><strong>R2</strong>: Limites technologiques de la miniaturisation avec des composants atteignant des dimensions critiques pour l'assemblage manuel ou automatisé</li>
<li><strong>R3</strong>: Fragmentation extrême de la chaîne d'approvisionnement avec des centaines de fournisseurs spécialisés et peu substituables</li>
<li><strong>R4</strong>: Volatilité du marché et cycles de produits très courts rendant difficile la planification de production à moyen terme</li>
<li><strong>R5</strong>: Combinaison de matériaux multiples dans des volumes très restreints rendant le recyclage particulièrement complexe</li>
<li><strong>R6</strong>: Absence de standardisation entre fabricants limitant les économies d'échelle sur les équipements d'assemblage</li>
</ul>
<h3>Indice de Herfindahl-Hirschmann</h3>
<table role="table" summary="Matrice des risques liés à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>IHH</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Modéré</strong></th>
<th scope="col" style="text-align: left;"><strong>Élevé</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Acteurs</strong></td>
<td style="text-align: left;"><strong>14</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
</tr>
<tr>
<td style="text-align: left;"><strong>Pays</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"><strong>36</strong></td>
</tr>
</tbody>
<caption>Matrice des risques liés à l'assemblage</caption></table>
<h4>IHH par entreprise (acteurs)</h4>
<p>LIHH pour les assembleurs dobjets connectés et wearables est de <strong>14</strong>, ce qui indique une <strong>concentration faible</strong>. Bien que <strong>Foxconn, Luxshare et Goertek</strong> regroupent plus de 55 % du marché, plusieurs autres groupes comme Compal, Inventec, Flex et Jabil viennent équilibrer le secteur. Cette structure permet une <strong>certaine résilience industrielle</strong>, avec plusieurs options en cas de tension sur un acteur majeur.</p>
<h4>IHH par pays</h4>
<p>LIHH par pays atteint <strong>36</strong>, révélant une <strong>concentration géographique élevée</strong>. La <strong>Chine domine avec 56 %</strong> des capacités dassemblage, suivie du Vietnam (15 %) et de la Malaisie (12 %). Cette dépendance marquée à lAsie de lEst expose fortement la chaîne à des <strong>risques géopolitiques, logistiques ou sanitaires localisés</strong>.</p>
<h4>En résumé</h4>
<ul>
<li>Le marché présente une <strong>structure dacteurs plutôt diversifiée</strong> (IHH 14), favorable à la flexibilité</li>
<li>La <strong>concentration géographique est élevée</strong> (IHH 36), notamment en faveur de la Chine et de ses sous-traitants</li>
<li>Cette configuration <strong>confirme la pertinence des scénarios critiques projetés</strong>, en particulier ceux liés aux capteurs et à la régulation</li>
<li>La diversification géographique ou sectorielle (IoT industriel, médical, etc.) est un axe stratégique majeur pour renforcer la robustesse de la chaîne</li>
</ul>
<hr/>
<p>Souhaites-tu que je clôture cette série par un tableau comparatif des IHH pour tous les assemblages traités ?</p>
<hr/></details>
<details><summary>Scénarios critiques projetés</summary><h2>Scénarios critiques projetés</h2>
<h3>Scénario 1 : Pénurie mondiale de capteurs ou batteries miniaturisées</h3>
<ul>
<li><strong>Type</strong> : Technique / Rupture de composants</li>
<li><strong>Impact</strong> : Arrêt ou ralentissement de lignes entières de wearables et IoT domestiques</li>
<li><strong>Chaînes affectées</strong> : Appareils santé, montres connectées, objets domotiques</li>
<li><strong>Répercussions</strong> : Hausse de prix, baisse de qualité (remplacement par composants de moindre performance), perte de part de marché</li>
</ul>
<h3>Scénario 2 : Durcissement réglementaire sur la confidentialité des données</h3>
<ul>
<li><strong>Type</strong> : Réglementaire / géopolitique</li>
<li><strong>Impact</strong> : Mise à larrêt dassemblages destinés à certaines régions (ex. Europe)</li>
<li><strong>Chaînes affectées</strong> : Fabricants de trackers de santé, assistants vocaux, objets connectés intelligents</li>
<li><strong>Répercussions</strong> : Modification des configurations logicielles/hardware en fin de ligne, requalification des produits, nécessité de relocalisation</li>
</ul>
<hr/></details>
<details><summary>Points de vigilance sur la cohérence des données</summary><h2>Points de vigilance sur la cohérence des données</h2>
<ul>
<li>Les parts de marché des assembleurs proviennent souvent de compilations indirectes ou de rapports non publics</li>
<li>Segmentation entre IoT, domotique, wearables et capteurs industriels parfois floue</li>
<li>Évolution rapide des standards (Bluetooth LE, Thread, UWB) non toujours visible dans les chiffres</li>
<li>Difficultés à tracer les chaînes dassemblage spécifiques par sous-produit</li>
</ul>
<hr/></details>
<details><summary>Sources utilisées</summary><h2>Sources utilisées</h2>
<ol>
<li><a href="https://www.semanticscholar.org/paper/b0793441d9350ae077a708818885bc3ffcd9fd00">Semanticscholar Supply Chain IoT Miniaturisation</a></li>
<li><a href="https://www.semanticscholar.org/paper/2ee146ce5aed986555d28af2be344f61c749718b">Semanticscholar Device Assembly Challenges</a></li>
<li><a href="https://it-recycle.uk/smartphone-materials/">IT-Recycle UK Smartphone Materials</a></li>
<li><a href="https://www.made-in-china.com/manufacturers/phone-assembly.html">Made-in-China IoT Device Assembly</a></li>
</ol></details>
</section>

View File

@ -0,0 +1,344 @@
<section role="region" aria-labelledby="fiche-assemblage-casque-vr">
<h1 id="fiche-assemblage-casque-vr">Fiche assemblage : Casque VR</h1>
<table role="table" summary>
<thead>
<tr>
<th scope="col" style="text-align: left;">Version</th>
<th scope="col" style="text-align: left;">Date</th>
<th scope="col" style="text-align: left;">Commentaire</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">1.0</td>
<td style="text-align: left;">22 avril 2025</td>
<td style="text-align: left;">Version initiale</td>
</tr>
</tbody>
</table>
<details><summary>Présentation synthétique</summary><h2>Présentation synthétique</h2>
<p>Les casques de réalité virtuelle (VR) représentent un segment en forte croissance du marché des périphériques immersifs, avec environ 15 millions d'unités vendues annuellement et une progression estimée à 20-25% par an. Leur assemblage est particulièrement complexe, combinant des composants optiques de précision, des écrans haute résolution et de nombreux capteurs dans un espace restreint tout en maintenant un poids et un confort acceptables. Le processus d'assemblage comprend l'intégration des écrans, des lentilles optiques, des cartes électroniques, des capteurs de mouvement, des modules audio et des caméras de tracking, avant l'installation du système de fixation ergonomique. Cette production est majoritairement concentrée en Chine, avec quelques sites spécialisés aux États-Unis et en Corée du Sud pour les modèles haut de gamme.</p></details>
<details><summary>Composants assemblés</summary><h2>Composants assemblés</h2>
<table role="table" summary="Composants assemblés">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Composant</strong></th>
<th scope="col" style="text-align: left;"><strong>Fonction</strong></th>
<th scope="col" style="text-align: left;"><strong>Origine (fiche composant)</strong></th>
<th scope="col" style="text-align: left;"><strong>Part dans le coût total</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Écran LCD/OLED/MicroLED</td>
<td style="text-align: left;">Affichage haute résolution pour chaque œil</td>
<td style="text-align: left;">Fiche composant écran</td>
<td style="text-align: left;">22-28%</td>
</tr>
<tr>
<td style="text-align: left;">Optiques (lentilles)</td>
<td style="text-align: left;">Focalisation et ajustement de l'image pour perception 3D</td>
<td style="text-align: left;">Fiche composant optiques</td>
<td style="text-align: left;">12-15%</td>
</tr>
<tr>
<td style="text-align: left;">Processeur ARM</td>
<td style="text-align: left;">Traitement des données, rendu graphique</td>
<td style="text-align: left;">Fiche composant processeur</td>
<td style="text-align: left;">15-18%</td>
</tr>
<tr>
<td style="text-align: left;">Mémoire RAM</td>
<td style="text-align: left;">Stockage temporaire pour applications en cours</td>
<td style="text-align: left;">Fiche composant mémoire</td>
<td style="text-align: left;">6-8%</td>
</tr>
<tr>
<td style="text-align: left;">Stockage eMMC/UFS</td>
<td style="text-align: left;">Stockage permanent des applications et du système</td>
<td style="text-align: left;">Fiche composant stockage</td>
<td style="text-align: left;">5-7%</td>
</tr>
<tr>
<td style="text-align: left;">Batterie</td>
<td style="text-align: left;">Alimentation électrique (modèles autonomes)</td>
<td style="text-align: left;">Fiche composant batterie</td>
<td style="text-align: left;">8-10%</td>
</tr>
<tr>
<td style="text-align: left;">Capteurs</td>
<td style="text-align: left;">Gyroscope, accéléromètre, proximité, suivi de mouvement</td>
<td style="text-align: left;">Fiche composant capteurs</td>
<td style="text-align: left;">8-12%</td>
</tr>
<tr>
<td style="text-align: left;">Caméra</td>
<td style="text-align: left;">Tracking du mouvement, réalité mixte</td>
<td style="text-align: left;">Fiche composant caméra</td>
<td style="text-align: left;">5-8%</td>
</tr>
<tr>
<td style="text-align: left;">Boîtier</td>
<td style="text-align: left;">Structure, confort et isolation lumineuse</td>
<td style="text-align: left;">Fiche composant boîtier</td>
<td style="text-align: left;">8-10%</td>
</tr>
<tr>
<td style="text-align: left;">Audio</td>
<td style="text-align: left;">Reproduction sonore spatiale</td>
<td style="text-align: left;">Fiche composant audio</td>
<td style="text-align: left;">3-5%</td>
</tr>
<tr>
<td style="text-align: left;">Connectivité</td>
<td style="text-align: left;">WiFi, Bluetooth, transmission sans fil du signal</td>
<td style="text-align: left;">Fiche composant connectivité</td>
<td style="text-align: left;">3-5%</td>
</tr>
<tr>
<td style="text-align: left;">Connecteurs</td>
<td style="text-align: left;">Ports d'alimentation et connexions</td>
<td style="text-align: left;">Fiche composant connecteurs</td>
<td style="text-align: left;">1-2%</td>
</tr>
</tbody>
<caption>Composants assemblés</caption></table>
<p><em>Note: Chaque composant listé fait l'objet d'une fiche détaillée séparée qui analyse sa propre chaîne d'approvisionnement et ses vulnérabilités spécifiques.</em></p></details>
<details><summary>Principaux assembleurs</summary><h2>Principaux assembleurs</h2>
<table role="table" summary="Principaux assembleurs">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Pays d'implantation</strong></th>
<th scope="col" style="text-align: left;"><strong>Entreprise</strong></th>
<th scope="col" style="text-align: left;"><strong>Pays d'origine</strong></th>
<th scope="col" style="text-align: left;"><strong>Part de marché</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Goertek</td>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">40 %</td>
</tr>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Luxshare Precision</td>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">22 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Chine</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Chine</strong></td>
<td style="text-align: left;"><strong>62 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">Foxconn</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">9 %</td>
</tr>
<tr>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">Pegatron</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">6 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Taïwan</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Taïwan</strong></td>
<td style="text-align: left;"><strong>15 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">États-Unis</td>
<td style="text-align: left;">Flextronics</td>
<td style="text-align: left;">États-Unis</td>
<td style="text-align: left;">7 %</td>
</tr>
<tr>
<td style="text-align: left;">États-Unis</td>
<td style="text-align: left;">Jabil Circuit</td>
<td style="text-align: left;">États-Unis</td>
<td style="text-align: left;">5 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>États-Unis</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>États-Unis</strong></td>
<td style="text-align: left;"><strong>12 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Corée du Sud</td>
<td style="text-align: left;">Samsung Electronics</td>
<td style="text-align: left;">Corée du Sud</td>
<td style="text-align: left;">6 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Corée du Sud</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Corée du Sud</strong></td>
<td style="text-align: left;"><strong>6 %</strong></td>
</tr>
</tbody>
<caption>Principaux assembleurs</caption></table>
<p><em>Note: Les capacités indiquées représentent la capacité d'assemblage annuelle en 2024-2025. On observe une spécialisation géographique selon le segment de marché, avec les modèles premium plus souvent assemblés aux États-Unis et en Corée du Sud.</em></p></details>
<details><summary>Contraintes spécifiques à l'assemblage</summary><h2>Contraintes spécifiques à l'assemblage</h2>
<table role="table" summary="Contraintes spécifiques à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Contrainte</strong></th>
<th scope="col" style="text-align: left;"><strong>Description</strong></th>
<th scope="col" style="text-align: left;"><strong>Impact sur la production</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Précision optique</td>
<td style="text-align: left;">Alignement critique des écrans et des lentilles à ±0.1mm</td>
<td style="text-align: left;">Taux de rejet de 5-8% lié à des défauts d'alignement</td>
</tr>
<tr>
<td style="text-align: left;">Calibration des capteurs</td>
<td style="text-align: left;">Étalonnage individuel de chaque unité</td>
<td style="text-align: left;">Augmente le temps d'assemblage de 15-20%</td>
</tr>
<tr>
<td style="text-align: left;">Ergonomie</td>
<td style="text-align: left;">Équilibre du poids et confort nécessitant des ajustements précis</td>
<td style="text-align: left;">Tests utilisateurs supplémentaires</td>
</tr>
<tr>
<td style="text-align: left;">Dissipation thermique</td>
<td style="text-align: left;">Concentration de composants à forte consommation près du visage</td>
<td style="text-align: left;">Solutions thermiques complexes ajoutant poids et coût</td>
</tr>
<tr>
<td style="text-align: left;">Étanchéité à la lumière</td>
<td style="text-align: left;">Nécessité d'isoler complètement l'utilisateur de la lumière extérieure</td>
<td style="text-align: left;">Tests spécifiques d'étanchéité lumineuse</td>
</tr>
<tr>
<td style="text-align: left;">Prévention de la buée</td>
<td style="text-align: left;">Systèmes anti-condensation sur les optiques</td>
<td style="text-align: left;">Composants additionnels et tests en environnement contrôlé</td>
</tr>
<tr>
<td style="text-align: left;">Assemblage mixte</td>
<td style="text-align: left;">Combinaison de processus automatisés et manuels pour les ajustements fins</td>
<td style="text-align: left;">Réduction limitée de la main d'œuvre (50-60%)</td>
</tr>
<tr>
<td style="text-align: left;">Formation spécialisée</td>
<td style="text-align: left;">Personnel qualifié pour l'assemblage optique et la calibration</td>
<td style="text-align: left;">Coût de formation 30-40% plus élevé que pour d'autres électroniques</td>
</tr>
</tbody>
<caption>Contraintes spécifiques à l'assemblage</caption></table>
<p><em>Note: Ces contraintes concernent spécifiquement l'étape d'assemblage final et non la fabrication des composants individuels qui ont leurs propres contraintes traitées dans les fiches spécifiques.</em></p></details>
<details><summary>Matrice des risques liés à l'assemblage</summary><h2>Matrice des risques liés à l'assemblage</h2>
<table role="table" summary="Matrice des risques liés à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Impact/Probabilité</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Moyen</strong></th>
<th scope="col" style="text-align: left;"><strong>Fort</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Fort</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">R1 (Qualité optique)</td>
<td style="text-align: left;">R2 (Pénurie écrans spécialisés)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Moyen</strong></td>
<td style="text-align: left;">R6 (Validation ergonomique)</td>
<td style="text-align: left;">R3 (Concentration géographique)</td>
<td style="text-align: left;">R4 (Compétences spécialisées)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Faible</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">R5 (Coûts de transport)</td>
<td style="text-align: left;"></td>
</tr>
</tbody>
<caption>Matrice des risques liés à l'assemblage</caption></table>
<p><strong>Détail des risques principaux:</strong></p>
<ul>
<li><strong>R1</strong>: Exigences critiques de qualité optique et d'alignement, premier facteur de rejet en production</li>
<li><strong>R2</strong>: Dépendance à des écrans haute résolution/haute fréquence spécifiques avec peu de fournisseurs alternatifs</li>
<li><strong>R3</strong>: Concentration de 62% de l'assemblage en Chine malgré les tentatives de diversification</li>
<li><strong>R4</strong>: Disponibilité limitée de techniciens qualifiés en optique et calibration de précision</li>
<li><strong>R5</strong>: Fragilité des composants optiques nécessitant des emballages spéciaux et augmentant les coûts logistiques</li>
<li><strong>R6</strong>: Nécessité de tests utilisateurs extensifs pour validation du confort, difficilement automatisables</li>
</ul>
<p>Voici comment je veux que ce soit présenté. Pour la fiche serveur, calcule lIHH acteurs et pays et donne moi le chapitre correspondant dans une zone texte à copier.</p>
<h3>Indice de Herfindahl-Hirschmann</h3>
<table role="table" summary="Matrice des risques liés à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>IHH</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Modéré</strong></th>
<th scope="col" style="text-align: left;"><strong>Élevé</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Acteurs</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">20</td>
<td style="text-align: left;"></td>
</tr>
<tr>
<td style="text-align: left;"><strong>Pays</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">41</td>
</tr>
</tbody>
<caption>Matrice des risques liés à l'assemblage</caption></table>
<h4>IHH par entreprise (acteurs)</h4>
<p>L'IHH calculé pour les principaux assembleurs de casques VR est de <strong>20</strong>, ce qui indique une <strong>concentration modérée</strong>. Le marché est dominé par <strong>Goertek et Luxshare</strong> qui totalisent <strong>62 %</strong> du marché, mais il subsiste une diversité dacteurs secondaires. Cela limite la dépendance extrême à un seul fournisseur tout en appelant à une vigilance en cas de consolidation future.</p>
<h4>IHH par pays</h4>
<p>L'IHH par pays atteint <strong>41</strong>, ce qui marque une <strong>forte concentration géographique</strong>. La Chine regroupe seule <strong>62 %</strong> des capacités dassemblage, ce qui rend la chaîne très vulnérable à des événements géopolitiques, sanitaires ou douaniers. Les autres pays (Taïwan, États-Unis, Corée du Sud) disposent de parts trop faibles pour rééquilibrer significativement le risque.</p>
<h4>En résumé</h4>
<ul>
<li>Le marché présente un <strong>risque modéré sur les acteurs industriels</strong> (IHH 20)</li>
<li>En revanche, il est <strong>hautement dépendant dun seul pays (Chine)</strong> pour lassemblage (IHH 41)</li>
<li>Cette dépendance géographique est lun des facteurs de vulnérabilité majeurs à surveiller</li>
<li>Le calcul de lIHH renforce ici la pertinence du scénario géopolitique ci-dessous</li>
</ul></details>
<details><summary>Scénarios critiques projetés</summary><h2>Scénarios critiques projetés</h2>
<h3>Scénario 1 : Pénurie ciblée de composants clés</h3>
<ul>
<li>Type : Technique / logistique</li>
<li>Impact : Retards de production dus à la non-disponibilité de composants spécifiques (ex. GPU, lentilles, modules RF)</li>
<li>Chaînes affectées : Lignes dépendantes de fournisseurs uniques ou zones géographiques spécifiques</li>
<li>Répercussions : Hausse des prix, délais étendus, réallocation vers d'autres modèles</li>
</ul>
<h3>Scénario 2 : Restrictions géopolitiques sur la chaîne dassemblage</h3>
<ul>
<li>Type : Géopolitique</li>
<li>Impact : Embargos ou sanctions affectant les sites dassemblage (ex. Chine, Taiwan)</li>
<li>Chaînes affectées : Réduction immédiate de capacité, besoins en relocalisation</li>
<li>Répercussions : Baisse temporaire de production, réorganisation logistique, dépendance à des stocks</li>
</ul></details>
<details><summary>Points de vigilance sur la cohérence des données</summary><h2>Points de vigilance sur la cohérence des données</h2>
<ul>
<li>Vérifier la couverture complète des acteurs chinois (Luxshare, Goertek) avec les marques</li>
<li>Absence de sources primaires publiques pour les parts de marché exactes : estimation par compilation</li>
<li>Segmentation premium/standard peut évoluer rapidement (influence des modèles Apple, Meta)</li>
<li>Données 2024-2025 sujettes à forte volatilité selon évolutions technologiques</li>
</ul></details>
<details><summary>Sources utilisées</summary><h2>Sources utilisées</h2>
<ol>
<li><a href="https://www.made-in-china.com/manufacturers/phone-assembly.html">Made-in-China Assemblage électronique</a></li>
<li><a href="https://it-recycle.uk/smartphone-materials/">IT-Recycle UK Smartphone Materials</a></li>
</ol></details>
</section>

View File

@ -0,0 +1,360 @@
<section role="region" aria-labelledby="fiche-assemblage-imprimante">
<h1 id="fiche-assemblage-imprimante">Fiche assemblage : Imprimante</h1>
<p>Les imprimantes représentent un segment mature mais toujours essentiel du marché des périphériques informatiques, avec environ 80 millions d'unités produites annuellement. Ce marché englobe diverses technologies (jet d'encre, laser, thermique, 3D) destinées aux usages personnels, professionnels et industriels. L'assemblage des imprimantes présente des défis spécifiques liés à la précision mécanique, à l'intégration de systèmes électromécaniques complexes et à la nécessité d'une fiabilité élevée. Le processus comprend généralement le montage d'un châssis mécanique, l'installation des moteurs et systèmes d'entraînement, l'intégration de la tête d'impression ou du système laser, le montage de la carte mère et des composants électroniques, puis l'assemblage du boîtier extérieur. La production est répartie entre quelques acteurs majeurs, avec une concentration en Asie pour les modèles grand public et une fabrication plus distribuée pour les équipements professionnels et industriels.</p>
<hr/>
<details><summary>Composants assemblés</summary><h2>Composants assemblés</h2>
<table role="table" summary="Composants assemblés">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Composant</strong></th>
<th scope="col" style="text-align: left;"><strong>Fonction</strong></th>
<th scope="col" style="text-align: left;"><strong>Origine (fiche composant)</strong></th>
<th scope="col" style="text-align: left;"><strong>Part dans le coût total</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Carte mère</td>
<td style="text-align: left;">Contrôle électronique et traitement des données</td>
<td style="text-align: left;">Fiche composant carte mère</td>
<td style="text-align: left;">12-15%</td>
</tr>
<tr>
<td style="text-align: left;">Processeur ARM</td>
<td style="text-align: left;">Interprétation des fichiers et contrôle des mécanismes</td>
<td style="text-align: left;">Fiche composant processeur</td>
<td style="text-align: left;">5-8%</td>
</tr>
<tr>
<td style="text-align: left;">Mémoire RAM</td>
<td style="text-align: left;">Stockage temporaire des travaux d'impression</td>
<td style="text-align: left;">Fiche composant mémoire</td>
<td style="text-align: left;">3-5%</td>
</tr>
<tr>
<td style="text-align: left;">Stockage eMMC</td>
<td style="text-align: left;">Firmware et configurations</td>
<td style="text-align: left;">Fiche composant stockage</td>
<td style="text-align: left;">2-4%</td>
</tr>
<tr>
<td style="text-align: left;">Mécanisme d'impression</td>
<td style="text-align: left;">Système jet d'encre, laser ou thermique</td>
<td style="text-align: left;">Fiche composant mécanisme d'impression</td>
<td style="text-align: left;">20-30%</td>
</tr>
<tr>
<td style="text-align: left;">Moteurs et systèmes d'entraînement</td>
<td style="text-align: left;">Déplacement du papier et des têtes d'impression</td>
<td style="text-align: left;">Fiche composant moteurs</td>
<td style="text-align: left;">15-20%</td>
</tr>
<tr>
<td style="text-align: left;">Connectivité</td>
<td style="text-align: left;">WiFi, Ethernet, USB, Bluetooth</td>
<td style="text-align: left;">Fiche composant connectivité</td>
<td style="text-align: left;">5-8%</td>
</tr>
<tr>
<td style="text-align: left;">Écran LCD</td>
<td style="text-align: left;">Interface utilisateur</td>
<td style="text-align: left;">Fiche composant écran</td>
<td style="text-align: left;">3-6%</td>
</tr>
<tr>
<td style="text-align: left;">Capteurs</td>
<td style="text-align: left;">Détection de papier, niveaux d'encre/toner</td>
<td style="text-align: left;">Fiche composant capteurs</td>
<td style="text-align: left;">4-6%</td>
</tr>
<tr>
<td style="text-align: left;">Connecteurs</td>
<td style="text-align: left;">Alimentation et interfaces physiques</td>
<td style="text-align: left;">Fiche composant connecteurs</td>
<td style="text-align: left;">2-3%</td>
</tr>
<tr>
<td style="text-align: left;">Alimentation</td>
<td style="text-align: left;">Conversion électrique et distribution</td>
<td style="text-align: left;">Fiche composant alimentation</td>
<td style="text-align: left;">5-7%</td>
</tr>
<tr>
<td style="text-align: left;">Boîtier</td>
<td style="text-align: left;">Structure et protection des composants</td>
<td style="text-align: left;">Fiche composant boîtier</td>
<td style="text-align: left;">10-15%</td>
</tr>
</tbody>
<caption>Composants assemblés</caption></table>
<p><em>Note: Chaque composant listé fait l'objet d'une fiche détaillée séparée qui analyse sa propre chaîne d'approvisionnement et ses vulnérabilités spécifiques. La répartition des coûts varie significativement selon la technologie d'impression et la gamme de produit.</em></p>
<hr/></details>
<details><summary>Principaux assembleurs</summary><h2>Principaux assembleurs</h2>
<table role="table" summary="Principaux assembleurs">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Pays d'implantation</strong></th>
<th scope="col" style="text-align: left;"><strong>Entreprise</strong></th>
<th scope="col" style="text-align: left;"><strong>Pays d'origine</strong></th>
<th scope="col" style="text-align: left;"><strong>Part de marché</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Foxconn</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">15 %</td>
</tr>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Cal-Comp</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">12 %</td>
</tr>
<tr>
<td style="text-align: left;">Chine</td>
<td style="text-align: left;">Kinpo Electronics</td>
<td style="text-align: left;">Taïwan</td>
<td style="text-align: left;">10 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Chine</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Chine</strong></td>
<td style="text-align: left;"><strong>37 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Thailande</td>
<td style="text-align: left;">Canon Thailand</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">14 %</td>
</tr>
<tr>
<td style="text-align: left;">Thailande</td>
<td style="text-align: left;">Epson Thailand</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">11 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Thailande</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Thailande</strong></td>
<td style="text-align: left;"><strong>25 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Philippines</td>
<td style="text-align: left;">Brother Industries</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">9 %</td>
</tr>
<tr>
<td style="text-align: left;">Philippines</td>
<td style="text-align: left;">Canon Philippines</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">6 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Philippines</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Philippines</strong></td>
<td style="text-align: left;"><strong>15 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">Canon</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">7 %</td>
</tr>
<tr>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">Ricoh</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">5 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Japon</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Japon</strong></td>
<td style="text-align: left;"><strong>12 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Malaisie</td>
<td style="text-align: left;">HP Malaysia</td>
<td style="text-align: left;">États-Unis</td>
<td style="text-align: left;">6 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Malaisie</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Malaisie</strong></td>
<td style="text-align: left;"><strong>6 %</strong></td>
</tr>
</tbody>
<caption>Principaux assembleurs</caption></table>
<p><em>Note: Les capacités indiquées représentent la capacité d'assemblage annuelle en 2024-2025. On observe une spécialisation par technologie et segment de marché, avec les modèles grand public principalement assemblés en Chine et les équipements professionnels au Japon.</em></p>
<hr/></details>
<details><summary>Contraintes spécifiques à l'assemblage</summary><h2>Contraintes spécifiques à l'assemblage</h2>
<table role="table" summary="Contraintes spécifiques à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Contrainte</strong></th>
<th scope="col" style="text-align: left;"><strong>Description</strong></th>
<th scope="col" style="text-align: left;"><strong>Impact sur la production</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Précision mécanique</td>
<td style="text-align: left;">Alignement des mécanismes d'impression à ±0.05mm</td>
<td style="text-align: left;">Calibration individuelle augmentant le temps d'assemblage</td>
</tr>
<tr>
<td style="text-align: left;">Diversité technologique</td>
<td style="text-align: left;">Différences fondamentales entre jet d'encre, laser, thermique</td>
<td style="text-align: left;">Lignes d'assemblage spécifiques avec faible interchangeabilité</td>
</tr>
<tr>
<td style="text-align: left;">Systèmes électromécaniques</td>
<td style="text-align: left;">Intégration de moteurs de précision et de transmissions</td>
<td style="text-align: left;">Nécessite des tests fonctionnels prolongés</td>
</tr>
<tr>
<td style="text-align: left;">Fiabilité à long terme</td>
<td style="text-align: left;">Appareils devant fonctionner pendant 3-7 ans</td>
<td style="text-align: left;">Tests de durabilité par échantillonnage</td>
</tr>
<tr>
<td style="text-align: left;">Gestion de l'encre/toner</td>
<td style="text-align: left;">Systèmes d'alimentation sans fuites ni contamination</td>
<td style="text-align: left;">Contrôles d'étanchéité spécifiques</td>
</tr>
<tr>
<td style="text-align: left;">Conception modulaire</td>
<td style="text-align: left;">Facilitation de la maintenance et des réparations</td>
<td style="text-align: left;">Architecture standardisée mais complexifiant l'assemblage</td>
</tr>
<tr>
<td style="text-align: left;">Conformité régionale</td>
<td style="text-align: left;">Adaptations électriques et réglementaires par marché</td>
<td style="text-align: left;">Versions multiples d'un même modèle</td>
</tr>
<tr>
<td style="text-align: left;">Coûts logistiques</td>
<td style="text-align: left;">Produits volumineux et relativement lourds</td>
<td style="text-align: left;">Impact significatif du transport sur la rentabilité</td>
</tr>
</tbody>
<caption>Contraintes spécifiques à l'assemblage</caption></table>
<p><em>Note: Ces contraintes concernent spécifiquement l'étape d'assemblage final et non la fabrication des composants individuels qui ont leurs propres contraintes traitées dans les fiches spécifiques.</em></p>
<hr/></details>
<details><summary>Matrice des risques liés à l'assemblage</summary><h2>Matrice des risques liés à l'assemblage</h2>
<table role="table" summary="Matrice des risques liés à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Impact/Probabilité</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Moyen</strong></th>
<th scope="col" style="text-align: left;"><strong>Fort</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Fort</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">R2 (Fiabilité mécanique)</td>
<td style="text-align: left;">R1 (Pièces mécaniques de précision)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Moyen</strong></td>
<td style="text-align: left;">R5 (Homologation)</td>
<td style="text-align: left;">R3 (Marché des consommables)</td>
<td style="text-align: left;">R4 (Composants électromécaniques)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Faible</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
</tr>
</tbody>
<caption>Matrice des risques liés à l'assemblage</caption></table>
<p><strong>Détail des risques principaux:</strong></p>
<ul>
<li><strong>R1</strong>: Dépendance à des pièces mécaniques de précision avec peu de fournisseurs alternatifs, particulièrement pour les entraînements et les têtes d'impression</li>
<li><strong>R2</strong>: Exigences élevées de fiabilité mécanique imposant des tests extensifs et des taux de rejet significatifs</li>
<li><strong>R3</strong>: Modèle économique fortement dépendant des consommables (encre, toner) influençant les choix technologiques et d'assemblage</li>
<li><strong>R4</strong>: Vulnérabilité aux pénuries de composants électromécaniques spécialisés (moteurs pas-à-pas, encodeurs)</li>
<li><strong>R5</strong>: Processus d'homologation et de certification variant selon les marchés (émissions chimiques, consommation électrique)</li>
</ul>
<h3>Indice de Herfindahl-Hirschmann</h3>
<table role="table" summary="Matrice des risques liés à l'assemblage">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>IHH</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Modéré</strong></th>
<th scope="col" style="text-align: left;"><strong>Élevé</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Acteurs</strong></td>
<td style="text-align: left;"><strong>10</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
</tr>
<tr>
<td style="text-align: left;"><strong>Pays</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"><strong>24</strong></td>
<td style="text-align: left;"></td>
</tr>
</tbody>
<caption>Matrice des risques liés à l'assemblage</caption></table>
<h4>IHH par entreprise (acteurs)</h4>
<p>LIHH calculé pour les assembleurs dimprimantes est de <strong>10</strong>, ce qui reflète une <strong>concentration industrielle faible</strong>. Aucun acteur nexcède 15 % de part de marché et la distribution reste bien ventilée entre plusieurs groupes asiatiques et japonais (Foxconn, Canon, Epson, Cal-Comp…). Cela suggère un <strong>marché industriel relativement compétitif</strong>, avec une résilience naturelle en cas de défaillance dun acteur majeur.</p>
<h4>IHH par pays</h4>
<p>LIHH par pays sélève à <strong>24</strong>, ce qui correspond à une <strong>concentration modérée</strong> selon les standards du DoJ. Bien que la <strong>Chine (37%)</strong> et la <strong>Thaïlande (25%)</strong> dominent, lexistence de capacités importantes aux Philippines, au Japon et en Malaisie limite partiellement les risques de dépendance extrême à une seule zone géographique.</p>
<h4>En résumé</h4>
<ul>
<li>La chaîne dassemblage dimprimantes est <strong>faiblement concentrée au niveau des acteurs</strong> (IHH 10), ce qui la rend robuste à moyen terme</li>
<li>La <strong>répartition géographique est modérément concentrée</strong> (IHH 24), mais montre des signes positifs de diversification</li>
<li>Cette structure rend la chaîne <strong>moins vulnérable quen impression 3D ou électronique mobile</strong></li>
<li>Le scénario géopolitique reste néanmoins pertinent en raison du poids cumulé de la Chine et de la Thaïlande</li>
</ul>
<hr/>
<p>Souhaites-tu un bloc équivalent pour une autre fiche encore non traitée ?</p>
<hr/></details>
<details><summary>Scénarios critiques projetés</summary><h2>Scénarios critiques projetés</h2>
<h3>Scénario 1 : Pénurie ciblée de composants clés</h3>
<ul>
<li>Type : Technique / logistique</li>
<li>Impact : Retards de production dus à la non-disponibilité de composants spécifiques (ex. GPU, lentilles, modules RF)</li>
<li>Chaînes affectées : Lignes dépendantes de fournisseurs uniques ou zones géographiques spécifiques</li>
<li>Répercussions : Hausse des prix, délais étendus, réallocation vers d'autres modèles</li>
</ul>
<h3>Scénario 2 : Restrictions géopolitiques sur la chaîne dassemblage</h3>
<ul>
<li>Type : Géopolitique</li>
<li>Impact : Embargos ou sanctions affectant les sites dassemblage (ex. Chine, Taiwan)</li>
<li>Chaînes affectées : Réduction immédiate de capacité, besoins en relocalisation</li>
<li>Répercussions : Baisse temporaire de production, réorganisation logistique, dépendance à des stocks</li>
</ul>
<hr/></details>
<details><summary>Points de vigilance sur la cohérence des données</summary><h2>Points de vigilance sur la cohérence des données</h2>
<ul>
<li>Vérifier la répartition par technologies (jet dencre, laser) dans les volumes, non toujours précisée</li>
<li>Origine des entreprises (Taiwan/Japon) et lieux dassemblage parfois confondus</li>
<li>Données marché 2024-2025 partiellement estimées sur tendances antérieures (2022-2023)</li>
<li>Absence de visibilité sur production 3D ou industrielle (très minoritaire mais non nulle)</li>
</ul>
<hr/></details>
<details><summary>Sources utilisées</summary><h2>Sources utilisées</h2>
<ol>
<li><a href="https://www.idc.com/getdoc.jsp?containerId=prUS51171023">IDC Worldwide Hardcopy Peripherals Tracker</a></li>
<li><a href="https://www.made-in-china.com/manufacturers/inkjet-printer.html">Made-in-China Fabricants dimprimantes</a></li>
<li><a href="https://ejournal.aibpmjournals.com/index.php/JICP/article/download/2233/1927">AIBPM Journal Supply Chain Printer Industry</a></li>
<li><a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7309121/">NCBI Impression et environnement</a></li>
</ol></details>
</section>

View File

@ -0,0 +1,359 @@
<section role="region" aria-labelledby="fiche-d-assemblage-matériels-de-photolithographie-duv">
<h1 id="fiche-d-assemblage-matériels-de-photolithographie-duv">Fiche dassemblage : Matériels de photolithographie DUV</h1>
<details><summary>Description générale</summary><h2>Description générale</h2>
<p>Les scanners <strong>DUV</strong> (Deep Ultraviolet  193 nm ArF immersion / 193 nm ArF sec / 248 nm KrF) couvrent les nœuds <strong>28 nm à 7 nm</strong> (couches critiques) et les niveaux moins exigeants.
Un ArF immersion de dernière génération (<strong>TWINSCANNXT:2100i</strong>) compte environ <strong>55000pièces</strong>, pèse 115t et coûte <strong>90140M€</strong>.
Les KrF modernes (<strong>NSRS635E</strong>, Nikon) se vendent autour de <strong>45M€</strong>.</p>
<p>Le flux dassemblage seffectue en 4 phases :</p>
<ol>
<li><strong>Préintégration modules</strong> (laser, optique, châssis) aux PaysBas ou au Japon</li>
<li><strong>Intégration finale en salle blanche</strong> (ASML Veldhoven, Nikon Kumagaya/Hiroshima, Canon Utsunomiya)</li>
<li><strong>Démontage logistique</strong> (≈1518conteneurs)</li>
<li><strong>assemblage &amp; qualification</strong> chez le fondeur (36 mois)</li>
</ol>
<table role="table" summary="Description générale">
<thead>
<tr>
<th scope="col" style="text-align: left;">Plateforme</th>
<th scope="col" style="text-align: center;">λ (nm)</th>
<th scope="col" style="text-align: center;">NA max</th>
<th scope="col" style="text-align: center;">Débit wafers/h</th>
<th scope="col" style="text-align: left;">Commercialisation</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>TWINSCANNXT (ASML)</strong></td>
<td style="text-align: center;">193i</td>
<td style="text-align: center;">1,35</td>
<td style="text-align: center;">275</td>
<td style="text-align: left;">2010</td>
</tr>
<tr>
<td style="text-align: left;"><strong>NSRS635E (Nikon)</strong></td>
<td style="text-align: center;">193i</td>
<td style="text-align: center;">1,35</td>
<td style="text-align: center;">250</td>
<td style="text-align: left;">2018</td>
</tr>
<tr>
<td style="text-align: left;"><strong>FPA3030iR (Canon)</strong></td>
<td style="text-align: center;">193i</td>
<td style="text-align: center;">1,35</td>
<td style="text-align: center;">240</td>
<td style="text-align: left;">2019</td>
</tr>
</tbody>
<caption>Description générale</caption></table>
<hr/></details>
<details><summary>Composants assemblés</summary><h2>Composants assemblés</h2>
<table role="table" summary="Composants assemblés">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Sous-système</strong></th>
<th scope="col" style="text-align: left;"><strong>Fonction</strong></th>
<th scope="col" style="text-align: left;"><strong>Fournisseur principal</strong></th>
<th scope="col" style="text-align: left;"><strong>Part dans le coût</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Source laser excimère (ArF / KrF)</td>
<td style="text-align: left;">Génère impulsions 193/248nm</td>
<td style="text-align: left;">Cymer (ASML), Gigaphoton</td>
<td style="text-align: left;">1822%</td>
</tr>
<tr>
<td style="text-align: left;">Optique projection &amp; illumination</td>
<td style="text-align: left;">Lentilles CaF₂ / fusedsilica</td>
<td style="text-align: left;">ZeissSMT, NikonHikari</td>
<td style="text-align: left;">2025%</td>
</tr>
<tr>
<td style="text-align: left;">Système immersion</td>
<td style="text-align: left;">Injecte eau ultrapure à 6L/s</td>
<td style="text-align: left;">ASMLHydra, NikonSIS</td>
<td style="text-align: left;">810%</td>
</tr>
<tr>
<td style="text-align: left;">Plateau wafer &amp; mécastatif</td>
<td style="text-align: left;">Positionne wafer ±2nm</td>
<td style="text-align: left;">ASMLMotion, NikonPrecision</td>
<td style="text-align: left;">1214%</td>
</tr>
<tr>
<td style="text-align: left;">Métrologie &amp; alignement</td>
<td style="text-align: left;">Mesure overlay &lt;2nm</td>
<td style="text-align: left;">ASMLHorus, NikonInChip</td>
<td style="text-align: left;">68%</td>
</tr>
<tr>
<td style="text-align: left;">Vide &amp; environnement</td>
<td style="text-align: left;">10⁻³mbar, filtration H₂O</td>
<td style="text-align: left;">Edwards, Pfeiffer</td>
<td style="text-align: left;">46%</td>
</tr>
<tr>
<td style="text-align: left;">Contrôle / logiciel</td>
<td style="text-align: left;">Pilotage tempsréel</td>
<td style="text-align: left;">ASMLTwinscanSW, NikonCTL</td>
<td style="text-align: left;">56%</td>
</tr>
</tbody>
<caption>Composants assemblés</caption></table>
<p><em>Coûts indicatifs pour NXT:2100i (2024).</em></p>
<hr/></details>
<details><summary>Principaux assembleurs (livraisons 2024)</summary><h2>Principaux assembleurs (livraisons 2024)</h2>
<table role="table" summary="Principaux assembleurs (livraisons 2024)">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Pays d'implantation</strong></th>
<th scope="col" style="text-align: left;"><strong>Entreprise</strong></th>
<th scope="col" style="text-align: left;"><strong>Pays d'origine</strong></th>
<th scope="col" style="text-align: left;"><strong>Part de marché</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">PaysBas</td>
<td style="text-align: left;">ASML</td>
<td style="text-align: left;">PaysBas</td>
<td style="text-align: left;">84 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Pays-Bas</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Pays-Bas</strong></td>
<td style="text-align: left;"><strong>84 %</strong></td>
</tr>
<tr>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">Nikon</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">12 %</td>
</tr>
<tr>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">Canon</td>
<td style="text-align: left;">Japon</td>
<td style="text-align: left;">4 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Japon</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Japon</strong></td>
<td style="text-align: left;"><strong>16 %</strong></td>
</tr>
</tbody>
<caption>Principaux assembleurs (livraisons 2024)</caption></table>
<p><em>Total 2024: ~240DUV scanners (toutes longueurs donde) livrés, dont 90% destinés à la Chine.</em></p>
<hr/></details>
<details><summary>Contraintes spécifiques</summary><h2>Contraintes spécifiques</h2>
<table role="table" summary="Contraintes spécifiques">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Contrainte</strong></th>
<th scope="col" style="text-align: left;"><strong>Description</strong></th>
<th scope="col" style="text-align: left;"><strong>Impact</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Qualité eau immersion</td>
<td style="text-align: left;">TOC&lt;1ppb, particules&lt;20nm</td>
<td style="text-align: left;">Risque bulles &amp; défauts</td>
</tr>
<tr>
<td style="text-align: left;">Lentilles CaF₂</td>
<td style="text-align: left;">Birefringence, hygroscopie</td>
<td style="text-align: left;">Variation de focus</td>
</tr>
<tr>
<td style="text-align: left;">Overlay multipatterning</td>
<td style="text-align: left;">2nm à 120pauses</td>
<td style="text-align: left;">Dépend stabilité stage</td>
</tr>
<tr>
<td style="text-align: left;">Exportcontrol</td>
<td style="text-align: left;">Aucune restriction stricte sur DUV</td>
<td style="text-align: left;">Chine peut acheter ArF</td>
</tr>
<tr>
<td style="text-align: left;">Vieillissement laser</td>
<td style="text-align: left;">Tubes ArF MTTF ≈5Gshots</td>
<td style="text-align: left;">OPEX source important</td>
</tr>
</tbody>
<caption>Contraintes spécifiques</caption></table>
<hr/></details>
<details><summary>Logistique et transport</summary><h2>Logistique et transport</h2>
<ul>
<li><strong>1518caisses</strong> (air + mer) ; modules ≤12t</li>
<li>Transport aérien Boeing7478F / 777F, conteneurs maritimes 40HC</li>
<li>Délai porteàporte: <strong>45jours</strong> (Europe →ÉtatsUnis ou Japon →Corée)</li>
<li>Assurance cargo typique <strong>100M$</strong> par scanner</li>
</ul>
<hr/></details>
<details><summary>Durabilité et cycle de vie</summary><h2>Durabilité et cycle de vie</h2>
<table role="table" summary="Durabilité et cycle de vie">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Volet</strong></th>
<th scope="col" style="text-align: left;"><strong>Détail</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Maintenance</td>
<td style="text-align: left;">Contrats 10ans, remplacement tube laser tous 6mois</td>
</tr>
<tr>
<td style="text-align: left;">Consommation</td>
<td style="text-align: left;">350kW (immersion) / 120kW (KrF)</td>
</tr>
<tr>
<td style="text-align: left;">Repolissage lentilles</td>
<td style="text-align: left;">Tous les 50kpl</td>
</tr>
<tr>
<td style="text-align: left;">Recyclabilité</td>
<td style="text-align: left;">75% masse métallique, CaF₂ recyclage dédié</td>
</tr>
</tbody>
<caption>Durabilité et cycle de vie</caption></table>
<hr/></details>
<details><summary>Matrice des risques</summary><h2>Matrice des risques</h2>
<table role="table" summary="Matrice des risques">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Impact / Probabilité</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Moyen</strong></th>
<th scope="col" style="text-align: left;"><strong>Fort</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Fort</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">R1 (Monopole laser ArF)</td>
<td style="text-align: left;">R2 (Optiques CaF₂)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Moyen</strong></td>
<td style="text-align: left;">R4 (Logistique transPacifique)</td>
<td style="text-align: left;">R3 (Eau immersion)</td>
<td style="text-align: left;">R5 (Concentration marché)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Faible</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
</tr>
</tbody>
<caption>Matrice des risques</caption></table>
<p><strong>Descriptions</strong>
- <strong>R1</strong> : Cymer +Gigaphoton = duopole sur lasers excimère haute puissance.
- <strong>R2</strong> : Goulot Zeiss / Nikon Hikari pour lentilles CaF₂ grand diamètre.
- <strong>R3</strong> : Qualité eau immersion impacte rendement et overlay.
- <strong>R4</strong> : Retards fret aérien / maritime ; 18caisses horsgabarit.
- <strong>R5</strong> : 84% des livraisons assurées par un seul acteur (ASML).</p>
<hr/></details>
<details><summary>Indice de Herfindahl-Hirschmann (HHI)</summary><h2>Indice de Herfindahl-Hirschmann (HHI)</h2>
<table role="table" summary="Indice de Herfindahl-Hirschmann (HHI)">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>IHH</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Modéré</strong></th>
<th scope="col" style="text-align: left;"><strong>Élevé</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Acteurs</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"><strong>72</strong></td>
</tr>
<tr>
<td style="text-align: left;"><strong>Pays</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"><strong>73</strong></td>
<td style="text-align: left;"></td>
</tr>
</tbody>
<caption>Indice de Herfindahl-Hirschmann (HHI)</caption></table>
<p><em>Acteurs</em>: ASML84%, Nikon12%, Canon4%
<em>Pays</em>: PaysBas + Japon dominants.</p>
<hr/></details>
<details><summary>En résumé</summary><h2>En résumé</h2>
<ul>
<li><strong>Trois assembleurs</strong> (ASML, Nikon, Canon) mais <strong>ASML domine</strong> le segment ArF immersion.</li>
<li>Le <strong>laser excimère</strong> (Cymer, Gigaphoton) constitue la plus forte dépendance.</li>
<li>Les scanners DUV restent <strong>vendables à la Chine</strong>, ce qui oriente une grande partie de la production.</li>
<li>Principaux risques: capacité optiques CaF₂, disponibilité lasers, logistique transPacifique.</li>
</ul>
<hr/></details>
<details><summary>Autres informations</summary><h2>Autres informations</h2>
<table role="table" summary="Autres informations">
<thead>
<tr>
<th scope="col" style="text-align: left;">Étape</th>
<th scope="col" style="text-align: left;">Localisation principale</th>
<th scope="col" style="text-align: left;">Commentaire</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Fabrication stages wafer/reticle</td>
<td style="text-align: left;"><strong>Wilton (CT, USA)</strong></td>
<td style="text-align: left;">Modules DUV/EUV expédiés vers Veldhoven</td>
</tr>
<tr>
<td style="text-align: left;">Production laser excimère</td>
<td style="text-align: left;"><strong>San Diego (Cymer, USA)</strong> &amp; <strong>Oyama (Gigaphoton, JP)</strong></td>
<td style="text-align: left;">Sources ArF /KrF</td>
</tr>
<tr>
<td style="text-align: left;">Optiques transmissives</td>
<td style="text-align: left;"><strong>Oberkochen (ZeissSMT, DE)</strong> / <strong>Kumagaya (NikonHikari, JP)</strong></td>
<td style="text-align: left;">Lentilles CaF₂ haute pureté</td>
</tr>
<tr>
<td style="text-align: left;">Intégration finale scanners ASML</td>
<td style="text-align: left;"><strong>Veldhoven (NL)</strong></td>
<td style="text-align: left;">Montage, alignement, qualification</td>
</tr>
<tr>
<td style="text-align: left;">Intégration finale scanners Nikon</td>
<td style="text-align: left;"><strong>Kumagaya &amp; Hiroshima (JP)</strong></td>
<td style="text-align: left;">Deux lignes DUV</td>
</tr>
<tr>
<td style="text-align: left;">Intégration finale scanners Canon</td>
<td style="text-align: left;"><strong>Utsunomiya (JP)</strong></td>
<td style="text-align: left;">Ligne iline / KrF / ArF</td>
</tr>
<tr>
<td style="text-align: left;">assemblage &amp; mise en service</td>
<td style="text-align: left;"><strong>Fabs client</strong> (TSMC, SMIC, UMC, Samsung)</td>
<td style="text-align: left;">Supervision constructeur</td>
</tr>
</tbody>
<caption>Autres informations</caption></table>
<hr/></details>
<details><summary>Sources techniques</summary><h2>Sources techniques</h2>
<ol>
<li>ASML Brochure «TWINSCANNXT:2100i» (2024)</li>
<li>Cymer «ArF immersion laser roadmap» (2025)</li>
<li>Gigaphoton «KrF / ArF Source Spec Sheet» (2024)</li>
<li>ZeissSMT «DUV Optics Whitepaper» (2023)</li>
<li>Nikon «NSR History &amp; Production Sites» (2024)</li>
<li>Canon Communiqué «Utsunomiya expansion lithography» (2024)</li>
<li>DigiTimes «ASML has installed 1400 DUV tools in China» (2025)</li>
<li>ASML Veldhoven Location &amp; manufacturing footprint (2024)</li>
</ol></details>
</section>

View File

@ -0,0 +1,305 @@
<section role="region" aria-labelledby="fiche-d-assemblage-matériels-de-photolithographie-euv">
<h1 id="fiche-d-assemblage-matériels-de-photolithographie-euv">Fiche dassemblage : Matériels de photolithographie EUV</h1>
<details><summary>Description générale</summary><h2>Description générale</h2>
<p>Les scanners <strong>EUV</strong> (Extreme Ultra Violet λ ≈ 13,5 nm) sont les équipements clés qui permettent de graver les nœuds &lt; 7 nm.
Une machine de dernière génération (NXE:3800E) compte plus de <strong>100 000 pièces</strong>, pèse 180 t et coûte 220260 M€ (EXE &gt; 350 M€ en High-NA) (<a href="https://www.digitimes.com/news/a20250417VL200/asml-euv-2025-earnings-demand.html">ASML to pass tariff costs to US customers, gain three High NA EUV customers</a>, <a href="https://www.barrons.com/articles/asml-stock-chip-equipment-cb5b6b40?utm_source=chatgpt.com">ASML Is the Chip-Equipment Leader. Its Stock Is Poised to Bounce Back.</a>).
Le flux dassemblage se déroule en 4 grandes phases :</p>
<ol>
<li><strong>Pré-intégration modules</strong> (source, optique, châssis) aux Pays-Bas et en Allemagne</li>
<li><strong>Intégration finale en salle blanche</strong> ASML Veldhoven</li>
<li><strong>Démontage logistique</strong> (≈ 35 conteneurs + 3 avions cargo)</li>
<li><strong>Ré-assemblage &amp; qualification</strong> chez le fondeur (69 mois)</li>
</ol>
<p>Les générations :
| Plateforme | NA | Débit wafers/h | Commercialisation |
| :-- | :--: | :--: | :-- |
| <strong>NXE</strong> | 0,33 | 220 | 2019 |
| <strong>EXE (High-NA)</strong> | 0,55 | 185<em> | 2024 </em>(phase R&amp;D)* |</p>
<hr/></details>
<details><summary>Composants assemblés</summary><h2>Composants assemblés</h2>
<table role="table" summary="Composants assemblés">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Sous-système</strong></th>
<th scope="col" style="text-align: left;"><strong>Fonction</strong></th>
<th scope="col" style="text-align: left;"><strong>Fournisseur principal</strong></th>
<th scope="col" style="text-align: left;"><strong>Part dans le coût</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Source EUV LPP</td>
<td style="text-align: left;">Génère plasma Sn → 13,5 nm</td>
<td style="text-align: left;">Cymer (ASML), Gigaphoton</td>
<td style="text-align: left;">2530 % ([Cymer</td>
</tr>
<tr>
<td style="text-align: left;">Optique collecteur &amp; miroirs</td>
<td style="text-align: left;">Réfléchit et façonne le faisceau</td>
<td style="text-align: left;">Zeiss SMT (DE)</td>
<td style="text-align: left;">2530 %</td>
</tr>
<tr>
<td style="text-align: left;">Projection &amp; masques (reticle)</td>
<td style="text-align: left;">Imprime le motif</td>
<td style="text-align: left;">Zeiss / ASML</td>
<td style="text-align: left;">1015 %</td>
</tr>
<tr>
<td style="text-align: left;">Plateau wafer &amp; méca-statif</td>
<td style="text-align: left;">Positionne wafer à ±1 nm</td>
<td style="text-align: left;">ASML Motion</td>
<td style="text-align: left;">1012 %</td>
</tr>
<tr>
<td style="text-align: left;">Métrologie &amp; alignement</td>
<td style="text-align: left;">Mesure overlay &lt; 1,5 nm</td>
<td style="text-align: left;">ASML Horus</td>
<td style="text-align: left;">810 %</td>
</tr>
<tr>
<td style="text-align: left;">Vide &amp; contamination</td>
<td style="text-align: left;">10⁻⁶ mbar + pièges Sn</td>
<td style="text-align: left;">Pfeiffer, Edwards</td>
<td style="text-align: left;">56 %</td>
</tr>
<tr>
<td style="text-align: left;">Contrôle/logiciel</td>
<td style="text-align: left;">Pilotage temps réel</td>
<td style="text-align: left;">ASML Twinscan SW</td>
<td style="text-align: left;">56 %</td>
</tr>
</tbody>
<caption>Composants assemblés</caption></table>
<p><em>Coûts indicatifs pour NXE :3800E (2024).</em></p>
<hr/></details>
<details><summary>Principaux assembleurs (livraisons par an, 2024)</summary><h2>Principaux assembleurs (livraisons par an, 2024)</h2>
<table role="table" summary="Principaux assembleurs (livraisons par an, 2024)">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Pays d'implantation</strong></th>
<th scope="col" style="text-align: left;"><strong>Entreprise</strong></th>
<th scope="col" style="text-align: left;"><strong>Pays d'origine</strong></th>
<th scope="col" style="text-align: left;"><strong>Part de marché</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Pays-Bas</td>
<td style="text-align: left;">ASML</td>
<td style="text-align: left;">Pays-Bas</td>
<td style="text-align: left;">100 %</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Pays-Bas</strong></td>
<td style="text-align: left;"><strong>Total</strong></td>
<td style="text-align: left;"><strong>Pays-Bas</strong></td>
<td style="text-align: left;"><strong>100 %</strong></td>
</tr>
</tbody>
<caption>Principaux assembleurs (livraisons par an, 2024)</caption></table>
<p><em>Total 2024 : 55 NXE livrées, 5 EXE High-NA déjà en R&amp;D chez Intel, TSMC, Samsung</em> (<a href="https://www.digitimes.com/news/a20250417VL200/asml-euv-2025-earnings-demand.html">ASML to pass tariff costs to US customers, gain three High NA EUV customers</a>, <a href="https://www.reuters.com/technology/belgiums-imec-reports-breakthroughs-with-new-asml-chip-printing-machine-2024-08-07/?utm_source=chatgpt.com">Belgium's imec reports breakthroughs with new ASML chip printing machine</a>).</p>
<hr/></details>
<details><summary>Contraintes spécifiques</summary><h2>Contraintes spécifiques</h2>
<table role="table" summary="Contraintes spécifiques">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Contrainte</strong></th>
<th scope="col" style="text-align: left;"><strong>Description</strong></th>
<th scope="col" style="text-align: left;"><strong>Impact</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Pureté du vide</td>
<td style="text-align: left;">Empreinte carbone/Sn &lt; ppm</td>
<td style="text-align: left;">Rendement optique, durée miroir</td>
</tr>
<tr>
<td style="text-align: left;">Optiques Mo/Si</td>
<td style="text-align: left;">6 paires miroir, planéité λ/100</td>
<td style="text-align: left;">Délais supply chain Zeiss</td>
</tr>
<tr>
<td style="text-align: left;">Vibrations &lt; 20 pm</td>
<td style="text-align: left;">Interféro-mécanique actif</td>
<td style="text-align: left;">Coût isolateurs &amp; fondations</td>
</tr>
<tr>
<td style="text-align: left;">Export-control</td>
<td style="text-align: left;">Règles NL/US (Wassenaar)</td>
<td style="text-align: left;">Risque blocage clients Chine</td>
</tr>
<tr>
<td style="text-align: left;">Pellicule EUV</td>
<td style="text-align: left;">Pellicle SiN &lt; 80 nm</td>
<td style="text-align: left;">Limite débit &amp; rendement</td>
</tr>
</tbody>
<caption>Contraintes spécifiques</caption></table>
<hr/></details>
<details><summary>Logistique et transport</summary><h2>Logistique et transport</h2>
<ul>
<li><strong>35 caisses</strong> (mer + air) ; modules &gt; 10 t chacun</li>
<li>Démontage en « kits » (&lt; 22 t) pour Boeing 747-8F</li>
<li>Délai porte-à-porte : <strong>100 jours</strong> (Europe → Taïwan)</li>
<li>Assurance cargo spécifique (valeur déclarée ≥ 250 M$)</li>
</ul>
<hr/></details>
<details><summary>Durabilité et cycle de vie</summary><h2>Durabilité et cycle de vie</h2>
<table role="table" summary="Durabilité et cycle de vie">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Volet</strong></th>
<th scope="col" style="text-align: left;"><strong>Détail</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Maintenance</td>
<td style="text-align: left;">Contrats sur 15 ans, upgrade optique tous 3 ans</td>
</tr>
<tr>
<td style="text-align: left;">Consommation</td>
<td style="text-align: left;">650 kW (NXE) / &gt; 1 MW (EXE)</td>
</tr>
<tr>
<td style="text-align: left;">Ré-usinage miroirs</td>
<td style="text-align: left;">Tous les 3040 kpl (000 wafers)</td>
</tr>
<tr>
<td style="text-align: left;">Recyclabilité</td>
<td style="text-align: left;">80 % masse métallique récupérable</td>
</tr>
</tbody>
<caption>Durabilité et cycle de vie</caption></table>
<hr/></details>
<details><summary>Matrice des risques</summary><h2>Matrice des risques</h2>
<table role="table" summary="Matrice des risques">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>Impact / Probabilité</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Moyen</strong></th>
<th scope="col" style="text-align: left;"><strong>Fort</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Fort</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">R1 (Monopole ASML)</td>
<td style="text-align: left;">R2 (Contrôle export)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Moyen</strong></td>
<td style="text-align: left;">R5 (Logistique)</td>
<td style="text-align: left;">R3 (Source LPP instable)</td>
<td style="text-align: left;">R4 (Pénurie optiques Zeiss)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Faible</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;">R6 (Pellicle)</td>
<td style="text-align: left;"></td>
</tr>
</tbody>
<caption>Matrice des risques</caption></table>
<p><strong>Descriptions</strong>
- <strong>R1</strong> : Concentration extrême un seul fournisseur EUV
- <strong>R2</strong> : Restrictions NL/US ↔ Chine, retards 6-12 mois
- <strong>R3</strong> : Disruption laser CO₂, tin debris → downtime
- <strong>R4</strong> : Goulot Zeiss pour miroirs 0,55 NA
- <strong>R5</strong> : Dégâts transport, douanes hors gabarit
- <strong>R6</strong> : Retard pellicle haute-NA réduit le yield</p>
<hr/></details>
<details><summary>Indice de Herfindahl-Hirschmann (HHI)</summary><h2>Indice de Herfindahl-Hirschmann (HHI)</h2>
<table role="table" summary="Indice de Herfindahl-Hirschmann (HHI)">
<thead>
<tr>
<th scope="col" style="text-align: left;"><strong>IHH</strong></th>
<th scope="col" style="text-align: left;"><strong>Faible</strong></th>
<th scope="col" style="text-align: left;"><strong>Modéré</strong></th>
<th scope="col" style="text-align: left;"><strong>Élevé</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>Acteurs</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"><strong>100</strong></td>
</tr>
<tr>
<td style="text-align: left;"><strong>Pays</strong></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"></td>
<td style="text-align: left;"><strong>100</strong></td>
</tr>
</tbody>
<caption>Indice de Herfindahl-Hirschmann (HHI)</caption></table>
<p><em>Acteurs : ASML ≈ 100 %</em> → monopole absolu.
*Pays : chaîne dominée par les Pays-Bas.</p>
<hr/></details>
<details><summary>En résumé</summary><h2>En résumé</h2>
<ul>
<li><strong>EUV = chaînon le plus concentré</strong> de toute la filière semi dépendance critique à ASML/Zeiss.</li>
<li>La <strong>montée en High-NA (EXE)</strong> réduit le nombre dexpositions, mais renforce la dépendance.</li>
<li><strong>Risque géopolitique majeur</strong> (export-control) et <strong>logistique complexe</strong> (35 conteneurs).</li>
<li>Les alternatives (NIL Canon, projets chinois) restent <strong>non qualifiées</strong> pour la production logic &amp; memory &lt; 3 nm.</li>
</ul>
<hr/></details>
<details><summary>Autres informations</summary><h2>Autres informations</h2>
<table role="table" summary="Autres informations">
<thead>
<tr>
<th scope="col" style="text-align: left;">Étape</th>
<th scope="col" style="text-align: left;">Localisation principale</th>
<th scope="col" style="text-align: left;">Commentaire</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Fabrication sous-ensembles mécatroniques (reticle stage, capots, capteurs)</td>
<td style="text-align: left;"><strong>Wilton (Connecticut, USA)</strong></td>
<td style="text-align: left;">Modules EUV/High-NA, expédiés en caisse vers Veldhoven ([7 things you didnt know about ASML Wilton history Stories</td>
</tr>
<tr>
<td style="text-align: left;">Source laser CO₂ &amp; optique collecteur Sn</td>
<td style="text-align: left;"><strong>San Diego (Cymer, USA)</strong> et partenaires Japon/DE</td>
<td style="text-align: left;">Modules livrés à Veldhoven</td>
</tr>
<tr>
<td style="text-align: left;">Miroirs Bragg &amp; optique projection</td>
<td style="text-align: left;"><strong>Oberkochen (Zeiss SMT, Allemagne)</strong></td>
<td style="text-align: left;">Transport ultra-propre vers NL</td>
</tr>
<tr>
<td style="text-align: left;">Clean-room dintégration complète (NXE &amp; EXE)</td>
<td style="text-align: left;"><strong>Veldhoven (NL)</strong></td>
<td style="text-align: left;">Seul endroit où lon « ferme la machine », laligne, la qualifie et où part le démontage logistique (<a href="https://www.reuters.com/technology/semiconductor-equipment-maker-asml-ships-second-high-na-euv-machine-2024-04-17/?utm_source=chatgpt.com">Semiconductor equipment maker ASML ships second 'High NA' EUV machine</a>)</td>
</tr>
<tr>
<td style="text-align: left;">Ré-assemblage et mise en service chez le client</td>
<td style="text-align: left;"><strong>Fabs client (Intel, TSMC, Samsung, SK Hynix…)</strong></td>
<td style="text-align: left;">Les modules sont remontés in-situ ; Intel a été le premier à assembler lui-même un EXE:5000 sous supervision ASML (<a href="https://www.reuters.com/technology/seeking-edge-over-rivals-intel-first-assemble-asmls-next-gen-chip-tool-2024-04-18/?utm_source=chatgpt.com">Seeking edge over rivals, Intel first to assemble ASML's next-gen ...</a>)</td>
</tr>
</tbody>
<caption>Autres informations</caption></table>
<blockquote>
<p>ASML ne possède <strong>pas</strong> dusine secondaire pour lassemblage final ; il expédie des « kits » depuis Veldhoven et supervise la reconstruction dans la salle blanche du client.</p>
</blockquote></details>
<details><summary>Sources techniques</summary><h2>Sources techniques</h2>
<ol>
<li>ASML Fiches produits EUV (NXE/EXE) (<a href="https://www.asml.com/products/euv-lithography-systems?utm_source=chatgpt.com">EUV lithography systems Products - ASML</a>, <a href="https://www.asml.com/en/news/stories/2024/5-things-high-na-euv?utm_source=chatgpt.com">5 things you should know about High NA EUV lithography - ASML</a>)</li>
<li>Digitimes, « ASML adds three High-NA EUV customers » (avr. 2025) (<a href="https://www.digitimes.com/news/a20250417VL200/asml-euv-2025-earnings-demand.html">ASML to pass tariff costs to US customers, gain three High NA EUV customers</a>)</li>
<li>Reuters, « IMEC breakthroughs with ASML High-NA tool » (2024) (<a href="https://www.reuters.com/technology/belgiums-imec-reports-breakthroughs-with-new-asml-chip-printing-machine-2024-08-07/?utm_source=chatgpt.com">Belgium's imec reports breakthroughs with new ASML chip printing machine</a>)</li>
<li>Barrons, « ASML stock &amp; EUV machine cost » (2025) (<a href="https://www.barrons.com/articles/asml-stock-chip-equipment-cb5b6b40?utm_source=chatgpt.com">ASML Is the Chip-Equipment Leader. Its Stock Is Poised to Bounce Back.</a>)</li>
<li>Cymer / ASML Light-source history (<a href="https://www.asml.com/company/about-asml/cymer?utm_source=chatgpt.com">Cymer | ASML - Supplying the semiconductor industry</a>)</li>
<li>Gigaphoton Avancées source EUV (2025) (<a href="https://www.gigaphoton.com/news/9333?utm_source=chatgpt.com">Gigaphoton to Showcase Technology Solutions at SPIE Advanced ...</a>)</li>
<li>Canon NIL FPA-1200NZ2C livraison (2024) (<a href="https://www.trendforce.com/news/2024/09/30/news-canon-delivers-nanoimprint-lithography-system-to-tie-reportedly-capable-of-producing-2nm-chips/?utm_source=chatgpt.com">[News] Canon Delivers Nanoimprint Lithography System to TIE ...</a>)</li>
<li>Nikon Semiconductor Systems overview (2024) (<a href="https://www.nikon.com/business/semi/?utm_source=chatgpt.com">Semiconductor Lithography Systems | Nikon Business</a>)</li>
<li>PowerElectronicsNews, « China €37 bn EUV initiative » (2025) (<a href="https://www.powerelectronicsnews.com/china-invests-e37-billion-to-develop-domestic-euv-lithography-systems/?utm_source=chatgpt.com">China Invests €37 Billion to Develop Domestic EUV Lithography ...</a>)</li>
</ol></details>
</section>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -30,14 +30,13 @@ Le fichier **requirements.txt** permet d'installer tout ce qui est nécessaire p
### Environnement
Le fichier **.env** n'est pas dans le dépôt car il contient la clé pour accéder au backend.
Le fichier **.env.local** qui contient GITEA_TOKEN n'est pas dans le dépôt car il contient la clé pour accéder au backend.
Pour l'environnement de pré-production, (https://fabnum-dev.peccini.fr)[https://fabnum-dev.peccini.fr] :
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"
@ -77,12 +76,75 @@ Pour automatiser le lancement, il est intégré dans systemd :
### fabnum.py
Le cœur du fonctionnement. C'est ce script qui permet de proposer l'interface de navigation, les analyses, les visualisations graphiques et l'accès aux fiches.
Le cœur de lapplication. Ce script gère :
Il se connecte au backend Gitea pour récupérer le fichier schema.txt contenant tous les nœuds et toutes les relations entre eux pour décrire la chaîne complète.
linterface utilisateur avec Streamlit,
Il s'y connecte aussi pour récupérer les fiches et les présenter.
le chargement des données depuis le backend Gitea (schéma, instructions, fiches),
lanalyse des chaînes de dépendances critiques (par Sankey interactif),
les visualisations statistiques (IHH, IVC, ISG),
la navigation hiérarchique dans les fiches,
et la personnalisation de produits finaux.
Il orchestre tous les composants de lapplication, notamment :
connexion.py pour lauthentification via Gitea,
utils/ pour les fonctions métiers (import graph, traitement, visualisation),
components/ pour laffichage modulaire (sidebar, header, footer, fiches),
et tickets_fiche.py pour la consultation et la création de tickets Gitea liés aux fiches.
Le fichier récupère automatiquement les données du dépôt Gitea configuré, et permet aux utilisateurs dinteragir avec les graphes, les métadonnées et les visualisations en toute autonomie
### tickets_fiche.py
Ce script est invoqué par fabnum.py pour assurer la coopération avec les internautes. Il permet de se connecter au backend Gitea pour récupérer les tickets associés à une fiche, les présenter ou en créée un nouveau.
Ce module assure la liaison entre les fiches documentaires et le système de tickets Gitea. Il permet :
de rechercher automatiquement les tickets ouverts liés à une fiche (via les labels définis dans fiches_labels.csv),
de les afficher classés par statut (En cours, Terminés, etc.),
de consulter les commentaires associés à chaque ticket,
de proposer un formulaire complet pour créer un nouveau ticket structuré à partir dun modèle Markdown,
de prévisualiser et publier ce ticket directement via lAPI Gitea.
Il gère également :
la détection de conflits ou erreurs lors des appels réseau,
lautomatisation du remplissage des champs (fiche concernée, type de contribution, etc.),
et la prise en compte des environnements (ENV) et des permissions via token.
Ce fichier est essentiel pour assurer la participation collaborative autour des fiches de la chaîne numérique.
### Organisation du code
fabnum_app/
├── app.py / fabnum.py # Point d'entrée principal
├── config.py # Chargement des variables denvironnement
├── utils/
│ ├── gitea.py # Connexion API Gitea
│ ├── graph_utils.py # Chemins, criticité, extraction de données
│ └── visualisation.py # Graphiques Altair, Plotly
├── components/
│ ├── sidebar.py # Menu latéral
│ ├── header.py # En-tête HTML
│ ├── footer.py # Pied de page
│ └── fiches.py # Lecture et affichage des fiches
├── tickets_fiche.py # Gestion des tickets associés aux fiches
├── assets/
│ ├── styles.css # Feuille de style personnalisée
│ └── impact_co2.js # Script pour calcul dimpact environnemental
├── .env # Configuration versionnée (sans secrets)
├── .env.local # Configuration locale (non versionnée)
├── .gitignore # Exclusion des fichiers sensibles
└── requirements.txt # Dépendances Python

28
assets/config.yaml Normal file
View File

@ -0,0 +1,28 @@
version: 1.1
date: 2025-05-06
seuils:
IVC: # Indice de vulnérabilité concurrentielle
vert: { max: 5 }
orange: { min: 5, max: 15 }
rouge: { min: 15 }
IHH: # Index Herfindahl-Hirschman
vert: { max: 15 }
orange: { min: 15, max: 25 }
rouge: { min: 25 }
ICS: # Indice de criticité de substitution
vert: { max: 0.30 }
orange: { min: 0.30, max: 0.60 }
rouge: { min: 0.60 }
ISG: # Indice de stabilité géopolitique (nouveau)
vert: { max: 40 }
orange: { min: 40, max: 70 }
rouge: { min: 70 }
ITH: # Indice « en préparation »
vert: { max: null } # à définir
orange: { min: null, max: null }
rouge: { min: null }

View File

@ -1,218 +0,0 @@
/* styles.css */
body,
html {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
}
.stAppHeader {
visibility: hidden;
}
/* Conteneur principal */
.block-container {
max-width: 1024px !important;
padding-left: 2rem;
padding-right: 2rem;
padding: 0rem 1rem 10rem;
}
.stVerticalBlock {
gap: 0.5rem !important;
}
/* Lien normal (non visité) */
a {
color: #1b5e20; /* vert foncé */
text-decoration: none;
}
/* Lien visité */
a:visited {
color: #388e3c; /* vert moyen */
}
/* Lien au survol */
a:hover {
color: #145a1a; /* vert encore plus foncé */
text-decoration: underline;
}
/* Lien actif */
a:active {
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;
}
/* Style pour impression */
@media print {
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;
}
.titre-header {
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;
}
div[role="radiogroup"] > label[data-selected="true"] {
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;
}
/* Pied de page */
/* Footer général */
.wide-footer {
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: #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;
}

395
assets/styles/base.css Normal file
View File

@ -0,0 +1,395 @@
/* --- Base.css --- */
.stAppHeader {
visibility: hidden;
}
/* Conteneur principal */
.block-container {
max-width: 1024px !important;
padding-left: 2rem;
padding-right: 2rem;
padding: 0rem 1rem 10rem;
}
.stVerticalBlock {
gap: 0.5rem !important;
}
/* Typographie & structure */
body,
html {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
}
.block-container {
max-width: 1024px !important;
padding: 0 1rem 10rem;
}
.stVerticalBlock {
gap: 0.5rem !important;
}
/* Boutons & sliders */
.stButton > button,
.stDownloadButton > button,
.stSlider > div > div {
background-color: darkgreen !important;
color: white !important;
border: 1px solid grey;
}
/* Sidebar (style fixe) */
section[data-testid="stSidebar"] {
background-color: #ccc !important;
color: #111 !important;
}
section[data-testid="stSidebar"] .stButton > button {
background-color: darkgreen !important;
color: white !important;
font-weight: bold;
border: 1px solid #ccc !important;
}
/* Footer commun */
.wide-footer {
width: 100vw;
margin-left: calc(-50vw + 50%);
margin-top: 3rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-top: 1px solid #ddd;
text-align: center;
padding-top: 1rem;
}
.info-footer {
font-size: 1rem !important;
font-weight: 800;
}
/* En-tête large */
.wide-header {
width: 100vw;
margin-left: calc(-50vw + 50%);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid #ddd;
text-align: center;
padding-top: 1rem;
margin-top: -1.25em;
}
.titre-header {
font-size: 2rem !important;
font-weight: bolder !important;
}
/* Accessibilité RGAA pour les onglets */
div[role="radiogroup"] > label {
padding: 0.5em 1em;
border-radius: 0.4em;
margin-right: 0.5em;
cursor: pointer;
border: 1px solid #fff;
}
div[role="radiogroup"] > label[data-selected="true"] {
font-weight: bold;
border: 2px solid #145a1a;
}
/* Styles pour les éléments décoratifs */
section[data-testid="stSidebar"] .decorative-heading {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
text-align: center;
color: #145a1a;
}
section[data-testid="stSidebar"] div[role="radiogroup"] {
justify-content: center !important;
display: flex !important;
gap: 1rem; /* Optionnel : espace entre les boutons */
}
/* Corrige la couleur du texte des boutons de la sidebar - identique en light ou en dark */
section[data-testid="stSidebar"] .stButton > button {
background-color: darkgreen !important;
color: white !important;
font-weight: bold !important;
border: 1px solid #ccc !important;
width: 100%;
}
/* 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;
}
/* 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;
}
button[data-testid="stBaseButton-primary"],
button[data-testid="stBaseButton-secondary"] {
color: white !important;
}
button[data-testid="stBaseButton-primary"] p,
,
button[data-testid="stBaseButton-secondary"] p {
color: white !important;
}
.bouton-fictif {
display: inline-flex;
-moz-box-align: center;
align-items: center;
-moz-box-pack: center;
justify-content: center;
padding: 0.25rem 0.75rem;
border-radius: 0.5rem;
min-height: 2.5rem;
margin-bottom: 20px;
line-height: 1;
text-transform: none;
font-size: x-large;
font-family: inherit;
user-select: none;
border: 1px solid rgba(49, 51, 63, 0.2);
background-color: darkgrey !important;
color: darkgreen !important;
font-weight: bold !important;
width: 100%;
letter-spacing: 0.2em;
}
/* Utilisation des variables CSS définies dans les fichiers de thème */
body,
.stApp,
.block-container {
background-color: var(--bg-color) !important;
color: var(--text-color) !important;
}
/* En-tête large */
.wide-header {
background-color: var(--header-bg);
}
.titre-header {
color: var(--header-title);
}
/* Footer commun */
.wide-footer {
background-color: var(--footer-bg);
}
.info-footer {
color: var(--footer-text);
}
/* Accessibilité RGAA pour les onglets */
section:not([data-testid="stSidebar"]) div[role="radiogroup"] > label p {
background-color: var(--radio-bg) !important;
color: var(--radio-text) !important;
}
section:not([data-testid="stSidebar"])
div[role="radiogroup"]
> label[data-selected="true"] {
background-color: var(--radio-selected-bg) !important;
color: var(--radio-selected-text) !important;
}
/* Graphiques */
.stPlotlyChart text {
fill: var(--plot-text) !important;
}
/* Paragraphes */
section:not([data-testid="stSidebar"])
div:not[data-testid="stElementContainer"]
p:not(#Authentification):not(#Theme) {
color: var(--paragraph-color) !important;
}
/* Champs de formulaire */
div[data-baseweb="select"],
section:not([data-testid="stSidebar"]) div[data-baseweb="base-input"],
section[data-testid="stFileUploaderDropzone"] {
border: 1px solid var(--input-border) !important;
border-radius: 5px;
padding: 4px;
}
/* Détails */
details {
border-color: var(--details-border) !important;
}
/* Tables */
table {
border: 1px solid var(--table-border) !important;
border-collapse: collapse;
width: 100%;
margin-bottom: 1.5em;
}
th,
td {
border: 1px solid var(--table-border) !important;
padding: 8px;
text-align: left;
}
/* Accessibilité RGAA */
caption {
caption-side: top;
font-weight: bold;
padding: 0.5em;
text-align: left;
caption-side: bottom;
text-align: center;
}
table[role="table"] th[scope="col"] {
background-color: var(--background-color);
}
/* Fin de Tables */
section:not([data-testid="stSidebar"]) div[data-testid="stSelectbox"] p,
section:not([data-testid="stSidebar"]) div[data-testid="stMultiSelect"] p,
section:not([data-testid="stSidebar"]) div[data-testid="stRadio"] p,
section:not([data-testid="stSidebar"]) div[data-testid="stCheckbox"] p,
section:not([data-testid="stSidebar"]) div[data-testid="stTextInput"] p,
section:not([data-testid="stSidebar"]) div[data-testid="stTextArea"] p,
section:not([data-testid="stSidebar"]) div[data-testid="stAlertContentInfo"] p {
color: var(--text-color) !important;
}
section:not([data-testid="stSidebar"]) hr {
background-color: var(--hr-color) !important;
}
.stPlotlyChart text {
fill: black !important;
text-shadow: none !important;
font-weight: bold !important;
font-size: 14px !important;
font-family: Verdana, sans-serif !important;
}
.conteneur_commentaire {
background: var(--background-color);
padding: 0.5rem;
border-radius: 8px;
margin-bottom: 0.5rem;
border: 1px solid #ccc;
border-radius: 8px;
padding: 1em;
margin-bottom: 1em;
}
.commentaire_auteur {
color: var(--text-color) !important;
margin: 0;
}
.commentaire_contenu {
color: var(--text-color) !important;
margin: 0.5rem 0 0;
}
.conteneur_ticket {
background: var(--background-color);
padding: 0.5rem;
border-radius: 8px;
margin-bottom: 0.5rem;
border: 1px solid #ccc;
border-radius: 8px;
padding: 1em;
margin-bottom: 1em;
}
.ticket_auteur {
color: var(--text-color) !important;
margin: 0;
}
.ticket_contenu {
color: var(--text-color) !important;
margin: 0.5rem 0 0;
}
button[data-testid="stBaseButton-headerNoPadding"] svg {
fill: var(--text-color) !important;
}
details {
border: 1px solid #ccc;
border-radius: 6px;
padding: 0.5em;
margin-bottom: 0.5em;
background-color: var(--background-color);
}
.math-block {
display: block;
text-align: center;
margin: 1em 0;
border: 1px solid var(--math-block-border);
border-radius: 10px;
background: var(--math-block-bg);
font-size: x-large;
color: var(--text-color);
}
.math-block math {
display: inline-block;
}

View File

@ -0,0 +1,30 @@
:root {
--bg-color: #222;
--text-color: #eee;
--header-bg: #060606;
--header-title: #bbb;
--footer-bg: #060606;
--footer-text: #ddd;
--radio-bg: #222;
--radio-text: #ddd;
--radio-selected-bg: #e5b2ef;
--radio-selected-text: black;
--plot-text: white;
--paragraph-color: white;
--input-border: #222;
--details-border: lightgray;
--table-border: #ccc;
--hr-color: #eee;
--math-block-bg: #222;
--math-block-border: #ccc;
}

View File

@ -0,0 +1,30 @@
:root {
--bg-color: #eee;
--text-color: #222;
--header-bg: #f9f9f9;
--header-title: #555;
--footer-bg: #f9f9f9;
--footer-text: #333;
--radio-bg: #eee;
--radio-text: #333;
--radio-selected-bg: #1b5e20;
--radio-selected-text: white;
--plot-text: black;
--paragraph-color: black;
--input-border: #aaa;
--details-border: darkgray;
--table-border: #333;
--hr-color: #222;
--math-block-bg: #ddd;
--math-block-border: #ccc;
}

204
components/fiches.py Normal file
View File

@ -0,0 +1,204 @@
import streamlit as st
import requests
import re
from config import GITEA_TOKEN
from utils.gitea import charger_arborescence_fiches
from utils.tickets_fiche import gerer_tickets_fiche
import markdown
from bs4 import BeautifulSoup
from latex2mathml.converter import convert as latex_to_mathml
from utils.fiche_utils import load_seuils, render_fiche_markdown
from utils.fiche_dynamic import build_dynamic_sections, build_ivc_sections, build_ihh_sections
import os
from utils.gitea import recuperer_date_dernier_commit
from datetime import datetime, timezone
import yaml
from config import GITEA_URL, ORGANISATION, DEPOT_FICHES, ENV
def remplacer_latex_par_mathml(markdown_text):
def remplacer_bloc_display(match):
formule_latex = match.group(1).strip()
try:
mathml = latex_to_mathml(formule_latex, display='block')
return f'<div class="math-block">{mathml}</div>'
except Exception as e:
return f"<pre>Erreur LaTeX block: {e}</pre>"
def remplacer_bloc_inline(match):
formule_latex = match.group(1).strip()
try:
mathml = latex_to_mathml(formule_latex, display='inline')
return f'<span class="math-inline">{mathml}</span>'
except Exception as e:
return f"<code>Erreur LaTeX inline: {e}</code>"
# Important : d'abord les $$...$$, ensuite les $...$
markdown_text = re.sub(r"\$\$(.*?)\$\$", remplacer_bloc_display, markdown_text, flags=re.DOTALL)
markdown_text = re.sub(r"(?<!\$)\$(.+?)\$(?!\$)", remplacer_bloc_inline, markdown_text, flags=re.DOTALL)
return markdown_text
def markdown_to_html_rgaa(markdown_text, caption_text=None):
html = markdown.markdown(markdown_text, extensions=['tables'])
soup = BeautifulSoup(html, "html.parser")
for i, table in enumerate(soup.find_all("table"), start=1):
table["role"] = "table"
table["summary"] = caption_text
if caption_text:
caption = soup.new_tag("caption")
caption.string = caption_text
table.insert(len(table.contents), caption)
for th in table.find_all("th"):
th["scope"] = "col"
return str(soup)
def creer_fiche(md_source: str, dossier: str, nom_fichier: str, seuils: dict) -> str:
# Extraire bloc YAML d'en-tête
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source)
context = {}
if front_match:
context = yaml.safe_load(front_match.group(1))
#md_source = md_source[front_match.end():] # retirer le front-matter du corps
# Traitement conditionnel selon type
type_fiche = context.get("type_fiche")
if type_fiche == "indice":
if context.get("indice_court") == "ICS":
md_source = build_dynamic_sections(md_source)
elif context.get("indice_court") == "IVC":
md_source = build_ivc_sections(md_source)
elif context.get("indice_court") == "IHH":
md_source = build_ihh_sections(md_source)
# Rendu markdown principal
contenu_md = render_fiche_markdown(md_source, seuils)
#st.code(md_source)
#md_pairs = build_dynamic_sections(md_source)
#contenu_md = render_fiche_markdown(md_pairs, seuils)
# Sauvegarde .md
md_path = os.path.join("Fiches", dossier, nom_fichier)
os.makedirs(os.path.dirname(md_path), exist_ok=True)
with open(md_path, "w", encoding="utf-8") as f:
f.write(contenu_md)
# Traitement en sections
lignes = contenu_md.split('\n')
sections_n1 = []
section_n1_actuelle = {"titre": None, "intro": [], "sections_n2": {}}
dans_section_n1 = False
section_n2_actuelle = None
for ligne in lignes:
if re.match(r'^#[^#]', ligne):
if section_n1_actuelle["titre"] or section_n1_actuelle["intro"] or section_n1_actuelle["sections_n2"]:
sections_n1.append(section_n1_actuelle)
section_n1_actuelle = {
"titre": ligne.strip('# ').strip(),
"intro": [],
"sections_n2": {}
}
section_n2_actuelle = None
dans_section_n1 = True
elif re.match(r'^##[^#]', ligne):
section_n2_actuelle = ligne.strip('# ').strip()
section_n1_actuelle["sections_n2"][section_n2_actuelle] = [f"## {section_n2_actuelle}"]
elif section_n2_actuelle:
section_n1_actuelle["sections_n2"][section_n2_actuelle].append(ligne)
elif dans_section_n1:
section_n1_actuelle["intro"].append(ligne)
if section_n1_actuelle["titre"] or section_n1_actuelle["intro"] or section_n1_actuelle["sections_n2"]:
sections_n1.append(section_n1_actuelle)
# Génération HTML
bloc_titre = sections_n1[0]["titre"] if sections_n1 and sections_n1[0]["titre"] else "fiche"
titre_id = re.sub(r'\W+', '-', bloc_titre.lower()).strip('-')
html_output = [f'<section role="region" aria-labelledby="{titre_id}">',
f'<h1 id="{titre_id}">{sections_n1[0]["titre"]}</h1>']
for bloc in sections_n1:
if bloc["titre"] and bloc["titre"] != sections_n1[0]["titre"]:
html_output.append(f"<h2>{bloc['titre']}</h2>")
if bloc["intro"]:
intro_md = remplacer_latex_par_mathml("\n".join(bloc["intro"]))
html_intro = markdown_to_html_rgaa(intro_md, caption_text=None)
html_output.append(html_intro)
for sous_titre, contenu in bloc["sections_n2"].items():
contenu_md = remplacer_latex_par_mathml("\n".join(contenu))
contenu_html = markdown_to_html_rgaa(contenu_md, caption_text=sous_titre)
html_output.append(f"<details><summary>{sous_titre}</summary>{contenu_html}</details>")
html_output.append("</section>")
html_dir = os.path.join("HTML", dossier)
os.makedirs(html_dir, exist_ok=True)
html_path = os.path.join(html_dir, os.path.splitext(nom_fichier)[0] + ".html")
with open(html_path, "w", encoding="utf-8") as f:
f.write("\n".join(html_output))
return html_path
def afficher_fiches():
if "fiches_arbo" not in st.session_state:
st.session_state["fiches_arbo"] = charger_arborescence_fiches()
arbo = st.session_state.get("fiches_arbo", {})
if not arbo:
st.warning("Aucune fiche disponible pour le moment.")
return
dossiers = sorted(arbo.keys(), key=lambda x: x.lower())
dossier_choisi = st.selectbox("Choisissez un dossier", ["-- Sélectionner un dossier --"] + dossiers)
if dossier_choisi and dossier_choisi != "-- Sélectionner un dossier --":
fiches = arbo.get(dossier_choisi, [])
noms_fiches = [f['nom'] for f in fiches]
fiche_choisie = st.selectbox("Choisissez une fiche", ["-- Sélectionner une fiche --"] + noms_fiches)
if fiche_choisie and fiche_choisie != "-- Sélectionner une fiche --":
fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None)
if fiche_info:
try:
headers = {"Authorization": f"token {GITEA_TOKEN}"}
reponse_fiche = requests.get(fiche_info["download_url"], headers=headers)
reponse_fiche.raise_for_status()
md_source = reponse_fiche.text
if "seuils" not in st.session_state:
SEUILS = load_seuils("assets/config.yaml")
st.session_state["seuils"] = SEUILS
elif "seuils" in st.session_state:
SEUILS = st.session_state["seuils"]
html_path = os.path.join("HTML", dossier_choisi, os.path.splitext(fiche_choisie)[0] + ".html")
path_relative = f"Documents/{dossier_choisi}/{fiche_choisie}"
commits_url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/commits?path={path_relative}&sha={ENV}"
regenerate = True
if os.path.exists(html_path):
remote_last_modified = recuperer_date_dernier_commit(commits_url)
local_last_modified = datetime.fromtimestamp(os.path.getmtime(html_path), tz=timezone.utc)
regenerate = not remote_last_modified or remote_last_modified > local_last_modified
if regenerate:
st.info("DEBUG : Régénération de la fiche")
html_path = creer_fiche(md_source, dossier_choisi, fiche_choisie, SEUILS)
else:
st.info("DEBUG : Pas de régénération")
with open(html_path, "r", encoding="utf-8") as f:
st.markdown(f.read(), unsafe_allow_html=True)
gerer_tickets_fiche(fiche_choisie)
except Exception as e:
st.error(f"Erreur lors du chargement de la fiche : {e}")

21
components/footer.py Normal file
View File

@ -0,0 +1,21 @@
import streamlit as st
def afficher_pied_de_page():
st.markdown("""
<section role="region" aria-label="Contenu principal" id="main-content">
""", unsafe_allow_html=True)
st.markdown("""
<div role='contentinfo' aria-labelledby='footer-appli' class='wide-footer'>
<div class='info-footer'>
<p id='footer-appli' class='info-footer'>
Fabnum © 2025 <a href='mailto:stephan-pro@peccini.fr'>Contact</a> Licence <a href='https://creativecommons.org/licenses/by-nc-sa/4.0/' target='_blank'>CC BY-NC-SA</a>
</p>
<p class='footer-note'>
🌱 Calculs CO₂ via <a href='https://www.thegreenwebfoundation.org/' target='_blank'>The Green Web Foundation</a><br>
🚀 Propulsé par <a href='https://streamlit.io/' target='_blank'>Streamlit</a>
</p>
</div>
</div>
""", unsafe_allow_html=True)

22
components/header.py Normal file
View File

@ -0,0 +1,22 @@
import streamlit as st
from config import ENV
def afficher_entete():
header = """
<header role="banner" aria-labelledby="entete-header">
<div class='wide-header'>
<p id='entete-header' class='titre-header'>FabNum - Chaîne de fabrication du numérique</p>
"""
if ENV == "dev":
header += "<p>🔧 Vous êtes dans l'environnement de développement.</p>"
else:
header += "<p>Parcours de l'écosystème et identification des vulnérabilités.</p>"
header += """
</div>
</header>
"""
st.markdown(header, unsafe_allow_html=True)

135
components/sidebar.py Normal file
View File

@ -0,0 +1,135 @@
import streamlit as st
from utils.connexion import connexion, bouton_deconnexion
import streamlit.components.v1 as components
def afficher_menu():
with st.sidebar:
st.markdown("""
<nav role="navigation" aria-label="Menu principal">
<div role="region" aria-label="Navigation principale" class="onglets-accessibles">
""", unsafe_allow_html=True)
if "onglet" not in st.session_state:
st.session_state.onglet = "Instructions"
onglet_choisi = None
onglets = ["Instructions", "Personnalisation", "Analyse", "Visualisations", "Fiches"]
for nom in onglets:
if st.session_state.onglet == nom:
st.markdown(f'<div class="bouton-fictif">{nom}</div>', unsafe_allow_html=True)
else:
if st.button(nom):
onglet_choisi = nom
st.markdown("""
<hr />
</div>
</nav>""", unsafe_allow_html=True)
# === GESTION DU THÈME ===
#
# Le changement de thème induit un st.rerun qui vide les formula
# Pour éviter de perdre les informations dans les formulaires,
# le changement de thème n'est proposé que si l'utilisateur est sur l'onglet "Instructions"
#
if st.session_state.onglet == "Instructions":
if "theme_mode" not in st.session_state:
st.session_state.theme_mode = "Clair"
st.markdown("""
<section role="region" aria-label="region-theme">
<div role="region" aria-labelledby="Theme">
<p id="Theme" class="decorative-heading">Thème</p>
""", unsafe_allow_html=True)
theme = st.radio("Thème", ["Clair", "Sombre"], index=["Clair", "Sombre"].index(st.session_state.theme_mode), horizontal=True, label_visibility="hidden")
st.markdown("""
<hr />
</div>
</nav>""", unsafe_allow_html=True)
else :
st.markdown("""
<section role="region" aria-label="region-theme">
<div role="region" aria-labelledby="Theme">
<p id="Theme" class="decorative-heading">Thème</p>
""", unsafe_allow_html=True)
st.info("Le changement de thème ne peut se faire que depuis l'onglet Instructions.")
st.markdown("""
<hr />
</div>
</nav>""", unsafe_allow_html=True)
theme = st.session_state.theme_mode
connexion()
if st.session_state.get("logged_in", False):
bouton_deconnexion()
# === RERUN SI BESOIN ===
if (onglet_choisi and onglet_choisi != st.session_state.onglet) or (theme != st.session_state.theme_mode):
if onglet_choisi: # Ne met à jour que si on a cliqué
st.session_state.onglet = onglet_choisi
st.session_state.theme_mode = theme
st.rerun()
def afficher_impact(total_bytes):
with st.sidebar:
components.html(f"""
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impact Environnemental</title>
<style>
body,
html {{
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
sans-serif;
}}
/* Div réseau pour impact CO₂ */
#network-usage {{
display: block;
font-size: small;
margin-top: 1rem;
text-align: center;
}}
.decorative-heading {{
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
color: #145a1a;
text-align: center;
}}
span {{
text-align: center;
}}
</style>
</head>
<body>
<hr />
<div role="region" aria-label="Impact environnemental" class="impact-environnement">
<p class="decorative-heading">Impact environnemental</p>
<p><span id="network-usage">Chargement en cours</span></p>
</div>
<script>
document.addEventListener("DOMContentLoaded", async function() {{
try {{
const module = await import("/assets/impact_co2.js");
module.calculerImpactCO2({total_bytes});
}} catch (error) {{
console.error("Erreur module impact_co2.js", error);
}}
}});
</script>
</body>
</html>
""")

15
config.py Normal file
View File

@ -0,0 +1,15 @@
import os
from dotenv import load_dotenv
load_dotenv(".env")
load_dotenv(".env.local", override=True)
GITEA_URL = os.getenv("GITEA_URL", "https://fabnum-git.peccini.fr/api/v1")
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "")
ORGANISATION = os.getenv("ORGANISATION", "fabnum")
DEPOT_FICHES = os.getenv("DEPOT_FICHES", "fiches")
DEPOT_CODE = os.getenv("DEPOT_CODE", "code")
ENV = os.getenv("ENV")
ENV_CODE = os.getenv("ENV_CODE")
DOT_FILE = os.getenv("DOT_FILE")
INSTRUCTIONS = os.getenv("INSTRUCTIONS", "Instructions.md")

870
fabnum.py

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,12 @@ def initialiser_logger():
return logger
def connexion():
if not st.session_state.get("logged_in", False):
st.title("Authentification")
if "logged_in" not in st.session_state or not st.session_state.logged_in:
st.html("""
<section role="region" aria-label="region-authentification">
<div role="region" aria-labelledby="Authentification">
<p id="Authentification" class="decorative-heading">Authentification</p>
""")
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
ORGANISATION = "FabNum"
@ -74,9 +78,19 @@ def connexion():
if erreur:
logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
st.error("❌ Accès refusé.")
st.html("""
</div>
</section>
""")
def bouton_deconnexion():
if st.session_state.get("logged_in", False):
st.html("""
<section role="region" aria-label="region-authentification">
<div role="region" aria-labelledby="Authentification">
<p id="Authentification" class="decorative-heading">Authentification</p>
""")
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
@ -84,3 +98,8 @@ def bouton_deconnexion():
st.session_state.token = ""
st.success("Déconnecté avec succès.")
st.rerun()
st.html("""
</div>
</section>
""")

332
utils/fiche_dynamic.py Normal file
View File

@ -0,0 +1,332 @@
import re, yaml, pandas as pd, textwrap
import unicodedata
from jinja2 import Template
import streamlit as st
# -------- repère chaque bloc ```yaml … ``` -------------
PAIR_RE = re.compile(r"```yaml[^\n]*\n(.*?)```", re.S | re.I)
def _pairs_dataframe(md: str) -> pd.DataFrame:
rows = []
for raw in PAIR_RE.findall(md):
bloc = yaml.safe_load(raw)
if isinstance(bloc, dict) and "pair" in bloc:
rows.append(bloc["pair"])
return pd.DataFrame(rows)
def _normalize_unicode(text: str) -> str:
return unicodedata.normalize("NFKC", text)
def _fill(segment: str, pair: dict) -> str:
segment = _normalize_unicode(segment)
for k, v in pair.items():
val = f"{v:.2f}" if isinstance(v, (int, float)) else str(v)
segment = re.sub(
rf"{{{{\s*{re.escape(k)}\s*}}}}",
val,
segment,
flags=re.I,
)
segment = re.sub(
r"ICS\s*=\s*[-+]?\d+(?:\.\d+)?",
f"ICS = {pair['ics']:.2f}",
segment,
count=1,
)
return segment
def _segments(md: str):
blocs = list(PAIR_RE.finditer(md))
for i, match in enumerate(blocs):
pair = yaml.safe_load(match.group(1))["pair"]
start = match.end()
end = blocs[i + 1].start() if i + 1 < len(blocs) else len(md)
segment = md[start:end]
yield pair, segment
def _pivot(df: pd.DataFrame) -> str:
out = []
for min_, g in df.groupby("minerai"):
out += [f"## {min_}",
"| Composant | ICS | Faisabilité technique | Délai d'implémentation | Impact économique |",
"| :-- | :--: | :--: | :--: | :--: |"]
for _, r in g.sort_values("ics", ascending=False).iterrows():
out += [f"| {r.composant} | {r.ics:.2f} | {r.f_tech:.2f} | "
f"{r.delai:.2f} | {r.cout:.2f} |"]
out.append("")
return "\n".join(out)
def _synth(df: pd.DataFrame) -> str:
lignes = ["| Composant | Minerai | ICS |", "| :-- | :-- | :--: |"]
for _, r in df.sort_values("ics", ascending=False).iterrows():
lignes.append(f"| {r.composant} | {r.minerai} | {r.ics:.2f} |")
return "\n".join(lignes)
def build_dynamic_sections(md_raw: str) -> str:
md_raw = _normalize_unicode(md_raw)
df = _pairs_dataframe(md_raw)
if df.empty:
return md_raw
couples = ["# Criticité par couple Composant -> Minerai"]
for pair, seg in _segments(md_raw):
if pair:
couples.append(_fill(seg, pair))
couples_md = "\n".join(couples)
pivot_md = _pivot(df)
synth_md = _synth(df)
md = re.sub(r"#\s+Criticité par couple.*", couples_md, md_raw, flags=re.S | re.I)
md = re.sub(r"<!---- AUTO-BEGIN:PIVOT -->.*?<!---- AUTO-END:PIVOT -->",
f"<!---- AUTO-BEGIN:PIVOT -->\n{pivot_md}\n<!---- AUTO-END:PIVOT -->",
md, flags=re.S)
md = re.sub(r"<!---- AUTO-BEGIN:TABLEAU-FINAL -->.*?<!---- AUTO-END:TABLEAU-FINAL -->",
f"<!---- AUTO-BEGIN:TABLEAU-FINAL -->\n{synth_md}\n<!---- AUTO-END:TABLEAU-FINAL -->",
md, flags=re.S)
return textwrap.dedent(md)
IVC_RE = re.compile(r"```yaml\s+minerai:(.*?)```", re.S | re.I)
def _synth_ivc(minerais: list[dict]) -> str:
"""Crée un tableau de synthèse pour les IVC des minerais."""
lignes = [
"| Minerai | IVC | Vulnérabilité |",
"| :-- | :-- | :-- |"
]
for minerai in minerais:
lignes.append(
f"| {minerai['nom']} | {minerai['ivc']} | {minerai['vulnerabilite']} |"
)
return "\n".join(lignes)
def _ivc_segments(md: str):
"""Yield (dict, segment) pour chaque bloc IVC yaml."""
pos = 0
for m in IVC_RE.finditer(md):
bloc = yaml.safe_load("minerai:" + m.group(1))
start = m.end()
next_match = IVC_RE.search(md, start)
end = next_match.start() if next_match else len(md)
yield bloc["minerai"], md[start:end].strip()
pos = end
yield None, md[pos:] # reste éventuel
def build_ivc_sections(md: str) -> str:
"""Remplace les blocs YAML minerai + segment avec rendu Jinja2, conserve l'intro."""
segments = []
minerais = [] # Pour collecter les données de chaque minerai
intro = None
matches = list(IVC_RE.finditer(md))
if matches:
first = matches[0]
intro = md[:first.start()].strip()
else:
return md # pas de blocs à traiter
for m in matches:
bloc = yaml.safe_load("minerai:" + m.group(1))
minerais.append(bloc["minerai"]) # Collecte les données
start = m.end()
next_match = IVC_RE.search(md, start)
end = next_match.start() if next_match else len(md)
rendered = Template(md[start:end].strip()).render(**bloc["minerai"])
segments.append(rendered)
if intro:
segments.insert(0, intro)
# Créer et insérer le tableau de synthèse
synth_table = _synth_ivc(minerais)
md_final = "\n\n".join(segments)
# Remplacer la section du tableau final
md_final = re.sub(
r"## Tableau de synthèse\s*\n<!---- AUTO-BEGIN:TABLEAU-FINAL -->.*?<!---- AUTO-END:TABLEAU-FINAL -->",
f"## Tableau de synthèse\n<!---- AUTO-BEGIN:TABLEAU-FINAL -->\n{synth_table}\n<!---- AUTO-END:TABLEAU-FINAL -->",
md_final,
flags=re.S
)
return md_final
# Regex pour capturer les blocs YAML d'opération dans les fiches IHH
IHH_RE = re.compile(r"```yaml\s+opération:(.*?)```", re.S | re.I)
def _synth_ihh(operations: list[dict]) -> str:
"""Crée un tableau de synthèse pour les IHH."""
# Créer un dictionnaire pour regrouper les données par minerai/produit/composant
data_by_item = {}
for op in operations:
nom = op.get('nom', '')
item_id = op.get('minerai', op.get('produit', op.get('composant', '')))
if not item_id:
continue
# Initialiser l'entrée si elle n'existe pas encore
if item_id not in data_by_item:
data_by_item[item_id] = {
'type': 'minerai' if 'extraction' in op or 'reserves' in op or 'traitement' in op else
'produit' if 'assemblage' in op else 'composant',
'extraction_ihh_pays': '-',
'extraction_ihh_acteurs': '-',
'reserves_ihh_pays': '-',
'traitement_ihh_pays': '-',
'traitement_ihh_acteurs': '-',
'assemblage_ihh_pays': '-',
'assemblage_ihh_acteurs': '-',
'fabrication_ihh_pays': '-',
'fabrication_ihh_acteurs': '-'
}
# Mettre à jour les valeurs selon le type d'opération
if 'extraction' in op:
data_by_item[item_id]['extraction_ihh_pays'] = op['extraction'].get('ihh_pays', '-')
data_by_item[item_id]['extraction_ihh_acteurs'] = op['extraction'].get('ihh_acteurs', '-')
data_by_item[item_id]['reserves_ihh_pays'] = op['reserves'].get('ihh_pays', '-')
data_by_item[item_id]['traitement_ihh_pays'] = op['traitement'].get('ihh_pays', '-')
data_by_item[item_id]['traitement_ihh_acteurs'] = op['traitement'].get('ihh_acteurs', '-')
elif 'assemblage' in op:
data_by_item[item_id]['assemblage_ihh_pays'] = op['assemblage'].get('ihh_pays', '-')
data_by_item[item_id]['assemblage_ihh_acteurs'] = op['assemblage'].get('ihh_acteurs', '-')
elif 'fabrication' in op:
data_by_item[item_id]['fabrication_ihh_pays'] = op['fabrication'].get('ihh_pays', '-')
data_by_item[item_id]['fabrication_ihh_acteurs'] = op['fabrication'].get('ihh_acteurs', '-')
# Compléter avec les autres types si présents
result = []
def pastille(indice, valeur):
if not valeur:
return ""
SEUILS = st.session_state['seuils']
VERT = SEUILS[indice]["vert"]["max"]
ROUGE = SEUILS[indice]["rouge"]["min"]
pastille_verte = ""
pastille_orange = "🔶"
pastille_rouge = "🔴"
if float(valeur) < VERT:
return pastille_verte
elif float(valeur) > ROUGE:
return pastille_rouge
else:
return pastille_orange
# Tableau des produits
produits = {k: v for k, v in data_by_item.items() if v['type'] == 'produit'}
if produits:
result.append("\n\n## Assemblage des produits\n")
produit_lignes = [
"| Produit | Assemblage IHH Pays | Assemblage IHH Acteurs |",
"| :-- | :--: | :--: |"
]
for produit, data in sorted(produits.items()):
pastille_1 = pastille("IHH", data['assemblage_ihh_pays'])
pastille_2 = pastille("IHH", data['assemblage_ihh_acteurs'])
produit_lignes.append(
f"| {produit} | {pastille_1} {data['assemblage_ihh_pays']} | {pastille_2} {data['assemblage_ihh_acteurs']} |"
)
result.append("\n".join(produit_lignes))
# Tableau des composants
composants = {k: v for k, v in data_by_item.items() if v['type'] == 'composant'}
if composants:
result.append("\n\n## Fabrication des composants\n")
composant_lignes = [
"| Composant | Fabrication IHH Pays | Fabrication IHH Acteurs |",
"| :-- | :--: | :--: |"
]
for composant, data in sorted(composants.items()):
pastille_1 = pastille("IHH", data['fabrication_ihh_pays'])
pastille_2 = pastille("IHH", data['fabrication_ihh_acteurs'])
composant_lignes.append(
f"| {composant} | {pastille_1} {data['fabrication_ihh_pays']} | {pastille_2} {data['fabrication_ihh_acteurs']} |"
)
result.append("\n".join(composant_lignes))
# Trier et créer le tableau de minerais (celui demandé)
minerais = {k: v for k, v in data_by_item.items() if v['type'] == 'minerai'}
if minerais:
result.append("\n\n## Opérations sur les minerais\n")
minerai_lignes = [
"| Minerai | Extraction IHH Pays | Extraction IHH Acteurs | Réserves IHH Pays | Traitement IHH Pays | Traitement IHH Acteurs |",
"| :-- | :--: | :--: | :--: | :--: | :--: |"
]
for minerai, data in sorted(minerais.items()):
pastille_1 = pastille("IHH", data['extraction_ihh_pays'])
pastille_2 = pastille("IHH", data['extraction_ihh_acteurs'])
pastille_3 = pastille("IHH", data['reserves_ihh_pays'])
pastille_4 = pastille("IHH", data['traitement_ihh_pays'])
pastille_5 = pastille("IHH", data['traitement_ihh_acteurs'])
minerai_lignes.append(
f"| {minerai} | {pastille_1} {data['extraction_ihh_pays']} | {pastille_2} {data['extraction_ihh_acteurs']} | "
f"{pastille_3} {data['reserves_ihh_pays']} | {pastille_4} {data['traitement_ihh_pays']} | {pastille_5} {data['traitement_ihh_acteurs']} |"
)
result.append("\n".join(minerai_lignes))
return "\n".join(result)
def build_ihh_sections(md: str) -> str:
"""Traite les fiches IHH pour les opérations, produits, composants et minerais."""
segments = []
operations = [] # Pour collecter les données de chaque opération
intro = None
matches = list(IHH_RE.finditer(md))
if matches:
first = matches[0]
intro = md[:first.start()].strip()
else:
return md # pas de blocs à traiter
# Traiter chaque bloc YAML et sa section correspondante
for m in matches:
bloc_text = m.group(1)
bloc = yaml.safe_load("opération:" + bloc_text)
operations.append(bloc["opération"]) # Collecte les données
start = m.end()
next_match = IHH_RE.search(md, start)
end = next_match.start() if next_match else len(md)
# Utiliser Jinja2 pour le rendu de la section
section_template = md[start:end].strip()
rendered = Template(section_template).render(**bloc["opération"])
segments.append(rendered)
if intro:
segments.insert(0, intro)
# Créer et insérer le tableau de synthèse si nécessaire
if "# Tableaux de synthèse" in md:
synth_table = _synth_ihh(operations)
md_final = "\n\n".join(segments)
# Remplacer la section du tableau final
md_final = re.sub(
r"(?:##?|#) Tableaux de synthèse\s*\n<!---- AUTO-BEGIN:TABLEAU-FINAL -->.*?<!---- AUTO-END:TABLEAU-FINAL -->",
f"# Tableaux de synthèse\n<!---- AUTO-BEGIN:TABLEAU-FINAL -->\n{synth_table}\n<!---- AUTO-END:TABLEAU-FINAL -->",
md_final,
flags=re.S
)
else:
md_final = "\n\n".join(segments)
return md_final

63
utils/fiche_utils.py Normal file
View File

@ -0,0 +1,63 @@
"""
fiche_utils.py  outils de lecture / rendu des fiches Markdown (indices et opérations)
Dépendances :
pip install python-frontmatter pyyaml jinja2
Usage :
from fiche_utils import load_seuils, render_fiche_markdown
seuils = load_seuils("config/indices_seuils.yaml")
markdown_rendered = render_fiche_markdown(raw_md_text, seuils)
"""
from __future__ import annotations
import frontmatter, yaml, jinja2, re, textwrap, pathlib
from typing import Dict
def load_seuils(path: str | pathlib.Path = "config/indices_seuils.yaml") -> Dict:
"""Charge le fichier YAML des seuils et renvoie le dict 'seuils'."""
data = yaml.safe_load(pathlib.Path(path).read_text(encoding="utf-8"))
return data.get("seuils", {})
def _migrate_metadata(meta: Dict) -> Dict:
"""Normalise les clés YAML (ex : sheet_type → type_fiche)."""
keymap = {
"sheet_type": "type_fiche",
"indice_code": "indice_court", # si besoin
}
for old, new in keymap.items():
if old in meta and new not in meta:
meta[new] = meta.pop(old)
return meta
def render_fiche_markdown(md_text: str, seuils: Dict) -> str:
"""Renvoie la fiche rendue (Markdown) :
 placeholders Jinja2 remplacés ({{ }})
 table seuils injectée via dict 'seuils'.
"""
post = frontmatter.loads(md_text)
meta = _migrate_metadata(dict(post.metadata))
body_template = post.content
# Instancie Jinja2 en 'StrictUndefined' pour signaler les placeholders manquants.
env = jinja2.Environment(
undefined=jinja2.StrictUndefined,
autoescape=False,
trim_blocks=True,
lstrip_blocks=True,
)
tpl = env.from_string(body_template)
rendered_body = tpl.render(**meta, seuils=seuils)
# Option : ajoute automatiquement titre + tableau version si absent.
header = f"# {meta.get('indice', meta.get('titre',''))} ({meta.get('indice_court','')})"
if not re.search(r"^# ", rendered_body, flags=re.M):
rendered_body = f"""{header}
{rendered_body}"""
return rendered_body

89
utils/gitea.py Normal file
View File

@ -0,0 +1,89 @@
import base64
import requests
import os
from dateutil import parser
from datetime import datetime, timezone
import logging
from config import GITEA_URL, GITEA_TOKEN, ORGANISATION, DEPOT_FICHES, DEPOT_CODE, ENV, ENV_CODE, DOT_FILE
def charger_instructions_depuis_gitea(nom_fichier="Instructions.md"):
headers = {"Authorization": f"token {GITEA_TOKEN}"}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/contents/{nom_fichier}?ref={ENV}"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
contenu_md = base64.b64decode(data["content"]).decode("utf-8")
return contenu_md
except Exception as e:
logging.error(f"Erreur chargement instructions Gitea : {e}")
return None
def recuperer_date_dernier_commit(url):
headers = {"Authorization": f"token {GITEA_TOKEN}"}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
commits = response.json()
if commits:
return parser.isoparse(commits[0]["commit"]["author"]["date"])
except Exception as e:
logging.error(f"Erreur récupération commit schema : {e}")
return None
def charger_schema_depuis_gitea(fichier_local="schema_temp.txt"):
headers = {"Authorization": f"token {GITEA_TOKEN}"}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_CODE}/contents/{DOT_FILE}?ref={ENV_CODE}"
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
remote_last_modified = recuperer_date_dernier_commit(f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_CODE}/commits?path={DOT_FILE}&sha={ENV_CODE}")
local_last_modified = datetime.fromtimestamp(os.path.getmtime(fichier_local), tz=timezone.utc) if os.path.exists(fichier_local) else None
if not local_last_modified or (remote_last_modified and remote_last_modified > local_last_modified):
dot_text = base64.b64decode(data["content"]).decode("utf-8")
with open(fichier_local, "w", encoding="utf-8") as f:
f.write(dot_text)
return "OK"
except Exception as e:
logging.error(f"Erreur chargement schema Gitea : {e}")
return None
def charger_arborescence_fiches():
headers = {"Authorization": f"token {GITEA_TOKEN}"}
url_base = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/contents/Documents?ref={ENV}"
try:
response = requests.get(url_base, headers=headers)
response.raise_for_status()
dossiers = response.json()
arbo = {}
for dossier in sorted(dossiers, key=lambda d: d['name'].lower()):
if dossier['type'] == 'dir':
dossier_name = dossier['name']
url_dossier = dossier['url']
response_dossier = requests.get(url_dossier, headers=headers)
response_dossier.raise_for_status()
fichiers = response_dossier.json()
fiches = sorted(
[
{"nom": f["name"], "download_url": f["download_url"]}
for f in fichiers if f["name"].endswith(".md")
],
key=lambda x: x['nom'].lower()
)
arbo[dossier_name] = fiches
return arbo
except Exception as e:
logging.error(f"Erreur chargement fiches : {e}")
return {}

241
utils/graph_utils.py Normal file
View File

@ -0,0 +1,241 @@
import networkx as nx
import pandas as pd
import logging
import streamlit as st
import json
def extraire_chemins_depuis(G, source):
chemins = []
stack = [(source, [source])]
while stack:
(node, path) = stack.pop()
voisins = list(G.successors(node))
if not voisins:
chemins.append(path)
else:
for voisin in voisins:
if voisin not in path:
stack.append((voisin, path + [voisin]))
return chemins
def extraire_chemins_vers(G, target, niveau_demande):
chemins = []
reverse_G = G.reverse()
niveaux = nx.get_node_attributes(G, "niveau")
stack = [(target, [target])]
while stack:
(node, path) = stack.pop()
voisins = list(reverse_G.successors(node))
if not voisins:
chemin_inverse = list(reversed(path))
contient_niveau = any(
int(niveaux.get(n, -1)) == niveau_demande for n in chemin_inverse
)
if contient_niveau:
chemins.append(chemin_inverse)
else:
for voisin in voisins:
if voisin not in path:
stack.append((voisin, path + [voisin]))
return chemins
def recuperer_donnees(graph, noeuds):
donnees = []
criticite = {}
for noeud in noeuds:
try:
operation, minerai = noeud.split('_', 1)
except ValueError:
logging.warning(f"Nom de nœud inattendu : {noeud}")
continue
if operation == "Traitement":
try:
fabrications = list(graph.predecessors(minerai))
valeurs = [
int(float(graph.get_edge_data(f, minerai)[0].get('criticite', 0)) * 100)
for f in fabrications
if graph.get_edge_data(f, minerai)
]
if valeurs:
criticite[minerai] = round(sum(valeurs) / len(valeurs))
except Exception as e:
logging.warning(f"Erreur criticité pour {noeud} : {e}")
criticite[minerai] = 50
for noeud in noeuds:
try:
operation, minerai = noeud.split('_', 1)
ihh_pays = int(graph.nodes[noeud].get('ihh_pays', 0))
ihh_acteurs = int(graph.nodes[noeud].get('ihh_acteurs', 0))
criticite_val = criticite.get(minerai, 50)
criticite_cat = 1 if criticite_val <= 33 else (2 if criticite_val <= 66 else 3)
donnees.append({
'categorie': operation,
'nom': minerai,
'ihh_pays': ihh_pays,
'ihh_acteurs': ihh_acteurs,
'criticite_minerai': criticite_val,
'criticite_cat': criticite_cat
})
except Exception as e:
logging.error(f"Erreur sur le nœud {noeud} : {e}", exc_info=True)
return pd.DataFrame(donnees)
def recuperer_donnees_2(graph, noeuds_2):
donnees = []
for minerai in noeuds_2:
try:
missing = []
if not graph.has_node(minerai):
missing.append(minerai)
if not graph.has_node(f"Extraction_{minerai}"):
missing.append(f"Extraction_{minerai}")
if not graph.has_node(f"Reserves_{minerai}"):
missing.append(f"Reserves_{minerai}")
if missing:
print(f"⚠️ Nœuds manquants pour {minerai} : {', '.join(missing)} — Ignoré.")
continue
ivc = int(graph.nodes[minerai].get('ivc', 0))
ihh_extraction_pays = int(graph.nodes[f"Extraction_{minerai}"].get('ihh_pays', 0))
ihh_reserves_pays = int(graph.nodes[f"Reserves_{minerai}"].get('ihh_pays', 0))
donnees.append({
'nom': minerai,
'ivc': ivc,
'ihh_extraction': ihh_extraction_pays,
'ihh_reserves': ihh_reserves_pays
})
except Exception as e:
print(f"Erreur avec le nœud {minerai} : {e}")
return donnees
def lancer_personnalisation(G):
st.markdown("""
# Personnalisation des produits finaux
Dans cette section, vous pouvez ajouter des produits finaux qui ne sont pas présents dans la liste,
par exemple des produits que vous concevez vous-même.
Vous pouvez aussi enregistrer ou recharger vos modifications.
---
""")
st.markdown("## Ajouter un nouveau produit final")
new_prod = st.text_input("Nom du nouveau produit (unique)", key="new_prod")
if new_prod:
ops_dispo = sorted([
n for n, d in G.nodes(data=True)
if d.get("niveau") == "10"
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
])
sel_new_op = st.selectbox("Opération d'assemblage (optionnelle)", ["-- Aucune --"] + ops_dispo, index=0)
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
sel_comps = st.multiselect("Composants à lier", options=niveau1)
if st.button("Créer le produit"):
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
if sel_new_op != "-- Aucune --":
G.add_edge(new_prod, sel_new_op)
for comp in sel_comps:
G.add_edge(new_prod, comp)
st.success(f"{new_prod} ajouté.")
st.markdown("## Modifier un produit final ajouté")
produits0 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "0" and d.get("personnalisation") == "oui"])
sel_display = st.multiselect("Produits à modifier", options=produits0)
if sel_display:
prod = sel_display[0]
if st.button(f"Supprimer {prod}"):
G.remove_node(prod)
st.success(f"{prod} supprimé.")
st.session_state.pop("prod_sel", None)
return G
ops_dispo = sorted([
n for n, d in G.nodes(data=True)
if d.get("niveau") == "10"
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
])
curr_ops = [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "10"]
default_idx = ops_dispo.index(curr_ops[0]) + 1 if curr_ops and curr_ops[0] in ops_dispo else 0
sel_op = st.selectbox("Opération d'assemblage liée", ["-- Aucune --"] + ops_dispo, index=default_idx)
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
linked = [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "1"]
nouveaux = st.multiselect(f"Composants liés à {prod}", options=niveau1, default=linked)
if st.button(f"Mettre à jour {prod}"):
for op in curr_ops:
if sel_op == "-- Aucune --" or op != sel_op:
G.remove_edge(prod, op)
if sel_op != "-- Aucune --" and (not curr_ops or sel_op not in curr_ops):
G.add_edge(prod, sel_op)
for comp in set(linked) - set(nouveaux):
G.remove_edge(prod, comp)
for comp in set(nouveaux) - set(linked):
G.add_edge(prod, comp)
st.success(f"{prod} mis à jour.")
st.markdown("## Sauvegarder ou restaurer la configuration")
if st.button("Exporter configuration"):
nodes = [n for n, d in G.nodes(data=True) if d.get("personnalisation") == "oui"]
edges = [(u, v) for u, v in G.edges() if u in nodes]
conf = {"nodes": nodes, "edges": edges}
json_str = json.dumps(conf, ensure_ascii=False)
st.download_button(
label="Télécharger (JSON)",
data=json_str,
file_name="config_personnalisation.json",
mime="application/json"
)
uploaded = st.file_uploader("Importer une configuration JSON (max 100 Ko)", type=["json"])
if uploaded:
if uploaded.size > 100 * 1024:
st.error("Fichier trop volumineux (max 100 Ko).")
else:
try:
conf = json.loads(uploaded.read().decode("utf-8"))
all_nodes = conf.get("nodes", [])
all_edges = conf.get("edges", [])
if not all_nodes:
st.warning("Aucun produit trouvé dans le fichier.")
else:
st.markdown("### Sélection des produits à restaurer")
sel_nodes = st.multiselect(
"Produits à restaurer",
options=all_nodes,
default=all_nodes,
key="restaurer_selection"
)
if st.button("Restaurer les éléments sélectionnés", type="primary"):
for node in sel_nodes:
if not G.has_node(node):
G.add_node(node, niveau="0", personnalisation="oui", label=node)
for u, v in all_edges:
if u in sel_nodes and v in sel_nodes + list(G.nodes()) and not G.has_edge(u, v):
G.add_edge(u, v)
st.success("Configuration partielle restaurée avec succès.")
except Exception as e:
st.error(f"Erreur d'import : {e}")
return G

View File

@ -41,7 +41,7 @@ def charger_fiches_et_labels():
return dictionnaire_fiches
def rechercher_tickets_gitea(fiche_selectionnee):
headers = {"Authorization": f"token " + GITEA_TOKEN}
headers = {"Authorization": f"token {GITEA_TOKEN}"}
params = {"state": "open"}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues"
@ -57,16 +57,12 @@ def rechercher_tickets_gitea(fiche_selectionnee):
return []
labels_cibles = set(cible["operations"] + [cible["item"]])
tickets_associes = []
for issue in issues:
if issue.get("ref") != f"refs/heads/{ENV}":
continue
issue_labels = set()
for label in issue.get("labels", []):
if isinstance(label, dict) and "name" in label:
issue_labels.add(label["name"])
issue_labels = set(label.get("name", "") for label in issue.get("labels", []))
if labels_cibles.issubset(issue_labels):
tickets_associes.append(issue)
@ -75,7 +71,6 @@ def rechercher_tickets_gitea(fiche_selectionnee):
except requests.RequestException as e:
st.error(f"Erreur lors de la récupération des tickets : {e}")
return []
def extraire_statut_par_label(ticket):
labels = [label.get('name', '') for label in ticket.get('labels', [])]
for statut in ["Backlog", "En attente de traitement", "En cours", "Terminés", "Non retenus"]:
@ -88,21 +83,17 @@ def afficher_tickets_par_fiche(tickets):
st.info("Aucun ticket lié à cette fiche.")
return
st.markdown("📝 **Tickets associés à cette fiche**")
st.markdown("**Tickets associés à cette fiche**")
tickets_groupes = defaultdict(list)
for ticket in tickets:
statut = extraire_statut_par_label(ticket)
tickets_groupes[statut].append(ticket)
nb_backlogs = len(tickets_groupes["Backlog"])
if nb_backlogs == 1:
st.info(f"{nb_backlogs} ticket en attente de modération n'est pas affiché.")
else :
if nb_backlogs:
st.info(f"{nb_backlogs} ticket(s) en attente de modération ne sont pas affichés.")
ordre_statuts = ["En attente de traitement", "En cours", "Terminés", "Non retenus", "Autres"]
for statut in ordre_statuts:
if tickets_groupes[statut]:
with st.expander(f"{statut} ({len(tickets_groupes[statut])})", expanded=(statut == "En cours")):
@ -110,19 +101,57 @@ def afficher_tickets_par_fiche(tickets):
afficher_carte_ticket(ticket)
def recuperer_commentaires_ticket(issue_index):
headers = {
"Authorization": f"token {GITEA_TOKEN}"
}
headers = {"Authorization": f"token {GITEA_TOKEN}"}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues/{issue_index}/comments"
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
commentaires = response.json()
return commentaires
return response.json()
except Exception as e:
st.error(f"Erreur lors de la récupération des commentaires pour le ticket {issue_index} : {e}")
st.error(f"Erreur lors de la récupération des commentaires : {e}")
return []
def get_labels_existants():
headers = {"Authorization": f"token {GITEA_TOKEN}"}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/labels"
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
return {label['name']: label['id'] for label in response.json()}
except Exception as e:
st.error(f"Erreur lors de la récupération des labels : {e}")
return {}
def nettoyer_labels(labels):
return sorted(set(l.strip() for l in labels if isinstance(l, str) and l.strip()))
def construire_corps_ticket_markdown(reponses):
return "\n\n".join(f"## {section}\n{texte}" for section, texte in reponses.items())
def creer_ticket_gitea(titre, corps, labels):
headers = {
"Authorization": f"token {GITEA_TOKEN}",
"Content-Type": "application/json"
}
data = {
"title": titre,
"body": corps,
"labels": labels,
"ref": f"refs/heads/{ENV}"
}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues"
try:
response = requests.post(url, headers=headers, data=json.dumps(data), timeout=10)
response.raise_for_status()
issue_url = response.json().get("html_url", "")
if issue_url:
st.success(f"Ticket créé ! [Voir le ticket]({issue_url})")
else:
st.success("Ticket créé avec succès.")
except Exception as e:
st.error(f"❌ Erreur création ticket : {e}")
def afficher_carte_ticket(ticket):
titre = ticket.get("title", "Sans titre")
url = ticket.get("html_url", "")
@ -137,210 +166,111 @@ def afficher_carte_ticket(ticket):
if match:
sujet = match.group(1).strip()
if created:
def format_date(iso):
try:
dt_created = parser.isoparse(created)
date_created_str = dt_created.strftime("%d/%m/%Y")
except Exception:
date_created_str = "Date inconnue"
else:
date_created_str = "Date inconnue"
return parser.isoparse(iso).strftime("%d/%m/%Y")
except:
return "?"
if updated and updated != created:
try:
dt_updated = parser.isoparse(updated)
date_updated_str = dt_updated.strftime("%d/%m/%Y")
maj_info = f"(MAJ {date_updated_str})"
except Exception:
maj_info = ""
else:
maj_info = ""
date_created_str = format_date(created)
maj_info = f"(MAJ {format_date(updated)})" if updated and updated != created else ""
commentaires = recuperer_commentaires_ticket(ticket.get("number"))
commentaires_html = ''
if commentaires:
commentaires_html = ""
for commentaire in commentaires:
auteur = html.escape(commentaire.get('user', {}).get('login', 'inconnu'))
contenu = html.escape(commentaire.get('body', ''))
date_commentaire = commentaire.get('created_at', '')
if date_commentaire:
try:
dt_comment = parser.isoparse(date_commentaire)
date_commentaire_str = dt_comment.strftime("%d/%m/%Y")
except Exception:
date_commentaire_str = ""
else:
date_commentaire_str = ""
date = format_date(commentaire.get('created_at', ''))
commentaires_html += f"""
<div style='background-color: #f0f0f0; padding: 0.5rem; border-radius: 8px; margin-bottom: 0.5rem;'>
<p style='margin: 0;'><strong>{auteur}</strong> <small>({date_commentaire_str})</small></p>
<p style='margin: 0.5rem 0 0 0;'>{contenu}</p>
<div class="conteneur_commentaire">
<p class="commentaire_auteur"><strong>{auteur}</strong> <small>({date})</small></p>
<p class="commentaire_contenu">{contenu}</p>
</div>
"""
else:
commentaires_html = '<p style="margin-top: 1rem;">Aucun commentaire.</p>'
with st.container():
st.markdown(f"""
<div style='border: 1px solid #ccc; border-radius: 12px; padding: 1rem; margin-bottom: 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);'>
<h4 style='margin-bottom: 0.5rem;'>🎫 <a href='{url}' target='_blank'>{titre}</a></h4>
<p style='margin: 0.2rem 0;'>Ouvert par <strong>{html.escape(user)}</strong> le {date_created_str} {maj_info}</p>
<p style='margin: 0.2rem 0;'>Sujet de la proposition : <strong>{html.escape(sujet)}</strong></p>
<p style='margin: 0.2rem 0;'><span>{''.join(html.escape(label) for label in labels) if labels else 'aucun'}</span></p>
<hr style='margin: 1.5rem 0;'>
<div class="conteneur_ticket">
<h4><a href='{url}' target='_blank'>{titre}</a></h4>
<p>Ouvert par <strong>{html.escape(user)}</strong> le {date_created_str} {maj_info}</p>
<p>Sujet : <strong>{html.escape(sujet)}</strong></p>
<p>Labels : {''.join(labels) if labels else 'aucun'}</p>
</div>
""", unsafe_allow_html=True)
st.markdown("**Contenu du ticket :**")
st.markdown(body, unsafe_allow_html=False)
st.markdown("---")
st.markdown("**Commentaires :**")
st.markdown(commentaires_html, unsafe_allow_html=True)
def get_labels_existants():
headers = {"Authorization": f"token {GITEA_TOKEN}"}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/labels"
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
labels_data = response.json()
return {label['name']: label['id'] for label in labels_data}
except Exception as e:
st.error(f"Erreur lors de la récupération des labels existants : {e}")
return {}
def creer_ticket_gitea(titre, corps, labels):
headers = {
"Authorization": f"token {GITEA_TOKEN}",
"Content-Type": "application/json"
}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues"
data = {
"title": titre,
"body": corps,
"labels": labels,
"ref": f"refs/heads/{ENV}"
}
try:
response = requests.post(url, headers=headers, data=json.dumps(data), timeout=10)
response.raise_for_status()
issue = response.json()
issue_url = issue.get("html_url", "")
if issue_url:
st.success(f"✅ Ticket créé avec succès ! [Voir le ticket]({issue_url})")
else:
st.success("✅ Ticket créé avec succès !")
except Exception as e:
st.error(f"❌ Erreur lors de la création du ticket : {e}")
st.markdown("**Commentaire(s) :**")
st.markdown(commentaires_html or "Aucun commentaire.", unsafe_allow_html=True)
def charger_modele_ticket():
headers = {"Authorization": f"token {GITEA_TOKEN}"}
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/contents/.gitea/ISSUE_TEMPLATE/Contenu.md"
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
contenu_base64 = response.json().get("content", "")
contenu = base64.b64decode(contenu_base64).decode("utf-8")
return contenu
r = requests.get(url, headers=headers, timeout=10)
r.raise_for_status()
return base64.b64decode(r.json().get("content", "")).decode("utf-8")
except Exception as e:
st.error(f"Erreur lors du chargement du modèle de ticket : {e}")
st.error(f"Erreur chargement modèle : {e}")
return ""
def gerer_tickets_fiche(fiche_selectionnee):
st.markdown("""
<hr style='border: 1px solid #ccc; margin: 2rem 0;' />
""", unsafe_allow_html=True)
st.markdown("### 🧾 Gestion des tickets pour cette fiche")
tickets = rechercher_tickets_gitea(fiche_selectionnee)
afficher_tickets_par_fiche(tickets)
def gerer_tickets_fiche(fiche_selectionnee):
st.markdown("<hr style='border: 1px solid #ccc; margin: 2rem 0;' />", unsafe_allow_html=True)
st.markdown("## Gestion des tickets pour cette fiche")
afficher_tickets_par_fiche(rechercher_tickets_gitea(fiche_selectionnee))
formulaire_creation_ticket_dynamique(fiche_selectionnee)
# Modification de formulaire_creation_ticket_dynamique pour ajouter Annuler
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
with st.expander(" Créer un nouveau ticket lié à cette fiche", expanded=False):
with st.expander("Créer un nouveau ticket lié à cette fiche", expanded=False):
contenu_modele = charger_modele_ticket()
if not contenu_modele:
st.error("Impossible de charger le modèle de ticket.")
return
sections = {}
lignes = contenu_modele.splitlines()
titre_courant = None
contenu_section = []
# Découpe le modèle en sections
sections, reponses = {}, {}
lignes, titre_courant, contenu = contenu_modele.splitlines(), None, []
for ligne in lignes:
if ligne.startswith("## ") and titre_courant:
sections[titre_courant] = "\n".join(contenu_section).strip()
titre_courant = ligne[3:].strip()
contenu_section = []
elif ligne.startswith("## "):
titre_courant = ligne[3:].strip()
contenu_section = []
if ligne.startswith("## "):
if titre_courant:
sections[titre_courant] = "\n".join(contenu).strip()
titre_courant, contenu = ligne[3:].strip(), []
elif titre_courant:
contenu_section.append(ligne)
if titre_courant and contenu_section:
sections[titre_courant] = "\n".join(contenu_section).strip()
reponses = {}
labels = []
selected_operations = []
contenu.append(ligne)
if titre_courant:
sections[titre_courant] = "\n".join(contenu).strip()
# Labels prédéfinis selon la fiche
labels, selected_ops = [], []
correspondances = charger_fiches_et_labels()
cible = correspondances.get(fiche_selectionnee)
if cible:
if len(cible["operations"]) == 1:
labels.append(cible["operations"][0])
elif len(cible["operations"]) > 1:
selected_operations = st.multiselect("Labels opération à associer", cible["operations"], default=cible["operations"])
selected_ops = st.multiselect("Labels opération à associer", cible["operations"], default=cible["operations"])
# Génération des champs
for section, aide in sections.items():
if "Type de contribution" in section:
options = re.findall(r"- \[.\] (.+)", aide)
clean_options = []
for opt in options:
base = opt.split(":")[0].strip()
if base not in clean_options:
clean_options.append(base)
if "Autre" not in clean_options:
clean_options.append("Autre")
type_contribution = st.radio("Type de contribution", clean_options)
if type_contribution == "Autre":
autre = st.text_input("Précisez le type de contribution")
reponses[section] = autre
else:
reponses[section] = type_contribution
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
if "Autre" not in options:
options.append("Autre")
choix = st.radio("Type de contribution", options)
reponses[section] = st.text_input("Précisez", "") if choix == "Autre" else choix
elif "Fiche concernée" in section:
base_url = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/"
url_fiche = f"{base_url}{fiche_selectionnee.replace(' ', '%20')}"
url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}"
reponses[section] = url_fiche
st.text_input("Fiche concernée", value=url_fiche, disabled=True)
elif "Sujet de la proposition" in section:
reponses[section] = st.text_input(section, help=aide)
else:
reponses[section] = st.text_area(section, help=aide)
col1, col2 = st.columns(2)
with col1:
if st.button("Prévisualiser le ticket"):
if col1.button("Prévisualiser le ticket"):
st.session_state.previsualiser = True
with col2:
if st.button("Annuler"):
if col2.button("Annuler"):
st.session_state.previsualiser = False
st.rerun()
@ -348,27 +278,21 @@ def formulaire_creation_ticket_dynamique(fiche_selectionnee):
st.subheader("Prévisualisation du ticket")
for section, texte in reponses.items():
st.markdown(f"#### {section}")
st.markdown(texte)
st.code(texte, language="markdown")
titre_ticket = reponses.get("Sujet de la proposition", "").strip() or "Ticket FabNum"
final_labels = nettoyer_labels(labels + selected_ops + ([cible["item"]] if cible else []))
st.markdown(f"**Résumé :**\n- **Titre** : `{titre_ticket}`\n- **Labels** : `{', '.join(final_labels)}`")
if st.button("Confirmer la création du ticket"):
if cible:
labels.append(cible["item"])
if selected_operations:
labels.extend(selected_operations)
labels = list(set([l.strip() for l in labels if l and l.strip()]))
titre_ticket = reponses.get("Sujet de la proposition", "").strip() or "Ticket FabNum"
labels_existants = get_labels_existants()
labels_ids = [labels_existants[l] for l in labels if l in labels_existants]
labels_ids = [labels_existants[l] for l in final_labels if l in labels_existants]
if "Backlog" in labels_existants:
labels_ids.append(labels_existants["Backlog"])
corps = ""
for section, texte in reponses.items():
corps += f"## {section}\n{texte}\n\n"
corps = construire_corps_ticket_markdown(reponses)
creer_ticket_gitea(titre_ticket, corps, labels_ids)
creer_ticket_gitea(titre=titre_ticket, corps=corps, labels=labels_ids)
st.success("Formulaire vidé après création du ticket.")
st.session_state.previsualiser = False
st.success("Ticket créé et formulaire vidé.")

174
utils/visualisation.py Normal file
View File

@ -0,0 +1,174 @@
import streamlit as st
import altair as alt
import numpy as np
from collections import Counter
import pandas as pd
def afficher_graphique_altair(df):
ordre_personnalise = ['Assemblage', 'Fabrication', 'Traitement', 'Extraction']
categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()]
for cat in categories:
st.markdown(f"### {cat}")
df_cat = df[df['categorie'] == cat].copy()
coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1)))
counts = Counter(coord_pairs)
offset_x = []
offset_y = {}
seen = Counter()
for pair in coord_pairs:
rank = seen[pair]
seen[pair] += 1
if counts[pair] > 1:
angle = rank * 1.5
radius = 0.8 + 0.4 * rank
offset_x.append(radius * np.cos(angle))
offset_y[pair] = radius * np.sin(angle)
else:
offset_x.append(0)
offset_y[pair] = 0
df_cat['ihh_pays'] += offset_x
df_cat['ihh_acteurs'] += [offset_y[p] for p in coord_pairs]
df_cat['ihh_pays_text'] = df_cat['ihh_pays'] + 0.5
df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
base = alt.Chart(df_cat).encode(
x=alt.X('ihh_pays:Q', title='IHH Pays (%)'),
y=alt.Y('ihh_acteurs:Q', title='IHH Acteurs (%)'),
size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred']))
)
points = base.mark_circle(opacity=0.6)
lines = alt.Chart(df_cat).mark_rule(strokeWidth=0.5, color='gray').encode(
x='ihh_pays:Q', x2='ihh_pays_text:Q',
y='ihh_acteurs:Q', y2='ihh_acteurs_text:Q'
)
labels = alt.Chart(df_cat).mark_text(
align='left', dx=3, dy=-3, fontSize=8, font='Arial', angle=335
).encode(
x='ihh_pays_text:Q',
y='ihh_acteurs_text:Q',
text='nom:N'
)
hline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15))
hline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25))
vline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15))
vline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25))
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
width=500,
height=400,
title=f"Concentration et criticité {cat}"
).interactive()
st.altair_chart(chart, use_container_width=True)
def creer_graphes(donnees):
if not donnees:
st.warning("Aucune donnée à afficher.")
return
try:
df = pd.DataFrame(donnees)
df['ivc_cat'] = df['ivc'].apply(lambda x: 1 if x <= 15 else (2 if x <= 30 else 3))
from collections import Counter
coord_pairs = list(zip(df['ihh_extraction'].round(1), df['ihh_reserves'].round(1)))
counts = Counter(coord_pairs)
offset_x, offset_y = [], {}
seen = Counter()
for pair in coord_pairs:
rank = seen[pair]
seen[pair] += 1
if counts[pair] > 1:
angle = rank * 1.5
radius = 0.8 + 0.4 * rank
offset_x.append(radius * np.cos(angle))
offset_y[pair] = radius * np.sin(angle)
else:
offset_x.append(0)
offset_y[pair] = 0
df['ihh_extraction'] += offset_x
df['ihh_reserves'] += [offset_y[p] for p in coord_pairs]
df['ihh_extraction_text'] = df['ihh_extraction'] + 0.5
df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5
base = alt.Chart(df).encode(
x=alt.X('ihh_extraction:Q', title='IHH Extraction (%)'),
y=alt.Y('ihh_reserves:Q', title='IHH Réserves (%)'),
size=alt.Size('ivc_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
color=alt.Color('ivc_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])),
tooltip=['nom:N', 'ivc:Q', 'ihh_extraction:Q', 'ihh_reserves:Q']
)
points = base.mark_circle(opacity=0.6)
lines = alt.Chart(df).mark_rule(strokeWidth=0.5, color='gray').encode(
x='ihh_extraction:Q', x2='ihh_extraction_text:Q',
y='ihh_reserves:Q', y2='ihh_reserves_text:Q'
)
labels = alt.Chart(df).mark_text(
align='left', dx=10, dy=-10, fontSize=10, font='Arial', angle=335
).encode(
x='ihh_extraction_text:Q',
y='ihh_reserves_text:Q',
text='nom:N'
)
hline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15))
hline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25))
vline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15))
vline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25))
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
width=600,
height=500,
title="Concentration des ressources critiques vs vulnérabilité IVC"
).interactive()
st.altair_chart(chart, use_container_width=True)
except Exception as e:
st.error(f"Erreur lors de la création du graphique : {e}")
def lancer_visualisation_ihh_criticite(graph):
try:
import networkx as nx
from utils.graph_utils import recuperer_donnees
niveaux = nx.get_node_attributes(graph, "niveau")
noeuds = [n for n, v in niveaux.items() if v == "10" and "Reserves" not in n]
noeuds.sort()
df = recuperer_donnees(graph, noeuds)
if df.empty:
st.warning("Aucune donnée à visualiser.")
else:
afficher_graphique_altair(df)
except Exception as e:
st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}")
def lancer_visualisation_ihh_ivc(graph):
try:
from utils.graph_utils import recuperer_donnees_2
noeuds_niveau_2 = [
n for n, data in graph.nodes(data=True)
if data.get("niveau") == "2" and "ivc" in data
]
if not noeuds_niveau_2:
return
data = recuperer_donnees_2(graph, noeuds_niveau_2)
creer_graphes(data)
except Exception as e:
st.error(f"Erreur dans la visualisation IHH vs IVC : {e}")