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,625 @@
1
+ window.RunnerPage = (function() {
2
+ var selectedType = null;
3
+ var config = {};
4
+ var isRunning = false;
5
+ var currentRunId = null;
6
+ var rpsHistory = [];
7
+ var lastLogIdx = -1;
8
+ var logEntries = [];
9
+ var prevProgress = { completed: 0, time: 0 };
10
+
11
+ var TEST_TYPES = [
12
+ { id: 'load', name: 'Load Test', desc: 'Sustained traffic at target RPS with ramp up/down', shape: [1,3,5,8,10,10,10,10,10,10,8,5,3] },
13
+ { id: 'stress', name: 'Stress Test', desc: 'Increasing load until the system breaks', shape: [1,2,3,4,5,6,7,8,9,10,11,12,13] },
14
+ { id: 'spike', name: 'Spike Test', desc: 'Sudden traffic surge to test recovery', shape: [3,3,3,3,13,13,3,3,3,3,3,3,3] },
15
+ { id: 'soak', name: 'Soak Test', desc: 'Steady load over long duration to find leaks', shape: [5,5,5,5,5,5,5,5,5,5,5,5,5] },
16
+ { id: 'ramp', name: 'Ramp Test', desc: 'Gradually increase RPS to find optimal point', shape: [1,2,3,4,5,6,7,8,9,10,11,12,13] },
17
+ { id: 'burst', name: 'Burst Test', desc: 'Fire all requests at once', shape: [13,13,0,0,0,0,0,0,0,0,0,0,0] },
18
+ { id: 'breakpoint', name: 'Breakpoint Test', desc: 'Binary search for exact degradation threshold', shape: [3,5,8,6,7,7,7,7,7,7,7,7,7] },
19
+ { id: 'custom', name: 'Custom Test', desc: 'Define your own load stages', shape: [2,5,5,10,10,10,7,7,3,3,1,1,0] },
20
+ { id: 'ratelimit', name: 'Rate Limit', desc: 'Test if API rate limiting is working', shape: [2,4,6,8,10,12,12,12,12,12,12,12,12] },
21
+ { id: 'sequential', name: 'Sequential', desc: 'Run collection in order, N iterations', shape: [3,3,3,3,3,3,3,3,3,3,3,3,3] }
22
+ ];
23
+
24
+ var ASSERTION_METRICS = [
25
+ { value: 'p50_latency_ms', label: 'P50 Latency (ms)' },
26
+ { value: 'p95_latency_ms', label: 'P95 Latency (ms)' },
27
+ { value: 'p99_latency_ms', label: 'P99 Latency (ms)' },
28
+ { value: 'max_latency_ms', label: 'Max Latency (ms)' },
29
+ { value: 'mean_latency_ms', label: 'Mean Latency (ms)' },
30
+ { value: 'error_rate_pct', label: 'Error Rate (%)' },
31
+ { value: 'success_rate_pct', label: 'Success Rate (%)' },
32
+ { value: 'avg_rps', label: 'Avg RPS' },
33
+ { value: 'total_requests', label: 'Total Requests' },
34
+ { value: 'rate_limited_count', label: 'Rate Limited Count' }
35
+ ];
36
+ var ASSERTION_OPERATORS = ['<', '<=', '>', '>=', '=='];
37
+ var thresholds = [];
38
+
39
+ var CONFIG_FIELDS = {
40
+ load: [
41
+ { key: 'target_rps', label: 'Target requests/sec', type: 'range', min: 1, max: 1000, value: 50, tip: 'Sustained RPS during the hold phase. Start low and increase.' },
42
+ { key: 'ramp_up_seconds', label: 'Ramp up duration', type: 'range', min: 0, max: 300, value: 30, unit: 's', tip: 'Time to gradually reach target RPS. Longer = more realistic.' },
43
+ { key: 'hold_duration_seconds', label: 'Hold duration', type: 'range', min: 10, max: 3600, value: 300, unit: 's', tip: 'How long to sustain the target RPS. 300s = 5 min.' },
44
+ { key: 'ramp_down_seconds', label: 'Ramp down duration', type: 'range', min: 0, max: 60, value: 10, unit: 's', tip: 'Gradual cooldown period after hold.' }
45
+ ],
46
+ stress: [
47
+ { key: 'start_rps', label: 'Start requests/sec', type: 'range', min: 1, max: 100, value: 10, tip: 'Initial load level. Usually your known baseline.' },
48
+ { key: 'step_rps', label: 'Step increase', type: 'range', min: 5, max: 100, value: 20, tip: 'RPS added each step. Smaller = more precise breakpoint.' },
49
+ { key: 'step_duration_seconds', label: 'Step duration', type: 'range', min: 5, max: 120, value: 30, unit: 's', tip: 'How long to hold each step before increasing.' },
50
+ { key: 'max_rps', label: 'Max requests/sec', type: 'range', min: 50, max: 2000, value: 500, tip: 'Upper limit. Test stops here even if no failure.' },
51
+ { key: 'failure_threshold_pct', label: 'Failure threshold', type: 'range', min: 10, max: 100, value: 80, unit: '%', tip: 'Stop when error rate exceeds this percentage.' }
52
+ ],
53
+ spike: [
54
+ { key: 'baseline_rps', label: 'Baseline requests/sec', type: 'range', min: 1, max: 200, value: 20, tip: 'Normal traffic level before and after the spike.' },
55
+ { key: 'spike_rps', label: 'Spike requests/sec', type: 'range', min: 10, max: 2000, value: 200, tip: 'Traffic during the spike. Usually 5-10x baseline.' },
56
+ { key: 'baseline_duration_seconds', label: 'Baseline duration', type: 'range', min: 10, max: 300, value: 60, unit: 's', tip: 'Warm-up period at baseline before spike hits.' },
57
+ { key: 'spike_duration_seconds', label: 'Spike duration', type: 'range', min: 5, max: 120, value: 30, unit: 's', tip: 'How long the spike lasts.' },
58
+ { key: 'recovery_duration_seconds', label: 'Recovery monitoring', type: 'range', min: 10, max: 300, value: 60, unit: 's', tip: 'Time to monitor recovery after spike ends.' }
59
+ ],
60
+ soak: [
61
+ { key: 'soak_rps', label: 'Requests/sec', type: 'range', min: 1, max: 200, value: 30, tip: 'Steady RPS. Use your normal expected traffic level.' },
62
+ { key: 'soak_duration_seconds', label: 'Duration', type: 'range', min: 60, max: 7200, value: 1800, unit: 's', tip: 'How long to sustain. 1800s = 30 min. Longer finds more leaks.' }
63
+ ],
64
+ ramp: [
65
+ { key: 'ramp_start_rps', label: 'Start requests/sec', type: 'range', min: 1, max: 100, value: 10, tip: 'Starting RPS. Usually your minimum expected load.' },
66
+ { key: 'ramp_end_rps', label: 'End requests/sec', type: 'range', min: 10, max: 1000, value: 200, tip: 'Maximum RPS to reach.' },
67
+ { key: 'step_rps', label: 'Step size', type: 'range', min: 1, max: 50, value: 10, tip: 'RPS increment at each step.' },
68
+ { key: 'step_duration_seconds', label: 'Step duration', type: 'range', min: 5, max: 60, value: 15, unit: 's', tip: 'Hold time at each step to measure performance.' }
69
+ ],
70
+ burst: [
71
+ { key: 'total_requests', label: 'Total requests', type: 'range', min: 10, max: 5000, value: 200, tip: 'Number of requests to fire simultaneously.' }
72
+ ],
73
+ breakpoint: [
74
+ { key: 'start_rps', label: 'Start requests/sec', type: 'range', min: 1, max: 100, value: 10, tip: 'Lower bound for binary search.' },
75
+ { key: 'max_rps', label: 'Max requests/sec', type: 'range', min: 50, max: 2000, value: 500, tip: 'Upper bound for binary search.' },
76
+ { key: 'precision_rps', label: 'Precision', type: 'range', min: 1, max: 20, value: 5, tip: 'How close to the exact breakpoint. Lower = more precise but slower.' },
77
+ { key: 'latency_threshold_ms', label: 'Latency threshold', type: 'range', min: 100, max: 10000, value: 2000, unit: 'ms', tip: 'P95 latency above this = degradation detected.' },
78
+ { key: 'error_threshold_pct', label: 'Error threshold', type: 'range', min: 1, max: 50, value: 10, unit: '%', tip: 'Error rate above this = degradation detected.' }
79
+ ],
80
+ ratelimit: [
81
+ { key: 'rate_limit_cap', label: 'Expected rate limit', type: 'range', min: 1, max: 1000, value: 60, unit: 'req/s', tip: 'What you expect the rate limit to be.' },
82
+ { key: 'rate_limit_requests', label: 'Burst requests', type: 'range', min: 10, max: 1000, value: 120, tip: 'Number of requests to fire in burst phase.' }
83
+ ],
84
+ sequential: [
85
+ { key: 'iterations', label: 'Iterations', type: 'range', min: 1, max: 100, value: 1, tip: 'Times to run through the full collection.' },
86
+ { key: 'delay_ms', label: 'Delay between requests', type: 'range', min: 0, max: 5000, value: 0, unit: 'ms', tip: 'Wait time between each request.' }
87
+ ],
88
+ custom: []
89
+ };
90
+
91
+ function render(container) {
92
+ var coll = window.OverloadApp.getCollection();
93
+ if (!coll) {
94
+ container.innerHTML =
95
+ '<h1 class="page-title">Test Runner</h1>' +
96
+ '<div class="working-dir-banner">Run <strong>overload</strong> from the directory containing your Postman collections for auto-detection.</div>' +
97
+ '<div class="card"><p style="color:var(--mut)">Upload a collection first.</p>' +
98
+ '<button class="btn btn-secondary" style="margin-top:12px" onclick="window.OverloadApp.navigate(\'collection\')">Go to Collection</button></div>';
99
+ return;
100
+ }
101
+
102
+ container.innerHTML =
103
+ '<h1 class="page-title">Test Runner</h1>' +
104
+ '<p class="page-desc">Choose a test type and configure parameters</p>' +
105
+ '<div class="test-types" id="testTypes"></div>' +
106
+ '<div id="testConfig" style="display:none"></div>' +
107
+ '<div id="liveDashboard" style="display:none"></div>';
108
+
109
+ renderTestTypes();
110
+ }
111
+
112
+ function renderTestTypes() {
113
+ var container = document.getElementById('testTypes');
114
+ container.innerHTML = TEST_TYPES.map(function(tt) {
115
+ var shapeHtml = '<div class="test-card-shape">' +
116
+ tt.shape.map(function(h) { return '<span style="width:7%;height:' + (h * 100 / 13) + '%"></span>'; }).join('') +
117
+ '</div>';
118
+ return '<div class="test-card' + (selectedType === tt.id ? ' selected' : '') + '" data-type="' + tt.id + '">' +
119
+ '<div class="test-card-name">' + tt.name + '</div>' +
120
+ '<div class="test-card-desc">' + tt.desc + '</div>' +
121
+ shapeHtml +
122
+ '</div>';
123
+ }).join('');
124
+
125
+ container.querySelectorAll('.test-card').forEach(function(card) {
126
+ card.addEventListener('click', function() { selectTestType(card.dataset.type); });
127
+ });
128
+ }
129
+
130
+ function selectTestType(type) {
131
+ selectedType = type;
132
+ config = {};
133
+ renderTestTypes();
134
+ renderConfig();
135
+ document.getElementById('testConfig').style.display = 'block';
136
+ }
137
+
138
+ function renderConfig() {
139
+ var fields = CONFIG_FIELDS[selectedType] || [];
140
+ var html = '<div class="card">';
141
+ html += '<div class="card-title">Configuration</div>';
142
+
143
+ html += '<div class="shape-preview"><div class="chart-title">Load Shape Preview</div><canvas id="shapeChart"></canvas></div>';
144
+
145
+ if (selectedType === 'custom') {
146
+ html += renderStagesEditor();
147
+ } else {
148
+ fields.forEach(function(f) {
149
+ var val = config[f.key] !== undefined ? config[f.key] : f.value;
150
+ config[f.key] = val;
151
+ html += '<div class="config-row">';
152
+ html += '<div class="config-label">' + f.label;
153
+ if (f.tip) html += ' <span class="tooltip" data-tip="' + esc(f.tip) + '">?</span>';
154
+ html += '</div>';
155
+ html += '<div class="config-input">';
156
+ html += '<input type="range" min="' + f.min + '" max="' + f.max + '" value="' + val + '" data-key="' + f.key + '" class="config-slider">';
157
+ html += '<input type="number" min="' + f.min + '" max="' + f.max + '" value="' + val + '" data-key="' + f.key + '" class="config-number">';
158
+ if (f.unit) html += '<span style="color:var(--mut);font-size:12px">' + f.unit + '</span>';
159
+ html += '</div></div>';
160
+ });
161
+ }
162
+
163
+ // Common settings
164
+ html += '<button class="advanced-toggle" id="advancedToggle">&#9660; Advanced Settings</button>';
165
+ html += '<div class="advanced-content" id="advancedContent">';
166
+ var concurrency = config.concurrency || 20;
167
+ var timeout = config.timeout_seconds || 30;
168
+ config.concurrency = concurrency;
169
+ config.timeout_seconds = timeout;
170
+ html += '<div class="config-row"><div class="config-label">Max concurrent connections <span class="tooltip" data-tip="Number of simultaneous HTTP connections. Higher = more parallel requests.">?</span></div><div class="config-input">';
171
+ html += '<input type="range" min="1" max="200" value="' + concurrency + '" data-key="concurrency" class="config-slider">';
172
+ html += '<input type="number" min="1" max="200" value="' + concurrency + '" data-key="concurrency" class="config-number">';
173
+ html += '</div></div>';
174
+ html += '<div class="config-row"><div class="config-label">Request timeout <span class="tooltip" data-tip="Max wait time per request before marking as timeout.">?</span></div><div class="config-input">';
175
+ html += '<input type="range" min="1" max="120" value="' + timeout + '" data-key="timeout_seconds" class="config-slider">';
176
+ html += '<input type="number" min="1" max="120" value="' + timeout + '" data-key="timeout_seconds" class="config-number">';
177
+ html += '<span style="color:var(--mut);font-size:12px">s</span></div></div>';
178
+
179
+ var saveResp = config.save_responses || false;
180
+ config.save_responses = saveResp;
181
+ html += '<div class="save-response-row">';
182
+ html += '<input type="checkbox" id="saveResponsesCheck" ' + (saveResp ? 'checked' : '') + '>';
183
+ html += '<label for="saveResponsesCheck">Save response bodies <span style="color:var(--mut)">(captures first 10KB per response — useful for debugging)</span></label>';
184
+ html += '</div>';
185
+ html += '</div>';
186
+
187
+ // Assertions editor
188
+ html += '<button class="advanced-toggle" id="assertionsToggle">&#9660; Assertions (CI Thresholds)</button>';
189
+ html += '<div class="advanced-content" id="assertionsContent">';
190
+ html += renderAssertionsEditor();
191
+ html += '</div>';
192
+
193
+ html += '<div style="margin-top:20px;display:flex;gap:12px;flex-wrap:wrap">';
194
+ html += '<button class="btn btn-primary" id="startBtn">Start Test</button>';
195
+ html += '<button class="btn btn-secondary" id="saveConfigBtn">Save Config</button>';
196
+ html += '<button class="btn btn-secondary" id="loadConfigBtn">Load Config</button>';
197
+ html += '</div>';
198
+ html += '</div>';
199
+
200
+ document.getElementById('testConfig').innerHTML = html;
201
+
202
+ // Bind sliders to number inputs
203
+ document.querySelectorAll('.config-slider').forEach(function(slider) {
204
+ var key = slider.dataset.key;
205
+ var number = document.querySelector('.config-number[data-key="' + key + '"]');
206
+ slider.addEventListener('input', function() {
207
+ number.value = slider.value;
208
+ config[key] = parseInt(slider.value);
209
+ updateShapePreview();
210
+ });
211
+ number.addEventListener('input', function() {
212
+ var val = parseInt(number.value);
213
+ if (!isNaN(val)) {
214
+ slider.value = val;
215
+ config[key] = val;
216
+ updateShapePreview();
217
+ }
218
+ });
219
+ });
220
+
221
+ document.getElementById('saveResponsesCheck').addEventListener('change', function() {
222
+ config.save_responses = this.checked;
223
+ });
224
+
225
+ document.getElementById('advancedToggle').addEventListener('click', function() {
226
+ var content = document.getElementById('advancedContent');
227
+ content.classList.toggle('open');
228
+ this.innerHTML = (content.classList.contains('open') ? '&#9650;' : '&#9660;') + ' Advanced Settings';
229
+ });
230
+
231
+ document.getElementById('startBtn').addEventListener('click', startTest);
232
+
233
+ document.getElementById('assertionsToggle').addEventListener('click', function() {
234
+ var content = document.getElementById('assertionsContent');
235
+ content.classList.toggle('open');
236
+ this.innerHTML = (content.classList.contains('open') ? '&#9650;' : '&#9660;') + ' Assertions (CI Thresholds)';
237
+ });
238
+
239
+ document.getElementById('saveConfigBtn').addEventListener('click', saveConfigToFile);
240
+ document.getElementById('loadConfigBtn').addEventListener('click', loadConfigFromFile);
241
+
242
+ bindAssertionsEditor();
243
+ if (selectedType === 'custom') bindStagesEditor();
244
+
245
+ setTimeout(updateShapePreview, 50);
246
+ }
247
+
248
+ function renderStagesEditor() {
249
+ var stages = config.stages || [{ duration: 60, rps: 50 }, { duration: 120, rps: 100 }, { duration: 60, rps: 50 }];
250
+ config.stages = stages;
251
+ var html = '<div class="card-title" style="margin-top:12px">Stages</div>';
252
+ html += '<div class="stages-list" id="stagesList">';
253
+ stages.forEach(function(s, i) {
254
+ html += '<div class="stage-row" data-idx="' + i + '">';
255
+ html += '<span style="color:var(--mut);font-size:12px;width:20px">' + (i + 1) + '.</span>';
256
+ html += '<input type="number" value="' + s.duration + '" data-field="duration" min="1" placeholder="Duration"> <span style="color:var(--mut);font-size:12px">sec at</span> ';
257
+ html += '<input type="number" value="' + s.rps + '" data-field="rps" min="1" placeholder="RPS"> <span style="color:var(--mut);font-size:12px">req/s</span>';
258
+ html += '<button class="stage-remove" data-idx="' + i + '">&#10005;</button>';
259
+ html += '</div>';
260
+ });
261
+ html += '</div>';
262
+ html += '<div class="stage-add" id="addStage">+ Add Stage</div>';
263
+ var totalDur = stages.reduce(function(s, st) { return s + st.duration; }, 0);
264
+ var totalReqs = stages.reduce(function(s, st) { return s + st.duration * st.rps; }, 0);
265
+ html += '<div style="color:var(--mut);font-size:12px;margin-top:8px">Total: ' + totalDur + 's (' + Math.round(totalDur / 60) + 'min), ~' + totalReqs + ' requests</div>';
266
+ return html;
267
+ }
268
+
269
+ function bindStagesEditor() {
270
+ var list = document.getElementById('stagesList');
271
+ if (!list) return;
272
+ list.querySelectorAll('input').forEach(function(input) {
273
+ input.addEventListener('change', function() {
274
+ var row = input.closest('.stage-row');
275
+ var idx = parseInt(row.dataset.idx);
276
+ config.stages[idx][input.dataset.field] = parseInt(input.value) || 1;
277
+ renderConfig();
278
+ bindStagesEditor();
279
+ });
280
+ });
281
+ list.querySelectorAll('.stage-remove').forEach(function(btn) {
282
+ btn.addEventListener('click', function() {
283
+ config.stages.splice(parseInt(btn.dataset.idx), 1);
284
+ renderConfig();
285
+ bindStagesEditor();
286
+ });
287
+ });
288
+ var addBtn = document.getElementById('addStage');
289
+ if (addBtn) {
290
+ addBtn.addEventListener('click', function() {
291
+ config.stages.push({ duration: 60, rps: 50 });
292
+ renderConfig();
293
+ bindStagesEditor();
294
+ });
295
+ }
296
+ }
297
+
298
+ function renderAssertionsEditor() {
299
+ var html = '<div class="assertions-list" id="assertionsList">';
300
+ if (!thresholds.length) {
301
+ html += '<div style="color:var(--mut);font-size:11px;padding:6px 0">No assertions. Add one to enable pass/fail verdicts.</div>';
302
+ }
303
+ thresholds.forEach(function(t, i) {
304
+ html += '<div class="stage-row" data-idx="' + i + '">';
305
+ html += '<select data-field="metric" class="assertion-select">';
306
+ ASSERTION_METRICS.forEach(function(m) {
307
+ html += '<option value="' + m.value + '"' + (t.metric === m.value ? ' selected' : '') + '>' + m.label + '</option>';
308
+ });
309
+ html += '</select>';
310
+ html += '<select data-field="operator" class="assertion-op">';
311
+ ASSERTION_OPERATORS.forEach(function(op) {
312
+ html += '<option value="' + esc(op) + '"' + (t.operator === op ? ' selected' : '') + '>' + esc(op) + '</option>';
313
+ });
314
+ html += '</select>';
315
+ html += '<input type="number" value="' + t.value + '" data-field="value" min="0" step="any" placeholder="Value" class="assertion-value">';
316
+ html += '<button class="stage-remove" data-idx="' + i + '">&#10005;</button>';
317
+ html += '</div>';
318
+ });
319
+ html += '</div>';
320
+ html += '<div class="stage-add" id="addAssertion">+ Add Assertion</div>';
321
+ return html;
322
+ }
323
+
324
+ function bindAssertionsEditor() {
325
+ var list = document.getElementById('assertionsList');
326
+ if (!list) return;
327
+ list.querySelectorAll('select, input').forEach(function(el) {
328
+ el.addEventListener('change', function() {
329
+ var row = el.closest('.stage-row');
330
+ var idx = parseInt(row.dataset.idx);
331
+ var field = el.dataset.field;
332
+ if (field === 'value') {
333
+ thresholds[idx][field] = parseFloat(el.value) || 0;
334
+ } else {
335
+ thresholds[idx][field] = el.value;
336
+ }
337
+ });
338
+ });
339
+ list.querySelectorAll('.stage-remove').forEach(function(btn) {
340
+ btn.addEventListener('click', function() {
341
+ thresholds.splice(parseInt(btn.dataset.idx), 1);
342
+ var container = document.getElementById('assertionsContent');
343
+ if (container) {
344
+ container.innerHTML = renderAssertionsEditor();
345
+ bindAssertionsEditor();
346
+ }
347
+ });
348
+ });
349
+ var addBtn = document.getElementById('addAssertion');
350
+ if (addBtn) {
351
+ addBtn.addEventListener('click', function() {
352
+ thresholds.push({ metric: 'p95_latency_ms', operator: '<', value: 500 });
353
+ var container = document.getElementById('assertionsContent');
354
+ if (container) {
355
+ container.innerHTML = renderAssertionsEditor();
356
+ bindAssertionsEditor();
357
+ }
358
+ });
359
+ }
360
+ }
361
+
362
+ function saveConfigToFile() {
363
+ var payload = {
364
+ test_type: selectedType,
365
+ config: config,
366
+ thresholds: thresholds
367
+ };
368
+ fetch('/api/config/save', {
369
+ method: 'POST',
370
+ headers: { 'Content-Type': 'application/json' },
371
+ body: JSON.stringify(payload)
372
+ })
373
+ .then(function(r) { return r.json(); })
374
+ .then(function(data) {
375
+ if (data.status === 'ok') {
376
+ App.toast('Config saved to overload.config.yaml', 'success');
377
+ } else {
378
+ App.toast('Error: ' + data.message, 'error');
379
+ }
380
+ })
381
+ .catch(function(err) { App.toast('Save failed: ' + err.message, 'error'); });
382
+ }
383
+
384
+ function loadConfigFromFile() {
385
+ fetch('/api/config/load')
386
+ .then(function(r) { return r.json(); })
387
+ .then(function(data) {
388
+ if (data.status === 'ok' && data.config) {
389
+ var cfg = data.config;
390
+ if (cfg.test_type) {
391
+ selectTestType(cfg.test_type);
392
+ }
393
+ if (cfg.config) {
394
+ Object.keys(cfg.config).forEach(function(k) {
395
+ config[k] = cfg.config[k];
396
+ });
397
+ }
398
+ if (cfg.thresholds && cfg.thresholds.length) {
399
+ thresholds = cfg.thresholds.map(function(t) {
400
+ return { metric: t.metric, operator: t.operator, value: t.value };
401
+ });
402
+ }
403
+ renderConfig();
404
+ App.toast('Config loaded from overload.config.yaml', 'success');
405
+ } else {
406
+ App.toast(data.message || 'No config file found', 'error');
407
+ }
408
+ })
409
+ .catch(function(err) { App.toast('Load failed: ' + err.message, 'error'); });
410
+ }
411
+
412
+ function updateShapePreview() {
413
+ OverloadCharts.loadShapePreview('shapeChart', selectedType, config);
414
+ }
415
+
416
+ function startTest() {
417
+ var coll = window.OverloadApp.getCollection();
418
+ if (!coll || !coll.requests.length) {
419
+ App.toast('No collection loaded', 'error');
420
+ return;
421
+ }
422
+
423
+ var payload = {
424
+ test_type: selectedType,
425
+ config: config,
426
+ thresholds: thresholds
427
+ };
428
+
429
+ fetch('/api/test/start', {
430
+ method: 'POST',
431
+ headers: { 'Content-Type': 'application/json' },
432
+ body: JSON.stringify(payload)
433
+ })
434
+ .then(function(r) { return r.json(); })
435
+ .then(function(data) {
436
+ if (data.status === 'ok') {
437
+ isRunning = true;
438
+ currentRunId = data.run_id;
439
+ rpsHistory = [];
440
+ logEntries = [];
441
+ lastLogIdx = -1;
442
+ prevProgress = { completed: 0, time: Date.now() };
443
+ showLiveDashboard();
444
+ window.OverloadApp.subscribeToRun(currentRunId, onProgress);
445
+ } else {
446
+ App.toast('Error: ' + data.message, 'error');
447
+ }
448
+ })
449
+ .catch(function(err) { App.toast('Failed to start: ' + err.message, 'error'); });
450
+ }
451
+
452
+ function showLiveDashboard() {
453
+ document.getElementById('testConfig').style.display = 'none';
454
+ document.getElementById('testTypes').style.display = 'none';
455
+ var dash = document.getElementById('liveDashboard');
456
+ dash.style.display = 'block';
457
+ dash.innerHTML =
458
+ '<div class="card">' +
459
+ '<div class="card-title">Running: ' + selectedType.toUpperCase() + ' — ' + currentRunId + '</div>' +
460
+ '<div class="kpi-grid" id="liveKpis">' +
461
+ '<div class="kpi kpi-mid"><div class="kpi-label">Total</div><div class="kpi-value" id="kpiTotal">0</div></div>' +
462
+ '<div class="kpi kpi-ok"><div class="kpi-label">Success Rate</div><div class="kpi-value" id="kpiSuccess">-</div></div>' +
463
+ '<div class="kpi kpi-blue"><div class="kpi-label">Avg Latency</div><div class="kpi-value" id="kpiLatency">-</div></div>' +
464
+ '<div class="kpi kpi-ok"><div class="kpi-label">Current RPS</div><div class="kpi-value" id="kpiRps">0</div></div>' +
465
+ '<div class="kpi kpi-mid"><div class="kpi-label">Elapsed</div><div class="kpi-value" id="kpiElapsed">0s</div></div>' +
466
+ '<div class="kpi kpi-bad"><div class="kpi-label">Errors</div><div class="kpi-value" id="kpiErrors">0</div></div>' +
467
+ '</div>' +
468
+ '<div class="progress-wrap">' +
469
+ '<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>' +
470
+ '<div class="progress-text"><span id="progressPhase">Starting...</span><span id="progressPct">0%</span></div>' +
471
+ '</div>' +
472
+ '<div class="chart-grid">' +
473
+ '<div class="chart-card"><div class="chart-title">Live RPS</div><canvas id="liveRpsChart"></canvas></div>' +
474
+ '<div class="chart-card"><div class="chart-title">Status Codes</div><canvas id="liveStatusChart"></canvas></div>' +
475
+ '</div>' +
476
+ '<div class="live-log" id="liveLog">' +
477
+ '<div class="live-log-title">Request Log</div>' +
478
+ '<div id="liveLogEntries"><div class="live-log-empty">Waiting for requests...</div></div>' +
479
+ '</div>' +
480
+ '<div style="margin-top:16px"><button class="btn btn-danger" id="stopBtn">Stop Test</button></div>' +
481
+ '</div>';
482
+
483
+ document.getElementById('stopBtn').addEventListener('click', stopTest);
484
+ }
485
+
486
+ function statusBadgeClass(code) {
487
+ if (code <= 0) return 'status-0';
488
+ if (code < 200) return 'status-1xx';
489
+ if (code < 300) return 'status-2xx';
490
+ if (code < 400) return 'status-3xx';
491
+ if (code < 500) return 'status-4xx';
492
+ return 'status-5xx';
493
+ }
494
+
495
+ function onProgress(data) {
496
+ if (!data) return;
497
+
498
+ document.getElementById('kpiTotal').textContent = data.completed_requests;
499
+ document.getElementById('kpiRps').textContent = data.current_rps;
500
+ document.getElementById('kpiLatency').textContent = data.avg_latency_ms ? data.avg_latency_ms + 'ms' : '-';
501
+ document.getElementById('kpiElapsed').textContent = data.elapsed_seconds + 's';
502
+ document.getElementById('kpiErrors').textContent = data.error_count || 0;
503
+
504
+ var total = data.total_requests || data.completed_requests;
505
+ var successRate = data.completed_requests > 0 && data.error_count !== undefined
506
+ ? Math.round((data.completed_requests - data.error_count) * 100 / data.completed_requests) + '%'
507
+ : '-';
508
+ document.getElementById('kpiSuccess').textContent = successRate;
509
+
510
+ var pct = total > 0 ? Math.round(data.completed_requests * 100 / total) : 0;
511
+ if (data.phase === 'complete' || (data.phase && data.phase.indexOf('complete') === 0)) pct = 100;
512
+ var fill = document.getElementById('progressFill');
513
+ if (fill) fill.style.width = pct + '%';
514
+ var pctEl = document.getElementById('progressPct');
515
+ if (pctEl) pctEl.textContent = pct + '%';
516
+ var phaseEl = document.getElementById('progressPhase');
517
+ if (phaseEl) phaseEl.textContent = data.phase || '';
518
+
519
+ // Live RPS chart
520
+ if (data.current_rps > 0 || rpsHistory.length > 0) {
521
+ rpsHistory.push({ t: Math.round(data.elapsed_seconds), rps: data.current_rps });
522
+ if (rpsHistory.length > 120) rpsHistory = rpsHistory.slice(-120);
523
+ OverloadCharts.liveRpsLine('liveRpsChart', rpsHistory);
524
+ }
525
+
526
+ // Status doughnut
527
+ if (data.status_codes) {
528
+ OverloadCharts.statusDoughnut('liveStatusChart', data.status_codes);
529
+ }
530
+
531
+ // Live log
532
+ if (data.recent_results && data.recent_results.length) {
533
+ var container = document.getElementById('liveLogEntries');
534
+ var empty = container.querySelector('.live-log-empty');
535
+ if (empty) empty.remove();
536
+
537
+ data.recent_results.forEach(function(r) {
538
+ if (r.idx <= lastLogIdx) return;
539
+ lastLogIdx = r.idx;
540
+ logEntries.push(r);
541
+
542
+ var entry = document.createElement('div');
543
+ entry.className = 'live-log-entry';
544
+ var methodClass = 'method-' + r.method;
545
+ entry.innerHTML =
546
+ '<span class="log-idx">' + (r.idx + 1) + '</span>' +
547
+ '<span class="log-method ' + methodClass + '">' + r.method + '</span>' +
548
+ '<span class="status-badge ' + statusBadgeClass(r.status) + '">' + r.status + '</span>' +
549
+ '<span class="log-name">' + esc(r.name) + '</span>' +
550
+ '<span class="log-latency">' + r.latency + 'ms</span>';
551
+ if (r.error) {
552
+ entry.innerHTML += '<span style="color:var(--bad);font-size:10px;margin-left:4px">' + esc(r.error) + '</span>';
553
+ }
554
+ container.appendChild(entry);
555
+ });
556
+
557
+ // Keep only last 100 entries in DOM
558
+ while (container.children.length > 100) {
559
+ container.removeChild(container.firstChild);
560
+ }
561
+
562
+ // Auto-scroll to bottom
563
+ var logPanel = document.getElementById('liveLog');
564
+ if (logPanel) logPanel.scrollTop = logPanel.scrollHeight;
565
+ }
566
+
567
+ // Test complete
568
+ if (data.phase === 'complete' || (data.phase && data.phase.indexOf('complete') === 0)) {
569
+ isRunning = false;
570
+ var stopBtn = document.getElementById('stopBtn');
571
+ if (stopBtn) {
572
+ stopBtn.textContent = 'View Results';
573
+ stopBtn.className = 'btn btn-primary';
574
+ stopBtn.onclick = function() { window.OverloadApp.navigate('results'); };
575
+ }
576
+ App.toast('Test complete! ' + data.completed_requests + ' requests.', 'success');
577
+
578
+ if (thresholds.length && currentRunId) {
579
+ fetch('/api/runs/' + currentRunId + '/data')
580
+ .then(function(r) { return r.json(); })
581
+ .then(function(runData) {
582
+ if (runData.verdict) {
583
+ var v = runData.verdict;
584
+ var banner = document.createElement('div');
585
+ banner.className = 'verdict-banner ' + (v.passed ? 'verdict-pass' : 'verdict-fail');
586
+ var icon = v.passed ? '&#x2705;' : '&#x274C;';
587
+ var html = '<div class="verdict-icon">' + icon + '</div>';
588
+ html += '<div class="verdict-body"><div class="verdict-title">' + (v.passed ? 'PASS' : 'FAIL') + '</div>';
589
+ html += '<div class="verdict-details">';
590
+ v.results.forEach(function(r) {
591
+ var mark = r.passed ? '<span style="color:var(--ok)">&#10003;</span>' : '<span style="color:var(--bad)">&#10007;</span>';
592
+ html += '<div>' + mark + ' ' + esc(r.metric) + ': ' + r.actual + ' ' + esc(r.operator) + ' ' + r.expected + '</div>';
593
+ });
594
+ html += '</div></div>';
595
+ banner.innerHTML = html;
596
+ var dash = document.getElementById('liveDashboard');
597
+ if (dash) dash.querySelector('.card').prepend(banner);
598
+ }
599
+ })
600
+ .catch(function() {});
601
+ }
602
+ }
603
+ }
604
+
605
+ function stopTest() {
606
+ fetch('/api/test/stop', { method: 'POST' })
607
+ .then(function() { App.toast('Stop signal sent', 'success'); })
608
+ .catch(function(err) { App.toast('Error: ' + err.message, 'error'); });
609
+ }
610
+
611
+ function esc(s) {
612
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
613
+ }
614
+
615
+ function reset() {
616
+ isRunning = false;
617
+ currentRunId = null;
618
+ rpsHistory = [];
619
+ logEntries = [];
620
+ lastLogIdx = -1;
621
+ thresholds = [];
622
+ }
623
+
624
+ return { render: render, reset: reset };
625
+ })();
@@ -0,0 +1,23 @@
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">
6
+ <title>Overload — Load Testing</title>
7
+ <link rel="stylesheet" href="/static/css/app.css">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ </head>
10
+ <body>
11
+ <nav id="sidebar">
12
+ <div class="logo">OVERLOAD</div>
13
+ <a href="#" data-page="collection" class="nav-link active">Collection</a>
14
+ <a href="#" data-page="runner" class="nav-link">Test Runner</a>
15
+ <a href="#" data-page="results" class="nav-link">Results</a>
16
+ </nav>
17
+ <main id="content"></main>
18
+ <script src="/static/js/charts.js"></script>
19
+ <script src="/static/js/collection.js"></script>
20
+ <script src="/static/js/runner.js"></script>
21
+ <script src="/static/js/app.js"></script>
22
+ </body>
23
+ </html>