wa-dev-tools/public/redirects.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

601 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redirect Analyzer — WA Dev Tools</title>
<script src="/vendor/tailwind.js"></script>
<script>window.WA_TOOL_ID = 'redirects';</script>
<script src="/shared.js"></script>
<link href="/vendor/fonts.css" rel="stylesheet">
<link href="/shared.css" rel="stylesheet">
<style>
/* Redirect Analyzer UI */
.panel {
background: var(--surface-800);
border: 1px solid var(--surface-600);
border-radius: 12px;
overflow: hidden;
}
.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;
min-width: 0;
}
.url-input:focus { border-color: rgba(0,84,230,.5); }
.url-input::placeholder { color: var(--text-muted); }
select.ctrl-select {
background: var(--select-bg);
border: 1px solid var(--surface-600);
border-radius: 8px;
padding: 0 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: var(--select-color);
outline: none;
cursor: pointer;
height: 40px;
white-space: nowrap;
}
select.ctrl-select:focus { border-color: rgba(0,84,230,.5); }
.analyze-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;
font-family: 'Manrope', sans-serif;
}
.analyze-btn:hover { background: #0043b8; }
.analyze-btn:disabled { background: #0054e680; cursor: not-allowed; }
/* Chain cards */
.chain-step {
position: relative;
}
.step-card {
background: var(--surface-700);
border: 1px solid var(--surface-600);
border-radius: 10px;
padding: 14px 16px;
transition: border-color .2s;
}
.step-card:hover { border-color: rgba(0,84,230,.3); }
.step-card.final { border-color: rgba(0,168,107,.3); background: rgba(0,168,107,.04); }
.step-num {
width: 24px; height: 24px;
border-radius: 50%;
background: rgba(0,84,230,.15);
color: #6b9fff;
font-size: 11px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.step-num.final { background: rgba(0,168,107,.15); color: #00a86b; }
.status-badge {
display: inline-flex; align-items: center;
padding: 2px 8px;
border-radius: 5px;
font-size: 11px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
}
.s-2xx { background: rgba(0,168,107,.15); color: #00a86b; }
.s-3xx { background: rgba(0,84,230,.15); color: #6b9fff; }
.s-4xx { background: rgba(255,165,0,.15); color: #ffa500; }
.s-5xx { background: rgba(255,82,82,.15); color: #ff5252; }
.arrow-connector {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 16px;
color: var(--text-muted);
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
}
.arrow-line {
flex: 1;
height: 1px;
background: var(--surface-600);
}
.final-badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px;
border-radius: 5px;
font-size: 10px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
background: rgba(0,168,107,.15);
color: #00a86b;
text-transform: uppercase;
letter-spacing: .05em;
}
/* Summary */
.stat-card {
background: var(--surface-700);
border: 1px solid var(--surface-600);
border-radius: 10px;
padding: 14px 18px;
display: flex; flex-direction: column; gap: 4px;
}
.stat-val {
font-size: 22px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
color: var(--text-primary);
}
.stat-label {
font-size: 11px;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
/* Issues */
.issue-item {
display: flex; align-items: flex-start; gap: 10px;
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
}
.issue-error { background: rgba(255,82,82,.07); color: #ff5252; border: 1px solid rgba(255,82,82,.2); }
.issue-warn { background: rgba(255,214,0,.07); color: #ffd600; border: 1px solid rgba(255,214,0,.15); }
.issue-info { background: rgba(0,84,230,.07); color: #6b9fff; border: 1px solid rgba(0,84,230,.15); }
/* Headers table */
.headers-table { width: 100%; border-collapse: collapse; font-size: 11px; font-family: 'JetBrains Mono', monospace; }
.headers-table th { text-align: left; padding: 5px 10px; color: var(--text-muted); border-bottom: 1px solid var(--surface-600); font-weight: 600; }
.headers-table td { padding: 5px 10px; color: var(--text-secondary); border-bottom: 1px solid rgba(255,255,255,.03); vertical-align: top; word-break: break-all; }
.headers-table td:first-child { color: #6b9fff; white-space: nowrap; min-width: 160px; }
/* Details accordion */
.details-accordion summary {
cursor: pointer;
list-style: none;
display: flex; align-items: center; gap: 6px;
padding: 10px 14px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
border-radius: 8px;
background: var(--surface-700);
border: 1px solid var(--surface-600);
user-select: none;
transition: color .15s;
}
.details-accordion summary:hover { color: var(--text-secondary); }
.details-accordion[open] summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom-color: transparent; }
.details-accordion .details-body {
border: 1px solid var(--surface-600);
border-top: none;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
overflow: hidden;
}
/* Export btns */
.export-btn {
background: var(--surface-700);
border: 1px solid var(--surface-600);
border-radius: 7px;
color: var(--text-muted);
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
padding: 6px 14px;
cursor: pointer;
transition: all .15s;
display: flex; align-items: center; gap: 5px;
}
.export-btn:hover { color: #0054e6; border-color: rgba(0,84,230,.4); background: rgba(0,84,230,.06); }
/* Spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { animation: spin 0.8s linear infinite; }
/* Copy button */
.copy-btn {
background: var(--surface-700);
border: 1px solid var(--surface-600);
border-radius: 6px;
color: var(--text-muted);
font-size: 11px;
font-family: 'JetBrains Mono', monospace;
padding: 4px 10px;
cursor: pointer;
transition: all .15s;
}
.copy-btn:hover { color: #0054e6; border-color: rgba(0,84,230,.4); }
/* URL truncate */
.url-text {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--text-secondary);
word-break: break-all;
flex: 1;
}
.url-text a { color: #6b9fff; text-decoration: none; }
.url-text a:hover { text-decoration: underline; }
</style>
</head>
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
<!-- Main -->
<div class="main-content">
<div class="tool-container relative z-10 max-w-5xl 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">
Redirect <span class="text-accent">Анализатор</span>
</h1>
<p class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">анализ цепочки HTTP-редиректов</p>
</div>
<!-- URL Bar -->
<div class="panel mb-6">
<div class="p-4 flex flex-wrap gap-3 items-center">
<input id="urlInput" type="text" class="url-input" placeholder="https://example.com или example.com" />
<select id="uaSelect" class="ctrl-select">
<option value="desktop">Десктоп</option>
<option value="mobile">Мобильный</option>
<option value="googlebot">Googlebot</option>
</select>
<select id="methodSelect" class="ctrl-select">
<option value="GET">GET</option>
<option value="HEAD">HEAD</option>
</select>
<button id="analyzeBtn" class="analyze-btn">
<svg id="btnIcon" width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803a7.5 7.5 0 0010.607 0z"/></svg>
Анализировать
</button>
</div>
</div>
<!-- States -->
<div id="stateEmpty">
<div class="panel p-10 text-center">
<svg class="mx-auto mb-4 opacity-20" width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
<p class="text-sm font-mono" style="color:var(--text-muted)">Введите URL для анализа цепочки редиректов</p>
<p class="text-xs font-mono mt-2" style="color:var(--text-muted)">Пример: <span style="color:#6b9fff">http://google.com</span></p>
</div>
</div>
<div id="stateLoading" class="hidden">
<div class="panel p-10 text-center">
<svg class="spinner mx-auto mb-4" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#0054e6" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
<p class="text-sm font-mono" style="color:var(--text-muted)">Анализируем...</p>
</div>
</div>
<div id="stateError" class="hidden">
<div class="panel p-6">
<div class="flex items-start gap-3">
<svg width="18" height="18" fill="none" stroke="#ff5252" viewBox="0 0 24 24" stroke-width="2" class="flex-shrink-0 mt-0.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
<div>
<p class="text-sm font-semibold" style="color:#ff5252">Ошибка</p>
<p id="errorMsg" class="text-sm font-mono mt-1" style="color:var(--text-muted)"></p>
</div>
</div>
</div>
</div>
<div id="stateResult" class="hidden space-y-6">
<!-- Summary stats -->
<div id="summaryBlock" class="grid grid-cols-2 sm:grid-cols-4 gap-3"></div>
<!-- Final URL -->
<div class="panel p-4 flex items-center gap-3">
<span class="text-xs font-mono flex-shrink-0" style="color:var(--text-muted)">КОНЕЧНЫЙ URL</span>
<span id="finalUrlText" class="url-text"></span>
<button class="copy-btn" id="copyFinalUrl">Копировать</button>
</div>
<!-- Issues -->
<div id="issuesBlock" class="hidden space-y-2">
<p class="text-xs font-mono font-semibold uppercase tracking-wider" style="color:var(--text-muted)">SEO Проблемы</p>
<div id="issuesList" class="space-y-2"></div>
</div>
<!-- Chain -->
<div>
<p class="text-xs font-mono font-semibold uppercase tracking-wider mb-3" style="color:var(--text-muted)">Цепочка редиректов</p>
<div id="chainContainer" class="space-y-1"></div>
</div>
<!-- Headers detail -->
<details class="details-accordion" id="headersAccordion">
<summary>
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"/></svg>
Детальные заголовки ответов
<svg class="ml-auto" width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
</summary>
<div class="details-body">
<div id="headersDetail" class="overflow-x-auto"></div>
</div>
</details>
<!-- Export -->
<div class="flex flex-wrap gap-2">
<button class="export-btn" id="btnCopyJson">
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"/></svg>
Копировать JSON
</button>
<button class="export-btn" id="btnDownloadJson">
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
Скачать JSON
</button>
<button class="export-btn" id="btnDownloadCsv">
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
Скачать CSV
</button>
</div>
</div><!-- /stateResult -->
</div>
</div>
<script>
(function() {
// State
let lastResult = null;
// UI refs
const urlInput = document.getElementById('urlInput');
const uaSelect = document.getElementById('uaSelect');
const methodSel = document.getElementById('methodSelect');
const analyzeBtn = document.getElementById('analyzeBtn');
const btnIcon = document.getElementById('btnIcon');
function showState(name) {
['Empty','Loading','Error','Result'].forEach(s => {
document.getElementById('state' + s).classList.toggle('hidden', s.toLowerCase() !== name);
});
}
function statusClass(code) {
if (code >= 500) return 's-5xx';
if (code >= 400) return 's-4xx';
if (code >= 300) return 's-3xx';
return 's-2xx';
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function renderChain(data) {
const container = document.getElementById('chainContainer');
container.innerHTML = '';
const chain = data.chain;
chain.forEach((step, i) => {
const isFinal = (i === chain.length - 1);
const isRedirect = step.status >= 300 && step.status < 400;
const sc = statusClass(step.status);
const card = document.createElement('div');
card.className = 'chain-step';
const cardHtml = `
<div class="step-card ${isFinal ? 'final' : ''}">
<div class="flex items-start gap-3">
<div class="step-num ${isFinal ? 'final' : ''}">${i + 1}</div>
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2 mb-1">
<span class="status-badge ${sc}">${escHtml(step.status)} ${escHtml(step.statusText)}</span>
${isFinal ? '<span class="final-badge"><svg width="10" height="10" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>ФИНАЛ</span>' : ''}
<span class="text-xs font-mono ml-auto" style="color:var(--text-muted)">${step.time}ms</span>
</div>
<div class="url-text"><a href="${escHtml(step.url)}" target="_blank" rel="noopener">${escHtml(step.url)}</a></div>
${step.location ? `<div class="text-xs font-mono mt-1" style="color:var(--text-muted)">Location: <span style="color:#6b9fff">${escHtml(step.location)}</span></div>` : ''}
</div>
</div>
</div>`;
card.innerHTML = cardHtml;
container.appendChild(card);
// Arrow between steps
if (!isFinal && i < chain.length - 1) {
const next = chain[i + 1];
const arrow = document.createElement('div');
arrow.className = 'arrow-connector';
arrow.innerHTML = `
<div class="arrow-line"></div>
<span class="status-badge ${sc}">${escHtml(step.status)}</span>
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3"/></svg>
<div class="arrow-line"></div>`;
container.appendChild(arrow);
}
});
}
function renderSummary(data) {
const redirectCount = data.chain.filter(s => s.status >= 300 && s.status < 400).length;
document.getElementById('summaryBlock').innerHTML = `
<div class="stat-card">
<div class="stat-val">${redirectCount}</div>
<div class="stat-label">Редиректов</div>
</div>
<div class="stat-card">
<div class="stat-val">${data.total_time}ms</div>
<div class="stat-label">Общее время</div>
</div>
<div class="stat-card">
<div class="stat-val">${data.chain.length}</div>
<div class="stat-label">Всего шагов</div>
</div>
<div class="stat-card">
<div class="stat-val ${data.loop_detected ? 'text-red-400' : ''}">${data.loop_detected ? 'ПЕТЛЯ' : 'OK'}</div>
<div class="stat-label">Петля</div>
</div>`;
}
function renderIssues(data) {
const issuesBlock = document.getElementById('issuesBlock');
const issuesList = document.getElementById('issuesList');
const issues = data.issues || [];
const hasLoop = data.loop_detected;
const messages = [];
if (hasLoop) {
messages.push({ cls: 'issue-error', icon: '&#10060;', text: 'Петля редиректов — URL уже встречался в цепочке' });
}
if (issues.includes('long_chain')) {
const n = data.chain.filter(s => s.status >= 300 && s.status < 400).length;
messages.push({ cls: 'issue-warn', icon: '&#9888;', text: `Chain too long — ${n} редиректов (рекомендуется не более 3)` });
}
if (issues.includes('mixed_protocol')) {
messages.push({ cls: 'issue-warn', icon: '&#9888;', text: 'Небезопасный редирект — переход с HTTPS на HTTP' });
}
if (issues.includes('302_not_301')) {
messages.push({ cls: 'issue-info', icon: '&#8505;', text: '302 вместо 301 — временный редирект не передаёт PageRank' });
}
if (messages.length === 0) {
issuesBlock.classList.add('hidden');
return;
}
issuesBlock.classList.remove('hidden');
issuesList.innerHTML = messages.map(m =>
`<div class="issue-item ${m.cls}"><span>${m.icon}</span><span>${m.text}</span></div>`
).join('');
}
function renderHeaders(data) {
const container = document.getElementById('headersDetail');
const rows = data.chain.map((step, i) => {
const hdrs = Object.entries(step.headers || {});
const hdrRows = hdrs.map(([k,v]) => `<tr><td>${escHtml(k)}</td><td>${escHtml(v)}</td></tr>`).join('');
return `
<div class="px-4 pt-3 pb-1">
<p class="text-xs font-mono font-semibold mb-2" style="color:var(--text-muted)">Шаг ${i+1}${escHtml(step.status)} ${escHtml(step.url.length > 60 ? step.url.substring(0,60)+'...' : step.url)}</p>
<table class="headers-table mb-3">
<thead><tr><th>Заголовок</th><th>Значение</th></tr></thead>
<tbody>${hdrRows || '<tr><td colspan="2" style="color:var(--text-muted)">— нет заголовков —</td></tr>'}</tbody>
</table>
</div>`;
}).join('<hr style="border-color:var(--surface-600);margin:0">');
container.innerHTML = rows;
}
async function analyze() {
let url = urlInput.value.trim();
if (!url) return;
// Auto-add protocol
if (!/^https?:\/\//i.test(url)) url = 'http://' + url;
analyzeBtn.disabled = true;
btnIcon.innerHTML = '<circle cx="12" cy="12" r="10" stroke="#fff" stroke-width="2" fill="none" stroke-dasharray="32" stroke-dashoffset="32" style="animation:spin .8s linear infinite"/>';
showState('loading');
try {
const resp = await fetch('/api/redirect-analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, userAgent: uaSelect.value, method: methodSel.value }),
});
const data = await resp.json();
if (!resp.ok || data.error) {
document.getElementById('errorMsg').textContent = data.error || `HTTP ${resp.status}`;
showState('error');
return;
}
lastResult = data;
// Final URL
const finalUrl = data.final_url || url;
document.getElementById('finalUrlText').innerHTML = `<a href="${escHtml(finalUrl)}" target="_blank" rel="noopener">${escHtml(finalUrl)}</a>`;
renderSummary(data);
renderIssues(data);
renderChain(data);
renderHeaders(data);
showState('result');
} catch (err) {
document.getElementById('errorMsg').textContent = err.message;
showState('error');
} finally {
analyzeBtn.disabled = false;
btnIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803a7.5 7.5 0 0010.607 0z"/>';
}
}
// Events
analyzeBtn.addEventListener('click', analyze);
urlInput.addEventListener('keydown', e => { if (e.key === 'Enter') analyze(); });
// Copy final URL
document.getElementById('copyFinalUrl').addEventListener('click', () => {
if (!lastResult) return;
navigator.clipboard.writeText(lastResult.final_url || '').then(() => {
const btn = document.getElementById('copyFinalUrl');
btn.textContent = 'Скопировано!';
setTimeout(() => btn.textContent = 'Копировать', 1500);
});
});
// Export
document.getElementById('btnCopyJson').addEventListener('click', () => {
if (!lastResult) return;
navigator.clipboard.writeText(JSON.stringify(lastResult, null, 2)).then(() => {
const btn = document.getElementById('btnCopyJson');
btn.innerHTML = btn.innerHTML.replace('Копировать JSON','Скопировано!');
setTimeout(() => btn.innerHTML = btn.innerHTML.replace('Скопировано!','Копировать JSON'), 1500);
});
});
document.getElementById('btnDownloadJson').addEventListener('click', () => {
if (!lastResult) return;
const blob = new Blob([JSON.stringify(lastResult, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'redirect-chain.json';
a.click();
});
document.getElementById('btnDownloadCsv').addEventListener('click', () => {
if (!lastResult) return;
const rows = [['Step','URL','Status','StatusText','Location','Time(ms)']];
lastResult.chain.forEach((s, i) => {
rows.push([i+1, s.url, s.status, s.statusText, s.location||'', s.time]);
});
const csv = rows.map(r => r.map(c => '"'+String(c).replace(/"/g,'""')+'"').join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'redirect-chain.csv';
a.click();
});
// Pre-fill from query param
const qp = new URLSearchParams(location.search);
if (qp.get('url')) { urlInput.value = qp.get('url'); analyze(); }
})();
</script>
</body>
</html>