const apiBase = ""; const speciesList = document.querySelector("#species-list"); const searchInput = document.querySelector("#search"); const archiveFilterGroup = document.querySelector("#archive-filter-group"); const detailEmpty = document.querySelector("#detail-empty"); const detail = document.querySelector("#detail"); const detailCode = document.querySelector("#detail-code"); const detailCommonName = document.querySelector("#detail-common-name"); const detailArchiveBadge = document.querySelector("#detail-archive-badge"); const detailArchiveNote = document.querySelector("#detail-archive-note"); const detailScientificName = document.querySelector("#detail-scientific-name"); const detailSummary = document.querySelector("#detail-summary"); const detailSections = document.querySelector("#detail-sections"); const speciesCount = document.querySelector("#species-count"); const sectionCount = document.querySelector("#section-count"); const authTokenInput = document.querySelector("#auth-token"); const authSaveButton = document.querySelector("#auth-save"); const authClearButton = document.querySelector("#auth-clear"); const authStatus = document.querySelector("#auth-status"); const editorPanel = document.querySelector("#editor-panel"); const editorPublicationStatus = document.querySelector("#editor-publication-status"); const editorSummary = document.querySelector("#editor-summary"); const editorNotes = document.querySelector("#editor-notes"); const editorIsArchived = document.querySelector("#editor-is-archived"); const editorSaveButton = document.querySelector("#editor-save"); const editorStatus = document.querySelector("#editor-status"); const auditPanel = document.querySelector("#audit-panel"); const auditList = document.querySelector("#audit-list"); let currentItems = []; let currentSlug = null; let currentSession = null; let currentArchiveFilter = "active"; function getAuthToken() { return window.localStorage.getItem("ecospecies_auth_token") || ""; } function getAuthHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; } function escapeHtml(value) { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } async function requestJson(path, options = {}) { const headers = new Headers(options.headers || {}); const authHeaders = getAuthHeaders(); for (const [key, value] of Object.entries(authHeaders)) { headers.set(key, value); } if (options.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const response = await fetch(`${apiBase}${path}`, { ...options, headers }); const data = await response.json(); return { response, data }; } function isEditorSession() { return Boolean(currentSession && currentSession.user && ["editor", "admin"].includes(currentSession.user.role)); } function getVisibleItems(items) { if (!isEditorSession()) { return items; } if (currentArchiveFilter === "archived") { return items.filter((item) => item.is_archived); } if (currentArchiveFilter === "all") { return items; } return items.filter((item) => !item.is_archived); } function syncArchiveFilterUi() { archiveFilterGroup.classList.toggle("hidden", !isEditorSession()); for (const button of archiveFilterGroup.querySelectorAll("[data-archive-filter]")) { button.classList.toggle("is-active", button.dataset.archiveFilter === currentArchiveFilter); } } async function loadSession() { const { data } = await requestJson("/api/auth/session"); currentSession = data; if (!isEditorSession()) { currentArchiveFilter = "active"; } authTokenInput.value = getAuthToken(); if (data.authenticated) { authStatus.textContent = `${data.user.username} (${data.user.role})`; } else if (data.auth_configured) { authStatus.textContent = "Auth configured, public session"; } else { authStatus.textContent = "Public access"; } syncArchiveFilterUi(); } async function loadSummary() { const { data } = await requestJson("/api/insights/summary"); speciesCount.textContent = data.species_count; sectionCount.textContent = data.section_count; } function renderSpecies(items) { speciesList.innerHTML = ""; const visibleItems = getVisibleItems(items); if (!visibleItems.length) { speciesList.innerHTML = `
${isEditorSession() ? "No species match the current archive filter." : "No species match the current search."}
`; return; } for (const item of visibleItems) { const button = document.createElement("button"); button.className = item.is_archived ? "species-card species-card-archived" : "species-card"; button.type = "button"; const archivedMeta = item.is_archived ? `Archived` : ""; button.innerHTML = ` ${escapeHtml(item.common_name || item.title)} ${escapeHtml((item.summary || "No summary extracted yet.").slice(0, 180))} `; button.addEventListener("click", () => loadSpecies(item.slug)); speciesList.appendChild(button); } } async function loadSpeciesList(search = "") { const query = search ? `?search=${encodeURIComponent(search)}` : ""; const path = isEditorSession() ? `/api/editor/species${query}` : `/api/species${query}`; const { data } = await requestJson(path); currentItems = data.items; syncArchiveFilterUi(); renderSpecies(currentItems); } async function loadSpecies(slug) { currentSlug = slug; const path = isEditorSession() ? `/api/editor/species/${slug}` : `/api/species/${slug}`; const { response, data } = await requestJson(path); if (!response.ok) { detailEmpty.classList.remove("hidden"); detail.classList.add("hidden"); speciesList.innerHTML = `${escapeHtml(data.error || "Unable to load species.")}
`; return; } detailEmpty.classList.add("hidden"); detail.classList.remove("hidden"); detailCode.textContent = data.flelmr_code ? `FLELMR ${data.flelmr_code}` : "Legacy source file"; detailCommonName.textContent = data.common_name || data.title; detailArchiveBadge.classList.toggle("hidden", !data.is_archived); detailArchiveNote.classList.toggle("hidden", !data.is_archived); detailScientificName.textContent = data.scientific_name || "Scientific name missing in source"; detailSummary.textContent = data.summary || "No summary extracted from the current source file."; editorPanel.classList.toggle("hidden", !isEditorSession()); auditPanel.classList.toggle("hidden", !isEditorSession()); if (isEditorSession()) { editorPublicationStatus.value = data.publication_status || "published"; editorSummary.value = data.summary || ""; editorNotes.value = data.editor_notes || ""; editorIsArchived.checked = Boolean(data.is_archived); editorStatus.textContent = data.last_modified_by ? `Last modified by ${data.last_modified_by}` : "Editor session active"; await loadAudit(slug); } detailSections.innerHTML = ""; if (data.diagnostics.length) { const diagnosticsEl = document.createElement("section"); diagnosticsEl.className = "detail-section detail-diagnostics"; diagnosticsEl.innerHTML = `${escapeHtml(section.content)}
`;
}
detailSections.appendChild(sectionEl);
}
if (isEditorSession()) {
for (const button of detailSections.querySelectorAll(".section-save")) {
button.addEventListener("click", async (event) => {
const position = event.currentTarget.dataset.sectionPosition;
const textarea = detailSections.querySelector(`textarea[data-section-position="${position}"]`);
await saveSectionContent(Number(position), textarea.value);
});
}
}
}
function renderAudit(items) {
auditList.innerHTML = "";
if (!items.length) {
auditList.innerHTML = `No audit entries yet.
`; return; } for (const item of items) { const entry = document.createElement("article"); entry.className = "audit-entry"; const detailRows = Object.entries(item.details) .map(([field, values]) => { if (values && typeof values === "object" && "from" in values && "to" in values) { return `${escapeHtml(data.error || "Unable to load audit history.")}
`; return; } renderAudit(data.items); } async function saveEditorialChanges() { if (!currentSlug || !isEditorSession()) { return; } editorStatus.textContent = "Saving..."; const { response, data } = await requestJson(`/api/editor/species/${currentSlug}/editorial`, { method: "POST", body: JSON.stringify({ publication_status: editorPublicationStatus.value, summary: editorSummary.value, editor_notes: editorNotes.value, is_archived: editorIsArchived.checked, }), }); if (!response.ok) { editorStatus.textContent = data.error || "Save failed"; return; } editorStatus.textContent = `Saved by ${data.last_modified_by}`; await Promise.all([loadSummary(), loadSpeciesList(searchInput.value), loadSpecies(currentSlug)]); } async function saveSectionContent(sectionPosition, content) { if (!currentSlug || !isEditorSession()) { return; } editorStatus.textContent = `Saving section ${sectionPosition}...`; const { response, data } = await requestJson(`/api/editor/species/${currentSlug}/sections/${sectionPosition}`, { method: "POST", body: JSON.stringify({ content }), }); if (!response.ok) { editorStatus.textContent = data.error || "Section save failed"; return; } editorStatus.textContent = `Section ${sectionPosition} saved by ${data.last_modified_by}`; await loadSpecies(currentSlug); } searchInput.addEventListener("input", async (event) => { await loadSpeciesList(event.target.value); }); for (const button of archiveFilterGroup.querySelectorAll("[data-archive-filter]")) { button.addEventListener("click", () => { currentArchiveFilter = button.dataset.archiveFilter || "active"; syncArchiveFilterUi(); renderSpecies(currentItems); }); } authSaveButton.addEventListener("click", async () => { const token = authTokenInput.value.trim(); if (token) { window.localStorage.setItem("ecospecies_auth_token", token); } await loadSession(); await loadSpeciesList(searchInput.value); if (currentSlug) { await loadSpecies(currentSlug); } }); authClearButton.addEventListener("click", async () => { window.localStorage.removeItem("ecospecies_auth_token"); authTokenInput.value = ""; await loadSession(); await loadSpeciesList(searchInput.value); if (currentSlug) { await loadSpecies(currentSlug); } }); editorSaveButton.addEventListener("click", saveEditorialChanges); async function bootstrap() { await loadSession(); await Promise.all([loadSummary(), loadSpeciesList()]); } bootstrap().catch((error) => { speciesList.innerHTML = `Failed to load data: ${escapeHtml(String(error))}
`; });