342 lines
10 KiB
JavaScript
342 lines
10 KiB
JavaScript
const baseUrl = window.location.origin;
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const baseEl = document.getElementById('baseUrl');
|
|
if (baseEl) baseEl.textContent = baseUrl;
|
|
|
|
document.querySelectorAll('[id^="curlUrl"]').forEach((el) => {
|
|
el.textContent = baseUrl;
|
|
});
|
|
});
|
|
|
|
function toggleEndpoint(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
new bootstrap.Collapse(el, { toggle: true });
|
|
}
|
|
|
|
function showResponse(id) {
|
|
const div = document.getElementById(id);
|
|
const body = document.getElementById(id + '-body');
|
|
if (div) div.style.display = 'block';
|
|
if (body) body.textContent = 'Loading...';
|
|
return { div, body };
|
|
}
|
|
|
|
function formatHeaders(headers) {
|
|
const entries = [];
|
|
for (const [k, v] of headers.entries()) entries.push([k, v]);
|
|
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
return entries.map(([k, v]) => `${k}: ${v}`).join('\n');
|
|
}
|
|
|
|
function cacheHeaderSummary(headers) {
|
|
const keys = ['x-from-cache', 'x-cache-type', 'x-generated-at', 'content-type', 'content-disposition'];
|
|
const out = [];
|
|
for (const k of keys) {
|
|
const v = headers.get(k);
|
|
if (v !== null) out.push(`${k}: ${v}`);
|
|
}
|
|
return out.join('\n');
|
|
}
|
|
|
|
async function readBodyAuto(response) {
|
|
const ct = (response.headers.get('content-type') || '').toLowerCase();
|
|
const isJson = ct.includes('application/json');
|
|
if (isJson) {
|
|
try {
|
|
return { kind: 'json', data: await response.json(), contentType: ct };
|
|
} catch {
|
|
return { kind: 'text', data: await response.text(), contentType: ct };
|
|
}
|
|
}
|
|
return { kind: 'text', data: await response.text(), contentType: ct };
|
|
}
|
|
|
|
function safeParseJsonFromTextarea(textareaId) {
|
|
const el = document.getElementById(textareaId);
|
|
if (!el) throw new Error(`Missing textarea: ${textareaId}`);
|
|
const raw = el.value;
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch (e) {
|
|
throw new Error(`Invalid JSON in textarea "${textareaId}": ${e.message}`);
|
|
}
|
|
}
|
|
|
|
function guessExtensionFromContentType(ct) {
|
|
const s = (ct || '').toLowerCase();
|
|
if (s.includes('application/json')) return 'json';
|
|
if (s.includes('text/csv')) return 'csv';
|
|
if (s.includes('application/javascript')) return 'js';
|
|
if (s.includes('text/plain')) return 'txt';
|
|
return 'txt';
|
|
}
|
|
|
|
function firstLines(text, maxLines = 80) {
|
|
const lines = String(text || '').split(/\r?\n/);
|
|
return lines.slice(0, maxLines).join('\n');
|
|
}
|
|
|
|
function tryApi(endpoint, method = 'GET', body = null) {
|
|
const responseId = 'response-' + endpoint.replace(/\//g, '-');
|
|
const { body: out } = showResponse(responseId);
|
|
|
|
const url = baseUrl + '/api/' + endpoint;
|
|
const opts = { method, headers: {} };
|
|
|
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
opts.headers['Content-Type'] = 'application/json';
|
|
opts.body = body == null ? '{}' : (typeof body === 'string' ? body : JSON.stringify(body));
|
|
}
|
|
|
|
fetch(url, opts)
|
|
.then(async (resp) => {
|
|
const parsed = await readBodyAuto(resp);
|
|
|
|
const meta = [
|
|
`HTTP ${resp.status} ${resp.statusText}`,
|
|
cacheHeaderSummary(resp.headers),
|
|
'\n--- Headers ---\n' + formatHeaders(resp.headers),
|
|
'\n--- Body ---\n'
|
|
].filter(Boolean).join('\n');
|
|
|
|
if (!resp.ok) {
|
|
const msg = parsed.kind === 'json'
|
|
? (parsed.data?.error || JSON.stringify(parsed.data, null, 2))
|
|
: String(parsed.data || '');
|
|
throw new Error(`${meta}${msg}`);
|
|
}
|
|
|
|
const pretty = parsed.kind === 'json'
|
|
? JSON.stringify(parsed.data, null, 2)
|
|
: String(parsed.data ?? '');
|
|
|
|
out.textContent = meta + pretty;
|
|
})
|
|
.catch((err) => {
|
|
out.textContent = 'Error: ' + err.message;
|
|
});
|
|
}
|
|
|
|
function tryPath(path, method = 'GET') {
|
|
const normalized = String(path || '').trim();
|
|
const key = normalized.replace(/[^\w-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
const responseId = 'response-' + (key || 'path');
|
|
const { body: out } = showResponse(responseId);
|
|
|
|
const url = baseUrl + normalized;
|
|
fetch(url, { method })
|
|
.then(async (resp) => {
|
|
const parsed = await readBodyAuto(resp);
|
|
|
|
const meta = [
|
|
`HTTP ${resp.status} ${resp.statusText}`,
|
|
cacheHeaderSummary(resp.headers),
|
|
'\n--- Headers ---\n' + formatHeaders(resp.headers),
|
|
'\n--- Body ---\n'
|
|
].filter(Boolean).join('\n');
|
|
|
|
if (!resp.ok) {
|
|
const msg = parsed.kind === 'json'
|
|
? (parsed.data?.error || JSON.stringify(parsed.data, null, 2))
|
|
: String(parsed.data || '');
|
|
throw new Error(`${meta}${msg}`);
|
|
}
|
|
|
|
const pretty = parsed.kind === 'json'
|
|
? JSON.stringify(parsed.data, null, 2)
|
|
: String(parsed.data ?? '');
|
|
|
|
out.textContent = meta + pretty;
|
|
})
|
|
.catch((err) => {
|
|
out.textContent = 'Error: ' + err.message;
|
|
});
|
|
}
|
|
|
|
function tryApiJsonTextarea(endpoint, textareaId) {
|
|
try {
|
|
const body = safeParseJsonFromTextarea(textareaId);
|
|
tryApi(endpoint, 'POST', body);
|
|
} catch (e) {
|
|
const responseId = 'response-' + endpoint.replace(/\//g, '-');
|
|
const { body: out } = showResponse(responseId);
|
|
out.textContent = 'Error: ' + e.message;
|
|
}
|
|
}
|
|
|
|
function tryInvalidateCountry() {
|
|
const countryInput = document.getElementById('invalidateCountry');
|
|
const country = (countryInput?.value || '').trim().toUpperCase();
|
|
|
|
const { body: out } = showResponse('response-cache-invalidate');
|
|
|
|
if (!country || country.length !== 2) {
|
|
out.textContent = 'Error: Please enter a valid 2-letter country code (e.g., CN, RU, US).';
|
|
return;
|
|
}
|
|
|
|
fetch(baseUrl + '/api/cache/invalidate/' + country, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: '{}'
|
|
})
|
|
.then(async (resp) => {
|
|
const parsed = await readBodyAuto(resp);
|
|
if (!resp.ok) {
|
|
const msg = parsed.kind === 'json'
|
|
? (parsed.data?.error || JSON.stringify(parsed.data, null, 2))
|
|
: String(parsed.data || '');
|
|
throw new Error(msg || `HTTP ${resp.status}`);
|
|
}
|
|
|
|
out.textContent = (parsed.kind === 'json')
|
|
? JSON.stringify(parsed.data, null, 2)
|
|
: String(parsed.data ?? '');
|
|
|
|
if (parsed.kind === 'json' && parsed.data?.success && countryInput) {
|
|
countryInput.value = '';
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
out.textContent = 'Error: ' + err.message;
|
|
});
|
|
}
|
|
|
|
async function fetchAsBlob(url, method, jsonBody) {
|
|
const resp = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(jsonBody ?? {})
|
|
});
|
|
|
|
const headersText = formatHeaders(resp.headers);
|
|
const cacheSummary = cacheHeaderSummary(resp.headers);
|
|
const ct = resp.headers.get('content-type') || '';
|
|
const cd = resp.headers.get('content-disposition') || '';
|
|
|
|
const blob = await resp.blob();
|
|
|
|
if (!resp.ok) {
|
|
let errText = '';
|
|
try { errText = await blob.text(); } catch { errText = ''; }
|
|
throw new Error(
|
|
`HTTP ${resp.status} ${resp.statusText}\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Body ---\n${errText}`.trim()
|
|
);
|
|
}
|
|
|
|
return { resp, blob, headersText, cacheSummary, contentType: ct, contentDisposition: cd };
|
|
}
|
|
|
|
function makeDownloadLink(blob, filename) {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.textContent = filename;
|
|
a.className = 'link-primary api-download-link';
|
|
a.onclick = () => setTimeout(() => URL.revokeObjectURL(url), 2500);
|
|
return a;
|
|
}
|
|
|
|
function downloadFromApiJsonTextarea(endpoint, textareaId, fileBaseName) {
|
|
const responseId = 'response-' + endpoint.replace(/\//g, '-');
|
|
const { body: out } = showResponse(responseId);
|
|
|
|
let body;
|
|
try {
|
|
body = safeParseJsonFromTextarea(textareaId);
|
|
} catch (e) {
|
|
out.textContent = 'Error: ' + e.message;
|
|
return;
|
|
}
|
|
|
|
const url = baseUrl + '/api/' + endpoint;
|
|
|
|
fetchAsBlob(url, 'POST', body)
|
|
.then(async ({ blob, headersText, cacheSummary, contentType, contentDisposition }) => {
|
|
const ext = guessExtensionFromContentType(contentType);
|
|
const filename = `${fileBaseName}.${ext}`;
|
|
|
|
let preview = '';
|
|
try {
|
|
const txt = await blob.text();
|
|
preview = firstLines(txt, 80);
|
|
} catch {
|
|
preview = '(binary content)';
|
|
}
|
|
|
|
out.textContent =
|
|
`OK\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Preview (first lines) ---\n${preview}\n\n--- Download ---\n`;
|
|
|
|
const link = makeDownloadLink(blob, filename);
|
|
out.parentElement.appendChild(link);
|
|
})
|
|
.catch((err) => {
|
|
out.textContent = 'Error: ' + err.message;
|
|
});
|
|
}
|
|
|
|
|
|
function previewTextFromGenerate(textareaId) {
|
|
const { body: out } = showResponse('response-generate-download');
|
|
|
|
let body;
|
|
try {
|
|
body = safeParseJsonFromTextarea(textareaId);
|
|
} catch (e) {
|
|
out.textContent = 'Error: ' + e.message;
|
|
return;
|
|
}
|
|
|
|
fetchAsBlob(baseUrl + '/api/generate', 'POST', body)
|
|
.then(async ({ blob, headersText, cacheSummary, contentType }) => {
|
|
let text = '';
|
|
try { text = await blob.text(); } catch { text = '(binary content)'; }
|
|
|
|
out.textContent =
|
|
`OK\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Preview (first ~80 lines) ---\n${firstLines(text, 80)}\n`;
|
|
})
|
|
.catch((err) => {
|
|
out.textContent = 'Error: ' + err.message;
|
|
});
|
|
}
|
|
|
|
function downloadFromGenerate(textareaId, fileBaseName) {
|
|
const { body: out } = showResponse('response-generate-download');
|
|
|
|
let body;
|
|
try {
|
|
body = safeParseJsonFromTextarea(textareaId);
|
|
} catch (e) {
|
|
out.textContent = 'Error: ' + e.message;
|
|
return;
|
|
}
|
|
|
|
fetchAsBlob(baseUrl + '/api/generate', 'POST', body)
|
|
.then(async ({ blob, headersText, cacheSummary, contentType, contentDisposition }) => {
|
|
const ext = guessExtensionFromContentType(contentType);
|
|
let filename = `${fileBaseName}.${ext}`;
|
|
|
|
const m = /filename="?([^"]+)"?/i.exec(contentDisposition || '');
|
|
if (m && m[1]) filename = m[1];
|
|
|
|
let preview = '';
|
|
try {
|
|
const txt = await blob.text();
|
|
preview = firstLines(txt, 80);
|
|
} catch {
|
|
preview = '(binary content)';
|
|
}
|
|
|
|
out.textContent =
|
|
`OK\n${cacheSummary}\n\n--- Headers ---\n${headersText}\n\n--- Preview (first lines) ---\n${preview}\n\n--- Download ---\n`;
|
|
|
|
const link = makeDownloadLink(blob, filename);
|
|
out.parentElement.appendChild(link);
|
|
})
|
|
.catch((err) => {
|
|
out.textContent = 'Error: ' + err.message;
|
|
});
|
|
} |