Add API tab with ERP-oriented docs and live request tester.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -251,6 +251,43 @@
|
|||||||
.rescan-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 10px; }
|
.rescan-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 10px; }
|
||||||
.rescan-box input[type=file] { font-size: 12px; }
|
.rescan-box input[type=file] { font-size: 12px; }
|
||||||
.rescan-status { font-family: var(--mono); font-size: 12px; color: var(--muted); min-height: 1.2em; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -268,6 +305,7 @@
|
|||||||
<div class="tab {% if not home_url %}active{% endif %}" onclick="switchTab('editor')">Ключови думи</div>
|
<div class="tab {% if not home_url %}active{% endif %}" onclick="switchTab('editor')">Ключови думи</div>
|
||||||
<div class="tab" onclick="switchTab('search')">Търсене</div>
|
<div class="tab" onclick="switchTab('search')">Търсене</div>
|
||||||
<div class="tab" onclick="switchTab('generator')">Документ</div>
|
<div class="tab" onclick="switchTab('generator')">Документ</div>
|
||||||
|
<div class="tab" onclick="switchTab('api')">API</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if home_url %}
|
{% if home_url %}
|
||||||
@@ -368,6 +406,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-api" class="panel">
|
||||||
|
<div class="api-layout">
|
||||||
|
<div class="api-card">
|
||||||
|
<h3>Документация — за ERP / интеграции</h3>
|
||||||
|
<p>Base URL: <code id="api-base"></code></p>
|
||||||
|
<p>JSON API за четене на help секции, търсене и (опционално) rescan. Готово за връзка с външни системи.</p>
|
||||||
|
|
||||||
|
<div class="api-ep"><span class="method">GET</span><code>/healthz</code>
|
||||||
|
<div class="desc">Здраве на услугата и БД; <code>output_dir_ok</code>, <code>txt_count</code>.</div></div>
|
||||||
|
<div class="api-ep"><span class="method">GET</span><code>/api/sections?prefix=RIP</code>
|
||||||
|
<div class="desc">Списък секции (метаданни + preview текст). Филтър по <code>prefix</code>.</div></div>
|
||||||
|
<div class="api-ep"><span class="method">GET</span><code>/api/search?q=…&prefix=RIP</code>
|
||||||
|
<div class="desc">Търсене по код / заглавие / keywords / html_text.</div></div>
|
||||||
|
<div class="api-ep"><span class="method">GET</span><code>/api/section/{code}</code>
|
||||||
|
<div class="desc">Една секция с <strong>пълен</strong> текст (<code>text</code>, <code>text_html</code>).</div></div>
|
||||||
|
<div class="api-ep"><span class="method post">POST</span><code>/api/keywords/{code}</code>
|
||||||
|
<div class="desc">Body JSON: <code>{"keywords":"а, б, в"}</code> — обновява ключови думи.</div></div>
|
||||||
|
<div class="api-ep"><span class="method post">POST</span><code>/api/rescan</code>
|
||||||
|
<div class="desc">multipart: <code>file</code> (zip/docx/…), <code>prefix</code>, <code>force</code>. Header опционално: <code>X-Rescan-Token</code>.</div></div>
|
||||||
|
<div class="api-ep"><span class="method">GET</span><code>/api/rescan/{job_id}</code>
|
||||||
|
<div class="desc">Статус на rescan задача (<code>queued</code> / <code>processing</code> / <code>done</code> / <code>error</code>).</div></div>
|
||||||
|
|
||||||
|
<p class="api-note">OpenAPI / Swagger: <a href="/docs" target="_blank" rel="noopener">/docs</a> · ReDoc: <a href="/redoc" target="_blank" rel="noopener">/redoc</a></p>
|
||||||
|
<p class="api-note">Типичен ERP поток: <code>GET /api/search</code> → избор на <code>code</code> → <code>GET /api/section/{code}</code> за съдържание.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-card">
|
||||||
|
<h3>Тестер</h3>
|
||||||
|
<div class="api-row">
|
||||||
|
<select id="api-endpoint" onchange="apiFillExample()">
|
||||||
|
<option value="GET /healthz">GET /healthz</option>
|
||||||
|
<option value="GET /api/sections">GET /api/sections</option>
|
||||||
|
<option value="GET /api/search" selected>GET /api/search</option>
|
||||||
|
<option value="GET /api/section">GET /api/section/{code}</option>
|
||||||
|
<option value="POST /api/keywords">POST /api/keywords/{code}</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="api-prefix" placeholder="prefix" value="{{ prefix or 'RIP' }}">
|
||||||
|
</div>
|
||||||
|
<div class="api-row">
|
||||||
|
<input type="text" id="api-q" placeholder="q / code / keywords JSON" value="лимитни карти">
|
||||||
|
</div>
|
||||||
|
<div class="api-row">
|
||||||
|
<button type="button" class="btn-primary" onclick="runApiTest()">Изпълни</button>
|
||||||
|
<button type="button" class="btn-ghost" onclick="copyApiOut()">Копирай отговора</button>
|
||||||
|
<span class="stats" id="api-status"></span>
|
||||||
|
</div>
|
||||||
|
<pre class="api-out" id="api-out">Избери endpoint и натисни „Изпълни“.</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
|
<script type="application/json" id="sections-data">{{ sections_json | safe }}</script>
|
||||||
@@ -401,13 +490,14 @@ function toast(msg, isErr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchTab(name) {
|
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('.tab').forEach((t,i) => t.classList.toggle('active', order[i] === name));
|
||||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||||
const panel = document.getElementById('tab-' + name);
|
const panel = document.getElementById('tab-' + name);
|
||||||
if (panel) panel.classList.add('active');
|
if (panel) panel.classList.add('active');
|
||||||
if (name === 'generator') renderGenerator();
|
if (name === 'generator') renderGenerator();
|
||||||
if (name === 'search') doSearch();
|
if (name === 'search') doSearch();
|
||||||
|
if (name === 'api') initApiTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRescan() {
|
async function startRescan() {
|
||||||
@@ -870,6 +960,97 @@ function download(filename, content, mime) {
|
|||||||
a.click();
|
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 ─────────────────────────────
|
// ── INIT ─────────────────────────────
|
||||||
document.getElementById('total-count').textContent = ALL.length + ' секции';
|
document.getElementById('total-count').textContent = ALL.length + ' секции';
|
||||||
try {
|
try {
|
||||||
@@ -877,6 +1058,7 @@ try {
|
|||||||
renderKwCloud();
|
renderKwCloud();
|
||||||
doSearch();
|
doSearch();
|
||||||
renderGenerator();
|
renderGenerator();
|
||||||
|
initApiTab();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('init failed', e);
|
console.error('init failed', e);
|
||||||
toast('Грешка при зареждане на UI: ' + e.message, true);
|
toast('Грешка при зареждане на UI: ' + e.message, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user