// articles page — list + keyword/status/source filtering + edit modal
// filter state is persisted to the url query so reload/share keeps it.
// depends on: app.js (api, toast, escapeHtml, PAGE, badgeHtml, query*)
let articleOffset = 0;
let currentArticle = null;
async function loadSources() {
const sources = await api("/admin/api/sources");
const sel = document.getElementById("f-source");
const current = queryGet("source");
sources.forEach(s => {
const opt = document.createElement("option");
opt.value = s;
opt.textContent = s;
sel.appendChild(opt);
});
if (current) sel.value = current;
}
function getFilters() {
const sel = document.getElementById("f-source");
return {
keyword: document.getElementById("f-keyword").value.trim(),
// fall back to url when the dropdown hasn't populated yet —
// lets us fire loadArticles in parallel with loadSources on init
source: sel.value || queryGet("source"),
content_status: document.getElementById("f-status").value,
from: document.getElementById("f-from").value,
to: document.getElementById("f-to").value,
};
}
// sync current filters + offset into url so the state is bookmarkable
function syncUrl() {
const f = getFilters();
queryWrite({
...f,
offset: articleOffset > 0 ? articleOffset : "",
});
}
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 => `