From 9f0493c98512a6315c74a3a72a7bfa931102ed6a Mon Sep 17 00:00:00 2001 From: Marc Lasserre Date: Thu, 28 May 2026 23:23:00 +0200 Subject: [PATCH] feat: implement 6 pillars, dynamic badge fctory and security --- CHANGELOG.md | 12 ++ app.js | 183 +++++++++++++------- index.html | 481 ++++++++++++++++++++++++++------------------------- style.css | 244 ++++++++++++++------------ 4 files changed, 517 insertions(+), 403 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89de242..c4c3e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ et ce projet respecte le [Versionnement Sémantique](https://semver.org/lang/fr/ --- +## [3.1.3] - 2026-05-28 + +### Ajouté +- **Cockpit Holistique à 6 Piliers :** Extension du tableau de bord de 4 à 6 indicateurs circulaires SVG autonomes représentant l'intégralité des dimensions de l'athlète (Entraînement, Récupération, Nutrition, Hydratation, Soins, Matériel). +- **Usine à Badges Dynamique :** Implémentation d'un générateur d'activités et de tâches de maintenance à la volée via un formulaire JavaScript natif interconnecté au moteur de planification. +- **Sécurisation de la Supply Chain (SRI) :** Intégration des verrous d'intégrité cryptographique `integrity` (SHA-384) sur les liaisons CDN externes pour se conformer aux exigences de sécurité strictes de SonarQube. + +### Modifié +- **Recentrage Produit (MVP) :** Nettoyage architectural complet de l'interface pour éliminer les briques tierces obsolètes (Espace Cohorte, Administrateur, MCO Infra) et recentrer l'application sur le profil unique de l'athlète d'endurance. +- **Moteur de Rendu Temporel :** Refonte de la logique événementielle dans `app.js` pour détruire et réassigner dynamiquement les écouteurs de glisser-déposer (*Drag & Drop*) sur les nouveaux composants injectés en cours de session. +- **Feuille de Style Souveraine :** Élimination des conflits d'opacité liés aux classes de transition natives de Bootstrap au profit d'un contrôle d'affichage exclusif et performant en CSS pur. + ## [3.1.0] - 2026-05-27 ### Ajouté - **Moteur Drag & Drop Natif :** Implémentation d'un système de planification agile (Kanban) dans `app.js` permettant de glisser-déposer les séances d'entraînement à la souris sans aucune dépendance logicielle externe. diff --git a/app.js b/app.js index a713383..4b8c587 100644 --- a/app.js +++ b/app.js @@ -1,83 +1,148 @@ -document.addEventListener("DOMContentLoaded", () => { - // --- NAV SWITCHER DES ONGLETS LATÉRAUX --- - const navLinks = document.querySelectorAll(".nav-link"); - const tabContents = document.querySelectorAll(".tab-content"); +/** + * ==================================================================== + * RYM HORIZON — ENGINE APPLICATION (v3.1.3) + * ==================================================================== + * Philosophie : Code découplé, souveraineté et performance pure. + * Architecture : Gestionnaire de vues + Moteur Drag & Drop + Usine à badges. + * Conformité : Sans dépendances tierces intrusives. + */ - navLinks.forEach(link => { - link.addEventListener("click", (e) => { - e.preventDefault(); - navLinks.forEach(l => l.classList.remove("active")); - tabContents.forEach(tc => tc.classList.remove("active")); +document.addEventListener('DOMContentLoaded', () => { + + // --- INTÉGRATION DE LA CHRONOLOGIE (SESSION LOG) --- + console.log("RYM Horizon Engine v3.1.3 : Initialisation du système..."); - link.classList.add("active"); - const tabId = link.getAttribute("data-tab"); - const targetSection = document.getElementById(tabId); - if (targetSection) { - targetSection.classList.add("active"); - } + // ========================================== + // MODULE 1 : MANAGER DE NAVIGATION SOUVERAIN + // ========================================== + const tabButtons = document.querySelectorAll('.nav-view-switcher .nav-link'); + const tabPanes = document.querySelectorAll('.tab-content .tab-pane'); + + tabButtons.forEach(button => { + button.addEventListener('click', (e) => { + const targetTabId = button.getAttribute('data-tab'); + + // 1. Mise à jour de l'état actif sur les boutons du menu + tabButtons.forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + + // 2. Commutation stricte des conteneurs de vues (Géré via CSS display !important) + tabPanes.forEach(pane => { + if (pane.id === targetTabId) { + pane.classList.add('active'); + } else { + pane.classList.remove('active'); + } + }); + console.log(`[Navigation] Passage sur la vue : ${targetTabId}`); }); }); - // --- DRAG & DROP ENGINE INTELLIGENT --- - const draggables = document.querySelectorAll(".rym-draggable-workout"); - const dropZones = document.querySelectorAll(".drop-zone"); - function recalculerChargeHebdo() { - let totalSéances = 0; + // ========================================== + // MODULE 2 : USINE DE CRÉATION DE BADGES (À LA VOLÉE) + // ========================================== + const generatorForm = document.getElementById('rym-coach-generator'); + const productionZone = document.getElementById('badge-production-zone'); + + // Dictionnaire de configuration des composants (Couleurs CSS natifs et Icônes) + const typeDictionary = { + run: { cssClass: 'run', icon: 'bi-lightning-fill' }, + bike: { cssClass: 'bike', icon: 'bi-bicycle' }, + swim: { cssClass: 'swim', icon: 'bi-water' }, + matos: { cssClass: 'run', icon: 'bi-gear-wide-connected', customStyle: 'background: #fef2f2; color: #991b1b; border: 1px dashed #f87171;' }, + soins: { cssClass: 'swim', icon: 'bi-heart-pulse-fill', customStyle: 'background: #faf5ff; color: #6b21a8; border: 1px dashed #c084fc;' } + }; + + generatorForm.addEventListener('submit', (e) => { + e.preventDefault(); // Bloque le rechargement de la page pour préserver le cache local + + const type = document.getElementById('gen-type').value; + const title = document.getElementById('gen-title').value; + const config = typeDictionary[type]; + const uniqueId = `w-generated-${Date.now()}`; // Identifiant unique basé sur le timestamp + + // Nettoyage de la zone de production + productionZone.innerHTML = ''; + + // Création du nœud DOM du badge + const badge = document.createElement('div'); + badge.id = uniqueId; + badge.className = `workout ${config.cssClass} rym-draggable-workout`; + badge.setAttribute('draggable', 'true'); - dropZones.forEach(zone => { - totalSéances += zone.querySelectorAll(".rym-draggable-workout").length; + if (config.customStyle) { + badge.setAttribute('style', config.customStyle); + } + + // Injection du contenu structuré + badge.innerHTML = ` ${title}`; + + // Activation immédiate de la mécanique Drag sur le nouveau composant + attachDragEventsToElement(badge); + + // Rendu final dans l'interface + productionZone.appendChild(badge); + generatorForm.reset(); + console.log(`[Engine] Nouveau badge instancié avec succès ID: ${uniqueId}`); + }); + + + // ========================================== + // MODULE 3 : MOTEUR DRAG & DROP MULTI-CIBLES + // ========================================== + let draggedElement = null; + + /** + * Attache les écouteurs d'événements Drag à un élément unique + * @param {HTMLElement} element + */ + function attachDragEventsToElement(element) { + element.addEventListener('dragstart', (e) => { + draggedElement = element; + e.dataTransfer.setData('text/plain', element.id); + element.style.opacity = '0.4'; + console.log(`[Drag] Début du déplacement de l'élément : ${element.id}`); }); - // Calcul dynamique : 25% de charge par bloc actif, plafonné à 100% - const nouvelleCharge = Math.min(totalSéances * 25, 100); - - // Cibler le deuxième spinner du dashboard (Charge Hebdo) - const spinnerCharge = document.querySelectorAll(".rym-spinner-wrapper")[1]; - if (spinnerCharge) { - const cercleStroke = spinnerCharge.querySelector(".circle-stroke"); - const texteValeur = spinnerCharge.querySelector(".rym-spinner-text span"); - - texteValeur.textContent = nouvelleCharge; - cercleStroke.setAttribute("stroke-dasharray", `${nouvelleCharge}, 100`); - } + element.addEventListener('dragend', () => { + element.style.opacity = '1'; + draggedElement = null; + }); } - draggables.forEach(draggable => { - draggable.addEventListener("dragstart", (e) => { - e.dataTransfer.setData("text/plain", draggable.id); - setTimeout(() => { draggable.style.display = "none"; }, 0); - }); + // Initialisation du moteur Drag sur les cartes pré-existantes au boot + document.querySelectorAll('.rym-draggable-workout').forEach(attachDragEventsToElement); - draggable.addEventListener("dragend", () => { - draggable.style.display = "block"; - }); - }); + // Configuration des zones de réception (Les jours de la semaine) + const dropZones = document.querySelectorAll('.drop-zone'); dropZones.forEach(zone => { - zone.addEventListener("dragover", (e) => { - e.preventDefault(); - zone.classList.add("drag-over"); + zone.addEventListener('dragover', (e) => { + e.preventDefault(); // Indispensable pour autoriser le drop dans le navigateur + zone.classList.add('drag-over'); }); - zone.addEventListener("dragleave", () => { - zone.classList.remove("drag-over"); + zone.addEventListener('dragleave', () => { + zone.classList.remove('drag-over'); }); - zone.addEventListener("drop", (e) => { + zone.addEventListener('drop', (e) => { e.preventDefault(); - zone.classList.remove("drag-over"); - - const id = e.dataTransfer.getData("text/plain"); - const draggableElement = document.getElementById(id); - - if (draggableElement) { - zone.appendChild(draggableElement); - recalculerChargeHebdo(); + zone.classList.remove('drag-over'); + + if (draggedElement) { + // Déplacement physique du nœud dans le conteneur du jour ciblé + zone.appendChild(draggedElement); + + // Si le badge provient de la zone de production, on remet le texte d'attente + if (productionZone.children.length === 0) { + productionZone.innerHTML = 'Aucun badge généré'; + } + + console.log(`[Drop] Élément réassigné au jour : ${zone.getAttribute('data-day')}`); } }); }); - // Calcul initial - recalculerChargeHebdo(); }); diff --git a/index.html b/index.html index 1101329..6b47703 100644 --- a/index.html +++ b/index.html @@ -3,244 +3,261 @@ - RYM Horizon | Écosystème Souverain - - + RYM Horizon — Cockpit Intégré v3.1.3 + + -
-
- - - -
- -
-

MCO Dashboard

-
-
Status Infra
-

● Online - France (OVH)

-
-
- -
-

RYM Bank

-
-
Solde Courant
-

12 450,80 €

-
-
- -
-
-

RYM Coach | Mon Planning

- Saison : Triathlon -
- -
-
Indicateurs de Performance & Souveraineté
-
- -
-
- - - - -
- 82% -
-
-
Fraîcheur VRC
-
- -
-
- - - - -
- 65% -
-
-
Charge Hebdo
-
- -
-
- - - - -
- 45% -
-
-
Chaîne Vélo
-
- -
-
- - - - -
- 90% -
-
-
Paires Carbone
-
- -
-
- -
-
Vue Hebdomadaire Dynamique
-
- -
-
-
Lundi
-
- Natation : 2500m -
-
-
- -
-
-
Mardi
-
- VMA Balayage -
-
-
- -
-
-
Mercredi
-
- Vélo Tempête -
-
-
- -
-
-
Jeudi
-
-
- -
-
-
Vendredi
-
-
- -
-
-
Samedi
-
-
- -
-
-
Dimanche
-
-
- -
-
- -
-
Agenda Précision Premium
-

Zone réservée aux flux haute précision asynchrones.

-
-
- -
-
-

Espace Expert | Dashboard Cohorte

-
- - -
-
- -
-
-
- - - - - - - - - - -
AthlèteSantéSync APIAction
Jean Dupont
Ironman Nice 2026
Optimal
-
-
- -
-
-
Bilan Anthropométrique (Cible)
-
-
Poids: 74.5 kg
Âge: 32
-
-
Cou: 39cm
Poitrine: 104cm
-
Hanches: 92cm
Taille: 80cm
-
Cuisse (D/G): 58/58
Mollet (D/G): 39/39
-
-
- Données Chiffrées (Souveraineté RYM) -
-
-
-
-
- -
+
+
+
+ RymHorizon + +
+
+
+ RYM BANK + 19 420,00 € +
+ +
-
+ + +
+ +
+
+
+
+
+ +
12h
+
+
EntraînementCharge Hebdo
+
+
+
+ +
82%
+
+
RécupérationVRC / Sommeil
+
+
+
+ +
75%
+
+
NutritionMacro Cibles
+
+
+
+ +
2.5L
+
+
HydratationEau / Électro
+
+
+
+ +
Kiné
+
+
Soins / SantéFatigue Musc.
+
+
+
+ +
22%
+
+
MatérielMCO Critique
+
+
+
+
+ +
+ +
+
+
+

Aujourd'hui, Samedi 23 mai

+
+ MATIN +
Natation - 1h15 Endurance (2500m)
+
+
+ APRÈS-MIDI +

Repos actif / Préparation vélo

+
+
+ SOIR +
Course - 45' Footing souple
+
+
+
+
+ +
+
+
+
+
RYM Coach Engine
+

Créez une activité ou une tâche de maintenance et planifiez-la par Drag & Drop.

+
+ +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ Aucun badge généré +
+
+
+
+ +
+
+

Timeline Hebdomadaire

+ Semaine 21 — Bloc Intensité +
+
+
+ Lundi 18 + Repos +
😴 Assimilation
+
+
+ Mardi 19 + Haute Intensité +
+ 6x800m VMA +
+
+
+ Mercredi 20 (Aujourd'hui) + Bi-quotidien +
+ Swim 2500m +
+
+ H.Trainer 1h +
+
+
+ Jeudi 21 + Récupération +
+ Footing 40' +
+
+
+ Vendredi 22 + Repos +
🧘 Flexibilité
+
+
+ Samedi 23 + Sortie Longue +
+
+ Dimanche 24 + Enchaînement +
+
+
+
+
+ +
+
+
+
+
Semaine 1
+
+ Focus : Volume Foncier +
+
+
+
+
Semaine 2
+
+ Focus : Charge Progressive +
+
+
+
+
Semaine 3
+
+ Focus : Assimilation / Repos +
+
+
+
+
Semaine 4
+
+ Focus : Intensité Cible +
+
+
+
+ +
+
+
+
+

Saison 2026 : Road to Ironman Vitoria

+

Cycle de 24 semaines — Actuellement en Phase de Spécificité 1.

+
+ Prep Foncière + Prep Spécifique + Affûtage +
+
+
+
J - 142
+ Avant l'objectif A +
+
+
+
+ +
+
- diff --git a/style.css b/style.css index ad9f7d7..174741f 100644 --- a/style.css +++ b/style.css @@ -1,150 +1,170 @@ -/* Variables de Design System RYM */ -:root { - --rym-main: #004a99; - --rym-dark: #121416; - --rym-accent: #00d1b2; - --rym-premium: #ffd700; - --rym-bg: #f0f2f5; +:root { + --rym-bg: #f8fafc; + --rym-surface: #ffffff; + --rym-accent: #2563eb; + --rym-text: #0f172a; + --rym-muted: #64748b; + --rym-stroke: #e2e8f0; } -/* Base App Layout */ -body { - background-color: var(--rym-bg); - font-family: 'Inter', sans-serif; - font-size: 0.9rem; +body { + background-color: var(--rym-bg); + color: var(--rym-text); + font-family: 'Inter', -apple-system, sans-serif; + letter-spacing: -0.01em; } -/* Navigation & Sidebar UI */ -.sidebar { - min-height: 100vh; - background: var(--rym-dark); - color: white; +/* Nav & Tabs Switcheur du haut */ +.nav-view-switcher { + background: #e2e8f0; + padding: 4px; + border-radius: 12px; + display: inline-flex; } -.sidebar-heading { - font-size: 0.7rem; - font-weight: 700; - text-transform: uppercase; - color: #495057; - margin-top: 1.5rem; - padding-bottom: 5px; - border-bottom: 1px solid #343a40; +.nav-view-switcher .nav-link { + border-radius: 10px; + color: var(--rym-muted); + font-weight: 600; + font-size: 0.85rem; + padding: 8px 16px; + border: none; + background: none; + transition: all 0.2s ease; } -.nav-link { - color: #8a8d91; - border-radius: 10px; - padding: 10px 15px; - margin-bottom: 4px; - cursor: pointer; - transition: background 0.2s ease, color 0.2s ease; +.nav-view-switcher .nav-link.active { + background: var(--rym-surface); + color: var(--rym-text); + box-shadow: 0 2px 4px rgba(0,0,0,0.05); } -.nav-link:hover { - color: white; - background: rgba(255, 255, 255, 0.05); -} - -.nav-link.active { - background: var(--rym-main); - color: white; -} - -/* UI Components */ -.card-rym { - border: none; - border-radius: 18px; - box-shadow: 0 4px 20px rgba(0,0,0,0.05); - background: white; -} - -.premium-zone { - border: 2px solid var(--rym-premium); - position: relative; -} - -/* Agenda Components */ -.agenda-scroll { - max-height: 250px; - overflow-y: auto; -} - -.hour-row { - border-bottom: 1px solid #eee; - min-height: 40px; - display: flex; - align-items: center; -} - -/* Router / Navigation Tabs States */ -.tab-content { - display: none; -} - -.tab-content.active { - display: block; - animation: fadeIn 0.3s forwards; -} - -/* Animations */ -@keyframes fadeIn { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } -} - -/* --- CONFIGURATION DES SPINNERS (OPTION 2) --- */ -.rym-spinner-wrapper { +/* Cockpit Spinners Originaux */ +.spinner-container { + width: 60px; + height: 60px; position: relative; - width: 90px; - height: 90px; - margin: 0 auto; } -.rym-spinner-svg { - width: 100%; - height: 100%; +.spinner-container svg { transform: rotate(-90deg); } -.circle-bg { - fill: none; - stroke: #e6e6e6; - stroke-width: 2.8; +.spinner-container circle { + transition: stroke-dashoffset 0.3s ease; } -.circle-stroke { - fill: none; - stroke-width: 2.8; - stroke-linecap: round; - transition: stroke-dasharray 0.5s ease; -} - -.rym-spinner-text { +.spinner-value { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); - text-align: center; + font-size: 0.75rem; + font-weight: 700; } -/* --- DRAG AND DROP KANBAN (OPTION 1) --- */ -.drop-zone { - min-height: 140px; +/* Timeline Hebdo Défilante */ +.timeline-scroll { + display: flex; + gap: 1.25rem; + overflow-x: auto; + padding: 10px 0 20px; + scroll-snap-type: x mandatory; +} + +.day-card { + min-width: 280px; + background: var(--rym-surface); + border: 1px solid var(--rym-stroke); + border-radius: 20px; + padding: 1.5rem; + scroll-snap-align: start; transition: background-color 0.2s ease, border-color 0.2s ease; } +.day-card.today { + border-color: var(--rym-accent); + border-width: 2px; +} + +/* Drag & Drop Zones visual feedback */ .drop-zone.drag-over { - background-color: #e2e8f0 !important; - border: 2px dashed #0dcaf0 !important; + background-color: #f1f5f9 !important; + border: 2px dashed var(--rym-accent) !important; } .rym-draggable-workout { cursor: grab; - transition: transform 0.1s ease, opacity 0.1s ease; user-select: none; } .rym-draggable-workout:active { cursor: grabbing; - opacity: 0.5; + opacity: 0.4; +} + +/* Vue Journalière (Matin/Midi/Soir) */ +.day-part-card { + border-left: 3px solid var(--rym-stroke); + padding-left: 15px; + margin-bottom: 20px; + position: relative; +} + +.day-part-card::before { + content: ''; + position: absolute; + left: -6px; + top: 0; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--rym-stroke); +} + +.day-part-active { + border-left-color: var(--rym-accent); +} + +.day-part-active::before { + background: var(--rym-accent); +} + +/* Badges de Séances d'endurance */ +.workout { + border-radius: 12px; + padding: 10px; + font-size: 0.9rem; + margin-top: 10px; + font-weight: 500; +} + +.run { background: #fff7ed; color: #9a3412; } +.bike { background: #f0f9ff; color: #075985; } +.swim { background: #f0fdfa; color: #115e59; } + +/* Carte Macro Annu */ +.macro-card { + background: linear-gradient(135deg, #1e293b, #0f172a); + color: white; + border-radius: 20px; + padding: 2rem; +} + +/* --- MOTEUR DE NAVIGATION SOUVERAIN (FIX DE L'AFFICHAGE) --- */ +.tab-pane { + display: none !important; + opacity: 0; +} + +/* Force l'affichage et l'opacité à 1 quand l'onglet est actif */ +.tab-pane.active { + display: block !important; + opacity: 1 !important; + animation: rymFadeIn 0.25s ease-out forwards; +} + +@keyframes rymFadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } }