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.
data/public/app.js ADDED
@@ -0,0 +1,569 @@
1
+ /*
2
+ Copyright (c) 2026 AdClin
3
+ Licensed under the GNU Affero General Public License v3.0 or later.
4
+ See the LICENSE file for details.
5
+ */
6
+
7
+ // Configuration Management
8
+
9
+ // Save configuration
10
+ async function sauvegarderConfig() {
11
+ // Validate required fields
12
+ const requiredFields = [
13
+ 'study_name', 'output_directory', 'data_directory',
14
+ 'define_path', 'event_key', 'intervention_key', 'finding_key',
15
+ 'finding_about_key', 'ds_key', 'relrec_key', 'CO_key',
16
+ 'TA_key', 'TE_key', 'TI_key', 'TS_key', 'TV_key'
17
+ ];
18
+
19
+ const missingFields = [];
20
+ for (const field of requiredFields) {
21
+ const element = document.getElementById(field);
22
+ if (!element.value.trim()) {
23
+ missingFields.push(field.replace(/_/g, ' ').toUpperCase());
24
+ element.classList.add('error-field');
25
+ } else {
26
+ element.classList.remove('error-field');
27
+ }
28
+ }
29
+
30
+ if (missingFields.length > 0) {
31
+ showMessage('message-config', 'error',
32
+ `⚠️ Missing required fields: ${missingFields.join(', ')}`);
33
+ return false;
34
+ }
35
+
36
+ const config = {
37
+ study_name: document.getElementById('study_name').value.trim(),
38
+ output_type: 'csv',
39
+ output_directory: document.getElementById('output_directory').value.trim(),
40
+ data_directory: document.getElementById('data_directory').value.trim(),
41
+ define_path: document.getElementById('define_path').value.trim(),
42
+ excluded_ds: document.getElementById('excluded_ds').value.trim(),
43
+ event_key: document.getElementById('event_key').value.trim(),
44
+ intervention_key: document.getElementById('intervention_key').value.trim(),
45
+ finding_key: document.getElementById('finding_key').value.trim(),
46
+ finding_about_key: document.getElementById('finding_about_key').value.trim(),
47
+ ds_key: document.getElementById('ds_key').value.trim(),
48
+ relrec_key: document.getElementById('relrec_key').value.trim(),
49
+ CO_key: document.getElementById('CO_key').value.trim(),
50
+ TA_key: document.getElementById('TA_key').value.trim(),
51
+ TE_key: document.getElementById('TE_key').value.trim(),
52
+ TI_key: document.getElementById('TI_key').value.trim(),
53
+ TS_key: document.getElementById('TS_key').value.trim(),
54
+ TV_key: document.getElementById('TV_key').value.trim()
55
+ };
56
+
57
+ showMessage('message-config', 'processing', '⏳ Saving configuration...');
58
+
59
+ try {
60
+ const response = await fetch('/config', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify(config)
64
+ });
65
+
66
+ const data = await response.json();
67
+
68
+ if (data.success) {
69
+ showMessage('message-config', 'success', '✅ ' + data.message);
70
+ return true;
71
+ } else {
72
+ showMessage('message-config', 'error', '❌ ' + data.erreur);
73
+ return false;
74
+ }
75
+ } catch (error) {
76
+ showMessage('message-config', 'error', '❌ Connection error: ' + error.message);
77
+ return false;
78
+ }
79
+ }
80
+
81
+ // Display full configuration
82
+ async function afficherConfigComplete() {
83
+ try {
84
+ const response = await fetch('/config');
85
+ const config = await response.json();
86
+
87
+ const configWindow = window.open('', 'Configuration', 'width=700,height=500');
88
+ configWindow.document.write(`
89
+ <!DOCTYPE html>
90
+ <html>
91
+ <head>
92
+ <title>Current Configuration</title>
93
+ <style>
94
+ body {
95
+ font-family: 'Courier New', monospace;
96
+ padding: 20px;
97
+ background: #f5f5f5;
98
+ }
99
+ pre {
100
+ background: white;
101
+ padding: 20px;
102
+ border-radius: 8px;
103
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
104
+ overflow: auto;
105
+ }
106
+ h1 { color: #333; }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <h1>Current Configuration</h1>
111
+ <pre>${JSON.stringify(config, null, 2)}</pre>
112
+ </body>
113
+ </html>
114
+ `);
115
+ } catch (error) {
116
+ showMessage('message-config', 'error', '❌ Error loading configuration: ' + error.message);
117
+ }
118
+ }
119
+
120
+ // Clear all fields
121
+ async function effacerChamps() {
122
+ if (!confirm('⚠️ Are you sure you want to clear the current configuration (config.yaml)?')) {
123
+ return;
124
+ }
125
+
126
+ showMessage('message-config', 'processing', '⏳ Clearing configuration...');
127
+
128
+ try {
129
+ const response = await fetch('/config/clear', {
130
+ method: 'POST'
131
+ });
132
+
133
+ const data = await response.json();
134
+
135
+ if (data.success) {
136
+ showMessage('message-config', 'success', '✅ ' + data.message);
137
+ setTimeout(() => location.reload(true), 1000);
138
+ } else {
139
+ showMessage('message-config', 'error', '❌ ' + data.erreur);
140
+ }
141
+ } catch (error) {
142
+ showMessage('message-config', 'error', '❌ Error: ' + error.message);
143
+ }
144
+ }
145
+
146
+ // Load default configuration
147
+ async function chargerDefaut() {
148
+ if (!confirm('⚠️ Load default configuration? This will replace config.yaml with config.default.yaml')) {
149
+ return;
150
+ }
151
+
152
+ showMessage('message-config', 'processing', '⏳ Loading config.default.yaml...');
153
+
154
+ try {
155
+ const response = await fetch('/config/reset', {
156
+ method: 'POST'
157
+ });
158
+
159
+ const data = await response.json();
160
+
161
+ if (data.success) {
162
+ showMessage('message-config', 'success', '✅ ' + data.message);
163
+ setTimeout(() => location.reload(true), 1000);
164
+ } else {
165
+ showMessage('message-config', 'error', '❌ ' + data.erreur);
166
+ }
167
+ } catch (error) {
168
+ showMessage('message-config', 'error', '❌ Error: ' + error.message);
169
+ }
170
+ }
171
+
172
+ // Processing
173
+
174
+ // Start processing
175
+ async function lancerTraitement() {
176
+ const statusDiv = document.getElementById('status-traitement');
177
+ const btn = document.getElementById('btn-traiter');
178
+
179
+ btn.disabled = true;
180
+ statusDiv.innerHTML = '<div class="status processing">⏳ Processing in progress...</div>';
181
+
182
+ // Sauvegarder la configuration avant de lancer le traitement
183
+ const configSaved = await sauvegarderConfig();
184
+
185
+ if (!configSaved) {
186
+ statusDiv.innerHTML = `
187
+ <div class="status error">
188
+ <h3>❌ Configuration Error</h3>
189
+ <p>The configuration could not be saved. Please correct the errors before starting the analysis.</p>
190
+ <p style="margin-top: 10px; color: #666; font-size: 0.9em;">
191
+ Check the error message in the Configuration section above.
192
+ </p>
193
+ </div>
194
+ `;
195
+ btn.disabled = false;
196
+ return;
197
+ }
198
+
199
+ try {
200
+ const response = await fetch('/traiter', {
201
+ method: 'POST'
202
+ });
203
+
204
+ const data = await response.json();
205
+
206
+ if (data.success) {
207
+ let messageSucces = '<h3>✅ Analysis completed successfully!</h3>';
208
+
209
+ // Display completed steps
210
+ if (data.resultat.etapes) {
211
+ messageSucces += '<div style="margin: 20px 0; text-align: left;">';
212
+ messageSucces += '<h4>📋 Completed Steps:</h4>';
213
+ messageSucces += '<ol>';
214
+
215
+ data.resultat.etapes.forEach(etape => {
216
+ let icone = '✅';
217
+ let couleur = '#28a745';
218
+
219
+ if (etape.statut === 'en_cours') {
220
+ icone = '⏳';
221
+ couleur = '#ffc107';
222
+ } else if (etape.statut === 'erreur') {
223
+ icone = '❌';
224
+ couleur = '#dc3545';
225
+ }
226
+
227
+ messageSucces += `
228
+ <li style="color: ${couleur};">
229
+ ${icone} <strong>${etape.nom}:</strong>
230
+ ${etape.message ? `<span style="color: #666; font-size: 0.9em;">${etape.message}</span>` : ''}
231
+ </li>
232
+ `;
233
+ });
234
+
235
+ messageSucces += '</ol></div>';
236
+ }
237
+
238
+ // Display generated files
239
+ if (data.resultat.rapport_path || data.resultat.rapport_name || data.resultat.output_directory) {
240
+ messageSucces += '<div style="margin: 20px 0; text-align: left;">';
241
+ messageSucces += '<h4>📄 Output Files:</h4>';
242
+ messageSucces += '<ul style="margin: 0; padding-left: 20px;">';
243
+
244
+ if (data.resultat.rapport_path && data.resultat.rapport_name) {
245
+ messageSucces += `
246
+ <li>
247
+ <strong>Report:</strong>
248
+ <a href="${data.resultat.rapport_path}" target="_blank" rel="noopener noreferrer">
249
+ ${data.resultat.rapport_name}
250
+ </a>
251
+ </li>
252
+ `;
253
+ }
254
+ if (data.resultat.output_directory) {
255
+ messageSucces += `<li><strong>Output Directory:</strong> ${data.resultat.output_directory}</li>`;
256
+ }
257
+
258
+ messageSucces += '</ul></div>';
259
+ }
260
+
261
+ // Display statistics
262
+ if (data.resultat.nombre_datasets_analyses || data.resultat.nombre_datasets_invalid) {
263
+ messageSucces += '<div style="margin: 20px 0; text-align: left;">';
264
+ messageSucces += '<h4>📊 Dataset Statistics:</h4>';
265
+ messageSucces += '<ul style="margin: 0; padding-left: 20px;">';
266
+
267
+ if (data.resultat.nombre_datasets_analyses) {
268
+ messageSucces += `<li><strong>Analyzed:</strong> ${data.resultat.nombre_datasets_analyses}</li>`;
269
+ }
270
+
271
+ const invalid_ds = data.resultat.nombre_datasets_invalid;
272
+ if (invalid_ds) {
273
+ if (invalid_ds.invalid_dataset) {
274
+ messageSucces += `<li><strong>With at least one issue:</strong> ${invalid_ds.invalid_dataset}</li>`;
275
+ }
276
+ if (invalid_ds.invalid_ascii) {
277
+ messageSucces += `<li><strong>ASCII issue:</strong> ${invalid_ds.invalid_ascii}</li>`;
278
+ }
279
+ if (invalid_ds.invalid_define) {
280
+ messageSucces += `<li><strong>Invalid keys in define.xml:</strong> ${invalid_ds.invalid_define}</li>`;
281
+ }
282
+ if (invalid_ds.invalid_data) {
283
+ messageSucces += `<li><strong>Minimum keys not found:</strong> ${invalid_ds.invalid_data}</li>`;
284
+ }
285
+ }
286
+
287
+ messageSucces += '</ul></div>';
288
+ }
289
+
290
+ statusDiv.innerHTML = `<div class="status">${messageSucces}</div>`;
291
+
292
+ // Auto-refresh results if already loaded
293
+ const listeDiv = document.getElementById('liste-resultats');
294
+ if (listeDiv.style.display !== 'none') {
295
+ rafraichirResultats();
296
+ }
297
+
298
+ } else {
299
+ // Display validation errors
300
+ let messageErreur = `<h3>❌ ${data.erreur}</h3>`;
301
+
302
+ if (data.details && data.details.length > 0) {
303
+ messageErreur += '<div style="margin: 20px 0; text-align: left;">';
304
+ messageErreur += '<h4>Validation Errors:</h4>';
305
+ messageErreur += '<ul style="margin-left: 20px; color: #721c24;">';
306
+ data.details.forEach(erreur => {
307
+ messageErreur += `<li style="margin: 5px 0;">${erreur}</li>`;
308
+ });
309
+ messageErreur += '</ul></div>';
310
+ messageErreur += '<p style="margin-top: 15px;">⚠️ Please correct these errors before restarting the analysis</p>';
311
+ } else {
312
+ messageErreur += `<p style="margin-top: 15px;">${data.erreur}</p>`;
313
+ }
314
+
315
+ statusDiv.innerHTML = `<div class="status error">${messageErreur}</div>`;
316
+ }
317
+ } catch (error) {
318
+ statusDiv.innerHTML = `
319
+ <div class="status error">
320
+ <h3>❌ Connection Error</h3>
321
+ <p>${error.message}</p>
322
+ <p style="margin-top: 10px; color: #666; font-size: 0.9em;">
323
+ Please check that the server is running correctly (ruby app.rb)
324
+ </p>
325
+ </div>
326
+ `;
327
+ } finally {
328
+ btn.disabled = false;
329
+ }
330
+ }
331
+
332
+ // Results Management
333
+
334
+ // Load results for the first time
335
+ async function chargerResultats() {
336
+ const btnLoad = document.getElementById('btn-load-results');
337
+ const btnRefresh = document.getElementById('btn-refresh-results');
338
+ const listeDiv = document.getElementById('liste-resultats');
339
+
340
+ // Hide Load button and show Refresh button
341
+ btnLoad.style.display = 'none';
342
+ btnRefresh.style.display = 'inline-block';
343
+ listeDiv.style.display = 'block';
344
+
345
+ await rafraichirResultats();
346
+ }
347
+
348
+ // Refresh results list
349
+ let isRefreshing = false;
350
+ async function rafraichirResultats() {
351
+ if (isRefreshing) return;
352
+ isRefreshing = true;
353
+
354
+ const listeDiv = document.getElementById('liste-resultats');
355
+ listeDiv.innerHTML = '<p style="color: #666;">⏳ Loading files...</p>';
356
+
357
+ try {
358
+ const response = await fetch('/resultats');
359
+ const data = await response.json();
360
+
361
+ if (!data.success) {
362
+ listeDiv.innerHTML = '<p style="color: #dc3545;">❌ Error loading results</p>';
363
+ return;
364
+ }
365
+
366
+ const arbo = data.arborescence;
367
+
368
+ // Check if there are files or folders
369
+ const hasFiles = arbo.fichiers && arbo.fichiers.length > 0;
370
+ const hasFolders = arbo.dossiers && Object.keys(arbo.dossiers).length > 0;
371
+
372
+ if (hasFiles || hasFolders) {
373
+ let html = '<div class="file-tree">';
374
+ html += construireArbre(arbo, '', true);
375
+ html += '</div>';
376
+
377
+ listeDiv.innerHTML = html;
378
+ } else {
379
+ listeDiv.innerHTML = '<p style="color: #666;">No result files yet. Run an analysis to generate files.</p>';
380
+ }
381
+ } catch (error) {
382
+ console.error('Error:', error);
383
+ listeDiv.innerHTML = '<p style="color: #dc3545;">❌ Error loading files: ' + error.message + '</p>';
384
+ } finally {
385
+ isRefreshing = false;
386
+ }
387
+ }
388
+
389
+ // Build HTML tree recursively
390
+ function construireArbre(noeud, chemin_parent, isRoot = false) {
391
+ let html = '';
392
+
393
+ // Display folders
394
+ if (noeud.dossiers) {
395
+ const dossiers = Object.keys(noeud.dossiers).sort();
396
+ dossiers.forEach(nomDossier => {
397
+ const cheminDossier = chemin_parent ? `${chemin_parent}/${nomDossier}` : nomDossier;
398
+ const sousDossier = noeud.dossiers[nomDossier];
399
+ const id = 'folder-' + cheminDossier.replace(/[\/\s]/g, '-');
400
+
401
+ const nbFichiers = sousDossier.fichiers ? sousDossier.fichiers.length : 0;
402
+ const nbDossiers = sousDossier.dossiers ? Object.keys(sousDossier.dossiers).length : 0;
403
+
404
+ // Check if folder contains CSV files
405
+ const hasCsvFiles = sousDossier.fichiers && sousDossier.fichiers.some(f => f.extension === '.csv');
406
+
407
+ // Check if this is a first-level folder (isRoot is true and no parent path)
408
+ const isFirstLevel = isRoot && !chemin_parent;
409
+
410
+ html += `
411
+ <div class="folder-item">
412
+ <div class="folder-header" onclick="toggleFolder('${id}')" role="button" tabindex="0" aria-expanded="false" aria-controls="${id}">
413
+ <span class="folder-icon" id="${id}-icon" aria-hidden="true">📁</span>
414
+ <strong>${escapeHtml(nomDossier)}</strong>
415
+ <span style="color: #666; font-size: 0.9em; margin-left: 10px;">
416
+ (${nbFichiers} file${nbFichiers !== 1 ? 's' : ''}, ${nbDossiers} folder${nbDossiers !== 1 ? 's' : ''})
417
+ </span>
418
+ <div class="folder-actions">
419
+ ${isFirstLevel ? `
420
+ <button onclick="event.stopPropagation(); telechargerZipDossier('${escapeHtml(cheminDossier)}')"
421
+ class="btn-primary"
422
+ aria-label="Download folder as ZIP">
423
+ 📦 Download as ZIP
424
+ </button>
425
+ ` : ''}
426
+ ${hasCsvFiles ? `
427
+ <button onclick="event.stopPropagation(); telechargerDossierExcel('${escapeHtml(cheminDossier)}')"
428
+ class="btn-primary"
429
+ aria-label="Download all CSV files as Excel">
430
+ 📊 Download as Excel
431
+ </button>
432
+ ` : ''}
433
+ </div>
434
+ </div>
435
+ <div class="folder-content" id="${id}" style="display: none; margin-left: 20px;" role="region">
436
+ ${construireArbre(sousDossier, cheminDossier, false)}
437
+ </div>
438
+ </div>
439
+ `;
440
+ });
441
+ }
442
+
443
+ // Display files
444
+ if (noeud.fichiers && noeud.fichiers.length > 0) {
445
+ noeud.fichiers.forEach(fichier => {
446
+ const icone = getIconePourExtension(fichier.extension);
447
+ const tailleMo = (fichier.taille / 1024).toFixed(2);
448
+
449
+ html += `
450
+ <div class="file-item">
451
+ <div class="file-info">
452
+ <div class="file-name">${icone} ${escapeHtml(fichier.nom)}</div>
453
+ <div class="file-meta">
454
+ ${tailleMo} KB • ${fichier.date}
455
+ </div>
456
+ </div>
457
+ <div>
458
+ ${fichier.extension === '.html' ?
459
+ `<button onclick="ouvrirFichier('${escapeHtml(fichier.chemin)}')" class="btn-secondary" style="margin-right: 5px;" aria-label="Open ${escapeHtml(fichier.nom)}">👁️ Open</button>
460
+ <button onclick="telechargerHtmlAsWord('${escapeHtml(fichier.chemin)}')" class="btn-secondary" style="margin-right: 5px;" aria-label="Download as Word">📄 Word</button>` :
461
+ ''}
462
+ <button onclick="telecharger('${escapeHtml(fichier.chemin)}')" class="btn-primary" aria-label="Download ${escapeHtml(fichier.nom)}">⬇️ Download</button>
463
+ </div>
464
+ </div>
465
+ `;
466
+ });
467
+ }
468
+
469
+ return html;
470
+ }
471
+
472
+ // Toggle folder display
473
+ function toggleFolder(id) {
474
+ const folder = document.getElementById(id);
475
+ const icon = document.getElementById(id + '-icon');
476
+ const header = folder.previousElementSibling;
477
+
478
+ if (folder && icon) {
479
+ const isExpanded = folder.style.display !== 'none';
480
+ folder.style.display = isExpanded ? 'none' : 'block';
481
+ icon.textContent = isExpanded ? '📁' : '📂';
482
+ if (header) {
483
+ header.setAttribute('aria-expanded', !isExpanded);
484
+ }
485
+ }
486
+ }
487
+
488
+ // Get icon based on extension
489
+ function getIconePourExtension(ext) {
490
+ const icons = {
491
+ '.html': '🌐',
492
+ '.csv': '📊',
493
+ '.xlsx': '📗',
494
+ '.xls': '📗',
495
+ '.pdf': '📕',
496
+ '.txt': '📄',
497
+ '.md': '📄',
498
+ '.yaml': '⚙️',
499
+ '.yml': '⚙️'
500
+ };
501
+ return icons[ext.toLowerCase()] || '📄';
502
+ }
503
+
504
+ // Download a file
505
+ function telecharger(chemin) {
506
+ window.location.href = '/telecharger/' + encodeURIComponent(chemin);
507
+ }
508
+
509
+ // Open HTML file in new tab
510
+ function ouvrirFichier(chemin) {
511
+ window.open('/telecharger/' + encodeURIComponent(chemin), '_blank', 'noopener,noreferrer');
512
+ }
513
+
514
+ // Utility Functions
515
+
516
+ // Show message with auto-dismiss
517
+ function showMessage(elementId, type, message, duration = 3000) {
518
+ const msgDiv = document.getElementById(elementId);
519
+ msgDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
520
+
521
+ if (duration > 0) {
522
+ setTimeout(() => msgDiv.innerHTML = '', duration);
523
+ }
524
+ }
525
+
526
+ // Escape HTML to prevent XSS
527
+ function escapeHtml(text) {
528
+ const div = document.createElement('div');
529
+ div.textContent = text;
530
+ return div.innerHTML;
531
+ }
532
+
533
+ // Keyboard accessibility for folder toggles
534
+ document.addEventListener('keydown', (e) => {
535
+ if (e.key === 'Enter' || e.key === ' ') {
536
+ if (e.target.classList.contains('folder-header')) {
537
+ e.preventDefault();
538
+ e.target.click();
539
+ }
540
+ }
541
+ });
542
+
543
+ // Download folder as ZIP
544
+ function telechargerZipDossier(cheminDossier) {
545
+ window.location.href = '/telecharger_zip_dossier/' + encodeURIComponent(cheminDossier);
546
+ }
547
+
548
+ // Download all CSV files in a folder as Excel
549
+ function telechargerDossierExcel(cheminDossier) {
550
+ window.location.href = '/telecharger_dossier_excel/' + encodeURIComponent(cheminDossier);
551
+ }
552
+
553
+ // Download HTML file as Word document
554
+ async function telechargerHtmlAsWord(chemin) {
555
+ const response = await fetch('/telecharger_html_word/' + encodeURIComponent(chemin));
556
+ const data = await response.json();
557
+ if (data.success) {
558
+ const cheminDocx = chemin.replace(/\.html$/, '.docx');
559
+ window.location.href = '/telecharger/' + encodeURIComponent(cheminDocx);
560
+ rafraichirResultats();
561
+ } else {
562
+ alert('❌ ' + data.erreur);
563
+ }
564
+ }
565
+
566
+ // Download HTML file as PDF
567
+ function telechargerHtmlAsPdf(chemin) {
568
+ window.location.href = '/telecharger_html_pdf/' + encodeURIComponent(chemin);
569
+ }