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 raw = await response.text(); const isJson = ct.includes('application/json'); if (isJson) { try { return { kind: 'json', data: JSON.parse(raw), contentType: ct, raw }; } catch { return { kind: 'text', data: raw, contentType: ct, raw }; } } return { kind: 'text', data: raw, contentType: ct, raw }; } 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 }) => { 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 }) => { 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; }); }