hlsv 1.0.0 → 2.0.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -3
  3. data/README.md +36 -29
  4. data/lib/hlsv/analysis_registry.rb +47 -0
  5. data/lib/hlsv/analysis_runner.rb +193 -0
  6. data/lib/hlsv/config_manager.rb +125 -0
  7. data/lib/hlsv/html2word.rb +18 -18
  8. data/lib/hlsv/path_guard.rb +32 -0
  9. data/lib/hlsv/sdtm_validation/dataset.rb +169 -0
  10. data/lib/hlsv/sdtm_validation/define.rb +142 -0
  11. data/lib/hlsv/sdtm_validation/report.rb +357 -0
  12. data/lib/hlsv/sdtm_validation.rb +390 -0
  13. data/lib/hlsv/url_helper.rb +28 -0
  14. data/lib/hlsv/version.rb +1 -1
  15. data/lib/hlsv/web_app.rb +264 -418
  16. data/lib/hlsv.rb +12 -5
  17. data/public/css/accessibility/accessibility.css +33 -0
  18. data/public/css/base/layout.css +33 -0
  19. data/public/css/base/reset.css +23 -0
  20. data/public/css/base/typography.css +31 -0
  21. data/public/css/components/buttons.css +142 -0
  22. data/public/css/components/file-tree.css +107 -0
  23. data/public/css/components/footer.css +43 -0
  24. data/public/css/components/forms.css +56 -0
  25. data/public/css/components/header.css +52 -0
  26. data/public/css/components/status.css +56 -0
  27. data/public/css/features/csv-table.css +204 -0
  28. data/public/css/features/file-browser.css +208 -0
  29. data/public/css/responsive/responsive.css +133 -0
  30. data/public/css/styles.css +25 -0
  31. data/public/css/styles_csv.css +23 -0
  32. data/public/favicon.ico +0 -0
  33. data/public/js/analysis.js +201 -0
  34. data/public/js/app.js +63 -0
  35. data/public/js/browser.js +172 -0
  36. data/public/js/config.js +214 -0
  37. data/public/js/results.js +240 -0
  38. data/public/js/utils.js +57 -0
  39. data/views/csv_view.erb +11 -12
  40. data/views/index.erb +70 -19
  41. data/views/{report_template.erb → report.erb} +203 -188
  42. metadata +39 -41
  43. data/lib/hlsv/find_keys.rb +0 -979
  44. data/lib/hlsv/mon_script.rb +0 -169
  45. data/public/app.js +0 -569
  46. data/public/styles.css +0 -586
  47. data/public/styles_csv.css +0 -448
@@ -22,7 +22,7 @@
22
22
  margin: 0 auto;
23
23
  padding: 20px;
24
24
  padding-left: 80px; /* Space for hamburger */
25
- background-color: #f5f7fa;
25
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
26
26
  line-height: 1.6;
27
27
  color: #333;
28
28
  position: relative;
@@ -323,34 +323,40 @@
323
323
  MAIN CONTENT
324
324
  =========================== */
325
325
  .container {
326
- background-color: white;
326
+ background: white;
327
327
  padding: 40px;
328
328
  border-radius: 8px;
329
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
329
+ margin-bottom: 25px;
330
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
331
+ transition: box-shadow 0.3s ease;
332
+ }
333
+
334
+ .container:hover {
335
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
330
336
  }
331
337
 
332
338
  h1 {
333
- color: #2c3e50;
334
- border-bottom: 3px solid #3498db;
335
- padding-bottom: 12px;
336
- margin-bottom: 25px;
339
+ color: #1a1a1a;
340
+ border-bottom: 3px solid #007bff;
341
+ padding-bottom: 15px;
342
+ margin-bottom: 20px;
337
343
  font-size: 2em;
338
344
  }
339
345
 
340
346
  h2 {
341
- color: #34495e;
342
- margin-top: 40px;
347
+ color: #333;
348
+ margin-top: 0;
343
349
  margin-bottom: 20px;
344
- border-bottom: 2px solid #ecf0f1;
350
+ border-bottom: 2px solid #e9ecef;
345
351
  padding-bottom: 10px;
346
- font-size: 1.6em;
352
+ font-size: 1.5em;
347
353
  }
348
354
 
349
355
  h3 {
350
- color: #546e7a;
351
- margin-top: 25px;
352
- margin-bottom: 12px;
353
- font-size: 1.3em;
356
+ color: #495057;
357
+ margin-top: 20px;
358
+ margin-bottom: 10px;
359
+ font-size: 1.2em;
354
360
  }
355
361
 
356
362
  /* ===========================
@@ -358,6 +364,7 @@
358
364
  =========================== */
359
365
  .info-box {
360
366
  background: linear-gradient(135deg, #e8f4f8 0%, #d1ecf1 100%);
367
+ color: #3498db;
361
368
  border-left: 4px solid #3498db;
362
369
  padding: 18px;
363
370
  margin: 25px 0;
@@ -367,29 +374,32 @@
367
374
 
368
375
  .warning-box {
369
376
  background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
370
- border-left: 4px solid #f39c12;
377
+ color: #b8860b;
378
+ border-left: 4px solid #ffc107;
371
379
  padding: 18px;
372
380
  margin: 25px 0;
373
381
  border-radius: 6px;
374
- box-shadow: 0 2px 4px rgba(243, 156, 18, 0.1);
382
+ box-shadow: 0 2px 4px rgba(255, 193, 7, 0.1);
375
383
  }
376
384
 
377
385
  .error-box {
378
386
  background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
379
- border-left: 4px solid #e74c3c;
387
+ color: #dc3545;
388
+ border-left: 4px solid #dc3545;
380
389
  padding: 18px;
381
390
  margin: 25px 0;
382
391
  border-radius: 6px;
383
- box-shadow: 0 2px 4px rgba(231, 76, 60, 0.1);
392
+ box-shadow: 0 2px 4px rgba(220, 53, 69, 0.1);
384
393
  }
385
394
 
386
395
  .success-box {
387
396
  background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
388
- border-left: 4px solid #27ae60;
397
+ color: #28a745;
398
+ border-left: 4px solid #28a745;
389
399
  padding: 18px;
390
400
  margin: 25px 0;
391
401
  border-radius: 6px;
392
- box-shadow: 0 2px 4px rgba(39, 174, 96, 0.1);
402
+ box-shadow: 0 2px 4px rgba(40, 167, 69, 0.1);
393
403
  }
394
404
 
395
405
  /* ===========================
@@ -447,12 +457,17 @@
447
457
  STATUS TEXT COLORS
448
458
  =========================== */
449
459
  .success-text {
450
- color: #27ae60;
460
+ color: #28a745;
461
+ font-weight: 600;
462
+ }
463
+
464
+ .error-text {
465
+ color: #dc3545;
451
466
  font-weight: 600;
452
467
  }
453
468
 
454
469
  .warning-text {
455
- color: #f39c12;
470
+ color: #b8860b;
456
471
  font-weight: 600;
457
472
  }
458
473
 
@@ -728,14 +743,11 @@
728
743
  <li><a href="#configuration">Configuration</a></li>
729
744
  <li><a href="#datasets-analysis">Datasets Analysis</a>
730
745
  <ul class="toc-sublist">
731
- <% dataset_reports.each do |ds_name, report| %>
746
+ <% datasets.keys.each do |ds_name| %>
732
747
  <li><a href="#ds-<%= ds_name %>"><%= ds_name %></a></li>
733
748
  <% end %>
734
749
  </ul>
735
750
  </li>
736
- <% if externe_file_type != 'csv' %>
737
- <li><a href="#output-file">Output File</a></li>
738
- <% end %>
739
751
  </ul>
740
752
  </nav>
741
753
 
@@ -761,168 +773,148 @@
761
773
  <div style="display: flex; justify-content: space-between; align-items: center;">
762
774
  <p style="margin: 0;">Report automatically generated on <%= Time.now.strftime("%Y-%m-%d at %H:%M") %></p>
763
775
  <% if @web_mode %>
764
- <button class="btn-word" id="convert-btn" onclick="convertToWord()"> 📄 Word </button>
776
+ <button class="btn-word" id="convert-btn" onclick="convertToWord()"> 📄 Word </button>
765
777
  <% end %>
766
778
  </div>
767
779
  </div>
768
780
 
769
781
  <section id="configuration">
770
- <h2>Configuration</h2>
771
- <p>
772
- This report is generated based on the information provided in the configuration form.
773
- This configuration is saved in the file: <code><%= init_reports[:config_name] %></code>
774
- </p>
775
-
776
- <h3>Report Coverage:</h3>
777
- <ul>
778
- <li>Presence of non-ASCII characters in the data</li>
779
- <li>Validity of keys declared in define.xml</li>
780
- <li>Ad hoc search for a natural key in all datasets</li>
781
- </ul>
782
-
783
- <h3>Define.xml Information</h3>
784
- <% define_info = init_reports[:define_information] %>
785
- <ul>
786
- <li><strong>Path:</strong> <%= define_info[:define_path] || 'Not specified' %></li>
787
- <% if define_info[:load] %>
788
- <li><strong>Status:</strong> <span class="success-text">Loaded successfully</span></li>
789
- <% elsif define_info[:define_path] == '-' %>
790
- <li><strong>Status:</strong> <span class="info-text">Not loaded (as expected)</span></li>
791
- <% else %>
792
- <li><strong>Status:</strong> <span class="warning-text">Not loaded</span></li>
793
- <% end %>
794
- </ul>
795
-
796
- <h3>Datasets Information</h3>
797
- <% data_info = init_reports[:data_information] %>
798
- <ul>
799
- <li><strong>Source directory:</strong> <%= data_info[:directory_name] %></li>
800
- <li><strong>Number of files detected:</strong> <%= data_info[:file_number] %></li>
801
- </ul>
802
- </section>
803
-
804
- <h2 id="datasets-analysis">Datasets Analysis</h2>
782
+ <h2>Configuration</h2>
783
+ <p> This report is generated based on the information provided in the configuration form.</p>
784
+ <p> This configuration is saved in the file: <code><%= config_file_path %></code> </p>
785
+
786
+ <h3>Report Coverage:</h3>
787
+ <ul>
788
+ <li>Presence of non-ASCII characters in the data</li>
789
+ <li>Validity of keys declared in define.xml</li>
790
+ <li>Ad hoc search for a natural key in all datasets</li>
791
+ </ul>
805
792
 
806
- <% dataset_reports.each do |ds_name, report| %>
807
- <article class="dataset-section" id="ds-<%= ds_name %>">
808
- <h3>Dataset: <%= ds_name %></h3>
793
+ <h3>Define.xml Information</h3>
794
+ <ul>
795
+ <% define.exception.each do |e| %>
796
+ <li> <%= e %></li>
797
+ <% end %>
798
+ <li><strong>Path:</strong> <%= define.path || 'Not specified' %></li>
799
+ <% if define.status %>
800
+ <li><strong>Status:</strong> <span class="success-text">Loaded successfully</span></li>
801
+ <% elsif define.status.nil? %>
802
+ <li><strong>Status:</strong> <span class="error-text">Not loaded</span></li>
803
+ <% else %>
804
+ <li><strong>Status:</strong> <span class="info-text">Not loaded (as expected)</span></li>
805
+ <% end %>
806
+ <li><strong>Number of datasets detected:</strong> <%= define.datasets.size %></li>
807
+ </ul>
809
808
 
810
- <% if report[:record_count] %>
811
- <p><strong>Number of records:</strong> <%= report[:record_count] %></p>
812
- <% end %>
809
+ <h3>Datasets Information</h3>
810
+ <ul>
811
+ <li><strong>Source directory:</strong> <%= ds_path %></li>
812
+ <li><strong>Number of datasets on disk detected:</strong> <%= datasets.values.reject { |ds| ds.label.nil? }.size %></li>
813
+ <% unless excluded_dataset.empty? %>
814
+ <li><strong>Excluded dataset:</strong> <span class="warning-text"><%= excluded_dataset.join(', ') %></span></li>
815
+ <% end %>
816
+ </ul>
817
+ </section>
813
818
 
814
- <% if report[:candidates_type] %>
815
- <p><strong>Dataset type:</strong> <%= report[:candidates_type] %></p>
816
- <% end %>
819
+ <h2 id="datasets-analysis">Datasets Analysis</h2>
820
+ <% datasets.values.each do |ds| %>
821
+ <article class="dataset-section" id="ds-<%= ds.name %>">
822
+ <h3>Dataset: <%= ds.name %></h3>
823
+
824
+ <p><strong>Number of records:</strong> <%= ds.records.size %></p>
825
+ <p><strong>Dataset type:</strong> <%= ds.type_to_s(ds.name) %></p>
826
+
827
+ <!-- ASCII Check -->
828
+ <% if ds.ascii_status %>
829
+ <div class="success-box" role="status">
830
+ <strong>✓ ASCII Verification</strong>
831
+ <p>No non-ASCII characters found</p>
832
+ </div>
833
+ <% elsif ds.ascii_status.nil? %>
834
+ <!-- Nothing to do -->
835
+ <% else %>
836
+ <div class="error-box" role="alert">
837
+ <strong>✗ Non-ASCII Characters Detected</strong>
838
+ <ul>
839
+ <% ds.non_ascii_values.each do |issue| %>
840
+ <li><%= issue %></li>
841
+ <% end %>
842
+ </ul>
843
+ </div>
844
+ <% end %>
817
845
 
818
- <!-- ASCII Check -->
819
- <% if report[:ascii_check] %>
820
- <% if report[:ascii_check][:valid] %>
821
- <div class="success-box" role="status">
822
- <strong>✓ ASCII Verification</strong>
823
- <p>No non-ASCII characters found</p>
824
- </div>
825
- <% else %>
846
+ <!-- Define Key Check -->
847
+ <% if define.status %>
848
+ <% if ds.define_status %>
849
+ <div class="success-box" role="status">
850
+ <strong>✓ Valid define.xml Key</strong>
851
+ <p>Key: <span class="key-info"><%= ds.define_keys.join(', ') %></span></p>
852
+ </div>
853
+ <% elsif ds.define_status.nil? %>
854
+ <% if ds.label.nil? %>
826
855
  <div class="error-box" role="alert">
827
- <strong>✗ Non-ASCII Characters Detected</strong>
828
- <ul>
829
- <% report[:ascii_check][:issues].each do |issue| %>
830
- <li><%= issue %></li>
831
- <% end %>
832
- </ul>
856
+ <strong>✗ Invalid define.xml Key</strong>
857
+ <p> dataset not on disk, found in the define.xml </p>
833
858
  </div>
834
- <% end %>
835
- <% end %>
836
-
837
- <!-- Define Key Check -->
838
- <% if report[:define_key_check] %>
839
- <% if report[:define_key_check][:valid]%>
840
- <% if report[:define_key_check][:key] %>
841
- <div class="success-box" role="status">
842
- <strong>✓ Valid define.xml Key</strong>
843
- <p>Key: <span class="key-info"><%= report[:define_key_check][:key].join(', ') %></span></p>
844
- </div>
845
- <% end %>
846
859
  <% else %>
847
860
  <div class="error-box" role="alert">
848
861
  <strong>✗ Invalid define.xml Key</strong>
849
- <% if report[:define_key_check][:absent] %>
850
- <p>dataset not found in the define.xml</p>
851
- <% end %>
852
- <% if report[:define_key_check][:key] %>
853
- <p>Tested key: <span class="key-info"><%= report[:define_key_check][:key].join(', ') %></span></p>
854
- <% end %>
855
- <% if report[:define_key_check][:duplicate_file] %>
856
- <p>File containing duplicated records:
857
- <a href="/csv_view?file=<%= URI.encode_www_form_component(report[:define_key_check][:duplicate_file]) %>"
858
- target="_blank"
859
- rel="noopener noreferrer">
860
- <%= File.basename(report[:define_key_check][:duplicate_file]) %>
861
- </a>
862
- </p>
863
- <% end %>
862
+ <p> dataset on disk, not found in the define.xml </p>
864
863
  </div>
865
864
  <% end %>
865
+ <% else %>
866
+ <div class="error-box" role="alert">
867
+ <strong>✗ Invalid define.xml Key</strong>
868
+ <p>Tested key: <span class="key-info"><%= ds.define_keys.join(', ') %></span></p>
869
+ <% if @web_mode %>
870
+ <p>File containing duplicated records:
871
+ <a href="<%= csv_view_url(file: ds.define_duplicate_file, study: title, dataset: ds.name, last_valid_key: ds.define_keys) %>"
872
+ data-offline-href="<%= relative_report_path(ds.define_duplicate_file) %>"
873
+ class="csv-link"
874
+ target="_blank"
875
+ rel="noopener noreferrer">
876
+ <%= File.basename(ds.define_duplicate_file) %>
877
+ </a>
878
+ </p>
879
+ <% else %>
880
+ <p>File containing duplicated records: <code><%= ds.define_duplicate_file %></code> </p>
881
+ <% end %>
882
+ </div>
866
883
  <% end %>
884
+ <% end %>
867
885
 
868
- <!-- Data Key Check -->
869
- <% if report[:data_key_check] %>
870
- <%
871
- # Handle case where data_key_check can be an array (SE, SV)
872
- checks = report[:data_key_check].is_a?(Array) ? report[:data_key_check] : [report[:data_key_check]]
873
- %>
874
-
875
- <% checks.each_with_index do |check, idx| %>
876
- <% if check[:valid] %>
877
- <div class="success-box" role="status">
878
- <strong>✓ Minimum Key Found <%= checks.size > 1 ? "(Option #{idx + 1})" : "" %></strong>
879
- <p>Key: <span class="key-info"><%= check[:key].join(', ') %></span></p>
880
- </div>
886
+ <!-- Data Key Check -->
887
+ <% ds.minimal_key_status.each_with_index do |status, i| %>
888
+ <% if status %>
889
+ <div class="success-box" role="status">
890
+ <strong>✓ Minimum Key Found </strong> <%= ds.minimal_key_status.size > 1 ? "(Option #{i + 1})" : "" %>
891
+ <p>Key: <span class="key-info"><%= ds.minimal_key[i].join(', ') %></span></p>
892
+ </div>
893
+ <% else %>
894
+ <div class="warning-box" role="status">
895
+ <strong>⚠ No Valid Key Found </strong> <%= ds.minimal_key_status.size > 1 ? "(Option #{i + 1})" : "" %>
896
+ <p><em>Tested variables: <%= ds.tested_key[i].join(', ') %></em></p>
897
+ <p>Last key tested: <span class="key-info"><%= ds.minimal_key[i]&.join(', ') %></span></p>
898
+ <% if @web_mode %>
899
+ <p>File containing duplicated records:
900
+ <a href="<%= csv_view_url(file: ds.data_duplicate_file[i], study: title, dataset: ds.name, last_valid_key: ds.minimal_key[i]) %>"
901
+ data-offline-href="<%= relative_report_path(ds.data_duplicate_file[i]) %>"
902
+ class="csv-link"
903
+ target="_blank"
904
+ rel="noopener noreferrer">
905
+ <%= File.basename(ds.data_duplicate_file[i]) %>
906
+ </a>
907
+ </p>
881
908
  <% else %>
882
- <div class="warning-box" role="status">
883
- <strong>⚠ No Valid Key Found <%= checks.size > 1 ? "(Option #{idx + 1})" : "" %></strong>
884
- <% if check[:candidates] %>
885
- <p><em>Tested variables: <%= check[:candidates].join(', ') %></em></p>
886
- <% end %>
887
- <% if check[:last_valid_key] %>
888
- <p>Last key tested: <span class="key-info"><%= check[:last_valid_key].join(', ') %></span></p>
889
- <% end %>
890
- <% if check[:duplicate_file] %>
891
- <p>File containing duplicated records:
892
- <a href="/csv_view?file=<%= URI.encode_www_form_component(check[:duplicate_file]) %>&last_valid_key=<%= URI.encode_www_form_component(check[:last_valid_key].join(',')) %>"
893
- target="_blank"
894
- rel="noopener noreferrer">
895
- <%= File.basename(check[:duplicate_file]) %>
896
- </a>
897
- </p>
898
- <% end %>
899
- </div>
909
+ <p>File containing duplicated records: <code><%= ds.data_duplicate_file[i] %></code> </p>
900
910
  <% end %>
901
- <% end %>
911
+ </div>
902
912
  <% end %>
913
+ <% end %>
903
914
 
904
- <!-- Verbose Details -->
905
- <% if report[:verbose_details] && report[:verbose_details].any? %>
906
- <details>
907
- <summary>Processing Details</summary>
908
- <ul>
909
- <% report[:verbose_details].each do |detail| %>
910
- <li><%= detail %></li>
911
- <% end %>
912
- </ul>
913
- </details>
914
- <% end %>
915
915
  </article>
916
916
  <% end %>
917
917
 
918
- <!-- Output File Section -->
919
- <% if output_file %>
920
- <section id="output-file">
921
- <h2>Output File</h2>
922
- <p>File containing duplicated records: <strong><%= File.basename(output_file) %></strong></p>
923
- <p>Full path: <code><%= output_file %></code></p>
924
- </section>
925
- <% end %>
926
918
  </main>
927
919
 
928
920
  <!-- Report Footer with Copyright and License -->
@@ -935,18 +927,41 @@
935
927
  </footer>
936
928
 
937
929
  <script>
938
- // Download HTML file as Word document
930
+ // Download this report as a Word document.
931
+ // The report is served from /report/:study/html - the docx route
932
+ // generates the file on click and serves it directly (no JSON round-trip
933
+ // needed anymore, since '/report/:study/docx' streams the file itself).
934
+ function isServerReachable(timeoutMs = 1500) {
935
+ const controller = new AbortController();
936
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
937
+ return fetch('/ping', { method: 'GET', signal: controller.signal })
938
+ .then(response => { clearTimeout(timeout); return response.ok; })
939
+ .catch(() => { clearTimeout(timeout); return false; });
940
+ }
941
+
942
+ const serverReachablePromise = isServerReachable();
943
+
939
944
  async function convertToWord() {
940
- const chemin = window.location.pathname.replace(/^\/telecharger\//, '');
941
- const response = await fetch('/telecharger_html_word/' + chemin);
942
- const data = await response.json();
943
- if (data.success) {
944
- const cheminDocx = chemin.replace(/\.html$/, '.docx');
945
- window.location.href = '/telecharger/' + cheminDocx;
946
- } else {
947
- alert('❌ ' + data.erreur);
945
+ const online = await serverReachablePromise;
946
+ if (!online) {
947
+ window.location.href = '<%= File.basename(@report_name) %>.docx';
948
+ return;
949
+ }
950
+ const match = window.location.pathname.match(/^\/report\/([^/]+)\/html$/);
951
+ if (!match) {
952
+ alert('❌ Unable to identify the study for this report.');
953
+ return;
948
954
  }
955
+ window.location.href = '/report/' + match[1] + '/docx';
949
956
  }
957
+
958
+ serverReachablePromise.then(online => {
959
+ if (online) return;
960
+ document.querySelectorAll('a.csv-link').forEach(link => {
961
+ link.href = link.dataset.offlineHref;
962
+ link.removeAttribute('target');
963
+ });
964
+ });
950
965
  // Enhanced TOC functionality with accessibility
951
966
 
952
967
  // DOM Elements
@@ -1049,7 +1064,7 @@
1049
1064
  }
1050
1065
 
1051
1066
  // Colour coding of datasets in the TOC according to their maximum error level
1052
- function coloriserTOCDatasets() {
1067
+ function colorizeTocDatasets() {
1053
1068
  document.querySelectorAll('.toc-sublist a').forEach(link => {
1054
1069
  const href = link.getAttribute('href');
1055
1070
  if (!href) return;
@@ -1057,29 +1072,29 @@
1057
1072
  const section = document.querySelector(href);
1058
1073
  if (!section) return;
1059
1074
 
1060
- const aErreur = section.querySelector('.error-box');
1061
- const aWarning = section.querySelector('.warning-box');
1075
+ const errorBox = section.querySelector('.error-box');
1076
+ const warningBox = section.querySelector('.warning-box');
1062
1077
 
1063
- if (aErreur) {
1064
- // Rouge - priorité maximale
1078
+ if (errorBox) {
1079
+ // Red - highest priority
1065
1080
  link.style.backgroundColor = '#f8d7da';
1066
- link.style.borderLeftColor = '#e74c3c';
1067
- link.style.color = '#c0392b';
1068
- } else if (aWarning) {
1081
+ link.style.borderLeftColor = '#dc3545';
1082
+ link.style.color = '#dc3545';
1083
+ } else if (warningBox) {
1069
1084
  // Orange
1070
1085
  link.style.backgroundColor = '#fff3cd';
1071
- link.style.borderLeftColor = '#f39c12';
1072
- link.style.color = '#856404';
1086
+ link.style.borderLeftColor = '#ffc107';
1087
+ link.style.color = '#b8860b';
1073
1088
  }
1074
- // Vert (success-box uniquement) → aucune couleur appliquée
1089
+ // Green (success-box only) → no color applied
1075
1090
  });
1076
1091
  }
1077
1092
 
1078
- // Attendre que le DOM soit prêt
1093
+ // Wait for the DOM to be ready
1079
1094
  if (document.readyState === 'loading') {
1080
- document.addEventListener('DOMContentLoaded', coloriserTOCDatasets);
1095
+ document.addEventListener('DOMContentLoaded', colorizeTocDatasets);
1081
1096
  } else {
1082
- coloriserTOCDatasets();
1097
+ colorizeTocDatasets();
1083
1098
  }
1084
1099
 
1085
1100
  // Smooth scroll for anchor links
@@ -1141,4 +1156,4 @@
1141
1156
  });
1142
1157
  </script>
1143
1158
  </body>
1144
- </html>
1159
+ </html>