Fotos: Gesichtssuche FH-weit (Studierende + Lektoren) als Job mit Live-Progress, Person-Info-Seiten, Scope-Auswahl

This commit is contained in:
Nicolas Gruber
2026-10-19 14:43:09 +02:00
parent 567bcb40ee
commit 6763faf9c1
7 changed files with 405 additions and 72 deletions
+54
View File
@@ -424,10 +424,64 @@ async function reloadVerbands() {
} catch (e) { /* Keep static options */ }
}
/* ================= Gesichtssuche: Job-Polling ================= */
function initSearchJob() {
const job = window.__SEARCH_JOB__;
if (!job) return;
const state = $("#search-state");
const count = $("#search-count");
const progress = $("#search-progress");
const timeBadge = $("#search-time");
const bar = $("#search-bar");
const grid = $("#match-grid");
const hint = $("#search-hint");
const t0 = Date.now();
function tile(r) {
const name = `${esc(r.nachname)}${r.vorname ? ", " + esc(r.vorname) : ""}`;
const pct = Math.max(0, Math.round(r.sim * 100));
const cls = r.sim >= 0.5 ? "match-strong" : (r.sim >= 0.35 ? "match-mid" : "");
const badge = r.kind === "lektor" ? ` <span class="badge badge-muted">Lektor</span>` : "";
return `<div class="match-tile ${cls}">
<a href="/person/${encodeURIComponent(r.uid)}"><img src="${esc(r.img)}" alt="${name}"></a>
<div class="match-name"><a href="/person/${encodeURIComponent(r.uid)}">${name}</a>${badge}</div>
<div class="match-sim">
<div class="sim-bar"><div style="width:${pct}%"></div></div>
<span class="sim-num">${pct} %</span>
</div>
</div>`;
}
function poll() {
fetch(`/api/suche/status/${job.id}`)
.then(r => { if (!r.ok) throw new Error("status"); return r.json(); })
.then(d => {
if (d.total) bar.style.width = (d.done / d.total * 100).toFixed(1) + "%";
progress.textContent = `${d.done}/${d.total}`;
grid.innerHTML = (d.top || []).map(tile).join("");
count.textContent = `${(d.top || []).filter(r => r.sim >= 0.35).length} Treffer`;
if (d.status === "done") {
clearInterval(timer);
state.textContent = d.error ? "Suche fehlgeschlagen" : "Fertig";
hint.style.display = "block";
if (d.error) hint.innerHTML = `<span style="color:#b3261e">${esc(d.error)}</span>`;
const secs = d.took ? `${d.took} s` : `${((Date.now() - t0) / 1000).toFixed(0)} s`;
timeBadge.textContent = secs;
timeBadge.style.display = "";
}
})
.catch(() => { /* retry on next tick */ });
}
const timer = setInterval(poll, 2000);
poll();
}
/* ================= boot ================= */
document.addEventListener("DOMContentLoaded", () => {
if (window.__PLAN_DATA__) initPlan();
if (window.__STGS__) decodeMat();
if (window.__SEARCH_JOB__) initSearchJob();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeDetail();
+33
View File
@@ -215,6 +215,35 @@ table.data .num { text-align: right; font-variant-numeric: tabular-nums; }
overflow: hidden; padding: 0;
}
.match-tile img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; background: var(--bg); }
.match-tile a { display: block; color: inherit; }
.search-progressbar {
height: 8px; background: #eef2f7; border-radius: 6px; overflow: hidden;
margin: 0 0 18px;
}
.search-progressbar > div {
height: 100%; width: 0; background: var(--blue); border-radius: 6px;
transition: width .5s ease;
}
/* ---------- person ---------- */
.person-card {
display: flex; gap: 28px; background: var(--card); border: 1px solid var(--line);
border-radius: var(--radius); box-shadow: var(--shadow); padding: 26px;
align-items: flex-start;
}
.person-photo { flex: 0 0 220px; }
.person-photo img, .person-photo .avatar-initials {
width: 220px; aspect-ratio: 3 / 4; object-fit: cover; border-radius: var(--radius);
border: 1px solid var(--line); background: var(--bg);
}
.person-name { margin: 0 0 10px; font-size: 26px; color: var(--ink); }
.person-badges { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.badge-green { background: #dcfce7; color: var(--green); }
.person-kv { width: 100%; border-collapse: collapse; margin-bottom: 18px; }
.person-kv td { padding: 7px 10px 7px 0; border-bottom: 1px solid var(--line); font-size: 14.5px; }
.person-kv td:first-child { color: var(--muted); width: 180px; font-weight: 600; }
.person-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.photo-name a { color: var(--ink); }
.match-name { padding: 8px 10px 4px; font-size: 13.5px; font-weight: 600; }
.match-name a { color: var(--ink); }
.match-sim { padding: 0 10px 12px; display: flex; align-items: center; gap: 8px; }
@@ -234,4 +263,8 @@ table.data .num { text-align: right; font-variant-numeric: tabular-nums; }
.topbar nav { flex-wrap: wrap; justify-content: center; }
.form-row { flex-direction: column; align-items: stretch; }
.field input, .field select { min-width: 0; width: 100%; }
.person-card { flex-direction: column; align-items: center; text-align: center; }
.person-badges { justify-content: center; }
.person-actions { justify-content: center; }
.person-kv td:first-child { width: 130px; }
}