@atlashub/smartstack-cli 4.41.0 → 4.43.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 +24 -9
  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 -22
  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 +15 -6
  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 +7 -7
  74. package/templates/skills/{derive-prd → business-analyse-handoff}/steps/step-01-transform.md +50 -11
  75. package/templates/skills/{derive-prd → business-analyse-handoff}/steps/step-02-export.md +36 -16
  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 +714 -286
  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 +102 -12
  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
  ============================================ */
@@ -2521,35 +2736,34 @@ function renderModuleNavItem(mod) {
2521
2736
  var html = '<div class="nav-module" data-group-id="' + groupId + '">';
2522
2737
 
2523
2738
  // Module header (clickable to expand + navigate to module spec)
2739
+ var totalItems = ucCount + brCount + entCount;
2524
2740
  html += '<a class="nav-item nav-module-header" onclick="toggleNavGroup(\'' + groupId + '\');showSection(\'module-spec-' + code + '\')" data-section="module-spec-' + code + '">';
2525
2741
  html += '<span class="nav-chevron ' + (collapsed ? '' : 'expanded') + '">&#9656;</span> ';
2526
2742
  html += (mod.name || mod.code);
2743
+ if (totalItems > 0) html += ' <span class="nav-badge">' + totalItems + '</span>';
2527
2744
  html += '</a>';
2528
2745
 
2529
- // Children: tabs
2746
+ // Children: sections/resources (navigable sub-tree)
2747
+ // Tab-level items (UC, BR, Données, etc.) are NOT shown here — they are accessed
2748
+ // via the tab bar in the module content area to avoid redundancy.
2530
2749
  html += '<div class="nav-children"' + (collapsed ? ' style="display:none;"' : '') + '>';
2531
- html += renderModuleTabNavItem(code, 'uc', 'Cas d\'utilisation', ucCount);
2532
- html += renderModuleTabNavItem(code, 'br', 'Règles métier', brCount);
2533
- html += renderModuleTabNavItem(code, 'ent', 'Données', entCount);
2534
- html += renderModuleTabNavItem(code, 'perm', 'Droits d\'accès');
2535
- html += renderModuleTabNavItem(code, 'mock', 'Maquettes');
2536
- html += renderModuleTabNavItem(code, 'struct', 'Structure', sections.length);
2537
-
2538
- // Children: sections/resources (deeper tree)
2539
2750
  if (sections.length > 0) {
2540
2751
  sections.forEach(function(section) {
2541
2752
  var resources = section.resources || [];
2753
+ var sectionUCs = (section.useCases || []).length;
2754
+ var sectionBRs = (section.businessRules || []).length;
2755
+ var contentCount = sectionUCs + sectionBRs + resources.length;
2542
2756
  html += '<div class="nav-section-item">';
2543
2757
  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>';
2758
+ html += '<span class="nav-icon nav-icon-section">&#9655;</span> ' + escapeHtml(section.code || section.name || '');
2759
+ if (contentCount > 0) html += ' <span class="nav-badge">' + contentCount + '</span>';
2546
2760
  html += '</a>';
2547
2761
  if (resources.length > 0) {
2548
2762
  html += '<div class="nav-children nav-resources">';
2549
2763
  resources.forEach(function(res) {
2550
2764
  var resName = typeof res === 'string' ? res : (res.code || res.name || '');
2551
2765
  html += '<a class="nav-item nav-resource-link">';
2552
- html += '<span class="nav-icon nav-icon-resource">&#8226;</span> ' + resName;
2766
+ html += '<span class="nav-icon nav-icon-resource">&#8226;</span> ' + escapeHtml(resName);
2553
2767
  html += '</a>';
2554
2768
  });
2555
2769
  html += '</div>';
@@ -2615,6 +2829,97 @@ function highlightActiveNavItem() {
2615
2829
  var navItem = document.querySelector('#sidebarNav [data-section="' + currentSectionId + '"]');
2616
2830
  if (navItem) navItem.classList.add('active');
2617
2831
  }
2832
+
2833
+ /* ---------- Mobile Sidebar ---------- */
2834
+
2835
+ function toggleMobileSidebar() {
2836
+ var sidebar = document.getElementById('sidebarAside');
2837
+ var overlay = document.getElementById('sidebarOverlay');
2838
+ if (!sidebar) return;
2839
+ var isOpen = sidebar.classList.contains('mobile-open');
2840
+ sidebar.classList.toggle('mobile-open', !isOpen);
2841
+ if (overlay) overlay.classList.toggle('visible', !isOpen);
2842
+ }
2843
+
2844
+ /* ---------- Search / Filter ---------- */
2845
+
2846
+ var _searchTimer;
2847
+ function filterNavItems(query) {
2848
+ clearTimeout(_searchTimer);
2849
+ _searchTimer = setTimeout(function() { doFilterNavItems(query); }, 200);
2850
+ }
2851
+
2852
+ function doFilterNavItems(query) {
2853
+ var q = (query || '').toLowerCase().trim();
2854
+ var nav = document.getElementById('sidebarNav');
2855
+ if (!nav) return;
2856
+
2857
+ // Clear previous highlights
2858
+ nav.querySelectorAll('.search-highlight').forEach(function(el) {
2859
+ el.outerHTML = el.textContent;
2860
+ });
2861
+
2862
+ if (!q) {
2863
+ // Show all items
2864
+ nav.querySelectorAll('.nav-item, .nav-group, .nav-module, .nav-section-item').forEach(function(el) {
2865
+ el.classList.remove('search-hidden');
2866
+ });
2867
+ return;
2868
+ }
2869
+
2870
+ // Filter nav items
2871
+ var allItems = nav.querySelectorAll('.nav-item');
2872
+ var visibleSections = new Set();
2873
+
2874
+ allItems.forEach(function(item) {
2875
+ var text = item.textContent.toLowerCase();
2876
+ if (text.includes(q)) {
2877
+ item.classList.remove('search-hidden');
2878
+ // Show parent groups
2879
+ var parent = item.parentElement;
2880
+ while (parent && parent.id !== 'sidebarNav') {
2881
+ parent.classList.remove('search-hidden');
2882
+ if (parent.querySelector(':scope > .nav-children')) {
2883
+ var children = parent.querySelector(':scope > .nav-children');
2884
+ if (children) children.style.display = '';
2885
+ }
2886
+ parent = parent.parentElement;
2887
+ }
2888
+ // Track section for content search
2889
+ var sectionId = item.dataset.section;
2890
+ if (sectionId) visibleSections.add(sectionId);
2891
+ } else {
2892
+ item.classList.add('search-hidden');
2893
+ }
2894
+ });
2895
+
2896
+ // Hide groups that have no visible children
2897
+ nav.querySelectorAll('.nav-group, .nav-module').forEach(function(group) {
2898
+ var visibleChildren = group.querySelectorAll('.nav-item:not(.search-hidden)');
2899
+ if (visibleChildren.length === 0) {
2900
+ group.classList.add('search-hidden');
2901
+ } else {
2902
+ group.classList.remove('search-hidden');
2903
+ }
2904
+ });
2905
+
2906
+ // Also search in section content and show matching sections
2907
+ document.querySelectorAll('.section').forEach(function(section) {
2908
+ var text = section.textContent.toLowerCase();
2909
+ if (text.includes(q) && !visibleSections.has(section.id)) {
2910
+ // Unhide corresponding nav item
2911
+ var navItem = nav.querySelector('[data-section="' + section.id + '"]');
2912
+ if (navItem) {
2913
+ navItem.classList.remove('search-hidden');
2914
+ var parent = navItem.parentElement;
2915
+ while (parent && parent.id !== 'sidebarNav') {
2916
+ parent.classList.remove('search-hidden');
2917
+ parent = parent.parentElement;
2918
+ }
2919
+ }
2920
+ }
2921
+ });
2922
+ }
2618
2923
 
2619
2924
 
2620
2925
  /* --- 03-render-cadrage.js --- */
@@ -2662,23 +2967,24 @@ function renderStakeholders() {
2662
2967
  grid.innerHTML = data.cadrage.stakeholders.map((s, i) => `
2663
2968
  <div class="stakeholder-card">
2664
2969
  <div style="display:flex;justify-content:space-between;align-items:start;">
2665
- <div class="stakeholder-role">${s.role}</div>
2970
+ <div class="stakeholder-role">${escapeHtml(s.role)}</div>
2666
2971
  <button class="btn btn-sm" onclick="removeStakeholder(${i})" style="opacity:0.5;font-size:0.7rem;">Supprimer</button>
2667
2972
  </div>
2668
- <div class="stakeholder-function">${s.function || ''}</div>
2973
+ <div class="stakeholder-function">${escapeHtml(s.function || '')}</div>
2669
2974
  <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('')}
2975
+ ${(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
2976
  </ul>
2672
2977
  <div class="stakeholder-meta">
2673
2978
  <span>${formatFrequency(s.frequency)}</span>
2674
2979
  <span>${formatAccess(s.access)}</span>
2675
2980
  </div>
2676
- ${s.frustrations ? '<div style="font-size:0.8rem;color:var(--warning);margin-top:0.5rem;font-style:italic;">' + s.frustrations + '</div>' : ''}
2981
+ ${s.frustrations ? '<div style="font-size:0.8rem;color:var(--warning);margin-top:0.5rem;font-style:italic;">' + escapeHtml(s.frustrations) + '</div>' : ''}
2677
2982
  </div>
2678
2983
  `).join('');
2679
2984
  }
2680
2985
 
2681
2986
  function removeStakeholder(index) {
2987
+ if (!confirm('Supprimer ce profil utilisateur ?')) return;
2682
2988
  data.cadrage.stakeholders.splice(index, 1);
2683
2989
  renderStakeholders();
2684
2990
  updateCounts();
@@ -2709,12 +3015,12 @@ function renderScope() {
2709
3015
  <div class="uc-item">
2710
3016
  <div class="uc-header">
2711
3017
  <span class="priority priority-${p}">${formatPriority(p)}</span>
2712
- <span class="uc-title">${item.name}</span>
3018
+ <span class="uc-title">${escapeHtml(item.name)}</span>
2713
3019
  <div class="uc-actions">
2714
3020
  <button class="btn btn-sm" onclick="removeScopeItem('${p}',${i})">Supprimer</button>
2715
3021
  </div>
2716
3022
  </div>
2717
- ${item.description ? '<div class="uc-detail">' + item.description + '</div>' : ''}
3023
+ ${item.description ? '<div class="uc-detail">' + escapeHtml(item.description) + '</div>' : ''}
2718
3024
  </div>
2719
3025
  `).join('');
2720
3026
  });
@@ -2759,7 +3065,7 @@ function renderCriteria() {
2759
3065
  container.innerHTML = criteria.map((c, i) => `
2760
3066
  <div class="uc-item" style="display:flex;align-items:center;gap:0.75rem;">
2761
3067
  <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>
3068
+ <span style="flex:1;${c.validated ? 'text-decoration:line-through;color:var(--text-muted);' : ''}">${escapeHtml(c.text)}</span>
2763
3069
  <button class="btn btn-sm" onclick="removeCriterion(${i})" style="opacity:0.5;flex-shrink:0;">&#10005;</button>
2764
3070
  </div>
2765
3071
  `).join('');
@@ -2839,10 +3145,10 @@ function renderModules() {
2839
3145
  <div class="module-card" onclick="showSection('module-spec-${m.code}')">
2840
3146
  <button class="module-card-remove" onclick="event.stopPropagation();removeModule(${i})">&#10005;</button>
2841
3147
  <div class="module-card-header">
2842
- <span class="module-card-code">${m.name}</span>
3148
+ <span class="module-card-code">${escapeHtml(m.name)}</span>
2843
3149
  <span class="module-card-type">${formatModuleType(m.featureType)}</span>
2844
3150
  </div>
2845
- <div class="module-card-desc">${m.description || ''}</div>
3151
+ <div class="module-card-desc">${escapeHtml(m.description || '')}</div>
2846
3152
  <div class="module-card-meta">
2847
3153
  <span>${(m.entities || []).length} données</span>
2848
3154
  <span>${(data.moduleSpecs[m.code]?.useCases || []).length} cas d'utilisation</span>
@@ -2899,10 +3205,10 @@ function renderDependencies() {
2899
3205
  const toName = data.modules.find(m => m.code === d.to)?.name || d.to;
2900
3206
  return `
2901
3207
  <div class="interaction-item">
2902
- <span style="font-weight:600;color:var(--text-bright);">${fromName}</span>
3208
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(fromName)}</span>
2903
3209
  <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>
3210
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(toName)}</span>
3211
+ <span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${escapeHtml(d.description || '')}</span>
2906
3212
  <button class="btn btn-sm" onclick="removeDependency(${i})" style="opacity:0.5;">Supprimer</button>
2907
3213
  </div>
2908
3214
  `;
@@ -2931,7 +3237,7 @@ function renderDepGraph() {
2931
3237
  <div class="dep-layer-modules">
2932
3238
  ${layer.map(code => {
2933
3239
  const m = data.modules.find(mod => mod.code === code);
2934
- return `<div class="dep-module">${m ? (m.name || m.code) : code}</div>`;
3240
+ return `<div class="dep-module">${escapeHtml(m ? (m.name || m.code) : code)}</div>`;
2935
3241
  }).join('')}
2936
3242
  </div>
2937
3243
  </div>
@@ -2979,7 +3285,7 @@ function renderProcessingOrder() {
2979
3285
  return `
2980
3286
  <div class="process-step">
2981
3287
  <div class="process-step-number">Étape ${i + 1}</div>
2982
- <div class="process-step-label">${m ? (m.name || m.code) : code}</div>
3288
+ <div class="process-step-label">${escapeHtml(m ? (m.name || m.code) : code)}</div>
2983
3289
  </div>
2984
3290
  ${i < order.length - 1 ? '<div class="process-arrow">&#8594;</div>' : ''}
2985
3291
  `;
@@ -3017,23 +3323,24 @@ function renderModuleSpecSection(mod) {
3017
3323
 
3018
3324
  return `
3019
3325
  <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>
3326
+ <h2 class="section-title">${escapeHtml(mod.name)}</h2>
3327
+ <p class="section-subtitle">${escapeHtml(mod.description || 'Spécification détaillée de ce domaine fonctionnel.')}</p>
3328
+
3329
+ <div class="tab-bar" role="tablist" aria-label="Spécifications du module ${escapeHtml(mod.name)}">
3330
+ <button class="tab-btn active" role="tab" aria-selected="true" aria-controls="tab-${code}-uc" onclick="switchTab('${code}', 'uc')">Cas d'utilisation</button>
3331
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-br" onclick="switchTab('${code}', 'br')">Règles métier</button>
3332
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-ent" onclick="switchTab('${code}', 'ent')">Données</button>
3333
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-perm" onclick="switchTab('${code}', 'perm')">Droits d'accès</button>
3334
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-mock" onclick="switchTab('${code}', 'mock')">Maquettes</button>
3335
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-notes" onclick="switchTab('${code}', 'notes')">Notes</button>
3336
+ <button class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-${code}-struct" onclick="switchTab('${code}', 'struct')">Structure</button>
3031
3337
  </div>
3032
3338
 
3033
3339
  <!-- TAB: Cas d'utilisation -->
3034
- <div class="tab-panel active" id="tab-${code}-uc">
3340
+ <div class="tab-panel active" id="tab-${code}-uc" role="tabpanel" aria-hidden="false">
3035
3341
  <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
3342
  <div id="ucList-${code}" class="uc-list">
3343
+ ${hasHierarchicalSpecs(mod) ? renderSectionGroupedItems(mod, 'useCases', code, renderUseCase) : ''}
3037
3344
  ${spec.useCases.map((uc, i) => renderUseCase(code, uc, i)).join('')}
3038
3345
  </div>
3039
3346
  <button class="add-btn" onclick="toggleForm('addUcForm-${code}')">+ Ajouter un cas d'utilisation</button>
@@ -3063,9 +3370,10 @@ function renderModuleSpecSection(mod) {
3063
3370
  </div>
3064
3371
 
3065
3372
  <!-- TAB: Règles métier -->
3066
- <div class="tab-panel" id="tab-${code}-br">
3373
+ <div class="tab-panel" id="tab-${code}-br" role="tabpanel" aria-hidden="true">
3067
3374
  <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
3375
  <div id="brList-${code}">
3376
+ ${hasHierarchicalSpecs(mod) ? renderSectionGroupedItems(mod, 'businessRules', code, renderBusinessRule) : ''}
3069
3377
  ${spec.businessRules.map((br, i) => renderBusinessRule(code, br, i)).join('')}
3070
3378
  </div>
3071
3379
  <button class="add-btn" onclick="toggleForm('addBrForm-${code}')">+ Ajouter une règle métier</button>
@@ -3101,7 +3409,7 @@ function renderModuleSpecSection(mod) {
3101
3409
  </div>
3102
3410
 
3103
3411
  <!-- TAB: Données (Entités) -->
3104
- <div class="tab-panel" id="tab-${code}-ent">
3412
+ <div class="tab-panel" id="tab-${code}-ent" role="tabpanel" aria-hidden="true">
3105
3413
  <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
3414
  <div id="entList-${code}">
3107
3415
  ${spec.entities.length > 0
@@ -3138,7 +3446,7 @@ function renderModuleSpecSection(mod) {
3138
3446
  </div>
3139
3447
 
3140
3448
  <!-- TAB: Droits d'accès -->
3141
- <div class="tab-panel" id="tab-${code}-perm">
3449
+ <div class="tab-panel" id="tab-${code}-perm" role="tabpanel" aria-hidden="true">
3142
3450
  <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
3451
  <div id="permGrid-${code}">
3144
3452
  ${renderPermissionGrid(code)}
@@ -3172,7 +3480,7 @@ function renderModuleSpecSection(mod) {
3172
3480
  </div>
3173
3481
 
3174
3482
  <!-- TAB: Maquettes -->
3175
- <div class="tab-panel" id="tab-${code}-mock">
3483
+ <div class="tab-panel" id="tab-${code}-mock" role="tabpanel" aria-hidden="true">
3176
3484
  <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
3485
  <div id="mockupContainer-${code}">
3178
3486
  ${renderModuleMockups(code)}
@@ -3180,7 +3488,7 @@ function renderModuleSpecSection(mod) {
3180
3488
  </div>
3181
3489
 
3182
3490
  <!-- TAB: Notes -->
3183
- <div class="tab-panel" id="tab-${code}-notes">
3491
+ <div class="tab-panel" id="tab-${code}-notes" role="tabpanel" aria-hidden="true">
3184
3492
  <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
3493
  <div class="card">
3186
3494
  <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 +3496,7 @@ function renderModuleSpecSection(mod) {
3188
3496
  </div>
3189
3497
 
3190
3498
  <!-- TAB: Structure (sections/resources) -->
3191
- <div class="tab-panel" id="tab-${code}-struct">
3499
+ <div class="tab-panel" id="tab-${code}-struct" role="tabpanel" aria-hidden="true">
3192
3500
  <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
3501
  <div id="structContainer-${code}">
3194
3502
  ${renderModuleStructure(code)}
@@ -3202,19 +3510,19 @@ function renderUseCase(code, uc, index) {
3202
3510
  <div class="uc-item">
3203
3511
  <div class="uc-header">
3204
3512
  <span class="uc-id">UC-${String(index + 1).padStart(3, '0')}</span>
3205
- <span class="uc-title">${uc.name}</span>
3513
+ <span class="uc-title">${escapeHtml(uc.name)}</span>
3206
3514
  <div class="uc-actions">
3207
3515
  <button class="btn btn-sm" onclick="removeUseCase('${code}',${index})">Supprimer</button>
3208
3516
  </div>
3209
3517
  </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>` : ''}
3518
+ <div class="uc-actors"><div class="uc-actor">${escapeHtml(uc.actor)}</div></div>
3519
+ ${uc.steps ? `<div class="uc-detail-label">Déroulement</div><div class="uc-detail">${escapeHtml(uc.steps).replace(/\n/g, '<br>')}</div>` : ''}
3520
+ ${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
3521
  <div style="padding:0.5rem 0.75rem;border-top:1px solid var(--border);background:var(--bg-input);border-radius:0 0 8px 8px;">
3214
3522
  <label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.25rem;">Commentaire / Feedback :</label>
3215
3523
  <textarea class="form-textarea" placeholder="Ajouter un commentaire sur ce cas d'utilisation..."
3216
3524
  onblur="updateSpecComment('${code}','uc',${index},this.value)"
3217
- style="min-height:45px;font-size:0.8rem;resize:vertical;">${getSpecComment(code, 'uc', index)}</textarea>
3525
+ style="min-height:45px;font-size:0.8rem;resize:vertical;">${escapeHtml(getSpecComment(code, 'uc', index))}</textarea>
3218
3526
  </div>
3219
3527
  </div>`;
3220
3528
  }
@@ -3237,6 +3545,7 @@ function addUseCase(code) {
3237
3545
  }
3238
3546
 
3239
3547
  function removeUseCase(code, index) {
3548
+ if (!confirm('Supprimer ce cas d\'utilisation ?')) return;
3240
3549
  data.moduleSpecs[code].useCases.splice(index, 1);
3241
3550
  renderAllModuleSpecs();
3242
3551
  updateCounts();
@@ -3249,18 +3558,18 @@ function renderBusinessRule(code, br, index) {
3249
3558
  return `
3250
3559
  <div style="margin-bottom:0.5rem;">
3251
3560
  <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>
3561
+ <span class="br-category ${catColors[br.category] || 'br-cat-validation'}">${escapeHtml(catLabels[br.category] || br.category)}</span>
3253
3562
  <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>` : ''}
3563
+ <div style="font-weight:600;color:var(--text-bright);margin-bottom:0.2rem;">${escapeHtml(br.name)}</div>
3564
+ <div>${escapeHtml(br.statement)}</div>
3565
+ ${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
3566
  </div>
3258
3567
  <button class="btn btn-sm" onclick="removeBusinessRule('${code}',${index})" style="opacity:0.5;flex-shrink:0;">&#10005;</button>
3259
3568
  </div>
3260
3569
  <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
3570
  <textarea class="form-textarea" placeholder="Commentaire sur cette règle..."
3262
3571
  onblur="updateSpecComment('${code}','br',${index},this.value)"
3263
- style="min-height:35px;font-size:0.8rem;resize:vertical;">${getSpecComment(code, 'br', index)}</textarea>
3572
+ style="min-height:35px;font-size:0.8rem;resize:vertical;">${escapeHtml(getSpecComment(code, 'br', index))}</textarea>
3264
3573
  </div>
3265
3574
  </div>`;
3266
3575
  }
@@ -3282,18 +3591,20 @@ function addBusinessRule(code) {
3282
3591
  }
3283
3592
 
3284
3593
  function removeBusinessRule(code, index) {
3594
+ if (!confirm('Supprimer cette règle métier ?')) return;
3285
3595
  data.moduleSpecs[code].businessRules.splice(index, 1);
3286
3596
  renderAllModuleSpecs();
3287
3597
  autoSave();
3288
3598
  }
3289
3599
 
3290
3600
  function renderEntity(code, ent, index) {
3601
+ var attrFormId = 'addAttrForm-' + code + '-' + index;
3291
3602
  return `
3292
3603
  <div class="entity-block">
3293
3604
  <div class="entity-header">
3294
3605
  <div>
3295
- <div class="entity-name">${ent.name}</div>
3296
- <div class="entity-desc">${ent.description || ''}</div>
3606
+ <div class="entity-name">${escapeHtml(ent.name)}</div>
3607
+ <div class="entity-desc">${escapeHtml(ent.description || '')}</div>
3297
3608
  </div>
3298
3609
  <button class="btn btn-sm" onclick="removeEntity('${code}',${index})" style="opacity:0.5;">Supprimer</button>
3299
3610
  </div>
@@ -3301,21 +3612,26 @@ function renderEntity(code, ent, index) {
3301
3612
  <table class="attr-table">
3302
3613
  <thead><tr><th>Information</th><th>Description</th></tr></thead>
3303
3614
  <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('')}
3615
+ ${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
3616
  </tbody>
3306
- </table>
3617
+ </table>` : ''}
3307
3618
  <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>` : ''}
3619
+ <button class="add-btn" style="font-size:0.75rem;padding:0.4rem;" onclick="toggleForm('${attrFormId}')">+ Ajouter un attribut</button>
3620
+ <div class="inline-form" id="${attrFormId}">
3621
+ <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>
3622
+ <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>
3623
+ <div class="form-actions"><button class="btn" onclick="toggleForm('${attrFormId}')">Annuler</button><button class="btn btn-primary" onclick="addEntityAttribute('${code}',${index})">Ajouter</button></div>
3624
+ </div>
3625
+ </div>
3310
3626
  ${(ent.relationships || []).length > 0 ? `
3311
3627
  <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(', ')}
3628
+ Relations : ${ent.relationships.map(r => `<span style="color:var(--accent);">${escapeHtml(typeof r === 'string' ? r : (r.target || ''))}</span>`).join(', ')}
3313
3629
  </div>` : ''}
3314
3630
  <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
3631
  <label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.25rem;">Commentaire :</label>
3316
3632
  <textarea class="form-textarea" placeholder="Commentaire sur cette entité..."
3317
3633
  onblur="updateSpecComment('${code}','ent',${index},this.value)"
3318
- style="min-height:35px;font-size:0.8rem;resize:vertical;">${getSpecComment(code, 'ent', index)}</textarea>
3634
+ style="min-height:35px;font-size:0.8rem;resize:vertical;">${escapeHtml(getSpecComment(code, 'ent', index))}</textarea>
3319
3635
  </div>
3320
3636
  </div>`;
3321
3637
  }
@@ -3343,20 +3659,21 @@ function addEntity(code) {
3343
3659
  }
3344
3660
 
3345
3661
  function removeEntity(code, index) {
3662
+ if (!confirm('Supprimer ce type de données ?')) return;
3346
3663
  data.moduleSpecs[code].entities.splice(index, 1);
3347
3664
  renderAllModuleSpecs();
3348
3665
  autoSave();
3349
3666
  }
3350
3667
 
3351
3668
  function addEntityAttribute(code, entityIndex) {
3352
- var attrName = prompt('Nom de l\'attribut :');
3353
- if (!attrName) return;
3354
- var attrDesc = prompt('Description (optionnel) :') || '';
3669
+ var attrName = document.getElementById('attr-name-' + code + '-' + entityIndex);
3670
+ var attrDesc = document.getElementById('attr-desc-' + code + '-' + entityIndex);
3671
+ if (!attrName || !attrName.value.trim()) return;
3355
3672
  if (!data.moduleSpecs[code]?.entities?.[entityIndex]) return;
3356
3673
  if (!data.moduleSpecs[code].entities[entityIndex].attributes) {
3357
3674
  data.moduleSpecs[code].entities[entityIndex].attributes = [];
3358
3675
  }
3359
- data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName, description: attrDesc });
3676
+ data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName.value.trim(), description: (attrDesc?.value || '').trim() });
3360
3677
  renderAllModuleSpecs();
3361
3678
  autoSave();
3362
3679
  }
@@ -3382,37 +3699,6 @@ function updateWireframeComment(code, screen, value) {
3382
3699
  autoSave();
3383
3700
  }
3384
3701
 
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
3702
  function renderWireframeMockup(code, wf, i) {
3417
3703
  const hasSvg = !!wf.svgContent;
3418
3704
  const wireframeId = `wf-${code}-${i}`;
@@ -3423,7 +3709,7 @@ function renderWireframeMockup(code, wf, i) {
3423
3709
  <div class="mockup-dot mockup-dot-red"></div>
3424
3710
  <div class="mockup-dot mockup-dot-yellow"></div>
3425
3711
  <div class="mockup-dot mockup-dot-green"></div>
3426
- <span class="mockup-title">${wf.screen || wf.section}</span>
3712
+ <span class="mockup-title">${escapeHtml(wf.screen || wf.section)}</span>
3427
3713
  ${hasSvg ? `
3428
3714
  <div class="wireframe-toggle">
3429
3715
  <button class="wireframe-toggle-btn active" data-target="${wireframeId}" data-view="svg" onclick="toggleWireframeView('${wireframeId}', 'svg')">SVG</button>
@@ -3440,11 +3726,11 @@ function renderWireframeMockup(code, wf, i) {
3440
3726
  </div>
3441
3727
  ${wf.description ? `
3442
3728
  <div class="wireframe-description">
3443
- <strong>Description:</strong> ${wf.description}
3729
+ <strong>Description:</strong> ${escapeHtml(wf.description)}
3444
3730
  </div>` : ''}
3445
3731
  ${(wf.elements || []).length > 0 ? `
3446
3732
  <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>
3733
+ <div><strong>Elements:</strong> ${wf.elements.map(e => escapeHtml(typeof e === 'string' ? e : (e.type || e.label || e.id || ''))).filter(Boolean).join(', ')}</div>
3448
3734
  </div>` : ''}
3449
3735
  ${(() => {
3450
3736
  const cm = wf.componentMapping;
@@ -3458,7 +3744,7 @@ function renderWireframeMockup(code, wf, i) {
3458
3744
  <thead><tr><th>Élément maquette</th><th>Composant React</th></tr></thead>
3459
3745
  <tbody>
3460
3746
  ${mappings.map(m =>
3461
- '<tr><td>' + (m.wireframeElement || '') + '</td><td><code>' + (m.reactComponent || '') + '</code></td></tr>'
3747
+ '<tr><td>' + escapeHtml(m.wireframeElement || '') + '</td><td><code>' + escapeHtml(m.reactComponent || '') + '</code></td></tr>'
3462
3748
  ).join('')}
3463
3749
  </tbody>
3464
3750
  </table>
@@ -3533,12 +3819,12 @@ function renderPermissionGrid(code) {
3533
3819
  <table class="mock-table" style="background:var(--bg-card);border-radius:8px;overflow:hidden;">
3534
3820
  <thead><tr>
3535
3821
  <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('')}
3822
+ ${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
3823
  </tr></thead>
3538
3824
  <tbody>
3539
3825
  ${roles.map((role, ri) => `
3540
3826
  <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>
3827
+ <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
3828
  ${actions.map(action => {
3543
3829
  const key = role + '|' + action;
3544
3830
  const wildcardKey = role + '|*';
@@ -3632,24 +3918,52 @@ function renderModuleStructure(code) {
3632
3918
 
3633
3919
  return sections.map(function(section) {
3634
3920
  var resources = section.resources || [];
3921
+ var sectionUCs = section.useCases || [];
3922
+ var sectionBRs = section.businessRules || [];
3635
3923
  var html = '<div class="struct-section">';
3636
3924
  html += '<div class="struct-section-header">';
3637
- html += '<span class="struct-section-code">' + (section.code || section.name || '') + '</span>';
3925
+ html += '<span class="struct-section-code">' + escapeHtml(section.code || section.name || '') + '</span>';
3638
3926
  if (section.description) {
3639
- html += '<span class="struct-section-desc">' + section.description + '</span>';
3927
+ html += '<span class="struct-section-desc">' + escapeHtml(section.description) + '</span>';
3640
3928
  }
3641
3929
  html += '<span class="struct-section-badge">' + resources.length + ' ressource' + (resources.length !== 1 ? 's' : '') + '</span>';
3642
3930
  html += '</div>';
3643
3931
 
3932
+ // Section metadata (route, permission)
3933
+ if (section.route || section.permission) {
3934
+ 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);">';
3935
+ 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>';
3936
+ 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>';
3937
+ html += '</div>';
3938
+ }
3939
+
3940
+ // Section-level UC summary
3941
+ if (sectionUCs.length > 0) {
3942
+ html += '<div style="padding:0.4rem 1rem;font-size:0.8rem;border-bottom:1px solid var(--border);">';
3943
+ html += '<span style="color:var(--text-muted);font-weight:500;">' + sectionUCs.length + ' cas d\'utilisation :</span> ';
3944
+ html += sectionUCs.map(function(uc) { return '<span style="color:var(--text-bright);">' + escapeHtml(uc.name || '') + '</span>'; }).join(', ');
3945
+ html += '</div>';
3946
+ }
3947
+
3948
+ // Section-level BR summary
3949
+ if (sectionBRs.length > 0) {
3950
+ html += '<div style="padding:0.4rem 1rem;font-size:0.8rem;border-bottom:1px solid var(--border);">';
3951
+ html += '<span style="color:var(--text-muted);font-weight:500;">' + sectionBRs.length + ' règle' + (sectionBRs.length > 1 ? 's' : '') + ' métier :</span> ';
3952
+ html += sectionBRs.map(function(br) { return '<span style="color:var(--text-bright);">' + escapeHtml(br.name || '') + '</span>'; }).join(', ');
3953
+ html += '</div>';
3954
+ }
3955
+
3644
3956
  if (resources.length > 0) {
3645
3957
  html += '<div class="struct-resources">';
3646
3958
  resources.forEach(function(res) {
3647
3959
  var resName = typeof res === 'string' ? res : (res.code || res.name || '');
3960
+ var resType = typeof res === 'object' ? (res.type || '') : '';
3648
3961
  var resDesc = typeof res === 'object' ? (res.description || '') : '';
3649
3962
  html += '<div class="struct-resource">';
3650
3963
  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>';
3964
+ html += '<span class="struct-resource-name">' + escapeHtml(resName) + '</span>';
3965
+ 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>';
3966
+ if (resDesc) html += '<span class="struct-resource-desc">' + escapeHtml(resDesc) + '</span>';
3653
3967
  html += '</div>';
3654
3968
  });
3655
3969
  html += '</div>';
@@ -3660,16 +3974,111 @@ function renderModuleStructure(code) {
3660
3974
  }).join('');
3661
3975
  }
3662
3976
 
3977
+ /* ---------- Section-Grouped Rendering (Hierarchical Mode) ---------- */
3978
+
3979
+ function renderSectionGroupedItems(mod, itemsKey, code, renderFn) {
3980
+ var sections = mod.anticipatedSections || [];
3981
+ var html = '';
3982
+ sections.forEach(function(section) {
3983
+ var items = section[itemsKey] || [];
3984
+ if (items.length === 0) return;
3985
+ html += '<div class="section-group">';
3986
+ html += '<div class="section-group-header">';
3987
+ html += '<span class="section-group-icon">&#9655;</span> ';
3988
+ html += '<span class="section-group-label">' + escapeHtml(section.code || section.name || '') + '</span>';
3989
+ if (section.route) html += '<span class="section-group-route">' + escapeHtml(section.route) + '</span>';
3990
+ html += '<span class="nav-badge">' + items.length + '</span>';
3991
+ html += '</div>';
3992
+ html += items.map(function(item, i) { return renderFn(code, item, i); }).join('');
3993
+ html += '</div>';
3994
+ });
3995
+ return html;
3996
+ }
3997
+
3998
+ function renderModuleMockups(code) {
3999
+ var spec = data.moduleSpecs[code] || {};
4000
+ var screens = spec.screens || [];
4001
+ var wireframes = EMBEDDED_ARTIFACTS?.wireframes?.[code] || [];
4002
+ var mod = data.modules.find(function(m) { return m.code === code; });
4003
+ var sections = mod ? (mod.anticipatedSections || []) : [];
4004
+
4005
+ // Priority 1: HTML mockups from screens[] specs
4006
+ if (screens.length > 0) {
4007
+ var html = '';
4008
+ if (typeof renderScreenMockups === 'function') {
4009
+ html = renderScreenMockups(code);
4010
+ }
4011
+ if (wireframes.length > 0) {
4012
+ html += '<h3 style="color:var(--text-bright);font-size:1rem;margin:2rem 0 1rem;">Wireframes</h3>';
4013
+ html += wireframes.map(function(wf, i) { return renderWireframeMockup(code, wf, i); }).join('');
4014
+ }
4015
+ return html;
4016
+ }
4017
+
4018
+ // Priority 2: Wireframes grouped by section (if sections exist)
4019
+ if (wireframes.length > 0 && sections.length > 1) {
4020
+ var grouped = {};
4021
+ var ungrouped = [];
4022
+ wireframes.forEach(function(wf, i) {
4023
+ var sectionCode = wf.section || wf.sectionCode || '';
4024
+ var matchingSection = sections.find(function(s) { return s.code === sectionCode; });
4025
+ if (matchingSection) {
4026
+ if (!grouped[sectionCode]) grouped[sectionCode] = { section: matchingSection, items: [] };
4027
+ grouped[sectionCode].items.push({ wf: wf, index: i });
4028
+ } else {
4029
+ ungrouped.push({ wf: wf, index: i });
4030
+ }
4031
+ });
4032
+
4033
+ var html = '';
4034
+ Object.keys(grouped).forEach(function(sectionCode) {
4035
+ var group = grouped[sectionCode];
4036
+ html += '<div class="section-group">';
4037
+ html += '<div class="section-group-header">';
4038
+ html += '<span class="section-group-icon">&#9655;</span> ';
4039
+ html += '<span class="section-group-label">' + escapeHtml(group.section.code || '') + '</span>';
4040
+ html += '<span class="nav-badge">' + group.items.length + '</span>';
4041
+ html += '</div>';
4042
+ group.items.forEach(function(item) {
4043
+ html += renderWireframeMockup(code, item.wf, item.index);
4044
+ });
4045
+ html += '</div>';
4046
+ });
4047
+ ungrouped.forEach(function(item) {
4048
+ html += renderWireframeMockup(code, item.wf, item.index);
4049
+ });
4050
+ return html;
4051
+ }
4052
+
4053
+ // Priority 3: Flat wireframe list
4054
+ if (wireframes.length === 0) {
4055
+ 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>';
4056
+ }
4057
+ return wireframes.map(function(wf, i) { return renderWireframeMockup(code, wf, i); }).join('');
4058
+ }
4059
+
3663
4060
  function switchTab(code, tabId) {
3664
4061
  const section = document.getElementById('module-spec-' + code);
3665
4062
  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'));
4063
+ section.querySelectorAll('.tab-btn').forEach(function(btn) {
4064
+ btn.classList.remove('active');
4065
+ btn.setAttribute('aria-selected', 'false');
4066
+ });
4067
+ section.querySelectorAll('.tab-panel').forEach(function(panel) {
4068
+ panel.classList.remove('active');
4069
+ panel.setAttribute('aria-hidden', 'true');
4070
+ });
3668
4071
  const targetPanel = document.getElementById('tab-' + code + '-' + tabId);
3669
- if (targetPanel) targetPanel.classList.add('active');
4072
+ if (targetPanel) {
4073
+ targetPanel.classList.add('active');
4074
+ targetPanel.setAttribute('aria-hidden', 'false');
4075
+ }
3670
4076
  const buttons = section.querySelectorAll('.tab-btn');
3671
4077
  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');
4078
+ if (buttons[tabIndex]) {
4079
+ buttons[tabIndex].classList.add('active');
4080
+ buttons[tabIndex].setAttribute('aria-selected', 'true');
4081
+ }
3673
4082
  }
3674
4083
 
3675
4084
 
@@ -3729,7 +4138,7 @@ function renderDataModel() {
3729
4138
 
3730
4139
  html += `<div class="dm-module-group">`;
3731
4140
  html += `<div class="dm-module-header">
3732
- <span class="dm-module-name">${m.name || m.code}</span>
4141
+ <span class="dm-module-name">${escapeHtml(m.name || m.code)}</span>
3733
4142
  <span class="dm-module-count">${entities.length} entité${entities.length > 1 ? 's' : ''}</span>
3734
4143
  </div>`;
3735
4144
 
@@ -3740,15 +4149,15 @@ function renderDataModel() {
3740
4149
  html += `
3741
4150
  <div class="dm-entity-card">
3742
4151
  <div class="dm-entity-header">
3743
- <span class="dm-entity-name">${ent.name}</span>
4152
+ <span class="dm-entity-name">${escapeHtml(ent.name)}</span>
3744
4153
  <span class="dm-entity-attr-count">${attrs.length} champ${attrs.length > 1 ? 's' : ''}</span>
3745
4154
  </div>
3746
- ${ent.description ? `<div class="dm-entity-desc">${ent.description}</div>` : ''}
4155
+ ${ent.description ? `<div class="dm-entity-desc">${escapeHtml(ent.description)}</div>` : ''}
3747
4156
  ${attrs.length > 0 ? `
3748
4157
  <table class="dm-attr-table">
3749
4158
  <thead><tr><th>Champ</th><th>Description</th></tr></thead>
3750
4159
  <tbody>
3751
- ${attrs.map(a => `<tr><td class="dm-attr-name">${a.name}</td><td class="dm-attr-desc">${a.description || ''}</td></tr>`).join('')}
4160
+ ${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
4161
  </tbody>
3753
4162
  </table>` : ''}
3754
4163
  ${rels.length > 0 ? `
@@ -3756,7 +4165,7 @@ function renderDataModel() {
3756
4165
  <div class="dm-relations-title">Relations</div>
3757
4166
  ${rels.map(r => {
3758
4167
  const relText = typeof r === 'string' ? r : (r.target + ' (' + r.type + ') - ' + (r.description || ''));
3759
- return `<div class="dm-relation-item">${relText}</div>`;
4168
+ return `<div class="dm-relation-item">${escapeHtml(relText)}</div>`;
3760
4169
  }).join('')}
3761
4170
  </div>` : ''}
3762
4171
  </div>`;
@@ -3776,11 +4185,11 @@ function renderConsolInteractions() {
3776
4185
  const toName = data.modules.find(m => m.code === d.to)?.name || d.to;
3777
4186
  return `
3778
4187
  <div class="interaction-item">
3779
- <span style="font-weight:600;color:var(--text-bright);">${fromName}</span>
4188
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(fromName)}</span>
3780
4189
  <span class="interaction-arrow">&#8594;</span>
3781
- <span style="font-weight:600;color:var(--text-bright);">${toName}</span>
4190
+ <span style="font-weight:600;color:var(--text-bright);">${escapeHtml(toName)}</span>
3782
4191
  <span class="interaction-type">Dépendance</span>
3783
- <span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${d.description || ''}</span>
4192
+ <span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${escapeHtml(d.description || '')}</span>
3784
4193
  </div>`;
3785
4194
  }).join('');
3786
4195
  }
@@ -3793,11 +4202,11 @@ function renderConsolPermissions() {
3793
4202
 
3794
4203
  let html = '<table class="mock-table" style="background:var(--bg-card);border-radius:8px;overflow:hidden;">';
3795
4204
  html += '<thead><tr><th>Profil</th>';
3796
- data.modules.forEach(m => { html += `<th style="text-align:center;">${m.name || m.code}</th>`; });
4205
+ data.modules.forEach(m => { html += `<th style="text-align:center;">${escapeHtml(m.name || m.code)}</th>`; });
3797
4206
  html += '</tr></thead><tbody>';
3798
4207
 
3799
4208
  roles.forEach(role => {
3800
- html += `<tr><td style="font-weight:500;color:var(--text-bright);">${role}</td>`;
4209
+ html += `<tr><td style="font-weight:500;color:var(--text-bright);">${escapeHtml(role)}</td>`;
3801
4210
  data.modules.forEach(m => {
3802
4211
  const allPerms = data.moduleSpecs[m.code]?.permissions || [];
3803
4212
  const hasWildcard = allPerms.includes(role + '|*');
@@ -3841,6 +4250,7 @@ function addE2EFlow() {
3841
4250
  }
3842
4251
 
3843
4252
  function removeE2EFlow(index) {
4253
+ if (!confirm('Supprimer ce parcours bout en bout ?')) return;
3844
4254
  data.consolidation.e2eFlows.splice(index, 1);
3845
4255
  renderE2EFlows();
3846
4256
  autoSave();
@@ -3853,15 +4263,15 @@ function renderE2EFlows() {
3853
4263
  container.innerHTML = data.consolidation.e2eFlows.map((flow, fi) => `
3854
4264
  <div class="card" style="margin-bottom:1rem;">
3855
4265
  <div class="card-header">
3856
- <span class="card-title">${flow.name}</span>
4266
+ <span class="card-title">${escapeHtml(flow.name)}</span>
3857
4267
  <button class="btn btn-sm" onclick="removeE2EFlow(${fi})" style="opacity:0.5;">Supprimer</button>
3858
4268
  </div>
3859
- ${flow.actors ? `<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem;">Intervenants : ${flow.actors}</div>` : ''}
4269
+ ${flow.actors ? `<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.5rem;">Intervenants : ${escapeHtml(flow.actors)}</div>` : ''}
3860
4270
  <div class="e2e-flow">
3861
4271
  ${flow.steps.map((s, i) => `
3862
4272
  <div class="e2e-step">
3863
- <div class="e2e-step-module">${s.module}</div>
3864
- <div class="e2e-step-action">${s.action}</div>
4273
+ <div class="e2e-step-module">${escapeHtml(s.module)}</div>
4274
+ <div class="e2e-step-action">${escapeHtml(s.action)}</div>
3865
4275
  </div>
3866
4276
  ${i < flow.steps.length - 1 ? '<div class="process-arrow">&#8594;</div>' : ''}
3867
4277
  `).join('')}
@@ -3887,7 +4297,7 @@ function renderScreenMockups(code) {
3887
4297
  var resources = screen.resources || [];
3888
4298
  return '<div class="screen-section" style="margin-bottom:2rem;">' +
3889
4299
  '<h3 style="color:var(--text-bright);font-size:1rem;margin-bottom:1rem;">' +
3890
- '<span style="color:var(--accent);">&#9656;</span> ' + (screen.sectionLabel || screen.sectionCode) +
4300
+ '<span style="color:var(--accent);">&#9656;</span> ' + escapeHtml(screen.sectionLabel || screen.sectionCode) +
3891
4301
  '</h3>' +
3892
4302
  resources.map(function(res, ri) {
3893
4303
  return renderResourceMockup(code, screen.sectionCode, res, ri);
@@ -3905,9 +4315,9 @@ function renderResourceMockup(code, sectionCode, res, index) {
3905
4315
  html += '<div class="mockup-dot mockup-dot-red"></div>';
3906
4316
  html += '<div class="mockup-dot mockup-dot-yellow"></div>';
3907
4317
  html += '<div class="mockup-dot mockup-dot-green"></div>';
3908
- html += '<span class="mockup-title">' + (res.label || res.code) + ' (' + res.type + ')</span>';
4318
+ html += '<span class="mockup-title">' + escapeHtml(res.label || res.code) + ' (' + res.type + ')</span>';
3909
4319
  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>';
4320
+ 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
4321
  }
3912
4322
  html += '</div>';
3913
4323
 
@@ -3940,7 +4350,7 @@ function renderResourceMockup(code, sectionCode, res, index) {
3940
4350
  // Notes
3941
4351
  if (res.notes) {
3942
4352
  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;
4353
+ html += '<strong>Notes:</strong> ' + escapeHtml(res.notes);
3944
4354
  html += '</div>';
3945
4355
  }
3946
4356
 
@@ -3967,7 +4377,7 @@ function renderSmartTableMockup(res) {
3967
4377
 
3968
4378
  // Header with actions
3969
4379
  html += '<div class="mock-header">';
3970
- html += '<span class="mock-title">' + (res.label || 'Liste') + '</span>';
4380
+ html += '<span class="mock-title">' + escapeHtml(res.label || 'Liste') + '</span>';
3971
4381
  html += '<div style="display:flex;gap:0.4rem;">';
3972
4382
  (res.actions || []).forEach(function(action) {
3973
4383
  var isPrimary = action === 'create' || action === 'export';
@@ -3989,7 +4399,7 @@ function renderSmartTableMockup(res) {
3989
4399
  html += '<table class="mock-table">';
3990
4400
  html += '<thead><tr>';
3991
4401
  columns.forEach(function(col) {
3992
- html += '<th>' + (col.label || col.field);
4402
+ html += '<th>' + escapeHtml(col.label || col.field);
3993
4403
  if (col.sortable) html += ' <span style="font-size:0.6rem;color:var(--text-muted);">&#9650;&#9660;</span>';
3994
4404
  html += '</th>';
3995
4405
  });
@@ -4040,7 +4450,7 @@ function renderSmartFormMockup(res) {
4040
4450
 
4041
4451
  // Header
4042
4452
  html += '<div class="mock-header">';
4043
- html += '<span class="mock-title">' + (res.label || 'Formulaire') + '</span>';
4453
+ html += '<span class="mock-title">' + escapeHtml(res.label || 'Formulaire') + '</span>';
4044
4454
  html += '<div style="display:flex;gap:0.4rem;">';
4045
4455
  (res.actions || ['save', 'cancel']).forEach(function(action) {
4046
4456
  var isPrimary = action === 'save';
@@ -4052,7 +4462,7 @@ function renderSmartFormMockup(res) {
4052
4462
  if (tabs.length > 1) {
4053
4463
  html += '<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1.5rem;">';
4054
4464
  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>';
4465
+ 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
4466
  });
4057
4467
  html += '</div>';
4058
4468
  }
@@ -4071,7 +4481,7 @@ function renderSmartFormMockup(res) {
4071
4481
  html += '<div class="mock-form-row">';
4072
4482
  row.forEach(function(field) {
4073
4483
  html += '<div class="mock-form-group">';
4074
- html += '<label class="mock-label">' + (field.label || field.field);
4484
+ html += '<label class="mock-label">' + escapeHtml(field.label || field.field);
4075
4485
  if (field.required) html += ' <span style="color:var(--error);">*</span>';
4076
4486
  html += '</label>';
4077
4487
  html += renderFormFieldMockup(field);
@@ -4107,11 +4517,11 @@ function renderSubtableMockup(field) {
4107
4517
  var entity = field.entity || 'Element';
4108
4518
  var html = '<div style="margin:1rem 0;border:1px solid var(--border);border-radius:8px;overflow:hidden;">';
4109
4519
  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>';
4520
+ html += '<span style="font-weight:500;color:var(--text-bright);font-size:0.85rem;">' + escapeHtml(entity) + '</span>';
4111
4521
  html += '<span class="mock-btn" style="font-size:0.75rem;padding:0.2rem 0.5rem;">+ Ajouter</span>';
4112
4522
  html += '</div>';
4113
4523
  html += '<table class="mock-table"><thead><tr>';
4114
- cols.forEach(function(c) { html += '<th>' + c + '</th>'; });
4524
+ cols.forEach(function(c) { html += '<th>' + escapeHtml(c) + '</th>'; });
4115
4525
  html += '</tr></thead><tbody>';
4116
4526
  html += '<tr>' + cols.map(function() { return '<td style="color:var(--text-muted);font-style:italic;">...</td>'; }).join('') + '</tr>';
4117
4527
  html += '</tbody></table></div>';
@@ -4121,7 +4531,7 @@ function renderSubtableMockup(field) {
4121
4531
  /* ---------- SmartCard ---------- */
4122
4532
  function renderSmartCardMockup(res) {
4123
4533
  var columns = res.columns || res.fields || [];
4124
- var html = '<div class="mock-header"><span class="mock-title">' + (res.label || 'Cartes') + '</span></div>';
4534
+ var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Cartes') + '</span></div>';
4125
4535
  html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem;">';
4126
4536
 
4127
4537
  for (var i = 0; i < 4; i++) {
@@ -4129,9 +4539,9 @@ function renderSmartCardMockup(res) {
4129
4539
  columns.forEach(function(col, ci) {
4130
4540
  var label = col.label || col.field || col;
4131
4541
  if (ci === 0) {
4132
- html += '<div style="font-weight:600;color:var(--text-bright);margin-bottom:0.5rem;">' + label + ' #' + (i + 1) + '</div>';
4542
+ html += '<div style="font-weight:600;color:var(--text-bright);margin-bottom:0.5rem;">' + escapeHtml(label) + ' #' + (i + 1) + '</div>';
4133
4543
  } 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>';
4544
+ 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
4545
  }
4136
4546
  });
4137
4547
  html += '</div>';
@@ -4143,14 +4553,14 @@ function renderSmartCardMockup(res) {
4143
4553
  /* ---------- SmartKanban ---------- */
4144
4554
  function renderSmartKanbanMockup(res) {
4145
4555
  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>';
4556
+ var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Kanban') + '</span></div>';
4147
4557
  html += '<div style="display:flex;gap:1rem;overflow-x:auto;padding-bottom:0.5rem;">';
4148
4558
 
4149
4559
  options.forEach(function(col, ci) {
4150
4560
  var colLabel = typeof col === 'string' ? col : (col.label || col.field || 'Colonne');
4151
4561
  html += '<div style="min-width:200px;flex:1;background:var(--bg-hover);border-radius:8px;padding:0.75rem;">';
4152
4562
  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>';
4563
+ 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
4564
  html += '</div>';
4155
4565
  for (var j = 0; j < Math.max(1, 3 - ci); j++) {
4156
4566
  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 +4576,7 @@ function renderSmartKanbanMockup(res) {
4166
4576
 
4167
4577
  /* ---------- SmartDashboard ---------- */
4168
4578
  function renderSmartDashboardMockup(res) {
4169
- var html = '<div class="mock-header"><span class="mock-title">' + (res.label || 'Tableau de bord') + '</span></div>';
4579
+ var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Tableau de bord') + '</span></div>';
4170
4580
 
4171
4581
  // KPI cards
4172
4582
  html += '<div class="mock-kpi-grid">';
@@ -4177,7 +4587,7 @@ function renderSmartDashboardMockup(res) {
4177
4587
  { label: 'Taux', value: '80%' }
4178
4588
  ];
4179
4589
  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>';
4590
+ 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
4591
  });
4182
4592
  html += '</div>';
4183
4593
 
@@ -4193,7 +4603,7 @@ function renderSmartFilterMockup(res) {
4193
4603
  var html = '<div style="display:flex;gap:0.4rem;flex-wrap:wrap;padding:0.5rem 0;">';
4194
4604
  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
4605
  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>';
4606
+ 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
4607
  });
4198
4608
  html += '</div>';
4199
4609
  return html;
@@ -4254,6 +4664,7 @@ function renderHandoff() {
4254
4664
  renderHandoffStats();
4255
4665
  renderHandoffModules();
4256
4666
  renderCoverageMatrix();
4667
+ renderProgressIndicator();
4257
4668
  }
4258
4669
 
4259
4670
  function renderHandoffStats() {
@@ -4292,8 +4703,8 @@ function renderHandoffModules() {
4292
4703
  <div style="display:flex;align-items:center;gap:0.75rem;">
4293
4704
  <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
4705
  <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>
4706
+ <div style="font-weight:600;color:var(--text-bright);">${escapeHtml(m.name)}</div>
4707
+ <div style="font-size:0.8rem;color:var(--text-muted);">${escapeHtml(m.description || '')}</div>
4297
4708
  </div>
4298
4709
  <div style="display:flex;gap:1rem;font-size:0.75rem;color:var(--text-muted);">
4299
4710
  <span>${(spec.useCases || []).length} cas d'utilisation</span>
@@ -4326,24 +4737,47 @@ function renderCoverageMatrix() {
4326
4737
  : (data.modules.length > 0 ? data.modules.map(m => m.name).join(', ') : 'À définir');
4327
4738
  return `
4328
4739
  <tr>
4329
- <td>${item.name}</td>
4740
+ <td>${escapeHtml(item.name)}</td>
4330
4741
  <td><span class="priority priority-${item.priority}">${formatPriority(item.priority)}</span></td>
4331
- <td style="color:var(--text-muted);">${moduleName}</td>
4742
+ <td style="color:var(--text-muted);">${escapeHtml(moduleName)}</td>
4332
4743
  <td style="text-align:center;color:var(--success);">&#10003;</td>
4333
4744
  </tr>`;
4334
4745
  }).join('')}
4335
4746
  </tbody>
4336
4747
  </table>`;
4337
4748
  }
4749
+
4750
+ function renderProgressIndicator() {
4751
+ var container = document.getElementById('progressIndicator');
4752
+ if (!container) return;
4753
+ var p = computeProgress();
4754
+ var html = '<div class="progress-bar-container">';
4755
+ html += '<div class="progress-bar-fill" style="width:' + p.pct + '%;"></div>';
4756
+ html += '</div>';
4757
+ html += '<div class="progress-label">' + p.pct + '% complet — ' + p.doneCount + '/' + p.total + ' critères</div>';
4758
+ html += '<div class="progress-checks">';
4759
+ p.checks.forEach(function(c) {
4760
+ html += '<div class="progress-check ' + (c.done ? 'done' : '') + '">';
4761
+ html += '<span class="progress-check-icon">' + (c.done ? '&#10003;' : '&#9675;') + '</span> ';
4762
+ html += escapeHtml(c.label);
4763
+ html += '</div>';
4764
+ });
4765
+ html += '</div>';
4766
+ container.innerHTML = html;
4767
+ }
4338
4768
 
4339
4769
 
4340
4770
  /* --- 08-editing.js --- */
4341
4771
  /* ============================================
4342
4772
  PERSISTENCE
4343
4773
  ============================================ */
4774
+ let _saveTimer;
4344
4775
  function autoSave() {
4345
- data.metadata.lastModified = new Date().toISOString();
4346
- localStorage.setItem(APP_KEY, JSON.stringify(data));
4776
+ clearTimeout(_saveTimer);
4777
+ _saveTimer = setTimeout(function() {
4778
+ data.metadata.lastModified = new Date().toISOString();
4779
+ localStorage.setItem(APP_KEY, JSON.stringify(data));
4780
+ }, 500);
4347
4781
  }
4348
4782
 
4349
4783
  function saveToLocalStorage() {
@@ -4477,13 +4911,10 @@ function loadFromLocalStorage() {
4477
4911
  /* ============================================
4478
4912
  EXPORT JSON
4479
4913
  ============================================ */
4480
- function exportJSON() {
4481
- // Collect all editable fields (cadrage)
4914
+ function collectEditableFields() {
4482
4915
  document.querySelectorAll('.editable[data-field]').forEach(el => {
4483
4916
  setNestedValue(data, 'cadrage.' + el.dataset.field, el.textContent.trim());
4484
4917
  });
4485
-
4486
- // Collect module editable fields
4487
4918
  document.querySelectorAll('.editable[data-module-field]').forEach(el => {
4488
4919
  const code = el.dataset.moduleCode;
4489
4920
  const field = el.dataset.moduleField;
@@ -4491,17 +4922,39 @@ function exportJSON() {
4491
4922
  data.moduleSpecs[code][field] = el.textContent.trim();
4492
4923
  }
4493
4924
  });
4925
+ }
4926
+
4927
+ function buildModuleSpecsExport() {
4928
+ var result = {};
4929
+ data.modules.forEach(function(m) {
4930
+ var spec = data.moduleSpecs[m.code] || {};
4931
+ result[m.code] = {
4932
+ module: m,
4933
+ useCases: (spec.useCases || []).map(function(uc, i) {
4934
+ return Object.assign({ id: 'UC-' + String(i + 1).padStart(3, '0') }, uc);
4935
+ }),
4936
+ businessRules: (spec.businessRules || []).map(function(br, i) {
4937
+ return Object.assign({ id: 'BR-' + (br.category || 'GEN').toUpperCase().substring(0, 4) + '-' + String(i + 1).padStart(3, '0') }, br);
4938
+ }),
4939
+ entities: spec.entities || [],
4940
+ permissions: spec.permissions || [],
4941
+ notes: spec.notes || ''
4942
+ };
4943
+ });
4944
+ return result;
4945
+ }
4494
4946
 
4947
+ function exportJSON() {
4948
+ collectEditableFields();
4495
4949
  data.metadata.lastModified = new Date().toISOString();
4496
4950
  data.metadata.exportedAt = new Date().toISOString();
4497
4951
 
4498
- // Build complete export with structured data
4499
4952
  const exportData = {
4500
4953
  metadata: data.metadata,
4501
4954
  cadrage: data.cadrage,
4502
4955
  modules: data.modules,
4503
4956
  dependencies: data.dependencies,
4504
- moduleSpecifications: {},
4957
+ moduleSpecifications: buildModuleSpecsExport(),
4505
4958
  consolidation: data.consolidation,
4506
4959
  artifacts: EMBEDDED_ARTIFACTS,
4507
4960
  wireframeComments: data.wireframeComments,
@@ -4511,25 +4964,6 @@ function exportJSON() {
4511
4964
  comments: data.comments
4512
4965
  };
4513
4966
 
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
4967
  const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
4534
4968
  const url = URL.createObjectURL(blob);
4535
4969
  const a = document.createElement('a');
@@ -4569,24 +5003,13 @@ function detectChanges(original, current) {
4569
5003
  }
4570
5004
 
4571
5005
  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
- });
5006
+ collectEditableFields();
4583
5007
 
4584
5008
  const changes = detectChanges(ORIGINAL_DATA, data);
4585
5009
  const hasChanges = changes.cadrage || changes.modulesAdded.length > 0
4586
5010
  || changes.modulesRemoved.length > 0 || changes.modulesModified.length > 0
4587
5011
  || changes.commentsCount > 0;
4588
5012
 
4589
- // Build review export with metadata envelope
4590
5013
  const reviewData = {
4591
5014
  _reviewMeta: {
4592
5015
  sourceVersion: data.metadata.version || '1.0',
@@ -4605,7 +5028,7 @@ function saveReviewJSON() {
4605
5028
  cadrage: data.cadrage,
4606
5029
  modules: data.modules,
4607
5030
  dependencies: data.dependencies,
4608
- moduleSpecifications: {},
5031
+ moduleSpecifications: buildModuleSpecsExport(),
4609
5032
  consolidation: data.consolidation,
4610
5033
  wireframeComments: data.wireframeComments,
4611
5034
  specComments: data.specComments,
@@ -4614,25 +5037,6 @@ function saveReviewJSON() {
4614
5037
  comments: data.comments
4615
5038
  };
4616
5039
 
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
5040
  const blob = new Blob([JSON.stringify(reviewData, null, 2)], { type: 'application/json' });
4637
5041
  const url = URL.createObjectURL(blob);
4638
5042
  const a = document.createElement('a');
@@ -4651,14 +5055,24 @@ function saveReviewJSON() {
4651
5055
 
4652
5056
  /**
4653
5057
  * Comments are stored in data.comments[] with structure:
4654
- * { id, sectionId, cardIndex, author, timestamp, content, status, category }
5058
+ * { id, sectionId, elementId, author, timestamp, content, status, category }
4655
5059
  *
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)
5060
+ * - sectionId: matches the section div id (e.g. "cadrage-context", "module-spec-Clients")
5061
+ * - elementId: stable identifier for the element (name, role, or generated id)
5062
+ * - cardIndex: (deprecated, kept for backward compat) positional index
4658
5063
  * - status: "to-review" | "validated"
4659
5064
  * - category: "clarification" | "correction" | "suggestion"
4660
5065
  */
4661
5066
 
5067
+ function getElementId(item, sectionId, index) {
5068
+ // Extract a stable ID from the element's data
5069
+ var title = item.querySelector('.uc-title, .entity-name, .stakeholder-role, .card-title');
5070
+ if (title) return title.textContent.trim();
5071
+ var nameEl = item.querySelector('[data-field]');
5072
+ if (nameEl) return nameEl.dataset.field;
5073
+ return sectionId + '-' + index;
5074
+ }
5075
+
4662
5076
  function initInlineComments() {
4663
5077
  // Cadrage sections: direct card children
4664
5078
  document.querySelectorAll('.section > .card, .section > .stakeholder-card, .section > .uc-item').forEach(function(card) {
@@ -4668,7 +5082,8 @@ function initInlineComments() {
4668
5082
  var sectionId = section ? section.id : 'unknown';
4669
5083
  var siblings = Array.from(section.querySelectorAll(':scope > .card, :scope > .stakeholder-card, :scope > .uc-item'));
4670
5084
  var index = siblings.indexOf(card);
4671
- card.appendChild(createCommentUI(sectionId, index));
5085
+ var elementId = getElementId(card, sectionId, index);
5086
+ card.appendChild(createCommentUI(sectionId, elementId));
4672
5087
  });
4673
5088
 
4674
5089
  // Module spec lists: nested items in ucList, brList, entList containers
@@ -4679,7 +5094,8 @@ function initInlineComments() {
4679
5094
  var listId = list.id;
4680
5095
  var siblings = Array.from(list.children);
4681
5096
  var index = siblings.indexOf(item);
4682
- item.appendChild(createCommentUI(listId, index));
5097
+ var elementId = getElementId(item, listId, index);
5098
+ item.appendChild(createCommentUI(listId, elementId));
4683
5099
  });
4684
5100
 
4685
5101
  // Stakeholder cards in grid
@@ -4691,7 +5107,8 @@ function initInlineComments() {
4691
5107
  var sectionId = section ? section.id : 'stakeholders';
4692
5108
  var siblings = Array.from(grid.children);
4693
5109
  var index = siblings.indexOf(card);
4694
- card.appendChild(createCommentUI(sectionId, index));
5110
+ var elementId = getElementId(card, sectionId, index);
5111
+ card.appendChild(createCommentUI(sectionId, elementId));
4695
5112
  });
4696
5113
 
4697
5114
  // Scope items
@@ -4701,36 +5118,39 @@ function initInlineComments() {
4701
5118
  container.querySelectorAll('.uc-item').forEach(function(item, index) {
4702
5119
  if (item.dataset.commentInitialized) return;
4703
5120
  item.dataset.commentInitialized = 'true';
4704
- item.appendChild(createCommentUI(containerId, index));
5121
+ var elementId = getElementId(item, containerId, index);
5122
+ item.appendChild(createCommentUI(containerId, elementId));
4705
5123
  });
4706
5124
  });
4707
5125
  }
4708
5126
 
4709
- function createCommentUI(sectionId, cardIndex) {
4710
- const comments = getCommentsForCard(sectionId, cardIndex);
5127
+ function createCommentUI(sectionId, elementId) {
5128
+ const comments = getCommentsForCard(sectionId, elementId);
4711
5129
  const count = comments.length;
5130
+ const safeElId = String(elementId).replace(/[^a-zA-Z0-9_-]/g, '_');
5131
+ const threadId = 'comment-thread-' + sectionId + '-' + safeElId;
4712
5132
 
4713
5133
  const container = document.createElement('div');
4714
5134
  container.className = 'comment-btn-container';
4715
5135
  container.dataset.sectionId = sectionId;
4716
- container.dataset.cardIndex = cardIndex;
5136
+ container.dataset.elementId = elementId;
4717
5137
 
4718
5138
  container.innerHTML = `
4719
- <button class="comment-toggle-btn" onclick="toggleCommentThread('${sectionId}', ${cardIndex})">
5139
+ <button class="comment-toggle-btn" onclick="toggleCommentThread('${sectionId}', '${safeElId}')">
4720
5140
  Commentaires <span class="comment-count ${count === 0 ? 'empty' : ''}">${count}</span>
4721
5141
  </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)}
5142
+ <div class="comment-thread" id="${threadId}">
5143
+ <div class="comment-items" id="comment-items-${sectionId}-${safeElId}">
5144
+ ${renderCommentItems(sectionId, elementId)}
4725
5145
  </div>
4726
5146
  <div class="comment-add-form">
4727
- <textarea id="comment-text-${sectionId}-${cardIndex}" placeholder="Ajouter un commentaire..."></textarea>
4728
- <select id="comment-cat-${sectionId}-${cardIndex}">
5147
+ <textarea id="comment-text-${sectionId}-${safeElId}" placeholder="Ajouter un commentaire..."></textarea>
5148
+ <select id="comment-cat-${sectionId}-${safeElId}">
4729
5149
  <option value="clarification">Clarification</option>
4730
5150
  <option value="correction">Correction</option>
4731
5151
  <option value="suggestion">Suggestion</option>
4732
5152
  </select>
4733
- <button onclick="addInlineComment('${sectionId}', ${cardIndex})">Ajouter</button>
5153
+ <button onclick="addInlineComment('${sectionId}', '${elementId.replace(/'/g, "\\'")}')">Ajouter</button>
4734
5154
  </div>
4735
5155
  </div>
4736
5156
  `;
@@ -4738,25 +5158,27 @@ function createCommentUI(sectionId, cardIndex) {
4738
5158
  return container;
4739
5159
  }
4740
5160
 
4741
- function getCommentsForCard(sectionId, cardIndex) {
4742
- return (data.comments || []).filter(c =>
4743
- c.sectionId === sectionId && c.cardIndex === cardIndex
4744
- );
5161
+ function getCommentsForCard(sectionId, elementId) {
5162
+ return (data.comments || []).filter(function(c) {
5163
+ if (c.elementId) return c.sectionId === sectionId && c.elementId === elementId;
5164
+ // Backward compat: match by cardIndex if elementId not set
5165
+ return c.sectionId === sectionId && c.cardIndex === elementId;
5166
+ });
4745
5167
  }
4746
5168
 
4747
- function toggleCommentThread(sectionId, cardIndex) {
4748
- const thread = document.getElementById('comment-thread-' + sectionId + '-' + cardIndex);
5169
+ function toggleCommentThread(sectionId, safeElId) {
5170
+ const thread = document.getElementById('comment-thread-' + sectionId + '-' + safeElId);
4749
5171
  if (thread) thread.classList.toggle('visible');
4750
5172
  }
4751
5173
 
4752
- function renderCommentItems(sectionId, cardIndex) {
4753
- const comments = getCommentsForCard(sectionId, cardIndex);
5174
+ function renderCommentItems(sectionId, elementId) {
5175
+ const comments = getCommentsForCard(sectionId, elementId);
4754
5176
  if (comments.length === 0) {
4755
5177
  return '<div style="font-size:0.8rem;color:var(--text-muted);padding:0.5rem 0;font-style:italic;">Aucun commentaire</div>';
4756
5178
  }
4757
5179
 
4758
- return comments.map((c, i) => {
4759
- const initials = (c.author || 'U').substring(0, 2).toUpperCase();
5180
+ return comments.map(function(c, i) {
5181
+ const initials = escapeHtml((c.author || 'U').substring(0, 2).toUpperCase());
4760
5182
  const date = c.timestamp ? new Date(c.timestamp).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
4761
5183
  const globalIndex = data.comments.indexOf(c);
4762
5184
 
@@ -4765,12 +5187,12 @@ function renderCommentItems(sectionId, cardIndex) {
4765
5187
  <div class="comment-avatar">${initials}</div>
4766
5188
  <div class="comment-body">
4767
5189
  <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>
5190
+ <span class="comment-author">${escapeHtml(c.author || 'Utilisateur')}</span>
5191
+ <span class="comment-date">${escapeHtml(date)}</span>
5192
+ <span class="comment-category comment-category-${escapeHtml(c.category)}">${escapeHtml(c.category)}</span>
4771
5193
  <span class="comment-status comment-status-${c.status}">${c.status === 'validated' ? 'Validé' : 'À revoir'}</span>
4772
5194
  </div>
4773
- <div class="comment-text">${c.content}</div>
5195
+ <div class="comment-text">${escapeHtml(c.content)}</div>
4774
5196
  <div class="comment-actions">
4775
5197
  <button class="comment-action-btn" onclick="toggleCommentStatus(${globalIndex})">${c.status === 'validated' ? 'Remettre à revoir' : 'Valider'}</button>
4776
5198
  <button class="comment-action-btn" onclick="deleteComment(${globalIndex})" style="color:var(--error);">Supprimer</button>
@@ -4781,16 +5203,17 @@ function renderCommentItems(sectionId, cardIndex) {
4781
5203
  }).join('');
4782
5204
  }
4783
5205
 
4784
- function addInlineComment(sectionId, cardIndex) {
4785
- const textEl = document.getElementById('comment-text-' + sectionId + '-' + cardIndex);
4786
- const catEl = document.getElementById('comment-cat-' + sectionId + '-' + cardIndex);
5206
+ function addInlineComment(sectionId, elementId) {
5207
+ var safeElId = String(elementId).replace(/[^a-zA-Z0-9_-]/g, '_');
5208
+ const textEl = document.getElementById('comment-text-' + sectionId + '-' + safeElId);
5209
+ const catEl = document.getElementById('comment-cat-' + sectionId + '-' + safeElId);
4787
5210
  const content = textEl.value.trim();
4788
5211
  if (!content) return;
4789
5212
 
4790
5213
  const comment = {
4791
5214
  id: 'comment-' + Date.now(),
4792
5215
  sectionId: sectionId,
4793
- cardIndex: cardIndex,
5216
+ elementId: elementId,
4794
5217
  author: 'Utilisateur',
4795
5218
  timestamp: new Date().toISOString(),
4796
5219
  content: content,
@@ -4801,7 +5224,7 @@ function addInlineComment(sectionId, cardIndex) {
4801
5224
  data.comments.push(comment);
4802
5225
  textEl.value = '';
4803
5226
 
4804
- refreshCommentUI(sectionId, cardIndex);
5227
+ refreshCommentUI(sectionId, elementId);
4805
5228
  renderReviewPanel();
4806
5229
  autoSave();
4807
5230
  }
@@ -4810,32 +5233,35 @@ function toggleCommentStatus(globalIndex) {
4810
5233
  const comment = data.comments[globalIndex];
4811
5234
  if (!comment) return;
4812
5235
  comment.status = comment.status === 'validated' ? 'to-review' : 'validated';
4813
- refreshCommentUI(comment.sectionId, comment.cardIndex);
5236
+ var elId = comment.elementId || comment.cardIndex;
5237
+ refreshCommentUI(comment.sectionId, elId);
4814
5238
  renderReviewPanel();
4815
5239
  autoSave();
4816
5240
  }
4817
5241
 
4818
5242
  function deleteComment(globalIndex) {
5243
+ if (!confirm('Supprimer ce commentaire ?')) return;
4819
5244
  const comment = data.comments[globalIndex];
4820
5245
  if (!comment) return;
4821
- const sectionId = comment.sectionId;
4822
- const cardIndex = comment.cardIndex;
5246
+ var sectionId = comment.sectionId;
5247
+ var elId = comment.elementId || comment.cardIndex;
4823
5248
  data.comments.splice(globalIndex, 1);
4824
- refreshCommentUI(sectionId, cardIndex);
5249
+ refreshCommentUI(sectionId, elId);
4825
5250
  renderReviewPanel();
4826
5251
  autoSave();
4827
5252
  }
4828
5253
 
4829
- function refreshCommentUI(sectionId, cardIndex) {
5254
+ function refreshCommentUI(sectionId, elementId) {
5255
+ var safeElId = String(elementId).replace(/[^a-zA-Z0-9_-]/g, '_');
4830
5256
  // Update comment items
4831
- const itemsContainer = document.getElementById('comment-items-' + sectionId + '-' + cardIndex);
5257
+ const itemsContainer = document.getElementById('comment-items-' + sectionId + '-' + safeElId);
4832
5258
  if (itemsContainer) {
4833
- itemsContainer.innerHTML = renderCommentItems(sectionId, cardIndex);
5259
+ itemsContainer.innerHTML = renderCommentItems(sectionId, elementId);
4834
5260
  }
4835
5261
 
4836
5262
  // 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}"]`);
5263
+ const count = getCommentsForCard(sectionId, elementId).length;
5264
+ const container = document.querySelector('.comment-btn-container[data-section-id="' + sectionId + '"][data-element-id="' + elementId + '"]');
4839
5265
  if (container) {
4840
5266
  const badge = container.querySelector('.comment-count');
4841
5267
  if (badge) {
@@ -4918,17 +5344,19 @@ function renderReviewPanel() {
4918
5344
  return;
4919
5345
  }
4920
5346
 
4921
- container.innerHTML = filtered.map((c, i) => {
5347
+ container.innerHTML = filtered.map(function(c, i) {
4922
5348
  const globalIndex = data.comments.indexOf(c);
4923
5349
  const sectionLabel = getSectionLabel(c.sectionId);
4924
5350
  const date = c.timestamp ? new Date(c.timestamp).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }) : '';
5351
+ var elId = c.elementId || c.cardIndex;
5352
+ var safeElId = String(elId).replace(/[^a-zA-Z0-9_-]/g, '_');
4925
5353
 
4926
5354
  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>
5355
+ <div class="review-comment-item" onclick="navigateToComment('${c.sectionId}', '${safeElId}')">
5356
+ <div class="review-comment-section">${escapeHtml(sectionLabel)}</div>
5357
+ <div class="review-comment-text">${escapeHtml(c.content)}</div>
4930
5358
  <div class="review-comment-footer">
4931
- <span class="review-comment-author">${c.author || 'Utilisateur'} - ${date}</span>
5359
+ <span class="review-comment-author">${escapeHtml(c.author || 'Utilisateur')} - ${escapeHtml(date)}</span>
4932
5360
  <div class="review-comment-actions">
4933
5361
  <button class="review-action-btn ${c.status === 'validated' ? 'reject' : 'validate'}"
4934
5362
  onclick="event.stopPropagation();toggleCommentStatus(${globalIndex})"
@@ -4984,29 +5412,29 @@ function getSectionLabel(sectionId) {
4984
5412
  return sectionId;
4985
5413
  }
4986
5414
 
4987
- function navigateToComment(sectionId, cardIndex) {
5415
+ function navigateToComment(sectionId, safeElId) {
4988
5416
  // Handle list-based sectionIds (ucList-*, brList-*, entList-*) → navigate to module + tab
4989
5417
  const listMatch = sectionId.match(/^(uc|br|ent)List-(.+)$/);
4990
5418
  if (listMatch) {
4991
5419
  const [, tabType, moduleCode] = listMatch;
4992
5420
  showSection('module-spec-' + moduleCode);
4993
- setTimeout(() => {
5421
+ setTimeout(function() {
4994
5422
  switchTab(moduleCode, tabType);
4995
- scrollToCommentThread(sectionId, cardIndex);
5423
+ scrollToCommentThread(sectionId, safeElId);
4996
5424
  }, 150);
4997
5425
  } else {
4998
5426
  showSection(sectionId);
4999
- scrollToCommentThread(sectionId, cardIndex);
5427
+ scrollToCommentThread(sectionId, safeElId);
5000
5428
  }
5001
5429
  }
5002
5430
 
5003
- function scrollToCommentThread(sectionId, cardIndex) {
5004
- setTimeout(() => {
5005
- const thread = document.getElementById('comment-thread-' + sectionId + '-' + cardIndex);
5431
+ function scrollToCommentThread(sectionId, safeElId) {
5432
+ setTimeout(function() {
5433
+ const thread = document.getElementById('comment-thread-' + sectionId + '-' + safeElId);
5006
5434
  if (thread && !thread.classList.contains('visible')) {
5007
5435
  thread.classList.add('visible');
5008
5436
  }
5009
- const container = document.querySelector(`.comment-btn-container[data-section-id="${sectionId}"][data-card-index="${cardIndex}"]`);
5437
+ const container = document.querySelector('.comment-btn-container[data-section-id="' + sectionId + '"][data-element-id]');
5010
5438
  if (container) {
5011
5439
  container.scrollIntoView({ behavior: 'smooth', block: 'center' });
5012
5440
  }