feat: add gzip toggle and cache mode (normal / no-cache / bust)

- Cache-mode selector: Normal, No-cache headers, Cache-bust URL+headers
- Cache-bust appends random ?_cb= per iteration to bypass CDN/proxy
- gzip toggle: on = k6 default (Accept-Encoding: gzip/br), off = identity
- Both options stored in DB with non-destructive migration
- History items show cache-mode and gzip pills
- Schema migration handles existing DBs gracefully

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 20:03:36 +02:00
parent 0cd0f96bab
commit 6ef7564e87
4 changed files with 119 additions and 11 deletions

View File

@@ -30,6 +30,15 @@ methodSel.addEventListener('change', () => {
bodyGroup.style.display = (m !== 'GET' && m !== 'HEAD') ? '' : 'none';
});
// gzip toggle hint
const gzipChk = document.getElementById('gzip');
const gzipHint = document.getElementById('gzip-hint');
gzipChk.addEventListener('change', () => {
gzipHint.textContent = gzipChk.checked
? 'k6 default — server may compress response'
: 'Sends Accept-Encoding: identity — forces uncompressed response';
});
// ---- Run Test ----
const form = document.getElementById('test-form');
const startBtn = document.getElementById('start-btn');
@@ -49,6 +58,8 @@ form.addEventListener('submit', async (e) => {
const httpMethod = document.getElementById('httpMethod').value;
const requestBody = document.getElementById('requestBody').value.trim();
const headers = document.getElementById('headers').value.trim() || '{}';
const cacheMode = document.getElementById('cacheMode').value;
const gzip = document.getElementById('gzip').checked;
// Validate headers JSON
try { JSON.parse(headers); } catch {
@@ -69,7 +80,7 @@ form.addEventListener('submit', async (e) => {
res = await fetch('/api/tests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers }),
body: JSON.stringify({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip }),
});
} catch (err) {
alert('Failed to start test: ' + err.message);
@@ -227,12 +238,22 @@ async function loadHistory() {
summaryLine = parts.join(' · ');
}
const cacheBadge = t.cache_mode === 'bust'
? '<span class="pill bust">cache-bust</span>'
: t.cache_mode === 'no-cache'
? '<span class="pill no-cache">no-cache</span>'
: '<span class="pill cached">cached</span>';
const gzipBadge = t.gzip
? '<span class="pill gzip">gzip</span>'
: '<span class="pill no-gzip">no-gzip</span>';
item.innerHTML = `
<div>
<div class="row1">
<span class="badge ${t.status}">${t.status}</span>
<span class="url">${escHtml(t.url)}</span>
<span class="meta">${t.http_method} · ${t.vus} VUs · ${dur}</span>
${cacheBadge}${gzipBadge}
</div>
<div class="meta">${date}</div>
${summaryLine ? `<div class="history-summary">${escHtml(summaryLine)}</div>` : ''}

View File

@@ -58,6 +58,27 @@
<input type="number" id="rpsLimit" min="0" value="0" />
</div>
<div class="form-group">
<label for="cacheMode">Cache Mode</label>
<select id="cacheMode">
<option value="normal">Normal (allow cache)</option>
<option value="no-cache">No-cache headers (revalidate)</option>
<option value="bust">Cache-bust URL + headers (full bypass)</option>
</select>
</div>
<div class="form-group">
<label>Encoding</label>
<div class="toggle-row">
<label class="toggle">
<input type="checkbox" id="gzip" checked />
<span class="toggle-track"></span>
<span class="toggle-label">Accept gzip / br</span>
</label>
</div>
<div class="hint" id="gzip-hint">k6 default — server may compress response</div>
</div>
<div class="form-group full" id="body-group" style="display:none">
<label for="requestBody">Request Body</label>
<textarea id="requestBody" rows="4" placeholder='{"key": "value"}'></textarea>
@@ -65,7 +86,7 @@
<div class="form-group full">
<label for="headers">
Custom Headers <span class="hint">(JSON, optional)</span>
Custom Headers <span class="hint">(JSON, optional — merged with above options)</span>
</label>
<input type="text" id="headers" placeholder='{"Authorization": "Bearer token"}' />
</div>

View File

@@ -112,6 +112,46 @@ textarea { font-family: var(--mono); resize: vertical; }
cursor: pointer;
transition: background .15s;
}
/* TOGGLE SWITCH */
.toggle-row { display: flex; align-items: center; gap: .75rem; margin-top: .15rem; }
.toggle { display: flex; align-items: center; gap: .55rem; cursor: pointer; }
.toggle input { display: none; }
.toggle-track {
width: 38px; height: 20px;
background: var(--border);
border-radius: 999px;
position: relative;
transition: background .2s;
flex-shrink: 0;
}
.toggle-track::after {
content: '';
position: absolute;
width: 14px; height: 14px;
background: #fff;
border-radius: 50%;
top: 3px; left: 3px;
transition: transform .2s;
}
.toggle input:checked + .toggle-track { background: var(--accent); }
.toggle input:checked + .toggle-track::after { transform: translateX(18px); }
.toggle-label { font-size: .88rem; color: var(--text); }
/* PILLS */
.pill {
display: inline-block;
font-size: .72rem;
font-weight: 600;
padding: .15rem .5rem;
border-radius: 999px;
letter-spacing: .04em;
}
.pill.cached { background: rgba(34,197,94,.12); color: #4ade80; }
.pill.no-cache { background: rgba(245,158,11,.12); color: #fbbf24; }
.pill.bust { background: rgba(239,68,68,.12); color: #f87171; }
.pill.gzip { background: rgba(124,58,237,.15); color: var(--accent2); }
.pill.no-gzip { background: rgba(100,116,139,.12); color: var(--muted); }
.btn-secondary:hover { background: rgba(124,58,237,.15); }
/* RESULT */