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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -3
- data/README.md +36 -29
- data/lib/hlsv/analysis_registry.rb +47 -0
- data/lib/hlsv/analysis_runner.rb +193 -0
- data/lib/hlsv/config_manager.rb +125 -0
- data/lib/hlsv/html2word.rb +18 -18
- data/lib/hlsv/path_guard.rb +32 -0
- data/lib/hlsv/sdtm_validation/dataset.rb +169 -0
- data/lib/hlsv/sdtm_validation/define.rb +142 -0
- data/lib/hlsv/sdtm_validation/report.rb +357 -0
- data/lib/hlsv/sdtm_validation.rb +390 -0
- data/lib/hlsv/url_helper.rb +28 -0
- data/lib/hlsv/version.rb +1 -1
- data/lib/hlsv/web_app.rb +264 -418
- data/lib/hlsv.rb +12 -5
- data/public/css/accessibility/accessibility.css +33 -0
- data/public/css/base/layout.css +33 -0
- data/public/css/base/reset.css +23 -0
- data/public/css/base/typography.css +31 -0
- data/public/css/components/buttons.css +142 -0
- data/public/css/components/file-tree.css +107 -0
- data/public/css/components/footer.css +43 -0
- data/public/css/components/forms.css +56 -0
- data/public/css/components/header.css +52 -0
- data/public/css/components/status.css +56 -0
- data/public/css/features/csv-table.css +204 -0
- data/public/css/features/file-browser.css +208 -0
- data/public/css/responsive/responsive.css +133 -0
- data/public/css/styles.css +25 -0
- data/public/css/styles_csv.css +23 -0
- data/public/favicon.ico +0 -0
- data/public/js/analysis.js +201 -0
- data/public/js/app.js +63 -0
- data/public/js/browser.js +172 -0
- data/public/js/config.js +214 -0
- data/public/js/results.js +240 -0
- data/public/js/utils.js +57 -0
- data/views/csv_view.erb +11 -12
- data/views/index.erb +70 -19
- data/views/{report_template.erb → report.erb} +203 -188
- metadata +39 -41
- data/lib/hlsv/find_keys.rb +0 -979
- data/lib/hlsv/mon_script.rb +0 -169
- data/public/app.js +0 -569
- data/public/styles.css +0 -586
- 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
|
|
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
|
|
326
|
+
background: white;
|
|
327
327
|
padding: 40px;
|
|
328
328
|
border-radius: 8px;
|
|
329
|
-
|
|
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: #
|
|
334
|
-
border-bottom: 3px solid #
|
|
335
|
-
padding-bottom:
|
|
336
|
-
margin-bottom:
|
|
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: #
|
|
342
|
-
margin-top:
|
|
347
|
+
color: #333;
|
|
348
|
+
margin-top: 0;
|
|
343
349
|
margin-bottom: 20px;
|
|
344
|
-
border-bottom: 2px solid #
|
|
350
|
+
border-bottom: 2px solid #e9ecef;
|
|
345
351
|
padding-bottom: 10px;
|
|
346
|
-
font-size: 1.
|
|
352
|
+
font-size: 1.5em;
|
|
347
353
|
}
|
|
348
354
|
|
|
349
355
|
h3 {
|
|
350
|
-
color: #
|
|
351
|
-
margin-top:
|
|
352
|
-
margin-bottom:
|
|
353
|
-
font-size: 1.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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: #
|
|
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: #
|
|
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
|
-
<%
|
|
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
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
<
|
|
777
|
-
<
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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>✗
|
|
828
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
|
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
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
alert('❌
|
|
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
|
|
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
|
|
1061
|
-
const
|
|
1075
|
+
const errorBox = section.querySelector('.error-box');
|
|
1076
|
+
const warningBox = section.querySelector('.warning-box');
|
|
1062
1077
|
|
|
1063
|
-
if (
|
|
1064
|
-
//
|
|
1078
|
+
if (errorBox) {
|
|
1079
|
+
// Red - highest priority
|
|
1065
1080
|
link.style.backgroundColor = '#f8d7da';
|
|
1066
|
-
link.style.borderLeftColor = '#
|
|
1067
|
-
link.style.color = '#
|
|
1068
|
-
} else if (
|
|
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 = '#
|
|
1072
|
-
link.style.color = '#
|
|
1086
|
+
link.style.borderLeftColor = '#ffc107';
|
|
1087
|
+
link.style.color = '#b8860b';
|
|
1073
1088
|
}
|
|
1074
|
-
//
|
|
1089
|
+
// Green (success-box only) → no color applied
|
|
1075
1090
|
});
|
|
1076
1091
|
}
|
|
1077
1092
|
|
|
1078
|
-
//
|
|
1093
|
+
// Wait for the DOM to be ready
|
|
1079
1094
|
if (document.readyState === 'loading') {
|
|
1080
|
-
document.addEventListener('DOMContentLoaded',
|
|
1095
|
+
document.addEventListener('DOMContentLoaded', colorizeTocDatasets);
|
|
1081
1096
|
} else {
|
|
1082
|
-
|
|
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>
|