test-reporting 4.0.0__tar.gz → 4.0.2__tar.gz

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.
Files changed (29) hide show
  1. {test_reporting-4.0.0 → test_reporting-4.0.2}/PKG-INFO +3 -3
  2. {test_reporting-4.0.0 → test_reporting-4.0.2}/README.md +2 -2
  3. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/config.py +0 -2
  4. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/report_server.py +1 -8
  5. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/index.html +0 -12
  6. test_reporting-4.0.2/reporting/templates/monthly-reports.html +661 -0
  7. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/report-generator.html +2 -48
  8. {test_reporting-4.0.0 → test_reporting-4.0.2}/setup.py +1 -1
  9. {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/PKG-INFO +3 -3
  10. {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/SOURCES.txt +1 -0
  11. {test_reporting-4.0.0 → test_reporting-4.0.2}/LICENSE +0 -0
  12. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/__init__.py +0 -0
  13. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/classifier.py +0 -0
  14. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/cli.py +0 -0
  15. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/__init__.py +0 -0
  16. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/pdf_exporter.py +0 -0
  17. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/pptx_exporter.py +0 -0
  18. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/exporters/xlsx_exporter.py +0 -0
  19. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/monthly_report.py +0 -0
  20. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/plugin.py +0 -0
  21. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/publisher.py +0 -0
  22. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/storage.py +0 -0
  23. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/project.html +0 -0
  24. {test_reporting-4.0.0 → test_reporting-4.0.2}/reporting/templates/run.html +0 -0
  25. {test_reporting-4.0.0 → test_reporting-4.0.2}/setup.cfg +0 -0
  26. {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/dependency_links.txt +0 -0
  27. {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/entry_points.txt +0 -0
  28. {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/requires.txt +0 -0
  29. {test_reporting-4.0.0 → test_reporting-4.0.2}/test_reporting.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-reporting
3
- Version: 4.0.0
3
+ Version: 4.0.2
4
4
  Summary: Multi-project test reporting dashboard — collect results locally or push to S3
5
5
  Home-page: https://github.com/amahdy77/test-reporting.git
6
6
  Author: Ashfaqur Mahdy
@@ -317,7 +317,7 @@ retention_days = 90 # Keep 90 days of artifacts and data
317
317
 
318
318
  **`run.html?r=<id>&p=<name>`** — Run detail. Test file breakdown, every failing test with its error message, screenshot and trace links.
319
319
 
320
- **`report-generator.html?key=<admin-key>`** — Monthly report generator (admin only). Generate comprehensive reports in PowerPoint, Excel, or PDF format.
320
+ **`monthly-reports.html`** — Monthly report generator. Generate comprehensive reports in PowerPoint, Excel, or PDF format. (Obscure URL, not linked from main pages)
321
321
 
322
322
  ---
323
323
 
@@ -336,7 +336,7 @@ test-report monthly-report --format pptx,xlsx,pdf
336
336
 
337
337
  # Or use the UI
338
338
  test-report serve
339
- # Then open: http://localhost:8000/report-generator.html?key=your-admin-key
339
+ # Then open: http://localhost:5555/monthly-reports.html
340
340
  ```
341
341
 
342
342
  ### Features
@@ -274,7 +274,7 @@ retention_days = 90 # Keep 90 days of artifacts and data
274
274
 
275
275
  **`run.html?r=<id>&p=<name>`** — Run detail. Test file breakdown, every failing test with its error message, screenshot and trace links.
276
276
 
277
- **`report-generator.html?key=<admin-key>`** — Monthly report generator (admin only). Generate comprehensive reports in PowerPoint, Excel, or PDF format.
277
+ **`monthly-reports.html`** — Monthly report generator. Generate comprehensive reports in PowerPoint, Excel, or PDF format. (Obscure URL, not linked from main pages)
278
278
 
279
279
  ---
280
280
 
@@ -293,7 +293,7 @@ test-report monthly-report --format pptx,xlsx,pdf
293
293
 
294
294
  # Or use the UI
295
295
  test-report serve
296
- # Then open: http://localhost:8000/report-generator.html?key=your-admin-key
296
+ # Then open: http://localhost:5555/monthly-reports.html
297
297
  ```
298
298
 
299
299
  ### Features
@@ -57,7 +57,6 @@ class ReportingConfig:
57
57
 
58
58
  # Monthly report settings
59
59
  monthly_section = self.config['monthly_report'] if 'monthly_report' in self.config else {}
60
- self.ADMIN_KEY = monthly_section.get('admin_key', 'change-me-in-production')
61
60
  self.REPORT_PERIOD_DAYS = int(monthly_section.get('report_period_days', 30))
62
61
  self.REPORT_TOP_N = int(monthly_section.get('top_n_items', 10))
63
62
 
@@ -88,7 +87,6 @@ class ReportingConfig:
88
87
  self.REGRESSION_TAG = os.getenv('REGRESSION_TAG') or None
89
88
 
90
89
  # Monthly report defaults
91
- self.ADMIN_KEY = os.getenv('ADMIN_KEY', 'change-me-in-production')
92
90
  self.REPORT_PERIOD_DAYS = 30
93
91
  self.REPORT_TOP_N = 10
94
92
 
@@ -56,13 +56,6 @@ def create_app(config=None):
56
56
  def generate_report():
57
57
  """Generate monthly report based on parameters."""
58
58
  try:
59
- # Validate admin key
60
- admin_key = request.headers.get('X-Admin-Key')
61
- expected_key = getattr(config, 'ADMIN_KEY', 'change-me-in-production')
62
-
63
- if admin_key != expected_key:
64
- return jsonify({'error': 'Unauthorized'}), 401
65
-
66
59
  # Parse parameters
67
60
  params = request.json
68
61
  start_date = params.get('start_date')
@@ -132,7 +125,7 @@ def run_server(host='127.0.0.1', port=5555, debug=False):
132
125
  app = create_app(config)
133
126
 
134
127
  print(f"[Report Server] Starting on http://{host}:{port}")
135
- print(f"[Report Server] Admin key: {getattr(config, 'ADMIN_KEY', 'change-me-in-production')}")
128
+ print(f"[Report Server] Report generator: http://{host}:{port}/monthly-reports")
136
129
  print(f"[Report Server] Press Ctrl+C to stop")
137
130
 
138
131
  app.run(host=host, port=port, debug=debug)
@@ -502,18 +502,6 @@
502
502
  '<p>Make sure <code>test-report deploy</code> and <code>test-report publish</code> have been run.</p>' +
503
503
  '</div>';
504
504
  });
505
-
506
- /* ── Admin section ── */
507
- const adminSection = document.createElement('div');
508
- adminSection.style.cssText = 'margin-top: 50px; padding: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); text-align: center;';
509
- adminSection.innerHTML =
510
- '<h3 style="font-size: 16px; margin-bottom: 16px; color: var(--muted);">📊 Admin Tools</h3>' +
511
- '<a href="report-generator.html?key=change-me-in-production" style="display: inline-block; padding: 12px 24px; background: var(--blue); color: white; text-decoration: none; border-radius: 6px; font-weight: 600; transition: all 0.2s;" onmouseover="this.style.background=\'#2c5aa0\'" onmouseout="this.style.background=\'var(--blue)\'">' +
512
- 'Generate Monthly Report' +
513
- '</a>' +
514
- '<p style="color: var(--muted); font-size: 12px; margin-top: 12px;">Admin access required</p>';
515
-
516
- document.querySelector('.page').appendChild(adminSection);
517
505
  </script>
518
506
  </body>
519
507
  </html>
@@ -0,0 +1,661 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Monthly Report Generator - Admin</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: #f4f6f9;
12
+ --surface: #ffffff;
13
+ --border: #e2e8f0;
14
+ --text: #1a202c;
15
+ --muted: #718096;
16
+ --green: #38a169;
17
+ --green-bg: #f0fff4;
18
+ --yellow: #d69e2e;
19
+ --yellow-bg: #fffff0;
20
+ --red: #e53e3e;
21
+ --red-bg: #fff5f5;
22
+ --blue: #3182ce;
23
+ --blue-bg: #ebf8ff;
24
+ --radius: 10px;
25
+ --shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
26
+ }
27
+
28
+ body {
29
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
30
+ background: var(--bg);
31
+ color: var(--text);
32
+ min-height: 100vh;
33
+ padding: 20px;
34
+ }
35
+
36
+ .container {
37
+ max-width: 800px;
38
+ margin: 0 auto;
39
+ background: var(--surface);
40
+ border-radius: var(--radius);
41
+ box-shadow: var(--shadow);
42
+ padding: 40px;
43
+ }
44
+
45
+ .header {
46
+ text-align: center;
47
+ margin-bottom: 40px;
48
+ padding-bottom: 20px;
49
+ border-bottom: 2px solid var(--border);
50
+ }
51
+
52
+ .header h1 {
53
+ font-size: 28px;
54
+ font-weight: 700;
55
+ margin-bottom: 8px;
56
+ color: var(--text);
57
+ }
58
+
59
+ .header .subtitle {
60
+ font-size: 14px;
61
+ color: var(--muted);
62
+ }
63
+
64
+ .admin-badge {
65
+ display: inline-block;
66
+ background: var(--blue-bg);
67
+ color: var(--blue);
68
+ padding: 4px 12px;
69
+ border-radius: 12px;
70
+ font-size: 12px;
71
+ font-weight: 600;
72
+ margin-top: 8px;
73
+ }
74
+
75
+ .section {
76
+ margin-bottom: 32px;
77
+ }
78
+
79
+ .section-title {
80
+ font-size: 16px;
81
+ font-weight: 600;
82
+ margin-bottom: 16px;
83
+ color: var(--text);
84
+ }
85
+
86
+ .form-group {
87
+ margin-bottom: 20px;
88
+ }
89
+
90
+ label {
91
+ display: block;
92
+ font-size: 14px;
93
+ font-weight: 500;
94
+ margin-bottom: 8px;
95
+ color: var(--text);
96
+ }
97
+
98
+ input[type="text"],
99
+ input[type="date"],
100
+ select {
101
+ width: 100%;
102
+ padding: 10px 12px;
103
+ border: 1px solid var(--border);
104
+ border-radius: 6px;
105
+ font-size: 14px;
106
+ background: var(--surface);
107
+ color: var(--text);
108
+ transition: border-color 0.2s;
109
+ }
110
+
111
+ input[type="text"]:focus,
112
+ input[type="date"]:focus,
113
+ select:focus {
114
+ outline: none;
115
+ border-color: var(--blue);
116
+ }
117
+
118
+ .radio-group {
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 10px;
122
+ }
123
+
124
+ .radio-option {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 8px;
128
+ }
129
+
130
+ .radio-option input[type="radio"] {
131
+ width: 18px;
132
+ height: 18px;
133
+ cursor: pointer;
134
+ }
135
+
136
+ .radio-option label {
137
+ margin: 0;
138
+ cursor: pointer;
139
+ font-weight: 400;
140
+ }
141
+
142
+ .date-range {
143
+ display: grid;
144
+ grid-template-columns: 1fr 1fr;
145
+ gap: 16px;
146
+ }
147
+
148
+ .checkbox-group {
149
+ display: grid;
150
+ grid-template-columns: repeat(2, 1fr);
151
+ gap: 12px;
152
+ }
153
+
154
+ .checkbox-option {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 8px;
158
+ }
159
+
160
+ .checkbox-option input[type="checkbox"] {
161
+ width: 18px;
162
+ height: 18px;
163
+ cursor: pointer;
164
+ }
165
+
166
+ .checkbox-option label {
167
+ margin: 0;
168
+ cursor: pointer;
169
+ font-weight: 400;
170
+ font-size: 14px;
171
+ }
172
+
173
+ .advanced-options {
174
+ background: var(--bg);
175
+ padding: 20px;
176
+ border-radius: 6px;
177
+ margin-top: 12px;
178
+ }
179
+
180
+ .advanced-grid {
181
+ display: grid;
182
+ grid-template-columns: repeat(2, 1fr);
183
+ gap: 16px;
184
+ }
185
+
186
+ .button-group {
187
+ display: flex;
188
+ gap: 12px;
189
+ margin-top: 32px;
190
+ padding-top: 24px;
191
+ border-top: 1px solid var(--border);
192
+ }
193
+
194
+ .btn {
195
+ flex: 1;
196
+ padding: 14px 24px;
197
+ border: none;
198
+ border-radius: 6px;
199
+ font-size: 15px;
200
+ font-weight: 600;
201
+ cursor: pointer;
202
+ transition: all 0.2s;
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ gap: 8px;
207
+ }
208
+
209
+ .btn-primary {
210
+ background: var(--blue);
211
+ color: white;
212
+ }
213
+
214
+ .btn-primary:hover {
215
+ background: #2c5aa0;
216
+ transform: translateY(-1px);
217
+ box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3);
218
+ }
219
+
220
+ .btn-primary:disabled {
221
+ background: var(--muted);
222
+ cursor: not-allowed;
223
+ transform: none;
224
+ box-shadow: none;
225
+ }
226
+
227
+ .btn-secondary {
228
+ background: var(--surface);
229
+ color: var(--text);
230
+ border: 1px solid var(--border);
231
+ }
232
+
233
+ .btn-secondary:hover {
234
+ background: var(--bg);
235
+ }
236
+
237
+ .status {
238
+ margin-top: 20px;
239
+ padding: 12px 16px;
240
+ border-radius: 6px;
241
+ font-size: 14px;
242
+ display: none;
243
+ }
244
+
245
+ .status.info {
246
+ background: var(--blue-bg);
247
+ color: var(--blue);
248
+ display: block;
249
+ }
250
+
251
+ .status.success {
252
+ background: var(--green-bg);
253
+ color: var(--green);
254
+ display: block;
255
+ }
256
+
257
+ .status.error {
258
+ background: var(--red-bg);
259
+ color: var(--red);
260
+ display: block;
261
+ }
262
+
263
+ .status.loading {
264
+ background: var(--yellow-bg);
265
+ color: var(--yellow);
266
+ display: block;
267
+ }
268
+
269
+ .spinner {
270
+ display: inline-block;
271
+ width: 16px;
272
+ height: 16px;
273
+ border: 2px solid currentColor;
274
+ border-top-color: transparent;
275
+ border-radius: 50%;
276
+ animation: spin 0.6s linear infinite;
277
+ }
278
+
279
+ @keyframes spin {
280
+ to { transform: rotate(360deg); }
281
+ }
282
+
283
+ .back-link {
284
+ display: inline-flex;
285
+ align-items: center;
286
+ gap: 6px;
287
+ color: var(--blue);
288
+ text-decoration: none;
289
+ font-size: 14px;
290
+ margin-bottom: 20px;
291
+ transition: color 0.2s;
292
+ }
293
+
294
+ .back-link:hover {
295
+ color: #2c5aa0;
296
+ }
297
+
298
+ .access-denied {
299
+ text-align: center;
300
+ padding: 60px 20px;
301
+ }
302
+
303
+ .access-denied h1 {
304
+ font-size: 48px;
305
+ color: var(--red);
306
+ margin-bottom: 16px;
307
+ }
308
+
309
+ .access-denied p {
310
+ color: var(--muted);
311
+ font-size: 16px;
312
+ margin-bottom: 24px;
313
+ }
314
+ </style>
315
+ </head>
316
+ <body>
317
+ <div id="app"></div>
318
+
319
+ <script>
320
+ function showReportGenerator() {
321
+
322
+ document.getElementById('app').innerHTML = `
323
+ <a href="index.html" class="back-link">← Back to Dashboard</a>
324
+
325
+ <div class="container">
326
+ <div class="header">
327
+ <h1>📊 Monthly Report Generator</h1>
328
+ <p class="subtitle">Generate comprehensive test reports with custom parameters</p>
329
+ <span class="admin-badge">ADMIN ACCESS</span>
330
+ </div>
331
+
332
+ <form id="reportForm">
333
+ <!-- Project Selection -->
334
+ <div class="section">
335
+ <div class="section-title">Project</div>
336
+ <div class="form-group">
337
+ <label for="project">Select Project</label>
338
+ <select id="project" name="project">
339
+ <option value="">All Projects</option>
340
+ </select>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- Time Period -->
345
+ <div class="section">
346
+ <div class="section-title">Time Period</div>
347
+ <div class="form-group">
348
+ <div class="radio-group">
349
+ <div class="radio-option">
350
+ <input type="radio" id="period-7" name="period" value="7">
351
+ <label for="period-7">Last 7 days</label>
352
+ </div>
353
+ <div class="radio-option">
354
+ <input type="radio" id="period-30" name="period" value="30" checked>
355
+ <label for="period-30">Last 30 days</label>
356
+ </div>
357
+ <div class="radio-option">
358
+ <input type="radio" id="period-90" name="period" value="90">
359
+ <label for="period-90">Last 90 days</label>
360
+ </div>
361
+ <div class="radio-option">
362
+ <input type="radio" id="period-custom" name="period" value="custom">
363
+ <label for="period-custom">Custom Range</label>
364
+ </div>
365
+ </div>
366
+ </div>
367
+ <div class="form-group" id="customRangeGroup" style="display: none;">
368
+ <div class="date-range">
369
+ <div>
370
+ <label for="startDate">Start Date</label>
371
+ <input type="date" id="startDate" name="startDate">
372
+ </div>
373
+ <div>
374
+ <label for="endDate">End Date</label>
375
+ <input type="date" id="endDate" name="endDate">
376
+ </div>
377
+ </div>
378
+ </div>
379
+ </div>
380
+
381
+ <!-- Output Format -->
382
+ <div class="section">
383
+ <div class="section-title">Output Format</div>
384
+ <div class="form-group">
385
+ <div class="checkbox-group">
386
+ <div class="checkbox-option">
387
+ <input type="checkbox" id="format-pptx" name="format" value="pptx" checked>
388
+ <label for="format-pptx">PowerPoint (.pptx)</label>
389
+ </div>
390
+ <div class="checkbox-option">
391
+ <input type="checkbox" id="format-xlsx" name="format" value="xlsx">
392
+ <label for="format-xlsx">Excel (.xlsx)</label>
393
+ </div>
394
+ <div class="checkbox-option">
395
+ <input type="checkbox" id="format-pdf" name="format" value="pdf">
396
+ <label for="format-pdf">PDF (.pdf)</label>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ <!-- Include Sections -->
403
+ <div class="section">
404
+ <div class="section-title">Include Sections</div>
405
+ <div class="form-group">
406
+ <div class="checkbox-group">
407
+ <div class="checkbox-option">
408
+ <input type="checkbox" id="section-summary" name="section" value="summary" checked>
409
+ <label for="section-summary">Executive Summary</label>
410
+ </div>
411
+ <div class="checkbox-option">
412
+ <input type="checkbox" id="section-trends" name="section" value="trends" checked>
413
+ <label for="section-trends">Pass Rate Trends</label>
414
+ </div>
415
+ <div class="checkbox-option">
416
+ <input type="checkbox" id="section-failures" name="section" value="failures" checked>
417
+ <label for="section-failures">Failure Analysis</label>
418
+ </div>
419
+ <div class="checkbox-option">
420
+ <input type="checkbox" id="section-flaky" name="section" value="flaky" checked>
421
+ <label for="section-flaky">Flaky Tests</label>
422
+ </div>
423
+ <div class="checkbox-option">
424
+ <input type="checkbox" id="section-slowest" name="section" value="slowest" checked>
425
+ <label for="section-slowest">Slowest Tests</label>
426
+ </div>
427
+ <div class="checkbox-option">
428
+ <input type="checkbox" id="section-regressions" name="section" value="regressions" checked>
429
+ <label for="section-regressions">Performance Regressions</label>
430
+ </div>
431
+ <div class="checkbox-option">
432
+ <input type="checkbox" id="section-file-health" name="section" value="file_health" checked>
433
+ <label for="section-file-health">File Health Report</label>
434
+ </div>
435
+ <div class="checkbox-option">
436
+ <input type="checkbox" id="section-recommendations" name="section" value="recommendations" checked>
437
+ <label for="section-recommendations">Recommendations</label>
438
+ </div>
439
+ </div>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- Advanced Options -->
444
+ <div class="section">
445
+ <div class="section-title">Advanced Options</div>
446
+ <div class="advanced-options">
447
+ <div class="advanced-grid">
448
+ <div class="form-group">
449
+ <label for="topN">Top N Items</label>
450
+ <select id="topN" name="topN">
451
+ <option value="5">5</option>
452
+ <option value="10" selected>10</option>
453
+ <option value="15">15</option>
454
+ <option value="20">20</option>
455
+ </select>
456
+ </div>
457
+ <div class="form-group">
458
+ <label for="chartTheme">Chart Theme</label>
459
+ <select id="chartTheme" name="chartTheme">
460
+ <option value="light" selected>Light</option>
461
+ <option value="dark">Dark</option>
462
+ </select>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ </div>
467
+
468
+ <!-- Buttons -->
469
+ <div class="button-group">
470
+ <button type="submit" class="btn btn-primary" id="generateBtn">
471
+ <span id="btnText">Generate Report</span>
472
+ </button>
473
+ <button type="button" class="btn btn-secondary" onclick="resetForm()">
474
+ Reset
475
+ </button>
476
+ </div>
477
+
478
+ <!-- Status -->
479
+ <div id="status" class="status"></div>
480
+ </form>
481
+ </div>
482
+ `;
483
+
484
+ // Initialize form
485
+ initializeForm();
486
+ }
487
+
488
+ function initializeForm() {
489
+ // Load projects
490
+ loadProjects();
491
+
492
+ // Set default dates
493
+ const today = new Date();
494
+ const thirtyDaysAgo = new Date(today);
495
+ thirtyDaysAgo.setDate(today.getDate() - 30);
496
+
497
+ document.getElementById('endDate').value = today.toISOString().split('T')[0];
498
+ document.getElementById('startDate').value = thirtyDaysAgo.toISOString().split('T')[0];
499
+
500
+ // Period radio change handler
501
+ document.querySelectorAll('input[name="period"]').forEach(radio => {
502
+ radio.addEventListener('change', (e) => {
503
+ const customRangeGroup = document.getElementById('customRangeGroup');
504
+ if (e.target.value === 'custom') {
505
+ customRangeGroup.style.display = 'block';
506
+ } else {
507
+ customRangeGroup.style.display = 'none';
508
+ }
509
+ });
510
+ });
511
+
512
+ // Form submit handler
513
+ document.getElementById('reportForm').addEventListener('submit', handleSubmit);
514
+ }
515
+
516
+ async function loadProjects() {
517
+ try {
518
+ const response = await fetch('data.json');
519
+ const data = await response.json();
520
+ const projects = Object.keys(data.projects || {});
521
+
522
+ const select = document.getElementById('project');
523
+ projects.forEach(project => {
524
+ const option = document.createElement('option');
525
+ option.value = project;
526
+ option.textContent = project;
527
+ select.appendChild(option);
528
+ });
529
+ } catch (error) {
530
+ console.error('Failed to load projects:', error);
531
+ }
532
+ }
533
+
534
+ async function handleSubmit(e) {
535
+ e.preventDefault();
536
+
537
+ const generateBtn = document.getElementById('generateBtn');
538
+ const btnText = document.getElementById('btnText');
539
+ const status = document.getElementById('status');
540
+
541
+ // Disable button
542
+ generateBtn.disabled = true;
543
+ btnText.innerHTML = '<span class="spinner"></span> Generating...';
544
+
545
+ // Get form data
546
+ const formData = getFormData();
547
+
548
+ // Validate
549
+ if (!validateFormData(formData)) {
550
+ showStatus('error', 'Please select at least one output format');
551
+ generateBtn.disabled = false;
552
+ btnText.textContent = 'Generate Report';
553
+ return;
554
+ }
555
+
556
+ showStatus('loading', 'Generating report... This may take a few moments.');
557
+
558
+ try {
559
+ // Call API
560
+ const response = await fetch('http://localhost:5555/api/generate-report', {
561
+ method: 'POST',
562
+ headers: {
563
+ 'Content-Type': 'application/json'
564
+ },
565
+ body: JSON.stringify(formData)
566
+ });
567
+
568
+ if (!response.ok) {
569
+ throw new Error('Failed to generate report');
570
+ }
571
+
572
+ // Download file
573
+ const blob = await response.blob();
574
+ const contentDisposition = response.headers.get('Content-Disposition');
575
+ const filename = contentDisposition
576
+ ? contentDisposition.split('filename=')[1].replace(/"/g, '')
577
+ : 'monthly-report.zip';
578
+
579
+ const url = window.URL.createObjectURL(blob);
580
+ const a = document.createElement('a');
581
+ a.href = url;
582
+ a.download = filename;
583
+ document.body.appendChild(a);
584
+ a.click();
585
+ window.URL.revokeObjectURL(url);
586
+ document.body.removeChild(a);
587
+
588
+ showStatus('success', 'Report generated successfully! Download should start automatically.');
589
+
590
+ } catch (error) {
591
+ console.error('Error generating report:', error);
592
+ showStatus('error', 'Failed to generate report. Make sure the report server is running (test-report serve).');
593
+ } finally {
594
+ generateBtn.disabled = false;
595
+ btnText.textContent = 'Generate Report';
596
+ }
597
+ }
598
+
599
+ function getFormData() {
600
+ const form = document.getElementById('reportForm');
601
+ const formData = new FormData(form);
602
+
603
+ // Get period
604
+ const period = formData.get('period');
605
+ let startDate, endDate;
606
+
607
+ if (period === 'custom') {
608
+ startDate = formData.get('startDate');
609
+ endDate = formData.get('endDate');
610
+ } else {
611
+ const today = new Date();
612
+ endDate = today.toISOString().split('T')[0];
613
+ const start = new Date(today);
614
+ start.setDate(today.getDate() - parseInt(period));
615
+ startDate = start.toISOString().split('T')[0];
616
+ }
617
+
618
+ // Get formats
619
+ const formats = [];
620
+ document.querySelectorAll('input[name="format"]:checked').forEach(cb => {
621
+ formats.push(cb.value);
622
+ });
623
+
624
+ // Get sections
625
+ const sections = {};
626
+ document.querySelectorAll('input[name="section"]').forEach(cb => {
627
+ sections[cb.value] = cb.checked;
628
+ });
629
+
630
+ return {
631
+ project: formData.get('project') || null,
632
+ start_date: startDate,
633
+ end_date: endDate,
634
+ formats: formats,
635
+ sections: sections,
636
+ top_n: parseInt(formData.get('topN')),
637
+ chart_theme: formData.get('chartTheme')
638
+ };
639
+ }
640
+
641
+ function validateFormData(data) {
642
+ return data.formats.length > 0;
643
+ }
644
+
645
+ function showStatus(type, message) {
646
+ const status = document.getElementById('status');
647
+ status.className = `status ${type}`;
648
+ status.textContent = message;
649
+ }
650
+
651
+ function resetForm() {
652
+ document.getElementById('reportForm').reset();
653
+ document.getElementById('customRangeGroup').style.display = 'none';
654
+ document.getElementById('status').className = 'status';
655
+ }
656
+
657
+ // Initialize on load
658
+ showReportGenerator();
659
+ </script>
660
+ </body>
661
+ </html>
@@ -317,46 +317,7 @@
317
317
  <div id="app"></div>
318
318
 
319
319
  <script>
320
- // Check admin access
321
- function checkAdminAccess() {
322
- const urlParams = new URLSearchParams(window.location.search);
323
- const key = urlParams.get('key');
324
-
325
- // Get admin key from config (will be loaded from data.json or config)
326
- const adminKey = getAdminKey();
327
-
328
- if (!key || key !== adminKey) {
329
- showAccessDenied();
330
- return false;
331
- }
332
- return true;
333
- }
334
-
335
- function getAdminKey() {
336
- // Try to get from localStorage (set during deployment)
337
- const storedKey = localStorage.getItem('admin_key');
338
- if (storedKey) return storedKey;
339
-
340
- // Default key (should be changed in production)
341
- return 'change-me-in-production';
342
- }
343
-
344
- function showAccessDenied() {
345
- document.getElementById('app').innerHTML = `
346
- <div class="access-denied">
347
- <h1>🔒</h1>
348
- <h2>Access Denied</h2>
349
- <p>You need admin access to view this page.</p>
350
- <a href="index.html" class="btn btn-primary" style="display: inline-flex; width: auto;">
351
- Return to Dashboard
352
- </a>
353
- </div>
354
- `;
355
- }
356
-
357
320
  function showReportGenerator() {
358
- const urlParams = new URLSearchParams(window.location.search);
359
- const adminKey = urlParams.get('key');
360
321
 
361
322
  document.getElementById('app').innerHTML = `
362
323
  <a href="index.html" class="back-link">← Back to Dashboard</a>
@@ -595,16 +556,11 @@
595
556
  showStatus('loading', 'Generating report... This may take a few moments.');
596
557
 
597
558
  try {
598
- // Get admin key from URL
599
- const urlParams = new URLSearchParams(window.location.search);
600
- const adminKey = urlParams.get('key');
601
-
602
559
  // Call API
603
560
  const response = await fetch('http://localhost:5555/api/generate-report', {
604
561
  method: 'POST',
605
562
  headers: {
606
- 'Content-Type': 'application/json',
607
- 'X-Admin-Key': adminKey
563
+ 'Content-Type': 'application/json'
608
564
  },
609
565
  body: JSON.stringify(formData)
610
566
  });
@@ -699,9 +655,7 @@
699
655
  }
700
656
 
701
657
  // Initialize on load
702
- if (checkAdminAccess()) {
703
- showReportGenerator();
704
- }
658
+ showReportGenerator();
705
659
  </script>
706
660
  </body>
707
661
  </html>
@@ -9,7 +9,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
9
9
 
10
10
  setup(
11
11
  name='test-reporting',
12
- version='4.0.0',
12
+ version='4.0.2',
13
13
  description='Multi-project test reporting dashboard — collect results locally or push to S3',
14
14
  long_description=long_description,
15
15
  long_description_content_type="text/markdown",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-reporting
3
- Version: 4.0.0
3
+ Version: 4.0.2
4
4
  Summary: Multi-project test reporting dashboard — collect results locally or push to S3
5
5
  Home-page: https://github.com/amahdy77/test-reporting.git
6
6
  Author: Ashfaqur Mahdy
@@ -317,7 +317,7 @@ retention_days = 90 # Keep 90 days of artifacts and data
317
317
 
318
318
  **`run.html?r=<id>&p=<name>`** — Run detail. Test file breakdown, every failing test with its error message, screenshot and trace links.
319
319
 
320
- **`report-generator.html?key=<admin-key>`** — Monthly report generator (admin only). Generate comprehensive reports in PowerPoint, Excel, or PDF format.
320
+ **`monthly-reports.html`** — Monthly report generator. Generate comprehensive reports in PowerPoint, Excel, or PDF format. (Obscure URL, not linked from main pages)
321
321
 
322
322
  ---
323
323
 
@@ -336,7 +336,7 @@ test-report monthly-report --format pptx,xlsx,pdf
336
336
 
337
337
  # Or use the UI
338
338
  test-report serve
339
- # Then open: http://localhost:8000/report-generator.html?key=your-admin-key
339
+ # Then open: http://localhost:5555/monthly-reports.html
340
340
  ```
341
341
 
342
342
  ### Features
@@ -15,6 +15,7 @@ reporting/exporters/pdf_exporter.py
15
15
  reporting/exporters/pptx_exporter.py
16
16
  reporting/exporters/xlsx_exporter.py
17
17
  reporting/templates/index.html
18
+ reporting/templates/monthly-reports.html
18
19
  reporting/templates/project.html
19
20
  reporting/templates/report-generator.html
20
21
  reporting/templates/run.html
File without changes
File without changes