Add server-side /api/rescan upload pipeline and Home UI.

Accept ZIP/single help files, process with help_processor into OUTPUT_DIR+Postgres, pollable jobs, optional RESCAN_TOKEN.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 09:20:45 +03:00
parent a81d5794d4
commit 46a2f2272b
8 changed files with 352 additions and 27 deletions

View File

@@ -195,12 +195,23 @@
.sep { color: var(--border); margin: 0 4px; }
.home-wrap { display: flex; justify-content: center; align-items: flex-start; padding: 24px; }
.home-wrap { display: flex; flex-direction: column; align-items: center; gap: 20px; padding: 24px; }
.home-wrap img {
max-width: 100%; max-height: calc(100vh - 200px);
border: 1px solid var(--border); border-radius: var(--radius-lg);
box-shadow: 0 4px 16px rgba(0,0,0,.06);
max-width: min(920px, 100%); height: auto; border-radius: var(--radius-lg);
border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,.08);
}
.rescan-box {
width: min(640px, 100%);
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
padding: 16px 18px;
}
.rescan-box h3 {
font-family: var(--mono); font-size: 12px; color: var(--muted);
letter-spacing: .06em; text-transform: uppercase; margin-bottom: 12px;
}
.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; }
</style>
</head>
<body>
@@ -222,7 +233,18 @@
{% if home_url %}
<div id="tab-home" class="panel active">
<div class="home-wrap"><img src="{{ home_url }}" alt="Home"></div>
<div class="home-wrap">
<img src="{{ home_url }}" alt="Home">
<div class="rescan-box">
<h3>Rescan — качи help файлове</h3>
<div class="rescan-row">
<input type="file" id="rescan-file" accept=".zip,.docx,.doc,.html,.htm,.pdf,.txt">
<label class="stats"><input type="checkbox" id="rescan-force"> force</label>
<button class="btn-primary" id="rescan-btn" onclick="startRescan()">Старт</button>
</div>
<div class="rescan-status" id="rescan-status">ZIP или един файл → обработка на сървъра → БД + Output</div>
</div>
</div>
</div>
{% endif %}
@@ -307,6 +329,58 @@ function switchTab(name) {
if (name === 'generator') renderGenerator();
}
async function startRescan() {
const inp = document.getElementById('rescan-file');
const st = document.getElementById('rescan-status');
const btn = document.getElementById('rescan-btn');
if (!inp.files || !inp.files[0]) {
st.textContent = 'Избери файл първо.';
return;
}
const fd = new FormData();
fd.append('file', inp.files[0]);
fd.append('prefix', {{ (prefix or 'RIP') | tojson }});
fd.append('force', document.getElementById('rescan-force').checked ? 'true' : 'false');
const token = localStorage.getItem('RESCAN_TOKEN') || '';
if (token) fd.append('token', token);
btn.disabled = true;
st.textContent = 'Качване...';
try {
const headers = {};
if (token) headers['X-Rescan-Token'] = token;
const res = await fetch('/api/rescan', { method: 'POST', body: fd, headers });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || ('HTTP ' + res.status));
st.textContent = 'Job ' + data.job_id + ' — обработка...';
await pollRescan(data.job_id);
} catch (e) {
st.textContent = 'Грешка: ' + e.message;
} finally {
btn.disabled = false;
}
}
async function pollRescan(jobId) {
const st = document.getElementById('rescan-status');
for (let i = 0; i < 180; i++) {
await new Promise(r => setTimeout(r, 2000));
const res = await fetch('/api/rescan/' + encodeURIComponent(jobId));
const data = await res.json();
st.textContent = data.status + (data.message ? ' — ' + data.message : '');
if (data.status === 'done') {
st.textContent = 'Готово. Презареждане...';
location.reload();
return;
}
if (data.status === 'error') {
st.textContent = 'Грешка: ' + (data.message || 'unknown');
return;
}
}
st.textContent = 'Timeout — провери /api/rescan/' + jobId;
}
// ── EDITOR ────────────────────────────
function renderEditor(rows) {
const tbody = document.getElementById('editor-body');