feat(ingest+поиск): приём ZIP через интерфейс, регистронезависимый поиск, чистка очереди
- Загрузка архива (§4.1): POST /ingest распаковывает ZIP, дораспознаёт и догружает прайсы в базу (append). Виджет в админке. Кэш эмбеддингов справочника (dict_emb.npy) — догрузка считает только новый файл, эмбеддинги best-effort с откатом на fuzzy при сбое Gemini. - Поиск по name_norm (lowercase): «УЗИ» и «узи» дают одинаковый результат — SQLite LIKE не сворачивает регистр для кириллицы. - Фильтр названий: код-мнемоники («МОЗО.15») больше не попадают в очередь. - /stats считается из базы — цифры на дашборде обновляются после загрузки. - Деплой через Dockerfile (зависимости в образе, код монтируется томом). - ruff: ядро и весь репозиторий проходят проверку. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,21 @@
|
||||
<h2 class="text-2xl font-bold mt-6">Дашборд обработки</h2>
|
||||
<div id="stats" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4"></div>
|
||||
|
||||
<!-- Загрузка нового архива (ТЗ §4.1) -->
|
||||
<div class="bg-white border border-[#ebedf0] rounded-2xl shadow-sm p-5 mt-6">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Загрузить архив прайсов</h3>
|
||||
<p class="text-sm text-tg mt-1">ZIP с прайсами клиник (pdf, xlsx, xls, docx). Система распакует, распознает и догрузит позиции в базу.</p>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<input id="zip" type="file" accept=".zip" class="hidden" onchange="uploadArchive(this)">
|
||||
<button onclick="$('zip').click()" class="bg-nomad text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-nomad-dark">Выбрать ZIP</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ingest-result" class="mt-3 text-sm hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-8">
|
||||
<h3 class="text-lg font-semibold">Очередь верификации</h3>
|
||||
<div id="vprogress" class="text-sm text-tg"></div>
|
||||
@@ -145,6 +160,33 @@ async function loadAdmin() {
|
||||
loadQueue();
|
||||
}
|
||||
|
||||
async function uploadArchive(input) {
|
||||
const f = input.files[0];
|
||||
if (!f) return;
|
||||
const box = $('ingest-result');
|
||||
box.classList.remove('hidden');
|
||||
box.innerHTML = `<span class="text-tg">⏳ Обрабатываю <b>${f.name}</b> — распаковка, распознавание и нормализация. Может занять до минуты…</span>`;
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
try {
|
||||
const res = await fetch(`${API}/ingest`, { method: 'POST', body: fd });
|
||||
const r = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
box.innerHTML = `<span class="text-[#dc2626]">Ошибка: ${r.detail || res.status}</span>`;
|
||||
} else {
|
||||
const note = r.embeddings ? '' : ' <span class="text-tg">(нормализация по кодам и нечёткому совпадению — без эмбеддингов)</span>';
|
||||
box.innerHTML = `<div class="bg-nomad-soft border border-[#ffd9cc] rounded-lg p-3">
|
||||
✓ Архив обработан: <b>${r.documents}</b> прайс(ов), <b>${r.items}</b> позиций,
|
||||
автонормализация <b class="text-nomad">${r.auto_matched_pct}%</b>${r.new_partners ? ', новых клиник ' + r.new_partners : ''}.${note}
|
||||
<div class="text-tg mt-1">Новые позиции уже доступны в поиске и в очереди верификации.</div></div>`;
|
||||
loadAdmin(); // обновить дашборд и очередь под новые данные
|
||||
}
|
||||
} catch (e) {
|
||||
box.innerHTML = `<span class="text-[#dc2626]">Сбой загрузки: ${e}</span>`;
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
async function loadQueue() {
|
||||
const items = await fetch(`${API}/unmatched?limit=25`).then(r => r.json());
|
||||
$('queue').innerHTML = items.map(renderCard).join('') || '<div class="text-tg p-4">Очередь пуста 🎉</div>';
|
||||
|
||||
Reference in New Issue
Block a user