@atlashub/smartstack-cli 4.41.0 → 4.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.documentation/apex.html +2 -2
  2. package/.documentation/business-analyse.html +26 -27
  3. package/.documentation/commands.html +6 -6
  4. package/dist/index.js +24 -13
  5. package/dist/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/templates/agents/ba-reader.md +2 -2
  8. package/templates/agents/ba-writer.md +44 -9
  9. package/templates/hooks/stop-hook.sh +6 -6
  10. package/templates/ralph/README.md +1 -1
  11. package/templates/scripts/setup-ralph-loop.sh +2 -2
  12. package/templates/skills/_resources/context-digest-template.md +1 -1
  13. package/templates/skills/_shared.md +13 -13
  14. package/templates/skills/apex/SKILL.md +14 -7
  15. package/templates/skills/apex/_shared.md +1 -1
  16. package/templates/skills/apex/references/challenge-questions.md +46 -13
  17. package/templates/skills/apex/references/core-seed-data.md +4 -4
  18. package/templates/skills/apex/references/error-classification.md +3 -3
  19. package/templates/skills/apex/references/smartstack-api.md +1 -1
  20. package/templates/skills/apex/references/smartstack-layers.md +1 -1
  21. package/templates/skills/apex/steps/step-00-init.md +46 -8
  22. package/templates/skills/apex/steps/step-01-analyze.md +1 -1
  23. package/templates/skills/apex/steps/step-02-plan.md +1 -1
  24. package/templates/skills/apex/steps/step-03-execute.md +1 -1
  25. package/templates/skills/business-analyse/SKILL.md +83 -22
  26. package/templates/skills/business-analyse/_shared.md +12 -9
  27. package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +13 -0
  28. package/templates/skills/business-analyse/questionnaire/03-data-ui.md +33 -0
  29. package/templates/skills/business-analyse/questionnaire/04-risks-metrics.md +1 -1
  30. package/templates/skills/business-analyse/react/components.md +1 -1
  31. package/templates/skills/business-analyse/react/schema.md +1 -1
  32. package/templates/skills/business-analyse/references/acceptance-criteria.md +3 -3
  33. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +1 -1
  34. package/templates/skills/business-analyse/references/detection-strategies.md +2 -2
  35. package/templates/skills/business-analyse/references/entity-architecture-decision.md +1 -1
  36. package/templates/skills/business-analyse/references/naming-conventions.md +6 -6
  37. package/templates/skills/business-analyse/references/robustness-checks.md +4 -4
  38. package/templates/skills/business-analyse/references/spec-auto-inference.md +2 -2
  39. package/templates/skills/business-analyse/references/validation-checklist.md +3 -3
  40. package/templates/skills/business-analyse/schemas/feature-schema.json +1 -1
  41. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +2 -2
  42. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +3 -2
  43. package/templates/skills/business-analyse/steps/step-00-init.md +15 -5
  44. package/templates/skills/business-analyse/steps/step-01-cadrage.md +14 -5
  45. package/templates/skills/business-analyse/steps/step-02-structure.md +17 -1
  46. package/templates/skills/business-analyse/steps/step-03-specify.md +136 -26
  47. package/templates/skills/business-analyse/steps/step-04-consolidate.md +44 -8
  48. package/templates/skills/business-analyse/templates/tpl-handoff.md +5 -5
  49. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +4 -4
  50. package/templates/skills/business-analyse/templates-frd.md +4 -4
  51. package/templates/skills/{ba-design-ui → business-analyse-design}/SKILL.md +9 -9
  52. package/templates/skills/{ba-design-ui → business-analyse-design}/steps/step-01-screens.md +9 -0
  53. package/templates/skills/{ba-design-ui → business-analyse-design}/steps/step-03-navigation.md +9 -2
  54. package/templates/skills/business-analyse-develop/SKILL.md +248 -0
  55. package/templates/skills/{ralph-loop → business-analyse-develop}/references/category-completeness.md +1 -1
  56. package/templates/skills/{ralph-loop → business-analyse-develop}/references/init-resume-recovery.md +8 -8
  57. package/templates/skills/{ralph-loop → business-analyse-develop}/references/multi-module-queue.md +1 -1
  58. package/templates/skills/business-analyse-develop/references/quality-gates.md +70 -0
  59. package/templates/skills/{ralph-loop → business-analyse-develop}/references/task-transform-legacy.md +1 -1
  60. package/templates/skills/{ralph-loop → business-analyse-develop}/steps/step-00-init.md +20 -4
  61. package/templates/skills/{ralph-loop → business-analyse-develop}/steps/step-01-task.md +3 -2
  62. package/templates/skills/business-analyse-develop/steps/step-01-v4-execute.md +131 -0
  63. package/templates/skills/business-analyse-develop/steps/step-02-v4-verify.md +156 -0
  64. package/templates/skills/{ralph-loop → business-analyse-develop}/steps/step-04-check.md +1 -1
  65. package/templates/skills/{ralph-loop → business-analyse-develop}/steps/step-05-report.md +1 -1
  66. package/templates/skills/{derive-prd → business-analyse-handoff}/SKILL.md +7 -7
  67. package/templates/skills/{derive-prd → business-analyse-handoff}/references/acceptance-criteria.md +5 -5
  68. package/templates/skills/{derive-prd → business-analyse-handoff}/references/handoff-file-templates.md +1 -1
  69. package/templates/skills/{derive-prd → business-analyse-handoff}/references/handoff-mappings.md +1 -1
  70. package/templates/skills/{derive-prd → business-analyse-handoff}/references/handoff-seeddata-generation.md +2 -2
  71. package/templates/skills/{derive-prd → business-analyse-handoff}/references/prd-generation.md +14 -14
  72. package/templates/skills/{derive-prd → business-analyse-handoff}/schemas/handoff-schema.json +2 -2
  73. package/templates/skills/{derive-prd → business-analyse-handoff}/steps/step-00-validate.md +6 -6
  74. package/templates/skills/{derive-prd → business-analyse-handoff}/steps/step-01-transform.md +46 -7
  75. package/templates/skills/{derive-prd → business-analyse-handoff}/steps/step-02-export.md +34 -14
  76. package/templates/skills/{ba-generate-html → business-analyse-html}/SKILL.md +4 -4
  77. package/templates/skills/{ba-generate-html → business-analyse-html}/html/ba-interactive.html +709 -277
  78. package/templates/skills/{ba-generate-html → business-analyse-html}/html/build-html.js +25 -3
  79. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/01-data-init.js +54 -0
  80. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/02-navigation.js +97 -3
  81. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/03-render-cadrage.js +8 -7
  82. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/04-render-modules.js +7 -7
  83. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/05-render-specs.js +188 -85
  84. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/06-render-consolidation.js +15 -14
  85. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/06-render-mockups.js +19 -19
  86. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/07-render-handoff.js +24 -4
  87. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/08-editing.js +6 -2
  88. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/09-export.js +27 -57
  89. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/10-comments.js +67 -45
  90. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/scripts/11-review-panel.js +15 -13
  91. package/templates/skills/business-analyse-html/html/src/styles/02-layout.css +216 -0
  92. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/05-modules.css +36 -0
  93. package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/template.html +22 -12
  94. package/templates/skills/{ba-generate-html → business-analyse-html}/references/data-build.md +1 -1
  95. package/templates/skills/{ba-generate-html → business-analyse-html}/references/data-mapping.md +5 -1
  96. package/templates/skills/{ba-generate-html → business-analyse-html}/references/output-modes.md +7 -7
  97. package/templates/skills/{ba-generate-html → business-analyse-html}/steps/step-01-collect.md +25 -1
  98. package/templates/skills/{ba-generate-html → business-analyse-html}/steps/step-02-build-data.md +33 -5
  99. package/templates/skills/{ba-generate-html → business-analyse-html}/steps/step-03-render.md +2 -2
  100. package/templates/skills/{ba-generate-html → business-analyse-html}/steps/step-04-verify.md +2 -2
  101. package/templates/skills/{ba-review → business-analyse-review}/SKILL.md +11 -10
  102. package/templates/skills/{ba-review → business-analyse-review}/references/review-data-mapping.md +2 -2
  103. package/templates/skills/business-analyse-review/steps/step-00-init.md +107 -0
  104. package/templates/skills/{ba-review → business-analyse-review}/steps/step-01-apply.md +19 -11
  105. package/templates/skills/business-analyse-status/SKILL.md +118 -0
  106. package/templates/skills/documentation/SKILL.md +2 -2
  107. package/templates/skills/sketch/SKILL.md +172 -0
  108. package/templates/skills/sketch/references/domain-heuristics.md +116 -0
  109. package/templates/skills/ba-generate-html/html/src/styles/02-layout.css +0 -101
  110. package/templates/skills/ralph-loop/SKILL.md +0 -240
  111. /package/templates/skills/{ba-design-ui → business-analyse-design}/steps/step-02-wireframes.md +0 -0
  112. /package/templates/skills/{ralph-loop → business-analyse-develop}/references/category-rules.md +0 -0
  113. /package/templates/skills/{ralph-loop → business-analyse-develop}/references/compact-loop.md +0 -0
  114. /package/templates/skills/{ralph-loop → business-analyse-develop}/references/module-transition.md +0 -0
  115. /package/templates/skills/{ralph-loop → business-analyse-develop}/references/parallel-execution.md +0 -0
  116. /package/templates/skills/{ralph-loop → business-analyse-develop}/references/section-splitting.md +0 -0
  117. /package/templates/skills/{ralph-loop → business-analyse-develop}/references/team-orchestration.md +0 -0
  118. /package/templates/skills/{ralph-loop → business-analyse-develop}/steps/step-02-execute.md +0 -0
  119. /package/templates/skills/{ralph-loop → business-analyse-develop}/steps/step-03-commit.md +0 -0
  120. /package/templates/skills/{derive-prd → business-analyse-handoff}/references/entity-domain-mapping.md +0 -0
  121. /package/templates/skills/{derive-prd → business-analyse-handoff}/references/readiness-scoring.md +0 -0
  122. /package/templates/skills/{derive-prd → business-analyse-handoff}/templates/tpl-progress.md +0 -0
  123. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/cadrage-context.html +0 -0
  124. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/cadrage-scope.html +0 -0
  125. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/cadrage-stakeholders.html +0 -0
  126. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/cadrage-success.html +0 -0
  127. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/consol-datamodel.html +0 -0
  128. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/consol-flows.html +0 -0
  129. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/consol-interactions.html +0 -0
  130. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/consol-permissions.html +0 -0
  131. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/decomp-dependencies.html +0 -0
  132. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/decomp-modules.html +0 -0
  133. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/handoff-summary.html +0 -0
  134. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/partials/module-spec-container.html +0 -0
  135. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/01-variables.css +0 -0
  136. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/03-navigation.css +0 -0
  137. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/04-cards.css +0 -0
  138. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/06-wireframes.css +0 -0
  139. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/07-comments.css +0 -0
  140. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/08-review-panel.css +0 -0
  141. /package/templates/skills/{ba-generate-html → business-analyse-html}/html/src/styles/09-mockups-html.css +0 -0
  142. /package/templates/skills/{ba-generate-html → business-analyse-html}/references/wireframe-svg-style-guide.md +0 -0
@@ -128,25 +128,140 @@ body {
128
128
  /* ============================================
129
129
  RESPONSIVE
130
130
  ============================================ */
131
+ .hamburger-btn {
132
+ display: none;
133
+ background: none;
134
+ border: 1px solid var(--border);
135
+ border-radius: 6px;
136
+ color: var(--text);
137
+ font-size: 1.2rem;
138
+ padding: 0.2rem 0.5rem;
139
+ cursor: pointer;
140
+ flex-shrink: 0;
141
+ }
142
+ .sidebar-overlay {
143
+ display: none;
144
+ position: fixed;
145
+ inset: 0;
146
+ background: rgba(0,0,0,0.5);
147
+ z-index: 149;
148
+ }
149
+ .sidebar-overlay.visible { display: block; }
150
+
131
151
  @media (max-width: 768px) {
132
- .sidebar { display: none; }
152
+ .hamburger-btn { display: block; }
153
+ .sidebar {
154
+ display: none;
155
+ position: fixed;
156
+ top: 0;
157
+ left: 0;
158
+ height: 100vh;
159
+ z-index: 150;
160
+ box-shadow: 4px 0 24px rgba(0,0,0,0.3);
161
+ }
162
+ .sidebar.mobile-open { display: block; }
133
163
  .main { padding: 1rem; }
134
164
  .form-row { grid-template-columns: 1fr; }
135
165
  .stakeholder-grid { grid-template-columns: 1fr; }
166
+ .module-grid { grid-template-columns: 1fr; }
167
+ .stat-grid { grid-template-columns: repeat(2, 1fr); }
168
+ .dm-entity-grid { grid-template-columns: 1fr; }
169
+ .mock-form-row { grid-template-columns: 1fr; }
170
+ .mock-kpi-grid { grid-template-columns: repeat(2, 1fr); }
171
+ .header-actions { gap: 0.25rem; }
172
+ .header-actions .btn-sm { padding: 0.2rem 0.4rem; font-size: 0.7rem; }
173
+ .header-title { display: none; }
174
+ .header-sep { display: none; }
175
+ }
176
+
177
+ /* ============================================
178
+ SEARCH
179
+ ============================================ */
180
+ .sidebar-search {
181
+ padding: 0.5rem 0.75rem;
182
+ border-bottom: 1px solid var(--border);
183
+ }
184
+ .sidebar-search input {
185
+ width: 100%;
186
+ padding: 0.4rem 0.6rem;
187
+ background: var(--bg-input);
188
+ border: 1px solid var(--border);
189
+ border-radius: 6px;
190
+ color: var(--text);
191
+ font-size: 0.8rem;
192
+ font-family: inherit;
193
+ }
194
+ .sidebar-search input:focus {
195
+ outline: none;
196
+ border-color: var(--primary);
197
+ }
198
+ .nav-item.search-hidden { display: none; }
199
+ .nav-group.search-hidden { display: none; }
200
+ .search-highlight { background: rgba(234,179,8,0.3); border-radius: 2px; }
201
+
202
+ /* ============================================
203
+ PROGRESS INDICATOR
204
+ ============================================ */
205
+ .progress-bar-container {
206
+ width: 100%;
207
+ height: 8px;
208
+ background: var(--bg-hover);
209
+ border-radius: 4px;
210
+ overflow: hidden;
211
+ margin-bottom: 0.5rem;
212
+ }
213
+ .progress-bar-fill {
214
+ height: 100%;
215
+ background: linear-gradient(90deg, var(--primary), var(--success));
216
+ border-radius: 4px;
217
+ transition: width 0.5s ease;
218
+ }
219
+ .progress-label {
220
+ font-size: 0.8rem;
221
+ color: var(--text-muted);
222
+ margin-bottom: 0.75rem;
136
223
  }
224
+ .progress-checks {
225
+ display: grid;
226
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
227
+ gap: 0.3rem;
228
+ }
229
+ .progress-check {
230
+ font-size: 0.8rem;
231
+ color: var(--text-muted);
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 0.4rem;
235
+ }
236
+ .progress-check.done { color: var(--success); }
237
+ .progress-check-icon { font-size: 0.9rem; width: 16px; text-align: center; }
137
238
 
138
239
  /* ============================================
139
240
  PRINT
140
241
  ============================================ */
141
242
  @media print {
142
- .sidebar, .header-actions, .add-btn, .uc-actions, .inline-form, .module-card-remove { display: none !important; }
143
- .main { max-width: 100%; padding: 0; }
144
- .section { display: block !important; page-break-inside: avoid; }
145
- body { background: #fff; color: #1a1a1a; }
146
- .card, .uc-item, .br-item, .module-card, .entity-block, .interaction-item { border-color: #ddd; }
147
- .tab-panel { display: block !important; }
148
- .tab-bar { display: none; }
149
- .review-panel, .comment-btn-container { display: none !important; }
243
+ .sidebar, .header-actions, .add-btn, .uc-actions, .inline-form, .module-card-remove, .hamburger-btn, .sidebar-search { display: none !important; }
244
+ .main { max-width: 100%; padding: 0.5cm; height: auto; overflow: visible; }
245
+ .body { display: block; }
246
+ .section { display: block !important; page-break-inside: avoid; margin-bottom: 1cm; }
247
+ .section + .section { page-break-before: auto; }
248
+ body { background: #fff !important; color: #1a1a1a !important; }
249
+ * { color-adjust: exact; -webkit-print-color-adjust: exact; }
250
+ .card, .uc-item, .br-item, .module-card, .entity-block, .interaction-item {
251
+ border-color: #ddd !important;
252
+ background: #fff !important;
253
+ color: #1a1a1a !important;
254
+ }
255
+ .card-label, .uc-detail-label, .section-subtitle, .form-label { color: #666 !important; }
256
+ .section-title { color: #1a1a1a !important; border-bottom-color: #333 !important; }
257
+ .tab-panel { display: block !important; page-break-inside: avoid; }
258
+ .tab-bar { display: none !important; }
259
+ .review-panel, .comment-btn-container, .notification, .wireframe-comment, [contenteditable] { display: none !important; }
260
+ .editable { border: none !important; padding: 0 !important; }
261
+ .header { position: static; background: #fff !important; border-bottom-color: #ddd !important; }
262
+ .header-logo { background: #333 !important; }
263
+ .header-title, .header-app-name { color: #1a1a1a !important; }
264
+ a { text-decoration: none !important; color: inherit !important; }
150
265
  }
151
266
 
152
267
 
@@ -916,6 +1031,42 @@ body {
916
1031
  color: var(--text-muted);
917
1032
  }
918
1033
 
1034
+ /* ============================================
1035
+ SECTION GROUPS (Hierarchical Mode)
1036
+ ============================================ */
1037
+ .section-group {
1038
+ margin-bottom: 1.5rem;
1039
+ border-left: 3px solid var(--primary);
1040
+ padding-left: 0;
1041
+ }
1042
+ .section-group-header {
1043
+ display: flex;
1044
+ align-items: center;
1045
+ gap: 0.5rem;
1046
+ padding: 0.5rem 0.75rem;
1047
+ background: rgba(99,102,241,0.05);
1048
+ border-bottom: 1px solid var(--border);
1049
+ margin-bottom: 0.5rem;
1050
+ border-radius: 0 6px 0 0;
1051
+ }
1052
+ .section-group-icon {
1053
+ font-size: 0.6rem;
1054
+ color: var(--primary-light);
1055
+ }
1056
+ .section-group-label {
1057
+ font-weight: 600;
1058
+ color: var(--primary-light);
1059
+ font-size: 0.9rem;
1060
+ }
1061
+ .section-group-route {
1062
+ font-size: 0.7rem;
1063
+ color: var(--text-muted);
1064
+ font-family: 'Courier New', monospace;
1065
+ background: var(--bg-input);
1066
+ padding: 0.1rem 0.4rem;
1067
+ border-radius: 3px;
1068
+ }
1069
+
919
1070
 
920
1071
  /* --- 06-wireframes.css --- */
921
1072
  /* ============================================
@@ -1768,20 +1919,21 @@ body {
1768
1919
  <!-- ============================================
1769
1920
  HEADER
1770
1921
  ============================================ -->
1771
- <header class="header">
1772
- <div class="header-logo">BA</div>
1922
+ <header class="header" role="banner">
1923
+ <button class="hamburger-btn" onclick="toggleMobileSidebar()" aria-label="Ouvrir le menu de navigation">&#9776;</button>
1924
+ <div class="header-logo" aria-hidden="true">BA</div>
1773
1925
  <span class="header-title">Analyse métier</span>
1774
1926
  <div class="header-sep"></div>
1775
1927
  <span class="header-app-name" id="appName">{{APPLICATION_NAME}}</span>
1776
1928
  <div class="header-spacer"></div>
1777
1929
  <div class="header-actions">
1778
- <button class="btn btn-sm" onclick="resetToEmbedded()" title="Réinitialiser depuis les données d'origine (supprime les modifications locales)">Reset</button>
1779
- <button class="btn btn-sm" onclick="saveToLocalStorage()" title="Sauvegarder les modifications dans le navigateur">Sauvegarder</button>
1780
- <button class="btn btn-sm btn-review" onclick="saveReviewJSON()" title="Sauvegarder les corrections pour créer une nouvelle version">Sauvegarder corrections</button>
1781
- <button class="btn btn-sm btn-primary" onclick="exportJSON()" title="Exporter les données au format JSON pour l'extraction">Exporter JSON</button>
1782
- <button class="btn btn-sm review-toggle-btn" id="reviewToggleBtn" onclick="toggleReviewPanel()" title="Ouvrir/fermer le panneau de review">
1930
+ <button class="btn btn-sm" onclick="resetToEmbedded()" title="Réinitialiser depuis les données d'origine (supprime les modifications locales)" aria-label="Réinitialiser les données">Reset</button>
1931
+ <button class="btn btn-sm" onclick="saveToLocalStorage()" title="Sauvegarder les modifications dans le navigateur" aria-label="Sauvegarder">Sauvegarder</button>
1932
+ <button class="btn btn-sm btn-review" onclick="saveReviewJSON()" title="Sauvegarder les corrections pour créer une nouvelle version" aria-label="Sauvegarder les corrections">Sauvegarder corrections</button>
1933
+ <button class="btn btn-sm btn-primary" onclick="exportJSON()" title="Exporter les données au format JSON pour l'extraction" aria-label="Exporter en JSON">Exporter JSON</button>
1934
+ <button class="btn btn-sm review-toggle-btn" id="reviewToggleBtn" onclick="toggleReviewPanel()" title="Ouvrir/fermer le panneau de review" aria-label="Panneau de review">
1783
1935
  Review
1784
- <span class="review-badge hidden" id="reviewBadge">0</span>
1936
+ <span class="review-badge hidden" id="reviewBadge" aria-live="polite">0</span>
1785
1937
  </button>
1786
1938
  </div>
1787
1939
  </header>
@@ -1790,11 +1942,16 @@ body {
1790
1942
  <!-- ============================================
1791
1943
  SIDEBAR - Navigation hiérarchique
1792
1944
  ============================================ -->
1793
- <aside class="sidebar">
1945
+ <div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleMobileSidebar()"></div>
1946
+ <aside class="sidebar" id="sidebarAside" role="navigation" aria-label="Navigation principale">
1794
1947
  <!-- Application Name -->
1795
1948
  <div class="sidebar-header">
1796
1949
  <span class="sidebar-app-name" id="sidebarAppName">{{APPLICATION_NAME}}</span>
1797
1950
  </div>
1951
+ <!-- Search -->
1952
+ <div class="sidebar-search">
1953
+ <input type="search" id="sidebarSearchInput" placeholder="Rechercher..." oninput="filterNavItems(this.value)" aria-label="Rechercher dans le document">
1954
+ </div>
1798
1955
  <!-- Dynamic Tree Navigation -->
1799
1956
  <div id="sidebarNav">
1800
1957
  <!-- Populated by buildNavTree() -->
@@ -1804,7 +1961,7 @@ body {
1804
1961
  <!-- ============================================
1805
1962
  MAIN CONTENT
1806
1963
  ============================================ -->
1807
- <main class="main" id="mainContent">
1964
+ <main class="main" id="mainContent" role="main">
1808
1965
 
1809
1966
  <!-- SECTION: Contexte (merged: problem + current + vision) -->
1810
1967
  <div class="section" id="cadrage-context">
@@ -2125,6 +2282,10 @@ body {
2125
2282
  <h2 class="section-title">Synthèse de l'analyse</h2>
2126
2283
  <p class="section-subtitle">Vue d'ensemble de toute l'analyse métier, prête pour le développement.</p>
2127
2284
 
2285
+ <div class="card" style="margin-bottom:1.5rem;" id="progressIndicator">
2286
+ <!-- Populated by renderProgressIndicator() -->
2287
+ </div>
2288
+
2128
2289
  <div class="stat-grid" id="handoffStats">
2129
2290
  <!-- Populated dynamically -->
2130
2291
  </div>
@@ -2151,10 +2312,10 @@ body {
2151
2312
  <!-- ============================================
2152
2313
  REVIEW PANEL (right sidebar)
2153
2314
  ============================================ -->
2154
- <aside class="review-panel" id="reviewPanel">
2315
+ <aside class="review-panel" id="reviewPanel" role="complementary" aria-label="Panneau de review">
2155
2316
  <div class="review-panel-header">
2156
2317
  <span class="review-panel-title">Review</span>
2157
- <button class="review-panel-close" onclick="toggleReviewPanel()" title="Fermer">&times;</button>
2318
+ <button class="review-panel-close" onclick="toggleReviewPanel()" title="Fermer" aria-label="Fermer le panneau de review">&times;</button>
2158
2319
  </div>
2159
2320
  <div class="review-filters">
2160
2321
  <button class="review-filter-btn active" onclick="filterReviewComments('all')" data-filter="all">Tous</button>
@@ -2262,8 +2423,23 @@ data.moduleSpecs = data.moduleSpecs || {};
2262
2423
  if (!data.moduleSpecs[m.code].screens) {
2263
2424
  data.moduleSpecs[m.code].screens = [];
2264
2425
  }
2426
+ // Ensure sections have initialized arrays for hierarchical data
2427
+ m.anticipatedSections.forEach(function(section) {
2428
+ section.useCases = section.useCases || [];
2429
+ section.businessRules = section.businessRules || [];
2430
+ section.resources = section.resources || [];
2431
+ section.permission = section.permission || '';
2432
+ section.route = section.route || '';
2433
+ });
2265
2434
  });
2266
2435
 
2436
+ // Detect if modules use section-level specs (hierarchical mode)
2437
+ function hasHierarchicalSpecs(mod) {
2438
+ return (mod.anticipatedSections || []).some(function(s) {
2439
+ return (s.useCases && s.useCases.length > 0) || (s.businessRules && s.businessRules.length > 0);
2440
+ });
2441
+ }
2442
+
2267
2443
  // Defensive: normalize stakeholder tasks (string → array)
2268
2444
  (data.cadrage.stakeholders || []).forEach(function(s) {
2269
2445
  if (typeof s.tasks === 'string') {
@@ -2335,6 +2511,45 @@ function formatModuleType(t) {
2335
2511
  return { 'data-centric': 'Données', 'workflow': 'Processus', 'reporting': 'Rapports', 'integration': 'Intégration', 'full-module': 'Complet' }[t] || t;
2336
2512
  }
2337
2513
 
2514
+ function escapeHtml(s) {
2515
+ if (s == null) return '';
2516
+ var d = document.createElement('div');
2517
+ d.textContent = String(s);
2518
+ return d.innerHTML;
2519
+ }
2520
+
2521
+ function computeProgress() {
2522
+ var totalModules = data.modules.length;
2523
+ var modulesWithUC = 0, modulesWithBR = 0, modulesWithEntities = 0, modulesWithWireframes = 0;
2524
+ var totalUCs = 0, totalBRs = 0, totalEntities = 0;
2525
+
2526
+ data.modules.forEach(function(m) {
2527
+ var spec = data.moduleSpecs[m.code] || {};
2528
+ var ucs = (spec.useCases || []).length;
2529
+ var brs = (spec.businessRules || []).length;
2530
+ var ents = (spec.entities || []).length;
2531
+ var wfs = (EMBEDDED_ARTIFACTS?.wireframes?.[m.code] || []).length + (spec.screens || []).length;
2532
+ totalUCs += ucs; totalBRs += brs; totalEntities += ents;
2533
+ if (ucs > 0) modulesWithUC++;
2534
+ if (brs > 0) modulesWithBR++;
2535
+ if (ents > 0) modulesWithEntities++;
2536
+ if (wfs > 0) modulesWithWireframes++;
2537
+ });
2538
+
2539
+ var checks = [];
2540
+ if (totalModules > 0) checks.push({ done: true, label: 'Modules définis (' + totalModules + ')' });
2541
+ checks.push({ done: modulesWithUC === totalModules && totalModules > 0, label: 'Cas d\'utilisation (' + modulesWithUC + '/' + totalModules + ' modules)' });
2542
+ checks.push({ done: modulesWithBR === totalModules && totalModules > 0, label: 'Règles métier (' + modulesWithBR + '/' + totalModules + ' modules)' });
2543
+ checks.push({ done: modulesWithEntities === totalModules && totalModules > 0, label: 'Données (' + modulesWithEntities + '/' + totalModules + ' modules)' });
2544
+ checks.push({ done: modulesWithWireframes === totalModules && totalModules > 0, label: 'Maquettes (' + modulesWithWireframes + '/' + totalModules + ' modules)' });
2545
+ checks.push({ done: data.cadrage.stakeholders.length > 0, label: 'Parties prenantes (' + data.cadrage.stakeholders.length + ')' });
2546
+ checks.push({ done: (data.cadrage.scope.inscope || []).length > 0, label: 'Périmètre défini' });
2547
+
2548
+ var doneCount = checks.filter(function(c) { return c.done; }).length;
2549
+ var pct = checks.length > 0 ? Math.round(doneCount / checks.length * 100) : 0;
2550
+ return { checks: checks, doneCount: doneCount, total: checks.length, pct: pct, totalUCs: totalUCs, totalBRs: totalBRs, totalEntities: totalEntities };
2551
+ }
2552
+
2338
2553
  /* ============================================
2339
2554
  INITIALIZATION
2340
2555
  ============================================ */
@@ -2539,17 +2754,20 @@ function renderModuleNavItem(mod) {
2539
2754
  if (sections.length > 0) {
2540
2755
  sections.forEach(function(section) {
2541
2756
  var resources = section.resources || [];
2757
+ var sectionUCs = (section.useCases || []).length;
2758
+ var sectionBRs = (section.businessRules || []).length;
2759
+ var contentCount = sectionUCs + sectionBRs + resources.length;
2542
2760
  html += '<div class="nav-section-item">';
2543
2761
  html += '<a class="nav-item nav-section-link" onclick="showSection(\'module-spec-' + code + '\');switchTab(\'' + code + '\',\'struct\')" data-section="module-struct-' + code + '-' + section.code + '">';
2544
- html += '<span class="nav-icon nav-icon-section">&#9655;</span> ' + section.code;
2545
- if (resources.length > 0) html += ' <span class="nav-badge">' + resources.length + '</span>';
2762
+ html += '<span class="nav-icon nav-icon-section">&#9655;</span> ' + escapeHtml(section.code || section.name || '');
2763
+ if (contentCount > 0) html += ' <span class="nav-badge">' + contentCount + '</span>';
2546
2764
  html += '</a>';
2547
2765
  if (resources.length > 0) {
2548
2766
  html += '<div class="nav-children nav-resources">';
2549
2767
  resources.forEach(function(res) {
2550
2768
  var resName = typeof res === 'string' ? res : (res.code || res.name || '');
2551
2769
  html += '<a class="nav-item nav-resource-link">';
2552
- html += '<span class="nav-icon nav-icon-resource">&#8226;</span> ' + resName;
2770
+ html += '<span class="nav-icon nav-icon-resource">&#8226;</span> ' + escapeHtml(resName);
2553
2771
  html += '</a>';
2554
2772
  });
2555
2773
  html += '</div>';
@@ -2615,6 +2833,97 @@ function highlightActiveNavItem() {
2615
2833
  var navItem = document.querySelector('#sidebarNav [data-section="' + currentSectionId + '"]');
2616
2834
  if (navItem) navItem.classList.add('active');
2617
2835
  }
2836
+
2837
+ /* ---------- Mobile Sidebar ---------- */
2838
+
2839
+ function toggleMobileSidebar() {
2840
+ var sidebar = document.getElementById('sidebarAside');
2841
+ var overlay = document.getElementById('sidebarOverlay');
2842
+ if (!sidebar) return;
2843
+ var isOpen = sidebar.classList.contains('mobile-open');
2844
+ sidebar.classList.toggle('mobile-open', !isOpen);
2845
+ if (overlay) overlay.classList.toggle('visible', !isOpen);
2846
+ }
2847
+
2848
+ /* ---------- Search / Filter ---------- */
2849
+
2850
+ var _searchTimer;
2851
+ function filterNavItems(query) {
2852
+ clearTimeout(_searchTimer);
2853
+ _searchTimer = setTimeout(function() { doFilterNavItems(query); }, 200);
2854
+ }
2855
+
2856
+ function doFilterNavItems(query) {
2857
+ var q = (query || '').toLowerCase().trim();
2858
+ var nav = document.getElementById('sidebarNav');
2859
+ if (!nav) return;
2860
+
2861
+ // Clear previous highlights
2862
+ nav.querySelectorAll('.search-highlight').forEach(function(el) {
2863
+ el.outerHTML = el.textContent;
2864
+ });
2865
+
2866
+ if (!q) {
2867
+ // Show all items
2868
+ nav.querySelectorAll('.nav-item, .nav-group, .nav-module, .nav-section-item').forEach(function(el) {
2869
+ el.classList.remove('search-hidden');
2870
+ });
2871
+ return;
2872
+ }
2873
+
2874
+ // Filter nav items
2875
+ var allItems = nav.querySelectorAll('.nav-item');
2876
+ var visibleSections = new Set();
2877
+
2878
+ allItems.forEach(function(item) {
2879
+ var text = item.textContent.toLowerCase();
2880
+ if (text.includes(q)) {
2881
+ item.classList.remove('search-hidden');
2882
+ // Show parent groups
2883
+ var parent = item.parentElement;
2884
+ while (parent && parent.id !== 'sidebarNav') {
2885
+ parent.classList.remove('search-hidden');
2886
+ if (parent.querySelector(':scope > .nav-children')) {
2887
+ var children = parent.querySelector(':scope > .nav-children');
2888
+ if (children) children.style.display = '';
2889
+ }
2890
+ parent = parent.parentElement;
2891
+ }
2892
+ // Track section for content search
2893
+ var sectionId = item.dataset.section;
2894
+ if (sectionId) visibleSections.add(sectionId);
2895
+ } else {
2896
+ item.classList.add('search-hidden');
2897
+ }
2898
+ });
2899
+
2900
+ // Hide groups that have no visible children
2901
+ nav.querySelectorAll('.nav-group, .nav-module').forEach(function(group) {
2902
+ var visibleChildren = group.querySelectorAll('.nav-item:not(.search-hidden)');
2903
+ if (visibleChildren.length === 0) {
2904
+ group.classList.add('search-hidden');
2905
+ } else {
2906
+ group.classList.remove('search-hidden');
2907
+ }
2908
+ });
2909
+
2910
+ // Also search in section content and show matching sections
2911
+ document.querySelectorAll('.section').forEach(function(section) {
2912
+ var text = section.textContent.toLowerCase();
2913
+ if (text.includes(q) && !visibleSections.has(section.id)) {
2914
+ // Unhide corresponding nav item
2915
+ var navItem = nav.querySelector('[data-section="' + section.id + '"]');
2916
+ if (navItem) {
2917
+ navItem.classList.remove('search-hidden');
2918
+ var parent = navItem.parentElement;
2919
+ while (parent && parent.id !== 'sidebarNav') {
2920
+ parent.classList.remove('search-hidden');
2921
+ parent = parent.parentElement;
2922
+ }
2923
+ }
2924
+ }
2925
+ });
2926
+ }
2618
2927
 
2619
2928
 
2620
2929
  /* --- 03-render-cadrage.js --- */
@@ -2662,23 +2971,24 @@ function renderStakeholders() {
2662
2971
  grid.innerHTML = data.cadrage.stakeholders.map((s, i) => `
2663
2972
  <div class="stakeholder-card">
2664
2973
  <div style="display:flex;justify-content:space-between;align-items:start;">
2665
- <div class="stakeholder-role">${s.role}</div>
2974
+ <div class="stakeholder-role">${escapeHtml(s.role)}</div>
2666
2975
  <button class="btn btn-sm" onclick="removeStakeholder(${i})" style="opacity:0.5;font-size:0.7rem;">Supprimer</button>
2667
2976
  </div>
2668
- <div class="stakeholder-function">${s.function || ''}</div>
2977
+ <div class="stakeholder-function">${escapeHtml(s.function || '')}</div>
2669
2978
  <ul class="stakeholder-tasks">
2670
- ${(Array.isArray(s.tasks) ? s.tasks : typeof s.tasks === 'string' ? s.tasks.split(',').map(t => t.trim()).filter(Boolean) : []).map(t => '<li>' + t + '</li>').join('')}
2979
+ ${(Array.isArray(s.tasks) ? s.tasks : typeof s.tasks === 'string' ? s.tasks.split(',').map(t => t.trim()).filter(Boolean) : []).map(t => '<li>' + escapeHtml(t) + '</li>').join('')}
2671
2980
  </ul>
2672
2981
  <div class="stakeholder-meta">
2673
2982
  <span>${formatFrequency(s.frequency)}</span>
2674
2983
  <span>${formatAccess(s.access)}</span>
2675
2984
  </div>
2676
- ${s.frustrations ? '<div style="font-size:0.8rem;color:var(--warning);margin-top:0.5rem;font-style:italic;">' + s.frustrations + '</div>' : ''}
2985
+ ${s.frustrations ? '<div style="font-size:0.8rem;color:var(--warning);margin-top:0.5rem;font-style:italic;">' + escapeHtml(s.frustrations) + '</div>' : ''}
2677
2986
  </div>
2678
2987
  `).join('');
2679
2988
  }
2680
2989
 
2681
2990
  function removeStakeholder(index) {
2991
+ if (!confirm('Supprimer ce profil utilisateur ?')) return;
2682
2992
  data.cadrage.stakeholders.splice(index, 1);
2683
2993
  renderStakeholders();
2684
2994
  updateCounts();
@@ -2709,12 +3019,12 @@ function renderScope() {
2709
3019
  <div class="uc-item">
2710
3020
  <div class="uc-header">
2711
3021
  <span class="priority priority-${p}">${formatPriority(p)}</span>
2712
- <span class="uc-title">${item.name}</span>
3022
+ <span class="uc-title">${escapeHtml(item.name)}</span>
2713
3023
  <div class="uc-actions">
2714
3024
  <button class="btn btn-sm" onclick="removeScopeItem('${p}',${i})">Supprimer</button>
2715
3025
  </div>
2716
3026
  </div>
2717
- ${item.description ? '<div class="uc-detail">' + item.description + '</div>' : ''}
3027
+ ${item.description ? '<div class="uc-detail">' + escapeHtml(item.description) + '</div>' : ''}
2718
3028
  </div>
2719
3029
  `).join('');
2720
3030
  });
@@ -2759,7 +3069,7 @@ function renderCriteria() {
2759
3069
  container.innerHTML = criteria.map((c, i) => `
2760
3070
  <div class="uc-item" style="display:flex;align-items:center;gap:0.75rem;">
2761
3071
  <input type="checkbox" ${c.validated ? 'checked' : ''} onchange="toggleCriterion(${i})" style="cursor:pointer;width:18px;height:18px;flex-shrink:0;">
2762
- <span style="flex:1;${c.validated ? 'text-decoration:line-through;color:var(--text-muted);' : ''}">${c.text}</span>
3072
+ <span style="flex:1;${c.validated ? 'text-decoration:line-through;color:var(--text-muted);' : ''}">${escapeHtml(c.text)}</span>
2763
3073
  <button class="btn btn-sm" onclick="removeCriterion(${i})" style="opacity:0.5;flex-shrink:0;">&#10005;</button>
2764
3074
  </div>
2765
3075
  `).join('');
@@ -2839,10 +3149,10 @@ function renderModules() {
2839
3149
  <div class="module-card" onclick="showSection('module-spec-${m.code}')">
2840
3150
  <button class="module-card-remove" onclick="event.stopPropagation();removeModule(${i})">&#10005;</button>
2841
3151
  <div class="module-card-header">
2842
- <span class="module-card-code">${m.name}</span>
3152
+ <span class="module-card-code">${escapeHtml(m.name)}</span>
2843
3153
  <span class="module-card-type">${formatModuleType(m.featureType)}</span>
2844
3154
  </div>
2845
- <div class="module-card-desc">${m.description || ''}</div>
3155
+ <div class="module-card-desc">${escapeHtml(m.description || '')}</div>
2846
3156
  <div class="module-card-meta">
2847
3157
  <span>${(m.entities || []).length} données</span>
2848
3158
  <span>${(data.moduleSpecs[m.code]?.useCases || []).length} cas d'utilisation</span>
@@ -2899,10 +3209,10 @@ function renderDependencies() {
2899
3209
  const toName = data.modules.find(m => m.code === d.to)?.name || d.to;
2900
3210
  return `
2901
3211
  <div class="interaction-item">
2902
- <span style="font-weight:600;color:var(--text-bright);">${fromName}</span>
3212
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(fromName)}</span>
2903
3213
  <span class="interaction-arrow">&#8594;</span>
2904
- <span style="font-weight:600;color:var(--text-bright);">${toName}</span>
2905
- <span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${d.description || ''}</span>
3214
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(toName)}</span>
3215
+ <span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${escapeHtml(d.description || '')}</span>
2906
3216
  <button class="btn btn-sm" onclick="removeDependency(${i})" style="opacity:0.5;">Supprimer</button>
2907
3217
  </div>
2908
3218
  `;
@@ -2931,7 +3241,7 @@ function renderDepGraph() {
2931
3241
  <div class="dep-layer-modules">
2932
3242
  ${layer.map(code => {
2933
3243
  const m = data.modules.find(mod => mod.code === code);
2934
- return `<div class="dep-module">${m ? (m.name || m.code) : code}</div>`;
3244
+ return `<div class="dep-module">${escapeHtml(m ? (m.name || m.code) : code)}</div>`;
2935
3245
  }).join('')}
2936
3246
  </div>
2937
3247
  </div>
@@ -2979,7 +3289,7 @@ function renderProcessingOrder() {
2979
3289
  return `
2980
3290
  <div class="process-step">
2981
3291
  <div class="process-step-number">Étape ${i + 1}</div>
2982
- <div class="process-step-label">${m ? (m.name || m.code) : code}</div>
3292
+ <div class="process-step-label">${escapeHtml(m ? (m.name || m.code) : code)}</div>
2983
3293
  </div>
2984
3294
  ${i < order.length - 1 ? '<div class="process-arrow">&#8594;</div>' : ''}
2985
3295
  `;
@@ -3017,23 +3327,24 @@ function renderModuleSpecSection(mod) {
3017
3327
 
3018
3328
  return `
3019
3329
  <div class="section" id="module-spec-${code}" style="display:none;">
3020
- <h2 class="section-title">${mod.name}</h2>
3021
- <p class="section-subtitle">${mod.description || 'Spécification détaillée de ce domaine fonctionnel.'}</p>
3022
-
3023
- <div class="tab-bar">
3024
- <button class="tab-btn active" onclick="switchTab('${code}', 'uc')">Cas d'utilisation</button>
3025
- <button class="tab-btn" onclick="switchTab('${code}', 'br')">Règles métier</button>
3026
- <button class="tab-btn" onclick="switchTab('${code}', 'ent')">Données</button>
3027
- <button class="tab-btn" onclick="switchTab('${code}', 'perm')">Droits d'accès</button>
3028
- <button class="tab-btn" onclick="switchTab('${code}', 'mock')">Maquettes</button>
3029
- <button class="tab-btn" onclick="switchTab('${code}', 'notes')">Notes</button>
3030
- <button class="tab-btn" onclick="switchTab('${code}', 'struct')">Structure</button>
3330
+ <h2 class="section-title">${escapeHtml(mod.name)}</h2>
3331
+ <p class="section-subtitle">${escapeHtml(mod.description || 'Spécification détaillée de ce domaine fonctionnel.')}</p>
3332
+
3333
+ <div class="tab-bar" role="tablist" aria-label="Spécifications du module ${escapeHtml(mod.name)}">
3334
+ <button class="tab-btn active" role="tab" aria-selected="true" aria-controls="tab-${code}-uc" onclick="switchTab('${code}', 'uc')">Cas d'utilisation</button>
3335
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-br" onclick="switchTab('${code}', 'br')">Règles métier</button>
3336
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-ent" onclick="switchTab('${code}', 'ent')">Données</button>
3337
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-perm" onclick="switchTab('${code}', 'perm')">Droits d'accès</button>
3338
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-mock" onclick="switchTab('${code}', 'mock')">Maquettes</button>
3339
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-notes" onclick="switchTab('${code}', 'notes')">Notes</button>
3340
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-struct" onclick="switchTab('${code}', 'struct')">Structure</button>
3031
3341
  </div>
3032
3342
 
3033
3343
  <!-- TAB: Cas d'utilisation -->
3034
- <div class="tab-panel active" id="tab-${code}-uc">
3344
+ <div class="tab-panel active" id="tab-${code}-uc" role="tabpanel" aria-hidden="false">
3035
3345
  <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Décrivez ce que chaque type d'utilisateur peut faire dans ce domaine. Un cas d'utilisation = une action concrète.</p>
3036
3346
  <div id="ucList-${code}" class="uc-list">
3347
+ ${hasHierarchicalSpecs(mod) ? renderSectionGroupedItems(mod, 'useCases', code, renderUseCase) : ''}
3037
3348
  ${spec.useCases.map((uc, i) => renderUseCase(code, uc, i)).join('')}
3038
3349
  </div>
3039
3350
  <button class="add-btn" onclick="toggleForm('addUcForm-${code}')">+ Ajouter un cas d'utilisation</button>
@@ -3063,9 +3374,10 @@ function renderModuleSpecSection(mod) {
3063
3374
  </div>
3064
3375
 
3065
3376
  <!-- TAB: Règles métier -->
3066
- <div class="tab-panel" id="tab-${code}-br">
3377
+ <div class="tab-panel" id="tab-${code}-br" role="tabpanel" aria-hidden="true">
3067
3378
  <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Les règles que le système doit respecter. Formulez-les sous forme de conditions : "Si... alors... sinon..."</p>
3068
3379
  <div id="brList-${code}">
3380
+ ${hasHierarchicalSpecs(mod) ? renderSectionGroupedItems(mod, 'businessRules', code, renderBusinessRule) : ''}
3069
3381
  ${spec.businessRules.map((br, i) => renderBusinessRule(code, br, i)).join('')}
3070
3382
  </div>
3071
3383
  <button class="add-btn" onclick="toggleForm('addBrForm-${code}')">+ Ajouter une règle métier</button>
@@ -3101,7 +3413,7 @@ function renderModuleSpecSection(mod) {
3101
3413
  </div>
3102
3414
 
3103
3415
  <!-- TAB: Données (Entités) -->
3104
- <div class="tab-panel" id="tab-${code}-ent">
3416
+ <div class="tab-panel" id="tab-${code}-ent" role="tabpanel" aria-hidden="true">
3105
3417
  <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Les types de données que ce domaine gère. Décrivez les informations importantes à enregistrer pour chaque type.</p>
3106
3418
  <div id="entList-${code}">
3107
3419
  ${spec.entities.length > 0
@@ -3138,7 +3450,7 @@ function renderModuleSpecSection(mod) {
3138
3450
  </div>
3139
3451
 
3140
3452
  <!-- TAB: Droits d'accès -->
3141
- <div class="tab-panel" id="tab-${code}-perm">
3453
+ <div class="tab-panel" id="tab-${code}-perm" role="tabpanel" aria-hidden="true">
3142
3454
  <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Définissez qui peut faire quoi dans ce domaine. Cochez les actions autorisées pour chaque profil.</p>
3143
3455
  <div id="permGrid-${code}">
3144
3456
  ${renderPermissionGrid(code)}
@@ -3172,7 +3484,7 @@ function renderModuleSpecSection(mod) {
3172
3484
  </div>
3173
3485
 
3174
3486
  <!-- TAB: Maquettes -->
3175
- <div class="tab-panel" id="tab-${code}-mock">
3487
+ <div class="tab-panel" id="tab-${code}-mock" role="tabpanel" aria-hidden="true">
3176
3488
  <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Maquettes validées lors de l'analyse. Ces wireframes montrent la structure exacte des écrans de ce domaine.</p>
3177
3489
  <div id="mockupContainer-${code}">
3178
3490
  ${renderModuleMockups(code)}
@@ -3180,7 +3492,7 @@ function renderModuleSpecSection(mod) {
3180
3492
  </div>
3181
3493
 
3182
3494
  <!-- TAB: Notes -->
3183
- <div class="tab-panel" id="tab-${code}-notes">
3495
+ <div class="tab-panel" id="tab-${code}-notes" role="tabpanel" aria-hidden="true">
3184
3496
  <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Notes libres, questions en suspens, éléments à clarifier pour ce domaine.</p>
3185
3497
  <div class="card">
3186
3498
  <div class="editable" contenteditable="true" data-module-code="${code}" data-module-field="notes" data-placeholder="Notez ici tout ce qui concerne ce domaine : questions, précisions, contraintes particulières...">${spec.notes || ''}</div>
@@ -3188,7 +3500,7 @@ function renderModuleSpecSection(mod) {
3188
3500
  </div>
3189
3501
 
3190
3502
  <!-- TAB: Structure (sections/resources) -->
3191
- <div class="tab-panel" id="tab-${code}-struct">
3503
+ <div class="tab-panel" id="tab-${code}-struct" role="tabpanel" aria-hidden="true">
3192
3504
  <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Organisation des écrans et ressources de ce domaine. Structure hiérarchique : sections regroupant des ressources.</p>
3193
3505
  <div id="structContainer-${code}">
3194
3506
  ${renderModuleStructure(code)}
@@ -3202,19 +3514,19 @@ function renderUseCase(code, uc, index) {
3202
3514
  <div class="uc-item">
3203
3515
  <div class="uc-header">
3204
3516
  <span class="uc-id">UC-${String(index + 1).padStart(3, '0')}</span>
3205
- <span class="uc-title">${uc.name}</span>
3517
+ <span class="uc-title">${escapeHtml(uc.name)}</span>
3206
3518
  <div class="uc-actions">
3207
3519
  <button class="btn btn-sm" onclick="removeUseCase('${code}',${index})">Supprimer</button>
3208
3520
  </div>
3209
3521
  </div>
3210
- <div class="uc-actors"><div class="uc-actor">${uc.actor}</div></div>
3211
- ${uc.steps ? `<div class="uc-detail-label">Déroulement</div><div class="uc-detail">${uc.steps.replace(/\n/g, '<br>')}</div>` : ''}
3212
- ${uc.alternative ? `<div class="uc-detail-label">En cas de problème</div><div class="uc-detail" style="color:var(--warning);">${uc.alternative}</div>` : ''}
3522
+ <div class="uc-actors"><div class="uc-actor">${escapeHtml(uc.actor)}</div></div>
3523
+ ${uc.steps ? `<div class="uc-detail-label">Déroulement</div><div class="uc-detail">${escapeHtml(uc.steps).replace(/\n/g, '<br>')}</div>` : ''}
3524
+ ${uc.alternative ? `<div class="uc-detail-label">En cas de problème</div><div class="uc-detail" style="color:var(--warning);">${escapeHtml(uc.alternative)}</div>` : ''}
3213
3525
  <div style="padding:0.5rem 0.75rem;border-top:1px solid var(--border);background:var(--bg-input);border-radius:0 0 8px 8px;">
3214
3526
  <label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.25rem;">Commentaire / Feedback :</label>
3215
3527
  <textarea class="form-textarea" placeholder="Ajouter un commentaire sur ce cas d'utilisation..."
3216
3528
  onblur="updateSpecComment('${code}','uc',${index},this.value)"
3217
- style="min-height:45px;font-size:0.8rem;resize:vertical;">${getSpecComment(code, 'uc', index)}</textarea>
3529
+ style="min-height:45px;font-size:0.8rem;resize:vertical;">${escapeHtml(getSpecComment(code, 'uc', index))}</textarea>
3218
3530
  </div>
3219
3531
  </div>`;
3220
3532
  }
@@ -3237,6 +3549,7 @@ function addUseCase(code) {
3237
3549
  }
3238
3550
 
3239
3551
  function removeUseCase(code, index) {
3552
+ if (!confirm('Supprimer ce cas d\'utilisation ?')) return;
3240
3553
  data.moduleSpecs[code].useCases.splice(index, 1);
3241
3554
  renderAllModuleSpecs();
3242
3555
  updateCounts();
@@ -3249,18 +3562,18 @@ function renderBusinessRule(code, br, index) {
3249
3562
  return `
3250
3563
  <div style="margin-bottom:0.5rem;">
3251
3564
  <div class="br-item" style="margin-bottom:0;border-radius:8px 8px 0 0;">
3252
- <span class="br-category ${catColors[br.category] || 'br-cat-validation'}">${catLabels[br.category] || br.category}</span>
3565
+ <span class="br-category ${catColors[br.category] || 'br-cat-validation'}">${escapeHtml(catLabels[br.category] || br.category)}</span>
3253
3566
  <div class="br-text" style="flex:1;">
3254
- <div style="font-weight:600;color:var(--text-bright);margin-bottom:0.2rem;">${br.name}</div>
3255
- <div>${br.statement}</div>
3256
- ${br.example ? `<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.25rem;font-style:italic;">Exemple : ${br.example}</div>` : ''}
3567
+ <div style="font-weight:600;color:var(--text-bright);margin-bottom:0.2rem;">${escapeHtml(br.name)}</div>
3568
+ <div>${escapeHtml(br.statement)}</div>
3569
+ ${br.example ? `<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.25rem;font-style:italic;">Exemple : ${escapeHtml(br.example)}</div>` : ''}
3257
3570
  </div>
3258
3571
  <button class="btn btn-sm" onclick="removeBusinessRule('${code}',${index})" style="opacity:0.5;flex-shrink:0;">&#10005;</button>
3259
3572
  </div>
3260
3573
  <div style="padding:0.4rem 0.75rem 0.6rem;background:var(--bg-input);border:1px solid var(--border);border-top:0;border-radius:0 0 8px 8px;">
3261
3574
  <textarea class="form-textarea" placeholder="Commentaire sur cette règle..."
3262
3575
  onblur="updateSpecComment('${code}','br',${index},this.value)"
3263
- style="min-height:35px;font-size:0.8rem;resize:vertical;">${getSpecComment(code, 'br', index)}</textarea>
3576
+ style="min-height:35px;font-size:0.8rem;resize:vertical;">${escapeHtml(getSpecComment(code, 'br', index))}</textarea>
3264
3577
  </div>
3265
3578
  </div>`;
3266
3579
  }
@@ -3282,18 +3595,20 @@ function addBusinessRule(code) {
3282
3595
  }
3283
3596
 
3284
3597
  function removeBusinessRule(code, index) {
3598
+ if (!confirm('Supprimer cette règle métier ?')) return;
3285
3599
  data.moduleSpecs[code].businessRules.splice(index, 1);
3286
3600
  renderAllModuleSpecs();
3287
3601
  autoSave();
3288
3602
  }
3289
3603
 
3290
3604
  function renderEntity(code, ent, index) {
3605
+ var attrFormId = 'addAttrForm-' + code + '-' + index;
3291
3606
  return `
3292
3607
  <div class="entity-block">
3293
3608
  <div class="entity-header">
3294
3609
  <div>
3295
- <div class="entity-name">${ent.name}</div>
3296
- <div class="entity-desc">${ent.description || ''}</div>
3610
+ <div class="entity-name">${escapeHtml(ent.name)}</div>
3611
+ <div class="entity-desc">${escapeHtml(ent.description || '')}</div>
3297
3612
  </div>
3298
3613
  <button class="btn btn-sm" onclick="removeEntity('${code}',${index})" style="opacity:0.5;">Supprimer</button>
3299
3614
  </div>
@@ -3301,21 +3616,26 @@ function renderEntity(code, ent, index) {
3301
3616
  <table class="attr-table">
3302
3617
  <thead><tr><th>Information</th><th>Description</th></tr></thead>
3303
3618
  <tbody>
3304
- ${ent.attributes.map(a => `<tr><td style="font-weight:500;color:var(--text-bright);">${a.name}</td><td>${a.description || ''}</td></tr>`).join('')}
3619
+ ${ent.attributes.map(a => `<tr><td style="font-weight:500;color:var(--text-bright);">${escapeHtml(a.name)}</td><td>${escapeHtml(a.description || '')}</td></tr>`).join('')}
3305
3620
  </tbody>
3306
- </table>
3621
+ </table>` : ''}
3307
3622
  <div style="padding:0.3rem 0.75rem;">
3308
- <button class="add-btn" style="font-size:0.75rem;padding:0.4rem;" onclick="addEntityAttribute('${code}',${index})">+ Ajouter un attribut</button>
3309
- </div>` : ''}
3623
+ <button class="add-btn" style="font-size:0.75rem;padding:0.4rem;" onclick="toggleForm('${attrFormId}')">+ Ajouter un attribut</button>
3624
+ <div class="inline-form" id="${attrFormId}">
3625
+ <div class="form-group"><label class="form-label">Nom de l'attribut</label><input type="text" class="form-input" id="attr-name-${code}-${index}" placeholder="Nom de l'attribut"></div>
3626
+ <div class="form-group"><label class="form-label">Description (optionnel)</label><input type="text" class="form-input" id="attr-desc-${code}-${index}" placeholder="Description"></div>
3627
+ <div class="form-actions"><button class="btn" onclick="toggleForm('${attrFormId}')">Annuler</button><button class="btn btn-primary" onclick="addEntityAttribute('${code}',${index})">Ajouter</button></div>
3628
+ </div>
3629
+ </div>
3310
3630
  ${(ent.relationships || []).length > 0 ? `
3311
3631
  <div style="padding:0.5rem 0.75rem;font-size:0.8rem;color:var(--text-muted);border-top:1px solid var(--border);">
3312
- Relations : ${ent.relationships.map(r => `<span style="color:var(--accent);">${r}</span>`).join(', ')}
3632
+ Relations : ${ent.relationships.map(r => `<span style="color:var(--accent);">${escapeHtml(typeof r === 'string' ? r : (r.target || ''))}</span>`).join(', ')}
3313
3633
  </div>` : ''}
3314
3634
  <div style="padding:0.4rem 0.75rem 0.6rem;background:var(--bg-input);border-top:1px solid var(--border);border-radius:0 0 8px 8px;">
3315
3635
  <label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.25rem;">Commentaire :</label>
3316
3636
  <textarea class="form-textarea" placeholder="Commentaire sur cette entité..."
3317
3637
  onblur="updateSpecComment('${code}','ent',${index},this.value)"
3318
- style="min-height:35px;font-size:0.8rem;resize:vertical;">${getSpecComment(code, 'ent', index)}</textarea>
3638
+ style="min-height:35px;font-size:0.8rem;resize:vertical;">${escapeHtml(getSpecComment(code, 'ent', index))}</textarea>
3319
3639
  </div>
3320
3640
  </div>`;
3321
3641
  }
@@ -3343,20 +3663,21 @@ function addEntity(code) {
3343
3663
  }
3344
3664
 
3345
3665
  function removeEntity(code, index) {
3666
+ if (!confirm('Supprimer ce type de données ?')) return;
3346
3667
  data.moduleSpecs[code].entities.splice(index, 1);
3347
3668
  renderAllModuleSpecs();
3348
3669
  autoSave();
3349
3670
  }
3350
3671
 
3351
3672
  function addEntityAttribute(code, entityIndex) {
3352
- var attrName = prompt('Nom de l\'attribut :');
3353
- if (!attrName) return;
3354
- var attrDesc = prompt('Description (optionnel) :') || '';
3673
+ var attrName = document.getElementById('attr-name-' + code + '-' + entityIndex);
3674
+ var attrDesc = document.getElementById('attr-desc-' + code + '-' + entityIndex);
3675
+ if (!attrName || !attrName.value.trim()) return;
3355
3676
  if (!data.moduleSpecs[code]?.entities?.[entityIndex]) return;
3356
3677
  if (!data.moduleSpecs[code].entities[entityIndex].attributes) {
3357
3678
  data.moduleSpecs[code].entities[entityIndex].attributes = [];
3358
3679
  }
3359
- data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName, description: attrDesc });
3680
+ data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName.value.trim(), description: (attrDesc?.value || '').trim() });
3360
3681
  renderAllModuleSpecs();
3361
3682
  autoSave();
3362
3683
  }
@@ -3382,37 +3703,6 @@ function updateWireframeComment(code, screen, value) {
3382
3703
  autoSave();
3383
3704
  }
3384
3705
 
3385
- function renderModuleMockups(code) {
3386
- var spec = data.moduleSpecs[code] || {};
3387
- var screens = spec.screens || [];
3388
- var wireframes = EMBEDDED_ARTIFACTS?.wireframes?.[code] || [];
3389
-
3390
- // Priority 1: HTML mockups from screens[] specs
3391
- if (screens.length > 0) {
3392
- var html = '';
3393
- if (typeof renderScreenMockups === 'function') {
3394
- html = renderScreenMockups(code);
3395
- }
3396
- // Also show wireframes below if available
3397
- if (wireframes.length > 0) {
3398
- html += '<h3 style="color:var(--text-bright);font-size:1rem;margin:2rem 0 1rem;">Wireframes</h3>';
3399
- html += wireframes.map(function(wf, i) { return renderWireframeMockup(code, wf, i); }).join('');
3400
- }
3401
- return html;
3402
- }
3403
-
3404
- // Priority 2: Wireframes from EMBEDDED_ARTIFACTS
3405
- if (wireframes.length === 0) {
3406
- return `
3407
- <div class="card" style="text-align:center;padding:2rem;color:var(--text-muted);">
3408
- <p>Aucune maquette disponible pour ce module.</p>
3409
- <p style="font-size:0.85rem;margin-top:0.5rem;">Les maquettes seront générées lors de la spécification détaillée.</p>
3410
- </div>`;
3411
- }
3412
-
3413
- return wireframes.map(function(wf, i) { return renderWireframeMockup(code, wf, i); }).join('');
3414
- }
3415
-
3416
3706
  function renderWireframeMockup(code, wf, i) {
3417
3707
  const hasSvg = !!wf.svgContent;
3418
3708
  const wireframeId = `wf-${code}-${i}`;
@@ -3423,7 +3713,7 @@ function renderWireframeMockup(code, wf, i) {
3423
3713
  <div class="mockup-dot mockup-dot-red"></div>
3424
3714
  <div class="mockup-dot mockup-dot-yellow"></div>
3425
3715
  <div class="mockup-dot mockup-dot-green"></div>
3426
- <span class="mockup-title">${wf.screen || wf.section}</span>
3716
+ <span class="mockup-title">${escapeHtml(wf.screen || wf.section)}</span>
3427
3717
  ${hasSvg ? `
3428
3718
  <div class="wireframe-toggle">
3429
3719
  <button class="wireframe-toggle-btn active" data-target="${wireframeId}" data-view="svg" onclick="toggleWireframeView('${wireframeId}', 'svg')">SVG</button>
@@ -3440,11 +3730,11 @@ function renderWireframeMockup(code, wf, i) {
3440
3730
  </div>
3441
3731
  ${wf.description ? `
3442
3732
  <div class="wireframe-description">
3443
- <strong>Description:</strong> ${wf.description}
3733
+ <strong>Description:</strong> ${escapeHtml(wf.description)}
3444
3734
  </div>` : ''}
3445
3735
  ${(wf.elements || []).length > 0 ? `
3446
3736
  <div class="wireframe-metadata">
3447
- <div><strong>Elements:</strong> ${wf.elements.map(e => typeof e === 'string' ? e : (e.type || e.label || e.id || '')).filter(Boolean).join(', ')}</div>
3737
+ <div><strong>Elements:</strong> ${wf.elements.map(e => escapeHtml(typeof e === 'string' ? e : (e.type || e.label || e.id || ''))).filter(Boolean).join(', ')}</div>
3448
3738
  </div>` : ''}
3449
3739
  ${(() => {
3450
3740
  const cm = wf.componentMapping;
@@ -3458,7 +3748,7 @@ function renderWireframeMockup(code, wf, i) {
3458
3748
  <thead><tr><th>Élément maquette</th><th>Composant React</th></tr></thead>
3459
3749
  <tbody>
3460
3750
  ${mappings.map(m =>
3461
- '<tr><td>' + (m.wireframeElement || '') + '</td><td><code>' + (m.reactComponent || '') + '</code></td></tr>'
3751
+ '<tr><td>' + escapeHtml(m.wireframeElement || '') + '</td><td><code>' + escapeHtml(m.reactComponent || '') + '</code></td></tr>'
3462
3752
  ).join('')}
3463
3753
  </tbody>
3464
3754
  </table>
@@ -3533,12 +3823,12 @@ function renderPermissionGrid(code) {
3533
3823
  <table class="mock-table" style="background:var(--bg-card);border-radius:8px;overflow:hidden;">
3534
3824
  <thead><tr>
3535
3825
  <th>Profil</th>
3536
- ${actions.map((a, i) => `<th style="text-align:center;">${a}${i >= baseActionsCount ? ' <span onclick="removeCustomAction('+`'${code}','${a}'`+')" style="cursor:pointer;color:var(--danger);font-size:0.7rem;" title="Supprimer cette action">&#10005;</span>' : ''}</th>`).join('')}
3826
+ ${actions.map((a, i) => `<th style="text-align:center;">${escapeHtml(a)}${i >= baseActionsCount ? ' <span onclick="removeCustomAction('+`'${code}','${a}'`+')" style="cursor:pointer;color:var(--danger);font-size:0.7rem;" title="Supprimer cette action">&#10005;</span>' : ''}</th>`).join('')}
3537
3827
  </tr></thead>
3538
3828
  <tbody>
3539
3829
  ${roles.map((role, ri) => `
3540
3830
  <tr>
3541
- <td style="font-weight:500;color:var(--text-bright);">${role}${ri >= baseRolesCount ? ' <span onclick="removeCustomRole('+`'${code}','${role}'`+')" style="cursor:pointer;color:var(--danger);font-size:0.7rem;" title="Supprimer ce rôle">&#10005;</span>' : ''}</td>
3831
+ <td style="font-weight:500;color:var(--text-bright);">${escapeHtml(role)}${ri >= baseRolesCount ? ' <span onclick="removeCustomRole('+`'${code}','${role}'`+')" style="cursor:pointer;color:var(--danger);font-size:0.7rem;" title="Supprimer ce rôle">&#10005;</span>' : ''}</td>
3542
3832
  ${actions.map(action => {
3543
3833
  const key = role + '|' + action;
3544
3834
  const wildcardKey = role + '|*';
@@ -3632,24 +3922,52 @@ function renderModuleStructure(code) {
3632
3922
 
3633
3923
  return sections.map(function(section) {
3634
3924
  var resources = section.resources || [];
3925
+ var sectionUCs = section.useCases || [];
3926
+ var sectionBRs = section.businessRules || [];
3635
3927
  var html = '<div class="struct-section">';
3636
3928
  html += '<div class="struct-section-header">';
3637
- html += '<span class="struct-section-code">' + (section.code || section.name || '') + '</span>';
3929
+ html += '<span class="struct-section-code">' + escapeHtml(section.code || section.name || '') + '</span>';
3638
3930
  if (section.description) {
3639
- html += '<span class="struct-section-desc">' + section.description + '</span>';
3931
+ html += '<span class="struct-section-desc">' + escapeHtml(section.description) + '</span>';
3640
3932
  }
3641
3933
  html += '<span class="struct-section-badge">' + resources.length + ' ressource' + (resources.length !== 1 ? 's' : '') + '</span>';
3642
3934
  html += '</div>';
3643
3935
 
3936
+ // Section metadata (route, permission)
3937
+ if (section.route || section.permission) {
3938
+ html += '<div style="padding:0.4rem 1rem;font-size:0.75rem;color:var(--text-muted);display:flex;gap:1rem;border-bottom:1px solid var(--border);">';
3939
+ if (section.route) html += '<span>Route: <code style="color:var(--accent);background:var(--bg-input);padding:0.1rem 0.3rem;border-radius:3px;">' + escapeHtml(section.route) + '</code></span>';
3940
+ if (section.permission) html += '<span>Permission: <code style="color:var(--accent);background:var(--bg-input);padding:0.1rem 0.3rem;border-radius:3px;">' + escapeHtml(section.permission) + '</code></span>';
3941
+ html += '</div>';
3942
+ }
3943
+
3944
+ // Section-level UC summary
3945
+ if (sectionUCs.length > 0) {
3946
+ html += '<div style="padding:0.4rem 1rem;font-size:0.8rem;border-bottom:1px solid var(--border);">';
3947
+ html += '<span style="color:var(--text-muted);font-weight:500;">' + sectionUCs.length + ' cas d\'utilisation :</span> ';
3948
+ html += sectionUCs.map(function(uc) { return '<span style="color:var(--text-bright);">' + escapeHtml(uc.name || '') + '</span>'; }).join(', ');
3949
+ html += '</div>';
3950
+ }
3951
+
3952
+ // Section-level BR summary
3953
+ if (sectionBRs.length > 0) {
3954
+ html += '<div style="padding:0.4rem 1rem;font-size:0.8rem;border-bottom:1px solid var(--border);">';
3955
+ html += '<span style="color:var(--text-muted);font-weight:500;">' + sectionBRs.length + ' règle' + (sectionBRs.length > 1 ? 's' : '') + ' métier :</span> ';
3956
+ html += sectionBRs.map(function(br) { return '<span style="color:var(--text-bright);">' + escapeHtml(br.name || '') + '</span>'; }).join(', ');
3957
+ html += '</div>';
3958
+ }
3959
+
3644
3960
  if (resources.length > 0) {
3645
3961
  html += '<div class="struct-resources">';
3646
3962
  resources.forEach(function(res) {
3647
3963
  var resName = typeof res === 'string' ? res : (res.code || res.name || '');
3964
+ var resType = typeof res === 'object' ? (res.type || '') : '';
3648
3965
  var resDesc = typeof res === 'object' ? (res.description || '') : '';
3649
3966
  html += '<div class="struct-resource">';
3650
3967
  html += '<span class="struct-resource-icon">&#8226;</span>';
3651
- html += '<span class="struct-resource-name">' + resName + '</span>';
3652
- if (resDesc) html += '<span class="struct-resource-desc">' + resDesc + '</span>';
3968
+ html += '<span class="struct-resource-name">' + escapeHtml(resName) + '</span>';
3969
+ if (resType) html += '<span style="font-size:0.7rem;color:var(--accent);background:rgba(6,182,212,0.1);padding:0.1rem 0.3rem;border-radius:3px;">' + escapeHtml(resType) + '</span>';
3970
+ if (resDesc) html += '<span class="struct-resource-desc">' + escapeHtml(resDesc) + '</span>';
3653
3971
  html += '</div>';
3654
3972
  });
3655
3973
  html += '</div>';
@@ -3660,16 +3978,111 @@ function renderModuleStructure(code) {
3660
3978
  }).join('');
3661
3979
  }
3662
3980
 
3981
+ /* ---------- Section-Grouped Rendering (Hierarchical Mode) ---------- */
3982
+
3983
+ function renderSectionGroupedItems(mod, itemsKey, code, renderFn) {
3984
+ var sections = mod.anticipatedSections || [];
3985
+ var html = '';
3986
+ sections.forEach(function(section) {
3987
+ var items = section[itemsKey] || [];
3988
+ if (items.length === 0) return;
3989
+ html += '<div class="section-group">';
3990
+ html += '<div class="section-group-header">';
3991
+ html += '<span class="section-group-icon">&#9655;</span> ';
3992
+ html += '<span class="section-group-label">' + escapeHtml(section.code || section.name || '') + '</span>';
3993
+ if (section.route) html += '<span class="section-group-route">' + escapeHtml(section.route) + '</span>';
3994
+ html += '<span class="nav-badge">' + items.length + '</span>';
3995
+ html += '</div>';
3996
+ html += items.map(function(item, i) { return renderFn(code, item, i); }).join('');
3997
+ html += '</div>';
3998
+ });
3999
+ return html;
4000
+ }
4001
+
4002
+ function renderModuleMockups(code) {
4003
+ var spec = data.moduleSpecs[code] || {};
4004
+ var screens = spec.screens || [];
4005
+ var wireframes = EMBEDDED_ARTIFACTS?.wireframes?.[code] || [];
4006
+ var mod = data.modules.find(function(m) { return m.code === code; });
4007
+ var sections = mod ? (mod.anticipatedSections || []) : [];
4008
+
4009
+ // Priority 1: HTML mockups from screens[] specs
4010
+ if (screens.length > 0) {
4011
+ var html = '';
4012
+ if (typeof renderScreenMockups === 'function') {
4013
+ html = renderScreenMockups(code);
4014
+ }
4015
+ if (wireframes.length > 0) {
4016
+ html += '<h3 style="color:var(--text-bright);font-size:1rem;margin:2rem 0 1rem;">Wireframes</h3>';
4017
+ html += wireframes.map(function(wf, i) { return renderWireframeMockup(code, wf, i); }).join('');
4018
+ }
4019
+ return html;
4020
+ }
4021
+
4022
+ // Priority 2: Wireframes grouped by section (if sections exist)
4023
+ if (wireframes.length > 0 && sections.length > 1) {
4024
+ var grouped = {};
4025
+ var ungrouped = [];
4026
+ wireframes.forEach(function(wf, i) {
4027
+ var sectionCode = wf.section || wf.sectionCode || '';
4028
+ var matchingSection = sections.find(function(s) { return s.code === sectionCode; });
4029
+ if (matchingSection) {
4030
+ if (!grouped[sectionCode]) grouped[sectionCode] = { section: matchingSection, items: [] };
4031
+ grouped[sectionCode].items.push({ wf: wf, index: i });
4032
+ } else {
4033
+ ungrouped.push({ wf: wf, index: i });
4034
+ }
4035
+ });
4036
+
4037
+ var html = '';
4038
+ Object.keys(grouped).forEach(function(sectionCode) {
4039
+ var group = grouped[sectionCode];
4040
+ html += '<div class="section-group">';
4041
+ html += '<div class="section-group-header">';
4042
+ html += '<span class="section-group-icon">&#9655;</span> ';
4043
+ html += '<span class="section-group-label">' + escapeHtml(group.section.code || '') + '</span>';
4044
+ html += '<span class="nav-badge">' + group.items.length + '</span>';
4045
+ html += '</div>';
4046
+ group.items.forEach(function(item) {
4047
+ html += renderWireframeMockup(code, item.wf, item.index);
4048
+ });
4049
+ html += '</div>';
4050
+ });
4051
+ ungrouped.forEach(function(item) {
4052
+ html += renderWireframeMockup(code, item.wf, item.index);
4053
+ });
4054
+ return html;
4055
+ }
4056
+
4057
+ // Priority 3: Flat wireframe list
4058
+ if (wireframes.length === 0) {
4059
+ return '<div class="card" style="text-align:center;padding:2rem;color:var(--text-muted);"><p>Aucune maquette disponible pour ce module.</p><p style="font-size:0.85rem;margin-top:0.5rem;">Les maquettes seront générées lors de la spécification détaillée.</p></div>';
4060
+ }
4061
+ return wireframes.map(function(wf, i) { return renderWireframeMockup(code, wf, i); }).join('');
4062
+ }
4063
+
3663
4064
  function switchTab(code, tabId) {
3664
4065
  const section = document.getElementById('module-spec-' + code);
3665
4066
  if (!section) return;
3666
- section.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
3667
- section.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active'));
4067
+ section.querySelectorAll('.tab-btn').forEach(function(btn) {
4068
+ btn.classList.remove('active');
4069
+ btn.setAttribute('aria-selected', 'false');
4070
+ });
4071
+ section.querySelectorAll('.tab-panel').forEach(function(panel) {
4072
+ panel.classList.remove('active');
4073
+ panel.setAttribute('aria-hidden', 'true');
4074
+ });
3668
4075
  const targetPanel = document.getElementById('tab-' + code + '-' + tabId);
3669
- if (targetPanel) targetPanel.classList.add('active');
4076
+ if (targetPanel) {
4077
+ targetPanel.classList.add('active');
4078
+ targetPanel.setAttribute('aria-hidden', 'false');
4079
+ }
3670
4080
  const buttons = section.querySelectorAll('.tab-btn');
3671
4081
  const tabIndex = { uc: 0, br: 1, ent: 2, perm: 3, mock: 4, notes: 5, struct: 6 }[tabId];
3672
- if (buttons[tabIndex]) buttons[tabIndex].classList.add('active');
4082
+ if (buttons[tabIndex]) {
4083
+ buttons[tabIndex].classList.add('active');
4084
+ buttons[tabIndex].setAttribute('aria-selected', 'true');
4085
+ }
3673
4086
  }
3674
4087
 
3675
4088
 
@@ -3729,7 +4142,7 @@ function renderDataModel() {
3729
4142
 
3730
4143
  html += `<div class="dm-module-group">`;
3731
4144
  html += `<div class="dm-module-header">
3732
- <span class="dm-module-name">${m.name || m.code}</span>
4145
+ <span class="dm-module-name">${escapeHtml(m.name || m.code)}</span>
3733
4146
  <span class="dm-module-count">${entities.length} entité${entities.length > 1 ? 's' : ''}</span>
3734
4147
  </div>`;
3735
4148
 
@@ -3740,15 +4153,15 @@ function renderDataModel() {
3740
4153
  html += `
3741
4154
  <div class="dm-entity-card">
3742
4155
  <div class="dm-entity-header">
3743
- <span class="dm-entity-name">${ent.name}</span>
4156
+ <span class="dm-entity-name">${escapeHtml(ent.name)}</span>
3744
4157
  <span class="dm-entity-attr-count">${attrs.length} champ${attrs.length > 1 ? 's' : ''}</span>
3745
4158
  </div>
3746
- ${ent.description ? `<div class="dm-entity-desc">${ent.description}</div>` : ''}
4159
+ ${ent.description ? `<div class="dm-entity-desc">${escapeHtml(ent.description)}</div>` : ''}
3747
4160
  ${attrs.length > 0 ? `
3748
4161
  <table class="dm-attr-table">
3749
4162
  <thead><tr><th>Champ</th><th>Description</th></tr></thead>
3750
4163
  <tbody>
3751
- ${attrs.map(a => `<tr><td class="dm-attr-name">${a.name}</td><td class="dm-attr-desc">${a.description || ''}</td></tr>`).join('')}
4164
+ ${attrs.map(a => `<tr><td class="dm-attr-name">${escapeHtml(a.name)}</td><td class="dm-attr-desc">${escapeHtml(a.description || '')}</td></tr>`).join('')}
3752
4165
  </tbody>
3753
4166
  </table>` : ''}
3754
4167
  ${rels.length > 0 ? `
@@ -3756,7 +4169,7 @@ function renderDataModel() {
3756
4169
  <div class="dm-relations-title">Relations</div>
3757
4170
  ${rels.map(r => {
3758
4171
  const relText = typeof r === 'string' ? r : (r.target + ' (' + r.type + ') - ' + (r.description || ''));
3759
- return `<div class="dm-relation-item">${relText}</div>`;
4172
+ return `<div class="dm-relation-item">${escapeHtml(relText)}</div>`;
3760
4173
  }).join('')}
3761
4174
  </div>` : ''}
3762
4175
  </div>`;
@@ -3776,11 +4189,11 @@ function renderConsolInteractions() {
3776
4189
  const toName = data.modules.find(m => m.code === d.to)?.name || d.to;
3777
4190
  return `
3778
4191
  <div class="interaction-item">
3779
- <span style="font-weight:600;color:var(--text-bright);">${fromName}</span>
4192
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(fromName)}</span>
3780
4193
  <span class="interaction-arrow">&#8594;</span>
3781
- <span style="font-weight:600;color:var(--text-bright);">${toName}</span>
4194
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(toName)}</span>
3782
4195
  <span class="interaction-type">Dépendance</span>
3783
- <span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${d.description || ''}</span>
4196
+ <span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${escapeHtml(d.description || '')}</span>
3784
4197
  </div>`;
3785
4198
  }).join('');
3786
4199
  }
@@ -3793,11 +4206,11 @@ function renderConsolPermissions() {
3793
4206
 
3794
4207
  let html = '<table class="mock-table" style="background:var(--bg-card);border-radius:8px;overflow:hidden;">';
3795
4208
  html += '<thead><tr><th>Profil</th>';
3796
- data.modules.forEach(m => { html += `<th style="text-align:center;">${m.name || m.code}</th>`; });
4209
+ data.modules.forEach(m => { html += `<th style="text-align:center;">${escapeHtml(m.name || m.code)}</th>`; });
3797
4210
  html += '</tr></thead><tbody>';
3798
4211
 
3799
4212
  roles.forEach(role => {
3800
- html += `<tr><td style="font-weight:500;color:var(--text-bright);">${role}</td>`;
4213
+ html += `<tr><td style="font-weight:500;color:var(--text-bright);">${escapeHtml(role)}</td>`;
3801
4214
  data.modules.forEach(m => {
3802
4215
  const allPerms = data.moduleSpecs[m.code]?.permissions || [];
3803
4216
  const hasWildcard = allPerms.includes(role + '|*');
@@ -3841,6 +4254,7 @@ function addE2EFlow() {
3841
4254
  }
3842
4255
 
3843
4256
  function removeE2EFlow(index) {
4257
+ if (!confirm('Supprimer ce parcours bout en bout ?')) return;
3844
4258
  data.consolidation.e2eFlows.splice(index, 1);
3845
4259
  renderE2EFlows();
3846
4260
  autoSave();
@@ -3853,15 +4267,15 @@ function renderE2EFlows() {
3853
4267
  container.innerHTML = data.consolidation.e2eFlows.map((flow, fi) => `
3854
4268
  <div class="card" style="margin-bottom:1rem;">
3855
4269
  <div class="card-header">
3856
- <span class="card-title">${flow.name}</span>
4270
+ <span class="card-title">${escapeHtml(flow.name)}</span>
3857
4271
  <button class="btn btn-sm" onclick="removeE2EFlow(${fi})" style="opacity:0.5;">Supprimer</button>
3858
4272
  </div>
3859
- ${flow.actors ? `<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem;">Intervenants : ${flow.actors}</div>` : ''}
4273
+ ${flow.actors ? `<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem;">Intervenants : ${escapeHtml(flow.actors)}</div>` : ''}
3860
4274
  <div class="e2e-flow">
3861
4275
  ${flow.steps.map((s, i) => `
3862
4276
  <div class="e2e-step">
3863
- <div class="e2e-step-module">${s.module}</div>
3864
- <div class="e2e-step-action">${s.action}</div>
4277
+ <div class="e2e-step-module">${escapeHtml(s.module)}</div>
4278
+ <div class="e2e-step-action">${escapeHtml(s.action)}</div>
3865
4279
  </div>
3866
4280
  ${i < flow.steps.length - 1 ? '<div class="process-arrow">&#8594;</div>' : ''}
3867
4281
  `).join('')}
@@ -3887,7 +4301,7 @@ function renderScreenMockups(code) {
3887
4301
  var resources = screen.resources || [];
3888
4302
  return '<div class="screen-section" style="margin-bottom:2rem;">' +
3889
4303
  '<h3 style="color:var(--text-bright);font-size:1rem;margin-bottom:1rem;">' +
3890
- '<span style="color:var(--accent);">&#9656;</span> ' + (screen.sectionLabel || screen.sectionCode) +
4304
+ '<span style="color:var(--accent);">&#9656;</span> ' + escapeHtml(screen.sectionLabel || screen.sectionCode) +
3891
4305
  '</h3>' +
3892
4306
  resources.map(function(res, ri) {
3893
4307
  return renderResourceMockup(code, screen.sectionCode, res, ri);
@@ -3905,9 +4319,9 @@ function renderResourceMockup(code, sectionCode, res, index) {
3905
4319
  html += '<div class="mockup-dot mockup-dot-red"></div>';
3906
4320
  html += '<div class="mockup-dot mockup-dot-yellow"></div>';
3907
4321
  html += '<div class="mockup-dot mockup-dot-green"></div>';
3908
- html += '<span class="mockup-title">' + (res.label || res.code) + ' (' + res.type + ')</span>';
4322
+ html += '<span class="mockup-title">' + escapeHtml(res.label || res.code) + ' (' + res.type + ')</span>';
3909
4323
  if (res.permission) {
3910
- html += '<span style="margin-left:auto;font-size:0.65rem;color:var(--text-muted);background:var(--bg-dark);padding:0.15rem 0.5rem;border-radius:4px;">' + res.permission + '</span>';
4324
+ html += '<span style="margin-left:auto;font-size:0.65rem;color:var(--text-muted);background:var(--bg-dark);padding:0.15rem 0.5rem;border-radius:4px;">' + escapeHtml(res.permission) + '</span>';
3911
4325
  }
3912
4326
  html += '</div>';
3913
4327
 
@@ -3940,7 +4354,7 @@ function renderResourceMockup(code, sectionCode, res, index) {
3940
4354
  // Notes
3941
4355
  if (res.notes) {
3942
4356
  html += '<div style="padding:0.5rem 1rem;font-size:0.8rem;color:var(--text-muted);border-top:1px solid var(--border);background:var(--bg-input);">';
3943
- html += '<strong>Notes:</strong> ' + res.notes;
4357
+ html += '<strong>Notes:</strong> ' + escapeHtml(res.notes);
3944
4358
  html += '</div>';
3945
4359
  }
3946
4360
 
@@ -3967,7 +4381,7 @@ function renderSmartTableMockup(res) {
3967
4381
 
3968
4382
  // Header with actions
3969
4383
  html += '<div class="mock-header">';
3970
- html += '<span class="mock-title">' + (res.label || 'Liste') + '</span>';
4384
+ html += '<span class="mock-title">' + escapeHtml(res.label || 'Liste') + '</span>';
3971
4385
  html += '<div style="display:flex;gap:0.4rem;">';
3972
4386
  (res.actions || []).forEach(function(action) {
3973
4387
  var isPrimary = action === 'create' || action === 'export';
@@ -3989,7 +4403,7 @@ function renderSmartTableMockup(res) {
3989
4403
  html += '<table class="mock-table">';
3990
4404
  html += '<thead><tr>';
3991
4405
  columns.forEach(function(col) {
3992
- html += '<th>' + (col.label || col.field);
4406
+ html += '<th>' + escapeHtml(col.label || col.field);
3993
4407
  if (col.sortable) html += ' <span style="font-size:0.6rem;color:var(--text-muted);">&#9650;&#9660;</span>';
3994
4408
  html += '</th>';
3995
4409
  });
@@ -4040,7 +4454,7 @@ function renderSmartFormMockup(res) {
4040
4454
 
4041
4455
  // Header
4042
4456
  html += '<div class="mock-header">';
4043
- html += '<span class="mock-title">' + (res.label || 'Formulaire') + '</span>';
4457
+ html += '<span class="mock-title">' + escapeHtml(res.label || 'Formulaire') + '</span>';
4044
4458
  html += '<div style="display:flex;gap:0.4rem;">';
4045
4459
  (res.actions || ['save', 'cancel']).forEach(function(action) {
4046
4460
  var isPrimary = action === 'save';
@@ -4052,7 +4466,7 @@ function renderSmartFormMockup(res) {
4052
4466
  if (tabs.length > 1) {
4053
4467
  html += '<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1.5rem;">';
4054
4468
  tabs.forEach(function(tab, i) {
4055
- html += '<span style="padding:0.5rem 1rem;font-size:0.85rem;cursor:pointer;border-bottom:2px solid ' + (i === 0 ? 'var(--primary)' : 'transparent') + ';color:' + (i === 0 ? 'var(--primary-light)' : 'var(--text-muted)') + ';">' + tab.label + '</span>';
4469
+ html += '<span style="padding:0.5rem 1rem;font-size:0.85rem;cursor:pointer;border-bottom:2px solid ' + (i === 0 ? 'var(--primary)' : 'transparent') + ';color:' + (i === 0 ? 'var(--primary-light)' : 'var(--text-muted)') + ';">' + escapeHtml(tab.label) + '</span>';
4056
4470
  });
4057
4471
  html += '</div>';
4058
4472
  }
@@ -4071,7 +4485,7 @@ function renderSmartFormMockup(res) {
4071
4485
  html += '<div class="mock-form-row">';
4072
4486
  row.forEach(function(field) {
4073
4487
  html += '<div class="mock-form-group">';
4074
- html += '<label class="mock-label">' + (field.label || field.field);
4488
+ html += '<label class="mock-label">' + escapeHtml(field.label || field.field);
4075
4489
  if (field.required) html += ' <span style="color:var(--error);">*</span>';
4076
4490
  html += '</label>';
4077
4491
  html += renderFormFieldMockup(field);
@@ -4107,11 +4521,11 @@ function renderSubtableMockup(field) {
4107
4521
  var entity = field.entity || 'Element';
4108
4522
  var html = '<div style="margin:1rem 0;border:1px solid var(--border);border-radius:8px;overflow:hidden;">';
4109
4523
  html += '<div style="display:flex;justify-content:space-between;align-items:center;padding:0.5rem 0.75rem;background:var(--bg-hover);">';
4110
- html += '<span style="font-weight:500;color:var(--text-bright);font-size:0.85rem;">' + entity + '</span>';
4524
+ html += '<span style="font-weight:500;color:var(--text-bright);font-size:0.85rem;">' + escapeHtml(entity) + '</span>';
4111
4525
  html += '<span class="mock-btn" style="font-size:0.75rem;padding:0.2rem 0.5rem;">+ Ajouter</span>';
4112
4526
  html += '</div>';
4113
4527
  html += '<table class="mock-table"><thead><tr>';
4114
- cols.forEach(function(c) { html += '<th>' + c + '</th>'; });
4528
+ cols.forEach(function(c) { html += '<th>' + escapeHtml(c) + '</th>'; });
4115
4529
  html += '</tr></thead><tbody>';
4116
4530
  html += '<tr>' + cols.map(function() { return '<td style="color:var(--text-muted);font-style:italic;">...</td>'; }).join('') + '</tr>';
4117
4531
  html += '</tbody></table></div>';
@@ -4121,7 +4535,7 @@ function renderSubtableMockup(field) {
4121
4535
  /* ---------- SmartCard ---------- */
4122
4536
  function renderSmartCardMockup(res) {
4123
4537
  var columns = res.columns || res.fields || [];
4124
- var html = '<div class="mock-header"><span class="mock-title">' + (res.label || 'Cartes') + '</span></div>';
4538
+ var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Cartes') + '</span></div>';
4125
4539
  html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem;">';
4126
4540
 
4127
4541
  for (var i = 0; i < 4; i++) {
@@ -4129,9 +4543,9 @@ function renderSmartCardMockup(res) {
4129
4543
  columns.forEach(function(col, ci) {
4130
4544
  var label = col.label || col.field || col;
4131
4545
  if (ci === 0) {
4132
- html += '<div style="font-weight:600;color:var(--text-bright);margin-bottom:0.5rem;">' + label + ' #' + (i + 1) + '</div>';
4546
+ html += '<div style="font-weight:600;color:var(--text-bright);margin-bottom:0.5rem;">' + escapeHtml(label) + ' #' + (i + 1) + '</div>';
4133
4547
  } else {
4134
- html += '<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.25rem;">' + label + ': <span style="color:var(--text);">valeur</span></div>';
4548
+ html += '<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.25rem;">' + escapeHtml(label) + ': <span style="color:var(--text);">valeur</span></div>';
4135
4549
  }
4136
4550
  });
4137
4551
  html += '</div>';
@@ -4143,14 +4557,14 @@ function renderSmartCardMockup(res) {
4143
4557
  /* ---------- SmartKanban ---------- */
4144
4558
  function renderSmartKanbanMockup(res) {
4145
4559
  var options = res.options || res.columns || ['À faire', 'En cours', 'Terminé'];
4146
- var html = '<div class="mock-header"><span class="mock-title">' + (res.label || 'Kanban') + '</span></div>';
4560
+ var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Kanban') + '</span></div>';
4147
4561
  html += '<div style="display:flex;gap:1rem;overflow-x:auto;padding-bottom:0.5rem;">';
4148
4562
 
4149
4563
  options.forEach(function(col, ci) {
4150
4564
  var colLabel = typeof col === 'string' ? col : (col.label || col.field || 'Colonne');
4151
4565
  html += '<div style="min-width:200px;flex:1;background:var(--bg-hover);border-radius:8px;padding:0.75rem;">';
4152
4566
  html += '<div style="font-weight:600;font-size:0.85rem;color:var(--text-bright);margin-bottom:0.75rem;display:flex;justify-content:space-between;">';
4153
- html += colLabel + ' <span style="font-size:0.7rem;background:var(--bg-card);padding:0.1rem 0.4rem;border-radius:4px;color:var(--text-muted);">' + (3 - ci) + '</span>';
4567
+ html += escapeHtml(colLabel) + ' <span style="font-size:0.7rem;background:var(--bg-card);padding:0.1rem 0.4rem;border-radius:4px;color:var(--text-muted);">' + (3 - ci) + '</span>';
4154
4568
  html += '</div>';
4155
4569
  for (var j = 0; j < Math.max(1, 3 - ci); j++) {
4156
4570
  html += '<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:6px;padding:0.5rem;margin-bottom:0.5rem;font-size:0.8rem;">';
@@ -4166,7 +4580,7 @@ function renderSmartKanbanMockup(res) {
4166
4580
 
4167
4581
  /* ---------- SmartDashboard ---------- */
4168
4582
  function renderSmartDashboardMockup(res) {
4169
- var html = '<div class="mock-header"><span class="mock-title">' + (res.label || 'Tableau de bord') + '</span></div>';
4583
+ var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Tableau de bord') + '</span></div>';
4170
4584
 
4171
4585
  // KPI cards
4172
4586
  html += '<div class="mock-kpi-grid">';
@@ -4177,7 +4591,7 @@ function renderSmartDashboardMockup(res) {
4177
4591
  { label: 'Taux', value: '80%' }
4178
4592
  ];
4179
4593
  kpis.forEach(function(kpi) {
4180
- html += '<div class="mock-kpi"><div class="mock-kpi-value">' + (kpi.value || '0') + '</div><div class="mock-kpi-label">' + (kpi.label || '') + '</div></div>';
4594
+ html += '<div class="mock-kpi"><div class="mock-kpi-value">' + escapeHtml(kpi.value || '0') + '</div><div class="mock-kpi-label">' + escapeHtml(kpi.label || '') + '</div></div>';
4181
4595
  });
4182
4596
  html += '</div>';
4183
4597
 
@@ -4193,7 +4607,7 @@ function renderSmartFilterMockup(res) {
4193
4607
  var html = '<div style="display:flex;gap:0.4rem;flex-wrap:wrap;padding:0.5rem 0;">';
4194
4608
  html += '<span style="padding:0.3rem 0.7rem;border-radius:16px;font-size:0.8rem;background:var(--primary);color:#fff;cursor:pointer;">Tous</span>';
4195
4609
  options.forEach(function(opt) {
4196
- html += '<span style="padding:0.3rem 0.7rem;border-radius:16px;font-size:0.8rem;background:var(--bg-hover);color:var(--text);border:1px solid var(--border);cursor:pointer;">' + opt + '</span>';
4610
+ html += '<span style="padding:0.3rem 0.7rem;border-radius:16px;font-size:0.8rem;background:var(--bg-hover);color:var(--text);border:1px solid var(--border);cursor:pointer;">' + escapeHtml(opt) + '</span>';
4197
4611
  });
4198
4612
  html += '</div>';
4199
4613
  return html;
@@ -4254,6 +4668,7 @@ function renderHandoff() {
4254
4668
  renderHandoffStats();
4255
4669
  renderHandoffModules();
4256
4670
  renderCoverageMatrix();
4671
+ renderProgressIndicator();
4257
4672
  }
4258
4673
 
4259
4674
  function renderHandoffStats() {
@@ -4292,8 +4707,8 @@ function renderHandoffModules() {
4292
4707
  <div style="display:flex;align-items:center;gap:0.75rem;">
4293
4708
  <div style="width:28px;height:28px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;color:#fff;font-size:0.75rem;font-weight:700;flex-shrink:0;">${i + 1}</div>
4294
4709
  <div style="flex:1;">
4295
- <div style="font-weight:600;color:var(--text-bright);">${m.name}</div>
4296
- <div style="font-size:0.8rem;color:var(--text-muted);">${m.description || ''}</div>
4710
+ <div style="font-weight:600;color:var(--text-bright);">${escapeHtml(m.name)}</div>
4711
+ <div style="font-size:0.8rem;color:var(--text-muted);">${escapeHtml(m.description || '')}</div>
4297
4712
  </div>
4298
4713
  <div style="display:flex;gap:1rem;font-size:0.75rem;color:var(--text-muted);">
4299
4714
  <span>${(spec.useCases || []).length} cas d'utilisation</span>
@@ -4326,24 +4741,47 @@ function renderCoverageMatrix() {
4326
4741
  : (data.modules.length > 0 ? data.modules.map(m => m.name).join(', ') : 'À définir');
4327
4742
  return `
4328
4743
  <tr>
4329
- <td>${item.name}</td>
4744
+ <td>${escapeHtml(item.name)}</td>
4330
4745
  <td><span class="priority priority-${item.priority}">${formatPriority(item.priority)}</span></td>
4331
- <td style="color:var(--text-muted);">${moduleName}</td>
4746
+ <td style="color:var(--text-muted);">${escapeHtml(moduleName)}</td>
4332
4747
  <td style="text-align:center;color:var(--success);">&#10003;</td>
4333
4748
  </tr>`;
4334
4749
  }).join('')}
4335
4750
  </tbody>
4336
4751
  </table>`;
4337
4752
  }
4753
+
4754
+ function renderProgressIndicator() {
4755
+ var container = document.getElementById('progressIndicator');
4756
+ if (!container) return;
4757
+ var p = computeProgress();
4758
+ var html = '<div class="progress-bar-container">';
4759
+ html += '<div class="progress-bar-fill" style="width:' + p.pct + '%;"></div>';
4760
+ html += '</div>';
4761
+ html += '<div class="progress-label">' + p.pct + '% complet — ' + p.doneCount + '/' + p.total + ' critères</div>';
4762
+ html += '<div class="progress-checks">';
4763
+ p.checks.forEach(function(c) {
4764
+ html += '<div class="progress-check ' + (c.done ? 'done' : '') + '">';
4765
+ html += '<span class="progress-check-icon">' + (c.done ? '&#10003;' : '&#9675;') + '</span> ';
4766
+ html += escapeHtml(c.label);
4767
+ html += '</div>';
4768
+ });
4769
+ html += '</div>';
4770
+ container.innerHTML = html;
4771
+ }
4338
4772
 
4339
4773
 
4340
4774
  /* --- 08-editing.js --- */
4341
4775
  /* ============================================
4342
4776
  PERSISTENCE
4343
4777
  ============================================ */
4778
+ let _saveTimer;
4344
4779
  function autoSave() {
4345
- data.metadata.lastModified = new Date().toISOString();
4346
- localStorage.setItem(APP_KEY, JSON.stringify(data));
4780
+ clearTimeout(_saveTimer);
4781
+ _saveTimer = setTimeout(function() {
4782
+ data.metadata.lastModified = new Date().toISOString();
4783
+ localStorage.setItem(APP_KEY, JSON.stringify(data));
4784
+ }, 500);
4347
4785
  }
4348
4786
 
4349
4787
  function saveToLocalStorage() {
@@ -4477,13 +4915,10 @@ function loadFromLocalStorage() {
4477
4915
  /* ============================================
4478
4916
  EXPORT JSON
4479
4917
  ============================================ */
4480
- function exportJSON() {
4481
- // Collect all editable fields (cadrage)
4918
+ function collectEditableFields() {
4482
4919
  document.querySelectorAll('.editable[data-field]').forEach(el => {
4483
4920
  setNestedValue(data, 'cadrage.' + el.dataset.field, el.textContent.trim());
4484
4921
  });
4485
-
4486
- // Collect module editable fields
4487
4922
  document.querySelectorAll('.editable[data-module-field]').forEach(el => {
4488
4923
  const code = el.dataset.moduleCode;
4489
4924
  const field = el.dataset.moduleField;
@@ -4491,17 +4926,39 @@ function exportJSON() {
4491
4926
  data.moduleSpecs[code][field] = el.textContent.trim();
4492
4927
  }
4493
4928
  });
4929
+ }
4930
+
4931
+ function buildModuleSpecsExport() {
4932
+ var result = {};
4933
+ data.modules.forEach(function(m) {
4934
+ var spec = data.moduleSpecs[m.code] || {};
4935
+ result[m.code] = {
4936
+ module: m,
4937
+ useCases: (spec.useCases || []).map(function(uc, i) {
4938
+ return Object.assign({ id: 'UC-' + String(i + 1).padStart(3, '0') }, uc);
4939
+ }),
4940
+ businessRules: (spec.businessRules || []).map(function(br, i) {
4941
+ return Object.assign({ id: 'BR-' + (br.category || 'GEN').toUpperCase().substring(0, 4) + '-' + String(i + 1).padStart(3, '0') }, br);
4942
+ }),
4943
+ entities: spec.entities || [],
4944
+ permissions: spec.permissions || [],
4945
+ notes: spec.notes || ''
4946
+ };
4947
+ });
4948
+ return result;
4949
+ }
4494
4950
 
4951
+ function exportJSON() {
4952
+ collectEditableFields();
4495
4953
  data.metadata.lastModified = new Date().toISOString();
4496
4954
  data.metadata.exportedAt = new Date().toISOString();
4497
4955
 
4498
- // Build complete export with structured data
4499
4956
  const exportData = {
4500
4957
  metadata: data.metadata,
4501
4958
  cadrage: data.cadrage,
4502
4959
  modules: data.modules,
4503
4960
  dependencies: data.dependencies,
4504
- moduleSpecifications: {},
4961
+ moduleSpecifications: buildModuleSpecsExport(),
4505
4962
  consolidation: data.consolidation,
4506
4963
  artifacts: EMBEDDED_ARTIFACTS,
4507
4964
  wireframeComments: data.wireframeComments,
@@ -4511,25 +4968,6 @@ function exportJSON() {
4511
4968
  comments: data.comments
4512
4969
  };
4513
4970
 
4514
- // Structure module specs for export
4515
- data.modules.forEach(m => {
4516
- const spec = data.moduleSpecs[m.code] || {};
4517
- exportData.moduleSpecifications[m.code] = {
4518
- module: m,
4519
- useCases: (spec.useCases || []).map((uc, i) => ({
4520
- id: 'UC-' + String(i + 1).padStart(3, '0'),
4521
- ...uc
4522
- })),
4523
- businessRules: (spec.businessRules || []).map((br, i) => ({
4524
- id: 'BR-' + br.category.toUpperCase().substring(0, 4) + '-' + String(i + 1).padStart(3, '0'),
4525
- ...br
4526
- })),
4527
- entities: spec.entities || [],
4528
- permissions: spec.permissions || [],
4529
- notes: spec.notes || ''
4530
- };
4531
- });
4532
-
4533
4971
  const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
4534
4972
  const url = URL.createObjectURL(blob);
4535
4973
  const a = document.createElement('a');
@@ -4569,24 +5007,13 @@ function detectChanges(original, current) {
4569
5007
  }
4570
5008
 
4571
5009
  function saveReviewJSON() {
4572
- // Collect all editable fields (same as exportJSON)
4573
- document.querySelectorAll('.editable[data-field]').forEach(el => {
4574
- setNestedValue(data, 'cadrage.' + el.dataset.field, el.textContent.trim());
4575
- });
4576
- document.querySelectorAll('.editable[data-module-field]').forEach(el => {
4577
- const code = el.dataset.moduleCode;
4578
- const field = el.dataset.moduleField;
4579
- if (data.moduleSpecs[code]) {
4580
- data.moduleSpecs[code][field] = el.textContent.trim();
4581
- }
4582
- });
5010
+ collectEditableFields();
4583
5011
 
4584
5012
  const changes = detectChanges(ORIGINAL_DATA, data);
4585
5013
  const hasChanges = changes.cadrage || changes.modulesAdded.length > 0
4586
5014
  || changes.modulesRemoved.length > 0 || changes.modulesModified.length > 0
4587
5015
  || changes.commentsCount > 0;
4588
5016
 
4589
- // Build review export with metadata envelope
4590
5017
  const reviewData = {
4591
5018
  _reviewMeta: {
4592
5019
  sourceVersion: data.metadata.version || '1.0',
@@ -4605,7 +5032,7 @@ function saveReviewJSON() {
4605
5032
  cadrage: data.cadrage,
4606
5033
  modules: data.modules,
4607
5034
  dependencies: data.dependencies,
4608
- moduleSpecifications: {},
5035
+ moduleSpecifications: buildModuleSpecsExport(),
4609
5036
  consolidation: data.consolidation,
4610
5037
  wireframeComments: data.wireframeComments,
4611
5038
  specComments: data.specComments,
@@ -4614,25 +5041,6 @@ function saveReviewJSON() {
4614
5041
  comments: data.comments
4615
5042
  };
4616
5043
 
4617
- // Structure module specs (same logic as exportJSON)
4618
- data.modules.forEach(m => {
4619
- const spec = data.moduleSpecs[m.code] || {};
4620
- reviewData.moduleSpecifications[m.code] = {
4621
- module: m,
4622
- useCases: (spec.useCases || []).map((uc, i) => ({
4623
- id: 'UC-' + String(i + 1).padStart(3, '0'),
4624
- ...uc
4625
- })),
4626
- businessRules: (spec.businessRules || []).map((br, i) => ({
4627
- id: 'BR-' + br.category.toUpperCase().substring(0, 4) + '-' + String(i + 1).padStart(3, '0'),
4628
- ...br
4629
- })),
4630
- entities: spec.entities || [],
4631
- permissions: spec.permissions || [],
4632
- notes: spec.notes || ''
4633
- };
4634
- });
4635
-
4636
5044
  const blob = new Blob([JSON.stringify(reviewData, null, 2)], { type: 'application/json' });
4637
5045
  const url = URL.createObjectURL(blob);
4638
5046
  const a = document.createElement('a');
@@ -4651,14 +5059,24 @@ function saveReviewJSON() {
4651
5059
 
4652
5060
  /**
4653
5061
  * Comments are stored in data.comments[] with structure:
4654
- * { id, sectionId, cardIndex, author, timestamp, content, status, category }
5062
+ * { id, sectionId, elementId, author, timestamp, content, status, category }
4655
5063
  *
4656
- * - sectionId: matches the section div id (e.g. "cadrage-problem", "module-spec-Clients")
4657
- * - cardIndex: index of the card within that section (0-based)
5064
+ * - sectionId: matches the section div id (e.g. "cadrage-context", "module-spec-Clients")
5065
+ * - elementId: stable identifier for the element (name, role, or generated id)
5066
+ * - cardIndex: (deprecated, kept for backward compat) positional index
4658
5067
  * - status: "to-review" | "validated"
4659
5068
  * - category: "clarification" | "correction" | "suggestion"
4660
5069
  */
4661
5070
 
5071
+ function getElementId(item, sectionId, index) {
5072
+ // Extract a stable ID from the element's data
5073
+ var title = item.querySelector('.uc-title, .entity-name, .stakeholder-role, .card-title');
5074
+ if (title) return title.textContent.trim();
5075
+ var nameEl = item.querySelector('[data-field]');
5076
+ if (nameEl) return nameEl.dataset.field;
5077
+ return sectionId + '-' + index;
5078
+ }
5079
+
4662
5080
  function initInlineComments() {
4663
5081
  // Cadrage sections: direct card children
4664
5082
  document.querySelectorAll('.section > .card, .section > .stakeholder-card, .section > .uc-item').forEach(function(card) {
@@ -4668,7 +5086,8 @@ function initInlineComments() {
4668
5086
  var sectionId = section ? section.id : 'unknown';
4669
5087
  var siblings = Array.from(section.querySelectorAll(':scope > .card, :scope > .stakeholder-card, :scope > .uc-item'));
4670
5088
  var index = siblings.indexOf(card);
4671
- card.appendChild(createCommentUI(sectionId, index));
5089
+ var elementId = getElementId(card, sectionId, index);
5090
+ card.appendChild(createCommentUI(sectionId, elementId));
4672
5091
  });
4673
5092
 
4674
5093
  // Module spec lists: nested items in ucList, brList, entList containers
@@ -4679,7 +5098,8 @@ function initInlineComments() {
4679
5098
  var listId = list.id;
4680
5099
  var siblings = Array.from(list.children);
4681
5100
  var index = siblings.indexOf(item);
4682
- item.appendChild(createCommentUI(listId, index));
5101
+ var elementId = getElementId(item, listId, index);
5102
+ item.appendChild(createCommentUI(listId, elementId));
4683
5103
  });
4684
5104
 
4685
5105
  // Stakeholder cards in grid
@@ -4691,7 +5111,8 @@ function initInlineComments() {
4691
5111
  var sectionId = section ? section.id : 'stakeholders';
4692
5112
  var siblings = Array.from(grid.children);
4693
5113
  var index = siblings.indexOf(card);
4694
- card.appendChild(createCommentUI(sectionId, index));
5114
+ var elementId = getElementId(card, sectionId, index);
5115
+ card.appendChild(createCommentUI(sectionId, elementId));
4695
5116
  });
4696
5117
 
4697
5118
  // Scope items
@@ -4701,36 +5122,39 @@ function initInlineComments() {
4701
5122
  container.querySelectorAll('.uc-item').forEach(function(item, index) {
4702
5123
  if (item.dataset.commentInitialized) return;
4703
5124
  item.dataset.commentInitialized = 'true';
4704
- item.appendChild(createCommentUI(containerId, index));
5125
+ var elementId = getElementId(item, containerId, index);
5126
+ item.appendChild(createCommentUI(containerId, elementId));
4705
5127
  });
4706
5128
  });
4707
5129
  }
4708
5130
 
4709
- function createCommentUI(sectionId, cardIndex) {
4710
- const comments = getCommentsForCard(sectionId, cardIndex);
5131
+ function createCommentUI(sectionId, elementId) {
5132
+ const comments = getCommentsForCard(sectionId, elementId);
4711
5133
  const count = comments.length;
5134
+ const safeElId = String(elementId).replace(/[^a-zA-Z0-9_-]/g, '_');
5135
+ const threadId = 'comment-thread-' + sectionId + '-' + safeElId;
4712
5136
 
4713
5137
  const container = document.createElement('div');
4714
5138
  container.className = 'comment-btn-container';
4715
5139
  container.dataset.sectionId = sectionId;
4716
- container.dataset.cardIndex = cardIndex;
5140
+ container.dataset.elementId = elementId;
4717
5141
 
4718
5142
  container.innerHTML = `
4719
- <button class="comment-toggle-btn" onclick="toggleCommentThread('${sectionId}', ${cardIndex})">
5143
+ <button class="comment-toggle-btn" onclick="toggleCommentThread('${sectionId}', '${safeElId}')">
4720
5144
  Commentaires <span class="comment-count ${count === 0 ? 'empty' : ''}">${count}</span>
4721
5145
  </button>
4722
- <div class="comment-thread" id="comment-thread-${sectionId}-${cardIndex}">
4723
- <div class="comment-items" id="comment-items-${sectionId}-${cardIndex}">
4724
- ${renderCommentItems(sectionId, cardIndex)}
5146
+ <div class="comment-thread" id="${threadId}">
5147
+ <div class="comment-items" id="comment-items-${sectionId}-${safeElId}">
5148
+ ${renderCommentItems(sectionId, elementId)}
4725
5149
  </div>
4726
5150
  <div class="comment-add-form">
4727
- <textarea id="comment-text-${sectionId}-${cardIndex}" placeholder="Ajouter un commentaire..."></textarea>
4728
- <select id="comment-cat-${sectionId}-${cardIndex}">
5151
+ <textarea id="comment-text-${sectionId}-${safeElId}" placeholder="Ajouter un commentaire..."></textarea>
5152
+ <select id="comment-cat-${sectionId}-${safeElId}">
4729
5153
  <option value="clarification">Clarification</option>
4730
5154
  <option value="correction">Correction</option>
4731
5155
  <option value="suggestion">Suggestion</option>
4732
5156
  </select>
4733
- <button onclick="addInlineComment('${sectionId}', ${cardIndex})">Ajouter</button>
5157
+ <button onclick="addInlineComment('${sectionId}', '${elementId.replace(/'/g, "\\'")}')">Ajouter</button>
4734
5158
  </div>
4735
5159
  </div>
4736
5160
  `;
@@ -4738,25 +5162,27 @@ function createCommentUI(sectionId, cardIndex) {
4738
5162
  return container;
4739
5163
  }
4740
5164
 
4741
- function getCommentsForCard(sectionId, cardIndex) {
4742
- return (data.comments || []).filter(c =>
4743
- c.sectionId === sectionId && c.cardIndex === cardIndex
4744
- );
5165
+ function getCommentsForCard(sectionId, elementId) {
5166
+ return (data.comments || []).filter(function(c) {
5167
+ if (c.elementId) return c.sectionId === sectionId && c.elementId === elementId;
5168
+ // Backward compat: match by cardIndex if elementId not set
5169
+ return c.sectionId === sectionId && c.cardIndex === elementId;
5170
+ });
4745
5171
  }
4746
5172
 
4747
- function toggleCommentThread(sectionId, cardIndex) {
4748
- const thread = document.getElementById('comment-thread-' + sectionId + '-' + cardIndex);
5173
+ function toggleCommentThread(sectionId, safeElId) {
5174
+ const thread = document.getElementById('comment-thread-' + sectionId + '-' + safeElId);
4749
5175
  if (thread) thread.classList.toggle('visible');
4750
5176
  }
4751
5177
 
4752
- function renderCommentItems(sectionId, cardIndex) {
4753
- const comments = getCommentsForCard(sectionId, cardIndex);
5178
+ function renderCommentItems(sectionId, elementId) {
5179
+ const comments = getCommentsForCard(sectionId, elementId);
4754
5180
  if (comments.length === 0) {
4755
5181
  return '<div style="font-size:0.8rem;color:var(--text-muted);padding:0.5rem 0;font-style:italic;">Aucun commentaire</div>';
4756
5182
  }
4757
5183
 
4758
- return comments.map((c, i) => {
4759
- const initials = (c.author || 'U').substring(0, 2).toUpperCase();
5184
+ return comments.map(function(c, i) {
5185
+ const initials = escapeHtml((c.author || 'U').substring(0, 2).toUpperCase());
4760
5186
  const date = c.timestamp ? new Date(c.timestamp).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
4761
5187
  const globalIndex = data.comments.indexOf(c);
4762
5188
 
@@ -4765,12 +5191,12 @@ function renderCommentItems(sectionId, cardIndex) {
4765
5191
  <div class="comment-avatar">${initials}</div>
4766
5192
  <div class="comment-body">
4767
5193
  <div class="comment-meta">
4768
- <span class="comment-author">${c.author || 'Utilisateur'}</span>
4769
- <span class="comment-date">${date}</span>
4770
- <span class="comment-category comment-category-${c.category}">${c.category}</span>
5194
+ <span class="comment-author">${escapeHtml(c.author || 'Utilisateur')}</span>
5195
+ <span class="comment-date">${escapeHtml(date)}</span>
5196
+ <span class="comment-category comment-category-${escapeHtml(c.category)}">${escapeHtml(c.category)}</span>
4771
5197
  <span class="comment-status comment-status-${c.status}">${c.status === 'validated' ? 'Validé' : 'À revoir'}</span>
4772
5198
  </div>
4773
- <div class="comment-text">${c.content}</div>
5199
+ <div class="comment-text">${escapeHtml(c.content)}</div>
4774
5200
  <div class="comment-actions">
4775
5201
  <button class="comment-action-btn" onclick="toggleCommentStatus(${globalIndex})">${c.status === 'validated' ? 'Remettre à revoir' : 'Valider'}</button>
4776
5202
  <button class="comment-action-btn" onclick="deleteComment(${globalIndex})" style="color:var(--error);">Supprimer</button>
@@ -4781,16 +5207,17 @@ function renderCommentItems(sectionId, cardIndex) {
4781
5207
  }).join('');
4782
5208
  }
4783
5209
 
4784
- function addInlineComment(sectionId, cardIndex) {
4785
- const textEl = document.getElementById('comment-text-' + sectionId + '-' + cardIndex);
4786
- const catEl = document.getElementById('comment-cat-' + sectionId + '-' + cardIndex);
5210
+ function addInlineComment(sectionId, elementId) {
5211
+ var safeElId = String(elementId).replace(/[^a-zA-Z0-9_-]/g, '_');
5212
+ const textEl = document.getElementById('comment-text-' + sectionId + '-' + safeElId);
5213
+ const catEl = document.getElementById('comment-cat-' + sectionId + '-' + safeElId);
4787
5214
  const content = textEl.value.trim();
4788
5215
  if (!content) return;
4789
5216
 
4790
5217
  const comment = {
4791
5218
  id: 'comment-' + Date.now(),
4792
5219
  sectionId: sectionId,
4793
- cardIndex: cardIndex,
5220
+ elementId: elementId,
4794
5221
  author: 'Utilisateur',
4795
5222
  timestamp: new Date().toISOString(),
4796
5223
  content: content,
@@ -4801,7 +5228,7 @@ function addInlineComment(sectionId, cardIndex) {
4801
5228
  data.comments.push(comment);
4802
5229
  textEl.value = '';
4803
5230
 
4804
- refreshCommentUI(sectionId, cardIndex);
5231
+ refreshCommentUI(sectionId, elementId);
4805
5232
  renderReviewPanel();
4806
5233
  autoSave();
4807
5234
  }
@@ -4810,32 +5237,35 @@ function toggleCommentStatus(globalIndex) {
4810
5237
  const comment = data.comments[globalIndex];
4811
5238
  if (!comment) return;
4812
5239
  comment.status = comment.status === 'validated' ? 'to-review' : 'validated';
4813
- refreshCommentUI(comment.sectionId, comment.cardIndex);
5240
+ var elId = comment.elementId || comment.cardIndex;
5241
+ refreshCommentUI(comment.sectionId, elId);
4814
5242
  renderReviewPanel();
4815
5243
  autoSave();
4816
5244
  }
4817
5245
 
4818
5246
  function deleteComment(globalIndex) {
5247
+ if (!confirm('Supprimer ce commentaire ?')) return;
4819
5248
  const comment = data.comments[globalIndex];
4820
5249
  if (!comment) return;
4821
- const sectionId = comment.sectionId;
4822
- const cardIndex = comment.cardIndex;
5250
+ var sectionId = comment.sectionId;
5251
+ var elId = comment.elementId || comment.cardIndex;
4823
5252
  data.comments.splice(globalIndex, 1);
4824
- refreshCommentUI(sectionId, cardIndex);
5253
+ refreshCommentUI(sectionId, elId);
4825
5254
  renderReviewPanel();
4826
5255
  autoSave();
4827
5256
  }
4828
5257
 
4829
- function refreshCommentUI(sectionId, cardIndex) {
5258
+ function refreshCommentUI(sectionId, elementId) {
5259
+ var safeElId = String(elementId).replace(/[^a-zA-Z0-9_-]/g, '_');
4830
5260
  // Update comment items
4831
- const itemsContainer = document.getElementById('comment-items-' + sectionId + '-' + cardIndex);
5261
+ const itemsContainer = document.getElementById('comment-items-' + sectionId + '-' + safeElId);
4832
5262
  if (itemsContainer) {
4833
- itemsContainer.innerHTML = renderCommentItems(sectionId, cardIndex);
5263
+ itemsContainer.innerHTML = renderCommentItems(sectionId, elementId);
4834
5264
  }
4835
5265
 
4836
5266
  // Update count badge
4837
- const count = getCommentsForCard(sectionId, cardIndex).length;
4838
- const container = document.querySelector(`.comment-btn-container[data-section-id="${sectionId}"][data-card-index="${cardIndex}"]`);
5267
+ const count = getCommentsForCard(sectionId, elementId).length;
5268
+ const container = document.querySelector('.comment-btn-container[data-section-id="' + sectionId + '"][data-element-id="' + elementId + '"]');
4839
5269
  if (container) {
4840
5270
  const badge = container.querySelector('.comment-count');
4841
5271
  if (badge) {
@@ -4918,17 +5348,19 @@ function renderReviewPanel() {
4918
5348
  return;
4919
5349
  }
4920
5350
 
4921
- container.innerHTML = filtered.map((c, i) => {
5351
+ container.innerHTML = filtered.map(function(c, i) {
4922
5352
  const globalIndex = data.comments.indexOf(c);
4923
5353
  const sectionLabel = getSectionLabel(c.sectionId);
4924
5354
  const date = c.timestamp ? new Date(c.timestamp).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }) : '';
5355
+ var elId = c.elementId || c.cardIndex;
5356
+ var safeElId = String(elId).replace(/[^a-zA-Z0-9_-]/g, '_');
4925
5357
 
4926
5358
  return `
4927
- <div class="review-comment-item" onclick="navigateToComment('${c.sectionId}', ${c.cardIndex})">
4928
- <div class="review-comment-section">${sectionLabel}</div>
4929
- <div class="review-comment-text">${c.content}</div>
5359
+ <div class="review-comment-item" onclick="navigateToComment('${c.sectionId}', '${safeElId}')">
5360
+ <div class="review-comment-section">${escapeHtml(sectionLabel)}</div>
5361
+ <div class="review-comment-text">${escapeHtml(c.content)}</div>
4930
5362
  <div class="review-comment-footer">
4931
- <span class="review-comment-author">${c.author || 'Utilisateur'} - ${date}</span>
5363
+ <span class="review-comment-author">${escapeHtml(c.author || 'Utilisateur')} - ${escapeHtml(date)}</span>
4932
5364
  <div class="review-comment-actions">
4933
5365
  <button class="review-action-btn ${c.status === 'validated' ? 'reject' : 'validate'}"
4934
5366
  onclick="event.stopPropagation();toggleCommentStatus(${globalIndex})"
@@ -4984,29 +5416,29 @@ function getSectionLabel(sectionId) {
4984
5416
  return sectionId;
4985
5417
  }
4986
5418
 
4987
- function navigateToComment(sectionId, cardIndex) {
5419
+ function navigateToComment(sectionId, safeElId) {
4988
5420
  // Handle list-based sectionIds (ucList-*, brList-*, entList-*) → navigate to module + tab
4989
5421
  const listMatch = sectionId.match(/^(uc|br|ent)List-(.+)$/);
4990
5422
  if (listMatch) {
4991
5423
  const [, tabType, moduleCode] = listMatch;
4992
5424
  showSection('module-spec-' + moduleCode);
4993
- setTimeout(() => {
5425
+ setTimeout(function() {
4994
5426
  switchTab(moduleCode, tabType);
4995
- scrollToCommentThread(sectionId, cardIndex);
5427
+ scrollToCommentThread(sectionId, safeElId);
4996
5428
  }, 150);
4997
5429
  } else {
4998
5430
  showSection(sectionId);
4999
- scrollToCommentThread(sectionId, cardIndex);
5431
+ scrollToCommentThread(sectionId, safeElId);
5000
5432
  }
5001
5433
  }
5002
5434
 
5003
- function scrollToCommentThread(sectionId, cardIndex) {
5004
- setTimeout(() => {
5005
- const thread = document.getElementById('comment-thread-' + sectionId + '-' + cardIndex);
5435
+ function scrollToCommentThread(sectionId, safeElId) {
5436
+ setTimeout(function() {
5437
+ const thread = document.getElementById('comment-thread-' + sectionId + '-' + safeElId);
5006
5438
  if (thread && !thread.classList.contains('visible')) {
5007
5439
  thread.classList.add('visible');
5008
5440
  }
5009
- const container = document.querySelector(`.comment-btn-container[data-section-id="${sectionId}"][data-card-index="${cardIndex}"]`);
5441
+ const container = document.querySelector('.comment-btn-container[data-section-id="' + sectionId + '"][data-element-id]');
5010
5442
  if (container) {
5011
5443
  container.scrollIntoView({ behavior: 'smooth', block: 'center' });
5012
5444
  }