feat: add desktop/mobile device toggle (User-Agent)

- Segmented Desktop / Mobile button in the form
- Desktop: Chrome 124 on Windows
- Mobile: iPhone Safari 17 (iOS 17.4)
- User-Agent injected into k6 script headers
- Stored in DB with migration for existing data
- History items show device pill alongside cache/gzip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 20:13:13 +02:00
parent 6ef7564e87
commit cf51a4620b
4 changed files with 72 additions and 8 deletions

View File

@@ -39,6 +39,20 @@ gzipChk.addEventListener('change', () => {
: 'Sends Accept-Encoding: identity — forces uncompressed response';
});
// Device toggle hint
const UA = {
desktop: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
mobile: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1',
};
const uaHint = document.getElementById('ua-hint');
document.querySelectorAll('input[name="device"]').forEach(radio => {
radio.addEventListener('change', () => {
uaHint.textContent = radio.value === 'mobile'
? 'iPhone Safari 17 (iOS 17.4)'
: 'Chrome 124 on Windows';
});
});
// ---- Run Test ----
const form = document.getElementById('test-form');
const startBtn = document.getElementById('start-btn');
@@ -60,6 +74,7 @@ form.addEventListener('submit', async (e) => {
const headers = document.getElementById('headers').value.trim() || '{}';
const cacheMode = document.getElementById('cacheMode').value;
const gzip = document.getElementById('gzip').checked;
const device = document.querySelector('input[name="device"]:checked').value;
// Validate headers JSON
try { JSON.parse(headers); } catch {
@@ -80,7 +95,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, cacheMode, gzip }),
body: JSON.stringify({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip, device }),
});
} catch (err) {
alert('Failed to start test: ' + err.message);
@@ -238,6 +253,9 @@ async function loadHistory() {
summaryLine = parts.join(' · ');
}
const deviceBadge = t.device === 'mobile'
? '<span class="pill mobile">&#128241; mobile</span>'
: '<span class="pill desktop">&#x1F5A5; desktop</span>';
const cacheBadge = t.cache_mode === 'bust'
? '<span class="pill bust">cache-bust</span>'
: t.cache_mode === 'no-cache'
@@ -253,7 +271,7 @@ async function loadHistory() {
<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}
${deviceBadge}${cacheBadge}${gzipBadge}
</div>
<div class="meta">${date}</div>
${summaryLine ? `<div class="history-summary">${escHtml(summaryLine)}</div>` : ''}

View File

@@ -67,6 +67,17 @@
</select>
</div>
<div class="form-group">
<label for="device">Device</label>
<div class="device-toggle">
<input type="radio" name="device" id="device-desktop" value="desktop" checked />
<label for="device-desktop" class="device-btn">&#x1F5A5; Desktop</label>
<input type="radio" name="device" id="device-mobile" value="mobile" />
<label for="device-mobile" class="device-btn">&#128241; Mobile</label>
</div>
<div class="hint" id="ua-hint">Chrome 124 on Windows</div>
</div>
<div class="form-group">
<label>Encoding</label>
<div class="toggle-row">

View File

@@ -152,6 +152,30 @@ textarea { font-family: var(--mono); resize: vertical; }
.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); }
.pill.desktop { background: rgba(56,189,248,.12); color: #38bdf8; }
.pill.mobile { background: rgba(251,146,60,.12); color: #fb923c; }
/* DEVICE TOGGLE */
.device-toggle { display: flex; gap: 0; margin-top: .15rem; }
.device-toggle input[type=radio] { display: none; }
.device-btn {
flex: 1;
text-align: center;
padding: .45rem .75rem;
font-size: .85rem;
border: 1px solid var(--border);
cursor: pointer;
transition: background .15s, color .15s;
color: var(--muted);
background: transparent;
}
.device-btn:first-of-type { border-radius: 6px 0 0 6px; }
.device-btn:last-of-type { border-radius: 0 6px 6px 0; border-left: none; }
.device-toggle input[type=radio]:checked + .device-btn {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.btn-secondary:hover { background: rgba(124,58,237,.15); }
/* RESULT */