241 lines
6.8 KiB
JavaScript
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`);
|
|
});
|