EcoSpecies-Atlas/tests/ui/server.js

241 lines
6.8 KiB
JavaScript

const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = Number(process.env.PORT || 4173);
const WEB_ROOT = path.resolve(__dirname, "../../apps/web");
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
const baseSpecies = [
{
slug: "active-shad",
source_file: "Active Shad.txt",
title: "Active Shad (Alosa activa)",
common_name: "Active Shad",
scientific_name: "Alosa activa",
flelmr_code: "1001",
summary: "An active editor-visible species.",
section_count: 2,
publication_status: "published",
is_archived: false,
editor_notes: "",
last_modified_by: "system-import",
diagnostics: [],
sections: [
{ id: 1, position: 1, heading: "HEADER", content: "Header content" },
{ id: 2, position: 2, heading: "HABITAT", content: "Habitat content" },
],
audit: [],
},
{
slug: "archived-shad",
source_file: "Archived Shad.txt",
title: "Archived Shad (Alosa archiva)",
common_name: "Archived Shad",
scientific_name: "Alosa archiva",
flelmr_code: "1002",
summary: "An archived species.",
section_count: 1,
publication_status: "published",
is_archived: true,
editor_notes: "Archived from prior import.",
last_modified_by: "system-import",
diagnostics: [],
sections: [
{ id: 3, position: 1, heading: "HEADER", content: "Archived header" },
],
audit: [
{
id: 1,
changed_by: "system-import",
changed_at: "2026-03-26T00:00:00+00:00",
action: "import_archive",
details: { is_archived: { from: false, to: true } },
},
],
},
];
let speciesRecords = clone(baseSpecies);
function resetState() {
speciesRecords = clone(baseSpecies);
}
function getSession(req) {
const auth = req.headers.authorization || "";
if (auth === "Bearer editor-token") {
return {
authenticated: true,
auth_configured: true,
user: { username: "editor", role: "editor" },
};
}
return {
authenticated: false,
auth_configured: true,
user: null,
};
}
function sendJson(res, status, payload) {
const body = JSON.stringify(payload);
res.writeHead(status, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": Buffer.byteLength(body),
});
res.end(body);
}
function sendFile(res, filePath) {
fs.readFile(filePath, (error, content) => {
if (error) {
sendJson(res, 404, { error: "Not found" });
return;
}
const ext = path.extname(filePath);
const type =
ext === ".html"
? "text/html; charset=utf-8"
: ext === ".js"
? "application/javascript; charset=utf-8"
: "text/css; charset=utf-8";
res.writeHead(200, { "Content-Type": type });
res.end(content);
});
}
function parseBody(req) {
return new Promise((resolve, reject) => {
let raw = "";
req.on("data", (chunk) => {
raw += chunk;
});
req.on("end", () => {
try {
resolve(raw ? JSON.parse(raw) : {});
} catch (error) {
reject(error);
}
});
req.on("error", reject);
});
}
function getEditorList() {
return speciesRecords.map((item) => ({
slug: item.slug,
title: item.title,
common_name: item.common_name,
publication_status: item.publication_status,
is_archived: item.is_archived,
last_modified_by: item.last_modified_by,
diagnostics: item.diagnostics,
}));
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
if (pathname === "/__reset" && req.method === "POST") {
resetState();
sendJson(res, 200, { status: "ok" });
return;
}
if (pathname === "/api/auth/session" && req.method === "GET") {
sendJson(res, 200, getSession(req));
return;
}
if (pathname === "/api/insights/summary" && req.method === "GET") {
sendJson(res, 200, { species_count: 1, section_count: 3, diagnostic_counts: {} });
return;
}
if (pathname === "/api/editor/species" && req.method === "GET") {
sendJson(res, 200, { items: getEditorList(), count: speciesRecords.length });
return;
}
if (pathname.startsWith("/api/editor/species/") && pathname.endsWith("/audit") && req.method === "GET") {
const slug = pathname.slice("/api/editor/species/".length, -"/audit".length).replace(/\/$/, "");
const item = speciesRecords.find((record) => record.slug === slug);
if (!item) {
sendJson(res, 404, { error: "Not found" });
return;
}
sendJson(res, 200, { items: item.audit, count: item.audit.length });
return;
}
if (pathname.startsWith("/api/editor/species/") && pathname.endsWith("/editorial") && req.method === "POST") {
const slug = pathname.slice("/api/editor/species/".length, -"/editorial".length).replace(/\/$/, "");
const item = speciesRecords.find((record) => record.slug === slug);
if (!item) {
sendJson(res, 404, { error: "Not found" });
return;
}
const payload = await parseBody(req);
const beforeArchived = item.is_archived;
item.publication_status = payload.publication_status || item.publication_status;
item.summary = payload.summary ?? item.summary;
item.editor_notes = payload.editor_notes ?? item.editor_notes;
item.is_archived = Boolean(payload.is_archived);
item.last_modified_by = "editor";
item.audit.unshift({
id: item.audit.length + 1,
changed_by: "editor",
changed_at: "2026-03-26T00:01:00+00:00",
action: "editorial_update",
details: {
is_archived: { from: beforeArchived, to: item.is_archived },
},
});
sendJson(res, 200, {
status: "ok",
slug: item.slug,
summary: item.summary,
publication_status: item.publication_status,
editor_notes: item.editor_notes,
is_archived: item.is_archived,
last_modified_by: item.last_modified_by,
changed_fields: {
is_archived: { from: beforeArchived, to: item.is_archived },
},
});
return;
}
if (pathname.startsWith("/api/editor/species/") && req.method === "GET") {
const slug = pathname.slice("/api/editor/species/".length).replace(/\/$/, "");
const item = speciesRecords.find((record) => record.slug === slug);
if (!item) {
sendJson(res, 404, { error: "Not found" });
return;
}
sendJson(res, 200, item);
return;
}
if (pathname === "/" || pathname === "/index.html") {
sendFile(res, path.join(WEB_ROOT, "index.html"));
return;
}
if (pathname === "/app.js" || pathname === "/styles.css") {
sendFile(res, path.join(WEB_ROOT, pathname.slice(1)));
return;
}
sendJson(res, 404, { error: "Not found" });
});
server.listen(PORT, "127.0.0.1", () => {
process.stdout.write(`UI test server listening on ${PORT}\n`);
});