Add launcher request board

This commit is contained in:
Andraxion 2026-06-26 22:55:50 -04:00
parent 6c6b1295a3
commit 4eef0d4850
3 changed files with 628 additions and 33 deletions

166
server.js
View file

@ -38,6 +38,7 @@ const dataRoot = path.resolve(__dirname, "data");
const catalogMetaPath = path.join(dataRoot, "catalog_meta.json");
const dialogueNodeMetaPath = path.join(dataRoot, "dialogue_node_meta.json");
const editorSettingsPath = path.join(dataRoot, "editor_settings.json");
const launcherRequestsPath = path.join(dataRoot, "launcher_requests.json");
const imagesCatalogPath = path.join(contentRoot, "images.json");
const legacyTilesCatalogPath = path.join(contentRoot, "tiles.json");
const legacySpritesCatalogPath = path.join(contentRoot, "sprites.json");
@ -218,6 +219,57 @@ function readEditorSettings() {
return normalizeEditorSettings(readJsonSafe(editorSettingsPath, createDefaultEditorSettings()));
}
function createLauncherRequestId() {
return `request_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
}
function normalizeLauncherRequestEntry(entry, index = 0) {
const source = entry && typeof entry === "object" && !Array.isArray(entry)
? entry
: null;
if (!source) {
return null;
}
const text = String(source.text || "").trim();
if (!text) {
return null;
}
const createdAt = String(source.createdAt || "").trim() || new Date().toISOString();
const updatedAt = String(source.updatedAt || "").trim() || createdAt;
const fallbackId = `request_${index + 1}`;
return {
id: String(source.id || fallbackId).trim() || fallbackId,
text,
done: source.done === true,
createdAt,
updatedAt,
};
}
function readLauncherRequestsPayload() {
const fallback = { schemaVersion: 1, requests: [] };
const payload = readJsonSafe(launcherRequestsPath, fallback);
const requests = Array.isArray(payload?.requests)
? payload.requests
.map((entry, index) => normalizeLauncherRequestEntry(entry, index))
.filter(Boolean)
: [];
return {
schemaVersion: typeof payload?.schemaVersion === "number" ? payload.schemaVersion : 1,
requests,
};
}
function writeLauncherRequestsPayload(payload) {
const requests = Array.isArray(payload?.requests) ? payload.requests : [];
writeJsonAtomic(launcherRequestsPath, {
schemaVersion: typeof payload?.schemaVersion === "number" ? payload.schemaVersion : 1,
requests: requests
.map((entry, index) => normalizeLauncherRequestEntry(entry, index))
.filter(Boolean),
});
}
function normalizeBackgroundTileId(value, idToSymbol = null) {
const normalizedId = String(value || "").trim();
if (!normalizedId) {
@ -2043,6 +2095,120 @@ app.get("/api/debug/recent-saves", (_req, res) => {
});
});
app.get("/api/launcher-requests", (_req, res) => {
try {
res.json(readLauncherRequestsPayload());
} catch (err) {
res.status(500).json({ error: `Failed to read launcher requests: ${String(err)}` });
}
});
app.post("/api/launcher-requests", (req, res) => {
try {
const text = String(req.body?.text || "").trim();
if (!text) {
res.status(400).json({ error: "Request text is required." });
return;
}
if (text.length > 1000) {
res.status(400).json({ error: "Request text must be 1000 characters or fewer." });
return;
}
const payload = readLauncherRequestsPayload();
const now = new Date().toISOString();
const requestEntry = normalizeLauncherRequestEntry({
id: createLauncherRequestId(),
text,
done: false,
createdAt: now,
updatedAt: now,
}, payload.requests.length);
const nextPayload = {
schemaVersion: payload.schemaVersion,
requests: [...payload.requests, requestEntry],
};
writeLauncherRequestsPayload(nextPayload);
recordSaveEvent({
type: "launcher-request-add",
requestId: requestEntry.id,
textPreview: requestEntry.text.slice(0, 80),
});
res.status(201).json({
ok: true,
request: requestEntry,
requests: nextPayload.requests,
});
} catch (err) {
res.status(500).json({ error: `Failed to save launcher request: ${String(err)}` });
}
});
app.patch("/api/launcher-requests/:requestId", (req, res) => {
const requestId = String(req.params.requestId || "").trim();
try {
const payload = readLauncherRequestsPayload();
const index = payload.requests.findIndex((entry) => entry.id === requestId);
if (index < 0) {
res.status(404).json({ error: "Request not found." });
return;
}
const existing = payload.requests[index];
const nextDone = req.body?.done === true;
const updated = normalizeLauncherRequestEntry({
...existing,
done: nextDone,
updatedAt: new Date().toISOString(),
}, index);
const nextRequests = [...payload.requests];
nextRequests[index] = updated;
writeLauncherRequestsPayload({
schemaVersion: payload.schemaVersion,
requests: nextRequests,
});
recordSaveEvent({
type: "launcher-request-update",
requestId,
done: updated.done,
});
res.json({
ok: true,
request: updated,
requests: nextRequests,
});
} catch (err) {
res.status(500).json({ error: `Failed to update launcher request: ${String(err)}` });
}
});
app.delete("/api/launcher-requests/:requestId", (req, res) => {
const requestId = String(req.params.requestId || "").trim();
try {
const payload = readLauncherRequestsPayload();
const existing = payload.requests.find((entry) => entry.id === requestId);
if (!existing) {
res.status(404).json({ error: "Request not found." });
return;
}
const nextRequests = payload.requests.filter((entry) => entry.id !== requestId);
writeLauncherRequestsPayload({
schemaVersion: payload.schemaVersion,
requests: nextRequests,
});
recordSaveEvent({
type: "launcher-request-delete",
requestId,
textPreview: existing.text.slice(0, 80),
});
res.json({
ok: true,
deletedRequestId: requestId,
requests: nextRequests,
});
} catch (err) {
res.status(500).json({ error: `Failed to delete launcher request: ${String(err)}` });
}
});
app.get("/api/world-default", (_req, res) => {
try {
const indexPayload = readWorldIndexPayload();