hlsv 1.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.
@@ -0,0 +1,1144 @@
1
+ <!--
2
+ Copyright (c) 2026 AdClin
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+ -->
9
+
10
+ <!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <style>
16
+ /* ===========================
17
+ BASE STYLES
18
+ =========================== */
19
+ body {
20
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
21
+ max-width: 1200px;
22
+ margin: 0 auto;
23
+ padding: 20px;
24
+ padding-left: 80px; /* Space for hamburger */
25
+ background-color: #f5f7fa;
26
+ line-height: 1.6;
27
+ color: #333;
28
+ position: relative;
29
+ }
30
+
31
+ /* ===========================
32
+ REPORT HEADER WITH BRANDING
33
+ =========================== */
34
+ .report-header {
35
+ display: flex;
36
+ justify-content: space-between;
37
+ align-items: center;
38
+ margin-bottom: 25px;
39
+ padding: 25px;
40
+ background: white;
41
+ border-radius: 8px;
42
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
43
+ }
44
+
45
+ .report-header h1 {
46
+ margin: 0;
47
+ padding: 0;
48
+ border: none;
49
+ color: #2c3e50;
50
+ font-size: 2em;
51
+ }
52
+
53
+ .company-branding {
54
+ display: flex;
55
+ align-items: center;
56
+ }
57
+
58
+ .company-link {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 12px;
62
+ text-decoration: none;
63
+ transition: all 0.3s ease;
64
+ }
65
+
66
+ .company-link:hover {
67
+ opacity: 0.8;
68
+ transform: translateY(-2px);
69
+ }
70
+
71
+ .company-logo {
72
+ height: 50px;
73
+ width: auto;
74
+ object-fit: contain;
75
+ }
76
+
77
+ .company-name {
78
+ font-size: 1.2em;
79
+ font-weight: 600;
80
+ color: #2c3e50;
81
+ }
82
+
83
+ /* ===========================
84
+ REPORT FOOTER WITH COPYRIGHT AND LICENSE
85
+ =========================== */
86
+ .report-footer {
87
+ margin-top: 40px;
88
+ padding: 20px;
89
+ background: white;
90
+ border-radius: 8px;
91
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
92
+ text-align: center;
93
+ }
94
+
95
+ .footer-content {
96
+ display: flex;
97
+ justify-content: center;
98
+ align-items: center;
99
+ flex-wrap: wrap;
100
+ gap: 15px;
101
+ color: #6c757d;
102
+ font-size: 0.9em;
103
+ }
104
+
105
+ .footer-content .separator {
106
+ color: #dee2e6;
107
+ font-weight: bold;
108
+ }
109
+
110
+ .footer-content a {
111
+ color: #3498db;
112
+ text-decoration: none;
113
+ transition: color 0.3s ease;
114
+ }
115
+
116
+ .footer-content a:hover {
117
+ color: #2980b9;
118
+ text-decoration: underline;
119
+ }
120
+
121
+ /* ===========================
122
+ BUTTON
123
+ =========================== */
124
+ .hamburger {
125
+ position: fixed;
126
+ top: 20px;
127
+ left: 20px;
128
+ z-index: 1001;
129
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
130
+ border: none;
131
+ border-radius: 8px;
132
+ padding: 12px;
133
+ cursor: pointer;
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 5px;
137
+ box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
138
+ transition: all 0.3s ease;
139
+ }
140
+
141
+ .hamburger:hover {
142
+ background: linear-gradient(135deg, #2980b9 0%, #21618c 100%);
143
+ transform: scale(1.05);
144
+ box-shadow: 0 6px 16px rgba(52, 152, 219, 0.4);
145
+ }
146
+
147
+ .hamburger:focus {
148
+ outline: 2px solid #3498db;
149
+ outline-offset: 2px;
150
+ }
151
+
152
+ .hamburger span {
153
+ display: block;
154
+ width: 25px;
155
+ height: 3px;
156
+ background-color: white;
157
+ border-radius: 2px;
158
+ transition: all 0.3s ease;
159
+ }
160
+
161
+ .btn-word {
162
+ background-color: #2B579A;
163
+ color: white;
164
+ border: none;
165
+ padding: 8px 16px;
166
+ border-radius: 4px;
167
+ cursor: pointer;
168
+ font-size: 14px;
169
+ display: inline-flex;
170
+ align-items: center;
171
+ gap: 6px;
172
+ white-space: nowrap;
173
+ }
174
+
175
+ /* ===========================
176
+ TABLE OF CONTENTS
177
+ =========================== */
178
+ .toc {
179
+ position: fixed;
180
+ top: 0;
181
+ left: -350px;
182
+ width: 320px;
183
+ height: 100vh;
184
+ background-color: white;
185
+ box-shadow: 2px 0 20px rgba(0,0,0,0.1);
186
+ z-index: 1002;
187
+ overflow-y: auto;
188
+ transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
189
+ padding: 20px;
190
+ }
191
+
192
+ .toc.active {
193
+ left: 0;
194
+ }
195
+
196
+ .toc-header {
197
+ display: flex;
198
+ justify-content: space-between;
199
+ align-items: center;
200
+ margin-bottom: 25px;
201
+ padding-bottom: 15px;
202
+ border-bottom: 2px solid #ecf0f1;
203
+ }
204
+
205
+ .toc-header h2 {
206
+ margin: 0;
207
+ color: #2c3e50;
208
+ font-size: 1.4em;
209
+ border: none;
210
+ padding: 0;
211
+ }
212
+
213
+ .close-btn {
214
+ background: none;
215
+ border: none;
216
+ font-size: 2em;
217
+ color: #95a5a6;
218
+ cursor: pointer;
219
+ padding: 0;
220
+ width: 35px;
221
+ height: 35px;
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ transition: all 0.3s ease;
226
+ border-radius: 4px;
227
+ }
228
+
229
+ .close-btn:hover {
230
+ color: #e74c3c;
231
+ background-color: #fef5f5;
232
+ }
233
+
234
+ .close-btn:focus {
235
+ outline: 2px solid #3498db;
236
+ outline-offset: 2px;
237
+ }
238
+
239
+ .toc-list {
240
+ list-style: none;
241
+ padding: 0;
242
+ margin: 0;
243
+ }
244
+
245
+ .toc-list > li {
246
+ margin-bottom: 12px;
247
+ }
248
+
249
+ .toc-list a {
250
+ display: block;
251
+ padding: 10px 14px;
252
+ color: #2c3e50;
253
+ text-decoration: none;
254
+ border-radius: 6px;
255
+ transition: all 0.3s ease;
256
+ font-weight: 500;
257
+ border-left: 3px solid transparent;
258
+ }
259
+
260
+ .toc-list a:hover {
261
+ background-color: #e8f4f8;
262
+ color: #3498db;
263
+ border-left-color: #3498db;
264
+ transform: translateX(5px);
265
+ }
266
+
267
+ .toc-list a:focus {
268
+ outline: 2px solid #3498db;
269
+ outline-offset: 2px;
270
+ }
271
+
272
+ .toc-list a.active {
273
+ background-color: #d6eef7;
274
+ color: #2980b9;
275
+ border-left-color: #2980b9;
276
+ font-weight: 600;
277
+ }
278
+
279
+ .toc-sublist {
280
+ list-style: none;
281
+ padding-left: 20px;
282
+ margin-top: 8px;
283
+ }
284
+
285
+ .toc-sublist li {
286
+ margin-bottom: 6px;
287
+ }
288
+
289
+ .toc-sublist a {
290
+ font-size: 0.9em;
291
+ font-weight: normal;
292
+ padding: 8px 12px;
293
+ color: #7f8c8d;
294
+ }
295
+
296
+ .toc-sublist a:hover {
297
+ background-color: #f8f9fa;
298
+ color: #3498db;
299
+ }
300
+
301
+ /* ===========================
302
+ OVERLAY
303
+ =========================== */
304
+ .overlay {
305
+ position: fixed;
306
+ top: 0;
307
+ left: 0;
308
+ width: 100%;
309
+ height: 100%;
310
+ background-color: rgba(0, 0, 0, 0.5);
311
+ z-index: 1000;
312
+ opacity: 0;
313
+ visibility: hidden;
314
+ transition: all 0.3s ease;
315
+ }
316
+
317
+ .overlay.active {
318
+ opacity: 1;
319
+ visibility: visible;
320
+ }
321
+
322
+ /* ===========================
323
+ MAIN CONTENT
324
+ =========================== */
325
+ .container {
326
+ background-color: white;
327
+ padding: 40px;
328
+ border-radius: 8px;
329
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
330
+ }
331
+
332
+ h1 {
333
+ color: #2c3e50;
334
+ border-bottom: 3px solid #3498db;
335
+ padding-bottom: 12px;
336
+ margin-bottom: 25px;
337
+ font-size: 2em;
338
+ }
339
+
340
+ h2 {
341
+ color: #34495e;
342
+ margin-top: 40px;
343
+ margin-bottom: 20px;
344
+ border-bottom: 2px solid #ecf0f1;
345
+ padding-bottom: 10px;
346
+ font-size: 1.6em;
347
+ }
348
+
349
+ h3 {
350
+ color: #546e7a;
351
+ margin-top: 25px;
352
+ margin-bottom: 12px;
353
+ font-size: 1.3em;
354
+ }
355
+
356
+ /* ===========================
357
+ STATUS BOXES
358
+ =========================== */
359
+ .info-box {
360
+ background: linear-gradient(135deg, #e8f4f8 0%, #d1ecf1 100%);
361
+ border-left: 4px solid #3498db;
362
+ padding: 18px;
363
+ margin: 25px 0;
364
+ border-radius: 6px;
365
+ box-shadow: 0 2px 4px rgba(52, 152, 219, 0.1);
366
+ }
367
+
368
+ .warning-box {
369
+ background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
370
+ border-left: 4px solid #f39c12;
371
+ padding: 18px;
372
+ margin: 25px 0;
373
+ border-radius: 6px;
374
+ box-shadow: 0 2px 4px rgba(243, 156, 18, 0.1);
375
+ }
376
+
377
+ .error-box {
378
+ background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
379
+ border-left: 4px solid #e74c3c;
380
+ padding: 18px;
381
+ margin: 25px 0;
382
+ border-radius: 6px;
383
+ box-shadow: 0 2px 4px rgba(231, 76, 60, 0.1);
384
+ }
385
+
386
+ .success-box {
387
+ background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
388
+ border-left: 4px solid #27ae60;
389
+ padding: 18px;
390
+ margin: 25px 0;
391
+ border-radius: 6px;
392
+ box-shadow: 0 2px 4px rgba(39, 174, 96, 0.1);
393
+ }
394
+
395
+ /* ===========================
396
+ DATASET SECTIONS
397
+ =========================== */
398
+ .dataset-section {
399
+ margin: 35px 0;
400
+ padding: 25px;
401
+ background-color: #fafafa;
402
+ border-radius: 8px;
403
+ border: 1px solid #e0e0e0;
404
+ transition: all 0.3s ease;
405
+ }
406
+
407
+ .dataset-section:hover {
408
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
409
+ transform: translateY(-2px);
410
+ }
411
+
412
+ /* ===========================
413
+ LISTS
414
+ =========================== */
415
+ ul {
416
+ margin: 15px 0;
417
+ padding-left: 25px;
418
+ }
419
+
420
+ ul li {
421
+ margin: 8px 0;
422
+ line-height: 1.6;
423
+ }
424
+
425
+ .info-box ul,
426
+ .warning-box ul,
427
+ .error-box ul,
428
+ .success-box ul {
429
+ margin: 12px 0 0 0;
430
+ padding-left: 25px;
431
+ }
432
+
433
+ /* ===========================
434
+ KEY INFO STYLING
435
+ =========================== */
436
+ .key-info {
437
+ font-family: 'Courier New', monospace;
438
+ background-color: #f4f4f4;
439
+ padding: 4px 8px;
440
+ border-radius: 4px;
441
+ font-size: 0.95em;
442
+ color: #2c3e50;
443
+ border: 1px solid #dee2e6;
444
+ }
445
+
446
+ /* ===========================
447
+ STATUS TEXT COLORS
448
+ =========================== */
449
+ .success-text {
450
+ color: #27ae60;
451
+ font-weight: 600;
452
+ }
453
+
454
+ .warning-text {
455
+ color: #f39c12;
456
+ font-weight: 600;
457
+ }
458
+
459
+ .info-text {
460
+ color: #3498db;
461
+ font-weight: 600;
462
+ }
463
+
464
+ /* ===========================
465
+ LINKS
466
+ =========================== */
467
+ a {
468
+ color: #3498db;
469
+ text-decoration: none;
470
+ transition: all 0.2s ease;
471
+ }
472
+
473
+ a:hover {
474
+ color: #2980b9;
475
+ text-decoration: underline;
476
+ }
477
+
478
+ a:focus {
479
+ outline: 2px solid #3498db;
480
+ outline-offset: 2px;
481
+ }
482
+
483
+ /* ===========================
484
+ DETAILS/SUMMARY
485
+ =========================== */
486
+ details {
487
+ margin: 20px 0;
488
+ padding: 15px;
489
+ background-color: #f8f9fa;
490
+ border-radius: 6px;
491
+ border: 1px solid #dee2e6;
492
+ }
493
+
494
+ summary {
495
+ cursor: pointer;
496
+ font-weight: 600;
497
+ padding: 8px;
498
+ color: #495057;
499
+ transition: color 0.2s ease;
500
+ }
501
+
502
+ summary:hover {
503
+ color: #3498db;
504
+ }
505
+
506
+ summary:focus {
507
+ outline: 2px solid #3498db;
508
+ outline-offset: 2px;
509
+ }
510
+
511
+ details[open] summary {
512
+ margin-bottom: 12px;
513
+ padding-bottom: 12px;
514
+ border-bottom: 1px solid #dee2e6;
515
+ }
516
+
517
+ details ul {
518
+ margin-top: 12px;
519
+ }
520
+
521
+ /* ===========================
522
+ TEXT ELEMENTS
523
+ =========================== */
524
+ strong {
525
+ color: #2c3e50;
526
+ font-weight: 600;
527
+ }
528
+
529
+ em {
530
+ color: #6c757d;
531
+ font-style: italic;
532
+ }
533
+
534
+ code {
535
+ background-color: #f8f9fa;
536
+ padding: 3px 6px;
537
+ border-radius: 4px;
538
+ font-family: 'Courier New', monospace;
539
+ color: #e83e8c;
540
+ font-size: 0.9em;
541
+ border: 1px solid #e9ecef;
542
+ }
543
+
544
+ p {
545
+ margin: 12px 0;
546
+ line-height: 1.7;
547
+ }
548
+
549
+ /* ===========================
550
+ PRINT STYLES
551
+ =========================== */
552
+ @media print {
553
+ body {
554
+ padding-left: 20px;
555
+ background: white;
556
+ }
557
+
558
+ .hamburger,
559
+ .toc,
560
+ .overlay {
561
+ display: none !important;
562
+ }
563
+
564
+ .report-header,
565
+ .container,
566
+ .report-footer {
567
+ box-shadow: none;
568
+ padding: 20px;
569
+ }
570
+
571
+ .dataset-section {
572
+ page-break-inside: avoid;
573
+ }
574
+
575
+ .company-link {
576
+ pointer-events: none;
577
+ }
578
+
579
+ a {
580
+ color: #000;
581
+ text-decoration: underline;
582
+ }
583
+
584
+ .footer-content a {
585
+ color: #000;
586
+ }
587
+ }
588
+
589
+ /* ===========================
590
+ RESPONSIVE DESIGN
591
+ =========================== */
592
+ @media (max-width: 768px) {
593
+ body {
594
+ padding: 15px;
595
+ padding-left: 70px;
596
+ }
597
+
598
+ .report-header {
599
+ flex-direction: column;
600
+ gap: 15px;
601
+ text-align: center;
602
+ padding: 20px;
603
+ }
604
+
605
+ .report-header h1 {
606
+ font-size: 1.6em;
607
+ }
608
+
609
+ .company-logo {
610
+ height: 40px;
611
+ }
612
+
613
+ .company-name {
614
+ font-size: 1em;
615
+ }
616
+
617
+ .container {
618
+ padding: 25px;
619
+ }
620
+
621
+ .hamburger {
622
+ top: 15px;
623
+ left: 15px;
624
+ padding: 10px;
625
+ }
626
+
627
+ .toc {
628
+ width: 280px;
629
+ left: -300px;
630
+ }
631
+
632
+ h2 {
633
+ font-size: 1.4em;
634
+ }
635
+
636
+ h3 {
637
+ font-size: 1.2em;
638
+ }
639
+
640
+ .footer-content {
641
+ flex-direction: column;
642
+ gap: 8px;
643
+ }
644
+
645
+ .footer-content .separator {
646
+ display: none;
647
+ }
648
+ }
649
+
650
+ @media (max-width: 480px) {
651
+ body {
652
+ padding: 10px;
653
+ padding-left: 60px;
654
+ }
655
+
656
+ .container,
657
+ .report-header,
658
+ .report-footer {
659
+ padding: 20px;
660
+ }
661
+
662
+ .toc {
663
+ width: 100%;
664
+ left: -100%;
665
+ }
666
+
667
+ .report-header h1 {
668
+ font-size: 1.4em;
669
+ }
670
+
671
+ h2 {
672
+ font-size: 1.2em;
673
+ }
674
+
675
+ .company-logo {
676
+ height: 35px;
677
+ }
678
+ }
679
+
680
+ /* ===========================
681
+ ACCESSIBILITY
682
+ =========================== */
683
+ @media (prefers-reduced-motion: reduce) {
684
+ * {
685
+ animation: none !important;
686
+ transition: none !important;
687
+ }
688
+ }
689
+
690
+ @media (prefers-contrast: high) {
691
+ .container,
692
+ .report-header,
693
+ .report-footer {
694
+ border: 2px solid #000;
695
+ }
696
+
697
+ .info-box,
698
+ .warning-box,
699
+ .error-box,
700
+ .success-box {
701
+ border: 2px solid #000;
702
+ }
703
+ }
704
+
705
+ :focus-visible {
706
+ outline: 2px solid #3498db;
707
+ outline-offset: 2px;
708
+ }
709
+ </style>
710
+ <title><%= title %> - High Level Report</title>
711
+ </head>
712
+ <body>
713
+ <!-- Hamburger button -->
714
+ <button class="hamburger" id="hamburger" aria-label="Toggle table of contents" aria-expanded="false" aria-controls="toc">
715
+ <span></span>
716
+ <span></span>
717
+ <span></span>
718
+ </button>
719
+
720
+ <!-- Table of contents (sidebar) -->
721
+ <nav class="toc" id="toc" aria-label="Table of contents">
722
+ <div class="toc-header">
723
+ <h2>Table of Contents</h2>
724
+ <button class="close-btn" id="close-toc" aria-label="Close table of contents">&times;</button>
725
+ </div>
726
+ <ul class="toc-list">
727
+ <li><a href="#summary">Summary</a></li>
728
+ <li><a href="#configuration">Configuration</a></li>
729
+ <li><a href="#datasets-analysis">Datasets Analysis</a>
730
+ <ul class="toc-sublist">
731
+ <% dataset_reports.each do |ds_name, report| %>
732
+ <li><a href="#ds-<%= ds_name %>"><%= ds_name %></a></li>
733
+ <% end %>
734
+ </ul>
735
+ </li>
736
+ <% if externe_file_type != 'csv' %>
737
+ <li><a href="#output-file">Output File</a></li>
738
+ <% end %>
739
+ </ul>
740
+ </nav>
741
+
742
+ <!-- Overlay to close TOC by clicking outside -->
743
+ <div class="overlay" id="overlay" aria-hidden="true"></div>
744
+
745
+ <!-- Report Header with Branding -->
746
+ <header class="report-header">
747
+ <h1><%= title %> - High Level Report</h1>
748
+ <div class="company-branding">
749
+ <a href="https://adclin.com" target="_blank" rel="noopener noreferrer" class="company-link">
750
+ <% if @logo_base64 %>
751
+ <img src="<%= @logo_base64 %>" alt="AdClin Logo" class="company-logo">
752
+ <% else %>
753
+ <img src="/Contact-LOGO.png" alt="AdClin Logo" class="company-logo">
754
+ <% end %>
755
+ </a>
756
+ </div>
757
+ </header>
758
+
759
+ <main class="container">
760
+ <div class="info-box" id="summary">
761
+ <div style="display: flex; justify-content: space-between; align-items: center;">
762
+ <p style="margin: 0;">Report automatically generated on <%= Time.now.strftime("%Y-%m-%d at %H:%M") %></p>
763
+ <% if @web_mode %>
764
+ <button class="btn-word" id="convert-btn" onclick="convertToWord()"> 📄 Word </button>
765
+ <% end %>
766
+ </div>
767
+ </div>
768
+
769
+ <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>
805
+
806
+ <% dataset_reports.each do |ds_name, report| %>
807
+ <article class="dataset-section" id="ds-<%= ds_name %>">
808
+ <h3>Dataset: <%= ds_name %></h3>
809
+
810
+ <% if report[:record_count] %>
811
+ <p><strong>Number of records:</strong> <%= report[:record_count] %></p>
812
+ <% end %>
813
+
814
+ <% if report[:candidates_type] %>
815
+ <p><strong>Dataset type:</strong> <%= report[:candidates_type] %></p>
816
+ <% end %>
817
+
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 %>
826
+ <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>
833
+ </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
+ <% else %>
847
+ <div class="error-box" role="alert">
848
+ <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 %>
864
+ </div>
865
+ <% end %>
866
+ <% end %>
867
+
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>
881
+ <% 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>
900
+ <% end %>
901
+ <% end %>
902
+ <% end %>
903
+
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
+ </article>
916
+ <% end %>
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
+ </main>
927
+
928
+ <!-- Report Footer with Copyright and License -->
929
+ <footer class="report-footer">
930
+ <div class="footer-content">
931
+ <span class="copyright">© 1999-2026 AdClin. All rights reserved.</span>
932
+ <span class="separator">•</span>
933
+ <span class="license">Licensed under <a href="/LICENSE" target="_blank" rel="noopener noreferrer">AGPL v3</a></span>
934
+ </div>
935
+ </footer>
936
+
937
+ <script>
938
+ // Download HTML file as Word document
939
+ 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);
948
+ }
949
+ }
950
+ // Enhanced TOC functionality with accessibility
951
+
952
+ // DOM Elements
953
+ const hamburger = document.getElementById('hamburger');
954
+ const toc = document.getElementById('toc');
955
+ const overlay = document.getElementById('overlay');
956
+ const closeBtn = document.getElementById('close-toc');
957
+ const tocLinks = document.querySelectorAll('.toc-list a');
958
+
959
+ // Open TOC
960
+ function openTOC() {
961
+ if (toc && overlay) {
962
+ toc.classList.add('active');
963
+ overlay.classList.add('active');
964
+ document.body.style.overflow = 'hidden';
965
+
966
+ if (hamburger) hamburger.setAttribute('aria-expanded', 'true');
967
+ if (overlay) overlay.setAttribute('aria-hidden', 'false');
968
+
969
+ if (closeBtn) {
970
+ setTimeout(() => closeBtn.focus(), 100);
971
+ }
972
+ }
973
+ }
974
+
975
+ // Close TOC
976
+ function closeTOC() {
977
+ if (toc && overlay) {
978
+ toc.classList.remove('active');
979
+ overlay.classList.remove('active');
980
+ document.body.style.overflow = '';
981
+
982
+ if (hamburger) hamburger.setAttribute('aria-expanded', 'false');
983
+ if (overlay) overlay.setAttribute('aria-hidden', 'true');
984
+
985
+ if (hamburger) {
986
+ setTimeout(() => hamburger.focus(), 100);
987
+ }
988
+ }
989
+ }
990
+
991
+ // Event Listeners
992
+ if (hamburger) {
993
+ hamburger.addEventListener('click', openTOC);
994
+ hamburger.addEventListener('keydown', (e) => {
995
+ if (e.key === 'Enter' || e.key === ' ') {
996
+ e.preventDefault();
997
+ openTOC();
998
+ }
999
+ });
1000
+ }
1001
+
1002
+ if (closeBtn) {
1003
+ closeBtn.addEventListener('click', closeTOC);
1004
+ closeBtn.addEventListener('keydown', (e) => {
1005
+ if (e.key === 'Enter' || e.key === ' ') {
1006
+ e.preventDefault();
1007
+ closeTOC();
1008
+ }
1009
+ });
1010
+ }
1011
+
1012
+ if (overlay) {
1013
+ overlay.addEventListener('click', closeTOC);
1014
+ }
1015
+
1016
+ tocLinks.forEach(link => {
1017
+ link.addEventListener('click', () => {
1018
+ closeTOC();
1019
+ });
1020
+ });
1021
+
1022
+ document.addEventListener('keydown', (e) => {
1023
+ if (e.key === 'Escape' && toc && toc.classList.contains('active')) {
1024
+ closeTOC();
1025
+ }
1026
+ });
1027
+
1028
+ if (toc) {
1029
+ toc.addEventListener('keydown', (e) => {
1030
+ if (e.key === 'Tab') {
1031
+ const focusableElements = toc.querySelectorAll(
1032
+ 'button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])'
1033
+ );
1034
+
1035
+ if (focusableElements.length === 0) return;
1036
+
1037
+ const firstElement = focusableElements[0];
1038
+ const lastElement = focusableElements[focusableElements.length - 1];
1039
+
1040
+ if (e.shiftKey && document.activeElement === firstElement) {
1041
+ e.preventDefault();
1042
+ lastElement.focus();
1043
+ } else if (!e.shiftKey && document.activeElement === lastElement) {
1044
+ e.preventDefault();
1045
+ firstElement.focus();
1046
+ }
1047
+ }
1048
+ });
1049
+ }
1050
+
1051
+ // Colour coding of datasets in the TOC according to their maximum error level
1052
+ function coloriserTOCDatasets() {
1053
+ document.querySelectorAll('.toc-sublist a').forEach(link => {
1054
+ const href = link.getAttribute('href');
1055
+ if (!href) return;
1056
+
1057
+ const section = document.querySelector(href);
1058
+ if (!section) return;
1059
+
1060
+ const aErreur = section.querySelector('.error-box');
1061
+ const aWarning = section.querySelector('.warning-box');
1062
+
1063
+ if (aErreur) {
1064
+ // Rouge - priorité maximale
1065
+ link.style.backgroundColor = '#f8d7da';
1066
+ link.style.borderLeftColor = '#e74c3c';
1067
+ link.style.color = '#c0392b';
1068
+ } else if (aWarning) {
1069
+ // Orange
1070
+ link.style.backgroundColor = '#fff3cd';
1071
+ link.style.borderLeftColor = '#f39c12';
1072
+ link.style.color = '#856404';
1073
+ }
1074
+ // Vert (success-box uniquement) → aucune couleur appliquée
1075
+ });
1076
+ }
1077
+
1078
+ // Attendre que le DOM soit prêt
1079
+ if (document.readyState === 'loading') {
1080
+ document.addEventListener('DOMContentLoaded', coloriserTOCDatasets);
1081
+ } else {
1082
+ coloriserTOCDatasets();
1083
+ }
1084
+
1085
+ // Smooth scroll for anchor links
1086
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
1087
+ anchor.addEventListener('click', function (e) {
1088
+ e.preventDefault();
1089
+ const target = document.querySelector(this.getAttribute('href'));
1090
+
1091
+ if (target) {
1092
+ const offset = 20;
1093
+ const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset;
1094
+
1095
+ window.scrollTo({
1096
+ top: targetPosition,
1097
+ behavior: 'smooth'
1098
+ });
1099
+
1100
+ target.setAttribute('tabindex', '-1');
1101
+ target.focus();
1102
+ target.addEventListener('blur', () => {
1103
+ target.removeAttribute('tabindex');
1104
+ }, { once: true });
1105
+ }
1106
+ });
1107
+ });
1108
+
1109
+ // Highlight current section in TOC
1110
+ let observerOptions = {
1111
+ root: null,
1112
+ rootMargin: '-20% 0px -35% 0px',
1113
+ threshold: 0
1114
+ };
1115
+
1116
+ let observer = new IntersectionObserver((entries) => {
1117
+ entries.forEach(entry => {
1118
+ if (entry.isIntersecting) {
1119
+ const id = entry.target.getAttribute('id');
1120
+
1121
+ tocLinks.forEach(link => {
1122
+ link.classList.remove('active');
1123
+ });
1124
+
1125
+ const activeLink = document.querySelector(`.toc-list a[href="#${id}"]`);
1126
+ if (activeLink) {
1127
+ activeLink.classList.add('active');
1128
+ }
1129
+ }
1130
+ });
1131
+ }, observerOptions);
1132
+
1133
+ document.querySelectorAll('[id]').forEach(section => {
1134
+ if (section.tagName.match(/^H[1-3]$/) || section.classList.contains('dataset-section')) {
1135
+ observer.observe(section);
1136
+ }
1137
+ });
1138
+
1139
+ document.addEventListener('DOMContentLoaded', () => {
1140
+ console.log('Report loaded successfully');
1141
+ });
1142
+ </script>
1143
+ </body>
1144
+ </html>