Weryfikacja 0-odwolan z 01db57a liczyla tylko podzbior prefiksow — poza nim
zostalo 13 wskaznikow w plikach niemarkdownowych i w prozie dokumentow:
docs/kb/modules/05-faza3-plan.md -> kb/phases/kb-m5-faza3.md (2x systemd)
docs/kb/modules/05-faza4-plan.md -> kb/phases/kb-m5-faza4.md (kb-query app.js)
docs/kb/modules/DECYZJE-*.md -> kb/decisions/kb-dokumenty-otwarte.md
docs/backlog.md (npm panel admina) -> kb/decisions/backlog-aktywne.md
docs/backlog/ (uid/gid floty) -> kb/decisions/backlog-uid-gid-flota.md
docs/kb/modules/0X-*.md -> kb/phases/kb-m*.md
docs/incidents/2026-07-30-*.md -> kb/incidents/ (3x node-agent)
docs/architecture/RECON-multi*.md -> kb/subsystems/recon-multiagent.md
jobs/deploy-runner/README.md -> kb/services/job-deploy-runner.md
Wyjatek zamierzony: `docs/kb/modules/05-faza3-pilot-streszczen.md` w §11 planu
fazy 3 to nazwa artefaktu, ktory nigdy nie powstal — przepiety na docelowa
konwencje (kb/phases/kb-m5-faza3-pilot-streszczen.md), zeby przyszly plik
wyladowal w nowym drzewie, a nie w skasowanym katalogu.
Weryfikacja: skan po 67 sciezkach zmigrowanych w tej galezi (git grep -F na
kazdej) = 0 trafien poza docs/sessions (logi historyczne, celowo nietkniete);
0 martwych linkow markdown na 190 plikach; check_okf.py 190/190 ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
235 lines
7.6 KiB
JavaScript
235 lines
7.6 KiB
JavaScript
// kb-query frontend -- module 5 phase 4 (kb/phases/kb-m5-faza4.md §7). Vanilla JS, no
|
|
// build step (plan §2 decision 4): fetch()s /search, renders results grouped by envelope_id.
|
|
//
|
|
// Threshold rule (plan §7, task spec): dist < 0.45 green, 0.45-0.55 yellow (still rendered with
|
|
// a visual warning), > 0.55 never rendered as an individual result. If a query ends up with zero
|
|
// renderable chunks (every hit > 0.55, or no hits at all), the whole group list is replaced by
|
|
// one message instead -- "brak odpowiedzi w KB", carrying the best (lowest) dist seen so the
|
|
// user can tell a near-miss from nothing at all.
|
|
(function () {
|
|
'use strict';
|
|
|
|
const GREEN_MAX = 0.45;
|
|
const YELLOW_MAX = 0.55;
|
|
|
|
function buildSearchUrl(query, mode) {
|
|
const params = new URLSearchParams({ q: query, mode: mode });
|
|
return '/search?' + params.toString();
|
|
}
|
|
|
|
function distClass(dist) {
|
|
if (dist < GREEN_MAX) return 'dist-green';
|
|
if (dist <= YELLOW_MAX) return 'dist-yellow';
|
|
return 'dist-red';
|
|
}
|
|
|
|
// Flat /search results -> per-envelope groups, chunks sorted best-first within each group.
|
|
// Insertion order follows first appearance, which already tracks best-group-first since the
|
|
// API's own result list is globally dist-sorted (kb_retrieval.cascade_retrieve/flat_retrieve).
|
|
function groupResults(results) {
|
|
const groups = [];
|
|
const byId = new Map();
|
|
for (const r of results) {
|
|
let group = byId.get(r.envelope_id);
|
|
if (!group) {
|
|
group = {
|
|
envelopeId: r.envelope_id,
|
|
source: r.source,
|
|
summary: r.summary,
|
|
summaryTags: r.summary_tags || [],
|
|
link: r.link,
|
|
mailUiUrl: r.mail_ui_url,
|
|
subject: r.subject,
|
|
from: r.from,
|
|
date: r.date,
|
|
chunks: [],
|
|
};
|
|
byId.set(r.envelope_id, group);
|
|
groups.push(group);
|
|
}
|
|
group.chunks.push(r);
|
|
}
|
|
for (const group of groups) {
|
|
group.chunks.sort((a, b) => a.dist - b.dist);
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
function copyToClipboard(text) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
return navigator.clipboard.writeText(text);
|
|
}
|
|
// Fallback for plain-http LAN access (no secure context -> Clipboard API unavailable).
|
|
const el = document.createElement('textarea');
|
|
el.value = text;
|
|
el.style.position = 'fixed';
|
|
el.style.opacity = '0';
|
|
document.body.appendChild(el);
|
|
el.select();
|
|
try {
|
|
document.execCommand('copy');
|
|
} finally {
|
|
document.body.removeChild(el);
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
function renderSolStatus(el, status) {
|
|
el.textContent = 'SOLARIA: ' + (status === 'up' ? 'online' : 'offline (fallback embed)');
|
|
el.className = 'sol-status ' + (status === 'up' ? 'sol-up' : 'sol-down');
|
|
}
|
|
|
|
function renderChunk(chunk) {
|
|
const details = document.createElement('details');
|
|
details.className = 'chunk ' + distClass(chunk.dist);
|
|
const summaryEl = document.createElement('summary');
|
|
summaryEl.textContent = 'Fragment ' + chunk.chunk_index + ' (dist ' + chunk.dist.toFixed(4) + ')';
|
|
const body = document.createElement('pre');
|
|
body.className = 'chunk-text';
|
|
body.textContent = chunk.text;
|
|
details.appendChild(summaryEl);
|
|
details.appendChild(body);
|
|
return details;
|
|
}
|
|
|
|
function renderGroup(group) {
|
|
const article = document.createElement('article');
|
|
article.className = 'result-group';
|
|
|
|
const header = document.createElement('h3');
|
|
header.textContent = group.summary || group.envelopeId;
|
|
article.appendChild(header);
|
|
|
|
if (group.summaryTags.length) {
|
|
const tags = document.createElement('div');
|
|
tags.className = 'tags';
|
|
tags.textContent = group.summaryTags.join(', ');
|
|
article.appendChild(tags);
|
|
}
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'source-meta';
|
|
if (group.source === 'paperless' && group.link) {
|
|
const a = document.createElement('a');
|
|
a.href = group.link;
|
|
a.target = '_blank';
|
|
a.rel = 'noopener';
|
|
a.textContent = 'Otwórz w Paperless';
|
|
meta.appendChild(a);
|
|
} else if (group.source === 'gmail') {
|
|
const info = document.createElement('span');
|
|
info.textContent = [group.subject, group.from, group.date].filter(Boolean).join(' — ');
|
|
meta.appendChild(info);
|
|
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.textContent = 'Kopiuj Message-ID';
|
|
btn.addEventListener('click', function () {
|
|
copyToClipboard(group.envelopeId).then(function () {
|
|
btn.textContent = 'Skopiowano!';
|
|
setTimeout(function () {
|
|
btn.textContent = 'Kopiuj Message-ID';
|
|
}, 1500);
|
|
});
|
|
});
|
|
meta.appendChild(btn);
|
|
}
|
|
if (meta.childNodes.length) {
|
|
article.appendChild(meta);
|
|
}
|
|
|
|
for (const chunk of group.chunks) {
|
|
article.appendChild(renderChunk(chunk));
|
|
}
|
|
return article;
|
|
}
|
|
|
|
function render(container, data) {
|
|
container.innerHTML = '';
|
|
|
|
let bestDist = null;
|
|
for (const r of data.results) {
|
|
if (bestDist === null || r.dist < bestDist) bestDist = r.dist;
|
|
}
|
|
|
|
const groups = groupResults(data.results);
|
|
let anyVisible = false;
|
|
for (const group of groups) {
|
|
group.chunks = group.chunks.filter(function (c) {
|
|
return c.dist <= YELLOW_MAX;
|
|
});
|
|
if (group.chunks.length) anyVisible = true;
|
|
}
|
|
|
|
if (!anyVisible) {
|
|
const msg = document.createElement('p');
|
|
msg.className = 'no-answer';
|
|
msg.textContent = bestDist === null
|
|
? 'Brak odpowiedzi w KB dla tego zapytania.'
|
|
: 'Brak odpowiedzi w KB dla tego zapytania (najlepszy dist: ' + bestDist.toFixed(4) + ').';
|
|
container.appendChild(msg);
|
|
return;
|
|
}
|
|
|
|
for (const group of groups) {
|
|
if (group.chunks.length) {
|
|
container.appendChild(renderGroup(group));
|
|
}
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
const form = document.getElementById('search-form');
|
|
const queryInput = document.getElementById('query');
|
|
const modeToggle = document.getElementById('mode-flat');
|
|
const results = document.getElementById('results');
|
|
const errorBox = document.getElementById('error');
|
|
const solStatusEl = document.getElementById('sol-status');
|
|
|
|
async function refreshSolStatus() {
|
|
try {
|
|
const resp = await fetch('/healthz');
|
|
const data = await resp.json();
|
|
renderSolStatus(solStatusEl, data.sol_status);
|
|
} catch (err) {
|
|
renderSolStatus(solStatusEl, 'down');
|
|
}
|
|
}
|
|
|
|
form.addEventListener('submit', async function (event) {
|
|
event.preventDefault();
|
|
const query = queryInput.value.trim();
|
|
if (!query) return;
|
|
const mode = modeToggle.checked ? 'flat' : 'cascade';
|
|
|
|
errorBox.hidden = true;
|
|
results.innerHTML = '<p class="loading">Szukam…</p>';
|
|
|
|
try {
|
|
const resp = await fetch(buildSearchUrl(query, mode));
|
|
if (!resp.ok) {
|
|
const body = await resp.json().catch(function () {
|
|
return {};
|
|
});
|
|
throw new Error(body.detail || ('Błąd wyszukiwania (' + resp.status + ')'));
|
|
}
|
|
const data = await resp.json();
|
|
render(results, data);
|
|
renderSolStatus(solStatusEl, data.sol_status);
|
|
} catch (err) {
|
|
results.innerHTML = '';
|
|
errorBox.textContent = 'Wyszukiwanie chwilowo niedostępne: ' + err.message;
|
|
errorBox.hidden = false;
|
|
}
|
|
});
|
|
|
|
refreshSolStatus();
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { buildSearchUrl, groupResults, distClass };
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
}
|
|
})();
|