Trop de modifications
This commit is contained in:
parent
5436ccff5e
commit
967ca4bcf2
10
.env
Normal file
10
.env
Normal 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
7
.gitignore
vendored
@ -1,6 +1,5 @@
|
|||||||
# Ignorer fichiers sensibles
|
# Ignorer fichiers sensibles
|
||||||
.env
|
.env.local
|
||||||
*.env
|
|
||||||
|
|
||||||
# Ignorer fichiers utilisateurs
|
# Ignorer fichiers utilisateurs
|
||||||
*.pyc
|
*.pyc
|
||||||
@ -17,7 +16,8 @@ __pycache__/
|
|||||||
# Ignorer config locale
|
# Ignorer config locale
|
||||||
.ropeproject/
|
.ropeproject/
|
||||||
.streamlit/
|
.streamlit/
|
||||||
venv
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
# Ignorer données Fiches (adapté à ton projet)
|
# Ignorer données Fiches (adapté à ton projet)
|
||||||
Instructions.md
|
Instructions.md
|
||||||
@ -25,4 +25,3 @@ Fiches/
|
|||||||
|
|
||||||
# Autres spécifiques si besoin
|
# Autres spécifiques si besoin
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
349
HTML/Assemblage/Fiche assemblage IoT_Wearables.html
Normal file
349
HTML/Assemblage/Fiche assemblage IoT_Wearables.html
Normal 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>L’IHH pour les assembleurs d’objets 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>L’IHH 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 d’assemblage, suivie du Vietnam (15 %) et de la Malaisie (12 %). Cette dépendance marquée à l’Asie de l’Est 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 d’acteurs 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 à l’arrêt d’assemblages 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 d’assemblage 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>
|
||||||
344
HTML/Assemblage/Fiche assemblage casques VR.html
Normal file
344
HTML/Assemblage/Fiche assemblage casques VR.html
Normal 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é d’acteurs 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 d’assemblage, 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 d’un seul pays (Chine)</strong> pour l’assemblage (IHH 41)</li>
|
||||||
|
<li>Cette dépendance géographique est l’un des facteurs de vulnérabilité majeurs à surveiller</li>
|
||||||
|
<li>Le calcul de l’IHH 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 d’assemblage</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Type : Géopolitique</li>
|
||||||
|
<li>Impact : Embargos ou sanctions affectant les sites d’assemblage (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>
|
||||||
360
HTML/Assemblage/Fiche assemblage imprimante.html
Normal file
360
HTML/Assemblage/Fiche assemblage imprimante.html
Normal 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>L’IHH calculé pour les assembleurs d’imprimantes est de <strong>10</strong>, ce qui reflète une <strong>concentration industrielle faible</strong>. Aucun acteur n’excè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 d’un acteur majeur.</p>
|
||||||
|
<h4>IHH par pays</h4>
|
||||||
|
<p>L’IHH 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, l’existence 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 d’assemblage d’imprimantes 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 qu’en 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 d’assemblage</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Type : Géopolitique</li>
|
||||||
|
<li>Impact : Embargos ou sanctions affectant les sites d’assemblage (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 d’encre, laser) dans les volumes, non toujours précisée</li>
|
||||||
|
<li>Origine des entreprises (Taiwan/Japon) et lieux d’assemblage 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 d’imprimantes</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>
|
||||||
359
HTML/Connexe/Fiche assemblage photolitographie DUV.html
Normal file
359
HTML/Connexe/Fiche assemblage photolitographie DUV.html
Normal 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 d’assemblage : 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>TWINSCAN NXT:2100i</strong>) compte environ <strong>55 000 pièces</strong>, pèse 115 t et coûte <strong>90 – 140 M€</strong>.
|
||||||
|
Les KrF modernes (<strong>NSR‑S635E</strong>, Nikon) se vendent autour de <strong>45 M€</strong>.</p>
|
||||||
|
<p>Le flux d’assemblage s’effectue en 4 phases :</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Pré‑intégration modules</strong> (laser, optique, châssis) aux Pays‑Bas 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> (≈ 15–18 conteneurs)</li>
|
||||||
|
<li><strong>Ré‑assemblage & qualification</strong> chez le fondeur (3–6 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>TWINSCAN NXT (ASML)</strong></td>
|
||||||
|
<td style="text-align: center;">193 i</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>NSR‑S635E (Nikon)</strong></td>
|
||||||
|
<td style="text-align: center;">193 i</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>FPA‑3030iR (Canon)</strong></td>
|
||||||
|
<td style="text-align: center;">193 i</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 / 248 nm</td>
|
||||||
|
<td style="text-align: left;">Cymer (ASML), Gigaphoton</td>
|
||||||
|
<td style="text-align: left;">18–22 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Optique projection & illumination</td>
|
||||||
|
<td style="text-align: left;">Lentilles CaF₂ / fused‑silica</td>
|
||||||
|
<td style="text-align: left;">Zeiss SMT, Nikon Hikari</td>
|
||||||
|
<td style="text-align: left;">20–25 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Système immersion</td>
|
||||||
|
<td style="text-align: left;">Injecte eau ultra‑pure à 6 L/s</td>
|
||||||
|
<td style="text-align: left;">ASML Hydra, Nikon SIS</td>
|
||||||
|
<td style="text-align: left;">8–10 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Plateau wafer & méca‑statif</td>
|
||||||
|
<td style="text-align: left;">Positionne wafer ± 2 nm</td>
|
||||||
|
<td style="text-align: left;">ASML Motion, Nikon Precision</td>
|
||||||
|
<td style="text-align: left;">12–14 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Métrologie & alignement</td>
|
||||||
|
<td style="text-align: left;">Mesure overlay < 2 nm</td>
|
||||||
|
<td style="text-align: left;">ASML Horus, Nikon In‑Chip</td>
|
||||||
|
<td style="text-align: left;">6–8 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Vide & 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;">4–6 %</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, Nikon CTL</td>
|
||||||
|
<td style="text-align: left;">5–6 %</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;">Pays‑Bas</td>
|
||||||
|
<td style="text-align: left;">ASML</td>
|
||||||
|
<td style="text-align: left;">Pays‑Bas</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 : ~ 240 DUV scanners (toutes longueurs d’onde) 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 < 1 ppb, particules < 20 nm</td>
|
||||||
|
<td style="text-align: left;">Risque bulles & 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 multi‑patterning</td>
|
||||||
|
<td style="text-align: left;">≤ 2 nm à 120 pauses</td>
|
||||||
|
<td style="text-align: left;">Dépend stabilité stage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Export‑control</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 ≈ 5 Gshots</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>15–18 caisses</strong> (air + mer) ; modules ≤ 12 t</li>
|
||||||
|
<li>Transport aérien Boeing 747‑8F / 777F, conteneurs maritimes 40’ HC</li>
|
||||||
|
<li>Délai porte‑à‑porte : <strong>45 jours</strong> (Europe → États‑Unis ou Japon → Corée)</li>
|
||||||
|
<li>Assurance cargo typique <strong>100 M$</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 10 ans, remplacement tube laser tous 6 mois</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Consommation</td>
|
||||||
|
<td style="text-align: left;">350 kW (immersion) / 120 kW (KrF)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Re‑polissage lentilles</td>
|
||||||
|
<td style="text-align: left;">Tous les 50 kpl</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 trans‑Pacifique)</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 ; 18 caisses hors‑gabarit.
|
||||||
|
- <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> : ASML 84 %, Nikon 12 %, Canon 4 %
|
||||||
|
<em>Pays</em> : Pays‑Bas + 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 trans‑Pacifique.</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> & <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 (Zeiss SMT, DE)</strong> / <strong>Kumagaya (Nikon Hikari, 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 & 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 i‑line / KrF / ArF</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Ré‑assemblage & 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 « TWINSCAN NXT:2100i » (2024)</li>
|
||||||
|
<li>Cymer – « ArF immersion laser roadmap » (2025)</li>
|
||||||
|
<li>Gigaphoton – « KrF / ArF Source Spec Sheet » (2024)</li>
|
||||||
|
<li>Zeiss SMT – « DUV Optics White‑paper » (2023)</li>
|
||||||
|
<li>Nikon – « NSR History & Production Sites » (2024)</li>
|
||||||
|
<li>Canon – Communiqué « Utsunomiya expansion lithography » (2024)</li>
|
||||||
|
<li>DigiTimes – « ASML has installed 1 400 DUV tools in China » (2025)</li>
|
||||||
|
<li>ASML Veldhoven – Location & manufacturing footprint (2024)</li>
|
||||||
|
</ol></details>
|
||||||
|
</section>
|
||||||
305
HTML/Connexe/Fiche assemblage photolitographie EUV.html
Normal file
305
HTML/Connexe/Fiche assemblage photolitographie EUV.html
Normal 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 d’assemblage : 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 < 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 220–260 M€ (EXE > 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 d’assemblage 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 & qualification</strong> chez le fondeur (6–9 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&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;">25–30 % ([Cymer</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Optique collecteur & 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;">25–30 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Projection & 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;">10–15 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Plateau wafer & 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;">10–12 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Métrologie & alignement</td>
|
||||||
|
<td style="text-align: left;">Mesure overlay < 1,5 nm</td>
|
||||||
|
<td style="text-align: left;">ASML Horus</td>
|
||||||
|
<td style="text-align: left;">8–10 %</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Vide & 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;">5–6 %</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;">5–6 %</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&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 < 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 < 20 pm</td>
|
||||||
|
<td style="text-align: left;">Interféro-mécanique actif</td>
|
||||||
|
<td style="text-align: left;">Coût isolateurs & 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 < 80 nm</td>
|
||||||
|
<td style="text-align: left;">Limite débit & 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 > 10 t chacun</li>
|
||||||
|
<li>Démontage en « kits » (< 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) / > 1 MW (EXE)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Ré-usinage miroirs</td>
|
||||||
|
<td style="text-align: left;">Tous les 30–40 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 d’expositions, 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 & memory < 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 didn’t know about ASML Wilton history – Stories</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Source laser CO₂ & 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 & 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 d’intégration complète (NXE & EXE)</td>
|
||||||
|
<td style="text-align: left;"><strong>Veldhoven (NL)</strong></td>
|
||||||
|
<td style="text-align: left;">Seul endroit où l’on « ferme la machine », l’aligne, 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> d’usine secondaire pour l’assemblage 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>Barron’s, « ASML stock & 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>
|
||||||
8760
HTML/Criticités/Fiche technique ICS.html
Normal file
8760
HTML/Criticités/Fiche technique ICS.html
Normal file
File diff suppressed because it is too large
Load Diff
5136
HTML/Criticités/Fiche technique IHH.html
Normal file
5136
HTML/Criticités/Fiche technique IHH.html
Normal file
File diff suppressed because it is too large
Load Diff
1577
HTML/Criticités/Fiche technique IVC.html
Normal file
1577
HTML/Criticités/Fiche technique IVC.html
Normal file
File diff suppressed because it is too large
Load Diff
74
README.md
74
README.md
@ -30,14 +30,13 @@ Le fichier **requirements.txt** permet d'installer tout ce qui est nécessaire p
|
|||||||
|
|
||||||
### Environnement
|
### 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] :
|
Pour l'environnement de pré-production, (https://fabnum-dev.peccini.fr)[https://fabnum-dev.peccini.fr] :
|
||||||
|
|
||||||
ENV=dev
|
ENV=dev
|
||||||
PORT=8502
|
PORT=8502
|
||||||
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
|
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
|
||||||
GITEA_TOKEN = "LE_TOKEN_POUR_ACCEDER_A_GITEA"
|
|
||||||
ORGANISATION = "fabnum"
|
ORGANISATION = "fabnum"
|
||||||
DEPOT_FICHES = "fiches"
|
DEPOT_FICHES = "fiches"
|
||||||
|
|
||||||
@ -77,12 +76,75 @@ Pour automatiser le lancement, il est intégré dans systemd :
|
|||||||
|
|
||||||
### fabnum.py
|
### 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 l’application. 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.
|
l’interface 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),
|
||||||
|
|
||||||
|
l’analyse 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 l’application, notamment :
|
||||||
|
|
||||||
|
connexion.py pour l’authentification via Gitea,
|
||||||
|
|
||||||
|
utils/ pour les fonctions métiers (import graph, traitement, visualisation),
|
||||||
|
|
||||||
|
components/ pour l’affichage 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 d’interagir avec les graphes, les métadonnées et les visualisations en toute autonomie
|
||||||
|
|
||||||
### tickets_fiche.py
|
### 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 d’un modèle Markdown,
|
||||||
|
|
||||||
|
de prévisualiser et publier ce ticket directement via l’API Gitea.
|
||||||
|
|
||||||
|
Il gère également :
|
||||||
|
|
||||||
|
la détection de conflits ou erreurs lors des appels réseau,
|
||||||
|
|
||||||
|
l’automatisation 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 d’environnement
|
||||||
|
├── 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 d’impact 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
28
assets/config.yaml
Normal 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 }
|
||||||
@ -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
395
assets/styles/base.css
Normal 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;
|
||||||
|
}
|
||||||
30
assets/styles/theme-dark.css
Normal file
30
assets/styles/theme-dark.css
Normal 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;
|
||||||
|
}
|
||||||
30
assets/styles/theme-light.css
Normal file
30
assets/styles/theme-light.css
Normal 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
204
components/fiches.py
Normal 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
21
components/footer.py
Normal 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
22
components/header.py
Normal 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
135
components/sidebar.py
Normal 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
15
config.py
Normal 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")
|
||||||
@ -18,8 +18,12 @@ def initialiser_logger():
|
|||||||
return logger
|
return logger
|
||||||
|
|
||||||
def connexion():
|
def connexion():
|
||||||
if not st.session_state.get("logged_in", False):
|
if "logged_in" not in st.session_state or not st.session_state.logged_in:
|
||||||
st.title("Authentification")
|
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"
|
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
|
||||||
ORGANISATION = "FabNum"
|
ORGANISATION = "FabNum"
|
||||||
@ -74,9 +78,19 @@ def connexion():
|
|||||||
if erreur:
|
if erreur:
|
||||||
logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
|
logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
|
||||||
st.error("❌ Accès refusé.")
|
st.error("❌ Accès refusé.")
|
||||||
|
st.html("""
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
""")
|
||||||
|
|
||||||
def bouton_deconnexion():
|
def bouton_deconnexion():
|
||||||
if st.session_state.get("logged_in", False):
|
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}`")
|
st.sidebar.markdown(f"Connecté en tant que `{st.session_state.username}`")
|
||||||
if st.sidebar.button("Se déconnecter"):
|
if st.sidebar.button("Se déconnecter"):
|
||||||
st.session_state.logged_in = False
|
st.session_state.logged_in = False
|
||||||
@ -84,3 +98,8 @@ def bouton_deconnexion():
|
|||||||
st.session_state.token = ""
|
st.session_state.token = ""
|
||||||
st.success("Déconnecté avec succès.")
|
st.success("Déconnecté avec succès.")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
st.html("""
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
""")
|
||||||
332
utils/fiche_dynamic.py
Normal file
332
utils/fiche_dynamic.py
Normal 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
63
utils/fiche_utils.py
Normal 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
89
utils/gitea.py
Normal 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
241
utils/graph_utils.py
Normal 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
|
||||||
@ -41,7 +41,7 @@ def charger_fiches_et_labels():
|
|||||||
return dictionnaire_fiches
|
return dictionnaire_fiches
|
||||||
|
|
||||||
def rechercher_tickets_gitea(fiche_selectionnee):
|
def rechercher_tickets_gitea(fiche_selectionnee):
|
||||||
headers = {"Authorization": f"token " + GITEA_TOKEN}
|
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||||
params = {"state": "open"}
|
params = {"state": "open"}
|
||||||
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues"
|
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues"
|
||||||
|
|
||||||
@ -57,16 +57,12 @@ def rechercher_tickets_gitea(fiche_selectionnee):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
labels_cibles = set(cible["operations"] + [cible["item"]])
|
labels_cibles = set(cible["operations"] + [cible["item"]])
|
||||||
|
|
||||||
tickets_associes = []
|
tickets_associes = []
|
||||||
|
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
if issue.get("ref") != f"refs/heads/{ENV}":
|
if issue.get("ref") != f"refs/heads/{ENV}":
|
||||||
continue
|
continue
|
||||||
issue_labels = set()
|
issue_labels = set(label.get("name", "") for label in issue.get("labels", []))
|
||||||
for label in issue.get("labels", []):
|
|
||||||
if isinstance(label, dict) and "name" in label:
|
|
||||||
issue_labels.add(label["name"])
|
|
||||||
|
|
||||||
if labels_cibles.issubset(issue_labels):
|
if labels_cibles.issubset(issue_labels):
|
||||||
tickets_associes.append(issue)
|
tickets_associes.append(issue)
|
||||||
|
|
||||||
@ -75,7 +71,6 @@ def rechercher_tickets_gitea(fiche_selectionnee):
|
|||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
st.error(f"Erreur lors de la récupération des tickets : {e}")
|
st.error(f"Erreur lors de la récupération des tickets : {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def extraire_statut_par_label(ticket):
|
def extraire_statut_par_label(ticket):
|
||||||
labels = [label.get('name', '') for label in ticket.get('labels', [])]
|
labels = [label.get('name', '') for label in ticket.get('labels', [])]
|
||||||
for statut in ["Backlog", "En attente de traitement", "En cours", "Terminés", "Non retenus"]:
|
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.")
|
st.info("Aucun ticket lié à cette fiche.")
|
||||||
return
|
return
|
||||||
|
|
||||||
st.markdown("📝 **Tickets associés à cette fiche**")
|
st.markdown("**Tickets associés à cette fiche**")
|
||||||
|
|
||||||
tickets_groupes = defaultdict(list)
|
tickets_groupes = defaultdict(list)
|
||||||
for ticket in tickets:
|
for ticket in tickets:
|
||||||
statut = extraire_statut_par_label(ticket)
|
statut = extraire_statut_par_label(ticket)
|
||||||
tickets_groupes[statut].append(ticket)
|
tickets_groupes[statut].append(ticket)
|
||||||
|
|
||||||
nb_backlogs = len(tickets_groupes["Backlog"])
|
nb_backlogs = len(tickets_groupes["Backlog"])
|
||||||
if nb_backlogs == 1:
|
if nb_backlogs:
|
||||||
st.info(f" ⤇ {nb_backlogs} ticket en attente de modération n'est pas affiché.")
|
st.info(f"⤇ {nb_backlogs} ticket(s) en attente de modération ne sont pas affichés.")
|
||||||
else :
|
|
||||||
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"]
|
ordre_statuts = ["En attente de traitement", "En cours", "Terminés", "Non retenus", "Autres"]
|
||||||
|
|
||||||
for statut in ordre_statuts:
|
for statut in ordre_statuts:
|
||||||
if tickets_groupes[statut]:
|
if tickets_groupes[statut]:
|
||||||
with st.expander(f"{statut} ({len(tickets_groupes[statut])})", expanded=(statut == "En cours")):
|
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)
|
afficher_carte_ticket(ticket)
|
||||||
|
|
||||||
def recuperer_commentaires_ticket(issue_index):
|
def recuperer_commentaires_ticket(issue_index):
|
||||||
headers = {
|
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||||
"Authorization": f"token {GITEA_TOKEN}"
|
|
||||||
}
|
|
||||||
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues/{issue_index}/comments"
|
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/issues/{issue_index}/comments"
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=headers, timeout=10)
|
response = requests.get(url, headers=headers, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
commentaires = response.json()
|
return response.json()
|
||||||
return commentaires
|
|
||||||
except Exception as e:
|
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 []
|
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):
|
def afficher_carte_ticket(ticket):
|
||||||
titre = ticket.get("title", "Sans titre")
|
titre = ticket.get("title", "Sans titre")
|
||||||
url = ticket.get("html_url", "")
|
url = ticket.get("html_url", "")
|
||||||
@ -137,238 +166,133 @@ def afficher_carte_ticket(ticket):
|
|||||||
if match:
|
if match:
|
||||||
sujet = match.group(1).strip()
|
sujet = match.group(1).strip()
|
||||||
|
|
||||||
if created:
|
def format_date(iso):
|
||||||
try:
|
try:
|
||||||
dt_created = parser.isoparse(created)
|
return parser.isoparse(iso).strftime("%d/%m/%Y")
|
||||||
date_created_str = dt_created.strftime("%d/%m/%Y")
|
except:
|
||||||
except Exception:
|
return "?"
|
||||||
date_created_str = "Date inconnue"
|
|
||||||
else:
|
|
||||||
date_created_str = "Date inconnue"
|
|
||||||
|
|
||||||
if updated and updated != created:
|
date_created_str = format_date(created)
|
||||||
try:
|
maj_info = f"(MAJ {format_date(updated)})" if updated and updated != created else ""
|
||||||
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 = ""
|
|
||||||
|
|
||||||
commentaires = recuperer_commentaires_ticket(ticket.get("number"))
|
commentaires = recuperer_commentaires_ticket(ticket.get("number"))
|
||||||
|
commentaires_html = ""
|
||||||
commentaires_html = ''
|
for commentaire in commentaires:
|
||||||
if commentaires:
|
auteur = html.escape(commentaire.get('user', {}).get('login', 'inconnu'))
|
||||||
for commentaire in commentaires:
|
contenu = html.escape(commentaire.get('body', ''))
|
||||||
auteur = html.escape(commentaire.get('user', {}).get('login', 'inconnu'))
|
date = format_date(commentaire.get('created_at', ''))
|
||||||
contenu = html.escape(commentaire.get('body', ''))
|
commentaires_html += f"""
|
||||||
date_commentaire = commentaire.get('created_at', '')
|
<div class="conteneur_commentaire">
|
||||||
if date_commentaire:
|
<p class="commentaire_auteur"><strong>{auteur}</strong> <small>({date})</small></p>
|
||||||
try:
|
<p class="commentaire_contenu">{contenu}</p>
|
||||||
dt_comment = parser.isoparse(date_commentaire)
|
</div>
|
||||||
date_commentaire_str = dt_comment.strftime("%d/%m/%Y")
|
"""
|
||||||
except Exception:
|
|
||||||
date_commentaire_str = ""
|
|
||||||
else:
|
|
||||||
date_commentaire_str = ""
|
|
||||||
|
|
||||||
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>
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
commentaires_html = '<p style="margin-top: 1rem;">Aucun commentaire.</p>'
|
|
||||||
|
|
||||||
with st.container():
|
with st.container():
|
||||||
st.markdown(f"""
|
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);'>
|
<div class="conteneur_ticket">
|
||||||
<h4 style='margin-bottom: 0.5rem;'>🎫 <a href='{url}' target='_blank'>{titre}</a></h4>
|
<h4><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>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>Sujet : <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>
|
<p>Labels : {' • '.join(labels) if labels else 'aucun'}</p>
|
||||||
<hr style='margin: 1.5rem 0;'>
|
</div>
|
||||||
</div>
|
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
st.markdown("**Contenu du ticket :**")
|
|
||||||
st.markdown(body, unsafe_allow_html=False)
|
st.markdown(body, unsafe_allow_html=False)
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("**Commentaires :**")
|
st.markdown("**Commentaire(s) :**")
|
||||||
st.markdown(commentaires_html, unsafe_allow_html=True)
|
st.markdown(commentaires_html or "Aucun commentaire.", 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}")
|
|
||||||
|
|
||||||
def charger_modele_ticket():
|
def charger_modele_ticket():
|
||||||
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||||
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/contents/.gitea/ISSUE_TEMPLATE/Contenu.md"
|
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/contents/.gitea/ISSUE_TEMPLATE/Contenu.md"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=headers, timeout=10)
|
r = requests.get(url, headers=headers, timeout=10)
|
||||||
response.raise_for_status()
|
r.raise_for_status()
|
||||||
contenu_base64 = response.json().get("content", "")
|
return base64.b64decode(r.json().get("content", "")).decode("utf-8")
|
||||||
contenu = base64.b64decode(contenu_base64).decode("utf-8")
|
|
||||||
return contenu
|
|
||||||
except Exception as e:
|
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 ""
|
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)
|
def gerer_tickets_fiche(fiche_selectionnee):
|
||||||
afficher_tickets_par_fiche(tickets)
|
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)
|
formulaire_creation_ticket_dynamique(fiche_selectionnee)
|
||||||
|
|
||||||
# Modification de formulaire_creation_ticket_dynamique pour ajouter Annuler
|
|
||||||
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
|
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()
|
contenu_modele = charger_modele_ticket()
|
||||||
|
|
||||||
if not contenu_modele:
|
if not contenu_modele:
|
||||||
st.error("Impossible de charger le modèle de ticket.")
|
st.error("Impossible de charger le modèle de ticket.")
|
||||||
return
|
return
|
||||||
|
|
||||||
sections = {}
|
# Découpe le modèle en sections
|
||||||
lignes = contenu_modele.splitlines()
|
sections, reponses = {}, {}
|
||||||
titre_courant = None
|
lignes, titre_courant, contenu = contenu_modele.splitlines(), None, []
|
||||||
contenu_section = []
|
|
||||||
|
|
||||||
for ligne in lignes:
|
for ligne in lignes:
|
||||||
if ligne.startswith("## ") and titre_courant:
|
if ligne.startswith("## "):
|
||||||
sections[titre_courant] = "\n".join(contenu_section).strip()
|
if titre_courant:
|
||||||
titre_courant = ligne[3:].strip()
|
sections[titre_courant] = "\n".join(contenu).strip()
|
||||||
contenu_section = []
|
titre_courant, contenu = ligne[3:].strip(), []
|
||||||
elif ligne.startswith("## "):
|
|
||||||
titre_courant = ligne[3:].strip()
|
|
||||||
contenu_section = []
|
|
||||||
elif titre_courant:
|
elif titre_courant:
|
||||||
contenu_section.append(ligne)
|
contenu.append(ligne)
|
||||||
|
if titre_courant:
|
||||||
if titre_courant and contenu_section:
|
sections[titre_courant] = "\n".join(contenu).strip()
|
||||||
sections[titre_courant] = "\n".join(contenu_section).strip()
|
|
||||||
|
|
||||||
reponses = {}
|
|
||||||
labels = []
|
|
||||||
selected_operations = []
|
|
||||||
|
|
||||||
|
# Labels prédéfinis selon la fiche
|
||||||
|
labels, selected_ops = [], []
|
||||||
correspondances = charger_fiches_et_labels()
|
correspondances = charger_fiches_et_labels()
|
||||||
cible = correspondances.get(fiche_selectionnee)
|
cible = correspondances.get(fiche_selectionnee)
|
||||||
if cible:
|
if cible:
|
||||||
if len(cible["operations"]) == 1:
|
if len(cible["operations"]) == 1:
|
||||||
labels.append(cible["operations"][0])
|
labels.append(cible["operations"][0])
|
||||||
elif len(cible["operations"]) > 1:
|
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():
|
for section, aide in sections.items():
|
||||||
if "Type de contribution" in section:
|
if "Type de contribution" in section:
|
||||||
options = re.findall(r"- \[.\] (.+)", aide)
|
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
|
||||||
clean_options = []
|
if "Autre" not in options:
|
||||||
for opt in options:
|
options.append("Autre")
|
||||||
base = opt.split(":")[0].strip()
|
choix = st.radio("Type de contribution", options)
|
||||||
if base not in clean_options:
|
reponses[section] = st.text_input("Précisez", "") if choix == "Autre" else choix
|
||||||
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
|
|
||||||
|
|
||||||
elif "Fiche concernée" in section:
|
elif "Fiche concernée" in section:
|
||||||
base_url = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/"
|
url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}"
|
||||||
url_fiche = f"{base_url}{fiche_selectionnee.replace(' ', '%20')}"
|
|
||||||
reponses[section] = url_fiche
|
reponses[section] = url_fiche
|
||||||
st.text_input("Fiche concernée", value=url_fiche, disabled=True)
|
st.text_input("Fiche concernée", value=url_fiche, disabled=True)
|
||||||
|
|
||||||
elif "Sujet de la proposition" in section:
|
elif "Sujet de la proposition" in section:
|
||||||
reponses[section] = st.text_input(section, help=aide)
|
reponses[section] = st.text_input(section, help=aide)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
reponses[section] = st.text_area(section, help=aide)
|
reponses[section] = st.text_area(section, help=aide)
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
if col1.button("Prévisualiser le ticket"):
|
||||||
with col1:
|
st.session_state.previsualiser = True
|
||||||
if st.button("Prévisualiser le ticket"):
|
if col2.button("Annuler"):
|
||||||
st.session_state.previsualiser = True
|
st.session_state.previsualiser = False
|
||||||
|
st.rerun()
|
||||||
with col2:
|
|
||||||
if st.button("Annuler"):
|
|
||||||
st.session_state.previsualiser = False
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
if st.session_state.get("previsualiser", False):
|
if st.session_state.get("previsualiser", False):
|
||||||
st.subheader("Prévisualisation du ticket")
|
st.subheader("Prévisualisation du ticket")
|
||||||
for section, texte in reponses.items():
|
for section, texte in reponses.items():
|
||||||
st.markdown(f"#### {section}")
|
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 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_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:
|
if "Backlog" in labels_existants:
|
||||||
labels_ids.append(labels_existants["Backlog"])
|
labels_ids.append(labels_existants["Backlog"])
|
||||||
|
|
||||||
corps = ""
|
corps = construire_corps_ticket_markdown(reponses)
|
||||||
for section, texte in reponses.items():
|
creer_ticket_gitea(titre_ticket, corps, labels_ids)
|
||||||
corps += f"## {section}\n{texte}\n\n"
|
|
||||||
|
|
||||||
creer_ticket_gitea(titre=titre_ticket, corps=corps, labels=labels_ids)
|
st.session_state.previsualiser = False
|
||||||
|
st.success("Ticket créé et formulaire vidé.")
|
||||||
st.success("Formulaire vidé après création du ticket.")
|
|
||||||
174
utils/visualisation.py
Normal file
174
utils/visualisation.py
Normal 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}")
|
||||||
Loading…
x
Reference in New Issue
Block a user