@arcadialdev/arcality 3.0.1 β†’ 3.0.3

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.
@@ -1,712 +1,1240 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const fs_1 = __importDefault(require("fs"));
7
- const path_1 = __importDefault(require("path"));
8
- class ArcalityReporter {
9
- constructor(options = {}) {
10
- this.results = [];
11
- this.totalDuration = 0;
12
- this.startTime = new Date();
13
- this.outputDir = options.outputDir || 'arcality-report';
14
- }
15
- onBegin(config, suite) {
16
- // Clean output directory to avoid stale reports/attachments
17
- if (fs_1.default.existsSync(this.outputDir)) {
18
- try {
19
- fs_1.default.rmSync(this.outputDir, { recursive: true, force: true });
20
- console.log(`🧹 Cleaned old report data at: ${this.outputDir}`);
21
- }
22
- catch (err) {
23
- console.warn(`⚠️ Could not fully clean report dir: ${err.message}`);
24
- }
25
- }
26
- if (!fs_1.default.existsSync(this.outputDir)) {
27
- fs_1.default.mkdirSync(this.outputDir, { recursive: true });
28
- }
29
- this.startTime = new Date();
30
- console.log(`πŸš€ Arcality Mission started at ${this.startTime.toLocaleTimeString()}`);
31
- }
32
- onTestEnd(test, result) {
33
- // Collect steps with recursion
34
- const stepsHtml = result.steps.map(step => this.renderStep(step, 0)).join('');
35
- // Handle attachments: copy to report dir
36
- const processedAttachments = result.attachments.map(att => {
37
- let extension = '.png';
38
- if (att.contentType === 'application/json')
39
- extension = '.json';
40
- else if (att.contentType === 'text/plain')
41
- extension = '.txt';
42
- else if (att.contentType === 'text/yaml')
43
- extension = '.yaml';
44
- const fileName = att.path ? path_1.default.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}${extension}`;
45
- const attachmentsDir = path_1.default.join(this.outputDir, 'attachments');
46
- const destPath = path_1.default.join(attachmentsDir, fileName);
47
- if (!fs_1.default.existsSync(attachmentsDir)) {
48
- fs_1.default.mkdirSync(attachmentsDir, { recursive: true });
49
- }
50
- try {
51
- if (att.path && fs_1.default.existsSync(att.path)) {
52
- // Si ya existe el archivo fΓ­sico, lo copiamos
53
- fs_1.default.copyFileSync(att.path, destPath);
54
- }
55
- else if (att.body) {
56
- // Si viene como Buffer (evidencia del agente), lo guardamos
57
- fs_1.default.writeFileSync(destPath, att.body);
58
- }
59
- return { ...att, path: `attachments/${fileName}` };
60
- }
61
- catch (e) {
62
- console.error(`Failed to process attachment ${att.name}:`, e);
63
- return att;
64
- }
65
- });
66
- if (result.status === 'failed' || result.error) {
67
- const isAppBug = result.error?.message?.includes('APPLICATION BUG');
68
- const prefix = isAppBug ? '🐞 [APPLICATION BUG FOUND]' : '❌ [TEST ENGINE FAILURE]';
69
- console.log(`\n${prefix} ${test.title} failed:`);
70
- console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}\n`);
71
- if (result.error?.stack && !isAppBug) {
72
- console.log(result.error.stack);
73
- }
74
- }
75
- this.results.push({ test, result, stepsHtml, attachments: processedAttachments });
76
- }
77
- async onEnd(result) {
78
- this.totalDuration = result.duration;
79
- const htmlContent = this.generateHtml();
80
- const reportPath = path_1.default.join(this.outputDir, 'index.html');
81
- fs_1.default.writeFileSync(reportPath, htmlContent);
82
- console.log(`✨ Arcality Mission Report generated at: ${reportPath}`);
83
- }
84
- renderStep(step, depth) {
85
- // Humanize: Hide internal Playwright hooks to only show Agent decisions
86
- if (step.category === 'hook' || step.category === 'fixture')
87
- return '';
88
- const padding = depth * 16;
89
- const statusClass = step.error ? 'step-error' : 'step-passed';
90
- const icon = step.error ? 'βœ•' : 'βœ“';
91
- const duration = step.duration >= 0 ? `<span class="step-duration">${step.duration}ms</span>` : '';
92
- let title = this.escapeHtml(step.title);
93
- // Highlight common playwright actions
94
- title = title.replace(/^(click|fill|navigate|wait|expect|goto|press)/i, '<span class="step-action">$1</span>');
95
- let children = '';
96
- if (step.steps && step.steps.length > 0) {
97
- children = `<div class="step-children">${step.steps.map(s => this.renderStep(s, depth + 1)).join('')}</div>`;
98
- }
99
- const isExpandable = step.steps && step.steps.length > 0;
100
- const expandableAttr = isExpandable ? 'data-expandable="true"' : '';
101
- return `
102
- <div class="step-wrapper ${statusClass}" style="margin-left: ${padding}px" ${expandableAttr}>
103
- <div class="step-header">
104
- <span class="step-icon">${icon}</span>
105
- <span class="step-title">${title}</span>
106
- ${duration}
107
- </div>
108
- ${children}
109
- </div>
110
- `;
111
- }
112
- getEngineVersion() {
113
- try {
114
- const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
115
- if (fs_1.default.existsSync(packageJsonPath)) {
116
- const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
117
- return pkg.version || '2.2.0';
118
- }
119
- }
120
- catch (e) {
121
- console.error('Error reading package.json version:', e);
122
- }
123
- return '2.2.0';
124
- }
125
- generateHtml() {
126
- const stats = {
127
- total: this.results.length,
128
- passed: this.results.filter(r => r.result.status === 'passed').length,
129
- failed: this.results.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length,
130
- skipped: this.results.filter(r => r.result.status === 'skipped').length,
131
- flaky: this.results.filter(r => r.result.status === 'passed' && r.result.retry > 0).length
132
- };
133
- return `
134
- <!DOCTYPE html>
135
- <html lang="en">
136
- <head>
137
- <meta charset="UTF-8">
138
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
139
- <title>Arcality Mission Report</title>
140
- <link rel="shortcut icon" href="https://raw.githubusercontent.com/microsoft/playwright/main/packages/playwright-core/src/server/chromium/test-results/favicon.ico">
141
- <style>
142
- @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
143
-
144
- :root {
145
- --bg-deep: #020617;
146
- --bg-card: #0f172a;
147
- --bg-hover: #1e293b;
148
- --text-main: #f8fafc;
149
- --text-muted: #94a3b8;
150
- --primary: #6366f1;
151
- --success: #10b981;
152
- --error: #f43f5e;
153
- --warning: #f59e0b;
154
- --border: #1e293b;
155
- --accent: #c084fc;
156
- }
157
-
158
- * { box-sizing: border-box; }
159
- body {
160
- font-family: 'Plus Jakarta Sans', sans-serif;
161
- background-color: var(--bg-deep);
162
- color: var(--text-main);
163
- margin: 0;
164
- line-height: 1.6;
165
- }
166
-
167
- .container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
168
-
169
- /* HEADER */
170
- .main-header {
171
- display: flex;
172
- justify-content: space-between;
173
- align-items: flex-start;
174
- margin-bottom: 3rem;
175
- padding-bottom: 2rem;
176
- border-bottom: 1px solid var(--border);
177
- }
178
-
179
- .brand-section h1 {
180
- font-size: 2.5rem;
181
- font-weight: 800;
182
- margin: 0;
183
- background: linear-gradient(to right, #818cf8, #c084fc);
184
- -webkit-background-clip: text;
185
- -webkit-text-fill-color: transparent;
186
- }
187
- .mission-id {
188
- font-family: 'JetBrains Mono', monospace;
189
- font-size: 0.8rem;
190
- color: var(--text-muted);
191
- text-transform: uppercase;
192
- letter-spacing: 3px;
193
- }
194
-
195
- /* DASHBOARD STATS */
196
- .stats-grid {
197
- display: grid;
198
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
199
- gap: 1.5rem;
200
- margin-bottom: 3rem;
201
- }
202
- .stat-card {
203
- background: var(--bg-card);
204
- padding: 1.5rem;
205
- border-radius: 16px;
206
- border: 1px solid var(--border);
207
- text-align: center;
208
- }
209
- .stat-value { font-size: 2rem; font-weight: 800; display: block; }
210
- .stat-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; font-weight: 700; }
211
-
212
- .val-total { color: var(--text-main); }
213
- .val-passed { color: var(--success); }
214
- .val-failed { color: var(--error); }
215
- .val-skipped { color: var(--text-muted); }
216
- .val-flaky { color: var(--warning); }
217
-
218
- /* TEST LIST */
219
- .test-entry {
220
- background: var(--bg-card);
221
- border: 1px solid var(--border);
222
- border-radius: 20px;
223
- margin-bottom: 2rem;
224
- overflow: hidden;
225
- transition: transform 0.2s;
226
- }
227
- .test-entry:hover { border-color: var(--primary); }
228
-
229
- .test-header {
230
- padding: 1.5rem 2rem;
231
- display: flex;
232
- justify-content: space-between;
233
- align-items: center;
234
- cursor: pointer;
235
- background: rgba(255,255,255,0.02);
236
- }
237
- .test-info { display: flex; align-items: center; gap: 1rem; }
238
- .status-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
239
- .status-passed .status-dot { background: var(--success); box-shadow: 0 0 10px var(--success); }
240
- .status-failed .status-dot { background: var(--error); box-shadow: 0 0 10px var(--error); }
241
- .status-skipped .status-dot { background: var(--text-muted); }
242
-
243
- .test-name { font-weight: 700; font-size: 1.1rem; }
244
- .test-meta { font-size: 0.8rem; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; }
245
-
246
- .test-details { padding: 0 2rem 2rem; }
247
-
248
- /* ERROR BLOCK */
249
- .error-block {
250
- background: rgba(244, 63, 94, 0.1);
251
- border-left: 4px solid var(--error);
252
- padding: 1.5rem;
253
- border-radius: 8px;
254
- margin: 1rem 0;
255
- font-family: 'JetBrains Mono', monospace;
256
- font-size: 0.9rem;
257
- color: #fda4af;
258
- white-space: pre-wrap;
259
- }
260
-
261
- /* SUCCESS BLOCK */
262
- .success-block {
263
- background: rgba(16, 185, 129, 0.1);
264
- border-left: 4px solid var(--success);
265
- padding: 1.2rem;
266
- border-radius: 12px;
267
- margin: 1rem 0;
268
- font-size: 1.1rem;
269
- font-weight: 600;
270
- color: #6ee7b7;
271
- display: flex;
272
- align-items: center;
273
- gap: 12px;
274
- }
275
- .success-block::before {
276
- content: "✨";
277
- font-size: 1.4rem;
278
- }
279
-
280
- /* STEPS UI */
281
- .steps-section { margin-top: 2rem; }
282
- .section-title { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 2px; color: var(--accent); margin-bottom: 1rem; display: block; }
283
-
284
- .step-wrapper {
285
- border-left: 1px solid var(--border);
286
- padding: 8px 16px;
287
- position: relative;
288
- }
289
- .step-header { display: flex; align-items: flex-start; gap: 10px; font-size: 0.9rem; }
290
- .step-icon { width: 16px; font-size: 0.8rem; opacity: 0.7; }
291
- .step-action { color: var(--primary); font-weight: 700; }
292
- .step-duration { font-size: 0.7rem; color: var(--text-muted); margin-left: auto; }
293
- .step-error { border-left-color: var(--error); color: #fb7185; }
294
- .step-error .step-icon { color: var(--error); }
295
-
296
- /* ATTACHMENTS (Screenshots, Videos) */
297
- .attachments-grid {
298
- display: grid;
299
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
300
- gap: 1.5rem;
301
- margin-top: 2rem;
302
- }
303
- .attachment-item {
304
- background: var(--bg-deep);
305
- border-radius: 12px;
306
- overflow: hidden;
307
- border: 1px solid var(--border);
308
- }
309
- .attachment-item img, .attachment-item video {
310
- width: 100%;
311
- height: 180px;
312
- object-fit: cover;
313
- display: block;
314
- cursor: zoom-in;
315
- }
316
- .att-label {
317
- padding: 0.75rem;
318
- font-size: 0.75rem;
319
- font-weight: 600;
320
- text-align: center;
321
- background: rgba(255,255,255,0.05);
322
- display: block;
323
- text-decoration: none;
324
- color: var(--text-muted);
325
- }
326
- .att-label:hover { color: var(--text-main); }
327
-
328
- .btn-trace {
329
- background: var(--primary);
330
- color: white;
331
- padding: 6px 12px;
332
- border-radius: 6px;
333
- font-size: 0.7rem;
334
- font-weight: 700;
335
- text-decoration: none;
336
- display: inline-flex;
337
- align-items: center;
338
- gap: 5px;
339
- }
340
-
341
- footer {
342
- margin-top: 5rem;
343
- text-align: center;
344
- padding: 2rem;
345
- font-size: 0.8rem;
346
- color: var(--text-muted);
347
- border-top: 1px solid var(--border);
348
- }
349
-
350
- /* UTILS */
351
- .tag { padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 800; text-transform: uppercase; }
352
- .tag-project { background: rgba(192, 132, 252, 0.2); color: var(--accent); }
353
- .tag-retry { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
354
-
355
- /* SECURITY REPORT SECTION */
356
- .security-section {
357
- margin: 2rem 0;
358
- background: var(--bg-card);
359
- border-radius: 16px;
360
- overflow: hidden;
361
- border: 1px solid #4a4a4a
362
- }
363
- .security-header {
364
- padding: 1rem 1.5rem;
365
- cursor: pointer;
366
- display: flex;
367
- align-items: center;
368
- gap: 12px;
369
- background: linear-gradient(90deg, rgba(245, 158, 11, 0.1), rgba(245, 158, 11, 0.0) 70%);
370
- }
371
- .security-header:hover {
372
- background: linear-gradient(90deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.05) 70%);
373
- }
374
- .security-header h3 {
375
- margin: 0;
376
- font-size: 1.2rem;
377
- color: var(--warning);
378
- font-weight: 700;
379
- }
380
- .security-risk-badge {
381
- padding: 4px 10px;
382
- border-radius: 6px;
383
- font-size: 0.8rem;
384
- font-weight: 800;
385
- color: white;
386
- margin-left: auto;
387
- }
388
- .security-details {
389
- padding: 1.5rem;
390
- background: #0c1322;
391
- }
392
- .security-summary {
393
- display: grid;
394
- grid-template-columns: repeat(4, 1fr);
395
- gap: 1rem;
396
- margin-bottom: 2rem;
397
- padding-bottom: 1.5rem;
398
- border-bottom: 1px solid var(--border);
399
- text-align: center;
400
- }
401
- .summary-item .count {
402
- font-size: 1.5rem;
403
- font-weight: 800;
404
- }
405
- .summary-item .label {
406
- font-size: 0.7rem;
407
- text-transform: uppercase;
408
- color: var(--text-muted);
409
- }
410
- .count-Critical { color: var(--error); }
411
- .count-High { color: #fb923c; }
412
- .count-Medium { color: var(--warning); }
413
- .count-Low { color: #38bdf8; }
414
-
415
- .vuln-list { display: flex; flex-direction: column; gap: 1rem; }
416
- .vuln-card {
417
- background: var(--bg-deep);
418
- border: 1px solid var(--border);
419
- border-left-width: 4px;
420
- border-radius: 8px;
421
- padding: 1.5rem;
422
- }
423
- .vuln-card-title { font-weight: 700; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 10px;}
424
- .vuln-card-desc { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1rem; }
425
- .vuln-card-evidence {
426
- background: #020617;
427
- padding: 0.75rem;
428
- border-radius: 4px;
429
- font-family: 'JetBrains Mono', monospace;
430
- font-size: 0.8rem;
431
- white-space: pre-wrap;
432
- word-break: break-all;
433
- margin-bottom: 1rem;
434
- border: 1px solid var(--border);
435
- max-height: 200px;
436
- overflow-y: auto;
437
- }
438
- .vuln-card-remediation { font-size: 0.9rem; color: var(--success); }
439
- pre { margin: 0; }
440
- </style>
441
- </head>
442
- <body>
443
- <div class="container">
444
- <div class="main-header">
445
- <div class="brand-section">
446
- <span class="mission-id">Mission Alpha System</span>
447
- <h1>Arcality Mission Report</h1>
448
- <p style="color: var(--text-muted)">Testing Intelligence & Diagnostics Console</p>
449
- </div>
450
- <div style="text-align: right">
451
- <div class="meta-item" style="color: var(--accent); font-weight: 700;">${this.startTime.toLocaleDateString()}</div>
452
- <div class="meta-item" style="font-family: 'JetBrains Mono'; font-size: 0.8rem;">ENGINE VERSION ${this.getEngineVersion()}</div>
453
- </div>
454
- </div>
455
-
456
- <div class="stats-grid">
457
- <div class="stat-card"><span class="stat-value val-total">${stats.total}</span><span class="stat-label">Total Missions</span></div>
458
- <div class="stat-card"><span class="stat-value val-passed">${stats.passed}</span><span class="stat-label">Successful</span></div>
459
- <div class="stat-card"><span class="stat-value val-failed">${stats.failed}</span><span class="stat-label">Fatal Errors</span></div>
460
- <div class="stat-card"><span class="stat-value val-flaky">${stats.flaky}</span><span class="stat-label">Flaky Tests</span></div>
461
- <div class="stat-card"><span class="stat-value val-total">${(this.totalDuration / 1000).toFixed(2)}s</span><span class="stat-label">Total Duration</span></div>
462
- </div>
463
-
464
- <div class="test-list">
465
- ${this.results.map((r, i) => {
466
- const statusClass = `status-${r.result.status}`;
467
- const projectName = r.test.parent.project()?.name || 'default';
468
- return `
469
- <div class="test-entry ${statusClass}">
470
- <div class="test-header" onclick="toggleDetails('details-${i}')">
471
- <div class="test-info">
472
- <span class="status-dot"></span>
473
- <div>
474
- <div class="test-name">${this.escapeHtml(r.test.title)}</div>
475
- <div class="test-meta">
476
- <span class="tag tag-project">${projectName}</span>
477
- ${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
478
- <span>${(r.result.duration / 1000).toFixed(2)}s</span>
479
- </div>
480
- </div>
481
- </div>
482
- <div style="display:flex; gap: 10px; align-items: center;">
483
- <span style="opacity:0.5; font-size: 1.2rem">β–Ό</span>
484
- </div>
485
- </div>
486
-
487
- <div class="test-details" id="details-${i}" style="display: none;">
488
- ${r.result.status === 'passed' ? (() => {
489
- const successSummary = r.attachments.find(a => a.name === 'success_summary');
490
- const text = this.getAttachmentText(successSummary);
491
- return text ? `<div class="success-block">${this.escapeHtml(text)}</div>` : '';
492
- })() : ''}
493
-
494
- ${r.result.error ? `
495
- <div class="steps-section">
496
- <span class="section-title">Analysis of the Obstacle</span>
497
- <div class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</div>
498
- </div>
499
- ` : ''}
500
-
501
- ${this._renderSecuritySection(r.attachments, i)}
502
-
503
- <div class="steps-section">
504
- <span class="section-title">🧠 Agent Cognitive Process</span>
505
- <div class="steps-container">
506
- ${r.stepsHtml || '<p style="opacity:0.5">No steps recorded.</p>'}
507
- </div>
508
- </div>
509
-
510
- ${r.attachments.length > 0 ? `
511
- <div class="steps-section">
512
- <span class="section-title">Visual Evidence</span>
513
- <div class="attachments-grid">
514
- ${r.attachments.map(att => {
515
- if (att.contentType.startsWith('image/')) {
516
- return `
517
- <div class="attachment-item">
518
- <img src="${att.path}" alt="${att.name}" onclick="window.open('${att.path}')">
519
- <a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
520
- </div>
521
- `;
522
- }
523
- if (att.contentType.startsWith('video/')) {
524
- return `
525
- <div class="attachment-item">
526
- <video controls src="${att.path}"></video>
527
- <a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
528
- </div>
529
- `;
530
- }
531
- return '';
532
- }).join('')}
533
- </div>
534
- </div>
535
- ` : ''}
536
- </div>
537
- </div>
538
- `;
539
- }).join('')}
540
- </div>
541
-
542
- <footer>
543
- ARCALITY ENGINE v${this.getEngineVersion()} β€’ SYSTEM TIME [${new Date().toISOString()}] β€’ GENERATED FOR ARCADIAL
544
- </footer>
545
- </div>
546
-
547
- <script>
548
- function toggleDetails(id) {
549
- const el = document.getElementById(id);
550
- if (el) {
551
- const isSecurityDetails = id.startsWith('security-details');
552
- if (isSecurityDetails) {
553
- // Independent toggle for security section
554
- el.style.display = (el.style.display === 'none') ? 'block' : 'none';
555
- } else {
556
- // Main test details toggle
557
- el.style.display = (el.style.display === 'none' || el.style.display === '') ? 'block' : 'none';
558
- }
559
- }
560
- }
561
- </script>
562
- </body>
563
- </html>
564
- `;
565
- }
566
- getAttachmentText(att) {
567
- if (!att)
568
- return null;
569
- if (att.body)
570
- return att.body.toString();
571
- if (att.path) {
572
- const actualPath = path_1.default.isAbsolute(att.path) ? att.path : path_1.default.join(this.outputDir, att.path);
573
- if (fs_1.default.existsSync(actualPath)) {
574
- return fs_1.default.readFileSync(actualPath, 'utf8');
575
- }
576
- }
577
- return null;
578
- }
579
- _renderSecuritySection(attachments, index) {
580
- let vulnerabilities = [];
581
- const reportAtt = attachments.find(a => a.name === 'security_report');
582
- if (reportAtt) {
583
- const text = this.getAttachmentText(reportAtt);
584
- if (text) {
585
- try {
586
- const parsed = JSON.parse(text);
587
- if (Array.isArray(parsed))
588
- vulnerabilities.push(...parsed);
589
- }
590
- catch (e) { }
591
- }
592
- }
593
- const findingAtts = attachments.filter(a => a.name === 'security_finding');
594
- for (const findingAtt of findingAtts) {
595
- const text = this.getAttachmentText(findingAtt);
596
- if (text) {
597
- try {
598
- const parsed = JSON.parse(text);
599
- vulnerabilities.push(parsed);
600
- }
601
- catch (e) { }
602
- }
603
- }
604
- if (vulnerabilities.length === 0)
605
- return '';
606
- const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, 'Info': 4 };
607
- const severityCounts = { 'Critical': 0, 'High': 0, 'Medium': 0, 'Low': 0, 'Info': 0 };
608
- let worstSeverity = 'Info';
609
- vulnerabilities.forEach(v => {
610
- const severity = v.severity;
611
- if (severityCounts[severity] !== undefined) {
612
- severityCounts[severity]++;
613
- }
614
- if (severityOrder[severity] < severityOrder[worstSeverity]) {
615
- worstSeverity = severity;
616
- }
617
- });
618
- const getSeverityColor = (sev) => {
619
- switch (sev) {
620
- case 'Critical': return 'var(--error)';
621
- case 'High': return '#fb923c';
622
- case 'Medium': return 'var(--warning)';
623
- case 'Low': return '#38bdf8';
624
- case 'Info': return '#94a3b8';
625
- }
626
- };
627
- const riskColor = getSeverityColor(worstSeverity);
628
- return `
629
- <div class="security-section">
630
- <div class="security-header" onclick="toggleDetails('security-details-${index}')">
631
- <h3>πŸ›‘οΈ Security Assessment</h3>
632
- <span class="security-risk-badge" style="background-color: ${riskColor};">${worstSeverity} Risk</span>
633
- </div>
634
- <div class="security-details" id="security-details-${index}" style="display: block;">
635
- <div class="security-summary">
636
- <div class="summary-item">
637
- <div class="count count-Critical">${severityCounts.Critical}</div>
638
- <div class="label">Critical</div>
639
- </div>
640
- <div class="summary-item">
641
- <div class="count count-High">${severityCounts.High}</div>
642
- <div class="label">High</div>
643
- </div>
644
- <div class="summary-item">
645
- <div class="count count-Medium">${severityCounts.Medium}</div>
646
- <div class="label">Medium</div>
647
- </div>
648
- <div class="summary-item">
649
- <div class="count count-Low">${severityCounts.Low}</div>
650
- <div class="label">Low</div>
651
- </div>
652
- </div>
653
- <div class="vuln-list">
654
- ${vulnerabilities.map(v => {
655
- const sev = v.severity;
656
- const cardColor = getSeverityColor(sev);
657
- return `
658
- <div class="vuln-card" style="border-left-color: ${cardColor};">
659
- <div class="vuln-card-title">
660
- <span class="tag" style="background-color: ${cardColor}20; color: ${cardColor};">${v.severity}</span>
661
- <span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7">β€” ${this.escapeHtml(v.subcategory)}</span>` : ''}</span>
662
- </div>
663
- <div style="font-size: 0.8rem; opacity: 0.8; margin-top: 5px; margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 12px;">
664
- ${v.type ? `<span><strong style="color:var(--accent);">Type:</strong> ${this.escapeHtml(v.type)}</span>` : ''}
665
- ${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` : ''}
666
- ${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` : ''}
667
- </div>
668
- <p class="vuln-card-desc">${this.escapeHtml(v.description)}</p>
669
- ${v.impact ? `<p class="vuln-card-impact" style="font-size: 0.85rem; color: #fb923c; margin-bottom: 10px;"><strong>Potential Impact:</strong> ${this.escapeHtml(v.impact)}</p>` : ''}
670
-
671
- <div class="vuln-card-evidence">
672
- <strong>Evidence:</strong>
673
- <pre>${this.escapeHtml(JSON.stringify(v.evidence, null, 2))}</pre>
674
- </div>
675
-
676
- ${v.reproductionSteps && v.reproductionSteps.length > 0 ? `
677
- <div class="vuln-card-repro" style="margin: 10px 0; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 6px; border-left: 2px solid ${cardColor}80;">
678
- <strong style="font-size: 0.85rem; color: var(--text-muted);">Reproduction Steps:</strong>
679
- <ol style="margin: 5px 0 0 20px; font-size: 0.85rem; color: var(--text-main);">
680
- ${v.reproductionSteps.map((s) => `<li>${this.escapeHtml(s)}</li>`).join('')}
681
- </ol>
682
- </div>` : ''}
683
-
684
- <div class="vuln-card-remediation">
685
- <strong>β†’ Remediation:</strong> ${this.escapeHtml(v.remediation)}
686
- </div>
687
-
688
- ${v.classification ? `
689
- <div style="font-size: 0.75rem; opacity: 0.5; margin-top: 12px; display: flex; gap: 15px; font-family: 'JetBrains Mono', monospace;">
690
- ${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` : ''}
691
- ${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` : ''}
692
- </div>` : ''}
693
- </div>
694
- `;
695
- }).join('')}
696
- </div>
697
- </div>
698
- </div>
699
- `;
700
- }
701
- escapeHtml(unsafe) {
702
- if (!unsafe)
703
- return '';
704
- return unsafe
705
- .replace(/&/g, "&amp;")
706
- .replace(/</g, "&lt;")
707
- .replace(/>/g, "&gt;")
708
- .replace(/"/g, "&quot;")
709
- .replace(/'/g, "&#039;");
710
- }
711
- }
712
- exports.default = ArcalityReporter;
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ class ArcalityReporter {
9
+ constructor(options = {}) {
10
+ this.results = [];
11
+ this.totalDuration = 0;
12
+ this.startTime = new Date();
13
+ this.outputDir = options.outputDir || 'arcality-report';
14
+ }
15
+ onBegin(config, suite) {
16
+ // Clean output directory to avoid stale reports/attachments.
17
+ if (fs_1.default.existsSync(this.outputDir)) {
18
+ try {
19
+ fs_1.default.rmSync(this.outputDir, { recursive: true, force: true });
20
+ console.log(`Cleaned old report data at: ${this.outputDir}`);
21
+ }
22
+ catch (err) {
23
+ console.warn(`Could not fully clean report dir: ${err.message}`);
24
+ }
25
+ }
26
+ if (!fs_1.default.existsSync(this.outputDir)) {
27
+ fs_1.default.mkdirSync(this.outputDir, { recursive: true });
28
+ }
29
+ this.startTime = new Date();
30
+ console.log(`Arcality Mission started at ${this.startTime.toLocaleTimeString()}`);
31
+ }
32
+ onTestEnd(test, result) {
33
+ const stepsHtml = result.steps.map(step => this.renderStep(step, 0)).join('');
34
+ const processedAttachments = result.attachments.map(att => {
35
+ let extension = '.png';
36
+ if (att.contentType === 'application/json')
37
+ extension = '.json';
38
+ else if (att.contentType === 'text/plain')
39
+ extension = '.txt';
40
+ else if (att.contentType === 'text/yaml')
41
+ extension = '.yaml';
42
+ const fileName = att.path ? path_1.default.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}${extension}`;
43
+ const attachmentsDir = path_1.default.join(this.outputDir, 'attachments');
44
+ const destPath = path_1.default.join(attachmentsDir, fileName);
45
+ if (!fs_1.default.existsSync(attachmentsDir)) {
46
+ fs_1.default.mkdirSync(attachmentsDir, { recursive: true });
47
+ }
48
+ try {
49
+ let copied = false;
50
+ if (att.path && fs_1.default.existsSync(att.path)) {
51
+ fs_1.default.copyFileSync(att.path, destPath);
52
+ copied = true;
53
+ }
54
+ else if (att.body) {
55
+ fs_1.default.writeFileSync(destPath, att.body);
56
+ copied = true;
57
+ }
58
+ return copied ? { ...att, path: `attachments/${fileName}` } : att;
59
+ }
60
+ catch (e) {
61
+ console.error(`Failed to process attachment ${att.name}:`, e);
62
+ return att;
63
+ }
64
+ });
65
+ if (result.status === 'failed' || result.error) {
66
+ const isAppBug = result.error?.message?.includes('APPLICATION BUG');
67
+ const prefix = isAppBug ? '[APPLICATION BUG FOUND]' : '[TEST ENGINE FAILURE]';
68
+ console.log(`\n${prefix} ${test.title} failed:`);
69
+ console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}\n`);
70
+ if (result.error?.stack && !isAppBug) {
71
+ console.log(result.error.stack);
72
+ }
73
+ }
74
+ this.results.push({ test, result, stepsHtml, attachments: processedAttachments });
75
+ }
76
+ async onEnd(result) {
77
+ this.totalDuration = result.duration;
78
+ const htmlContent = this.generateHtml();
79
+ const reportPath = path_1.default.join(this.outputDir, 'index.html');
80
+ fs_1.default.writeFileSync(reportPath, htmlContent);
81
+ console.log(`Arcality Mission Report generated at: ${reportPath}`);
82
+ }
83
+ renderStep(step, depth) {
84
+ if (step.category === 'hook' || step.category === 'fixture')
85
+ return '';
86
+ const statusClass = step.error ? 'step-error' : 'step-passed';
87
+ const duration = step.duration >= 0 ? `<span class="step-duration">${this.formatDuration(step.duration)}</span>` : '';
88
+ let title = this.escapeHtml(step.title);
89
+ title = title.replace(/^(click|fill|navigate|wait|expect|goto|press)/i, '<span class="step-action">$1</span>');
90
+ const children = step.steps && step.steps.length > 0
91
+ ? `<div class="step-children">${step.steps.map(s => this.renderStep(s, depth + 1)).join('')}</div>`
92
+ : '';
93
+ const expandableAttr = step.steps && step.steps.length > 0 ? 'data-expandable="true"' : '';
94
+ return `
95
+ <div class="step-wrapper ${statusClass}" style="--step-depth: ${Math.min(depth, 8)}" ${expandableAttr}>
96
+ <div class="step-header">
97
+ <span class="step-marker" aria-hidden="true"></span>
98
+ <span class="step-title">${title}</span>
99
+ ${duration}
100
+ </div>
101
+ ${children}
102
+ </div>
103
+ `;
104
+ }
105
+ formatDuration(milliseconds) {
106
+ if (milliseconds >= 1000) {
107
+ const seconds = milliseconds / 1000;
108
+ return `${seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1)}s`;
109
+ }
110
+ return `${Math.max(0, Math.round(milliseconds))}ms`;
111
+ }
112
+ getStatusLabel(status) {
113
+ switch (status) {
114
+ case 'passed': return 'Exitosa';
115
+ case 'failed': return 'Error';
116
+ case 'timedOut': return 'Tiempo agotado';
117
+ case 'skipped': return 'Omitida';
118
+ case 'interrupted': return 'Interrumpida';
119
+ default: return status;
120
+ }
121
+ }
122
+ getStatusTone(status) {
123
+ if (status === 'failed' || status === 'timedOut' || status === 'interrupted')
124
+ return 'failed';
125
+ if (status === 'passed')
126
+ return 'passed';
127
+ return 'neutral';
128
+ }
129
+ getEngineVersion() {
130
+ try {
131
+ const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
132
+ if (fs_1.default.existsSync(packageJsonPath)) {
133
+ const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
134
+ return pkg.version || '2.2.0';
135
+ }
136
+ }
137
+ catch (e) {
138
+ console.error('Error reading package.json version:', e);
139
+ }
140
+ return '2.2.0';
141
+ }
142
+ getBrandLogoDataUri() {
143
+ const roots = [
144
+ process.env.ARCALITY_ROOT,
145
+ path_1.default.resolve(__dirname, '..', '..'),
146
+ process.cwd()
147
+ ].filter(Boolean);
148
+ const logoNames = ['logo.png', 'logo-arcadial-blanco.png'];
149
+ for (const root of roots) {
150
+ for (const logoName of logoNames) {
151
+ const logoPath = path_1.default.join(root, 'public', logoName);
152
+ if (!fs_1.default.existsSync(logoPath))
153
+ continue;
154
+ try {
155
+ const logoBuffer = fs_1.default.readFileSync(logoPath);
156
+ return `data:image/png;base64,${logoBuffer.toString('base64')}`;
157
+ }
158
+ catch (e) {
159
+ console.warn(`Could not load Arcality logo from ${logoPath}`);
160
+ }
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+ generateHtml() {
166
+ const stats = {
167
+ total: this.results.length,
168
+ passed: this.results.filter(r => r.result.status === 'passed').length,
169
+ failed: this.results.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length,
170
+ skipped: this.results.filter(r => r.result.status === 'skipped').length,
171
+ flaky: this.results.filter(r => r.result.status === 'passed' && r.result.retry > 0).length
172
+ };
173
+ const evidenceCount = this.results.reduce((count, r) => {
174
+ return count + r.attachments.filter(att => att.path && (att.contentType.startsWith('image/') || att.contentType.startsWith('video/'))).length;
175
+ }, 0);
176
+ const reportTone = stats.failed > 0 ? 'failed' : stats.total === 0 ? 'neutral' : 'passed';
177
+ const reportStatus = stats.failed > 0 ? 'Ejecucion con errores' : stats.total === 0 ? 'Sin ejecuciones' : 'Ejecucion exitosa';
178
+ const reportSummary = stats.failed > 0
179
+ ? `${stats.failed} mision${stats.failed === 1 ? '' : 'es'} fallida${stats.failed === 1 ? '' : 's'}. Revisa el diagnostico y la evidencia visual.`
180
+ : stats.total === 0
181
+ ? 'No se registraron misiones en esta ejecucion.'
182
+ : 'Todas las misiones ejecutadas finalizaron correctamente.';
183
+ const generatedAt = new Date();
184
+ const reportTitle = stats.total > 1 ? 'Reporte de coleccion' : 'Reporte de ejecucion';
185
+ const brandLogo = this.getBrandLogoDataUri();
186
+ return `
187
+ <!DOCTYPE html>
188
+ <html lang="es">
189
+ <head>
190
+ <meta charset="UTF-8">
191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
192
+ <title>Arcality Mission Report</title>
193
+ <link rel="shortcut icon" href="https://raw.githubusercontent.com/microsoft/playwright/main/packages/playwright-core/src/server/chromium/test-results/favicon.ico">
194
+ <style>
195
+ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
196
+
197
+ :root {
198
+ --bg: #07080d;
199
+ --surface: #10131b;
200
+ --surface-soft: #151925;
201
+ --line: #293042;
202
+ --line-soft: rgba(255,255,255,0.08);
203
+ --text: #f7f4ff;
204
+ --muted: #a9afc0;
205
+ --faint: #72798c;
206
+ --brand: #b56cff;
207
+ --brand-soft: rgba(181, 108, 255, 0.16);
208
+ --blue: #6bbcff;
209
+ --success: #39d99f;
210
+ --success-soft: rgba(57, 217, 159, 0.12);
211
+ --error: #ff5f7a;
212
+ --error-soft: rgba(255, 95, 122, 0.12);
213
+ --warning: #ffc857;
214
+ --warning-soft: rgba(255, 200, 87, 0.12);
215
+ --shadow: 0 18px 60px rgba(0,0,0,0.28);
216
+ }
217
+
218
+ * { box-sizing: border-box; }
219
+ html { background: var(--bg); }
220
+ body {
221
+ font-family: 'Plus Jakarta Sans', sans-serif;
222
+ background: linear-gradient(180deg, #080910 0%, #10121a 44%, #07080d 100%);
223
+ color: var(--text);
224
+ margin: 0;
225
+ min-height: 100vh;
226
+ line-height: 1.55;
227
+ }
228
+ body::before {
229
+ content: "";
230
+ position: fixed;
231
+ inset: 0;
232
+ pointer-events: none;
233
+ background-image:
234
+ linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
235
+ linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
236
+ background-size: 52px 52px;
237
+ mask-image: linear-gradient(to bottom, rgba(0,0,0,0.75), transparent 68%);
238
+ }
239
+
240
+ button, a { font: inherit; }
241
+ a { color: inherit; }
242
+ .container {
243
+ position: relative;
244
+ max-width: 1480px;
245
+ margin: 0 auto;
246
+ padding: 32px;
247
+ }
248
+
249
+ .mission-brief {
250
+ display: grid;
251
+ grid-template-columns: minmax(0, 1fr) auto;
252
+ gap: 28px;
253
+ align-items: start;
254
+ padding: 6px 0 28px;
255
+ border-bottom: 1px solid var(--line);
256
+ margin-bottom: 22px;
257
+ }
258
+ .brand-row {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 14px;
262
+ margin-bottom: 26px;
263
+ }
264
+ .brand-mark {
265
+ width: 38px;
266
+ height: 38px;
267
+ border: 1px solid rgba(181,108,255,0.45);
268
+ border-radius: 8px;
269
+ background: linear-gradient(135deg, rgba(181,108,255,0.92), rgba(107,188,255,0.62)), #141720;
270
+ box-shadow: 0 0 28px rgba(181,108,255,0.22);
271
+ }
272
+ .brand-mark.has-logo {
273
+ width: 42px;
274
+ height: 42px;
275
+ padding: 5px;
276
+ background: rgba(255,255,255,0.035);
277
+ box-shadow: 0 0 24px rgba(181,108,255,0.12);
278
+ }
279
+ .brand-mark img {
280
+ display: block;
281
+ width: 100%;
282
+ height: 100%;
283
+ object-fit: contain;
284
+ filter: drop-shadow(0 0 10px rgba(181,108,255,0.28));
285
+ }
286
+ .brand-name {
287
+ font-size: 0.74rem;
288
+ color: var(--muted);
289
+ letter-spacing: 0.16em;
290
+ text-transform: uppercase;
291
+ font-weight: 800;
292
+ }
293
+ .brand-subtitle {
294
+ color: var(--faint);
295
+ font-size: 0.86rem;
296
+ margin-top: 1px;
297
+ }
298
+ .mission-title {
299
+ font-size: clamp(2rem, 4vw, 4.6rem);
300
+ line-height: 0.96;
301
+ letter-spacing: 0;
302
+ max-width: 900px;
303
+ margin: 0;
304
+ font-weight: 800;
305
+ }
306
+ .mission-copy {
307
+ margin: 18px 0 0;
308
+ max-width: 720px;
309
+ color: var(--muted);
310
+ font-size: 1rem;
311
+ }
312
+ .status-card {
313
+ width: min(380px, 100%);
314
+ border: 1px solid var(--line);
315
+ background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.018));
316
+ border-radius: 8px;
317
+ padding: 18px;
318
+ box-shadow: var(--shadow);
319
+ }
320
+ .status-card.passed { border-color: rgba(57,217,159,0.42); }
321
+ .status-card.failed { border-color: rgba(255,95,122,0.48); }
322
+ .status-card.neutral { border-color: rgba(169,175,192,0.28); }
323
+ .status-eyebrow {
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: space-between;
327
+ gap: 16px;
328
+ color: var(--faint);
329
+ font-size: 0.72rem;
330
+ font-family: 'JetBrains Mono', monospace;
331
+ letter-spacing: 0.08em;
332
+ text-transform: uppercase;
333
+ }
334
+ .status-pill {
335
+ display: inline-flex;
336
+ align-items: center;
337
+ gap: 7px;
338
+ border: 1px solid var(--line-soft);
339
+ border-radius: 999px;
340
+ padding: 5px 9px;
341
+ color: var(--text);
342
+ background: rgba(255,255,255,0.04);
343
+ }
344
+ .status-pill::before {
345
+ content: "";
346
+ width: 7px;
347
+ height: 7px;
348
+ border-radius: 50%;
349
+ background: var(--faint);
350
+ }
351
+ .status-card.passed .status-pill::before { background: var(--success); }
352
+ .status-card.failed .status-pill::before { background: var(--error); }
353
+ .status-title {
354
+ font-size: 1.7rem;
355
+ line-height: 1.05;
356
+ margin: 24px 0 10px;
357
+ font-weight: 800;
358
+ }
359
+ .status-summary {
360
+ color: var(--muted);
361
+ margin: 0;
362
+ font-size: 0.95rem;
363
+ }
364
+ .brief-grid {
365
+ display: grid;
366
+ grid-template-columns: repeat(6, minmax(120px, 1fr));
367
+ gap: 1px;
368
+ background: var(--line);
369
+ border: 1px solid var(--line);
370
+ border-radius: 8px;
371
+ overflow: hidden;
372
+ margin-bottom: 18px;
373
+ }
374
+ .brief-metric {
375
+ background: rgba(16,19,27,0.94);
376
+ padding: 16px 18px;
377
+ min-height: 92px;
378
+ }
379
+ .metric-value {
380
+ display: block;
381
+ font-size: 1.7rem;
382
+ line-height: 1;
383
+ font-weight: 800;
384
+ }
385
+ .metric-label {
386
+ display: block;
387
+ margin-top: 10px;
388
+ color: var(--faint);
389
+ font-size: 0.72rem;
390
+ letter-spacing: 0.1em;
391
+ text-transform: uppercase;
392
+ font-weight: 800;
393
+ }
394
+ .val-passed { color: var(--success); }
395
+ .val-failed { color: var(--error); }
396
+ .val-flaky { color: var(--warning); }
397
+ .val-blue { color: var(--blue); }
398
+
399
+ .test-entry {
400
+ border-top: 1px solid var(--line);
401
+ }
402
+ .test-entry:last-child {
403
+ border-bottom: 1px solid var(--line);
404
+ }
405
+ .test-header {
406
+ width: 100%;
407
+ appearance: none;
408
+ border: 0;
409
+ color: var(--text);
410
+ background: transparent;
411
+ padding: 22px 0;
412
+ display: flex;
413
+ justify-content: space-between;
414
+ align-items: center;
415
+ gap: 24px;
416
+ text-align: left;
417
+ cursor: pointer;
418
+ }
419
+ .test-header:hover .test-name { color: var(--brand); }
420
+ .test-info {
421
+ min-width: 0;
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 14px;
425
+ }
426
+ .status-dot {
427
+ width: 11px;
428
+ height: 44px;
429
+ border-radius: 999px;
430
+ background: var(--faint);
431
+ flex: 0 0 auto;
432
+ }
433
+ .status-passed .status-dot { background: var(--success); }
434
+ .status-failed .status-dot { background: var(--error); }
435
+ .status-neutral .status-dot { background: var(--faint); }
436
+ .test-name {
437
+ font-weight: 800;
438
+ font-size: 1.08rem;
439
+ line-height: 1.25;
440
+ transition: color 0.16s ease;
441
+ }
442
+ .test-meta {
443
+ display: flex;
444
+ flex-wrap: wrap;
445
+ gap: 8px;
446
+ margin-top: 8px;
447
+ color: var(--muted);
448
+ font-size: 0.78rem;
449
+ font-family: 'JetBrains Mono', monospace;
450
+ }
451
+ .test-chevron {
452
+ width: 32px;
453
+ height: 32px;
454
+ border: 1px solid var(--line);
455
+ border-radius: 8px;
456
+ display: inline-grid;
457
+ place-items: center;
458
+ color: var(--muted);
459
+ flex: 0 0 auto;
460
+ transition: transform 0.16s ease, border-color 0.16s ease;
461
+ }
462
+ .test-header[aria-expanded="true"] .test-chevron {
463
+ transform: rotate(180deg);
464
+ border-color: rgba(181,108,255,0.48);
465
+ color: var(--text);
466
+ }
467
+ .test-details {
468
+ padding: 0 0 28px;
469
+ }
470
+ .test-details-grid {
471
+ display: grid;
472
+ grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.8fr);
473
+ gap: 24px;
474
+ align-items: start;
475
+ }
476
+ .primary-column,
477
+ .context-column {
478
+ min-width: 0;
479
+ }
480
+ .context-column {
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 14px;
484
+ }
485
+ .outcome-block,
486
+ .side-panel,
487
+ .timeline-wrap {
488
+ border: 1px solid var(--line);
489
+ border-radius: 8px;
490
+ background: rgba(16,19,27,0.78);
491
+ }
492
+ .outcome-block {
493
+ padding: 18px;
494
+ margin-bottom: 18px;
495
+ }
496
+ .outcome-block.success {
497
+ border-color: rgba(57,217,159,0.36);
498
+ background: var(--success-soft);
499
+ }
500
+ .outcome-block.error {
501
+ border-color: rgba(255,95,122,0.4);
502
+ background: var(--error-soft);
503
+ }
504
+ .outcome-label {
505
+ display: block;
506
+ color: var(--faint);
507
+ font-size: 0.72rem;
508
+ letter-spacing: 0.12em;
509
+ text-transform: uppercase;
510
+ font-weight: 800;
511
+ margin-bottom: 8px;
512
+ }
513
+ .success-block {
514
+ margin: 0;
515
+ color: #bff7df;
516
+ font-size: 1rem;
517
+ font-weight: 700;
518
+ }
519
+ .error-block {
520
+ font-family: 'JetBrains Mono', monospace;
521
+ font-size: 0.86rem;
522
+ color: #ffd0d8;
523
+ white-space: pre-wrap;
524
+ margin: 0;
525
+ }
526
+ .panel-header {
527
+ display: flex;
528
+ justify-content: space-between;
529
+ align-items: center;
530
+ gap: 12px;
531
+ padding: 14px 16px;
532
+ border-bottom: 1px solid var(--line-soft);
533
+ }
534
+ .panel-title {
535
+ color: var(--text);
536
+ font-size: 0.8rem;
537
+ letter-spacing: 0.1em;
538
+ text-transform: uppercase;
539
+ font-weight: 800;
540
+ }
541
+ .panel-kicker {
542
+ color: var(--faint);
543
+ font-size: 0.72rem;
544
+ font-family: 'JetBrains Mono', monospace;
545
+ text-transform: uppercase;
546
+ }
547
+ .panel-body {
548
+ padding: 16px;
549
+ }
550
+ .info-block {
551
+ font-family: 'JetBrains Mono', monospace;
552
+ font-size: 0.8rem;
553
+ color: #d7dcff;
554
+ white-space: pre-wrap;
555
+ margin: 0;
556
+ }
557
+ .metadata-list {
558
+ display: grid;
559
+ gap: 12px;
560
+ margin: 0;
561
+ }
562
+ .metadata-row {
563
+ display: flex;
564
+ justify-content: space-between;
565
+ gap: 16px;
566
+ border-bottom: 1px solid var(--line-soft);
567
+ padding-bottom: 10px;
568
+ }
569
+ .metadata-row:last-child {
570
+ border-bottom: 0;
571
+ padding-bottom: 0;
572
+ }
573
+ .metadata-row dt {
574
+ color: var(--faint);
575
+ margin: 0;
576
+ font-size: 0.74rem;
577
+ text-transform: uppercase;
578
+ letter-spacing: 0.08em;
579
+ font-weight: 800;
580
+ }
581
+ .metadata-row dd {
582
+ margin: 0;
583
+ color: var(--text);
584
+ font-family: 'JetBrains Mono', monospace;
585
+ font-size: 0.8rem;
586
+ text-align: right;
587
+ }
588
+
589
+ .steps-section { margin-top: 0; }
590
+ .section-title {
591
+ display: block;
592
+ padding: 15px 18px;
593
+ border-bottom: 1px solid var(--line-soft);
594
+ color: var(--muted);
595
+ font-size: 0.76rem;
596
+ text-transform: uppercase;
597
+ letter-spacing: 0.12em;
598
+ font-weight: 800;
599
+ }
600
+ .timeline-wrap {
601
+ overflow: hidden;
602
+ }
603
+ .steps-container {
604
+ padding: 12px 0;
605
+ }
606
+ .step-wrapper {
607
+ margin-left: calc(var(--step-depth) * 18px);
608
+ padding: 7px 18px 7px 20px;
609
+ position: relative;
610
+ }
611
+ .step-wrapper::before {
612
+ content: "";
613
+ position: absolute;
614
+ left: 5px;
615
+ top: 0;
616
+ bottom: 0;
617
+ width: 1px;
618
+ background: var(--line-soft);
619
+ }
620
+ .step-header {
621
+ display: grid;
622
+ grid-template-columns: 14px minmax(0, 1fr) auto;
623
+ align-items: start;
624
+ gap: 10px;
625
+ font-size: 0.9rem;
626
+ }
627
+ .step-marker {
628
+ width: 8px;
629
+ height: 8px;
630
+ border-radius: 50%;
631
+ background: var(--success);
632
+ margin-top: 7px;
633
+ box-shadow: 0 0 0 4px rgba(57,217,159,0.08);
634
+ }
635
+ .step-title {
636
+ color: #e6e9f5;
637
+ min-width: 0;
638
+ overflow-wrap: anywhere;
639
+ }
640
+ .step-action {
641
+ color: var(--blue);
642
+ font-weight: 800;
643
+ }
644
+ .step-duration {
645
+ color: var(--faint);
646
+ font-size: 0.72rem;
647
+ font-family: 'JetBrains Mono', monospace;
648
+ white-space: nowrap;
649
+ padding-top: 2px;
650
+ }
651
+ .step-error .step-title { color: #ffd0d8; }
652
+ .step-error .step-marker {
653
+ background: var(--error);
654
+ box-shadow: 0 0 0 4px rgba(255,95,122,0.09);
655
+ }
656
+ .step-children {
657
+ margin-top: 2px;
658
+ }
659
+
660
+ .evidence-hero {
661
+ position: relative;
662
+ overflow: hidden;
663
+ background: #080910;
664
+ }
665
+ .evidence-hero img,
666
+ .evidence-hero video {
667
+ width: 100%;
668
+ max-height: 320px;
669
+ object-fit: contain;
670
+ display: block;
671
+ cursor: zoom-in;
672
+ }
673
+ .evidence-link,
674
+ .evidence-thumb {
675
+ text-decoration: none;
676
+ }
677
+ .evidence-link {
678
+ display: flex;
679
+ justify-content: space-between;
680
+ gap: 12px;
681
+ padding: 11px 13px;
682
+ color: var(--muted);
683
+ border-top: 1px solid var(--line-soft);
684
+ font-size: 0.78rem;
685
+ font-family: 'JetBrains Mono', monospace;
686
+ }
687
+ .evidence-link:hover,
688
+ .evidence-thumb:hover {
689
+ color: var(--text);
690
+ }
691
+ .evidence-thumbs {
692
+ display: grid;
693
+ grid-template-columns: repeat(2, minmax(0, 1fr));
694
+ gap: 10px;
695
+ padding: 12px;
696
+ border-top: 1px solid var(--line-soft);
697
+ }
698
+ .evidence-thumb {
699
+ border: 1px solid var(--line-soft);
700
+ border-radius: 8px;
701
+ overflow: hidden;
702
+ color: var(--muted);
703
+ background: rgba(255,255,255,0.03);
704
+ font-size: 0.72rem;
705
+ }
706
+ .evidence-thumb img,
707
+ .evidence-thumb video {
708
+ width: 100%;
709
+ aspect-ratio: 16 / 9;
710
+ object-fit: cover;
711
+ display: block;
712
+ background: #07080d;
713
+ }
714
+ .thumb-name {
715
+ display: block;
716
+ padding: 8px;
717
+ white-space: nowrap;
718
+ overflow: hidden;
719
+ text-overflow: ellipsis;
720
+ }
721
+
722
+ footer {
723
+ margin-top: 34px;
724
+ text-align: center;
725
+ padding: 22px 0 6px;
726
+ font-size: 0.76rem;
727
+ color: var(--faint);
728
+ font-family: 'JetBrains Mono', monospace;
729
+ }
730
+
731
+ .tag {
732
+ display: inline-flex;
733
+ align-items: center;
734
+ min-height: 22px;
735
+ padding: 3px 8px;
736
+ border-radius: 999px;
737
+ border: 1px solid var(--line-soft);
738
+ background: rgba(255,255,255,0.035);
739
+ color: var(--muted);
740
+ font-size: 0.68rem;
741
+ font-weight: 800;
742
+ text-transform: uppercase;
743
+ letter-spacing: 0.06em;
744
+ }
745
+ .tag-project {
746
+ color: #e2c7ff;
747
+ background: var(--brand-soft);
748
+ border-color: rgba(181,108,255,0.26);
749
+ }
750
+ .tag-retry {
751
+ color: #ffe2a6;
752
+ background: var(--warning-soft);
753
+ border-color: rgba(255,200,87,0.28);
754
+ }
755
+ .tag-status-passed {
756
+ color: #bff7df;
757
+ background: var(--success-soft);
758
+ border-color: rgba(57,217,159,0.28);
759
+ }
760
+ .tag-status-failed {
761
+ color: #ffd0d8;
762
+ background: var(--error-soft);
763
+ border-color: rgba(255,95,122,0.28);
764
+ }
765
+ .tag-status-neutral {
766
+ color: var(--muted);
767
+ }
768
+
769
+ .security-section {
770
+ overflow: hidden;
771
+ }
772
+ .security-header {
773
+ width: 100%;
774
+ border: 0;
775
+ background: transparent;
776
+ color: var(--text);
777
+ cursor: pointer;
778
+ }
779
+ .security-risk-badge {
780
+ padding: 5px 9px;
781
+ border-radius: 999px;
782
+ font-size: 0.72rem;
783
+ font-weight: 800;
784
+ color: #07080d;
785
+ }
786
+ .security-summary {
787
+ display: grid;
788
+ grid-template-columns: repeat(4, 1fr);
789
+ gap: 1px;
790
+ background: var(--line-soft);
791
+ text-align: center;
792
+ border-bottom: 1px solid var(--line-soft);
793
+ }
794
+ .summary-item {
795
+ background: rgba(7,8,13,0.48);
796
+ padding: 12px 8px;
797
+ }
798
+ .summary-item .count {
799
+ font-size: 1.2rem;
800
+ font-weight: 800;
801
+ }
802
+ .summary-item .label {
803
+ font-size: 0.66rem;
804
+ text-transform: uppercase;
805
+ color: var(--faint);
806
+ letter-spacing: 0.08em;
807
+ }
808
+ .count-Critical { color: var(--error); }
809
+ .count-High { color: #ff9d66; }
810
+ .count-Medium { color: var(--warning); }
811
+ .count-Low { color: var(--blue); }
812
+
813
+ .vuln-list {
814
+ display: flex;
815
+ flex-direction: column;
816
+ gap: 12px;
817
+ padding: 14px;
818
+ }
819
+ .vuln-card {
820
+ background: rgba(7,8,13,0.52);
821
+ border: 1px solid var(--line-soft);
822
+ border-left-width: 4px;
823
+ border-radius: 8px;
824
+ padding: 14px;
825
+ }
826
+ .vuln-card-title {
827
+ font-weight: 800;
828
+ margin-bottom: 8px;
829
+ display: flex;
830
+ align-items: center;
831
+ gap: 10px;
832
+ flex-wrap: wrap;
833
+ }
834
+ .vuln-card-desc {
835
+ color: var(--muted);
836
+ font-size: 0.88rem;
837
+ margin: 10px 0;
838
+ }
839
+ .vuln-card-evidence {
840
+ background: #07080d;
841
+ padding: 10px;
842
+ border-radius: 4px;
843
+ font-family: 'JetBrains Mono', monospace;
844
+ font-size: 0.74rem;
845
+ white-space: pre-wrap;
846
+ word-break: break-all;
847
+ margin: 10px 0;
848
+ border: 1px solid var(--line-soft);
849
+ max-height: 200px;
850
+ overflow-y: auto;
851
+ }
852
+ .vuln-card-remediation {
853
+ font-size: 0.88rem;
854
+ color: #bff7df;
855
+ }
856
+ pre { margin: 0; }
857
+
858
+ @media (max-width: 1040px) {
859
+ .mission-brief,
860
+ .test-details-grid {
861
+ grid-template-columns: 1fr;
862
+ }
863
+ .status-card {
864
+ width: 100%;
865
+ }
866
+ .brief-grid {
867
+ grid-template-columns: repeat(3, minmax(0, 1fr));
868
+ }
869
+ }
870
+ @media (max-width: 680px) {
871
+ .container {
872
+ padding: 20px;
873
+ }
874
+ .mission-title {
875
+ font-size: 2.3rem;
876
+ }
877
+ .brief-grid {
878
+ grid-template-columns: repeat(2, minmax(0, 1fr));
879
+ }
880
+ .test-header {
881
+ align-items: flex-start;
882
+ }
883
+ .test-details-grid {
884
+ gap: 16px;
885
+ }
886
+ .step-header {
887
+ grid-template-columns: 14px minmax(0, 1fr);
888
+ }
889
+ .step-duration {
890
+ grid-column: 2;
891
+ justify-self: start;
892
+ padding-top: 0;
893
+ }
894
+ .evidence-thumbs,
895
+ .security-summary {
896
+ grid-template-columns: 1fr 1fr;
897
+ }
898
+ }
899
+ </style>
900
+ </head>
901
+ <body>
902
+ <div class="container">
903
+ <header class="mission-brief">
904
+ <div>
905
+ <div class="brand-row">
906
+ <span class="brand-mark ${brandLogo ? 'has-logo' : ''}" aria-hidden="true">
907
+ ${brandLogo ? `<img src="${brandLogo}" alt="">` : ''}
908
+ </span>
909
+ <div>
910
+ <div class="brand-name">Arcality QA Engine</div>
911
+ <div class="brand-subtitle">Consola de diagnostico autonomo</div>
912
+ </div>
913
+ </div>
914
+ <h1 class="mission-title">${reportTitle}</h1>
915
+ <p class="mission-copy">${reportSummary}</p>
916
+ </div>
917
+ <div class="status-card ${reportTone}">
918
+ <div class="status-eyebrow">
919
+ <span>Estado general</span>
920
+ <span class="status-pill">${reportStatus}</span>
921
+ </div>
922
+ <div class="status-title">${reportStatus}</div>
923
+ <p class="status-summary">Inicio ${this.startTime.toLocaleString()} | Engine ${this.getEngineVersion()}</p>
924
+ </div>
925
+ </header>
926
+
927
+ <div class="brief-grid" aria-label="Resumen de ejecucion">
928
+ <div class="brief-metric"><span class="metric-value">${stats.total}</span><span class="metric-label">Misiones</span></div>
929
+ <div class="brief-metric"><span class="metric-value val-passed">${stats.passed}</span><span class="metric-label">Exitosas</span></div>
930
+ <div class="brief-metric"><span class="metric-value val-failed">${stats.failed}</span><span class="metric-label">Errores</span></div>
931
+ <div class="brief-metric"><span class="metric-value val-flaky">${stats.flaky}</span><span class="metric-label">Reintentos</span></div>
932
+ <div class="brief-metric"><span class="metric-value val-blue">${evidenceCount}</span><span class="metric-label">Evidencias</span></div>
933
+ <div class="brief-metric"><span class="metric-value">${this.formatDuration(this.totalDuration)}</span><span class="metric-label">Duracion</span></div>
934
+ </div>
935
+
936
+ <div class="test-list">
937
+ ${this.results.map((r, i) => {
938
+ const tone = this.getStatusTone(r.result.status);
939
+ const statusClass = `status-${tone}`;
940
+ const projectName = r.test.parent.project()?.name || 'default';
941
+ const isOpen = this.results.length === 1 || tone === 'failed' || i === 0;
942
+ const successSummary = r.attachments.find(a => a.name === 'success_summary');
943
+ const successText = this.getAttachmentText(successSummary);
944
+ const evidenceHtml = this.renderVisualEvidence(r.attachments);
945
+ const contextHtml = this.renderTextAttachmentSection(r.attachments, 'qa_context_summary', 'Contexto QA aplicado');
946
+ const securityHtml = this._renderSecuritySection(r.attachments, i);
947
+ return `
948
+ <div class="test-entry ${statusClass}">
949
+ <button class="test-header" type="button" onclick="toggleDetails('details-${i}', this)" aria-expanded="${isOpen ? 'true' : 'false'}">
950
+ <div class="test-info">
951
+ <span class="status-dot"></span>
952
+ <div>
953
+ <div class="test-name">${this.escapeHtml(r.test.title)}</div>
954
+ <div class="test-meta">
955
+ <span class="tag tag-status-${tone}">${this.getStatusLabel(r.result.status)}</span>
956
+ <span class="tag tag-project">${this.escapeHtml(projectName)}</span>
957
+ ${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
958
+ <span class="tag">${this.formatDuration(r.result.duration)}</span>
959
+ </div>
960
+ </div>
961
+ </div>
962
+ <span class="test-chevron" aria-hidden="true">v</span>
963
+ </button>
964
+
965
+ <div class="test-details" id="details-${i}" style="display: ${isOpen ? 'block' : 'none'};">
966
+ <div class="test-details-grid">
967
+ <main class="primary-column">
968
+ ${r.result.status === 'passed' && successText ? `
969
+ <div class="outcome-block success">
970
+ <span class="outcome-label">Resultado</span>
971
+ <p class="success-block">${this.escapeHtml(successText)}</p>
972
+ </div>
973
+ ` : ''}
974
+
975
+ ${r.result.error ? `
976
+ <div class="outcome-block error">
977
+ <span class="outcome-label">Diagnostico del bloqueo</span>
978
+ <pre class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</pre>
979
+ </div>
980
+ ` : ''}
981
+
982
+ <section class="steps-section timeline-wrap">
983
+ <span class="section-title">Proceso cognitivo del agente</span>
984
+ <div class="steps-container">
985
+ ${r.stepsHtml || '<p style="opacity:0.5; padding: 0 18px;">No hay pasos registrados.</p>'}
986
+ </div>
987
+ </section>
988
+ </main>
989
+
990
+ <aside class="context-column">
991
+ ${evidenceHtml}
992
+ ${contextHtml}
993
+ ${securityHtml}
994
+ <section class="side-panel">
995
+ <div class="panel-header">
996
+ <span class="panel-title">Metadata</span>
997
+ </div>
998
+ <div class="panel-body">
999
+ <dl class="metadata-list">
1000
+ <div class="metadata-row"><dt>Proyecto</dt><dd>${this.escapeHtml(projectName)}</dd></div>
1001
+ <div class="metadata-row"><dt>Estado</dt><dd>${this.getStatusLabel(r.result.status)}</dd></div>
1002
+ <div class="metadata-row"><dt>Duracion</dt><dd>${this.formatDuration(r.result.duration)}</dd></div>
1003
+ <div class="metadata-row"><dt>Retry</dt><dd>${r.result.retry}</dd></div>
1004
+ </dl>
1005
+ </div>
1006
+ </section>
1007
+ </aside>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+ `;
1012
+ }).join('')}
1013
+ </div>
1014
+
1015
+ <footer>
1016
+ ARCALITY ENGINE v${this.getEngineVersion()} | ${generatedAt.toISOString()} | GENERATED FOR ARCADIAL
1017
+ </footer>
1018
+ </div>
1019
+
1020
+ <script>
1021
+ function toggleDetails(id, trigger) {
1022
+ const el = document.getElementById(id);
1023
+ if (!el) return;
1024
+ const isHidden = el.style.display === 'none' || el.style.display === '';
1025
+ el.style.display = isHidden ? 'block' : 'none';
1026
+ if (trigger) trigger.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
1027
+ }
1028
+ </script>
1029
+ </body>
1030
+ </html>
1031
+ `;
1032
+ }
1033
+ renderVisualEvidence(attachments) {
1034
+ const media = attachments.filter(att => att.path && (att.contentType.startsWith('image/') || att.contentType.startsWith('video/')));
1035
+ if (media.length === 0)
1036
+ return '';
1037
+ const primary = media[0];
1038
+ const primaryPath = this.escapeHtml(primary.path);
1039
+ const primaryName = this.escapeHtml(primary.name);
1040
+ const primaryMedia = primary.contentType.startsWith('image/')
1041
+ ? `<img src="${primaryPath}" alt="${primaryName}" onclick="window.open(this.src)">`
1042
+ : `<video controls src="${primaryPath}"></video>`;
1043
+ return `
1044
+ <section class="side-panel evidence-panel">
1045
+ <div class="panel-header">
1046
+ <span class="panel-title">Evidencia visual</span>
1047
+ <span class="panel-kicker">${media.length} archivo${media.length === 1 ? '' : 's'}</span>
1048
+ </div>
1049
+ <div class="evidence-hero">
1050
+ ${primaryMedia}
1051
+ <a class="evidence-link" href="${primaryPath}" target="_blank" rel="noreferrer">
1052
+ <span>${primaryName}</span>
1053
+ <span>Abrir</span>
1054
+ </a>
1055
+ </div>
1056
+ ${media.length > 1 ? `
1057
+ <div class="evidence-thumbs">
1058
+ ${media.map(att => {
1059
+ const mediaPath = this.escapeHtml(att.path);
1060
+ const mediaName = this.escapeHtml(att.name);
1061
+ const thumb = att.contentType.startsWith('image/')
1062
+ ? `<img src="${mediaPath}" alt="${mediaName}">`
1063
+ : `<video src="${mediaPath}"></video>`;
1064
+ return `
1065
+ <a class="evidence-thumb" href="${mediaPath}" target="_blank" rel="noreferrer">
1066
+ ${thumb}
1067
+ <span class="thumb-name">${mediaName}</span>
1068
+ </a>
1069
+ `;
1070
+ }).join('')}
1071
+ </div>
1072
+ ` : ''}
1073
+ </section>
1074
+ `;
1075
+ }
1076
+ getAttachmentText(att) {
1077
+ if (!att)
1078
+ return null;
1079
+ if (att.body)
1080
+ return att.body.toString();
1081
+ if (att.path) {
1082
+ const actualPath = path_1.default.isAbsolute(att.path) ? att.path : path_1.default.join(this.outputDir, att.path);
1083
+ if (fs_1.default.existsSync(actualPath)) {
1084
+ return fs_1.default.readFileSync(actualPath, 'utf8');
1085
+ }
1086
+ }
1087
+ return null;
1088
+ }
1089
+ renderTextAttachmentSection(attachments, attachmentName, title) {
1090
+ const attachment = attachments.find(att => att.name === attachmentName);
1091
+ const text = this.getAttachmentText(attachment);
1092
+ if (!text)
1093
+ return '';
1094
+ return `
1095
+ <section class="side-panel">
1096
+ <div class="panel-header">
1097
+ <span class="panel-title">${this.escapeHtml(title)}</span>
1098
+ </div>
1099
+ <div class="panel-body">
1100
+ <pre class="info-block">${this.escapeHtml(text)}</pre>
1101
+ </div>
1102
+ </section>
1103
+ `;
1104
+ }
1105
+ _renderSecuritySection(attachments, index) {
1106
+ let vulnerabilities = [];
1107
+ const reportAtt = attachments.find(a => a.name === 'security_report');
1108
+ if (reportAtt) {
1109
+ const text = this.getAttachmentText(reportAtt);
1110
+ if (text) {
1111
+ try {
1112
+ const parsed = JSON.parse(text);
1113
+ if (Array.isArray(parsed))
1114
+ vulnerabilities.push(...parsed);
1115
+ }
1116
+ catch (e) { }
1117
+ }
1118
+ }
1119
+ const findingAtts = attachments.filter(a => a.name === 'security_finding');
1120
+ for (const findingAtt of findingAtts) {
1121
+ const text = this.getAttachmentText(findingAtt);
1122
+ if (text) {
1123
+ try {
1124
+ const parsed = JSON.parse(text);
1125
+ vulnerabilities.push(parsed);
1126
+ }
1127
+ catch (e) { }
1128
+ }
1129
+ }
1130
+ if (vulnerabilities.length === 0)
1131
+ return '';
1132
+ const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, 'Info': 4 };
1133
+ const severityCounts = { 'Critical': 0, 'High': 0, 'Medium': 0, 'Low': 0, 'Info': 0 };
1134
+ let worstSeverity = 'Info';
1135
+ const normalizeSeverity = (value) => {
1136
+ const severity = String(value || 'Info');
1137
+ return severityCounts[severity] !== undefined ? severity : 'Info';
1138
+ };
1139
+ vulnerabilities.forEach(v => {
1140
+ const severity = normalizeSeverity(v.severity);
1141
+ severityCounts[severity]++;
1142
+ if (severityOrder[severity] < severityOrder[worstSeverity]) {
1143
+ worstSeverity = severity;
1144
+ }
1145
+ });
1146
+ const getSeverityColor = (sev) => {
1147
+ switch (sev) {
1148
+ case 'Critical': return 'var(--error)';
1149
+ case 'High': return '#ff9d66';
1150
+ case 'Medium': return 'var(--warning)';
1151
+ case 'Low': return 'var(--blue)';
1152
+ case 'Info': return 'var(--faint)';
1153
+ }
1154
+ };
1155
+ const riskColor = getSeverityColor(worstSeverity);
1156
+ return `
1157
+ <section class="side-panel security-section">
1158
+ <button type="button" class="security-header panel-header" onclick="toggleDetails('security-details-${index}', this)" aria-expanded="false">
1159
+ <span class="panel-title">Evaluacion de seguridad</span>
1160
+ <span class="security-risk-badge" style="background-color: ${riskColor};">${worstSeverity}</span>
1161
+ </button>
1162
+ <div class="security-summary">
1163
+ <div class="summary-item">
1164
+ <div class="count count-Critical">${severityCounts.Critical}</div>
1165
+ <div class="label">Critical</div>
1166
+ </div>
1167
+ <div class="summary-item">
1168
+ <div class="count count-High">${severityCounts.High}</div>
1169
+ <div class="label">High</div>
1170
+ </div>
1171
+ <div class="summary-item">
1172
+ <div class="count count-Medium">${severityCounts.Medium}</div>
1173
+ <div class="label">Medium</div>
1174
+ </div>
1175
+ <div class="summary-item">
1176
+ <div class="count count-Low">${severityCounts.Low}</div>
1177
+ <div class="label">Low</div>
1178
+ </div>
1179
+ </div>
1180
+ <div class="security-details" id="security-details-${index}" style="display: none;">
1181
+ <div class="vuln-list">
1182
+ ${vulnerabilities.map(v => {
1183
+ const sev = normalizeSeverity(v.severity);
1184
+ const cardColor = getSeverityColor(sev);
1185
+ return `
1186
+ <div class="vuln-card" style="border-left-color: ${cardColor};">
1187
+ <div class="vuln-card-title">
1188
+ <span class="tag" style="background-color: ${cardColor}20; color: ${cardColor};">${sev}</span>
1189
+ <span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7">- ${this.escapeHtml(v.subcategory)}</span>` : ''}</span>
1190
+ </div>
1191
+ <div style="font-size: 0.78rem; opacity: 0.82; margin: 6px 0 10px; display: flex; flex-wrap: wrap; gap: 10px;">
1192
+ ${v.type ? `<span><strong style="color:var(--brand);">Type:</strong> ${this.escapeHtml(v.type)}</span>` : ''}
1193
+ ${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` : ''}
1194
+ ${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` : ''}
1195
+ </div>
1196
+ <p class="vuln-card-desc">${this.escapeHtml(v.description)}</p>
1197
+ ${v.impact ? `<p class="vuln-card-desc" style="color:#ffd89a;"><strong>Impact:</strong> ${this.escapeHtml(v.impact)}</p>` : ''}
1198
+
1199
+ <div class="vuln-card-evidence">
1200
+ <strong>Evidence:</strong>
1201
+ <pre>${this.escapeHtml(JSON.stringify(v.evidence, null, 2))}</pre>
1202
+ </div>
1203
+
1204
+ ${v.reproductionSteps && v.reproductionSteps.length > 0 ? `
1205
+ <div style="margin: 10px 0; background: rgba(0,0,0,0.18); padding: 10px; border-radius: 6px; border-left: 2px solid ${cardColor}80;">
1206
+ <strong style="font-size: 0.82rem; color: var(--muted);">Reproduction Steps:</strong>
1207
+ <ol style="margin: 5px 0 0 20px; font-size: 0.82rem; color: var(--text);">
1208
+ ${v.reproductionSteps.map((s) => `<li>${this.escapeHtml(s)}</li>`).join('')}
1209
+ </ol>
1210
+ </div>` : ''}
1211
+
1212
+ <div class="vuln-card-remediation">
1213
+ <strong>Remediation:</strong> ${this.escapeHtml(v.remediation)}
1214
+ </div>
1215
+
1216
+ ${v.classification ? `
1217
+ <div style="font-size: 0.72rem; opacity: 0.55; margin-top: 12px; display: flex; gap: 15px; font-family: 'JetBrains Mono', monospace; flex-wrap: wrap;">
1218
+ ${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` : ''}
1219
+ ${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` : ''}
1220
+ </div>` : ''}
1221
+ </div>
1222
+ `;
1223
+ }).join('')}
1224
+ </div>
1225
+ </div>
1226
+ </section>
1227
+ `;
1228
+ }
1229
+ escapeHtml(unsafe) {
1230
+ if (!unsafe)
1231
+ return '';
1232
+ return unsafe
1233
+ .replace(/&/g, "&amp;")
1234
+ .replace(/</g, "&lt;")
1235
+ .replace(/>/g, "&gt;")
1236
+ .replace(/"/g, "&quot;")
1237
+ .replace(/'/g, "&#039;");
1238
+ }
1239
+ }
1240
+ exports.default = ArcalityReporter;