feat: show moderation provenance

This commit is contained in:
ik
2026-09-12 21:51:09 +07:00
parent def5c39aa1
commit 9681bdbd36
4 changed files with 17 additions and 1 deletions
@@ -64,6 +64,19 @@ const apiUrl = import.meta.env.PUBLIC_API_URL || "http://localhost:8000";
if (pageNumber) pageNumber.textContent = `Страница ${offset / 50 + 1}`;
if (!pending.length) { list.innerHTML = '<div class="state"><h2>Очередь пуста</h2><p>Все внешние записи обработаны.</p></div>'; return; }
list.innerHTML = pending.map((row: Record<string, unknown>) => { const complete = row.x != null && row.y != null && row.weight_g != null; const sourceUrl = safeHttpUrl(row.source_url); return `<article class="moderation-card" data-observation-id="${esc(row.id)}"><div class="moderation-summary"><span class="activity-pill"><i></i>${esc(row.source_system)} · ${esc(row.status)}</span><h2>${esc(row.fish_name)}</h2><p>${esc(row.waterbody_name)}</p><dl><div><dt>Координаты</dt><dd>${row.x == null || row.y == null ? "нет" : `${esc(row.x)}:${esc(row.y)}`}</dd></div><div><dt>Вес</dt><dd>${row.weight_g == null ? "нет" : `${esc(row.weight_g)} г`}</dd></div><div><dt>ID источника</dt><dd>${esc(row.source_external_id)}</dd></div><div><dt>Состояние</dt><dd>${complete ? "полная запись" : "неполная"}</dd></div></dl>${sourceUrl ? `<a href="${sourceUrl}" target="_blank" rel="noreferrer">Открыть первоисточник</a>` : '<span class="privacy">Ссылка источника имеет небезопасный формат</span>'}</div><div class="moderation-actions"><label>Каноническая рыба<select name="fish" required><option value="">Выберите…</option>${options(fishes, row.fish_slug)}</select></label><label>Канонический водоём<select name="waterbody" required><option value="">Выберите…</option>${options(waters, row.waterbody_slug)}</select></label><label>Примечание<textarea name="note" rows="2" maxlength="1000">${esc(row.review_note ?? "")}</textarea></label><p data-alias-message role="status"></p><div><button data-action="secondary" type="button" data-suggest>Подсказать соответствия</button><button data-action="secondary" type="button" data-map>Сопоставить</button><button data-action="primary" type="button" data-publish ${complete && row.status === "ready" ? "" : "disabled"}>Опубликовать</button><button data-action="danger" type="button" data-reject>Отклонить</button></div></div></article>`; }).join("");
const payloadLabels: Record<string, string> = {bait:"Приманка",fishing_method:"Метод ловли",rig_type:"Оснастка",retrieve_method:"Проводка",retrieve_speed:"Скорость проводки",player_name:"Игрок",published_at:"Опубликовано источником",region:"Регион",category:"Категория"};
const missingLabels: Record<string, string> = {coordinates:"координаты",weight_g:"вес"};
pending.forEach((row: Record<string, unknown>, index: number) => {
const card = list.children[index] as HTMLElement | undefined;
const summary = card?.querySelector<HTMLElement>(".moderation-summary");
if (!summary) return;
const payload = Object.entries((row.source_payload as Record<string, unknown>) ?? {});
const missing = ((row.missing_fields as string[]) ?? []).map(key => missingLabels[key] ?? key);
const details = document.createElement("details");
details.className = "moderation-provenance";
details.innerHTML = `<summary>Происхождение и исходные поля</summary><dl><div><dt>Впервые замечено</dt><dd>${esc(new Date(String(row.first_seen_at)).toLocaleString("ru-RU"))}</dd></div><div><dt>Последний раз</dt><dd>${esc(new Date(String(row.last_seen_at)).toLocaleString("ru-RU"))}</dd></div><div><dt>Проверено</dt><dd>${row.reviewed_at ? esc(new Date(String(row.reviewed_at)).toLocaleString("ru-RU")) : "ещё нет"}</dd></div><div><dt>Не хватает</dt><dd>${missing.length ? esc(missing.join(", ")) : "ничего"}</dd></div>${payload.map(([key,value]) => `<div><dt>${esc(payloadLabels[key] ?? key)}</dt><dd>${esc(value)}</dd></div>`).join("")}</dl><p>Показаны только разрешённые поля, сохранённые парсером. Перед публикацией сверьте их с первоисточником.</p>`;
summary.append(details);
});
}
async function changePage(delta: number) {
const oldOffset = offset;