Worldshaper/src/launcher/useLauncherRequestBoard.ts

642 lines
24 KiB
TypeScript
Raw Normal View History

2026-06-27 04:36:26 -04:00
// @ts-nocheck
import { useEffect, useState } from "react";
import type {
AdminDetailTab,
BoardTab,
LauncherRequest,
LauncherRequestAnalysisItem,
} from "./types";
import {
createLauncherRequest,
deleteLauncherRequest,
loadLauncherRecentSaveEvents,
loadLauncherRequestMeta,
loadLauncherRequests,
promoteLauncherAdminRequest,
requeueLauncherAdminRequest,
saveLauncherAdminRequest,
triggerLauncherPendingQueue,
verifyLauncherAdminPassword,
} from "./requestApi";
import {
appendUniqueString,
cloneLauncherRequest,
hydrateLauncherRequestForUi,
isAdminAccessError,
isNeedsReviewRequest,
isQueuedPendingRequest,
normalizeStringList,
openAdminPanelWindow,
removeStringValue,
requestMatchesFilters,
} from "./utils";
export function useLauncherRequestBoard({ adminWindowMode }) {
const adminPanelOpen = adminWindowMode;
const [activeBoardTab, setActiveBoardTab] = useState<BoardTab>(adminWindowMode ? "requests" : "news");
const [requests, setRequests] = useState<LauncherRequest[]>([]);
const [requestsLoading, setRequestsLoading] = useState(true);
const [requestsError, setRequestsError] = useState("");
const [requestDraftOpen, setRequestDraftOpen] = useState(false);
const [requestDraft, setRequestDraft] = useState("");
const [requestSubmitting, setRequestSubmitting] = useState(false);
const [requestMutatingId, setRequestMutatingId] = useState("");
const [requestSearchText, setRequestSearchText] = useState("");
const [requestFilterMenuOpen, setRequestFilterMenuOpen] = useState(false);
const [requestStatusFilters, setRequestStatusFilters] = useState<string[]>([]);
const [requestTagFilters, setRequestTagFilters] = useState<string[]>([]);
const [allowedRequestTags, setAllowedRequestTags] = useState<string[]>([]);
const [expandedRequestIds, setExpandedRequestIds] = useState<string[]>([]);
const [adminAccessGranted, setAdminAccessGranted] = useState(false);
const [adminPassword, setAdminPassword] = useState("");
const [adminPasswordDraft, setAdminPasswordDraft] = useState("");
const [adminAuthSubmitting, setAdminAuthSubmitting] = useState(false);
const [adminPasswordError, setAdminPasswordError] = useState("");
const [selectedAdminRequestId, setSelectedAdminRequestId] = useState("");
const [selectedAdminAnalysisIndex, setSelectedAdminAnalysisIndex] = useState(0);
const [adminSearchText, setAdminSearchText] = useState("");
const [adminFilterMenuOpen, setAdminFilterMenuOpen] = useState(false);
const [adminStatusFilters, setAdminStatusFilters] = useState<string[]>([]);
const [adminTagFilters, setAdminTagFilters] = useState<string[]>([]);
const [adminEditorDraft, setAdminEditorDraft] = useState<LauncherRequest | null>(null);
const [adminDetailTab, setAdminDetailTab] = useState<AdminDetailTab>("routing");
const [adminSaving, setAdminSaving] = useState(false);
const [recentSaveEvents, setRecentSaveEvents] = useState([]);
const [logsLoading, setLogsLoading] = useState(false);
const [logsModalOpen, setLogsModalOpen] = useState(false);
const [logsError, setLogsError] = useState("");
const [queueTriggering, setQueueTriggering] = useState(false);
const [requeueingMode, setRequeueingMode] = useState<"" | "saved" | "draft">("");
const [adminNotice, setAdminNotice] = useState("");
async function loadRequests(options?: { silent?: boolean }): Promise<void> {
const silent = options?.silent === true;
if (!silent) {
setRequestsLoading(true);
}
try {
const payload = await loadLauncherRequests();
setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []);
setRequestsError("");
} catch (nextError: unknown) {
setRequestsError(String(nextError || "Failed to load requests."));
} finally {
if (!silent) {
setRequestsLoading(false);
}
}
}
async function loadRequestMeta(): Promise<void> {
try {
const payload = await loadLauncherRequestMeta();
setAllowedRequestTags(Array.isArray(payload.allowedTags) ? payload.allowedTags : []);
} catch {
setAllowedRequestTags([]);
}
}
async function loadRecentSaveEvents(): Promise<void> {
setLogsLoading(true);
try {
const payload = await loadLauncherRecentSaveEvents(adminPassword);
setRecentSaveEvents(Array.isArray(payload.saves) ? payload.saves : []);
setLogsError("");
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
}
setLogsError(String(nextError || "Failed to load admin logs."));
} finally {
setLogsLoading(false);
}
}
async function verifyAdminPassword(password: string): Promise<void> {
await verifyLauncherAdminPassword(password);
}
async function refreshAdminData(options?: { includeLogs?: boolean; silentRequests?: boolean }): Promise<void> {
await loadRequests({ silent: options?.silentRequests === true });
if (options?.includeLogs && adminAccessGranted && adminPassword) {
await loadRecentSaveEvents();
}
}
useEffect(() => {
void loadRequests();
void loadRequestMeta();
}, []);
useEffect(() => {
if (!adminPanelOpen || !adminAccessGranted || !adminPassword) {
return;
}
void loadRecentSaveEvents();
}, [adminPanelOpen, adminAccessGranted, adminPassword]);
useEffect(() => {
if (activeBoardTab !== "requests") {
return;
}
let cancelled = false;
const refreshBoard = async (): Promise<void> => {
if (!adminPanelOpen) {
try {
const payload = await loadLauncherRequests();
if (!cancelled) {
setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []);
}
} catch {
// Keep the current list visible during background refresh failures.
}
}
if (!adminPanelOpen || !adminAccessGranted || !adminPassword) {
return;
}
try {
const payload = await loadLauncherRecentSaveEvents(adminPassword);
if (!cancelled) {
setRecentSaveEvents(Array.isArray(payload.saves) ? payload.saves : []);
}
} catch {
// Avoid surfacing noisy polling failures in the admin panel.
}
};
const intervalId = window.setInterval(() => {
void refreshBoard();
}, 15000);
return () => {
cancelled = true;
window.clearInterval(intervalId);
};
}, [activeBoardTab, adminPanelOpen, adminAccessGranted, adminPassword]);
useEffect(() => {
if (!adminPanelOpen || !adminAccessGranted) {
return;
}
if (requests.length === 0) {
setSelectedAdminRequestId("");
setAdminEditorDraft(null);
return;
}
const selectedRequest = requests.find((entry) => entry.id === selectedAdminRequestId);
if (selectedRequest) {
if (!adminEditorDraft || adminEditorDraft.id !== selectedRequest.id) {
setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(selectedRequest)));
}
return;
}
setSelectedAdminRequestId(requests[0].id);
setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(requests[0])));
}, [adminPanelOpen, adminAccessGranted, requests, selectedAdminRequestId, adminEditorDraft]);
useEffect(() => {
setSelectedAdminAnalysisIndex(0);
}, [selectedAdminRequestId]);
useEffect(() => {
setAdminDetailTab("routing");
}, [selectedAdminRequestId]);
async function handleAddRequest(): Promise<void> {
const text = requestDraft.trim();
if (!text) {
setRequestsError("Write a request before saving it.");
return;
}
setRequestSubmitting(true);
try {
const payload = await createLauncherRequest(text);
setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []);
setRequestDraft("");
setRequestDraftOpen(false);
setRequestsError("");
setAdminNotice("Request saved. The VPS queue worker will pick it up if analysis autorun is enabled.");
if (adminPanelOpen && adminAccessGranted) {
void loadRecentSaveEvents();
}
window.setTimeout(() => {
void refreshAdminData({ includeLogs: adminPanelOpen && adminAccessGranted, silentRequests: true });
}, 3500);
} catch (nextError: unknown) {
setRequestsError(String(nextError || "Failed to save request."));
} finally {
setRequestSubmitting(false);
}
}
async function handleAdminPanelToggle(): Promise<void> {
setRequestDraftOpen(false);
setRequestsError("");
setLogsError("");
if (adminWindowMode) {
return;
}
if (!openAdminPanelWindow()) {
setAdminNotice("Allow popups to open the admin review window.");
}
}
async function handleAdminUnlock(): Promise<void> {
const submittedPassword = adminPasswordDraft.trim();
if (!submittedPassword) {
setAdminPasswordError("Enter the admin password to continue.");
return;
}
setAdminAuthSubmitting(true);
setAdminPasswordError("");
try {
await verifyAdminPassword(submittedPassword);
setAdminPassword(submittedPassword);
setAdminAccessGranted(true);
setAdminNotice("Admin access granted.");
await refreshAdminData({ includeLogs: true, silentRequests: true });
} catch (nextError: unknown) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError(String(nextError || "Failed to unlock the admin panel."));
} finally {
setAdminAuthSubmitting(false);
}
}
function handleSelectAdminRequest(requestId: string): void {
const nextRequest = requests.find((entry) => entry.id === requestId);
setSelectedAdminRequestId(requestId);
setSelectedAdminAnalysisIndex(0);
setAdminEditorDraft(nextRequest ? cloneLauncherRequest(hydrateLauncherRequestForUi(nextRequest)) : null);
setAdminNotice("");
setAdminPasswordError("");
}
function updateAdminDraft(updater: (current: LauncherRequest) => LauncherRequest): void {
setAdminEditorDraft((current) => (current ? updater(current) : current));
}
function updateAdminDraftItem(
itemIndex: number,
updater: (item: LauncherRequestAnalysisItem) => LauncherRequestAnalysisItem,
): void {
updateAdminDraft((current) => {
const next = cloneLauncherRequest(current);
if (!next.analysis) {
next.analysis = {
state: "needs_review",
items: [],
};
}
const items = Array.isArray(next.analysis.items) ? [...next.analysis.items] : [];
const existingItem = items[itemIndex] || {};
items[itemIndex] = updater({
...existingItem,
tags: Array.isArray(existingItem.tags) ? [...existingItem.tags] : [],
affectedSystems: Array.isArray(existingItem.affectedSystems) ? [...existingItem.affectedSystems] : [],
affectedFiles: Array.isArray(existingItem.affectedFiles) ? [...existingItem.affectedFiles] : [],
reviewOptions: Array.isArray(existingItem.reviewOptions) ? [...existingItem.reviewOptions] : [],
});
next.analysis.items = items;
next.analysis.itemCount = items.length;
next.analysis.updatedAt = new Date().toISOString();
return next;
});
}
async function handleSaveAdminRequest(): Promise<void> {
if (!adminEditorDraft) {
return;
}
setAdminSaving(true);
try {
const payload = await saveLauncherAdminRequest(adminPassword, adminEditorDraft);
const nextRequests = Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : requests;
setRequests(nextRequests);
const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || hydrateLauncherRequestForUi(payload.request || adminEditorDraft);
setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(refreshed)));
setAdminNotice(`Saved admin changes for "${adminEditorDraft.title}".`);
if (adminPanelOpen) {
void loadRecentSaveEvents();
}
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
setLogsError(String(nextError || "Failed to save admin changes."));
} finally {
setAdminSaving(false);
}
}
async function handleApproveAdminRequest(): Promise<void> {
if (!adminEditorDraft) {
return;
}
const nextDraft = cloneLauncherRequest(adminEditorDraft);
if (!nextDraft.analysis) {
setLogsError("This request has no analysis to approve yet.");
return;
}
const items = Array.isArray(nextDraft.analysis.items) ? nextDraft.analysis.items : [];
if (items.length === 0) {
setLogsError("This request does not have a structured analysis item to approve yet.");
return;
}
nextDraft.analysis.items = items.map((item) => ({
...item,
statusRecommendation: "active",
}));
nextDraft.analysis.state = "processed";
nextDraft.analysis.updatedAt = new Date().toISOString();
setAdminEditorDraft(nextDraft);
setAdminSaving(true);
try {
await saveLauncherAdminRequest(adminPassword, nextDraft);
const promotePayload = await promoteLauncherAdminRequest(adminPassword, nextDraft);
const nextRequests = Array.isArray(promotePayload.requests) ? promotePayload.requests.map(hydrateLauncherRequestForUi) : [];
setRequests(nextRequests);
const fallbackSelection = nextRequests[0] || null;
setSelectedAdminRequestId(fallbackSelection?.id || "");
setAdminEditorDraft(fallbackSelection ? cloneLauncherRequest(hydrateLauncherRequestForUi(fallbackSelection)) : null);
setAdminNotice(`Approved "${nextDraft.title}" and promoted its active request item${(nextDraft.analysis.items?.length || 0) === 1 ? "" : "s"}.`);
if (adminPanelOpen) {
void loadRecentSaveEvents();
}
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
setLogsError(String(nextError || "Failed to approve this request."));
} finally {
setAdminSaving(false);
}
}
async function handleRequeueAnalysis(mode: "saved" | "draft"): Promise<void> {
if (!adminEditorDraft) {
return;
}
setRequeueingMode(mode);
setLogsError("");
try {
const payload = await requeueLauncherAdminRequest(adminPassword, adminEditorDraft, mode);
const nextRequests = Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : requests;
setRequests(nextRequests);
const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || hydrateLauncherRequestForUi(payload.request || adminEditorDraft);
setAdminEditorDraft(cloneLauncherRequest(hydrateLauncherRequestForUi(refreshed)));
if (payload.launched) {
setAdminNotice(mode === "draft"
? "Edited draft resubmitted to the analyzer."
: "Saved request resubmitted to the analyzer.");
} else {
const reason = String(payload.reason || "no-op");
if (reason === "request-analysis-already-running") {
setAdminNotice("The queue worker is already running. This request will be picked up on the next pass.");
} else if (reason === "request-not-queued") {
setAdminNotice("That request is not currently eligible for review reruns.");
} else {
setAdminNotice(`Review rerun returned: ${reason}.`);
}
}
await refreshAdminData({ includeLogs: true, silentRequests: true });
window.setTimeout(() => {
void refreshAdminData({ includeLogs: true, silentRequests: true });
}, 4200);
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
setLogsError(String(nextError || "Failed to requeue this request for review."));
} finally {
setRequeueingMode("");
}
}
function handleToggleExpandedRequest(requestId: string): void {
setExpandedRequestIds((current) => (
current.includes(requestId)
? current.filter((entry) => entry !== requestId)
: [...current, requestId]
));
}
async function handleDeleteRequest(requestEntry: LauncherRequest): Promise<void> {
const confirmed = window.confirm(`Delete this request?\n\n${requestEntry.title}`);
if (!confirmed) {
return;
}
setRequestMutatingId(requestEntry.id);
try {
const payload = await deleteLauncherRequest(adminPassword, requestEntry.id);
setRequests(Array.isArray(payload.requests) ? payload.requests.map(hydrateLauncherRequestForUi) : []);
setRequestsError("");
setExpandedRequestIds((current) => current.filter((entry) => entry !== requestEntry.id));
setAdminNotice(`Deleted request "${requestEntry.title}".`);
if (adminPanelOpen) {
void loadRecentSaveEvents();
}
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
setRequestsError(String(nextError || "Failed to delete request."));
} finally {
setRequestMutatingId("");
}
}
async function handleProcessPendingQueue(): Promise<void> {
setQueueTriggering(true);
try {
const payload = await triggerLauncherPendingQueue(adminPassword);
if (payload.launched) {
setAdminNotice(`Queue worker launched for ${payload.queuedPendingCount ?? 0} pending request${payload.queuedPendingCount === 1 ? "" : "s"}.`);
} else {
const reason = String(payload.reason || "no-op");
if (reason === "no-pending-requests") {
setAdminNotice("No unprocessed pending requests are waiting in the queue.");
} else if (reason === "request-analysis-already-running") {
setAdminNotice("The request analysis worker is already running on the VPS.");
} else if (reason === "request-analysis-not-configured") {
setAdminNotice("Request analysis is not configured on the server.");
} else {
setAdminNotice(`Queue trigger returned: ${reason}.`);
}
}
await refreshAdminData({ includeLogs: true, silentRequests: true });
if (payload.launched) {
window.setTimeout(() => {
void refreshAdminData({ includeLogs: true, silentRequests: true });
}, 4200);
}
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
setLogsError(String(nextError || "Failed to trigger the queue worker."));
} finally {
setQueueTriggering(false);
}
}
const requestCount = requests.length;
const pendingRequestCount = requests.filter((entry) => entry.status === "pending").length;
const activeRequestCount = requests.filter((entry) => entry.status === "active").length;
const implementedRequestCount = requests.filter((entry) => entry.status === "implemented").length;
const queuedPendingRequestCount = requests.filter(isQueuedPendingRequest).length;
const needsReviewRequestCount = requests.filter(isNeedsReviewRequest).length;
const requestTags = (allowedRequestTags.length > 0
? allowedRequestTags
: Array.from(new Set(
requests
.flatMap((entry) => Array.isArray(entry.tags) ? entry.tags : [])
.map((entry) => String(entry || "").trim())
.filter(Boolean),
))).sort((a, b) => a.localeCompare(b));
const requestTagFilterOptions = requestTags
.map((tag) => ({
tag,
count: requests.filter((entry) => entry.tags.includes(tag)).length,
}))
.filter((entry) => entry.count > 0);
const requestStatusFilterOptions = [
{ id: "pending", label: "Pending", count: pendingRequestCount },
{ id: "queued", label: "Queued", count: queuedPendingRequestCount },
{ id: "review", label: "Needs Review", count: needsReviewRequestCount },
{ id: "active", label: "Active", count: activeRequestCount },
{ id: "implemented", label: "Implemented", count: implementedRequestCount },
].filter((entry) => entry.count > 0);
const filteredRequests = requests.filter((entry) => requestMatchesFilters(
entry,
requestSearchText,
requestStatusFilters,
requestTagFilters,
));
const adminFilteredRequests = requests.filter((entry) => requestMatchesFilters(
entry,
adminSearchText,
adminStatusFilters,
adminTagFilters,
));
const selectedAnalysisItem = adminEditorDraft?.analysis?.items?.[selectedAdminAnalysisIndex] || null;
const standardizedTagOptions = normalizeStringList([
...allowedRequestTags,
...requestTags,
...requests.flatMap((entry) => [
...entry.tags,
...(Array.isArray(entry.analysis?.routing?.suggestedTags) ? entry.analysis.routing.suggestedTags : []),
...(Array.isArray(entry.analysis?.items) ? entry.analysis.items.flatMap((item) => Array.isArray(item.tags) ? item.tags : []) : []),
]),
...(adminEditorDraft?.tags || []),
...(Array.isArray(adminEditorDraft?.analysis?.routing?.suggestedTags) ? adminEditorDraft.analysis.routing.suggestedTags : []),
...(Array.isArray(selectedAnalysisItem?.tags) ? selectedAnalysisItem.tags : []),
]);
const categoryOptions = normalizeStringList([
...standardizedTagOptions,
...requests.map((entry) => entry.category),
...requests.flatMap((entry) => Array.isArray(entry.analysis?.items) ? entry.analysis.items.map((item) => String(item.primaryCategory || "")) : []),
String(adminEditorDraft?.category || ""),
String(selectedAnalysisItem?.primaryCategory || ""),
]);
const boardTitle = adminWindowMode ? "Worldshaper Admin" : "Worldshaper Board";
const boardHint = adminWindowMode
? `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active, ${implementedRequestCount} implemented`
: (activeBoardTab === "news"
? "Latest announcements"
: `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active, ${implementedRequestCount} implemented`);
return {
adminPanelOpen,
activeBoardTab,
setActiveBoardTab,
requests,
requestsLoading,
requestsError,
requestDraftOpen,
setRequestDraftOpen,
requestDraft,
setRequestDraft,
requestSubmitting,
requestMutatingId,
requestSearchText,
setRequestSearchText,
requestFilterMenuOpen,
setRequestFilterMenuOpen,
requestStatusFilters,
setRequestStatusFilters,
requestTagFilters,
setRequestTagFilters,
allowedRequestTags,
expandedRequestIds,
adminAccessGranted,
adminPassword,
adminPasswordDraft,
setAdminPasswordDraft,
adminAuthSubmitting,
adminPasswordError,
selectedAdminRequestId,
selectedAdminAnalysisIndex,
setSelectedAdminAnalysisIndex,
adminSearchText,
setAdminSearchText,
adminFilterMenuOpen,
setAdminFilterMenuOpen,
adminStatusFilters,
setAdminStatusFilters,
adminTagFilters,
setAdminTagFilters,
adminEditorDraft,
adminDetailTab,
setAdminDetailTab,
adminSaving,
recentSaveEvents,
logsLoading,
logsModalOpen,
setLogsModalOpen,
logsError,
queueTriggering,
requeueingMode,
adminNotice,
refreshAdminData,
loadRecentSaveEvents,
handleAddRequest,
handleAdminPanelToggle,
handleAdminUnlock,
handleSelectAdminRequest,
updateAdminDraft,
updateAdminDraftItem,
handleSaveAdminRequest,
handleApproveAdminRequest,
handleRequeueAnalysis,
handleToggleExpandedRequest,
handleDeleteRequest,
handleProcessPendingQueue,
requestCount,
pendingRequestCount,
activeRequestCount,
implementedRequestCount,
queuedPendingRequestCount,
needsReviewRequestCount,
requestTags,
requestTagFilterOptions,
requestStatusFilterOptions,
filteredRequests,
adminFilteredRequests,
selectedAnalysisItem,
standardizedTagOptions,
categoryOptions,
boardTitle,
boardHint,
};
}