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
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
import { escapeHtml } from './utils.js';
|
|
8
|
+
|
|
9
|
+
export async function loadResults() {
|
|
10
|
+
const btnLoad = document.getElementById('btn-load-results');
|
|
11
|
+
const btnRefresh = document.getElementById('btn-refresh-results');
|
|
12
|
+
const resultsList = document.getElementById('results-list');
|
|
13
|
+
|
|
14
|
+
// Hide Load button and show Refresh button
|
|
15
|
+
btnLoad.style.display = 'none';
|
|
16
|
+
btnRefresh.style.display = 'inline-block';
|
|
17
|
+
resultsList.style.display = 'block';
|
|
18
|
+
|
|
19
|
+
await refreshResults();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Refresh results list
|
|
23
|
+
let isRefreshing = false;
|
|
24
|
+
|
|
25
|
+
export async function refreshResults() {
|
|
26
|
+
if (isRefreshing) return;
|
|
27
|
+
isRefreshing = true;
|
|
28
|
+
|
|
29
|
+
const resultsList = document.getElementById('results-list');
|
|
30
|
+
resultsList.innerHTML = '<p style="color: #666;">⏳ Loading files...</p>';
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch('/results');
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
|
|
36
|
+
if (!data.success) {
|
|
37
|
+
resultsList.innerHTML = '<p style="color: #dc3545;">❌ Error loading results</p>';
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tree = data.tree;
|
|
42
|
+
|
|
43
|
+
// Check if there are files or folders
|
|
44
|
+
const hasFiles = tree.files && tree.files.length > 0;
|
|
45
|
+
const hasFolders = tree.folders && Object.keys(tree.folders).length > 0;
|
|
46
|
+
|
|
47
|
+
if (hasFiles || hasFolders) {
|
|
48
|
+
let html = '<div class="file-tree">';
|
|
49
|
+
html += buildTree(tree, '', true);
|
|
50
|
+
html += '</div>';
|
|
51
|
+
|
|
52
|
+
resultsList.innerHTML = html;
|
|
53
|
+
} else {
|
|
54
|
+
resultsList.innerHTML = '<p style="color: #666;">No result files yet. Run an analysis to generate files.</p>';
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error:', error);
|
|
58
|
+
resultsList.innerHTML = '<p style="color: #dc3545;">❌ Error loading files: ' + error.message + '</p>';
|
|
59
|
+
} finally {
|
|
60
|
+
isRefreshing = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Build HTML tree recursively
|
|
65
|
+
|
|
66
|
+
export function buildTree(node, parentPath, isRoot = false) {
|
|
67
|
+
let html = '';
|
|
68
|
+
|
|
69
|
+
// Display folders
|
|
70
|
+
if (node.folders) {
|
|
71
|
+
const folders = Object.keys(node.folders).sort();
|
|
72
|
+
folders.forEach(folderName => {
|
|
73
|
+
const dirPath = parentPath ? `${parentPath}/${folderName}` : folderName;
|
|
74
|
+
const subFolder = node.folders[folderName];
|
|
75
|
+
const id = 'folder-' + dirPath.replace(/[\/\s]/g, '-');
|
|
76
|
+
|
|
77
|
+
const fileCount = subFolder.files ? subFolder.files.length : 0;
|
|
78
|
+
const folderCount = subFolder.folders ? Object.keys(subFolder.folders).length : 0;
|
|
79
|
+
|
|
80
|
+
// Check if folder contains CSV files
|
|
81
|
+
const hasCsvFiles = subFolder.files && subFolder.files.some(f => f.extension === '.csv');
|
|
82
|
+
|
|
83
|
+
// Check if this is a first-level folder (isRoot is true and no parent path)
|
|
84
|
+
const isFirstLevel = isRoot && !parentPath;
|
|
85
|
+
|
|
86
|
+
// The study is always the first path segment, regardless of how deep
|
|
87
|
+
// this folder is (e.g. "essai/duplicates" -> "essai") - the Excel
|
|
88
|
+
// export is generated per-study (all datasets with duplicates in one
|
|
89
|
+
// workbook), not per-folder.
|
|
90
|
+
const study = dirPath.split('/')[0];
|
|
91
|
+
|
|
92
|
+
html += `
|
|
93
|
+
<div class="folder-item">
|
|
94
|
+
<div class="folder-header" onclick="toggleFolder('${id}')" role="button" tabindex="0" aria-expanded="false" aria-controls="${id}">
|
|
95
|
+
<span class="folder-icon" id="${id}-icon" aria-hidden="true">📁</span>
|
|
96
|
+
<strong>${escapeHtml(folderName)}</strong>
|
|
97
|
+
<span style="color: #666; font-size: 0.9em; margin-left: 10px;">
|
|
98
|
+
(${fileCount} file${fileCount !== 1 ? 's' : ''}, ${folderCount} folder${folderCount !== 1 ? 's' : ''})
|
|
99
|
+
</span>
|
|
100
|
+
<div class="folder-actions">
|
|
101
|
+
${isFirstLevel ? `
|
|
102
|
+
<button onclick="event.stopPropagation(); viewHtmlReport('${escapeHtml(folderName)}')"
|
|
103
|
+
class="btn-secondary"
|
|
104
|
+
aria-label="View HTML report">
|
|
105
|
+
👁️ View HTML Report
|
|
106
|
+
</button>
|
|
107
|
+
<button onclick="event.stopPropagation(); downloadFolderAsZip('${escapeHtml(dirPath)}')"
|
|
108
|
+
class="btn-primary"
|
|
109
|
+
aria-label="Download folder as ZIP">
|
|
110
|
+
📦 Download as ZIP
|
|
111
|
+
</button>
|
|
112
|
+
` : ''}
|
|
113
|
+
${hasCsvFiles ? `
|
|
114
|
+
<button onclick="event.stopPropagation(); downloadExcelStudy('${escapeHtml(study)}')"
|
|
115
|
+
class="btn-primary"
|
|
116
|
+
aria-label="Download all datasets with duplicates as Excel">
|
|
117
|
+
📊 Download as Excel
|
|
118
|
+
</button>
|
|
119
|
+
` : ''}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="folder-content" id="${id}" style="display: none; margin-left: 20px;" role="region">
|
|
123
|
+
${buildTree(subFolder, dirPath, false)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
`;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Display files
|
|
131
|
+
if (node.files && node.files.length > 0) {
|
|
132
|
+
node.files.forEach(file => {
|
|
133
|
+
const icon = getIconForExtension(file.extension);
|
|
134
|
+
const sizeKb = (file.size / 1024).toFixed(2);
|
|
135
|
+
|
|
136
|
+
html += `
|
|
137
|
+
<div class="file-item">
|
|
138
|
+
<div class="file-info">
|
|
139
|
+
<div class="file-name">${icon} ${escapeHtml(file.name)}</div>
|
|
140
|
+
<div class="file-meta">
|
|
141
|
+
${sizeKb} KB • ${file.date}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div>
|
|
145
|
+
${file.extension === '.html' ?
|
|
146
|
+
`<button onclick="openFile('${escapeHtml(file.path)}')" class="btn-secondary" style="margin-right: 5px;" aria-label="Open ${escapeHtml(file.name)}">👁️ Open</button>
|
|
147
|
+
<button onclick="downloadHtmlAsWord('${escapeHtml(file.path)}')" class="btn-secondary" aria-label="Download as Word">📄 Word</button>` :
|
|
148
|
+
`<button onclick="download('${escapeHtml(file.path)}')" class="btn-primary" aria-label="Download ${escapeHtml(file.name)}">⬇️ Download</button>`}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
`;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return html;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Toggle folder display
|
|
159
|
+
|
|
160
|
+
export function toggleFolder(id) {
|
|
161
|
+
const folder = document.getElementById(id);
|
|
162
|
+
const icon = document.getElementById(id + '-icon');
|
|
163
|
+
const header = folder.previousElementSibling;
|
|
164
|
+
|
|
165
|
+
if (folder && icon) {
|
|
166
|
+
const isExpanded = folder.style.display !== 'none';
|
|
167
|
+
folder.style.display = isExpanded ? 'none' : 'block';
|
|
168
|
+
icon.textContent = isExpanded ? '📁' : '📂';
|
|
169
|
+
if (header) {
|
|
170
|
+
header.setAttribute('aria-expanded', !isExpanded);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get icon based on extension
|
|
176
|
+
|
|
177
|
+
export function getIconForExtension(ext) {
|
|
178
|
+
const icons = {
|
|
179
|
+
'.html': '🌐',
|
|
180
|
+
'.csv': '📊',
|
|
181
|
+
'.xlsx': '📗',
|
|
182
|
+
'.xls': '📗',
|
|
183
|
+
'.pdf': '📕',
|
|
184
|
+
'.txt': '📄',
|
|
185
|
+
'.md': '📄',
|
|
186
|
+
'.yaml': '⚙️',
|
|
187
|
+
'.yml': '⚙️'
|
|
188
|
+
};
|
|
189
|
+
return icons[ext.toLowerCase()] || '📄';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Download a file (forces the browser to save it, even for types normally shown inline like .html/.pdf)
|
|
193
|
+
|
|
194
|
+
export function download(filePath) {
|
|
195
|
+
window.location.href = '/download/' + encodeURIComponent(filePath) + '?download=1';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Open HTML file in new tab (viewed inline, not downloaded)
|
|
199
|
+
|
|
200
|
+
export function openFile(filePath) {
|
|
201
|
+
window.open('/download/' + encodeURIComponent(filePath), '_blank', 'noopener,noreferrer');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// View the HTML report for a study, generating it on demand if needed.
|
|
205
|
+
// `study` is the top-level folder name in hlsv_results/, which matches the
|
|
206
|
+
// key used by AnalysisRegistry - this works as soon as the study folder
|
|
207
|
+
// exists, even before the html file itself has ever been generated.
|
|
208
|
+
|
|
209
|
+
export function viewHtmlReport(study) {
|
|
210
|
+
window.open('/report/' + encodeURIComponent(study) + '/html', '_blank', 'noopener,noreferrer');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Utility Functions
|
|
214
|
+
|
|
215
|
+
// Duration in milliseconds
|
|
216
|
+
|
|
217
|
+
export function downloadFolderAsZip(dirPath) {
|
|
218
|
+
window.location.href = '/download_zip_dir/' + encodeURIComponent(dirPath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Download all datasets with duplicates for a study as one Excel workbook.
|
|
222
|
+
// This is per-study, not per-folder - sv.report.excel aggregates every
|
|
223
|
+
// dataset that has duplicates into a single workbook, wherever in the
|
|
224
|
+
// study's tree the button was clicked.
|
|
225
|
+
|
|
226
|
+
export function downloadExcelStudy(study) {
|
|
227
|
+
window.location.href = '/report/' + encodeURIComponent(study) + '/excel';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Download HTML report as Word document.
|
|
231
|
+
// `filePath` is relative to hlsv_results/, e.g. "MyStudy/myStudy_high_level_check.html" -
|
|
232
|
+
// its first segment is the study name, which is the key used by AnalysisRegistry.
|
|
233
|
+
|
|
234
|
+
export function downloadHtmlAsWord(filePath) {
|
|
235
|
+
const study = filePath.split('/')[0];
|
|
236
|
+
window.location.href = '/report/' + encodeURIComponent(study) + '/docx';
|
|
237
|
+
setTimeout(refreshResults, 1500); // let the file appear before refreshing the tree
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Download HTML file as PDF
|
data/public/js/utils.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
export function formatDuration(milliseconds) {
|
|
8
|
+
if (milliseconds < 1000) {
|
|
9
|
+
return `${Math.round(milliseconds)} ms`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const totalSeconds = milliseconds / 1000;
|
|
13
|
+
|
|
14
|
+
if (totalSeconds < 60) {
|
|
15
|
+
return `${totalSeconds.toFixed(2)} s`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
19
|
+
const seconds = totalSeconds % 60;
|
|
20
|
+
|
|
21
|
+
return `${minutes} min ${seconds.toFixed(2)} s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Show message with auto-dismiss
|
|
25
|
+
// Broadcasts to every element sharing the given data-message-group (not a
|
|
26
|
+
// single id) so the same status can be shown in more than one place at
|
|
27
|
+
// once - e.g. config-issue now appears both next to the top quick actions
|
|
28
|
+
// and next to the bottom button group, since with two button rows the
|
|
29
|
+
// user might be looking at either one.
|
|
30
|
+
export function showMessage(groupName, type, message, duration = 3000) {
|
|
31
|
+
const targets = document.querySelectorAll(`[data-message-group="${groupName}"]`);
|
|
32
|
+
targets.forEach(msgDiv => {
|
|
33
|
+
msgDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (duration > 0) {
|
|
37
|
+
setTimeout(() => targets.forEach(msgDiv => msgDiv.innerHTML = ''), duration);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Escape HTML to prevent XSS
|
|
42
|
+
|
|
43
|
+
export function escapeHtml(text) {
|
|
44
|
+
const div = document.createElement('div');
|
|
45
|
+
div.textContent = text;
|
|
46
|
+
return div.innerHTML;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Keyboard accessibility for folder toggles
|
|
50
|
+
document.addEventListener('keydown', (e) => {
|
|
51
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
52
|
+
if (e.target.classList.contains('folder-header')) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
e.target.click();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
data/views/csv_view.erb
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
<meta charset="utf-8">
|
|
14
14
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
15
|
<title><%= @ds_name %>: Duplicate Analysis</title>
|
|
16
|
-
<link rel="stylesheet" href="/styles_csv.css">
|
|
16
|
+
<link rel="stylesheet" href="/css/styles_csv.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
|
|
20
20
|
<!-- Header with title and logo -->
|
|
21
|
-
<header class="
|
|
21
|
+
<header class="app-header">
|
|
22
22
|
<h1>Duplicates in <%= @ds_name %> for <%= @type %></h1>
|
|
23
23
|
<div class="company-branding">
|
|
24
24
|
<a href="https://adclin.com" target="_blank" rel="noopener noreferrer" class="company-link">
|
|
@@ -27,15 +27,7 @@
|
|
|
27
27
|
</div>
|
|
28
28
|
</header>
|
|
29
29
|
|
|
30
|
-
<
|
|
31
|
-
<a class="btn-export"
|
|
32
|
-
href="/excel_export?file=<%= URI.encode_www_form_component(params[:file]) %>&last_valid_key=<%= URI.encode_www_form_component(@last_valid_key.join(',')) %>"
|
|
33
|
-
aria-label="Export to Excel">
|
|
34
|
-
⬇️ Export to Excel
|
|
35
|
-
</a>
|
|
36
|
-
</nav>
|
|
37
|
-
|
|
38
|
-
<section class="info-section" role="region" aria-label="Information">
|
|
30
|
+
<section class="info" role="region" aria-label="Information">
|
|
39
31
|
<p>This table displays the detected duplicates, grouped according to the last key tested.
|
|
40
32
|
The duplicate groups are represented by a number in the "No" column.
|
|
41
33
|
Alternating colors are used to distinguish them visually.</p>
|
|
@@ -44,6 +36,13 @@
|
|
|
44
36
|
<% unless @last_valid_key.empty? %>
|
|
45
37
|
<p>Last key tested: <strong><%= @last_valid_key.join(', ') %></strong></p>
|
|
46
38
|
<% end %>
|
|
39
|
+
<% if @study %>
|
|
40
|
+
<a class="btn-quick-action btn-quick-primary"
|
|
41
|
+
href="<%= export_csv_excel_url(file: @file, study: @study, dataset: @ds_name, last_valid_key: @last_valid_key) %>"
|
|
42
|
+
aria-label="Export as Excel">
|
|
43
|
+
📊 Export as Excel
|
|
44
|
+
</a>
|
|
45
|
+
<% end %>
|
|
47
46
|
</section>
|
|
48
47
|
|
|
49
48
|
<div class="table-container" role="region" aria-label="Duplicates table" tabindex="0">
|
|
@@ -72,7 +71,7 @@
|
|
|
72
71
|
</div>
|
|
73
72
|
|
|
74
73
|
<!-- Footer with copyright and license on same line -->
|
|
75
|
-
<footer class="
|
|
74
|
+
<footer class="app-footer">
|
|
76
75
|
<div class="footer-content">
|
|
77
76
|
<span class="copyright">© 1999-2026 AdClin. All rights reserved.</span>
|
|
78
77
|
<span class="separator">•</span>
|
data/views/index.erb
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
<title>High Level Check on SDTM Packages</title>
|
|
14
14
|
<meta charset="UTF-8">
|
|
15
15
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
16
|
-
<link rel="
|
|
16
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
|
17
|
+
<link rel="stylesheet" href="/css/styles.css">
|
|
17
18
|
</head>
|
|
18
19
|
<body>
|
|
19
20
|
<!-- Header with title and logo -->
|
|
@@ -32,7 +33,29 @@
|
|
|
32
33
|
|
|
33
34
|
<!-- Configuration -->
|
|
34
35
|
<div class="container">
|
|
35
|
-
<
|
|
36
|
+
<div class="config-header-row">
|
|
37
|
+
<h2>⚙️ Configuration</h2>
|
|
38
|
+
<div class="quick-actions">
|
|
39
|
+
<button type="button" class="btn-quick-action btn-quick-primary" onclick="configSave()" aria-label="Save current configuration">
|
|
40
|
+
💾 Save
|
|
41
|
+
</button>
|
|
42
|
+
<button type="button" class="btn-quick-action" onclick="fullConfigViewer()" aria-label="View current configuration">
|
|
43
|
+
📄 View Current
|
|
44
|
+
</button>
|
|
45
|
+
<button type="button" class="btn-quick-action btn-quick-danger" onclick="clearFields()" aria-label="Clear all fields">
|
|
46
|
+
🗑️ Clear
|
|
47
|
+
</button>
|
|
48
|
+
<button type="button" class="btn-quick-action" onclick="loadDefaultConfig()" aria-label="Load default configuration">
|
|
49
|
+
📋 Load Default
|
|
50
|
+
</button>
|
|
51
|
+
<button type="button" class="btn-quick-action" onclick="loadExistingConfig()" aria-label="Load an existing configuration file">
|
|
52
|
+
📂 Load Existing
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div data-message-group="config-issue" role="status" aria-live="polite"></div>
|
|
58
|
+
|
|
36
59
|
<h3>General Information</h3>
|
|
37
60
|
|
|
38
61
|
<div class="config-grid">
|
|
@@ -50,13 +73,19 @@
|
|
|
50
73
|
</div>
|
|
51
74
|
|
|
52
75
|
<div class="form-group">
|
|
53
|
-
<
|
|
76
|
+
<div class="form-label-row">
|
|
77
|
+
<label for="data_directory">Datasets Directory <span class="required">*</span></label>
|
|
78
|
+
<button type="button" class="btn-browse-link" onclick="openBrowser('data_directory', 'directory')" aria-label="Browse for datasets directory">📁 Browse</button>
|
|
79
|
+
</div>
|
|
54
80
|
<input type="text" id="data_directory" value="<%= @config['data_directory'] %>" required aria-required="true">
|
|
55
81
|
<small>Path to the directory containing the <strong>xpt</strong> datasets to analyze</small>
|
|
56
82
|
</div>
|
|
57
83
|
|
|
58
84
|
<div class="form-group">
|
|
59
|
-
<
|
|
85
|
+
<div class="form-label-row">
|
|
86
|
+
<label for="define_path">Path to define.xml <span class="required">*</span></label>
|
|
87
|
+
<button type="button" class="btn-browse-link" onclick="openBrowser('define_path', 'file', '.xml')" aria-label="Browse for define.xml file">📁 Browse</button>
|
|
88
|
+
</div>
|
|
60
89
|
<input type="text" id="define_path" value="<%= @config['define_path'] %>" required aria-required="true">
|
|
61
90
|
<small>Enter "-" to skip the define.xml validation part of the analysis</small>
|
|
62
91
|
</div>
|
|
@@ -164,24 +193,27 @@
|
|
|
164
193
|
<input type="text" id="TV_key" value="<%= @config['TV_key'] %>" required aria-required="true">
|
|
165
194
|
</div>
|
|
166
195
|
</div>
|
|
167
|
-
|
|
196
|
+
|
|
168
197
|
<!-- Configuration Management Buttons -->
|
|
169
|
-
<div class="button-group">
|
|
170
|
-
<button onclick="
|
|
171
|
-
💾 Save
|
|
198
|
+
<div class="button-group button-group-compact">
|
|
199
|
+
<button onclick="configSave()" class="btn-primary" aria-label="Save current configuration">
|
|
200
|
+
💾 Save Configuration
|
|
172
201
|
</button>
|
|
173
|
-
<button class="btn-secondary" onclick="
|
|
174
|
-
📄 View
|
|
202
|
+
<button class="btn-secondary" onclick="fullConfigViewer()" aria-label="View current configuration">
|
|
203
|
+
📄 View Configuration
|
|
175
204
|
</button>
|
|
176
|
-
<button class="btn-danger" onclick="
|
|
205
|
+
<button class="btn-danger" onclick="clearFields()" aria-label="Clear all fields">
|
|
177
206
|
🗑️ Clear All Fields
|
|
178
207
|
</button>
|
|
179
|
-
<button class="btn-secondary" onclick="
|
|
208
|
+
<button class="btn-secondary" onclick="loadDefaultConfig()" aria-label="Load default configuration">
|
|
180
209
|
📋 Load Default Configuration
|
|
181
210
|
</button>
|
|
211
|
+
<button class="btn-secondary" onclick="loadExistingConfig()" aria-label="Load an existing configuration file">
|
|
212
|
+
📂 Load Existing Configuration
|
|
213
|
+
</button>
|
|
182
214
|
</div>
|
|
183
215
|
|
|
184
|
-
<div
|
|
216
|
+
<div data-message-group="config-issue" role="status" aria-live="polite"></div>
|
|
185
217
|
</div>
|
|
186
218
|
|
|
187
219
|
<!-- Processing -->
|
|
@@ -195,11 +227,11 @@
|
|
|
195
227
|
<li>Ad hoc search for a natural key for all datasets</li>
|
|
196
228
|
</ul>
|
|
197
229
|
|
|
198
|
-
<button class="btn-success" onclick="
|
|
230
|
+
<button class="btn-success" onclick="startAnalysis()" id="btn-process" aria-label="Start analysis">
|
|
199
231
|
🚀 Start Analysis
|
|
200
232
|
</button>
|
|
201
233
|
|
|
202
|
-
<div id="status
|
|
234
|
+
<div id="analysis-status" role="status" aria-live="polite"></div>
|
|
203
235
|
</div>
|
|
204
236
|
|
|
205
237
|
<!-- Results -->
|
|
@@ -207,16 +239,16 @@
|
|
|
207
239
|
<h2>📁 Result Files</h2>
|
|
208
240
|
|
|
209
241
|
<div class="button-group">
|
|
210
|
-
<button class="btn-secondary" onclick="
|
|
242
|
+
<button class="btn-secondary" onclick="loadResults()" id="btn-load-results" aria-label="Load results">
|
|
211
243
|
📂 Load Results
|
|
212
244
|
</button>
|
|
213
|
-
<button class="btn-secondary" onclick="
|
|
245
|
+
<button class="btn-secondary" onclick="refreshResults()" id="btn-refresh-results" style="display: none;" aria-label="Refresh results">
|
|
214
246
|
🔄 Refresh
|
|
215
247
|
</button>
|
|
216
248
|
<p>Browse files generated in the <code>hlsv_results/</code> directory</p>
|
|
217
249
|
</div>
|
|
218
250
|
|
|
219
|
-
<div id="
|
|
251
|
+
<div id="results-list" style="display: none;" role="region" aria-label="Results list"></div>
|
|
220
252
|
</div>
|
|
221
253
|
|
|
222
254
|
<!-- Footer with copyright and license on same line -->
|
|
@@ -228,6 +260,25 @@
|
|
|
228
260
|
</div>
|
|
229
261
|
</footer>
|
|
230
262
|
|
|
231
|
-
|
|
263
|
+
<!-- File/Directory Browser Modal -->
|
|
264
|
+
<div id="browse-modal" class="modal-overlay" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="browse-modal-title">
|
|
265
|
+
<div class="modal-box">
|
|
266
|
+
<div class="modal-header">
|
|
267
|
+
<h3 id="browse-modal-title">📁 Select a directory</h3>
|
|
268
|
+
<button type="button" class="modal-close" onclick="closeBrowser()" aria-label="Close">✖</button>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="modal-path-bar">
|
|
271
|
+
<button type="button" id="browse-up" onclick="browseUp()" aria-label="Go to parent directory">⬆️</button>
|
|
272
|
+
<input type="text" id="browse-current-path" readonly aria-label="Current directory">
|
|
273
|
+
</div>
|
|
274
|
+
<div id="browse-list" class="modal-list" role="listbox" aria-label="Directory contents"></div>
|
|
275
|
+
<div class="modal-footer">
|
|
276
|
+
<button type="button" class="btn-secondary" onclick="closeBrowser()">Cancel</button>
|
|
277
|
+
<button type="button" class="btn-primary" id="browse-select-btn" onclick="confirmBrowseSelection()">Select this folder</button>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<script type="module" src="/js/app.js"></script>
|
|
232
283
|
</body>
|
|
233
284
|
</html>
|