Duriin-API/public/admin/assets/js/articles.js

145 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// articles page — list + keyword/status/source filtering + edit modal
// depends on: app.js (api, toast, escapeHtml, PAGE, badgeHtml)
let articleOffset = 0;
let currentArticle = null;
async function loadSources() {
const sources = await api("/admin/api/sources");
const sel = document.getElementById("f-source");
sources.forEach(s => {
const opt = document.createElement("option");
opt.value = s;
opt.textContent = s;
sel.appendChild(opt);
});
}
function getFilters() {
return {
keyword: document.getElementById("f-keyword").value.trim(),
source: document.getElementById("f-source").value,
content_status: document.getElementById("f-status").value,
from: document.getElementById("f-from").value,
to: document.getElementById("f-to").value,
};
}
async function loadArticles() {
const f = getFilters();
const params = new URLSearchParams({ limit: PAGE, offset: articleOffset });
if (f.keyword) params.set("keyword", f.keyword);
if (f.source) params.set("source", f.source);
if (f.content_status) params.set("content_status", f.content_status);
if (f.from) params.set("from", f.from + "T00:00:00");
if (f.to) params.set("to", f.to + "T23:59:59");
const data = await api(`/admin/api/articles?${params}`);
const tbody = document.getElementById("articleTable");
tbody.innerHTML = data.rows.map(r => `
<tr>
<td style="color:var(--muted-dark); font-size:12px">${r.id}</td>
<td>
<span class="truncate" title="${escapeHtml(r.title)}">${escapeHtml(r.title)}</span>
<a class="url-link" href="${r.url}" target="_blank" style="font-size:11px; display:block; margin-top:3px; color:var(--muted-dark)">↗ ${new URL(r.url).hostname}</a>
</td>
<td style="color:var(--muted)">${r.source}</td>
<td>${badgeHtml(r.content_status)}</td>
<td style="color:var(--muted-dark); white-space:nowrap; font-size:12px">${r.ingested_at ? r.ingested_at.slice(0, 16) : "—"}</td>
<td><button onclick="openArticle(${r.id})">Edit</button></td>
</tr>
`).join("");
const total = data.total;
document.getElementById("pageInfo").textContent =
`${articleOffset + 1}${Math.min(articleOffset + PAGE, total)} of ${total.toLocaleString()}`;
document.getElementById("prevBtn").disabled = articleOffset === 0;
document.getElementById("nextBtn").disabled = articleOffset + PAGE >= total;
}
async function openArticle(id) {
currentArticle = await api(`/admin/api/articles/${id}`);
const a = currentArticle;
document.getElementById("modalTitle").textContent = `Article #${a.id}`;
document.getElementById("modalMeta").innerHTML = [
a.source && `<span>source: <b>${escapeHtml(a.source)}</b></span>`,
a.pub_date && `<span>pub: ${a.pub_date.slice(0, 16)}</span>`,
`<span>has_embedding: ${a.has_embedding ? "yes" : "no"}</span>`,
a.content_error && `<span style="color:#fca5a5">error: ${escapeHtml(a.content_error.slice(0, 80))}</span>`,
].filter(Boolean).join("");
document.getElementById("m-title").value = a.title || "";
document.getElementById("m-desc").value = a.description || "";
document.getElementById("m-content").value = a.content || "";
document.getElementById("m-status").value = a.content_status || "";
document.getElementById("m-lang").value = a.language || "";
document.getElementById("m-pubdate").value = a.pub_date || "";
document.getElementById("m-indexpage").value = String(a.is_index_page || 0);
document.getElementById("articleOverlay").classList.add("open");
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("searchBtn").onclick = () => { articleOffset = 0; loadArticles(); };
document.getElementById("prevBtn").onclick = () => { articleOffset = Math.max(0, articleOffset - PAGE); loadArticles(); };
document.getElementById("nextBtn").onclick = () => { articleOffset += PAGE; loadArticles(); };
document.querySelector(".filters").addEventListener("keydown", e => {
if (e.key === "Enter") { articleOffset = 0; loadArticles(); }
});
document.getElementById("cancelBtn").onclick = () =>
document.getElementById("articleOverlay").classList.remove("open");
document.getElementById("saveBtn").onclick = async () => {
if (!currentArticle) return;
try {
await api(`/admin/api/articles/${currentArticle.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: document.getElementById("m-title").value,
description: document.getElementById("m-desc").value,
content: document.getElementById("m-content").value,
content_status: document.getElementById("m-status").value || null,
language: document.getElementById("m-lang").value || null,
pub_date: document.getElementById("m-pubdate").value || null,
is_index_page: parseInt(document.getElementById("m-indexpage").value, 10),
}),
});
document.getElementById("articleOverlay").classList.remove("open");
toast("Saved");
loadArticles();
} catch (e) {
toast("Save failed", true);
}
};
document.getElementById("deleteBtn").onclick = async () => {
if (!currentArticle) return;
if (!confirm(`Delete article #${currentArticle.id}? This cannot be undone.`)) return;
try {
await api(`/admin/api/articles/${currentArticle.id}`, { method: "DELETE" });
document.getElementById("articleOverlay").classList.remove("open");
toast("Deleted");
loadArticles();
loadGlobalStats();
} catch (e) {
toast("Delete failed", true);
}
};
loadSources();
loadArticles();
});