Initial SQL-only 1C adapter baseline

This commit is contained in:
2026-07-22 03:03:47 +03:00
commit e2503b77e7
545 changed files with 184711 additions and 0 deletions
+866
View File
@@ -0,0 +1,866 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LLM Control Console</title>
<style>
:root {
--bg: #eef0ed;
--surface: #fbfbf7;
--surface-2: #e1e6e1;
--ink: #202522;
--muted: #66706a;
--line: #cbd3cc;
--accent: #0f6b5f;
--accent-2: #8a5a12;
--ok: #146c43;
--warn: #9a5b00;
--bad: #ad2f2b;
--info: #315f85;
--radius: 8px;
--mono: "Cascadia Mono", "JetBrains Mono", Consolas, monospace;
--sans: "Aptos", "Segoe UI", Tahoma, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
letter-spacing: 0;
}
button, input, select { font: inherit; }
button {
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
min-height: 34px;
border-radius: 6px;
cursor: pointer;
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
}
button:hover { border-color: #8b978e; }
button:active { transform: translateY(1px); }
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
button.warn { background: #f7ead3; border-color: #d2a456; color: #4f360d; }
input, select {
width: 100%;
min-height: 34px;
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
color: var(--ink);
padding: 7px 9px;
}
.app {
min-height: 100vh;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
}
aside {
background: #dfe5df;
border-right: 1px solid var(--line);
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
}
.brand h1 { margin: 0; font-size: 18px; line-height: 1.1; }
.brand p { margin: 6px 0 0; color: var(--muted); font-size: 12px; line-height: 1.35; }
nav { display: grid; gap: 6px; }
nav button {
text-align: left;
padding: 8px 10px;
background: transparent;
}
nav button.active { background: var(--surface); border-color: #9da99f; font-weight: 700; }
.side-block {
border: 1px solid var(--line);
border-radius: var(--radius);
background: rgba(251, 251, 247, 0.55);
padding: 10px;
display: grid;
gap: 8px;
font-size: 12px;
color: var(--muted);
}
main {
padding: 16px;
display: grid;
gap: 14px;
align-content: start;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--line);
padding-bottom: 12px;
}
.topbar h2 { margin: 0; font-size: 20px; }
.topbar .meta { color: var(--muted); font-size: 12px; font-family: var(--mono); overflow-wrap: anywhere; }
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.panel {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
min-width: 0;
}
.panel-head {
min-height: 42px;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.panel-head h3 { margin: 0; font-size: 14px; }
.panel-body { padding: 12px; }
.metric {
display: grid;
gap: 4px;
min-height: 92px;
}
.metric .value { font-size: 28px; font-weight: 780; line-height: 1; }
.metric .label { color: var(--muted); font-size: 12px; }
.status {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 64px;
height: 24px;
padding: 0 8px;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12px;
color: var(--muted);
background: #fff;
white-space: nowrap;
}
.status.ok { color: var(--ok); border-color: rgba(20, 108, 67, .34); }
.status.failed, .status.error, .status.timeout { color: var(--bad); border-color: rgba(173, 47, 43, .34); }
.status.missing { color: var(--bad); border-color: rgba(173, 47, 43, .34); }
.status.warning, .status.stale { color: var(--warn); border-color: rgba(154, 91, 0, .34); }
.status.running, .status.queued { color: var(--info); border-color: rgba(49, 95, 133, .34); }
.badge {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 2px 7px;
border-radius: 999px;
border: 1px solid var(--line);
background: #fff;
color: var(--muted);
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.badge.docs { color: var(--ok); border-color: rgba(20, 108, 67, .34); }
.badge.example { color: var(--warn); border-color: rgba(154, 91, 0, .38); background: #fff6e7; }
.warning-note {
margin: 0 0 10px;
border-left: 3px solid var(--warn);
background: #fff8eb;
padding: 8px 10px;
color: #5b3b08;
font-size: 13px;
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
th { color: var(--muted); font-size: 11px; text-transform: uppercase; }
td.mono, .mono { font-family: var(--mono); font-size: 12px; }
.two { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(340px, .85fr); gap: 10px; }
.commands { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.stack {
display: grid;
gap: 10px;
}
.ask-row { display: grid; grid-template-columns: minmax(0, 1fr) 220px 110px; gap: 8px; margin-bottom: 10px; }
.filter-row { display: grid; grid-template-columns: 150px 230px minmax(180px, 1fr) 100px; gap: 8px; margin-bottom: 10px; }
.table-scroll { overflow: auto; max-height: 430px; border: 1px solid var(--line); border-radius: 6px; background: #fff; }
.table-scroll table { min-width: 920px; }
.doc-title { font-weight: 700; max-width: 360px; overflow-wrap: anywhere; }
.doc-url { color: var(--muted); overflow-wrap: anywhere; }
.tiny-btn { min-height: 28px; padding: 3px 8px; }
.command-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
text-align: left;
padding: 8px 10px;
min-height: 42px;
}
.command-btn span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.command-btn small { color: var(--muted); font-size: 11px; }
pre {
margin: 0;
max-height: 360px;
overflow: auto;
background: #17201c;
color: #dfe9e2;
padding: 10px;
border-radius: 6px;
font: 12px/1.45 var(--mono);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.hidden { display: none; }
@media (max-width: 1080px) {
.app { grid-template-columns: 1fr; }
aside { position: static; }
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.two { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
main { padding: 10px; }
.grid, .commands { grid-template-columns: 1fr; }
.filter-row { grid-template-columns: 1fr; }
.topbar { align-items: flex-start; flex-direction: column; }
.toolbar { justify-content: flex-start; }
th:nth-child(4), td:nth-child(4) { display: none; }
}
</style>
</head>
<body>
<div class="app">
<aside>
<section class="brand">
<h1>LLM Control Console</h1>
<p>Модели, RAG, 1C adapter, проверки и переносимые артефакты.</p>
</section>
<nav id="nav">
<button data-view="overview" class="active">Обзор</button>
<button data-view="models">Модели</button>
<button data-view="rag">RAG и ИТС</button>
<button data-view="onec">1С</button>
<button data-view="checks">Проверки</button>
<button data-view="jobs">Журнал</button>
</nav>
<section class="side-block">
<div>Workspace</div>
<div class="mono" id="workspace">...</div>
</section>
</aside>
<main>
<section class="topbar">
<div>
<h2 id="title">Обзор</h2>
<div class="meta" id="generated">...</div>
</div>
<div class="toolbar">
<button id="refreshBtn">Обновить</button>
<button class="primary" data-command="artifact_manifest">Manifest</button>
<button data-command="artifact_check">Check</button>
</div>
</section>
<section id="view-overview" class="view"></section>
<section id="view-models" class="view hidden"></section>
<section id="view-rag" class="view hidden"></section>
<section id="view-onec" class="view hidden"></section>
<section id="view-checks" class="view hidden"></section>
<section id="view-jobs" class="view hidden"></section>
</main>
</div>
<script>
const state = {
data: null,
view: "overview",
selectedJob: null,
officialDocs: null,
officialDocText: null,
officialDocFilters: {status: "all", source_type: "all", q: ""}
};
const fmtBytes = (v) => {
const n = Number(v || 0);
if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(2)} GB`;
if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
return `${n} B`;
};
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
const status = (s) => `<span class="status ${esc(String(s || 'unknown').toLowerCase())}">${esc(s || "unknown")}</span>`;
const fileMeta = (f) => f?.exists ? `${fmtBytes(f.size_bytes)} · ${new Date(f.mtime).toLocaleString()}` : "нет файла";
const sourceBadge = (scope, type) => {
const t = String(type || "");
if (scope === "metadata" || scope === "all_with_examples" || t === "metadata") return `<span class="badge example">Пример</span>`;
if (t.startsWith("official_1c_its") || scope === "official_1c_docs") return `<span class="badge docs">Документация</span>`;
return `<span class="badge">${esc(t || "Источник")}</span>`;
};
async function api(path, options) {
const res = await fetch(path, options);
const data = await res.json();
if (!res.ok) throw new Error(data.error || res.statusText);
return data;
}
async function refresh() {
state.data = await api("/api/overview");
render();
}
async function runCommand(id) {
const data = await api("/api/run", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({command_id: id})
});
state.selectedJob = data.job.id;
await refresh();
pollJob(data.job.id);
}
async function fetchTargetItsDoc() {
const url = document.getElementById("itsTargetUrl").value.trim();
const target = document.getElementById("itsTargetResult");
if (!url) {
target.innerHTML = `<pre>Введите URL страницы ИТС.</pre>`;
return;
}
target.innerHTML = `<pre>Запускаю прицельную загрузку...</pre>`;
const data = await api("/api/official-docs/fetch-target", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({url, max_pages: 12, max_depth: 2})
});
state.selectedJob = data.job.id;
target.innerHTML = `<pre>Задание создано: ${esc(data.job.id)}</pre>`;
await refresh();
pollJob(data.job.id);
}
async function askRag() {
const question = document.getElementById("ragQuestion").value.trim();
const sourceType = document.getElementById("ragSourceType").value;
const platformVersion = document.getElementById("ragPlatformVersion")?.value.trim() || "";
const platformDocId = document.getElementById("ragPlatformDocId")?.value.trim() || "";
const target = document.getElementById("ragAnswer");
if (!question) {
target.innerHTML = `<pre>Введите вопрос.</pre>`;
return;
}
target.innerHTML = `<pre>Ищу контекст...</pre>`;
const data = await api("/api/rag/query", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({question, source_type: sourceType, platform_version: platformVersion, platform_doc_id: platformDocId, limit: 5})
});
const scopeWarning = ["metadata", "all_with_examples"].includes(data.source_scope)
? `<p class="warning-note">Выбран контур с примерами. Такой результат нельзя использовать как факт текущей базы.</p>`
: "";
target.innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Ответ</h3><span class="status ${data.result_count ? "ok" : "missing"}">${esc(data.result_count)} источн.</span></div>
<div class="panel-body">
${scopeWarning}
<p>${esc(data.answer)}</p>
<table><thead><tr><th>Score</th><th>Источник</th><th>Фрагмент</th></tr></thead><tbody>
${data.results.map(r => `<tr><td>${esc(r.score)}</td><td>${sourceBadge(data.source_scope, r.source_type)}<br>${esc(r.title)}<br><span class="mono">${esc([r.platform_version, r.platform_doc_id, r.doc_book].filter(Boolean).join(" · "))}</span><br><span class="mono">${esc(r.doc_coordinate || "")}</span><br><span class="mono">${esc(r.url || r.source_path)}</span></td><td>${esc(r.snippet)}</td></tr>`).join("")}
</tbody></table>
</div>
</div>`;
}
async function loadOfficialDocs() {
const statusValue = document.getElementById("itsDocStatus")?.value || state.officialDocFilters.status;
const sourceValue = document.getElementById("itsDocSource")?.value || state.officialDocFilters.source_type;
const queryValue = document.getElementById("itsDocQuery")?.value.trim() || "";
state.officialDocFilters = {status: statusValue, source_type: sourceValue, q: queryValue};
const params = new URLSearchParams({status: statusValue, source_type: sourceValue, q: queryValue, limit: "500"});
state.officialDocs = await api(`/api/official-docs/pages?${params.toString()}`);
state.officialDocText = null;
renderOfficialDocs();
}
async function showOfficialDocText(row) {
const params = new URLSearchParams();
if (row.normalized_file) params.set("normalized_file", row.normalized_file);
if (row.raw_file) params.set("raw_file", row.raw_file);
params.set("title", row.title || "");
params.set("source_type", row.source_type || "");
state.officialDocText = await api(`/api/official-docs/page-text?${params.toString()}`);
renderOfficialDocs();
}
function renderOfficialDocs() {
const target = document.getElementById("itsDocsTable");
if (!target) return;
const data = state.officialDocs;
if (!data) {
target.innerHTML = `
<div class="filter-row">
<select id="itsDocStatus">
<option value="all">Все статусы</option>
<option value="passed">Прошли</option>
<option value="skipped">Пропущены</option>
</select>
<select id="itsDocSource"><option value="all">Все источники</option></select>
<input id="itsDocQuery" value="" placeholder="Поиск по заголовку, URL, причине">
<button id="itsDocApply" class="primary">Показать</button>
</div>
<pre>Список страниц еще не загружен.</pre>`;
document.getElementById("itsDocApply").onclick = () => loadOfficialDocs().catch(err => {
target.innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
return;
}
const rows = data.rows || [];
const sourceOptions = ["all", ...(data.source_types || [])]
.map(value => `<option value="${esc(value)}" ${state.officialDocFilters.source_type === value ? "selected" : ""}>${esc(value === "all" ? "Все источники" : value)}</option>`)
.join("");
const table = rows.length ? `
<div class="table-scroll"><table><thead><tr><th>Статус</th><th>Страница</th><th>Причина</th><th>Качество</th><th></th></tr></thead><tbody>
${rows.map((row, index) => {
const q = row.quality || {};
return `<tr>
<td>${status(row.status === "passed" ? "ok" : "skipped")}<br><span class="mono">${esc(row.source_id || "")}</span></td>
<td><div class="doc-title">${esc(row.title || "")}</div><div class="doc-url">${esc(row.url || "")}</div><div class="mono">${esc(row.normalized_file || row.raw_file || "")}</div></td>
<td>${esc(row.skip_reason || "прошла нормализацию")}</td>
<td class="mono">строк ${esc(q.content_lines ?? "")}<br>текст ${esc(q.prose_lines ?? "")}<br>слов ${esc(q.word_count ?? "")}</td>
<td><button class="tiny-btn" data-its-doc-index="${index}">Текст</button></td>
</tr>`;
}).join("")}
</tbody></table></div>` : `<pre>По фильтру страниц нет.</pre>`;
const detail = state.officialDocText
? `<div class="panel" style="margin-top:10px"><div class="panel-head"><h3>Просмотр текста</h3><span class="badge">${esc(state.officialDocText.kind)}</span></div><div class="panel-body"><div class="mono" style="margin-bottom:8px">${esc(state.officialDocText.path)}</div><pre>${esc(state.officialDocText.text || "")}</pre></div></div>`
: "";
target.innerHTML = `
<div class="filter-row">
<select id="itsDocStatus">
<option value="all" ${state.officialDocFilters.status === "all" ? "selected" : ""}>Все статусы</option>
<option value="passed" ${state.officialDocFilters.status === "passed" ? "selected" : ""}>Прошли</option>
<option value="skipped" ${state.officialDocFilters.status === "skipped" ? "selected" : ""}>Пропущены</option>
</select>
<select id="itsDocSource">${sourceOptions}</select>
<input id="itsDocQuery" value="${esc(state.officialDocFilters.q)}" placeholder="Поиск по заголовку, URL, причине">
<button id="itsDocApply" class="primary">Показать</button>
</div>
<p class="mono">Прошло: ${esc(data.counts?.passed ?? 0)} · пропущено: ${esc(data.counts?.skipped ?? 0)} · в таблице: ${esc(data.counts?.filtered ?? 0)}</p>
${table}
${detail}`;
document.getElementById("itsDocApply").onclick = () => loadOfficialDocs().catch(err => {
target.innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
document.querySelectorAll("[data-its-doc-index]").forEach(btn => {
btn.onclick = () => showOfficialDocText(rows[Number(btn.dataset.itsDocIndex)]).catch(err => {
state.officialDocText = {kind: "error", path: "", text: err.stack || err.message};
renderOfficialDocs();
});
});
}
async function resolveFact() {
const target = document.getElementById("factAnswer");
const sourceKind = document.getElementById("factSourceKind").value;
const sourcePath = document.getElementById("factSourcePath").value.trim();
const path = document.getElementById("factPath").value.trim();
const view = document.getElementById("factView").value;
if (!path) {
target.innerHTML = `<pre>Введите путь факта.</pre>`;
return;
}
target.innerHTML = `<pre>Проверяю факт в источнике конфигурации...</pre>`;
const data = await api("/api/1c/fact", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({source_kind: sourceKind, source_path: sourcePath, path, view})
});
target.innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Проверка факта</h3>${status(data.exists ? "ok" : "missing")}</div>
<div class="panel-body">
<table><tbody>
<tr><th>Факт</th><td class="mono">${esc(path)}</td></tr>
<tr><th>Источник</th><td>${esc(data.source?.kind || "")}<br><span class="mono">${esc(data.source?.path || "")}</span></td></tr>
<tr><th>Объект</th><td>${esc(data.object?.kind_ru || data.object?.kind || "")}.${esc(data.object?.name || "")}</td></tr>
<tr><th>Статус</th><td>${esc(data.exists ? "подтвержден" : "не подтвержден")} · ${esc(data.confidence || "")} · ${esc(data.reason || "")}</td></tr>
<tr><th>Найдено</th><td><pre>${esc(JSON.stringify(data.match || data.nearest || null, null, 2))}</pre></td></tr>
</tbody></table>
</div>
</div>`;
}
async function routeOneCQuestion() {
const target = document.getElementById("routeAnswer");
const question = document.getElementById("routeQuestion").value.trim();
const sourcePath = document.getElementById("routeSourcePath").value.trim();
const view = document.getElementById("routeView").value;
if (!question) {
target.innerHTML = `<pre>Введите вопрос или задачу.</pre>`;
return;
}
target.innerHTML = `<pre>Определяю маршрут...</pre>`;
const data = await api("/api/1c/intake", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({question, source_path: sourcePath, view})
});
const route = data.route || {};
const decision = route.decision || {};
const checks = route.fact_checks || [];
const risks = route.source_risks || [];
const facts = data.facts || {};
const policy = data.answer_policy || {};
target.innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Маршрут</h3>${status(decision.route || "unknown")}</div>
<div class="panel-body">
${risks.length ? `<p class="warning-note">${esc(risks.map(r => r.message).join(" "))}</p>` : ""}
<table><tbody>
<tr><th>Документация</th><td>${esc(decision.needs_docs_rag ? "нужна" : "не нужна")} · ${esc(decision.safe_rag_scope || "")}</td></tr>
<tr><th>Текущая база</th><td>${esc(decision.needs_current_config ? "нужна" : "не нужна")}</td></tr>
<tr><th>Код</th><td>${status(policy.code_generation_allowed ? "ok" : "blocked")} ${esc(policy.must_check_current_config_before_code ? "проверка фактов обязательна" : "")}</td></tr>
<tr><th>Факты</th><td>${esc(facts.confirmed_count || 0)} подтвержд. · ${esc(facts.unresolved_count || 0)} не подтвержд.</td></tr>
<tr><th>Пути фактов</th><td class="mono">${esc((route.fact_paths || []).join(", ") || "нет")}</td></tr>
</tbody></table>
${checks.length ? `<table><thead><tr><th>Факт</th><th>Статус</th><th>Деталь</th></tr></thead><tbody>
${checks.map(row => {
const r = row.result || {};
const match = r.match || {};
return `<tr><td class="mono">${esc(row.path)}</td><td>${status(r.exists ? "ok" : "missing")}</td><td>${esc(r.exists ? `${match.name || ""} ${match.type || ""}` : (r.reason || row.reason || ""))}<br><span class="mono">${esc(JSON.stringify(match.origin || {}, null, 0))}</span></td></tr>`;
}).join("")}
</tbody></table>` : ""}
<pre>${esc(JSON.stringify(data.next_commands || [], null, 2))}</pre>
</div>
</div>`;
}
async function pollJob(id) {
const data = await api(`/api/jobs/${id}`);
const job = data.job;
if (!job) return;
state.selectedJob = id;
await refresh();
if (["queued", "running"].includes(job.status)) setTimeout(() => pollJob(id), 1200);
}
function setView(view) {
state.view = view;
document.querySelectorAll("nav button").forEach(btn => btn.classList.toggle("active", btn.dataset.view === view));
document.querySelectorAll(".view").forEach(el => el.classList.add("hidden"));
document.getElementById(`view-${view}`).classList.remove("hidden");
document.getElementById("title").textContent = {
overview: "Обзор", models: "Модели", rag: "RAG и ИТС", onec: "1С", checks: "Проверки", jobs: "Журнал"
}[view];
}
function renderMetrics(d) {
const a = d.artifacts.counts || {};
const m = d.models.counts || {};
return `
<div class="grid">
<div class="panel metric"><div class="panel-body"><div class="value">${a.artifacts || 0}</div><div class="label">групп артефактов</div></div></div>
<div class="panel metric"><div class="panel-body"><div class="value">${fmtBytes(a.total_size_bytes)}</div><div class="label">локальные модели и RAG</div></div></div>
<div class="panel metric"><div class="panel-body"><div class="value">${m.ok || 0}</div><div class="label">моделей OK</div></div></div>
<div class="panel metric"><div class="panel-body"><div class="value">${d.one_c.status || "..."}</div><div class="label">1C plugin</div></div></div>
</div>`;
}
function renderOverview(d) {
document.getElementById("view-overview").innerHTML = `
${renderMetrics(d)}
<div class="two">
<div class="panel">
<div class="panel-head"><h3>Артефакты</h3>${status((d.artifacts.file.exists && "ready") || "missing")}</div>
<div class="panel-body">${artifactTable(d.artifacts.artifacts.slice(0, 8))}</div>
</div>
<div class="panel">
<div class="panel-head"><h3>Последние задания</h3></div>
<div class="panel-body">${jobsList(d.jobs.slice(0, 6))}</div>
</div>
</div>`;
}
function artifactTable(items) {
return `<table><thead><tr><th>Группа</th><th>Файлы</th><th>Размер</th><th>Статус</th></tr></thead><tbody>${
items.map(a => `<tr><td>${esc(a.name)}</td><td>${a.files || 0}</td><td>${fmtBytes(a.size_bytes)}</td><td>${status(a.exists ? "ok" : "missing")}</td></tr>`).join("")
}</tbody></table>`;
}
function renderModels(d) {
const q6 = d.models.q6_download || {};
const q6Service = d.models.q6_service || {};
const rows = (d.models.models || []).map(m => {
const servedOnGpu = q6Service.available && m.served_model_name === q6Service.expected_model;
const statusText = servedOnGpu
? `${status("ok")}<br><span class="mono">served on GPU; local storage: ${esc(m.status || "")}${m.reason ? ` (${esc(m.reason)})` : ""}</span>`
: `${status(m.status)}<br><span class="mono">${esc(m.reason || "")}</span>`;
return `
<tr>
<td>
<div class="doc-title">${esc(m.id)}</div>
<span class="mono">${esc(m.served_model_name || "")}</span>
</td>
<td>${esc(m.runtime)}<br><span class="mono">${esc(m.quantization || "")}</span></td>
<td>${statusText}</td>
<td class="mono">${esc(m.storage_path)}${m.filename ? `<br>${esc(m.filename)}` : ""}</td>
</tr>`;
}).join("");
document.getElementById("view-models").innerHTML = `
<div class="panel">
<div class="panel-head"><h3>Q6 1C service</h3>${status(q6Service.state || "unknown")}</div>
<div class="panel-body">
<div class="stack">
<div class="commands">
<button data-command="q6_service_start">Запустить сервис Q6</button>
<button data-command="q6_service_restart">Перезапустить Q6</button>
<button data-command="q6_service_status">Статус сервиса Q6</button>
<button data-command="q6_service_stop">Остановить Q6</button>
</div>
<table><tbody>
<tr><th>Endpoint</th><td class="mono">${esc(q6Service.base_url || "")}</td></tr>
<tr><th>OpenAI base</th><td class="mono">${esc(q6Service.openai_base_url || "")}</td></tr>
<tr><th>Model</th><td>${status(q6Service.available ? "ok" : "missing")} <span class="mono">${esc(q6Service.expected_model || "")}</span></td></tr>
<tr><th>Served</th><td class="mono">${esc((q6Service.models || []).join(", ") || "нет")}</td></tr>
<tr><th>Container</th><td class="mono">${esc(q6Service.container || "")}<br>${esc(q6Service.container_status || "")}</td></tr>
<tr><th>Error</th><td>${esc(q6Service.error || "")}</td></tr>
</tbody></table>
<div class="panel" style="border-style:dashed">
<div class="panel-head"><h3>OpenAI connection</h3><span class="badge">external agent</span></div>
<div class="panel-body">
<table><tbody>
<tr><th>OPENAI_BASE_URL</th><td class="mono">${esc(q6Service.openai_base_url || "")}</td></tr>
<tr><th>OPENAI_API_KEY</th><td class="mono">${esc(q6Service.openai_api_key_hint || "dummy")}</td></tr>
<tr><th>OPENAI_MODEL</th><td class="mono">${esc(q6Service.expected_model || "")}</td></tr>
</tbody></table>
<pre>OPENAI_BASE_URL=${esc(q6Service.openai_base_url || "")}
OPENAI_API_KEY=${esc(q6Service.openai_api_key_hint || "dummy")}
OPENAI_MODEL=${esc(q6Service.expected_model || "")}</pre>
</div>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-head"><h3>Q6 1C download</h3>${status(q6.state || "unknown")}</div>
<div class="panel-body">
<div class="commands">
<button data-command="q6_download_start">Скачать Q6</button>
<button data-command="q6_download_status">Статус загрузки Q6</button>
<button data-command="q6_download_stop">Остановить загрузку Q6</button>
<button data-command="model_storage">Проверить модели</button>
</div>
<table><tbody>
<tr><th>Progress</th><td>${esc(q6.percent ?? 0)}% · ${fmtBytes(q6.bytes || 0)} / ${fmtBytes(q6.expected_bytes || 0)}</td></tr>
<tr><th>Message</th><td>${esc(q6.message || "")}</td></tr>
<tr><th>File</th><td class="mono">${esc(q6.file || "")}</td></tr>
<tr><th>Log</th><td class="mono">${esc(q6.log || "")}</td></tr>
</tbody></table>
</div>
</div>
<div class="panel"><div class="panel-head"><h3>Model storage</h3>${status(d.models.status || "unknown")}</div>
<div class="panel-body"><p class="mono">${esc(fileMeta(d.models.file))}</p>
<table><thead><tr><th>Model / served</th><th>Runtime / quant</th><th>Status / reason</th><th>Path / file</th></tr></thead><tbody>${rows}</tbody></table></div></div>`;
}
function renderRag(d) {
const r = d.rag;
const p = r.official_progress || {};
const q = r.official_quality || {};
const sq = r.official_static_check || {};
const sc = r.official_start_coverage || {};
const pv = r.official_platform_versions || {};
const ac = r.official_access_check || {};
const qc = q.counts || {};
const sqc = sq.counts || {};
const scc = sc.counts || {};
const pvc = pv.counts || {};
const pvd = pv.defaults || {};
const latestActive = pvd.latest_active_8_3 || {};
document.getElementById("view-rag").innerHTML = `
<div class="two">
<div class="panel"><div class="panel-head"><h3>RAG corpus</h3></div><div class="panel-body">
<table><tbody>
<tr><th>Corpus</th><td>${esc(fileMeta(r.corpus))}</td></tr>
<tr><th>Index</th><td>${esc(fileMeta(r.index))}</td></tr>
<tr><th>Manifest</th><td>${esc(fileMeta(r.manifest))}</td></tr>
<tr><th>ITS starts</th><td>${esc(fileMeta(r.official_start_links))}</td></tr>
<tr><th>ITS platform versions</th><td>${esc(fileMeta(pv.file))} · версий ${esc(pvc.versions ?? 0)} · активных ${esc(pvc.active_versions ?? 0)} · default ${esc(latestActive.platform_version || "")}</td></tr>
<tr><th>ITS access</th><td>${status(ac.status === "ok" ? "ok" : "warning")} ${esc((ac.findings || []).join(", ") || "нет отчета")} · page ${esc(ac.page?.http_status || "")} · src ${esc(ac.src?.http_status || "")}</td></tr>
<tr><th>ITS start coverage</th><td>${status(sc.passed ? "ok" : "warning")} активных ${esc(scc.active_candidates ?? 0)}/${esc(scc.candidates ?? 0)} · источников ${esc(scc.sources ?? 0)}</td></tr>
<tr><th>ITS raw</th><td>${esc(fileMeta(r.official_raw))}</td></tr>
<tr><th>ITS progress</th><td>${status(p.status || "unknown")} ${esc(p.page_count || 0)} стр. · ошибок ${esc(p.error_count || 0)} · очередь ${esc(p.queue_size ?? "")} · resume ${esc(p.skipped_existing || 0)}/${esc(p.resumed_pages || 0)}</td></tr>
<tr><th>ITS current</th><td class="mono">${esc(p.current_url || "")}</td></tr>
<tr><th>ITS normalized</th><td>${esc(fileMeta(r.official_normalized))}</td></tr>
<tr><th>ITS media</th><td>${esc(fileMeta(r.official_media))}</td></tr>
<tr><th>ITS static</th><td>${esc(fileMeta(r.official_static))} ${r.official_static?.exists ? `<a class="badge" href="${esc(r.official_static_url)}" target="_blank">открыть</a>` : ""}</td></tr>
<tr><th>ITS static quality</th><td>${status(sq.passed ? "ok" : "warning")} ${esc(sqc.pages ?? 0)} стр. · media ${esc(sqc.media_files ?? 0)} · assets ${esc(sqc.asset_files ?? 0)} · внеш. img ${esc(sqc.external_image_pages ?? 0)} · внеш. css/js ${esc(sqc.external_asset_pages ?? 0)} · битых ссылок ${esc(sqc.broken_local_refs ?? 0)}</td></tr>
<tr><th>ITS quality</th><td>${status(q.passed ? "ok" : "warning")} ${esc(qc.normalized_pages ?? 0)} стр. · пропущено ${esc(qc.skipped_pages ?? 0)} · src ${esc(qc.content_src_pages ?? 0)}/${esc(qc.raw_pages ?? 0)} · img ${esc(qc.media_images ?? 0)} · предупреждений ${esc(qc.warnings ?? 0)}</td></tr>
</tbody></table>
</div></div>
<div class="panel"><div class="panel-head"><h3>Действия</h3></div><div class="panel-body">
<div class="ask-row">
<input id="itsTargetUrl" value="https://its.1c.ru/db/v8316doc#bookmark:dev:TI000000044" placeholder="URL страницы ИТС">
<button id="itsTargetFetchBtn" class="primary">Загрузить страницу ИТС</button>
</div>
<div id="itsTargetResult"><pre>Прицельная загрузка добавит страницу в RAG.</pre></div>
<div class="commands">
<button data-command="1c_rag_freshness">Проверить свежесть RAG</button>
<button data-command="1c_rag_source_governance">Проверить источники RAG</button>
<button data-command="official_docs_quality">Проверить качество ИТС</button>
<button data-command="official_docs_access">Проверить доступ ИТС</button>
<button data-command="official_docs_ingestion_logic">Проверить логику ИТС</button>
<button data-command="official_docs_discover_starts">Найти старты ИТС</button>
<button data-command="official_docs_start_coverage">Проверить старты ИТС</button>
<button data-command="official_docs_platform_versions">Проверить версии платформы</button>
<button data-command="official_docs_fetch_plan">План загрузки ИТС</button>
<button data-command="official_docs_download_media">Скачать медиа ИТС</button>
<button data-command="official_docs_static_site">Собрать статический ИТС</button>
<button data-command="official_docs_static_quality">Проверить статический ИТС</button>
<button data-command="official_docs_private">Проверить ИТС artifacts</button>
<button data-command="official_docs_reload_fresh">Перезагрузить ИТС 2000 стр.</button>
<button class="warn" data-command="its_cookie_prompt">Обновить cookie ИТС</button>
</div>
</div></div>
</div>
<div class="panel">
<div class="panel-head"><h3>Загруженные страницы ИТС</h3><span class="badge">контроль качества</span></div>
<div class="panel-body" id="itsDocsTable"><pre>Нажмите "Показать", чтобы загрузить список страниц.</pre></div>
</div>
<div class="panel">
<div class="panel-head"><h3>Вопрос по документации</h3></div>
<div class="panel-body">
<div class="ask-row">
<input id="ragQuestion" value="Что такое форма в 1С?" placeholder="Вопрос по документации 1С">
<select id="ragSourceType">
<option value="official_1c_docs">Документация: все</option>
<option value="official_1c_its_glossary">ИТС: глоссарий</option>
<option value="official_1c_its_platform_doc">ИТС: документация платформы</option>
<option value="official_1c_its_developer_guide">ИТС: руководство разработчика</option>
<option value="metadata">Примеры metadata</option>
<option value="all_with_examples">Все, включая примеры</option>
</select>
<input id="ragPlatformVersion" value="" placeholder="8.3.16">
<input id="ragPlatformDocId" value="" placeholder="v8316doc">
<button id="ragAskBtn" class="primary">Спросить</button>
</div>
<div id="ragAnswer"><pre>Ответ появится здесь.</pre></div>
</div>
</div>`;
}
function renderOneC(d) {
document.getElementById("view-onec").innerHTML = `
<div class="panel"><div class="panel-head"><h3>1C plugin</h3>${status(d.one_c.status || "unknown")}</div>
<div class="panel-body">
<div class="commands">
<button data-command="1c_plugin">Запустить healthcheck 1С</button>
<button data-command="1c_question_router">Проверить маршрутизатор</button>
<button data-command="1c_agent_intake">Проверить intake</button>
</div>
<p class="mono">${esc(fileMeta(d.one_c.file))}</p>
<table><tbody>
<tr><th>ID</th><td>${esc(d.one_c.manifest.id || "")}</td></tr>
<tr><th>Status</th><td>${esc(d.one_c.manifest.status || "")}</td></tr>
<tr><th>Failed</th><td>${esc((d.one_c.failed_checks || []).join(", ") || "нет")}</td></tr>
</tbody></table>
</div></div>
<div class="panel">
<div class="panel-head"><h3>Маршрут вопроса</h3></div>
<div class="panel-body">
<div class="ask-row">
<input id="routeQuestion" value="В примере RAG есть реквизит Артикул у справочника Номенклатура. Напиши код для текущей базы." placeholder="Вопрос или задача по 1С">
<select id="routeView">
<option value="effective">effective</option>
<option value="base">base</option>
</select>
<button id="routeAskBtn" class="primary">Разобрать</button>
</div>
<input id="routeSourcePath" value="reports/1c-sql/upo/unified-object-route-index.json" placeholder="Путь к route-index">
<div id="routeAnswer"><pre>Маршрут появится здесь.</pre></div>
</div>
</div>
<div class="panel">
<div class="panel-head"><h3>Проверка факта конфигурации</h3></div>
<div class="panel-body">
<div class="ask-row">
<select id="factSourceKind">
<option value="route_index">Текущий route-index</option>
<option value="metadata_snapshot">Явный metadata snapshot</option>
</select>
<input id="factSourcePath" value="reports/1c-sql/upo/unified-object-route-index.json" placeholder="Путь к источнику фактов">
</div>
<div class="ask-row">
<input id="factPath" value="Документ.ПриходнаяНакладная.ДатаСоздания" placeholder="Например: Справочник.Номенклатура.Цвет">
<select id="factView">
<option value="effective">effective</option>
<option value="base">base</option>
<option value="extension">extension</option>
</select>
<button id="factAskBtn" class="primary">Проверить</button>
</div>
<div id="factAnswer"><pre>Результат появится здесь.</pre></div>
</div>
</div>`;
}
function renderChecks(d) {
const grouped = {};
for (const cmd of d.commands) (grouped[cmd.category] ||= []).push(cmd);
document.getElementById("view-checks").innerHTML = Object.entries(grouped).map(([group, cmds]) => `
<div class="panel"><div class="panel-head"><h3>${esc(group)}</h3></div><div class="panel-body"><div class="commands">
${cmds.map(c => `<button class="command-btn" data-command="${esc(c.id)}"><span>${esc(c.label)}</span><small>${esc(c.id)}</small></button>`).join("")}
</div></div></div>
`).join("");
}
function jobsList(jobs) {
if (!jobs.length) return `<p class="mono">Заданий пока нет.</p>`;
return `<table><thead><tr><th>Задание</th><th>Статус</th><th>Время</th></tr></thead><tbody>${
jobs.map(j => `<tr><td>${esc(j.label)}</td><td>${status(j.status)}</td><td class="mono">${esc(j.finished_at || j.started_at || j.created_at)}</td></tr>
${j.id === state.selectedJob ? `<tr><td colspan="3"><pre>${esc((j.stdout || "") + (j.stderr ? "\\nERR:\\n" + j.stderr : ""))}</pre></td></tr>` : ""}`).join("")
}</tbody></table>`;
}
function renderJobs(d) {
document.getElementById("view-jobs").innerHTML = `<div class="panel"><div class="panel-head"><h3>Журнал выполнения</h3></div><div class="panel-body">${jobsList(d.jobs)}</div></div>`;
}
function render() {
const d = state.data;
if (!d) return;
document.getElementById("workspace").textContent = d.workspace;
document.getElementById("generated").textContent = d.generated_at;
renderOverview(d); renderModels(d); renderRag(d); renderOneC(d); renderChecks(d); renderJobs(d);
renderOfficialDocs();
setView(state.view);
document.querySelectorAll("[data-command]").forEach(btn => btn.onclick = () => runCommand(btn.dataset.command));
const askBtn = document.getElementById("ragAskBtn");
if (askBtn) askBtn.onclick = () => askRag().catch(err => {
document.getElementById("ragAnswer").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const factBtn = document.getElementById("factAskBtn");
if (factBtn) factBtn.onclick = () => resolveFact().catch(err => {
document.getElementById("factAnswer").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const routeBtn = document.getElementById("routeAskBtn");
if (routeBtn) routeBtn.onclick = () => routeOneCQuestion().catch(err => {
document.getElementById("routeAnswer").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const itsDocApply = document.getElementById("itsDocApply");
if (itsDocApply) itsDocApply.onclick = () => loadOfficialDocs().catch(err => {
document.getElementById("itsDocsTable").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
const itsTargetFetchBtn = document.getElementById("itsTargetFetchBtn");
if (itsTargetFetchBtn) itsTargetFetchBtn.onclick = () => fetchTargetItsDoc().catch(err => {
document.getElementById("itsTargetResult").innerHTML = `<pre>${esc(err.stack || err.message)}</pre>`;
});
}
document.getElementById("refreshBtn").onclick = refresh;
document.querySelectorAll("nav button").forEach(btn => btn.onclick = () => { setView(btn.dataset.view); render(); });
refresh().catch(err => {
document.getElementById("view-overview").innerHTML = `<div class="panel"><div class="panel-body"><pre>${esc(err.stack || err.message)}</pre></div></div>`;
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff