wa-dev-tools/public/httpclient.html
treamz 69ec8f4ede UI: light theme default, remove free mentions, fix scroll animation
- Default theme: light (was dark)
- Removed all бесплатно/free/навсегда mentions across all pages
- Landing: title, hero badge, hero text, CTA button, advantages, footer CTA
- Dashboard: hero text, about section, stats
- Register: subtitle
- Fixed scroll-hint animation (was bouncing infinitely, now static)
- Removed class=dark from all 18 HTML files (theme managed by JS)
2026-03-22 00:10:12 +03:00

702 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HTTP Client — WA Dev Tools</title>
<script src="/vendor/tailwind.js"></script>
<script>window.WA_TOOL_ID = 'httpclient';</script>
<script src="/shared.js"></script>
<link href="/vendor/fonts.css" rel="stylesheet">
<link href="/shared.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/hljs-github-dark.css">
<style>
/* HTTP Client UI */
.panel {
background: var(--surface-800);
border: 1px solid var(--surface-600);
border-radius: 12px;
overflow: hidden;
}
.tab-btn {
padding: 6px 14px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
border-radius: 8px;
border: none;
background: transparent;
cursor: pointer;
transition: all .2s;
white-space: nowrap;
}
.tab-btn:hover { color: var(--text-secondary); background: rgba(0,84,230,.05); }
.tab-btn.active { color: #0054e6; background: rgba(0,84,230,.08); }
.kv-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.kv-row input {
flex: 1;
background: var(--surface-800);
border: 1px solid var(--surface-600);
border-radius: 6px;
padding: 5px 8px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: var(--select-color);
outline: none;
min-width: 0;
}
.kv-row input:focus { border-color: rgba(0,84,230,.5); }
.kv-row input::placeholder { color: var(--text-muted); }
.kv-del {
width: 24px; height: 24px;
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
color: var(--text-muted);
transition: all .15s;
flex-shrink: 0;
}
.kv-del:hover { color: #ff5252; background: rgba(255,82,82,.1); }
.kv-add {
font-size: 11px; font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
cursor: pointer;
padding: 4px 0;
transition: color .15s;
}
.kv-add:hover { color: #0054e6; }
.method-badge {
display: inline-flex;
align-items: center;
padding: 1px 7px;
border-radius: 4px;
font-size: 10px;
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
}
.m-GET { background: rgba(0,168,107,.15); color: #00a86b; }
.m-POST { background: rgba(255,165,0,.15); color: #ffa500; }
.m-PUT { background: rgba(0,84,230,.15); color: #6b9fff; }
.m-PATCH { background: rgba(148,0,211,.15); color: #cc66ff; }
.m-DELETE { background: rgba(255,82,82,.15); color: #ff5252; }
.m-HEAD { background: rgba(150,150,150,.15); color: #aaa; }
.m-OPTIONS{ background: rgba(150,150,150,.15); color: #aaa; }
.status-2xx { color: #00a86b; }
.status-3xx { color: #6b9fff; }
.status-4xx { color: #ffa500; }
.status-5xx { color: #ff5252; }
.send-btn {
background: #0054e6;
color: #fff;
border: none;
border-radius: 8px;
padding: 0 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex; align-items: center; gap: 6px;
transition: background .2s;
white-space: nowrap;
height: 40px;
}
.send-btn:hover { background: #0043b8; }
.send-btn:disabled { background: #0054e680; cursor: not-allowed; }
.url-input {
flex: 1;
background: var(--surface-800);
border: 1px solid var(--surface-600);
border-radius: 8px;
padding: 0 12px;
font-size: 13px;
font-family: 'JetBrains Mono', monospace;
color: var(--select-color);
outline: none;
height: 40px;
}
.url-input:focus { border-color: rgba(0,84,230,.5); }
.url-input::placeholder { color: var(--text-muted); }
select.method-select {
background: var(--surface-800);
border: 1px solid var(--surface-600);
border-radius: 8px;
padding: 0 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
color: var(--select-color);
outline: none;
cursor: pointer;
height: 40px;
}
select.method-select:focus { border-color: rgba(0,84,230,.5); }
.response-body-pre {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
padding: 12px;
margin: 0;
min-height: 200px;
overflow: auto;
color: var(--text-secondary);
}
.hljs { background: transparent !important; padding: 0 !important; }
.history-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 8px;
cursor: pointer;
transition: background .15s;
}
.history-item:hover { background: rgba(255,255,255,.04); }
.history-url {
flex: 1;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.history-meta {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--text-muted);
white-space: nowrap;
}
.body-type-select {
background: var(--surface-800);
border: 1px solid var(--surface-600);
border-radius: 6px;
padding: 4px 8px;
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
color: var(--select-color);
outline: none;
cursor: pointer;
}
textarea.body-area {
width: 100%;
background: var(--surface-800);
border: 1px solid var(--surface-600);
border-radius: 8px;
padding: 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: var(--select-color);
outline: none;
resize: vertical;
min-height: 160px;
line-height: 1.6;
transition: border-color .2s;
}
textarea.body-area:focus { border-color: rgba(0,84,230,.5); }
textarea.body-area::placeholder { color: var(--text-muted); }
.spinner {
width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin .6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.panels-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media(max-width: 900px) {
.panels-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
<div class="main-content">
<div class="tool-container relative z-10 max-w-7xl mx-auto px-4">
<!-- Header -->
<div class="page-header">
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
HTTP <span class="text-accent">Клиент</span>
</h1>
<p class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">тест API запросов</p>
</div>
<!-- URL Bar -->
<div class="flex gap-2 mb-4">
<select id="method" class="method-select">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
</select>
<input id="urlInput" type="text" class="url-input" placeholder="https://api.example.com/endpoint" autocomplete="off" spellcheck="false" />
<button id="sendBtn" class="send-btn">
<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
Отправить
</button>
</div>
<!-- Panels -->
<div class="panels-grid mb-4">
<!-- REQUEST PANEL -->
<div class="panel">
<div class="flex border-b border-surface-600">
<button class="tab-btn active" data-reqtab="params">Параметры</button>
<button class="tab-btn" data-reqtab="headers">Заголовки</button>
<button class="tab-btn" data-reqtab="body">Тело</button>
<button class="tab-btn" data-reqtab="auth">Авторизация</button>
</div>
<!-- Params tab -->
<div id="reqTab-params" class="p-3">
<div id="params-rows"></div>
<div class="kv-add" id="params-add">+ Добавить параметр</div>
</div>
<!-- Headers tab -->
<div id="reqTab-headers" class="p-3 hidden">
<div id="headers-rows"></div>
<div class="kv-add" id="headers-add">+ Добавить заголовок</div>
</div>
<!-- Body tab -->
<div id="reqTab-body" class="p-3 hidden">
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono text-muted">Тип:</span>
<select id="bodyType" class="body-type-select">
<option value="none">нет</option>
<option value="json">JSON</option>
<option value="form">form-urlencoded</option>
<option value="raw">текст</option>
</select>
</div>
<div id="body-none" class="text-xs font-mono text-muted py-4 text-center">Тело запроса не отправляется</div>
<div id="body-json" class="hidden">
<textarea id="bodyJson" class="body-area" placeholder='{"key": "value"}'></textarea>
</div>
<div id="body-form" class="hidden">
<div id="form-rows"></div>
<div class="kv-add" id="form-add">+ Добавить поле</div>
</div>
<div id="body-raw" class="hidden">
<textarea id="bodyRaw" class="body-area" placeholder="Raw request body..."></textarea>
</div>
</div>
<!-- Auth tab -->
<div id="reqTab-auth" class="p-3 hidden">
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono text-muted">Тип:</span>
<select id="authType" class="body-type-select">
<option value="none">нет</option>
<option value="bearer">Bearer Токен</option>
<option value="basic">Basic Auth</option>
</select>
</div>
<div id="auth-none" class="text-xs font-mono text-muted py-4 text-center">Без авторизации</div>
<div id="auth-bearer" class="hidden">
<input id="bearerToken" type="text" class="kv-row" style="width:100%;display:block;margin:0;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Токен..." />
</div>
<div id="auth-basic" class="hidden">
<input id="basicUser" type="text" style="width:100%;display:block;margin-bottom:8px;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Логин" />
<input id="basicPass" type="password" style="width:100%;display:block;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Пароль" />
</div>
</div>
</div>
<!-- RESPONSE PANEL -->
<div class="panel">
<!-- Status bar -->
<div id="responseStatusBar" class="flex items-center gap-3 px-4 py-2 border-b border-surface-600 min-h-[41px]">
<span class="text-xs font-mono text-muted">Нажмите «Отправить» для запроса</span>
</div>
<!-- Tabs -->
<div class="flex border-b border-surface-600">
<button class="tab-btn active" data-restab="body">Тело</button>
<button class="tab-btn" data-restab="headers">Заголовки</button>
</div>
<!-- Body -->
<div id="resTab-body">
<div id="responseBodyWrap" style="max-height:480px;overflow:auto;position:relative">
<div id="responseBodyEmpty" class="flex flex-col items-center justify-center py-16 text-center">
<svg width="32" height="32" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1" class="mb-3 opacity-20"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
<span class="text-xs font-mono text-muted">Ответ появится здесь</span>
</div>
<pre id="responseBodyPre" class="response-body-pre hidden"><code id="responseBodyCode"></code></pre>
</div>
<div id="responseCopyBar" class="hidden flex items-center justify-between px-3 py-1.5 border-t border-surface-600">
<span id="responseSizeLabel" class="text-xs font-mono text-muted"></span>
<button id="copyBodyBtn" class="text-xs font-mono text-muted hover:text-accent transition-colors">Копировать</button>
</div>
</div>
<!-- Response Headers -->
<div id="resTab-headers" class="hidden p-3" style="max-height:480px;overflow:auto">
<div id="responseHeadersContent" class="text-xs font-mono space-y-1"></div>
</div>
</div>
</div>
<!-- History -->
<div class="panel">
<div class="flex items-center justify-between px-4 py-2 border-b border-surface-600 cursor-pointer select-none" id="historyToggle">
<span class="text-xs font-mono font-semibold" style="color:var(--text-secondary)">История запросов</span>
<div class="flex items-center gap-3">
<button id="clearHistoryBtn" class="text-xs font-mono text-muted hover:text-danger transition-colors hidden">Очистить</button>
<svg id="historyArrow" width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" style="color:var(--text-muted);transition:transform .2s"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
</div>
</div>
<div id="historyList" class="px-2 py-1" style="max-height:260px;overflow-y:auto"></div>
</div>
</div>
</div><!-- /main-content -->
<script src="/vendor/highlight.min.js"></script>
<script>
(() => {
// State
let reqTabs = {}, resTabs = {};
let isLoading = false;
// KV row builder
function makeKvRow(container, keyVal, valVal) {
const row = document.createElement('div');
row.className = 'kv-row';
const k = document.createElement('input');
k.type = 'text'; k.placeholder = 'Ключ'; k.value = keyVal || '';
k.className = 'kv-input-key';
const v = document.createElement('input');
v.type = 'text'; v.placeholder = 'Значение'; v.value = valVal || '';
v.className = 'kv-input-val';
const del = document.createElement('div');
del.className = 'kv-del';
del.innerHTML = '<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>';
del.onclick = () => row.remove();
row.appendChild(k); row.appendChild(v); row.appendChild(del);
container.appendChild(row);
return row;
}
function getKvRows(container) {
const result = {};
container.querySelectorAll('.kv-row').forEach(row => {
const k = row.querySelector('.kv-input-key')?.value?.trim();
const v = row.querySelector('.kv-input-val')?.value?.trim();
if (k) result[k] = v || '';
});
return result;
}
// Init KV containers
const paramsRows = document.getElementById('params-rows');
const headersRows = document.getElementById('headers-rows');
const formRows = document.getElementById('form-rows');
makeKvRow(paramsRows, '', '');
makeKvRow(headersRows, 'Content-Type', 'application/json');
makeKvRow(formRows, '', '');
document.getElementById('params-add').onclick = () => makeKvRow(paramsRows, '', '');
document.getElementById('headers-add').onclick = () => makeKvRow(headersRows, '', '');
document.getElementById('form-add').onclick = () => makeKvRow(formRows, '', '');
// Request tabs
document.querySelectorAll('[data-reqtab]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-reqtab]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.reqtab;
['params','headers','body','auth'].forEach(t => {
document.getElementById('reqTab-' + t).classList.toggle('hidden', t !== tab);
});
});
});
// Response tabs
document.querySelectorAll('[data-restab]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-restab]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.restab;
['body','headers'].forEach(t => {
document.getElementById('resTab-' + t).classList.toggle('hidden', t !== tab);
});
});
});
// Body type
document.getElementById('bodyType').addEventListener('change', function() {
['none','json','form','raw'].forEach(t => {
document.getElementById('body-' + t).classList.toggle('hidden', t !== this.value);
});
});
// Auth type
document.getElementById('authType').addEventListener('change', function() {
['none','bearer','basic'].forEach(t => {
document.getElementById('auth-' + t).classList.toggle('hidden', t !== this.value);
});
});
// Build request config
function buildRequest() {
const method = document.getElementById('method').value;
let url = document.getElementById('urlInput').value.trim();
if (!url) return null;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
document.getElementById('urlInput').value = url;
}
// Params -> append to URL
const params = getKvRows(paramsRows);
if (Object.keys(params).length > 0) {
try {
const u = new URL(url);
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v));
url = u.toString();
} catch {}
}
// Headers
const headers = getKvRows(headersRows);
// Auth
const authType = document.getElementById('authType').value;
if (authType === 'bearer') {
const tok = document.getElementById('bearerToken').value.trim();
if (tok) headers['Authorization'] = 'Bearer ' + tok;
} else if (authType === 'basic') {
const u = document.getElementById('basicUser').value;
const p = document.getElementById('basicPass').value;
headers['Authorization'] = 'Basic ' + btoa(u + ':' + p);
}
// Body
let body = null;
const bodyType = document.getElementById('bodyType').value;
if (!['GET','HEAD'].includes(method)) {
if (bodyType === 'json') {
body = document.getElementById('bodyJson').value.trim();
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
} else if (bodyType === 'form') {
const formData = getKvRows(formRows);
body = new URLSearchParams(formData).toString();
headers['Content-Type'] = 'application/x-www-form-urlencoded';
} else if (bodyType === 'raw') {
body = document.getElementById('bodyRaw').value;
}
}
return { method, url, headers, body };
}
// Send
async function sendRequest() {
if (isLoading) return;
const req = buildRequest();
if (!req) {
document.getElementById('urlInput').focus();
return;
}
isLoading = true;
const sendBtn = document.getElementById('sendBtn');
sendBtn.disabled = true;
sendBtn.innerHTML = '<div class="spinner"></div> Отправка...';
// Status bar: loading
const statusBar = document.getElementById('responseStatusBar');
statusBar.innerHTML = '<span class="text-xs font-mono text-muted">Отправка запроса...</span>';
// Hide response
document.getElementById('responseBodyEmpty').classList.remove('hidden');
document.getElementById('responseBodyPre').classList.add('hidden');
document.getElementById('responseCopyBar').classList.add('hidden');
document.getElementById('responseHeadersContent').innerHTML = '';
try {
const t0 = performance.now();
const resp = await fetch('/api/proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req),
});
const data = await resp.json();
const elapsed = Math.round(performance.now() - t0);
if (data.error && !data.status) {
// Network/timeout error
statusBar.innerHTML = `<span class="text-xs font-mono" style="color:#ff5252">Ошибка: ${escHtml(data.error)}</span><span class="text-xs font-mono text-muted ml-auto">${data.time || elapsed}мс</span>`;
showBody('❌ ' + data.error, 'plaintext');
addToHistory(req.method, req.url, null, data.time || elapsed);
return;
}
// Status badge
const statusClass = data.status >= 500 ? 'status-5xx' : data.status >= 400 ? 'status-4xx' : data.status >= 300 ? 'status-3xx' : 'status-2xx';
const sizeStr = formatSize(data.size);
statusBar.innerHTML = `
<span class="text-sm font-mono font-bold ${statusClass}">${data.status} ${escHtml(data.statusText || '')}</span>
<span class="text-xs font-mono text-muted">${data.time}мс</span>
<span class="text-xs font-mono text-muted">${sizeStr}</span>
${data.truncated ? '<span class="text-xs font-mono" style="color:#ffa500">⚠ обрезано до 10MB</span>' : ''}
${data.url !== req.url ? '<span class="text-xs font-mono text-muted ml-auto" title="' + escHtml(data.url) + '">→ redirect</span>' : ''}
`;
// Response body
const ct = (data.headers?.['content-type'] || '').toLowerCase();
let lang = 'plaintext';
let bodyText = data.body || '';
if (ct.includes('json')) {
lang = 'json';
try { bodyText = JSON.stringify(JSON.parse(bodyText), null, 2); } catch {}
} else if (ct.includes('html')) {
lang = 'xml';
} else if (ct.includes('xml')) {
lang = 'xml';
} else if (ct.includes('javascript')) {
lang = 'javascript';
}
showBody(bodyText, lang);
// Response headers
const hContent = document.getElementById('responseHeadersContent');
hContent.innerHTML = '';
if (data.headers) {
Object.entries(data.headers).sort().forEach(([k, v]) => {
const row = document.createElement('div');
row.className = 'flex gap-2 py-0.5';
row.innerHTML = `<span style="color:#6b9fff;min-width:160px">${escHtml(k)}</span><span style="color:var(--text-secondary)">${escHtml(v)}</span>`;
hContent.appendChild(row);
});
}
// Copy button
const copyBar = document.getElementById('responseCopyBar');
copyBar.classList.remove('hidden');
document.getElementById('responseSizeLabel').textContent = sizeStr;
document.getElementById('copyBodyBtn').onclick = () => {
navigator.clipboard.writeText(data.body || '').then(() => {
document.getElementById('copyBodyBtn').textContent = 'Скопировано!';
setTimeout(() => document.getElementById('copyBodyBtn').textContent = 'Копировать', 1500);
});
};
addToHistory(req.method, req.url, data.status, data.time);
} catch (err) {
statusBar.innerHTML = `<span class="text-xs font-mono" style="color:#ff5252">Ошибка соединения: ${escHtml(err.message)}</span>`;
showBody('Ошибка: ' + err.message, 'plaintext');
} finally {
isLoading = false;
sendBtn.disabled = false;
sendBtn.innerHTML = '<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg> Отправить';
}
}
function showBody(text, lang) {
const empty = document.getElementById('responseBodyEmpty');
const pre = document.getElementById('responseBodyPre');
const code = document.getElementById('responseBodyCode');
empty.classList.add('hidden');
pre.classList.remove('hidden');
code.textContent = text;
code.className = 'language-' + lang;
if (window.hljs && lang !== 'plaintext') {
hljs.highlightElement(code);
}
}
function formatSize(bytes) {
if (!bytes) return '0 B';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
return (bytes/(1024*1024)).toFixed(1) + ' MB';
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Send button + Enter
document.getElementById('sendBtn').onclick = sendRequest;
document.getElementById('urlInput').addEventListener('keydown', e => {
if (e.key === 'Enter') sendRequest();
});
// History (server-side, per session)
const methodColors = { GET:'#00a86b', POST:'#ffa500', PUT:'#6b9fff', PATCH:'#cc66ff', DELETE:'#ff5252', HEAD:'#aaa', OPTIONS:'#aaa' };
async function addToHistory(method, url, status, time) {
await fetch('/api/history/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method, url, status, time, ts: Date.now() }),
}).catch(() => {});
renderHistory();
}
async function renderHistory() {
const list = document.getElementById('historyList');
const clearBtn = document.getElementById('clearHistoryBtn');
let h = [];
try {
const r = await fetch('/api/history');
h = await r.json();
} catch {}
if (!Array.isArray(h) || h.length === 0) {
list.innerHTML = '<div class="text-xs font-mono text-muted py-4 text-center">История пуста</div>';
clearBtn.classList.add('hidden');
return;
}
clearBtn.classList.remove('hidden');
list.innerHTML = '';
h.forEach((item) => {
const div = document.createElement('div');
div.className = 'history-item';
const statusClass = !item.status ? 'text-muted' : item.status >= 500 ? 'status-5xx' : item.status >= 400 ? 'status-4xx' : item.status >= 300 ? 'status-3xx' : 'status-2xx';
const statusText = item.status ? item.status : '—';
div.innerHTML = `
<span class="method-badge m-${item.method}" style="min-width:56px;justify-content:center">${item.method}</span>
<span class="history-url">${escHtml(item.url)}</span>
<span class="history-meta ${statusClass}">${statusText}</span>
<span class="history-meta">${item.time}мс</span>
`;
div.onclick = () => loadHistoryItem(item);
list.appendChild(div);
});
}
function loadHistoryItem(item) {
document.getElementById('method').value = item.method;
document.getElementById('urlInput').value = item.url;
}
document.getElementById('clearHistoryBtn').onclick = async (e) => {
e.stopPropagation();
await fetch('/api/history', { method: 'DELETE' }).catch(() => {});
renderHistory();
};
// History toggle
let historyOpen = true;
document.getElementById('historyToggle').onclick = () => {
historyOpen = !historyOpen;
document.getElementById('historyList').style.display = historyOpen ? '' : 'none';
document.getElementById('historyArrow').style.transform = historyOpen ? '' : 'rotate(-90deg)';
};
renderHistory();
})();
</script>
</body>
</html>