overload-cli 0.1.0__py3-none-any.whl

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 (40) hide show
  1. overload/__init__.py +3 -0
  2. overload/__main__.py +5 -0
  3. overload/cli.py +393 -0
  4. overload/collection/__init__.py +1 -0
  5. overload/collection/environment.py +23 -0
  6. overload/collection/models.py +88 -0
  7. overload/collection/parser.py +220 -0
  8. overload/collection/variables.py +84 -0
  9. overload/config_file.py +73 -0
  10. overload/engine/__init__.py +1 -0
  11. overload/engine/assertions.py +151 -0
  12. overload/engine/auth.py +87 -0
  13. overload/engine/events.py +50 -0
  14. overload/engine/http_client.py +274 -0
  15. overload/engine/load_patterns.py +730 -0
  16. overload/engine/models.py +254 -0
  17. overload/engine/rate_limiter.py +124 -0
  18. overload/engine/runner.py +86 -0
  19. overload/report/__init__.py +1 -0
  20. overload/report/exporters.py +77 -0
  21. overload/report/generator.py +71 -0
  22. overload/report/templates/report.html +369 -0
  23. overload/utils/__init__.py +1 -0
  24. overload/utils/naming.py +26 -0
  25. overload/web/__init__.py +1 -0
  26. overload/web/app.py +38 -0
  27. overload/web/routes/__init__.py +1 -0
  28. overload/web/routes/api.py +461 -0
  29. overload/web/routes/ws.py +77 -0
  30. overload/web/static/css/app.css +242 -0
  31. overload/web/static/js/app.js +241 -0
  32. overload/web/static/js/charts.js +385 -0
  33. overload/web/static/js/collection.js +344 -0
  34. overload/web/static/js/runner.js +625 -0
  35. overload/web/templates/index.html +23 -0
  36. overload_cli-0.1.0.dist-info/METADATA +267 -0
  37. overload_cli-0.1.0.dist-info/RECORD +40 -0
  38. overload_cli-0.1.0.dist-info/WHEEL +4 -0
  39. overload_cli-0.1.0.dist-info/entry_points.txt +2 -0
  40. overload_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,242 @@
1
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
2
+ :root{
3
+ --bg:#f5f6fa;--sur:#ffffff;--sur2:#f0f1f5;--sur3:#e8eaf0;
4
+ --bdr:#dde1ec;--bdr2:#c8cfe0;
5
+ --ok:#0e8a5f;--bad:#c0392b;--mid:#b7600a;--blue:#1d5fa8;--purple:#7c3aed;
6
+ --txt:#1a1d2e;--txt2:#374151;--mut:#6b7280;
7
+ --ok-bg:#e6f4ef;--bad-bg:#fdecea;--mid-bg:#fef3e6;--blue-bg:#eef4ff;
8
+ --radius:6px;
9
+ --shadow:0 1px 3px rgba(0,0,0,.06);
10
+ }
11
+ body{background:var(--bg);color:var(--txt);font-family:ui-monospace,'Cascadia Code','JetBrains Mono',monospace;font-size:12px;line-height:1.6;display:flex;min-height:100vh}
12
+
13
+ /* Sidebar */
14
+ #sidebar{width:200px;background:var(--sur);border-right:1px solid var(--bdr);padding:20px 0;display:flex;flex-direction:column;position:fixed;top:0;bottom:0;left:0;z-index:10;box-shadow:var(--shadow)}
15
+ .logo{font-size:15px;font-weight:800;letter-spacing:2px;color:var(--ok);padding:0 20px 20px;border-bottom:1px solid var(--bdr)}
16
+ .nav-link{display:block;padding:8px 20px;color:var(--mut);text-decoration:none;font-size:11px;font-weight:600;letter-spacing:.03em;border-left:3px solid transparent;transition:all .15s}
17
+ .nav-link:first-of-type{margin-top:12px}
18
+ .nav-link:hover{color:var(--txt);background:var(--sur2)}
19
+ .nav-link.active{color:var(--ok);border-left-color:var(--ok);background:var(--ok-bg)}
20
+
21
+ /* Main content */
22
+ #content{margin-left:200px;flex:1;padding:22px 32px;min-height:100vh;max-width:1280px}
23
+
24
+ /* Page headers */
25
+ .page-title{font-size:20px;font-weight:800;color:var(--txt);letter-spacing:-.3px;margin-bottom:4px}
26
+ .page-desc{color:var(--mut);font-size:11px;margin-bottom:20px}
27
+
28
+ /* Cards */
29
+ .card{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--radius);padding:16px;margin-bottom:12px;box-shadow:var(--shadow)}
30
+ .card-title{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--mut);margin-bottom:10px}
31
+
32
+ /* Detected collections */
33
+ .detected-section{margin-bottom:16px}
34
+ .detected-item{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--blue-bg);border:1px solid #b8d0f5;border-radius:var(--radius);margin-bottom:6px;cursor:pointer;transition:all .15s}
35
+ .detected-item:hover{border-color:var(--blue);background:#dce8fb}
36
+ .detected-item-info{display:flex;align-items:center;gap:8px}
37
+ .detected-item-icon{font-size:14px}
38
+ .detected-item-name{font-size:12px;font-weight:600;color:var(--blue)}
39
+ .detected-item-meta{font-size:10px;color:var(--mut)}
40
+ .detected-item-btn{font-size:10px;color:var(--blue);font-weight:600;text-transform:uppercase;letter-spacing:.06em}
41
+
42
+ /* Drop zone */
43
+ .drop-zone{border:2px dashed var(--bdr2);border-radius:var(--radius);padding:32px;text-align:center;cursor:pointer;transition:all .2s;background:var(--sur2)}
44
+ .drop-zone:hover,.drop-zone.dragover{border-color:var(--ok);background:var(--ok-bg)}
45
+ .drop-zone-text{font-size:13px;color:var(--txt2);margin-bottom:4px;font-weight:600}
46
+ .drop-zone-sub{font-size:10px;color:var(--mut)}
47
+ .drop-zone input[type=file]{display:none}
48
+
49
+ /* Collection tree */
50
+ .tree{margin:8px 0}
51
+ .tree-folder{margin-bottom:2px}
52
+ .tree-folder-name{display:flex;align-items:center;gap:6px;padding:3px 8px;color:var(--txt2);font-size:11px;font-weight:700;cursor:pointer;border-radius:3px}
53
+ .tree-folder-name:hover{background:var(--sur2)}
54
+ .tree-folder-icon{font-size:9px;color:var(--mut)}
55
+ .tree-children{padding-left:16px}
56
+ .tree-request{display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:3px;cursor:pointer;font-size:11px;border:1px solid transparent}
57
+ .tree-request:hover{background:var(--sur2)}
58
+ .tree-request.selected{background:var(--blue-bg);border-color:#b8d0f5}
59
+ .method-badge{font-size:9px;font-weight:700;padding:1px 5px;border-radius:2px;min-width:36px;text-align:center;letter-spacing:.03em}
60
+ .method-GET{background:var(--ok-bg);color:var(--ok)}
61
+ .method-POST{background:var(--mid-bg);color:var(--mid)}
62
+ .method-PUT{background:var(--blue-bg);color:var(--blue)}
63
+ .method-DELETE{background:var(--bad-bg);color:var(--bad)}
64
+ .method-PATCH{background:rgba(124,58,237,.1);color:var(--purple)}
65
+
66
+ /* Variables table */
67
+ .var-table{width:100%;border-collapse:collapse;font-size:11px}
68
+ .var-table th{text-align:left;padding:6px 8px;color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--bdr)}
69
+ .var-table td{padding:5px 8px;border-bottom:1px solid var(--sur2)}
70
+ .var-table input{background:var(--sur2);border:1px solid var(--bdr);color:var(--txt);padding:3px 6px;border-radius:3px;font-size:11px;font-family:inherit;width:100%}
71
+ .var-table input:focus{outline:none;border-color:var(--blue)}
72
+
73
+ /* Test type cards */
74
+ .test-types{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;margin-bottom:20px}
75
+ .test-card{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--radius);padding:12px;cursor:pointer;transition:all .15s;box-shadow:var(--shadow)}
76
+ .test-card:hover{border-color:var(--blue);box-shadow:0 2px 8px rgba(29,95,168,.1)}
77
+ .test-card.selected{border-color:var(--ok);background:var(--ok-bg)}
78
+ .test-card-name{font-size:12px;font-weight:700;margin-bottom:2px}
79
+ .test-card-desc{font-size:10px;color:var(--mut);line-height:1.4}
80
+ .test-card-shape{height:24px;margin-top:6px;display:flex;align-items:end;gap:1px}
81
+ .test-card-shape span{background:var(--ok);border-radius:1px;opacity:.4}
82
+
83
+ /* Config form */
84
+ .config-section{margin-bottom:16px}
85
+ .config-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
86
+ .config-label{width:170px;font-size:11px;color:var(--txt2);display:flex;align-items:center;gap:4px}
87
+ .config-label .tooltip{color:var(--blue);cursor:help;font-size:11px;position:relative;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--blue-bg);font-weight:700}
88
+ .config-label .tooltip:hover::after{content:attr(data-tip);position:absolute;left:calc(100% + 8px);top:50%;transform:translateY(-50%);background:var(--txt);color:#fff;padding:6px 10px;border-radius:4px;font-size:10px;font-weight:400;white-space:nowrap;z-index:50;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,.15)}
89
+ .config-label .tooltip:hover::before{content:'';position:absolute;left:calc(100% + 4px);top:50%;transform:translateY(-50%);border:4px solid transparent;border-right-color:var(--txt);z-index:50}
90
+ .config-input{flex:1;display:flex;align-items:center;gap:6px}
91
+ .config-input input[type=range]{flex:1;accent-color:var(--ok);height:3px}
92
+ .config-input input[type=number]{background:var(--sur);border:1px solid var(--bdr);color:var(--txt);padding:6px 10px;border-radius:3px;width:100px;font-size:13px;font-family:inherit;font-weight:600}
93
+ .config-input input[type=number]:focus{outline:none;border-color:var(--blue)}
94
+ .config-input select{background:var(--sur);border:1px solid var(--bdr);color:var(--txt);padding:4px 8px;border-radius:3px;font-size:11px;font-family:inherit}
95
+
96
+ /* Load shape preview */
97
+ .shape-preview{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--radius);padding:12px;margin-bottom:16px}
98
+ .shape-preview canvas{width:100%!important;height:100px!important}
99
+
100
+ /* Buttons */
101
+ .btn{display:inline-flex;align-items:center;gap:4px;padding:7px 16px;border-radius:var(--radius);font-size:11px;font-weight:700;font-family:inherit;cursor:pointer;border:none;transition:all .15s;letter-spacing:.03em}
102
+ .btn-primary{background:var(--ok);color:#fff}
103
+ .btn-primary:hover{background:#077a52}
104
+ .btn-danger{background:var(--bad);color:#fff}
105
+ .btn-danger:hover{background:#a93226}
106
+ .btn-secondary{background:var(--sur);color:var(--txt);border:1px solid var(--bdr)}
107
+ .btn-secondary:hover{background:var(--sur2)}
108
+ .btn:disabled{opacity:.5;cursor:not-allowed}
109
+
110
+ /* Live dashboard KPIs */
111
+ .kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-bottom:14px}
112
+ .kpi{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--radius);padding:12px 14px;position:relative;overflow:hidden}
113
+ .kpi::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
114
+ .kpi-ok::before{background:var(--ok)}
115
+ .kpi-bad::before{background:var(--bad)}
116
+ .kpi-mid::before{background:var(--mid)}
117
+ .kpi-blue::before{background:var(--blue)}
118
+ .kpi-label{font-size:10px;color:var(--mut);text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px}
119
+ .kpi-value{font-size:22px;font-weight:800}
120
+ .kpi-sub{font-size:10px;color:var(--mut);margin-top:2px}
121
+
122
+ /* Progress bar */
123
+ .progress-wrap{margin:16px 0}
124
+ .progress-bar{background:var(--sur2);border:1px solid var(--bdr);border-radius:3px;height:6px;overflow:hidden}
125
+ .progress-fill{height:100%;background:var(--ok);border-radius:3px;transition:width .3s}
126
+ .progress-text{display:flex;justify-content:space-between;margin-top:4px;font-size:10px;color:var(--mut)}
127
+
128
+ /* Charts */
129
+ .chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}
130
+ @media(max-width:900px){.chart-grid{grid-template-columns:1fr}}
131
+ .chart-card{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--radius);padding:14px}
132
+ .chart-title{font-size:10px;color:var(--mut);text-transform:uppercase;letter-spacing:.1em;margin-bottom:10px}
133
+ .chart-card canvas{width:100%!important;height:180px!important}
134
+
135
+ /* Log table */
136
+ .log-filters{display:flex;gap:4px;margin-bottom:8px}
137
+ .log-filter{background:var(--sur);border:1px solid var(--bdr);color:var(--mut);padding:3px 10px;border-radius:3px;cursor:pointer;font-size:10px;font-family:inherit;font-weight:600}
138
+ .log-filter.active{background:var(--blue);color:#fff;border-color:var(--blue)}
139
+ .log-table{width:100%;border-collapse:collapse;font-size:11px}
140
+ .log-table th{text-align:left;padding:7px 10px;color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.07em;border-bottom:1px solid var(--bdr);background:var(--sur2)}
141
+ .log-table td{padding:5px 10px;border-bottom:1px solid var(--sur2)}
142
+ .log-table tr:hover{background:var(--sur2)}
143
+ .status-badge{display:inline-block;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:600}
144
+ .status-ok{background:var(--ok-bg);color:var(--ok)}
145
+ .status-429{background:var(--bad-bg);color:var(--bad)}
146
+ .status-err{background:var(--mid-bg);color:var(--mid)}
147
+
148
+ /* Runs history */
149
+ .runs-table{width:100%;border-collapse:collapse;font-size:11px}
150
+ .runs-table th{text-align:left;padding:7px 10px;color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.07em;border-bottom:1px solid var(--bdr)}
151
+ .runs-table td{padding:7px 10px;border-bottom:1px solid var(--sur2)}
152
+ .runs-table tr:hover{background:var(--sur2);cursor:pointer}
153
+ .run-status{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px}
154
+ .run-status.complete{background:var(--ok)}
155
+ .run-status.running{background:var(--mid);animation:pulse 1s infinite}
156
+ .run-status.error{background:var(--bad)}
157
+
158
+ /* Stages editor */
159
+ .stages-list{margin:8px 0}
160
+ .stage-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;padding:6px 8px;background:var(--sur2);border-radius:3px;border:1px solid var(--bdr)}
161
+ .stage-row input{background:var(--sur);border:1px solid var(--bdr);color:var(--txt);padding:3px 6px;border-radius:3px;width:72px;font-size:11px;font-family:inherit}
162
+ .stage-remove{background:none;border:none;color:var(--bad);cursor:pointer;font-size:14px;padding:2px}
163
+ .stage-add{background:var(--sur);border:1px dashed var(--bdr2);color:var(--mut);padding:6px;border-radius:3px;cursor:pointer;text-align:center;font-size:10px;font-weight:600}
164
+ .stage-add:hover{border-color:var(--ok);color:var(--ok)}
165
+
166
+ /* Advanced toggle */
167
+ .advanced-toggle{display:flex;align-items:center;gap:6px;padding:6px 0;color:var(--mut);font-size:11px;cursor:pointer;border:none;background:none;font-family:inherit;font-weight:600}
168
+ .advanced-toggle:hover{color:var(--txt)}
169
+ .advanced-content{display:none}
170
+ .advanced-content.open{display:block}
171
+
172
+ /* Request detail */
173
+ .request-detail{background:var(--sur2);border:1px solid var(--bdr);border-radius:var(--radius);padding:14px;margin-top:10px}
174
+ .request-detail-header{display:flex;align-items:center;gap:6px;margin-bottom:10px}
175
+ .request-detail pre{background:var(--sur);border:1px solid var(--bdr);padding:10px;border-radius:3px;font-size:11px;overflow-x:auto;color:var(--txt)}
176
+
177
+ /* Toast */
178
+ .toast{position:fixed;bottom:20px;right:20px;background:var(--sur);border:1px solid var(--bdr);border-radius:var(--radius);padding:10px 16px;font-size:11px;box-shadow:0 4px 12px rgba(0,0,0,.1);z-index:100;animation:slideIn .2s;max-width:360px}
179
+ .toast.error{border-color:var(--bad);background:var(--bad-bg)}
180
+ .toast.success{border-color:var(--ok);background:var(--ok-bg)}
181
+
182
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
183
+ @keyframes slideIn{from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1}}
184
+
185
+ .checkbox-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:11px}
186
+ .checkbox-row input[type=checkbox]{accent-color:var(--ok)}
187
+
188
+ /* Live log panel */
189
+ .live-log{background:var(--sur);border:1px solid var(--bdr);border-radius:var(--radius);max-height:280px;overflow-y:auto;margin-top:12px}
190
+ .live-log-title{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--mut);padding:10px 12px 6px;border-bottom:1px solid var(--sur2);position:sticky;top:0;background:var(--sur);z-index:1}
191
+ .live-log-entry{display:flex;align-items:center;gap:8px;padding:4px 12px;font-size:11px;border-bottom:1px solid var(--sur2);animation:fadeIn .2s}
192
+ .live-log-entry:hover{background:var(--sur2)}
193
+ .live-log-entry .log-idx{color:var(--mut);width:32px;text-align:right;font-size:10px}
194
+ .live-log-entry .log-method{font-weight:700;font-size:9px;padding:1px 4px;border-radius:2px;min-width:32px;text-align:center}
195
+ .live-log-entry .log-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
196
+ .live-log-entry .log-latency{color:var(--mut);font-size:10px;min-width:60px;text-align:right}
197
+ .live-log-empty{padding:16px;text-align:center;color:var(--mut);font-size:11px}
198
+
199
+ @keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
200
+
201
+ /* Status color classes */
202
+ .status-1xx{background:#e0e7ff;color:#4338ca}
203
+ .status-2xx{background:var(--ok-bg);color:var(--ok)}
204
+ .status-3xx{background:var(--blue-bg);color:var(--blue)}
205
+ .status-4xx{background:var(--mid-bg);color:var(--mid)}
206
+ .status-5xx{background:var(--bad-bg);color:var(--bad)}
207
+ .status-0{background:#f3f4f6;color:#6b7280}
208
+
209
+ /* Save response toggle */
210
+ .save-response-row{display:flex;align-items:center;gap:8px;padding:8px 0;font-size:11px;color:var(--txt2)}
211
+ .save-response-row input[type=checkbox]{accent-color:var(--ok);width:14px;height:14px}
212
+
213
+ /* Working dir banner */
214
+ .working-dir-banner{background:var(--blue-bg);border:1px solid #b8d0f5;border-radius:var(--radius);padding:8px 14px;margin-bottom:12px;font-size:11px;color:var(--blue);display:flex;align-items:center;gap:6px}
215
+ .working-dir-banner .dir-path{font-weight:600;word-break:break-all}
216
+
217
+ /* Assertions editor */
218
+ .assertion-select{background:var(--sur);border:1px solid var(--bdr);color:var(--txt);padding:3px 6px;border-radius:3px;font-size:11px;font-family:inherit;flex:1}
219
+ .assertion-op{background:var(--sur);border:1px solid var(--bdr);color:var(--txt);padding:3px 6px;border-radius:3px;font-size:11px;font-family:inherit;width:50px}
220
+ .assertion-value{background:var(--sur);border:1px solid var(--bdr);color:var(--txt);padding:3px 6px;border-radius:3px;width:90px;font-size:11px;font-family:inherit}
221
+
222
+ /* Verdict banners */
223
+ .verdict-banner{display:flex;align-items:flex-start;gap:10px;padding:12px 16px;border-radius:var(--radius);margin-bottom:14px;border:1px solid}
224
+ .verdict-pass{background:var(--ok-bg);border-color:#a3d9bf}
225
+ .verdict-fail{background:var(--bad-bg);border-color:#f5b7b1}
226
+ .verdict-icon{font-size:20px;line-height:1}
227
+ .verdict-body{flex:1}
228
+ .verdict-title{font-size:14px;font-weight:800;letter-spacing:.05em}
229
+ .verdict-pass .verdict-title{color:var(--ok)}
230
+ .verdict-fail .verdict-title{color:var(--bad)}
231
+ .verdict-details{font-size:11px;color:var(--txt2);margin-top:4px;line-height:1.8}
232
+ .verdict-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:10px;font-weight:700;letter-spacing:.04em}
233
+ .verdict-badge-pass{background:var(--ok-bg);color:var(--ok)}
234
+ .verdict-badge-fail{background:var(--bad-bg);color:var(--bad)}
235
+
236
+ @media(max-width:768px){
237
+ #sidebar{width:56px}
238
+ #sidebar .nav-link{font-size:0;padding:10px}
239
+ #sidebar .logo{font-size:0;padding:10px 0;text-align:center}
240
+ #content{margin-left:56px;padding:14px}
241
+ .test-types{grid-template-columns:1fr 1fr}
242
+ }
@@ -0,0 +1,241 @@
1
+ window.OverloadApp = (function() {
2
+ var currentPage = 'collection';
3
+ var collection = null;
4
+ var ws = null;
5
+ var progressCallback = null;
6
+
7
+ function init() {
8
+ document.querySelectorAll('.nav-link').forEach(function(link) {
9
+ link.addEventListener('click', function(e) {
10
+ e.preventDefault();
11
+ navigate(link.dataset.page);
12
+ });
13
+ });
14
+ navigate('collection');
15
+ }
16
+
17
+ function navigate(page) {
18
+ currentPage = page;
19
+ document.querySelectorAll('.nav-link').forEach(function(link) {
20
+ link.classList.toggle('active', link.dataset.page === page);
21
+ });
22
+
23
+ OverloadCharts.destroyAll();
24
+
25
+ var content = document.getElementById('content');
26
+ switch (page) {
27
+ case 'collection':
28
+ CollectionPage.render(content);
29
+ break;
30
+ case 'runner':
31
+ RunnerPage.render(content);
32
+ break;
33
+ case 'results':
34
+ renderResults(content);
35
+ break;
36
+ }
37
+ }
38
+
39
+ function setCollection(coll) { collection = coll; }
40
+ function getCollection() { return collection; }
41
+
42
+ function connectWs() {
43
+ if (ws && ws.readyState === WebSocket.OPEN) return;
44
+ var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
45
+ ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
46
+ ws.onopen = function() { console.log('WebSocket connected'); };
47
+ ws.onmessage = function(e) {
48
+ try {
49
+ var msg = JSON.parse(e.data);
50
+ if (msg.type === 'progress' && progressCallback) {
51
+ progressCallback(msg.data);
52
+ }
53
+ } catch (err) { console.error('WS parse error:', err); }
54
+ };
55
+ ws.onclose = function() {
56
+ console.log('WebSocket disconnected, reconnecting in 2s...');
57
+ setTimeout(connectWs, 2000);
58
+ };
59
+ ws.onerror = function() { ws.close(); };
60
+ }
61
+
62
+ function subscribeToRun(runId, callback) {
63
+ progressCallback = callback;
64
+ connectWs();
65
+ var waitForOpen = function() {
66
+ if (ws.readyState === WebSocket.OPEN) {
67
+ ws.send(JSON.stringify({ type: 'subscribe', run_id: runId }));
68
+ } else {
69
+ setTimeout(waitForOpen, 100);
70
+ }
71
+ };
72
+ waitForOpen();
73
+ }
74
+
75
+ function renderResults(container) {
76
+ container.innerHTML =
77
+ '<h1 class="page-title">Results</h1>' +
78
+ '<p class="page-desc">View test results and download reports</p>' +
79
+ '<div id="resultsList"><p style="color:var(--mut)">Loading...</p></div>';
80
+
81
+ fetch('/api/runs')
82
+ .then(function(r) { return r.json(); })
83
+ .then(function(data) {
84
+ var runs = data.runs || [];
85
+ if (!runs.length) {
86
+ document.getElementById('resultsList').innerHTML = '<div class="card"><p style="color:var(--mut)">No test runs yet. Go to Test Runner to start one.</p></div>';
87
+ return;
88
+ }
89
+
90
+ var html = '<div class="card"><table class="runs-table"><thead><tr><th>Run ID</th><th>Test Type</th><th>Status</th><th>Verdict</th><th>Total</th><th>Success</th><th>Errors</th><th>Avg RPS</th><th>Actions</th></tr></thead><tbody>';
91
+ runs.reverse().forEach(function(run) {
92
+ var statusClass = run.status === 'complete' ? 'complete' : run.status === 'error' ? 'error' : 'running';
93
+ var verdictBadge = '-';
94
+ if (run.verdict === true) verdictBadge = '<span class="verdict-badge verdict-badge-pass">PASS</span>';
95
+ else if (run.verdict === false) verdictBadge = '<span class="verdict-badge verdict-badge-fail">FAIL</span>';
96
+ html += '<tr>' +
97
+ '<td><span class="run-status ' + statusClass + '"></span>' + esc(run.run_id) + '</td>' +
98
+ '<td>' + esc(run.test_type || '-') + '</td>' +
99
+ '<td>' + esc(run.status || '-') + '</td>' +
100
+ '<td>' + verdictBadge + '</td>' +
101
+ '<td>' + (run.total || '-') + '</td>' +
102
+ '<td>' + (run.ok || '-') + '</td>' +
103
+ '<td>' + (run.errors || '-') + '</td>' +
104
+ '<td>' + (run.avg_rps || '-') + '</td>' +
105
+ '<td>';
106
+ if (run.status === 'complete') {
107
+ html += '<a href="/api/runs/' + run.run_id + '/report" target="_blank" class="btn btn-secondary" style="padding:4px 10px;font-size:11px">HTML Report</a> ';
108
+ html += '<button class="btn btn-secondary view-details" data-run="' + run.run_id + '" style="padding:4px 10px;font-size:11px">Details</button>';
109
+ }
110
+ html += '</td></tr>';
111
+ });
112
+ html += '</tbody></table></div>';
113
+ html += '<div id="runDetailView"></div>';
114
+
115
+ document.getElementById('resultsList').innerHTML = html;
116
+
117
+ document.querySelectorAll('.view-details').forEach(function(btn) {
118
+ btn.addEventListener('click', function() { showRunDetail(btn.dataset.run); });
119
+ });
120
+ })
121
+ .catch(function(err) {
122
+ document.getElementById('resultsList').innerHTML = '<div class="card"><p style="color:var(--bad)">Error loading runs: ' + esc(err.message) + '</p></div>';
123
+ });
124
+ }
125
+
126
+ function showRunDetail(runId) {
127
+ fetch('/api/runs/' + runId + '/data')
128
+ .then(function(r) { return r.json(); })
129
+ .then(function(data) {
130
+ if (!data.stats) {
131
+ document.getElementById('runDetailView').innerHTML = '<div class="card"><p style="color:var(--mut)">No data available</p></div>';
132
+ return;
133
+ }
134
+ var d = data.stats;
135
+ var html = '<div class="card"><div class="card-title">Run: ' + runId + ' — ' + (data.test_type || '') + '</div>';
136
+
137
+ // Verdict banner
138
+ if (data.verdict) {
139
+ var v = data.verdict;
140
+ html += '<div class="verdict-banner ' + (v.passed ? 'verdict-pass' : 'verdict-fail') + '">';
141
+ html += '<div class="verdict-icon">' + (v.passed ? '&#x2705;' : '&#x274C;') + '</div>';
142
+ html += '<div class="verdict-body"><div class="verdict-title">' + (v.passed ? 'PASS' : 'FAIL') + '</div>';
143
+ html += '<div class="verdict-details">';
144
+ v.results.forEach(function(r) {
145
+ var mark = r.passed ? '<span style="color:var(--ok)">&#10003;</span>' : '<span style="color:var(--bad)">&#10007;</span>';
146
+ html += '<div>' + mark + ' ' + esc(r.metric) + ': ' + r.actual + ' ' + esc(r.operator) + ' ' + r.expected + '</div>';
147
+ });
148
+ html += '</div></div></div>';
149
+ }
150
+
151
+ // KPIs
152
+ html += '<div class="kpi-grid">' +
153
+ '<div class="kpi kpi-mid"><div class="kpi-label">Total</div><div class="kpi-value">' + d.total + '</div></div>' +
154
+ '<div class="kpi kpi-ok"><div class="kpi-label">Successful</div><div class="kpi-value">' + d.ok + '</div></div>' +
155
+ '<div class="kpi kpi-bad"><div class="kpi-label">Errors</div><div class="kpi-value">' + d.errors + '</div></div>' +
156
+ '<div class="kpi kpi-blue"><div class="kpi-label">Avg RPS</div><div class="kpi-value">' + d.avg_rps + '</div></div>' +
157
+ '<div class="kpi kpi-mid"><div class="kpi-label">Duration</div><div class="kpi-value">' + d.duration_seconds + 's</div></div>' +
158
+ '<div class="kpi kpi-ok"><div class="kpi-label">P95 Latency</div><div class="kpi-value">' + d.latency.p95 + 'ms</div></div>' +
159
+ '</div>';
160
+
161
+ // Charts
162
+ html += '<div class="chart-grid">' +
163
+ '<div class="chart-card"><div class="chart-title">Requests per Second</div><canvas id="detailRps"></canvas></div>' +
164
+ '<div class="chart-card"><div class="chart-title">Status Distribution</div><canvas id="detailStatus"></canvas></div>' +
165
+ '<div class="chart-card"><div class="chart-title">Latency Histogram</div><canvas id="detailLatHist"></canvas></div>' +
166
+ '<div class="chart-card"><div class="chart-title">Timeline</div><canvas id="detailTimeline"></canvas></div>' +
167
+ '</div>';
168
+
169
+ // Log
170
+ if (d.request_log && d.request_log.length) {
171
+ html += '<div class="card-title" style="margin-top:16px">Request Log (first 200)</div>';
172
+ html += '<div style="overflow-x:auto"><table class="log-table"><thead><tr><th>#</th><th>Time</th><th>Status</th><th>Latency</th><th>Method</th><th>Request</th></tr></thead><tbody>';
173
+ d.request_log.slice(0, 200).forEach(function(r, i) {
174
+ var statusClass;
175
+ if (r.status <= 0) statusClass = 'status-0';
176
+ else if (r.status < 200) statusClass = 'status-1xx';
177
+ else if (r.status < 300) statusClass = 'status-2xx';
178
+ else if (r.status < 400) statusClass = 'status-3xx';
179
+ else if (r.status < 500) statusClass = 'status-4xx';
180
+ else statusClass = 'status-5xx';
181
+ html += '<tr><td style="color:var(--mut)">' + (i + 1) + '</td>' +
182
+ '<td>' + r.timestamp + 's</td>' +
183
+ '<td><span class="status-badge ' + statusClass + '">' + r.status + '</span></td>' +
184
+ '<td>' + r.latency_ms + 'ms</td>' +
185
+ '<td>' + esc(r.method) + '</td>' +
186
+ '<td>' + esc(r.request_name) + '</td></tr>';
187
+ if (r.response_body) {
188
+ html += '<tr><td></td><td colspan="5"><pre style="background:var(--sur2);padding:6px 10px;border-radius:3px;font-size:10px;max-height:120px;overflow:auto;white-space:pre-wrap;word-break:break-all">' + esc(r.response_body.substring(0, 2000)) + '</pre></td></tr>';
189
+ }
190
+ });
191
+ if (d.request_log.length > 200) {
192
+ html += '<tr><td colspan="6" style="text-align:center;color:var(--mut)">Showing 200 of ' + d.request_log.length + '</td></tr>';
193
+ }
194
+ html += '</tbody></table></div>';
195
+ }
196
+
197
+ html += '</div>';
198
+ document.getElementById('runDetailView').innerHTML = html;
199
+
200
+ requestAnimationFrame(function() {
201
+ OverloadCharts.rpsBarChart('detailRps', d.per_second);
202
+ OverloadCharts.statusDoughnut('detailStatus', d.status_codes);
203
+ OverloadCharts.latencyHistogram('detailLatHist', d.timeline);
204
+ OverloadCharts.timelineScatter('detailTimeline', d.timeline);
205
+ });
206
+ })
207
+ .catch(function(err) {
208
+ document.getElementById('runDetailView').innerHTML = '<div class="card"><p style="color:var(--bad)">Error: ' + esc(err.message) + '</p></div>';
209
+ });
210
+ }
211
+
212
+ function esc(s) {
213
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
214
+ }
215
+
216
+ // Toast notification
217
+ window.App = window.App || {};
218
+ App.toast = function(msg, type) {
219
+ var existing = document.querySelector('.toast');
220
+ if (existing) existing.remove();
221
+ var el = document.createElement('div');
222
+ el.className = 'toast ' + (type || '');
223
+ el.textContent = msg;
224
+ document.body.appendChild(el);
225
+ setTimeout(function() { el.remove(); }, 4000);
226
+ };
227
+
228
+ Chart.defaults.color = '#6b7280';
229
+ Chart.defaults.borderColor = '#e5e7eb';
230
+ Chart.defaults.font.size = 10;
231
+ Chart.defaults.font.family = 'ui-monospace,monospace';
232
+
233
+ document.addEventListener('DOMContentLoaded', init);
234
+
235
+ return {
236
+ navigate: navigate,
237
+ setCollection: setCollection,
238
+ getCollection: getCollection,
239
+ subscribeToRun: subscribeToRun
240
+ };
241
+ })();