From 2e356653d78693201663c04e66b6859248fdd847 Mon Sep 17 00:00:00 2001 From: sabo Date: Thu, 27 Aug 2026 09:39:47 +0300 Subject: [PATCH] Add API tab with ERP-oriented docs and live request tester. Co-authored-by: Cursor --- webapp/templates/viewer.html | 184 ++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/webapp/templates/viewer.html b/webapp/templates/viewer.html index ea7c765..c894a82 100644 --- a/webapp/templates/viewer.html +++ b/webapp/templates/viewer.html @@ -251,6 +251,43 @@ .rescan-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 10px; } .rescan-box input[type=file] { font-size: 12px; } .rescan-status { font-family: var(--mono); font-size: 12px; color: var(--muted); min-height: 1.2em; } + + .api-layout { display: grid; grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.1fr); gap: 20px; } + @media (max-width: 900px) { .api-layout { grid-template-columns: 1fr; } } + .api-card { + background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg); + padding: 16px 18px; + } + .api-card h3 { + font-family: var(--mono); font-size: 12px; color: var(--muted); + letter-spacing: .06em; text-transform: uppercase; margin-bottom: 12px; + } + .api-card p { font-size: 13px; color: var(--text); line-height: 1.55; margin-bottom: 10px; } + .api-ep { + border: 1px solid var(--border); border-radius: var(--radius); + padding: 10px 12px; margin-bottom: 8px; background: var(--bg3); + } + .api-ep .method { + font-family: var(--mono); font-size: 11px; font-weight: 600; + color: #1e4a8c; background: #d6e4f7; padding: 2px 7px; border-radius: 4px; + margin-right: 8px; + } + .api-ep .method.post { color: #1a6b45; background: #d8f0e4; } + .api-ep code { font-family: var(--mono); font-size: 12px; } + .api-ep .desc { font-size: 12px; color: var(--muted); margin-top: 6px; line-height: 1.45; } + .api-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; } + .api-row select, .api-row input[type=text] { + background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius); + color: var(--text); font-family: var(--mono); font-size: 12px; padding: 7px 10px; + } + .api-row select { min-width: 160px; } + .api-row input[type=text] { flex: 1; min-width: 180px; } + .api-out { + font-family: var(--mono); font-size: 11px; line-height: 1.45; + background: #1a1a24; color: #d7dde8; border-radius: var(--radius); + padding: 12px; max-height: 420px; overflow: auto; white-space: pre-wrap; word-break: break-word; + } + .api-note { font-size: 12px; color: var(--muted); margin-top: 8px; } @@ -268,6 +305,7 @@
Ключови думи
Търсене
Документ
+
API
{% if home_url %} @@ -368,6 +406,57 @@ +
+
+
+

Документация — за ERP / интеграции

+

Base URL:

+

JSON API за четене на help секции, търсене и (опционално) rescan. Готово за връзка с външни системи.

+ +
GET/healthz +
Здраве на услугата и БД; output_dir_ok, txt_count.
+
GET/api/sections?prefix=RIP +
Списък секции (метаданни + preview текст). Филтър по prefix.
+
GET/api/search?q=…&prefix=RIP +
Търсене по код / заглавие / keywords / html_text.
+
GET/api/section/{code} +
Една секция с пълен текст (text, text_html).
+
POST/api/keywords/{code} +
Body JSON: {"keywords":"а, б, в"} — обновява ключови думи.
+
POST/api/rescan +
multipart: file (zip/docx/…), prefix, force. Header опционално: X-Rescan-Token.
+
GET/api/rescan/{job_id} +
Статус на rescan задача (queued / processing / done / error).
+ +

OpenAPI / Swagger: /docs · ReDoc: /redoc

+

Типичен ERP поток: GET /api/search → избор на code → GET /api/section/{code} за съдържание.

+
+ +
+

Тестер

+
+ + +
+
+ +
+
+ + + +
+
Избери endpoint и натисни „Изпълни“.
+
+
+
+
@@ -401,13 +490,14 @@ function toast(msg, isErr) { } function switchTab(name) { - const order = [{% if home_url %}'home',{% endif %}'editor','search','generator']; + const order = [{% if home_url %}'home',{% endif %}'editor','search','generator','api']; document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', order[i] === name)); document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); const panel = document.getElementById('tab-' + name); if (panel) panel.classList.add('active'); if (name === 'generator') renderGenerator(); if (name === 'search') doSearch(); + if (name === 'api') initApiTab(); } async function startRescan() { @@ -870,6 +960,97 @@ function download(filename, content, mime) { a.click(); } +// ── API TAB ───────────────────────────── +function initApiTab() { + const base = document.getElementById('api-base'); + if (base) base.textContent = location.origin; +} + +function apiFillExample() { + const v = document.getElementById('api-endpoint').value; + const q = document.getElementById('api-q'); + if (v.includes('/search')) q.value = 'лимитни карти'; + else if (v.includes('/section')) q.value = (ALL[0] && ALL[0].code) || 'RIP_0001_SEC_0001'; + else if (v.includes('/keywords')) q.value = '{"keywords":"тест, erp, demo"}'; + else if (v.includes('/sections')) q.value = ''; + else q.value = ''; +} + +async function runApiTest() { + const ep = document.getElementById('api-endpoint').value; + const prefix = (document.getElementById('api-prefix').value || '').trim(); + const q = (document.getElementById('api-q').value || '').trim(); + const st = document.getElementById('api-status'); + const out = document.getElementById('api-out'); + let method = 'GET'; + let url = ''; + let body = null; + let headers = {}; + + if (ep === 'GET /healthz') { + url = '/healthz'; + } else if (ep === 'GET /api/sections') { + url = '/api/sections' + (prefix ? ('?prefix=' + encodeURIComponent(prefix)) : ''); + } else if (ep === 'GET /api/search') { + const qs = new URLSearchParams(); + if (q) qs.set('q', q); + if (prefix) qs.set('prefix', prefix); + url = '/api/search?' + qs.toString(); + } else if (ep === 'GET /api/section') { + if (!q) { toast('Въведи code', true); return; } + url = '/api/section/' + encodeURIComponent(q); + } else if (ep === 'POST /api/keywords') { + method = 'POST'; + // q field: either code only (use sample keywords) or JSON with keywords; code from first token + let code = q; + let kwBody = { keywords: 'тест, erp' }; + try { + const parsed = JSON.parse(q); + if (parsed && typeof parsed === 'object') { + if (parsed.code) code = parsed.code; + if (parsed.keywords != null) kwBody = { keywords: parsed.keywords }; + } + } catch (_) { + // plain code — keep default keywords body + } + if (!code || code.startsWith('{')) { + code = (ALL[0] && ALL[0].code) || ''; + } + if (!code) { toast('Няма code за POST keywords', true); return; } + url = '/api/keywords/' + encodeURIComponent(code); + headers['Content-Type'] = 'application/json'; + body = JSON.stringify(kwBody); + } else { + toast('Непознат endpoint', true); + return; + } + + st.textContent = method + ' ' + url + ' …'; + out.textContent = 'Зареждане…'; + const t0 = performance.now(); + try { + const res = await fetch(url, { method, headers, body }); + const text = await res.text(); + let pretty = text; + try { pretty = JSON.stringify(JSON.parse(text), null, 2); } catch (_) {} + const ms = Math.round(performance.now() - t0); + st.textContent = res.status + ' · ' + ms + ' ms · ' + (text.length) + ' B'; + out.textContent = pretty; + } catch (e) { + st.textContent = 'грешка'; + out.textContent = String(e); + toast(e.message, true); + } +} + +function copyApiOut() { + const t = document.getElementById('api-out').textContent || ''; + navigator.clipboard.writeText(t).then( + () => toast('Копирано'), + () => toast('Неуспешно копиране', true) + ); +} + // ── INIT ───────────────────────────── document.getElementById('total-count').textContent = ALL.length + ' секции'; try { @@ -877,6 +1058,7 @@ try { renderKwCloud(); doSearch(); renderGenerator(); + initApiTab(); } catch (e) { console.error('init failed', e); toast('Грешка при зареждане на UI: ' + e.message, true);